Skip to content

Commit

Permalink
feat(core): allow to throw on unknown properties in tests (#45853)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
cexbrayat authored and dylhunn committed May 3, 2022
1 parent 7005da9 commit a667592
Show file tree
Hide file tree
Showing 9 changed files with 216 additions and 15 deletions.
2 changes: 2 additions & 0 deletions goldens/public-api/core/testing/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,7 @@ export class TestComponentRenderer {
// @public (undocumented)
export interface TestEnvironmentOptions {
errorOnUnknownElements?: boolean;
errorOnUnknownProperties?: boolean;
teardown?: ModuleTeardownOptions;
}

Expand All @@ -227,6 +228,7 @@ export type TestModuleMetadata = {
schemas?: Array<SchemaMetadata | any[]>;
teardown?: ModuleTeardownOptions;
errorOnUnknownElements?: boolean;
errorOnUnknownProperties?: boolean;
};

// @public
Expand Down
4 changes: 3 additions & 1 deletion packages/core/src/core_render3_private_export.ts
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,9 @@ export {
ɵɵtextInterpolateV,
ɵɵviewQuery,
ɵgetUnknownElementStrictMode,
ɵsetUnknownElementStrictMode
ɵsetUnknownElementStrictMode,
ɵgetUnknownPropertyStrictMode,
ɵsetUnknownPropertyStrictMode
} from './render3/index';
export {
LContext as ɵLContext,
Expand Down
4 changes: 3 additions & 1 deletion packages/core/src/render3/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,9 @@ export {
ɵɵtextInterpolate8,
ɵɵtextInterpolateV,
ɵgetUnknownElementStrictMode,
ɵsetUnknownElementStrictMode
ɵsetUnknownElementStrictMode,
ɵgetUnknownPropertyStrictMode,
ɵsetUnknownPropertyStrictMode
} from './instructions/all';
export {ɵɵi18n, ɵɵi18nApply, ɵɵi18nAttributes, ɵɵi18nEnd, ɵɵi18nExp,ɵɵi18nPostprocess, ɵɵi18nStart} from './instructions/i18n';
export {RenderFlags} from './interfaces/definition';
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/render3/instructions/all.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,4 @@ export * from './style_map_interpolation';
export * from './style_prop_interpolation';
export * from './host_property';
export * from './i18n';
export {ɵgetUnknownPropertyStrictMode, ɵsetUnknownPropertyStrictMode} from './shared';
30 changes: 25 additions & 5 deletions packages/core/src/render3/instructions/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,23 @@ import {getComponentLViewByIndex, getNativeByIndex, getNativeByTNode, isCreation
import {selectIndexInternal} from './advance';
import {attachLContainerDebug, attachLViewDebug, cloneToLViewFromTViewBlueprint, cloneToTViewData, LCleanup, LViewBlueprint, MatchesArray, TCleanup, TNodeDebug, TNodeInitialInputs, TNodeLocalNames, TViewComponents, TViewConstructor} from './lview_debug';

let shouldThrowErrorOnUnknownProperty = false;

/**
* Sets a strict mode for JIT-compiled components to throw an error on unknown properties,
* instead of just logging the error.
* (for AOT-compiled ones this check happens at build time).
*/
export function ɵsetUnknownPropertyStrictMode(shouldThrow: boolean) {
shouldThrowErrorOnUnknownProperty = shouldThrow;
}

/**
* Gets the current value of the strict mode.
*/
export function ɵgetUnknownPropertyStrictMode() {
return shouldThrowErrorOnUnknownProperty;
}

/**
* A permanent marker promise which signifies that the current CD tree is
Expand Down Expand Up @@ -1013,7 +1029,7 @@ export function elementPropertyInternal<T>(
validateAgainstEventProperties(propName);
if (!validateProperty(element, tNode.value, propName, tView.schemas)) {
// Return here since we only log warnings for unknown properties.
logUnknownPropertyError(propName, tNode.value);
handleUnknownPropertyError(propName, tNode.value);
return;
}
ngDevMode.rendererSetProperty++;
Expand All @@ -1032,7 +1048,7 @@ export function elementPropertyInternal<T>(
// If the node is a container and the property didn't
// match any of the inputs or schemas we should throw.
if (ngDevMode && !matchingSchemas(tView.schemas, tNode.value)) {
logUnknownPropertyError(propName, tNode.value);
handleUnknownPropertyError(propName, tNode.value);
}
}
}
Expand Down Expand Up @@ -1145,13 +1161,17 @@ export function matchingSchemas(schemas: SchemaMetadata[]|null, tagName: string|
}

/**
* Logs an error that a property is not supported on an element.
* Logs or throws an error that a property is not supported on an element.
* @param propName Name of the invalid property.
* @param tagName Name of the node on which we encountered the property.
*/
function logUnknownPropertyError(propName: string, tagName: string): void {
function handleUnknownPropertyError(propName: string, tagName: string): void {
const message = `Can't bind to '${propName}' since it isn't a known property of '${tagName}'.`;
console.error(formatRuntimeError(RuntimeErrorCode.UNKNOWN_BINDING, message));
if (shouldThrowErrorOnUnknownProperty) {
throw new RuntimeError(RuntimeErrorCode.UNKNOWN_BINDING, message);
} else {
console.error(formatRuntimeError(RuntimeErrorCode.UNKNOWN_BINDING, message));
}
}

/**
Expand Down
92 changes: 91 additions & 1 deletion packages/core/test/acceptance/ng_module_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@ describe('NgModule', () => {
});

describe('schemas', () => {
it('should throw on unknown props if NO_ERRORS_SCHEMA is absent', () => {
it('should log an error on unknown props if NO_ERRORS_SCHEMA is absent', () => {
@Component({
selector: 'my-comp',
template: `
Expand Down Expand Up @@ -255,6 +255,36 @@ describe('NgModule', () => {
expect(spy.calls.mostRecent().args[0])
.toMatch(/Can't bind to 'unknown-prop' since it isn't a known property of 'div'/);
});
it('should throw an error with errorOnUnknownProperties on unknown props if NO_ERRORS_SCHEMA is absent',
() => {
@Component({
selector: 'my-comp',
template: `
<ng-container *ngIf="condition">
<div [unknown-prop]="true"></div>
</ng-container>
`,
})
class MyComp {
condition = true;
}

@NgModule({
imports: [CommonModule],
declarations: [MyComp],
})
class MyModule {
}

TestBed.configureTestingModule({imports: [MyModule], errorOnUnknownProperties: true});

expect(() => {
const fixture = TestBed.createComponent(MyComp);
fixture.detectChanges();
})
.toThrowError(
/NG0303: Can't bind to 'unknown-prop' since it isn't a known property of 'div'/g);
});

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

it('should not throw on unknown props with errorOnUnknownProperties if NO_ERRORS_SCHEMA is present',
() => {
@Component({
selector: 'my-comp',
template: `
<ng-container *ngIf="condition">
<div [unknown-prop]="true"></div>
</ng-container>
`,
})
class MyComp {
condition = true;
}

@NgModule({
imports: [CommonModule],
schemas: [NO_ERRORS_SCHEMA],
declarations: [MyComp],
})
class MyModule {
}

TestBed.configureTestingModule({imports: [MyModule], errorOnUnknownProperties: true});

expect(() => {
const fixture = TestBed.createComponent(MyComp);
fixture.detectChanges();
}).not.toThrow();
});

it('should log an error about unknown element without CUSTOM_ELEMENTS_SCHEMA for element with dash in tag name',
() => {
@Component({template: `<custom-el></custom-el>`})
Expand Down Expand Up @@ -384,6 +444,21 @@ describe('NgModule', () => {
.toMatch(/Can't bind to 'unknownProp' since it isn't a known property of 'ng-content'/);
});

it('should throw an error on unknown property bindings on ng-content when errorOnUnknownProperties is enabled',
() => {
@Component({template: `<ng-content *unknownProp="123"></ng-content>`})
class App {
}

TestBed.configureTestingModule({declarations: [App], errorOnUnknownProperties: true});
expect(() => {
const fixture = TestBed.createComponent(App);
fixture.detectChanges();
})
.toThrowError(
/NG0303: Can't bind to 'unknownProp' since it isn't a known property of 'ng-content'/g);
});

it('should report unknown property bindings on ng-container', () => {
@Component({template: `<ng-container [unknown-prop]="123"></ng-container>`})
class App {
Expand All @@ -399,6 +474,21 @@ describe('NgModule', () => {
/Can't bind to 'unknown-prop' since it isn't a known property of 'ng-container'/);
});

it('should throw error on unknown property bindings on ng-container when errorOnUnknownProperties is enabled',
() => {
@Component({template: `<ng-container [unknown-prop]="123"></ng-container>`})
class App {
}

TestBed.configureTestingModule({declarations: [App], errorOnUnknownProperties: true});
expect(() => {
const fixture = TestBed.createComponent(App);
fixture.detectChanges();
})
.toThrowError(
/NG0303: Can't bind to 'unknown-prop' since it isn't a known property of 'ng-container'/g);
});

describe('AOT-compiled components', () => {
function createComponent(
template: (rf: any) => void, vars: number, consts?: (number|string)[][]) {
Expand Down
33 changes: 32 additions & 1 deletion packages/core/test/test_bed_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {expect} from '@angular/platform-browser/testing/src/matchers';

import {getNgModuleById} from '../public_api';
import {TestBedRender3} from '../testing/src/r3_test_bed';
import {TEARDOWN_TESTING_MODULE_ON_DESTROY_DEFAULT, THROW_ON_UNKNOWN_ELEMENTS_DEFAULT} from '../testing/src/test_bed_common';
import {TEARDOWN_TESTING_MODULE_ON_DESTROY_DEFAULT, THROW_ON_UNKNOWN_ELEMENTS_DEFAULT, THROW_ON_UNKNOWN_PROPERTIES_DEFAULT} from '../testing/src/test_bed_common';

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

Expand Down Expand Up @@ -1993,3 +1993,34 @@ describe('TestBed module `errorOnUnknownElements`', () => {
expect(TestBed.shouldThrowErrorOnUnknownElements()).toBe(false);
});
});

describe('TestBed module `errorOnUnknownProperties`', () => {
// Cast the `TestBed` to the internal data type since we're testing private APIs.
let TestBed: TestBedRender3;

beforeEach(() => {
TestBed = getTestBed() as unknown as TestBedRender3;
TestBed.resetTestingModule();
});

it('should not throw based on the default behavior', () => {
expect(TestBed.shouldThrowErrorOnUnknownProperties()).toBe(THROW_ON_UNKNOWN_PROPERTIES_DEFAULT);
});

it('should not throw if the option is omitted', () => {
TestBed.configureTestingModule({});
expect(TestBed.shouldThrowErrorOnUnknownProperties()).toBe(false);
});

it('should be able to configure the option', () => {
TestBed.configureTestingModule({errorOnUnknownProperties: true});
expect(TestBed.shouldThrowErrorOnUnknownProperties()).toBe(true);
});

it('should reset the option back to the default when TestBed is reset', () => {
TestBed.configureTestingModule({errorOnUnknownProperties: true});
expect(TestBed.shouldThrowErrorOnUnknownProperties()).toBe(true);
TestBed.resetTestingModule();
expect(TestBed.shouldThrowErrorOnUnknownProperties()).toBe(false);
});
});
38 changes: 37 additions & 1 deletion packages/core/testing/src/r3_test_bed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,13 @@ import {
Type,
ɵflushModuleScopingQueueAsMuchAsPossible as flushModuleScopingQueueAsMuchAsPossible,
ɵgetUnknownElementStrictMode as getUnknownElementStrictMode,
ɵgetUnknownPropertyStrictMode as getUnknownPropertyStrictMode,
ɵRender3ComponentFactory as ComponentFactory,
ɵRender3NgModuleRef as NgModuleRef,
ɵresetCompiledComponents as resetCompiledComponents,
ɵsetAllowDuplicateNgModuleIdsForTest as setAllowDuplicateNgModuleIdsForTest,
ɵsetUnknownElementStrictMode as setUnknownElementStrictMode,
ɵsetUnknownPropertyStrictMode as setUnknownPropertyStrictMode,
ɵstringify as stringify,
} from '@angular/core';

Expand All @@ -39,7 +41,7 @@ import {ComponentFixture} from './component_fixture';
import {MetadataOverride} from './metadata_override';
import {R3TestBedCompiler} from './r3_test_bed_compiler';
import {TestBed} from './test_bed';
import {ComponentFixtureAutoDetect, ComponentFixtureNoNgZone, ModuleTeardownOptions, TEARDOWN_TESTING_MODULE_ON_DESTROY_DEFAULT, TestBedStatic, TestComponentRenderer, TestEnvironmentOptions, TestModuleMetadata, THROW_ON_UNKNOWN_ELEMENTS_DEFAULT} from './test_bed_common';
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';

let _nextRootElementId = 0;

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

/**
* "Error on unknown properties" option that has been configured at the environment level.
* Used as a fallback if no instance-level option has been provided.
*/
private static _environmentErrorOnUnknownPropertiesOption: boolean|undefined;

/**
* Teardown options that have been configured at the `TestBed` instance level.
* These options take precedence over the environment-level ones.
Expand All @@ -79,12 +87,24 @@ export class TestBedRender3 implements TestBed {
*/
private _instanceErrorOnUnknownElementsOption: boolean|undefined;

/**
* "Error on unknown properties" option that has been configured at the `TestBed` instance level.
* This option takes precedence over the environment-level one.
*/
private _instanceErrorOnUnknownPropertiesOption: boolean|undefined;

/**
* Stores the previous "Error on unknown elements" option value,
* allowing to restore it in the reset testing module logic.
*/
private _previousErrorOnUnknownElementsOption: boolean|undefined;

/**
* Stores the previous "Error on unknown properties" option value,
* allowing to restore it in the reset testing module logic.
*/
private _previousErrorOnUnknownPropertiesOption: boolean|undefined;

/**
* Initialize the environment for testing with a compiler factory, a PlatformRef, and an
* angular module. These are common to every test in the suite.
Expand Down Expand Up @@ -259,6 +279,8 @@ export class TestBedRender3 implements TestBed {

TestBedRender3._environmentErrorOnUnknownElementsOption = options?.errorOnUnknownElements;

TestBedRender3._environmentErrorOnUnknownPropertiesOption = options?.errorOnUnknownProperties;

this.platform = platform;
this.ngModule = ngModule;
this._compiler = new R3TestBedCompiler(this.platform, this.ngModule);
Expand Down Expand Up @@ -294,6 +316,9 @@ export class TestBedRender3 implements TestBed {
// Restore the previous value of the "error on unknown elements" option
setUnknownElementStrictMode(
this._previousErrorOnUnknownElementsOption ?? THROW_ON_UNKNOWN_ELEMENTS_DEFAULT);
// Restore the previous value of the "error on unknown properties" option
setUnknownPropertyStrictMode(
this._previousErrorOnUnknownPropertiesOption ?? THROW_ON_UNKNOWN_PROPERTIES_DEFAULT);

// We have to chain a couple of try/finally blocks, because each step can
// throw errors and we don't want it to interrupt the next step and we also
Expand All @@ -309,6 +334,7 @@ export class TestBedRender3 implements TestBed {
this._testModuleRef = null;
this._instanceTeardownOptions = undefined;
this._instanceErrorOnUnknownElementsOption = undefined;
this._instanceErrorOnUnknownPropertiesOption = undefined;
}
}
}
Expand Down Expand Up @@ -336,10 +362,13 @@ export class TestBedRender3 implements TestBed {
// This ensures that we don't carry them between tests.
this._instanceTeardownOptions = moduleDef.teardown;
this._instanceErrorOnUnknownElementsOption = moduleDef.errorOnUnknownElements;
this._instanceErrorOnUnknownPropertiesOption = moduleDef.errorOnUnknownProperties;
// Store the current value of the strict mode option,
// so we can restore it later
this._previousErrorOnUnknownElementsOption = getUnknownElementStrictMode();
setUnknownElementStrictMode(this.shouldThrowErrorOnUnknownElements());
this._previousErrorOnUnknownPropertiesOption = getUnknownPropertyStrictMode();
setUnknownPropertyStrictMode(this.shouldThrowErrorOnUnknownProperties());
this.compiler.configureTestingModule(moduleDef);
}

Expand Down Expand Up @@ -532,6 +561,13 @@ export class TestBedRender3 implements TestBed {
THROW_ON_UNKNOWN_ELEMENTS_DEFAULT;
}

shouldThrowErrorOnUnknownProperties(): boolean {
// Check if a configuration has been provided to throw when an unknown property is found
return this._instanceErrorOnUnknownPropertiesOption ??
TestBedRender3._environmentErrorOnUnknownPropertiesOption ??
THROW_ON_UNKNOWN_PROPERTIES_DEFAULT;
}

shouldTearDownTestingModule(): boolean {
return this._instanceTeardownOptions?.destroyAfterEach ??
TestBedRender3._environmentTeardownOptions?.destroyAfterEach ??
Expand Down
Loading

0 comments on commit a667592

Please sign in to comment.