Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(core): allow to throw on unknown properties in tests #45853

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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