Skip to content

Commit e702caf

Browse files
cexbrayatdylhunn
authored andcommittedMay 2, 2022
feat(core): allow to throw on unknown elements in tests (#45479)
Allows to provide a TestBed option to throw on unknown elements in templates: ```ts getTestBed().initTestEnvironment( BrowserDynamicTestingModule, platformBrowserDynamicTesting(), { errorOnUnknownElements: true } ); ``` The default value of `errorOnUnknownElements` is `false`, so this is not a breaking change. PR Close #45479
1 parent 6a3ca0e commit e702caf

File tree

8 files changed

+231
-8
lines changed

8 files changed

+231
-8
lines changed
 

‎goldens/public-api/core/testing/index.md

+2
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,7 @@ export class TestComponentRenderer {
215215

216216
// @public (undocumented)
217217
export interface TestEnvironmentOptions {
218+
errorOnUnknownElements?: boolean;
218219
teardown?: ModuleTeardownOptions;
219220
}
220221

@@ -225,6 +226,7 @@ export type TestModuleMetadata = {
225226
imports?: any[];
226227
schemas?: Array<SchemaMetadata | any[]>;
227228
teardown?: ModuleTeardownOptions;
229+
errorOnUnknownElements?: boolean;
228230
};
229231

230232
// @public

‎packages/core/src/core_render3_private_export.ts

+2
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,8 @@ export {
209209
ɵɵtextInterpolate8,
210210
ɵɵtextInterpolateV,
211211
ɵɵviewQuery,
212+
ɵgetUnknownElementStrictMode,
213+
ɵsetUnknownElementStrictMode
212214
} from './render3/index';
213215
export {
214216
LContext as ɵLContext,

‎packages/core/src/render3/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,8 @@ export {
129129
ɵɵtextInterpolate7,
130130
ɵɵtextInterpolate8,
131131
ɵɵtextInterpolateV,
132+
ɵgetUnknownElementStrictMode,
133+
ɵsetUnknownElementStrictMode
132134
} from './instructions/all';
133135
export {ɵɵi18n, ɵɵi18nApply, ɵɵi18nAttributes, ɵɵi18nEnd, ɵɵi18nExp,ɵɵi18nPostprocess, ɵɵi18nStart} from './instructions/i18n';
134136
export {RenderFlags} from './interfaces/definition';

‎packages/core/src/render3/instructions/element.ts

+22-2
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {formatRuntimeError, RuntimeErrorCode} from '../../errors';
9+
import {formatRuntimeError, RuntimeError, RuntimeErrorCode} from '../../errors';
1010
import {SchemaMetadata} from '../../metadata/schema';
1111
import {assertDefined, assertEqual, assertIndexInRange} from '../../util/assert';
1212
import {assertFirstCreatePass, assertHasParent} from '../assert';
@@ -26,7 +26,23 @@ import {getConstant} from '../util/view_utils';
2626
import {setDirectiveInputsWhichShadowsStyling} from './property';
2727
import {createDirectivesInstances, executeContentQueries, getOrCreateTNode, matchingSchemas, resolveDirectives, saveResolvedLocalsInData} from './shared';
2828

29+
let shouldThrowErrorOnUnknownElement = false;
2930

31+
/**
32+
* Sets a strict mode for JIT-compiled components to throw an error on unknown elements,
33+
* instead of just logging the error.
34+
* (for AOT-compiled ones this check happens at build time).
35+
*/
36+
export function ɵsetUnknownElementStrictMode(shouldThrow: boolean) {
37+
shouldThrowErrorOnUnknownElement = shouldThrow;
38+
}
39+
40+
/**
41+
* Gets the current value of the strict mode.
42+
*/
43+
export function ɵgetUnknownElementStrictMode() {
44+
return shouldThrowErrorOnUnknownElement;
45+
}
3046

3147
function elementStartFirstCreatePass(
3248
index: number, tView: TView, lView: LView, native: RElement, name: string,
@@ -241,7 +257,11 @@ function validateElementIsKnown(
241257
message +=
242258
`2. To allow any element add 'NO_ERRORS_SCHEMA' to the '@NgModule.schemas' of this component.`;
243259
}
244-
console.error(formatRuntimeError(RuntimeErrorCode.UNKNOWN_ELEMENT, message));
260+
if (shouldThrowErrorOnUnknownElement) {
261+
throw new RuntimeError(RuntimeErrorCode.UNKNOWN_ELEMENT, message);
262+
} else {
263+
console.error(formatRuntimeError(RuntimeErrorCode.UNKNOWN_ELEMENT, message));
264+
}
245265
}
246266
}
247267
}

‎packages/core/test/acceptance/ng_module_spec.ts

+111
Original file line numberDiff line numberDiff line change
@@ -331,6 +331,19 @@ describe('NgModule', () => {
331331
expect(spy).not.toHaveBeenCalled();
332332
});
333333

334+
it('should throw an error about unknown element without CUSTOM_ELEMENTS_SCHEMA for element with dash in tag name',
335+
() => {
336+
@Component({template: `<custom-el></custom-el>`})
337+
class MyComp {
338+
}
339+
340+
TestBed.configureTestingModule({declarations: [MyComp], errorOnUnknownElements: true});
341+
expect(() => {
342+
const fixture = TestBed.createComponent(MyComp);
343+
fixture.detectChanges();
344+
}).toThrowError(/NG0304: 'custom-el' is not a known element/g);
345+
});
346+
334347
it('should log an error about unknown element without CUSTOM_ELEMENTS_SCHEMA for element without dash in tag name',
335348
() => {
336349
@Component({template: `<custom></custom>`})
@@ -344,6 +357,19 @@ describe('NgModule', () => {
344357
expect(spy.calls.mostRecent().args[0]).toMatch(/'custom' is not a known element/);
345358
});
346359

360+
it('should throw an error about unknown element without CUSTOM_ELEMENTS_SCHEMA for element without dash in tag name',
361+
() => {
362+
@Component({template: `<custom></custom>`})
363+
class MyComp {
364+
}
365+
366+
TestBed.configureTestingModule({declarations: [MyComp], errorOnUnknownElements: true});
367+
expect(() => {
368+
const fixture = TestBed.createComponent(MyComp);
369+
fixture.detectChanges();
370+
}).toThrowError(/NG0304: 'custom' is not a known element/g);
371+
});
372+
347373
it('should report unknown property bindings on ng-content', () => {
348374
@Component({template: `<ng-content *unknownProp="123"></ng-content>`})
349375
class App {
@@ -505,6 +531,25 @@ describe('NgModule', () => {
505531
expect(spy).not.toHaveBeenCalled();
506532
});
507533

534+
it('should not throw an error about unknown elements with CUSTOM_ELEMENTS_SCHEMA', () => {
535+
@Component({template: `<custom-el></custom-el>`})
536+
class MyComp {
537+
}
538+
539+
const spy = spyOn(console, 'error');
540+
TestBed.configureTestingModule({
541+
declarations: [MyComp],
542+
schemas: [CUSTOM_ELEMENTS_SCHEMA],
543+
errorOnUnknownElements: true
544+
});
545+
546+
const fixture = TestBed.createComponent(MyComp);
547+
fixture.detectChanges();
548+
// We do not expect any errors being thrown or logged in a console,
549+
// since the `CUSTOM_ELEMENTS_SCHEMA` is applied.
550+
expect(spy).not.toHaveBeenCalled();
551+
});
552+
508553
it('should not log an error about unknown elements with NO_ERRORS_SCHEMA', () => {
509554
@Component({template: `<custom-el></custom-el>`})
510555
class MyComp {
@@ -521,6 +566,22 @@ describe('NgModule', () => {
521566
expect(spy).not.toHaveBeenCalled();
522567
});
523568

569+
it('should not throw an error about unknown elements with NO_ERRORS_SCHEMA', () => {
570+
@Component({template: `<custom-el></custom-el>`})
571+
class MyComp {
572+
}
573+
574+
const spy = spyOn(console, 'error');
575+
TestBed.configureTestingModule(
576+
{declarations: [MyComp], schemas: [NO_ERRORS_SCHEMA], errorOnUnknownElements: true});
577+
578+
const fixture = TestBed.createComponent(MyComp);
579+
fixture.detectChanges();
580+
// We do not expect any errors being thrown or logged in a console,
581+
// since the `NO_ERRORS_SCHEMA` is applied.
582+
expect(spy).not.toHaveBeenCalled();
583+
});
584+
524585
it('should not log an error about unknown elements if element matches a directive', () => {
525586
@Component({
526587
selector: 'custom-el',
@@ -541,6 +602,29 @@ describe('NgModule', () => {
541602
expect(spy).not.toHaveBeenCalled();
542603
});
543604

605+
it('should not throw an error about unknown elements if element matches a directive', () => {
606+
@Component({
607+
selector: 'custom-el',
608+
template: '',
609+
})
610+
class CustomEl {
611+
}
612+
613+
@Component({template: `<custom-el></custom-el>`})
614+
class MyComp {
615+
}
616+
617+
const spy = spyOn(console, 'error');
618+
TestBed.configureTestingModule(
619+
{declarations: [MyComp, CustomEl], errorOnUnknownElements: true});
620+
621+
const fixture = TestBed.createComponent(MyComp);
622+
fixture.detectChanges();
623+
// We do not expect any errors being thrown or logged in a console,
624+
// since the element matches a directive.
625+
expect(spy).not.toHaveBeenCalled();
626+
});
627+
544628
it('should not log an error for HTML elements inside an SVG foreignObject', () => {
545629
@Component({
546630
template: `
@@ -565,6 +649,33 @@ describe('NgModule', () => {
565649
fixture.detectChanges();
566650
expect(spy).not.toHaveBeenCalled();
567651
});
652+
653+
it('should not throw an error for HTML elements inside an SVG foreignObject', () => {
654+
@Component({
655+
template: `
656+
<svg>
657+
<svg:foreignObject>
658+
<xhtml:div>Hello</xhtml:div>
659+
</svg:foreignObject>
660+
</svg>
661+
`,
662+
})
663+
class MyComp {
664+
}
665+
666+
@NgModule({declarations: [MyComp]})
667+
class MyModule {
668+
}
669+
670+
const spy = spyOn(console, 'error');
671+
TestBed.configureTestingModule({imports: [MyModule], errorOnUnknownElements: true});
672+
673+
const fixture = TestBed.createComponent(MyComp);
674+
fixture.detectChanges();
675+
// We do not expect any errors being thrown or logged in a console,
676+
// since the element is inside an SVG foreignObject.
677+
expect(spy).not.toHaveBeenCalled();
678+
});
568679
});
569680

570681
describe('createNgModuleRef function', () => {

‎packages/core/test/test_bed_spec.ts

+32-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import {expect} from '@angular/platform-browser/testing/src/matchers';
1313

1414
import {getNgModuleById} from '../public_api';
1515
import {TestBedRender3} from '../testing/src/r3_test_bed';
16-
import {TEARDOWN_TESTING_MODULE_ON_DESTROY_DEFAULT} from '../testing/src/test_bed_common';
16+
import {TEARDOWN_TESTING_MODULE_ON_DESTROY_DEFAULT, THROW_ON_UNKNOWN_ELEMENTS_DEFAULT} from '../testing/src/test_bed_common';
1717

1818
const NAME = new InjectionToken<string>('name');
1919

@@ -1714,3 +1714,34 @@ describe('TestBed module teardown', () => {
17141714
expect(TestBed.shouldRethrowTeardownErrors()).toBe(false);
17151715
});
17161716
});
1717+
1718+
describe('TestBed module `errorOnUnknownElements`', () => {
1719+
// Cast the `TestBed` to the internal data type since we're testing private APIs.
1720+
let TestBed: TestBedRender3;
1721+
1722+
beforeEach(() => {
1723+
TestBed = getTestBed() as unknown as TestBedRender3;
1724+
TestBed.resetTestingModule();
1725+
});
1726+
1727+
it('should not throw based on the default behavior', () => {
1728+
expect(TestBed.shouldThrowErrorOnUnknownElements()).toBe(THROW_ON_UNKNOWN_ELEMENTS_DEFAULT);
1729+
});
1730+
1731+
it('should not throw if the option is omitted', () => {
1732+
TestBed.configureTestingModule({});
1733+
expect(TestBed.shouldThrowErrorOnUnknownElements()).toBe(false);
1734+
});
1735+
1736+
it('should be able to configure the option', () => {
1737+
TestBed.configureTestingModule({errorOnUnknownElements: true});
1738+
expect(TestBed.shouldThrowErrorOnUnknownElements()).toBe(true);
1739+
});
1740+
1741+
it('should reset the option back to the default when TestBed is reset', () => {
1742+
TestBed.configureTestingModule({errorOnUnknownElements: true});
1743+
expect(TestBed.shouldThrowErrorOnUnknownElements()).toBe(true);
1744+
TestBed.resetTestingModule();
1745+
expect(TestBed.shouldThrowErrorOnUnknownElements()).toBe(false);
1746+
});
1747+
});

‎packages/core/testing/src/r3_test_bed.ts

+43-5
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,12 @@ import {
2424
ProviderToken,
2525
Type,
2626
ɵflushModuleScopingQueueAsMuchAsPossible as flushModuleScopingQueueAsMuchAsPossible,
27+
ɵgetUnknownElementStrictMode as getUnknownElementStrictMode,
2728
ɵRender3ComponentFactory as ComponentFactory,
2829
ɵRender3NgModuleRef as NgModuleRef,
2930
ɵresetCompiledComponents as resetCompiledComponents,
3031
ɵsetAllowDuplicateNgModuleIdsForTest as setAllowDuplicateNgModuleIdsForTest,
32+
ɵsetUnknownElementStrictMode as setUnknownElementStrictMode,
3133
ɵstringify as stringify,
3234
} from '@angular/core';
3335

@@ -37,7 +39,7 @@ import {ComponentFixture} from './component_fixture';
3739
import {MetadataOverride} from './metadata_override';
3840
import {R3TestBedCompiler} from './r3_test_bed_compiler';
3941
import {TestBed} from './test_bed';
40-
import {ComponentFixtureAutoDetect, ComponentFixtureNoNgZone, ModuleTeardownOptions, TEARDOWN_TESTING_MODULE_ON_DESTROY_DEFAULT, TestBedStatic, TestComponentRenderer, TestEnvironmentOptions, TestModuleMetadata} from './test_bed_common';
42+
import {ComponentFixtureAutoDetect, ComponentFixtureNoNgZone, ModuleTeardownOptions, TEARDOWN_TESTING_MODULE_ON_DESTROY_DEFAULT, TestBedStatic, TestComponentRenderer, TestEnvironmentOptions, TestModuleMetadata, THROW_ON_UNKNOWN_ELEMENTS_DEFAULT} from './test_bed_common';
4143

4244
let _nextRootElementId = 0;
4345

@@ -59,12 +61,30 @@ export class TestBedRender3 implements TestBed {
5961
*/
6062
private static _environmentTeardownOptions: ModuleTeardownOptions|undefined;
6163

64+
/**
65+
* "Error on unknown elements" option that has been configured at the environment level.
66+
* Used as a fallback if no instance-level option has been provided.
67+
*/
68+
private static _environmentErrorOnUnknownElementsOption: boolean|undefined;
69+
6270
/**
6371
* Teardown options that have been configured at the `TestBed` instance level.
64-
* These options take precedence over the environemnt-level ones.
72+
* These options take precedence over the environment-level ones.
6573
*/
6674
private _instanceTeardownOptions: ModuleTeardownOptions|undefined;
6775

76+
/**
77+
* "Error on unknown elements" option that has been configured at the `TestBed` instance level.
78+
* This option takes precedence over the environment-level one.
79+
*/
80+
private _instanceErrorOnUnknownElementsOption: boolean|undefined;
81+
82+
/**
83+
* Stores the previous "Error on unknown elements" option value,
84+
* allowing to restore it in the reset testing module logic.
85+
*/
86+
private _previousErrorOnUnknownElementsOption: boolean|undefined;
87+
6888
/**
6989
* Initialize the environment for testing with a compiler factory, a PlatformRef, and an
7090
* angular module. These are common to every test in the suite.
@@ -237,6 +257,8 @@ export class TestBedRender3 implements TestBed {
237257

238258
TestBedRender3._environmentTeardownOptions = options?.teardown;
239259

260+
TestBedRender3._environmentErrorOnUnknownElementsOption = options?.errorOnUnknownElements;
261+
240262
this.platform = platform;
241263
this.ngModule = ngModule;
242264
this._compiler = new R3TestBedCompiler(this.platform, this.ngModule);
@@ -269,6 +291,9 @@ export class TestBedRender3 implements TestBed {
269291
this.compiler.restoreOriginalState();
270292
}
271293
this._compiler = new R3TestBedCompiler(this.platform, this.ngModule);
294+
// Restore the previous value of the "error on unknown elements" option
295+
setUnknownElementStrictMode(
296+
this._previousErrorOnUnknownElementsOption ?? THROW_ON_UNKNOWN_ELEMENTS_DEFAULT);
272297

273298
// We have to chain a couple of try/finally blocks, because each step can
274299
// throw errors and we don't want it to interrupt the next step and we also
@@ -283,6 +308,7 @@ export class TestBedRender3 implements TestBed {
283308
} finally {
284309
this._testModuleRef = null;
285310
this._instanceTeardownOptions = undefined;
311+
this._instanceErrorOnUnknownElementsOption = undefined;
286312
}
287313
}
288314
}
@@ -306,9 +332,14 @@ export class TestBedRender3 implements TestBed {
306332
// description for additional info.
307333
this.checkGlobalCompilationFinished();
308334

309-
// Always re-assign the teardown options, even if they're undefined.
310-
// This ensures that we don't carry the options between tests.
335+
// Always re-assign the options, even if they're undefined.
336+
// This ensures that we don't carry them between tests.
311337
this._instanceTeardownOptions = moduleDef.teardown;
338+
this._instanceErrorOnUnknownElementsOption = moduleDef.errorOnUnknownElements;
339+
// Store the current value of the strict mode option,
340+
// so we can restore it later
341+
this._previousErrorOnUnknownElementsOption = getUnknownElementStrictMode();
342+
setUnknownElementStrictMode(this.shouldThrowErrorOnUnknownElements());
312343
this.compiler.configureTestingModule(moduleDef);
313344
}
314345

@@ -481,7 +512,7 @@ export class TestBedRender3 implements TestBed {
481512
}
482513
}
483514

484-
shouldRethrowTeardownErrors() {
515+
shouldRethrowTeardownErrors(): boolean {
485516
const instanceOptions = this._instanceTeardownOptions;
486517
const environmentOptions = TestBedRender3._environmentTeardownOptions;
487518

@@ -495,6 +526,13 @@ export class TestBedRender3 implements TestBed {
495526
this.shouldTearDownTestingModule();
496527
}
497528

529+
shouldThrowErrorOnUnknownElements(): boolean {
530+
// Check if a configuration has been provided to throw when an unknown element is found
531+
return this._instanceErrorOnUnknownElementsOption ??
532+
TestBedRender3._environmentErrorOnUnknownElementsOption ??
533+
THROW_ON_UNKNOWN_ELEMENTS_DEFAULT;
534+
}
535+
498536
shouldTearDownTestingModule(): boolean {
499537
return this._instanceTeardownOptions?.destroyAfterEach ??
500538
TestBedRender3._environmentTeardownOptions?.destroyAfterEach ??

0 commit comments

Comments
 (0)
Please sign in to comment.