Skip to content

Commit a667592

Browse files
cexbrayatdylhunn
authored andcommitted
feat(core): allow to throw on unknown properties in tests (#45853)
Allows to provide a TestBed option to throw on unknown properties in templates: ```ts getTestBed().initTestEnvironment( BrowserDynamicTestingModule, platformBrowserDynamicTesting(), { errorOnUnknownProperties: true } ); ``` The default value of `errorOnUnknownProperties` is `false`, so this is not a breaking change. PR Close #45853
1 parent 7005da9 commit a667592

9 files changed

Lines changed: 216 additions & 15 deletions

File tree

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,7 @@ export class TestComponentRenderer {
216216
// @public (undocumented)
217217
export interface TestEnvironmentOptions {
218218
errorOnUnknownElements?: boolean;
219+
errorOnUnknownProperties?: boolean;
219220
teardown?: ModuleTeardownOptions;
220221
}
221222

@@ -227,6 +228,7 @@ export type TestModuleMetadata = {
227228
schemas?: Array<SchemaMetadata | any[]>;
228229
teardown?: ModuleTeardownOptions;
229230
errorOnUnknownElements?: boolean;
231+
errorOnUnknownProperties?: boolean;
230232
};
231233

232234
// @public

packages/core/src/core_render3_private_export.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,9 @@ export {
210210
ɵɵtextInterpolateV,
211211
ɵɵviewQuery,
212212
ɵgetUnknownElementStrictMode,
213-
ɵsetUnknownElementStrictMode
213+
ɵsetUnknownElementStrictMode,
214+
ɵgetUnknownPropertyStrictMode,
215+
ɵsetUnknownPropertyStrictMode
214216
} from './render3/index';
215217
export {
216218
LContext as ɵLContext,

packages/core/src/render3/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,9 @@ export {
130130
ɵɵtextInterpolate8,
131131
ɵɵtextInterpolateV,
132132
ɵgetUnknownElementStrictMode,
133-
ɵsetUnknownElementStrictMode
133+
ɵsetUnknownElementStrictMode,
134+
ɵgetUnknownPropertyStrictMode,
135+
ɵsetUnknownPropertyStrictMode
134136
} from './instructions/all';
135137
export {ɵɵi18n, ɵɵi18nApply, ɵɵi18nAttributes, ɵɵi18nEnd, ɵɵi18nExp,ɵɵi18nPostprocess, ɵɵi18nStart} from './instructions/i18n';
136138
export {RenderFlags} from './interfaces/definition';

packages/core/src/render3/instructions/all.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,3 +50,4 @@ export * from './style_map_interpolation';
5050
export * from './style_prop_interpolation';
5151
export * from './host_property';
5252
export * from './i18n';
53+
export {ɵgetUnknownPropertyStrictMode, ɵsetUnknownPropertyStrictMode} from './shared';

packages/core/src/render3/instructions/shared.ts

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,23 @@ import {getComponentLViewByIndex, getNativeByIndex, getNativeByTNode, isCreation
4848
import {selectIndexInternal} from './advance';
4949
import {attachLContainerDebug, attachLViewDebug, cloneToLViewFromTViewBlueprint, cloneToTViewData, LCleanup, LViewBlueprint, MatchesArray, TCleanup, TNodeDebug, TNodeInitialInputs, TNodeLocalNames, TViewComponents, TViewConstructor} from './lview_debug';
5050

51+
let shouldThrowErrorOnUnknownProperty = false;
5152

53+
/**
54+
* Sets a strict mode for JIT-compiled components to throw an error on unknown properties,
55+
* instead of just logging the error.
56+
* (for AOT-compiled ones this check happens at build time).
57+
*/
58+
export function ɵsetUnknownPropertyStrictMode(shouldThrow: boolean) {
59+
shouldThrowErrorOnUnknownProperty = shouldThrow;
60+
}
61+
62+
/**
63+
* Gets the current value of the strict mode.
64+
*/
65+
export function ɵgetUnknownPropertyStrictMode() {
66+
return shouldThrowErrorOnUnknownProperty;
67+
}
5268

5369
/**
5470
* A permanent marker promise which signifies that the current CD tree is
@@ -1013,7 +1029,7 @@ export function elementPropertyInternal<T>(
10131029
validateAgainstEventProperties(propName);
10141030
if (!validateProperty(element, tNode.value, propName, tView.schemas)) {
10151031
// Return here since we only log warnings for unknown properties.
1016-
logUnknownPropertyError(propName, tNode.value);
1032+
handleUnknownPropertyError(propName, tNode.value);
10171033
return;
10181034
}
10191035
ngDevMode.rendererSetProperty++;
@@ -1032,7 +1048,7 @@ export function elementPropertyInternal<T>(
10321048
// If the node is a container and the property didn't
10331049
// match any of the inputs or schemas we should throw.
10341050
if (ngDevMode && !matchingSchemas(tView.schemas, tNode.value)) {
1035-
logUnknownPropertyError(propName, tNode.value);
1051+
handleUnknownPropertyError(propName, tNode.value);
10361052
}
10371053
}
10381054
}
@@ -1145,13 +1161,17 @@ export function matchingSchemas(schemas: SchemaMetadata[]|null, tagName: string|
11451161
}
11461162

11471163
/**
1148-
* Logs an error that a property is not supported on an element.
1164+
* Logs or throws an error that a property is not supported on an element.
11491165
* @param propName Name of the invalid property.
11501166
* @param tagName Name of the node on which we encountered the property.
11511167
*/
1152-
function logUnknownPropertyError(propName: string, tagName: string): void {
1168+
function handleUnknownPropertyError(propName: string, tagName: string): void {
11531169
const message = `Can't bind to '${propName}' since it isn't a known property of '${tagName}'.`;
1154-
console.error(formatRuntimeError(RuntimeErrorCode.UNKNOWN_BINDING, message));
1170+
if (shouldThrowErrorOnUnknownProperty) {
1171+
throw new RuntimeError(RuntimeErrorCode.UNKNOWN_BINDING, message);
1172+
} else {
1173+
console.error(formatRuntimeError(RuntimeErrorCode.UNKNOWN_BINDING, message));
1174+
}
11551175
}
11561176

11571177
/**

packages/core/test/acceptance/ng_module_spec.ts

Lines changed: 91 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,7 @@ describe('NgModule', () => {
227227
});
228228

229229
describe('schemas', () => {
230-
it('should throw on unknown props if NO_ERRORS_SCHEMA is absent', () => {
230+
it('should log an error on unknown props if NO_ERRORS_SCHEMA is absent', () => {
231231
@Component({
232232
selector: 'my-comp',
233233
template: `
@@ -255,6 +255,36 @@ describe('NgModule', () => {
255255
expect(spy.calls.mostRecent().args[0])
256256
.toMatch(/Can't bind to 'unknown-prop' since it isn't a known property of 'div'/);
257257
});
258+
it('should throw an error with errorOnUnknownProperties on unknown props if NO_ERRORS_SCHEMA is absent',
259+
() => {
260+
@Component({
261+
selector: 'my-comp',
262+
template: `
263+
<ng-container *ngIf="condition">
264+
<div [unknown-prop]="true"></div>
265+
</ng-container>
266+
`,
267+
})
268+
class MyComp {
269+
condition = true;
270+
}
271+
272+
@NgModule({
273+
imports: [CommonModule],
274+
declarations: [MyComp],
275+
})
276+
class MyModule {
277+
}
278+
279+
TestBed.configureTestingModule({imports: [MyModule], errorOnUnknownProperties: true});
280+
281+
expect(() => {
282+
const fixture = TestBed.createComponent(MyComp);
283+
fixture.detectChanges();
284+
})
285+
.toThrowError(
286+
/NG0303: Can't bind to 'unknown-prop' since it isn't a known property of 'div'/g);
287+
});
258288

259289
it('should not throw on unknown props if NO_ERRORS_SCHEMA is present', () => {
260290
@Component({
@@ -285,6 +315,36 @@ describe('NgModule', () => {
285315
}).not.toThrow();
286316
});
287317

318+
it('should not throw on unknown props with errorOnUnknownProperties if NO_ERRORS_SCHEMA is present',
319+
() => {
320+
@Component({
321+
selector: 'my-comp',
322+
template: `
323+
<ng-container *ngIf="condition">
324+
<div [unknown-prop]="true"></div>
325+
</ng-container>
326+
`,
327+
})
328+
class MyComp {
329+
condition = true;
330+
}
331+
332+
@NgModule({
333+
imports: [CommonModule],
334+
schemas: [NO_ERRORS_SCHEMA],
335+
declarations: [MyComp],
336+
})
337+
class MyModule {
338+
}
339+
340+
TestBed.configureTestingModule({imports: [MyModule], errorOnUnknownProperties: true});
341+
342+
expect(() => {
343+
const fixture = TestBed.createComponent(MyComp);
344+
fixture.detectChanges();
345+
}).not.toThrow();
346+
});
347+
288348
it('should log an error about unknown element without CUSTOM_ELEMENTS_SCHEMA for element with dash in tag name',
289349
() => {
290350
@Component({template: `<custom-el></custom-el>`})
@@ -384,6 +444,21 @@ describe('NgModule', () => {
384444
.toMatch(/Can't bind to 'unknownProp' since it isn't a known property of 'ng-content'/);
385445
});
386446

447+
it('should throw an error on unknown property bindings on ng-content when errorOnUnknownProperties is enabled',
448+
() => {
449+
@Component({template: `<ng-content *unknownProp="123"></ng-content>`})
450+
class App {
451+
}
452+
453+
TestBed.configureTestingModule({declarations: [App], errorOnUnknownProperties: true});
454+
expect(() => {
455+
const fixture = TestBed.createComponent(App);
456+
fixture.detectChanges();
457+
})
458+
.toThrowError(
459+
/NG0303: Can't bind to 'unknownProp' since it isn't a known property of 'ng-content'/g);
460+
});
461+
387462
it('should report unknown property bindings on ng-container', () => {
388463
@Component({template: `<ng-container [unknown-prop]="123"></ng-container>`})
389464
class App {
@@ -399,6 +474,21 @@ describe('NgModule', () => {
399474
/Can't bind to 'unknown-prop' since it isn't a known property of 'ng-container'/);
400475
});
401476

477+
it('should throw error on unknown property bindings on ng-container when errorOnUnknownProperties is enabled',
478+
() => {
479+
@Component({template: `<ng-container [unknown-prop]="123"></ng-container>`})
480+
class App {
481+
}
482+
483+
TestBed.configureTestingModule({declarations: [App], errorOnUnknownProperties: true});
484+
expect(() => {
485+
const fixture = TestBed.createComponent(App);
486+
fixture.detectChanges();
487+
})
488+
.toThrowError(
489+
/NG0303: Can't bind to 'unknown-prop' since it isn't a known property of 'ng-container'/g);
490+
});
491+
402492
describe('AOT-compiled components', () => {
403493
function createComponent(
404494
template: (rf: any) => void, vars: number, consts?: (number|string)[][]) {

packages/core/test/test_bed_spec.ts

Lines changed: 32 additions & 1 deletion
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, THROW_ON_UNKNOWN_ELEMENTS_DEFAULT} from '../testing/src/test_bed_common';
16+
import {TEARDOWN_TESTING_MODULE_ON_DESTROY_DEFAULT, THROW_ON_UNKNOWN_ELEMENTS_DEFAULT, THROW_ON_UNKNOWN_PROPERTIES_DEFAULT} from '../testing/src/test_bed_common';
1717

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

@@ -1993,3 +1993,34 @@ describe('TestBed module `errorOnUnknownElements`', () => {
19931993
expect(TestBed.shouldThrowErrorOnUnknownElements()).toBe(false);
19941994
});
19951995
});
1996+
1997+
describe('TestBed module `errorOnUnknownProperties`', () => {
1998+
// Cast the `TestBed` to the internal data type since we're testing private APIs.
1999+
let TestBed: TestBedRender3;
2000+
2001+
beforeEach(() => {
2002+
TestBed = getTestBed() as unknown as TestBedRender3;
2003+
TestBed.resetTestingModule();
2004+
});
2005+
2006+
it('should not throw based on the default behavior', () => {
2007+
expect(TestBed.shouldThrowErrorOnUnknownProperties()).toBe(THROW_ON_UNKNOWN_PROPERTIES_DEFAULT);
2008+
});
2009+
2010+
it('should not throw if the option is omitted', () => {
2011+
TestBed.configureTestingModule({});
2012+
expect(TestBed.shouldThrowErrorOnUnknownProperties()).toBe(false);
2013+
});
2014+
2015+
it('should be able to configure the option', () => {
2016+
TestBed.configureTestingModule({errorOnUnknownProperties: true});
2017+
expect(TestBed.shouldThrowErrorOnUnknownProperties()).toBe(true);
2018+
});
2019+
2020+
it('should reset the option back to the default when TestBed is reset', () => {
2021+
TestBed.configureTestingModule({errorOnUnknownProperties: true});
2022+
expect(TestBed.shouldThrowErrorOnUnknownProperties()).toBe(true);
2023+
TestBed.resetTestingModule();
2024+
expect(TestBed.shouldThrowErrorOnUnknownProperties()).toBe(false);
2025+
});
2026+
});

packages/core/testing/src/r3_test_bed.ts

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,13 @@ import {
2525
Type,
2626
ɵflushModuleScopingQueueAsMuchAsPossible as flushModuleScopingQueueAsMuchAsPossible,
2727
ɵgetUnknownElementStrictMode as getUnknownElementStrictMode,
28+
ɵgetUnknownPropertyStrictMode as getUnknownPropertyStrictMode,
2829
ɵRender3ComponentFactory as ComponentFactory,
2930
ɵRender3NgModuleRef as NgModuleRef,
3031
ɵresetCompiledComponents as resetCompiledComponents,
3132
ɵsetAllowDuplicateNgModuleIdsForTest as setAllowDuplicateNgModuleIdsForTest,
3233
ɵsetUnknownElementStrictMode as setUnknownElementStrictMode,
34+
ɵsetUnknownPropertyStrictMode as setUnknownPropertyStrictMode,
3335
ɵstringify as stringify,
3436
} from '@angular/core';
3537

@@ -39,7 +41,7 @@ import {ComponentFixture} from './component_fixture';
3941
import {MetadataOverride} from './metadata_override';
4042
import {R3TestBedCompiler} from './r3_test_bed_compiler';
4143
import {TestBed} from './test_bed';
42-
import {ComponentFixtureAutoDetect, ComponentFixtureNoNgZone, ModuleTeardownOptions, TEARDOWN_TESTING_MODULE_ON_DESTROY_DEFAULT, TestBedStatic, TestComponentRenderer, TestEnvironmentOptions, TestModuleMetadata, THROW_ON_UNKNOWN_ELEMENTS_DEFAULT} from './test_bed_common';
44+
import {ComponentFixtureAutoDetect, ComponentFixtureNoNgZone, ModuleTeardownOptions, TEARDOWN_TESTING_MODULE_ON_DESTROY_DEFAULT, TestBedStatic, TestComponentRenderer, TestEnvironmentOptions, TestModuleMetadata, THROW_ON_UNKNOWN_ELEMENTS_DEFAULT, THROW_ON_UNKNOWN_PROPERTIES_DEFAULT} from './test_bed_common';
4345

4446
let _nextRootElementId = 0;
4547

@@ -67,6 +69,12 @@ export class TestBedRender3 implements TestBed {
6769
*/
6870
private static _environmentErrorOnUnknownElementsOption: boolean|undefined;
6971

72+
/**
73+
* "Error on unknown properties" option that has been configured at the environment level.
74+
* Used as a fallback if no instance-level option has been provided.
75+
*/
76+
private static _environmentErrorOnUnknownPropertiesOption: boolean|undefined;
77+
7078
/**
7179
* Teardown options that have been configured at the `TestBed` instance level.
7280
* These options take precedence over the environment-level ones.
@@ -79,12 +87,24 @@ export class TestBedRender3 implements TestBed {
7987
*/
8088
private _instanceErrorOnUnknownElementsOption: boolean|undefined;
8189

90+
/**
91+
* "Error on unknown properties" option that has been configured at the `TestBed` instance level.
92+
* This option takes precedence over the environment-level one.
93+
*/
94+
private _instanceErrorOnUnknownPropertiesOption: boolean|undefined;
95+
8296
/**
8397
* Stores the previous "Error on unknown elements" option value,
8498
* allowing to restore it in the reset testing module logic.
8599
*/
86100
private _previousErrorOnUnknownElementsOption: boolean|undefined;
87101

102+
/**
103+
* Stores the previous "Error on unknown properties" option value,
104+
* allowing to restore it in the reset testing module logic.
105+
*/
106+
private _previousErrorOnUnknownPropertiesOption: boolean|undefined;
107+
88108
/**
89109
* Initialize the environment for testing with a compiler factory, a PlatformRef, and an
90110
* angular module. These are common to every test in the suite.
@@ -259,6 +279,8 @@ export class TestBedRender3 implements TestBed {
259279

260280
TestBedRender3._environmentErrorOnUnknownElementsOption = options?.errorOnUnknownElements;
261281

282+
TestBedRender3._environmentErrorOnUnknownPropertiesOption = options?.errorOnUnknownProperties;
283+
262284
this.platform = platform;
263285
this.ngModule = ngModule;
264286
this._compiler = new R3TestBedCompiler(this.platform, this.ngModule);
@@ -294,6 +316,9 @@ export class TestBedRender3 implements TestBed {
294316
// Restore the previous value of the "error on unknown elements" option
295317
setUnknownElementStrictMode(
296318
this._previousErrorOnUnknownElementsOption ?? THROW_ON_UNKNOWN_ELEMENTS_DEFAULT);
319+
// Restore the previous value of the "error on unknown properties" option
320+
setUnknownPropertyStrictMode(
321+
this._previousErrorOnUnknownPropertiesOption ?? THROW_ON_UNKNOWN_PROPERTIES_DEFAULT);
297322

298323
// We have to chain a couple of try/finally blocks, because each step can
299324
// throw errors and we don't want it to interrupt the next step and we also
@@ -309,6 +334,7 @@ export class TestBedRender3 implements TestBed {
309334
this._testModuleRef = null;
310335
this._instanceTeardownOptions = undefined;
311336
this._instanceErrorOnUnknownElementsOption = undefined;
337+
this._instanceErrorOnUnknownPropertiesOption = undefined;
312338
}
313339
}
314340
}
@@ -336,10 +362,13 @@ export class TestBedRender3 implements TestBed {
336362
// This ensures that we don't carry them between tests.
337363
this._instanceTeardownOptions = moduleDef.teardown;
338364
this._instanceErrorOnUnknownElementsOption = moduleDef.errorOnUnknownElements;
365+
this._instanceErrorOnUnknownPropertiesOption = moduleDef.errorOnUnknownProperties;
339366
// Store the current value of the strict mode option,
340367
// so we can restore it later
341368
this._previousErrorOnUnknownElementsOption = getUnknownElementStrictMode();
342369
setUnknownElementStrictMode(this.shouldThrowErrorOnUnknownElements());
370+
this._previousErrorOnUnknownPropertiesOption = getUnknownPropertyStrictMode();
371+
setUnknownPropertyStrictMode(this.shouldThrowErrorOnUnknownProperties());
343372
this.compiler.configureTestingModule(moduleDef);
344373
}
345374

@@ -532,6 +561,13 @@ export class TestBedRender3 implements TestBed {
532561
THROW_ON_UNKNOWN_ELEMENTS_DEFAULT;
533562
}
534563

564+
shouldThrowErrorOnUnknownProperties(): boolean {
565+
// Check if a configuration has been provided to throw when an unknown property is found
566+
return this._instanceErrorOnUnknownPropertiesOption ??
567+
TestBedRender3._environmentErrorOnUnknownPropertiesOption ??
568+
THROW_ON_UNKNOWN_PROPERTIES_DEFAULT;
569+
}
570+
535571
shouldTearDownTestingModule(): boolean {
536572
return this._instanceTeardownOptions?.destroyAfterEach ??
537573
TestBedRender3._environmentTeardownOptions?.destroyAfterEach ??

0 commit comments

Comments
 (0)