From 82c436d2513c9c9ae1193135fca3df9fbded5fce Mon Sep 17 00:00:00 2001 From: Andrey Vorobev <738482+vorobey@users.noreply.github.com> Date: Thu, 16 Jul 2026 16:25:48 +0300 Subject: [PATCH] AngularWrapper: fix nested legacy warnings (#34338) --- .../src/core/deprecated-config-warning.ts | 4 +- .../core/deprecated-config-warning.spec.ts | 54 +++++++++++++++++++ 2 files changed, 56 insertions(+), 2 deletions(-) create mode 100644 packages/devextreme-angular/tests/src/core/deprecated-config-warning.spec.ts diff --git a/packages/devextreme-angular/src/core/deprecated-config-warning.ts b/packages/devextreme-angular/src/core/deprecated-config-warning.ts index 5b82790eb259..1e41fd3ca966 100644 --- a/packages/devextreme-angular/src/core/deprecated-config-warning.ts +++ b/packages/devextreme-angular/src/core/deprecated-config-warning.ts @@ -5,7 +5,7 @@ import { logWarning } from './warning-helper'; const warnedUsages = new Set(); -const NESTED_CLASS_NAME_REGEXP = /^(Dx[io][A-Z]\w+)Component$/; +const NESTED_CLASS_NAME_REGEXP = /^_*(Dx[io][A-Z]\w+)Component$/; type DeprecatedConfigEntry = Record; @@ -32,7 +32,7 @@ function getHostMapping(host: INestedOptionContainer | undefined): DeprecatedCon while (current && !visited.has(current)) { visited.add(current); - const ctorName = current.constructor?.name; + const ctorName = current.constructor?.name?.replace(/^_+/, ''); if (ctorName && Object.prototype.hasOwnProperty.call(DEPRECATED_CONFIG_COMPONENTS, ctorName)) { return DEPRECATED_CONFIG_COMPONENTS[ctorName] as DeprecatedConfigEntry; } diff --git a/packages/devextreme-angular/tests/src/core/deprecated-config-warning.spec.ts b/packages/devextreme-angular/tests/src/core/deprecated-config-warning.spec.ts new file mode 100644 index 000000000000..c79397fc4a49 --- /dev/null +++ b/packages/devextreme-angular/tests/src/core/deprecated-config-warning.spec.ts @@ -0,0 +1,54 @@ +import { NestedOptionHost } from 'devextreme-angular'; + +// The warning is reached through NestedOptionHost.setNestedOption(), so these +// tests drive the real integration point rather than the internal helper. +// +// esbuild renames a class that self-references in its decorator metadata to +// `_ClassName`, so at runtime `constructor.name` is `_DxiItemComponent` / +// `_DxAccordionComponent`. We fake that here to lock in the underscore-tolerant +// lookup in getLegacySelector (regex) and getHostMapping (map key normalization). + +function fakeHost(ctorName: string): any { + return { constructor: { name: ctorName } }; +} + +function fakeNestedOption(ctorName: string): any { + return { + constructor: { name: ctorName }, + // setNestedOption calls this before warning; a no-op is enough. + setHost() {}, + }; +} + +describe('deprecated nested config warning', () => { + let warnSpy: jasmine.Spy; + + beforeEach(() => { + warnSpy = spyOn(console, 'warn'); + }); + + it('resolves the legacy mapping and warns exactly once when esbuild prefixes class names with "_"', () => { + const host = new NestedOptionHost(); + host.setHost(fakeHost('_DxAccordionComponent')); + + // Register the same legacy usage twice: the cache must collapse it to one warning. + host.setNestedOption(fakeNestedOption('_DxiItemComponent')); + host.setNestedOption(fakeNestedOption('_DxiItemComponent')); + + expect(warnSpy).toHaveBeenCalledTimes(1); + + const message = warnSpy.calls.mostRecent().args[0] as string; + expect(message).toContain('W3001'); + expect(message).toContain('legacy dxi-item'); + expect(message).toContain('dxi-accordion-item'); + }); + + it('does not warn when the new named configuration component is used', () => { + const host = new NestedOptionHost(); + host.setHost(fakeHost('_DxAccordionComponent')); + + host.setNestedOption(fakeNestedOption('_DxiAccordionItemComponent')); + + expect(warnSpy).not.toHaveBeenCalled(); + }); +});