diff --git a/packages/core/src/render3/instructions.ts b/packages/core/src/render3/instructions.ts index b4cfa1e870..1e791b4774 100644 --- a/packages/core/src/render3/instructions.ts +++ b/packages/core/src/render3/instructions.ts @@ -10,7 +10,7 @@ import {InjectFlags, InjectionToken, Injector} from '../di'; import {resolveForwardRef} from '../di/forward_ref'; import {ErrorHandler} from '../error_handler'; import {Type} from '../interface/type'; -import {validateAttribute, validateProperty} from '../sanitization/sanitization'; +import {validateAgainstEventAttributes, validateAgainstEventProperties} from '../sanitization/sanitization'; import {Sanitizer} from '../sanitization/security'; import {StyleSanitizeFn} from '../sanitization/style_sanitizer'; import {assertDataInRange, assertDefined, assertEqual, assertLessThan, assertNotEqual} from '../util/assert'; @@ -39,7 +39,7 @@ import {isNodeMatchingSelectorList, matchingSelectorIndex} from './node_selector import {decreaseElementDepthCount, enterView, getBindingsEnabled, getCheckNoChangesMode, getContextLView, getCurrentDirectiveDef, getElementDepthCount, getIsParent, getLView, getPreviousOrParentTNode, increaseElementDepthCount, isCreationMode, leaveView, nextContextImpl, resetComponentState, setBindingRoot, setCheckNoChangesMode, setCurrentDirectiveDef, setCurrentQueryIndex, setIsParent, setPreviousOrParentTNode} from './state'; import {getInitialClassNameValue, initializeStaticContext as initializeStaticStylingContext, patchContextWithStaticAttrs, renderInitialStylesAndClasses, renderStyling, updateClassProp as updateElementClassProp, updateContextWithBindings, updateStyleProp as updateElementStyleProp, updateStylingMap} from './styling/class_and_style_bindings'; import {BoundPlayerFactory} from './styling/player_factory'; -import {createEmptyStylingContext, getStylingContext, hasClassInput, hasStyling, isAnimationProp} from './styling/util'; +import {ANIMATION_PROP_PREFIX, createEmptyStylingContext, getStylingContext, hasClassInput, hasStyling, isAnimationProp} from './styling/util'; import {NO_CHANGE} from './tokens'; import {INTERPOLATION_DELIMITER, findComponentView, getComponentViewByIndex, getNativeByIndex, getNativeByTNode, getRootContext, getRootView, getTNode, isComponent, isComponentDef, isContentQueryHost, loadInternal, readElementValue, readPatchedLView, renderStringify} from './util'; @@ -1099,7 +1099,7 @@ export function elementAttribute( index: number, name: string, value: any, sanitizer?: SanitizerFn | null, namespace?: string): void { if (value !== NO_CHANGE) { - ngDevMode && validateAttribute(name); + ngDevMode && validateAgainstEventAttributes(name); const lView = getLView(); const renderer = lView[RENDERER]; const element = getNativeByIndex(index, lView); @@ -1193,7 +1193,8 @@ function elementPropertyInternal( } } else if (tNode.type === TNodeType.Element) { if (ngDevMode) { - validateProperty(propName); + validateAgainstEventProperties(propName); + validateAgainstUnknownProperties(element, propName, tNode); ngDevMode.rendererSetProperty++; } @@ -1212,6 +1213,18 @@ function elementPropertyInternal( } } +function validateAgainstUnknownProperties( + element: RElement | RComment, propName: string, tNode: TNode) { + // If prop is not a known property of the HTML element... + if (!(propName in element) && + // and isn't a synthetic animation property... + propName[0] !== ANIMATION_PROP_PREFIX) { + // ... it is probably a user error and we should throw. + throw new Error( + `Template error: Can't bind to '${propName}' since it isn't a known property of '${tNode.tagName}'.`); + } +} + /** * Stores debugging data for this property binding on first template pass. * This enables features like DebugElement.properties. diff --git a/packages/core/src/render3/styling/util.ts b/packages/core/src/render3/styling/util.ts index e2a9c14d27..5bc2c9441f 100644 --- a/packages/core/src/render3/styling/util.ts +++ b/packages/core/src/render3/styling/util.ts @@ -20,7 +20,7 @@ import {getTNode} from '../util'; import {CorePlayerHandler} from './core_player_handler'; -const ANIMATION_PROP_PREFIX = '@'; +export const ANIMATION_PROP_PREFIX = '@'; export function createEmptyStylingContext( element?: RElement | null, sanitizer?: StyleSanitizeFn | null, diff --git a/packages/core/src/sanitization/sanitization.ts b/packages/core/src/sanitization/sanitization.ts index 27552d8c51..c4c6b7cb4f 100644 --- a/packages/core/src/sanitization/sanitization.ts +++ b/packages/core/src/sanitization/sanitization.ts @@ -178,7 +178,7 @@ export const defaultStyleSanitizer = (function(prop: string, value?: string): st return sanitizeStyle(value); } as StyleSanitizeFn); -export function validateProperty(name: string) { +export function validateAgainstEventProperties(name: string) { if (name.toLowerCase().startsWith('on')) { const msg = `Binding to event property '${name}' is disallowed for security reasons, ` + `please use (${name.slice(2)})=...` + @@ -188,7 +188,7 @@ export function validateProperty(name: string) { } } -export function validateAttribute(name: string) { +export function validateAgainstEventAttributes(name: string) { if (name.toLowerCase().startsWith('on')) { const msg = `Binding to event attribute '${name}' is disallowed for security reasons, ` + `please use (${name.slice(2)})=...`; diff --git a/packages/core/test/linker/change_detection_integration_spec.ts b/packages/core/test/linker/change_detection_integration_spec.ts index 2302865089..19d6ae6a9b 100644 --- a/packages/core/test/linker/change_detection_integration_spec.ts +++ b/packages/core/test/linker/change_detection_integration_spec.ts @@ -6,8 +6,8 @@ * found in the LICENSE file at https://angular.io/license */ -import {DomElementSchemaRegistry, ElementSchemaRegistry, ResourceLoader, UrlResolver} from '@angular/compiler'; -import {MockResourceLoader, MockSchemaRegistry} from '@angular/compiler/testing'; +import {ResourceLoader, UrlResolver} from '@angular/compiler'; +import {MockResourceLoader} from '@angular/compiler/testing'; import {AfterContentChecked, AfterContentInit, AfterViewChecked, AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ContentChild, DebugElement, Directive, DoCheck, EventEmitter, HostBinding, Inject, Injectable, Input, OnChanges, OnDestroy, OnInit, Output, Pipe, PipeTransform, Provider, RenderComponentType, Renderer, RendererFactory2, RootRenderer, SimpleChange, SimpleChanges, TemplateRef, Type, ViewChild, ViewContainerRef, WrappedValue} from '@angular/core'; import {ComponentFixture, TestBed, fakeAsync} from '@angular/core/testing'; import {By} from '@angular/platform-browser/src/dom/debug/by'; @@ -20,14 +20,12 @@ export function createUrlResolverWithoutPackagePrefix(): UrlResolver { } const TEST_COMPILER_PROVIDERS: Provider[] = [ - {provide: ElementSchemaRegistry, useValue: new MockSchemaRegistry({}, {}, {}, [], [])}, {provide: ResourceLoader, useClass: MockResourceLoader, deps: []}, {provide: UrlResolver, useFactory: createUrlResolverWithoutPackagePrefix, deps: []} ]; (function() { - let elSchema: MockSchemaRegistry; let renderLog: RenderLog; let directiveLog: DirectiveLog; @@ -43,10 +41,8 @@ const TEST_COMPILER_PROVIDERS: Provider[] = [ } function initHelpers(): void { - elSchema = TestBed.get(ElementSchemaRegistry); renderLog = TestBed.get(RenderLog); directiveLog = TestBed.get(DirectiveLog); - elSchema.existingProperties['someProp'] = true; patchLoggingRenderer2(TestBed.get(RendererFactory2), renderLog); } @@ -67,7 +63,7 @@ const TEST_COMPILER_PROVIDERS: Provider[] = [ function _bindSimpleValue(expression: any, compType: Type): ComponentFixture; function _bindSimpleValue( expression: any, compType: Type = TestComponent): ComponentFixture { - return _bindSimpleProp(`[someProp]='${expression}'`, compType); + return _bindSimpleProp(`[id]='${expression}'`, compType); } function _bindAndCheckSimpleValue( @@ -117,145 +113,125 @@ const TEST_COMPILER_PROVIDERS: Provider[] = [ describe('expressions', () => { it('should support literals', - fakeAsync(() => { expect(_bindAndCheckSimpleValue(10)).toEqual(['someProp=10']); })); + fakeAsync(() => { expect(_bindAndCheckSimpleValue(10)).toEqual(['id=10']); })); it('should strip quotes from literals', - fakeAsync(() => { expect(_bindAndCheckSimpleValue('"str"')).toEqual(['someProp=str']); })); + fakeAsync(() => { expect(_bindAndCheckSimpleValue('"str"')).toEqual(['id=str']); })); - it('should support newlines in literals', fakeAsync(() => { - expect(_bindAndCheckSimpleValue('"a\n\nb"')).toEqual(['someProp=a\n\nb']); - })); + it('should support newlines in literals', + fakeAsync(() => { expect(_bindAndCheckSimpleValue('"a\n\nb"')).toEqual(['id=a\n\nb']); })); it('should support + operations', - fakeAsync(() => { expect(_bindAndCheckSimpleValue('10 + 2')).toEqual(['someProp=12']); })); + fakeAsync(() => { expect(_bindAndCheckSimpleValue('10 + 2')).toEqual(['id=12']); })); it('should support - operations', - fakeAsync(() => { expect(_bindAndCheckSimpleValue('10 - 2')).toEqual(['someProp=8']); })); + fakeAsync(() => { expect(_bindAndCheckSimpleValue('10 - 2')).toEqual(['id=8']); })); it('should support * operations', - fakeAsync(() => { expect(_bindAndCheckSimpleValue('10 * 2')).toEqual(['someProp=20']); })); + fakeAsync(() => { expect(_bindAndCheckSimpleValue('10 * 2')).toEqual(['id=20']); })); it('should support / operations', fakeAsync(() => { - expect(_bindAndCheckSimpleValue('10 / 2')).toEqual([`someProp=${5.0}`]); + expect(_bindAndCheckSimpleValue('10 / 2')).toEqual([`id=${5.0}`]); })); // dart exp=5.0, js exp=5 it('should support % operations', - fakeAsync(() => { expect(_bindAndCheckSimpleValue('11 % 2')).toEqual(['someProp=1']); })); + fakeAsync(() => { expect(_bindAndCheckSimpleValue('11 % 2')).toEqual(['id=1']); })); - it('should support == operations on identical', fakeAsync(() => { - expect(_bindAndCheckSimpleValue('1 == 1')).toEqual(['someProp=true']); - })); + it('should support == operations on identical', + fakeAsync(() => { expect(_bindAndCheckSimpleValue('1 == 1')).toEqual(['id=true']); })); - it('should support != operations', fakeAsync(() => { - expect(_bindAndCheckSimpleValue('1 != 1')).toEqual(['someProp=false']); - })); + it('should support != operations', + fakeAsync(() => { expect(_bindAndCheckSimpleValue('1 != 1')).toEqual(['id=false']); })); - it('should support == operations on coerceible', fakeAsync(() => { - expect(_bindAndCheckSimpleValue('1 == true')).toEqual([`someProp=true`]); - })); + it('should support == operations on coerceible', + fakeAsync(() => { expect(_bindAndCheckSimpleValue('1 == true')).toEqual([`id=true`]); })); - it('should support === operations on identical', fakeAsync(() => { - expect(_bindAndCheckSimpleValue('1 === 1')).toEqual(['someProp=true']); - })); + it('should support === operations on identical', + fakeAsync(() => { expect(_bindAndCheckSimpleValue('1 === 1')).toEqual(['id=true']); })); - it('should support !== operations', fakeAsync(() => { - expect(_bindAndCheckSimpleValue('1 !== 1')).toEqual(['someProp=false']); - })); + it('should support !== operations', + fakeAsync(() => { expect(_bindAndCheckSimpleValue('1 !== 1')).toEqual(['id=false']); })); it('should support === operations on coerceible', fakeAsync(() => { - expect(_bindAndCheckSimpleValue('1 === true')).toEqual(['someProp=false']); + expect(_bindAndCheckSimpleValue('1 === true')).toEqual(['id=false']); })); - it('should support true < operations', fakeAsync(() => { - expect(_bindAndCheckSimpleValue('1 < 2')).toEqual(['someProp=true']); - })); + it('should support true < operations', + fakeAsync(() => { expect(_bindAndCheckSimpleValue('1 < 2')).toEqual(['id=true']); })); - it('should support false < operations', fakeAsync(() => { - expect(_bindAndCheckSimpleValue('2 < 1')).toEqual(['someProp=false']); - })); + it('should support false < operations', + fakeAsync(() => { expect(_bindAndCheckSimpleValue('2 < 1')).toEqual(['id=false']); })); - it('should support false > operations', fakeAsync(() => { - expect(_bindAndCheckSimpleValue('1 > 2')).toEqual(['someProp=false']); - })); + it('should support false > operations', + fakeAsync(() => { expect(_bindAndCheckSimpleValue('1 > 2')).toEqual(['id=false']); })); - it('should support true > operations', fakeAsync(() => { - expect(_bindAndCheckSimpleValue('2 > 1')).toEqual(['someProp=true']); - })); + it('should support true > operations', + fakeAsync(() => { expect(_bindAndCheckSimpleValue('2 > 1')).toEqual(['id=true']); })); - it('should support true <= operations', fakeAsync(() => { - expect(_bindAndCheckSimpleValue('1 <= 2')).toEqual(['someProp=true']); - })); + it('should support true <= operations', + fakeAsync(() => { expect(_bindAndCheckSimpleValue('1 <= 2')).toEqual(['id=true']); })); - it('should support equal <= operations', fakeAsync(() => { - expect(_bindAndCheckSimpleValue('2 <= 2')).toEqual(['someProp=true']); - })); + it('should support equal <= operations', + fakeAsync(() => { expect(_bindAndCheckSimpleValue('2 <= 2')).toEqual(['id=true']); })); - it('should support false <= operations', fakeAsync(() => { - expect(_bindAndCheckSimpleValue('2 <= 1')).toEqual(['someProp=false']); - })); + it('should support false <= operations', + fakeAsync(() => { expect(_bindAndCheckSimpleValue('2 <= 1')).toEqual(['id=false']); })); - it('should support true >= operations', fakeAsync(() => { - expect(_bindAndCheckSimpleValue('2 >= 1')).toEqual(['someProp=true']); - })); + it('should support true >= operations', + fakeAsync(() => { expect(_bindAndCheckSimpleValue('2 >= 1')).toEqual(['id=true']); })); - it('should support equal >= operations', fakeAsync(() => { - expect(_bindAndCheckSimpleValue('2 >= 2')).toEqual(['someProp=true']); - })); + it('should support equal >= operations', + fakeAsync(() => { expect(_bindAndCheckSimpleValue('2 >= 2')).toEqual(['id=true']); })); - it('should support false >= operations', fakeAsync(() => { - expect(_bindAndCheckSimpleValue('1 >= 2')).toEqual(['someProp=false']); - })); + it('should support false >= operations', + fakeAsync(() => { expect(_bindAndCheckSimpleValue('1 >= 2')).toEqual(['id=false']); })); it('should support true && operations', fakeAsync(() => { - expect(_bindAndCheckSimpleValue('true && true')).toEqual(['someProp=true']); + expect(_bindAndCheckSimpleValue('true && true')).toEqual(['id=true']); })); it('should support false && operations', fakeAsync(() => { - expect(_bindAndCheckSimpleValue('true && false')).toEqual(['someProp=false']); + expect(_bindAndCheckSimpleValue('true && false')).toEqual(['id=false']); })); it('should support true || operations', fakeAsync(() => { - expect(_bindAndCheckSimpleValue('true || false')).toEqual(['someProp=true']); + expect(_bindAndCheckSimpleValue('true || false')).toEqual(['id=true']); })); it('should support false || operations', fakeAsync(() => { - expect(_bindAndCheckSimpleValue('false || false')).toEqual(['someProp=false']); + expect(_bindAndCheckSimpleValue('false || false')).toEqual(['id=false']); })); - it('should support negate', fakeAsync(() => { - expect(_bindAndCheckSimpleValue('!true')).toEqual(['someProp=false']); - })); + it('should support negate', + fakeAsync(() => { expect(_bindAndCheckSimpleValue('!true')).toEqual(['id=false']); })); - it('should support double negate', fakeAsync(() => { - expect(_bindAndCheckSimpleValue('!!true')).toEqual(['someProp=true']); - })); + it('should support double negate', + fakeAsync(() => { expect(_bindAndCheckSimpleValue('!!true')).toEqual(['id=true']); })); - it('should support true conditionals', fakeAsync(() => { - expect(_bindAndCheckSimpleValue('1 < 2 ? 1 : 2')).toEqual(['someProp=1']); - })); + it('should support true conditionals', + fakeAsync(() => { expect(_bindAndCheckSimpleValue('1 < 2 ? 1 : 2')).toEqual(['id=1']); })); - it('should support false conditionals', fakeAsync(() => { - expect(_bindAndCheckSimpleValue('1 > 2 ? 1 : 2')).toEqual(['someProp=2']); - })); + it('should support false conditionals', + fakeAsync(() => { expect(_bindAndCheckSimpleValue('1 > 2 ? 1 : 2')).toEqual(['id=2']); })); it('should support keyed access to a list item', fakeAsync(() => { - expect(_bindAndCheckSimpleValue('["foo", "bar"][0]')).toEqual(['someProp=foo']); + expect(_bindAndCheckSimpleValue('["foo", "bar"][0]')).toEqual(['id=foo']); })); it('should support keyed access to a map item', fakeAsync(() => { - expect(_bindAndCheckSimpleValue('{"foo": "bar"}["foo"]')).toEqual(['someProp=bar']); + expect(_bindAndCheckSimpleValue('{"foo": "bar"}["foo"]')).toEqual(['id=bar']); })); it('should report all changes on the first run including uninitialized values', fakeAsync(() => { - expect(_bindAndCheckSimpleValue('value', Uninitialized)).toEqual(['someProp=null']); + expect(_bindAndCheckSimpleValue('value', Uninitialized)).toEqual(['id=null']); })); it('should report all changes on the first run including null values', fakeAsync(() => { const ctx = _bindSimpleValue('a', TestData); ctx.componentInstance.a = null; ctx.detectChanges(false); - expect(renderLog.log).toEqual(['someProp=null']); + expect(renderLog.log).toEqual(['id=null']); })); it('should support simple chained property access', fakeAsync(() => { @@ -263,7 +239,7 @@ const TEST_COMPILER_PROVIDERS: Provider[] = [ ctx.componentInstance.name = 'Victor'; ctx.componentInstance.address = new Address('Grenoble'); ctx.detectChanges(false); - expect(renderLog.log).toEqual(['someProp=Grenoble']); + expect(renderLog.log).toEqual(['id=Grenoble']); })); describe('safe navigation operator', () => { @@ -271,54 +247,54 @@ const TEST_COMPILER_PROVIDERS: Provider[] = [ const ctx = _bindSimpleValue('address?.city', Person); ctx.componentInstance.address = null !; ctx.detectChanges(false); - expect(renderLog.log).toEqual(['someProp=null']); + expect(renderLog.log).toEqual(['id=null']); })); it('should support calling methods on nulls', fakeAsync(() => { const ctx = _bindSimpleValue('address?.toString()', Person); ctx.componentInstance.address = null !; ctx.detectChanges(false); - expect(renderLog.log).toEqual(['someProp=null']); + expect(renderLog.log).toEqual(['id=null']); })); it('should support reading properties on non nulls', fakeAsync(() => { const ctx = _bindSimpleValue('address?.city', Person); ctx.componentInstance.address = new Address('MTV'); ctx.detectChanges(false); - expect(renderLog.log).toEqual(['someProp=MTV']); + expect(renderLog.log).toEqual(['id=MTV']); })); it('should support calling methods on non nulls', fakeAsync(() => { const ctx = _bindSimpleValue('address?.toString()', Person); ctx.componentInstance.address = new Address('MTV'); ctx.detectChanges(false); - expect(renderLog.log).toEqual(['someProp=MTV']); + expect(renderLog.log).toEqual(['id=MTV']); })); it('should support short-circuting safe navigation', fakeAsync(() => { const ctx = _bindSimpleValue('value?.address.city', PersonHolder); ctx.componentInstance.value = null !; ctx.detectChanges(false); - expect(renderLog.log).toEqual(['someProp=null']); + expect(renderLog.log).toEqual(['id=null']); })); it('should support nested short-circuting safe navigation', fakeAsync(() => { const ctx = _bindSimpleValue('value.value?.address.city', PersonHolderHolder); ctx.componentInstance.value = new PersonHolder(); ctx.detectChanges(false); - expect(renderLog.log).toEqual(['someProp=null']); + expect(renderLog.log).toEqual(['id=null']); })); it('should support chained short-circuting safe navigation', fakeAsync(() => { const ctx = _bindSimpleValue('value?.value?.address.city', PersonHolderHolder); ctx.detectChanges(false); - expect(renderLog.log).toEqual(['someProp=null']); + expect(renderLog.log).toEqual(['id=null']); })); it('should support short-circuting array index operations', fakeAsync(() => { const ctx = _bindSimpleValue('value?.phones[0]', PersonHolder); ctx.detectChanges(false); - expect(renderLog.log).toEqual(['someProp=null']); + expect(renderLog.log).toEqual(['id=null']); })); it('should still throw if right-side would throw', fakeAsync(() => { @@ -335,21 +311,21 @@ const TEST_COMPILER_PROVIDERS: Provider[] = [ it('should support method calls', fakeAsync(() => { const ctx = _bindSimpleValue('sayHi("Jim")', Person); ctx.detectChanges(false); - expect(renderLog.log).toEqual(['someProp=Hi, Jim']); + expect(renderLog.log).toEqual(['id=Hi, Jim']); })); it('should support function calls', fakeAsync(() => { const ctx = _bindSimpleValue('a()(99)', TestData); ctx.componentInstance.a = () => (a: any) => a; ctx.detectChanges(false); - expect(renderLog.log).toEqual(['someProp=99']); + expect(renderLog.log).toEqual(['id=99']); })); it('should support chained method calls', fakeAsync(() => { const ctx = _bindSimpleValue('address.toString()', Person); ctx.componentInstance.address = new Address('MTV'); ctx.detectChanges(false); - expect(renderLog.log).toEqual(['someProp=MTV']); + expect(renderLog.log).toEqual(['id=MTV']); })); it('should support NaN', fakeAsync(() => { @@ -357,7 +333,7 @@ const TEST_COMPILER_PROVIDERS: Provider[] = [ ctx.componentInstance.age = NaN; ctx.detectChanges(false); - expect(renderLog.log).toEqual(['someProp=NaN']); + expect(renderLog.log).toEqual(['id=NaN']); renderLog.clear(); ctx.detectChanges(false); @@ -369,7 +345,7 @@ const TEST_COMPILER_PROVIDERS: Provider[] = [ ctx.componentInstance.name = 'misko'; ctx.detectChanges(false); - expect(renderLog.log).toEqual(['someProp=misko']); + expect(renderLog.log).toEqual(['id=misko']); renderLog.clear(); ctx.detectChanges(false); @@ -378,7 +354,7 @@ const TEST_COMPILER_PROVIDERS: Provider[] = [ ctx.componentInstance.name = 'Misko'; ctx.detectChanges(false); - expect(renderLog.log).toEqual(['someProp=Misko']); + expect(renderLog.log).toEqual(['id=Misko']); })); it('should support literal array made of literals', fakeAsync(() => { @@ -445,7 +421,7 @@ const TEST_COMPILER_PROVIDERS: Provider[] = [ it('should ignore empty bindings', fakeAsync(() => { - const ctx = _bindSimpleProp('[someProp]', TestData); + const ctx = _bindSimpleProp('[id]', TestData); ctx.componentInstance.a = 'value'; ctx.detectChanges(false); @@ -453,23 +429,23 @@ const TEST_COMPILER_PROVIDERS: Provider[] = [ })); it('should support interpolation', fakeAsync(() => { - const ctx = _bindSimpleProp('someProp="B{{a}}A"', TestData); + const ctx = _bindSimpleProp('id="B{{a}}A"', TestData); ctx.componentInstance.a = 'value'; ctx.detectChanges(false); - expect(renderLog.log).toEqual(['someProp=BvalueA']); + expect(renderLog.log).toEqual(['id=BvalueA']); })); it('should output empty strings for null values in interpolation', fakeAsync(() => { - const ctx = _bindSimpleProp('someProp="B{{a}}A"', TestData); + const ctx = _bindSimpleProp('id="B{{a}}A"', TestData); ctx.componentInstance.a = null; ctx.detectChanges(false); - expect(renderLog.log).toEqual(['someProp=BA']); + expect(renderLog.log).toEqual(['id=BA']); })); it('should escape values in literals that indicate interpolation', - fakeAsync(() => { expect(_bindAndCheckSimpleValue('"$"')).toEqual(['someProp=$']); })); + fakeAsync(() => { expect(_bindAndCheckSimpleValue('"$"')).toEqual(['id=$']); })); it('should read locals', fakeAsync(() => { const ctx = createCompFixture( @@ -515,7 +491,7 @@ const TEST_COMPILER_PROVIDERS: Provider[] = [ ctx.detectChanges(false); - expect(renderLog.log).toEqual(['someProp=Megatron']); + expect(renderLog.log).toEqual(['id=Megatron']); renderLog.clear(); ctx.detectChanges(false); @@ -528,12 +504,12 @@ const TEST_COMPILER_PROVIDERS: Provider[] = [ ctx.detectChanges(false); - expect(renderLog.log).toEqual(['someProp=Megatron']); + expect(renderLog.log).toEqual(['id=Megatron']); renderLog.clear(); ctx.detectChanges(false); - expect(renderLog.log).toEqual(['someProp=Megatron']); + expect(renderLog.log).toEqual(['id=Megatron']); })); it('should record unwrapped values via ngOnChanges', fakeAsync(() => { @@ -591,8 +567,8 @@ const TEST_COMPILER_PROVIDERS: Provider[] = [ .it('should call pure pipes that are used multiple times only when the arguments change and share state between pipe instances', fakeAsync(() => { const ctx = createCompFixture( - `
` + - '
', + `
` + + '
', Person); ctx.componentInstance.name = 'a'; ctx.componentInstance.age = 10; @@ -618,8 +594,8 @@ const TEST_COMPILER_PROVIDERS: Provider[] = [ it('should call pure pipes that are used multiple times only when the arguments change', fakeAsync(() => { const ctx = createCompFixture( - `
` + - '
', + `
` + + '
', Person); ctx.componentInstance.name = 'a'; ctx.componentInstance.age = 10; @@ -731,7 +707,7 @@ const TEST_COMPILER_PROVIDERS: Provider[] = [ describe('reading directives', () => { it('should read directive properties', fakeAsync(() => { const ctx = createCompFixture( - '
'); + '
'); ctx.detectChanges(false); expect(renderLog.loggedValues).toEqual([42]); })); @@ -1191,7 +1167,7 @@ const TEST_COMPILER_PROVIDERS: Provider[] = [ TestBed.configureTestingModule({declarations: [ChangingDirective]}); - const ctx = createCompFixture('
', TestData); + const ctx = createCompFixture('
', TestData); ctx.componentInstance.b = 1; const errMsgRegExp = ivyEnabled ? @@ -1210,7 +1186,7 @@ const TEST_COMPILER_PROVIDERS: Provider[] = [ TestBed.configureTestingModule({declarations: [ChangingDirective]}); - const ctx = createCompFixture('
', TestData); + const ctx = createCompFixture('
', TestData); ctx.componentInstance.b = 1; ctx.detectChanges(); @@ -1532,13 +1508,7 @@ const TEST_COMPILER_PROVIDERS: Provider[] = [ } const ctx = - TestBed - .configureCompiler({ - providers: - [{provide: ElementSchemaRegistry, useExisting: DomElementSchemaRegistry}] - }) - .configureTestingModule({declarations: [Comp, SomeDir]}) - .createComponent(Comp); + TestBed.configureTestingModule({declarations: [Comp, SomeDir]}).createComponent(Comp); ctx.detectChanges(); diff --git a/packages/core/test/linker/integration_spec.ts b/packages/core/test/linker/integration_spec.ts index af5087c199..b705cd2af8 100644 --- a/packages/core/test/linker/integration_spec.ts +++ b/packages/core/test/linker/integration_spec.ts @@ -22,7 +22,7 @@ import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter'; import {DOCUMENT} from '@angular/platform-browser/src/dom/dom_tokens'; import {dispatchEvent, el} from '@angular/platform-browser/testing/src/browser_util'; import {expect} from '@angular/platform-browser/testing/src/matchers'; -import {fixmeIvy, modifiedInIvy, obsoleteInIvy} from '@angular/private/testing'; +import {modifiedInIvy, obsoleteInIvy, onlyInIvy} from '@angular/private/testing'; import {stringify} from '../../src/util/stringify'; @@ -1592,7 +1592,7 @@ function declareTests(config?: {useJit: boolean}) { }); describe('Property bindings', () => { - fixmeIvy('FW-721: Bindings to unknown properties are not reported as errors') + modifiedInIvy('Unknown property error thrown during update mode, not creation mode') .it('should throw on bindings to unknown properties', () => { TestBed.configureTestingModule({declarations: [MyComp]}); const template = '
'; @@ -1606,6 +1606,21 @@ function declareTests(config?: {useJit: boolean}) { } }); + onlyInIvy('Unknown property error thrown during update mode, not creation mode') + .it('should throw on bindings to unknown properties', () => { + TestBed.configureTestingModule({declarations: [MyComp]}); + const template = '
'; + TestBed.overrideComponent(MyComp, {set: {template}}); + try { + const fixture = TestBed.createComponent(MyComp); + fixture.detectChanges(); + throw 'Should throw'; + } catch (e) { + expect(e.message).toMatch( + /Template error: Can't bind to 'unknown' since it isn't a known property of 'div'./); + } + }); + it('should not throw for property binding to a non-existing property when there is a matching directive property', () => { TestBed.configureTestingModule({declarations: [MyComp, MyDir]}); diff --git a/packages/core/test/linker/ng_module_integration_spec.ts b/packages/core/test/linker/ng_module_integration_spec.ts index 58a7afca00..ad9a3e7e2a 100644 --- a/packages/core/test/linker/ng_module_integration_spec.ts +++ b/packages/core/test/linker/ng_module_integration_spec.ts @@ -239,7 +239,7 @@ function declareTests(config?: {useJit: boolean}) { }); describe('schemas', () => { - fixmeIvy('FW-819: ngtsc compiler should support schemas') + modifiedInIvy('Unknown property error thrown during update mode, not creation mode') .it('should error on unknown bound properties on custom elements by default', () => { @Component({template: ''}) class ComponentUsingInvalidProperty { @@ -252,19 +252,36 @@ function declareTests(config?: {useJit: boolean}) { expect(() => createModule(SomeModule)).toThrowError(/Can't bind to 'someUnknownProp'/); }); - it('should not error on unknown bound properties on custom elements when using the CUSTOM_ELEMENTS_SCHEMA', - () => { - @Component({template: ''}) - class ComponentUsingInvalidProperty { - } + onlyInIvy('Unknown property error thrown during update mode, not creation mode') + .it('should error on unknown bound properties on custom elements by default', () => { + @Component({template: ''}) + class ComponentUsingInvalidProperty { + } - @NgModule( - {schemas: [CUSTOM_ELEMENTS_SCHEMA], declarations: [ComponentUsingInvalidProperty]}) - class SomeModule { - } + @NgModule({declarations: [ComponentUsingInvalidProperty]}) + class SomeModule { + } - expect(() => createModule(SomeModule)).not.toThrow(); - }); + const fixture = createComp(ComponentUsingInvalidProperty, SomeModule); + expect(() => fixture.detectChanges()).toThrowError(/Can't bind to 'someUnknownProp'/); + }); + + fixmeIvy('FW-819: ngtsc compiler should support schemas') + .it('should not error on unknown bound properties on custom elements when using the CUSTOM_ELEMENTS_SCHEMA', + () => { + @Component({template: ''}) + class ComponentUsingInvalidProperty { + } + + @NgModule({ + schemas: [CUSTOM_ELEMENTS_SCHEMA], + declarations: [ComponentUsingInvalidProperty] + }) + class SomeModule { + } + + expect(() => createModule(SomeModule)).not.toThrow(); + }); }); describe('id', () => { diff --git a/packages/core/test/linker/security_integration_spec.ts b/packages/core/test/linker/security_integration_spec.ts index fb518c98b6..8908822d05 100644 --- a/packages/core/test/linker/security_integration_spec.ts +++ b/packages/core/test/linker/security_integration_spec.ts @@ -255,7 +255,7 @@ function declareTests(config?: {useJit: boolean}) { expect(getDOM().getStyle(e, 'background')).not.toContain('javascript'); }); - fixmeIvy('FW-850: Should throw on unsafe SVG attributes') + modifiedInIvy('Unknown property error thrown during update mode, not creation mode') .it('should escape unsafe SVG attributes', () => { const template = `Text`; TestBed.overrideComponent(SecuredComponent, {set: {template}}); @@ -264,6 +264,15 @@ function declareTests(config?: {useJit: boolean}) { .toThrowError(/Can't bind to 'xlink:href'/); }); + onlyInIvy('Unknown property error thrown during update mode, not creation mode') + .it('should escape unsafe SVG attributes', () => { + const template = `Text`; + TestBed.overrideComponent(SecuredComponent, {set: {template}}); + + const fixture = TestBed.createComponent(SecuredComponent); + expect(() => fixture.detectChanges()).toThrowError(/Can't bind to 'xlink:href'/); + }); + it('should escape unsafe HTML values', () => { const template = `
Text
`; TestBed.overrideComponent(SecuredComponent, {set: {template}}); diff --git a/packages/core/test/render3/instructions_spec.ts b/packages/core/test/render3/instructions_spec.ts index ebf4ddc476..f69ec88ee5 100644 --- a/packages/core/test/render3/instructions_spec.ts +++ b/packages/core/test/render3/instructions_spec.ts @@ -182,11 +182,12 @@ describe('instructions', () => { }); it('should not stringify non string values', () => { - const t = new TemplateFixture(createDiv, () => {}, 1); + const t = new TemplateFixture(() => { element(0, 'input'); }, () => {}, 1); - t.update(() => elementProperty(0, 'hidden', false)); - // The hidden property would be true if `false` was stringified into `"false"`. - expect((t.hostElement as HTMLElement).querySelector('div') !.hidden).toEqual(false); + // Note: don't use 'hidden' here because IE10 does not support the hidden property + t.update(() => elementProperty(0, 'required', false)); + // The required property would be true if `false` was stringified into `"false"`. + expect((t.hostElement as HTMLElement).querySelector('input') !.required).toEqual(false); expect(ngDevMode).toHaveProperties({ firstTemplatePass: 1, tNode: 2, // 1 for div, 1 for host element diff --git a/packages/core/test/render3/lifecycle_spec.ts b/packages/core/test/render3/lifecycle_spec.ts index dc2c9ac96c..c721ff6613 100644 --- a/packages/core/test/render3/lifecycle_spec.ts +++ b/packages/core/test/render3/lifecycle_spec.ts @@ -443,7 +443,8 @@ describe('lifecycles', () => { factory: () => new Component(), template, consts: consts, vars: vars, - directives: directives + directives: directives, + inputs: {val: 'val'} }); }; } @@ -2665,10 +2666,10 @@ describe('lifecycles', () => { element(0, 'div'); } if (rf & RenderFlags.Update) { - elementProperty(0, 'data-a', bind(ctx.a)); + elementProperty(0, 'id', bind(ctx.a)); } }, - selectors: [['mycomp']], + selectors: [['my-comp']], inputs: { value: 'value', }, @@ -2683,7 +2684,7 @@ describe('lifecycles', () => { const App = createComponent('app', function(rf: RenderFlags, ctx: any) { if (rf & RenderFlags.Create) { - element(0, 'mycomp'); + element(0, 'my-comp'); } if (rf & RenderFlags.Update) { elementProperty(0, 'value', bind(1)); diff --git a/packages/core/test/render3/pipe_spec.ts b/packages/core/test/render3/pipe_spec.ts index 59bbb57929..a2a9050d42 100644 --- a/packages/core/test/render3/pipe_spec.ts +++ b/packages/core/test/render3/pipe_spec.ts @@ -170,12 +170,12 @@ describe('pipe', () => { elementEnd(); } if (rf & RenderFlags.Update) { - elementProperty(0, 'someProp', bind(pipeBind1(1, 1, 'Megatron'))); + elementProperty(0, 'id', bind(pipeBind1(1, 1, 'Megatron'))); } } renderToHtml(Template, person, 2, 3, null, [IdentityPipe], rendererFactory2); - expect(renderLog.log).toEqual(['someProp=Megatron']); + expect(renderLog.log).toEqual(['id=Megatron']); renderLog.clear(); renderToHtml(Template, person, 2, 3, null, pipes, rendererFactory2); @@ -255,8 +255,8 @@ describe('pipe', () => { container(4); } if (rf & RenderFlags.Update) { - elementProperty(0, 'someProp', bind(pipeBind1(1, 2, true))); - elementProperty(2, 'someProp', bind(pipeBind1(3, 4, true))); + elementProperty(0, 'id', bind(pipeBind1(1, 2, true))); + elementProperty(2, 'id', bind(pipeBind1(3, 4, true))); pipeInstances.push(load(1), load(3)); containerRefreshStart(4); { @@ -269,7 +269,7 @@ describe('pipe', () => { elementEnd(); } if (rf1 & RenderFlags.Update) { - elementProperty(0, 'someProp', bind(pipeBind1(1, 1, true))); + elementProperty(0, 'id', bind(pipeBind1(1, 1, true))); pipeInstances.push(load(1)); } } diff --git a/packages/core/test/render3/view_container_ref_spec.ts b/packages/core/test/render3/view_container_ref_spec.ts index db9f6b497c..c3ad8f260c 100644 --- a/packages/core/test/render3/view_container_ref_spec.ts +++ b/packages/core/test/render3/view_container_ref_spec.ts @@ -42,12 +42,14 @@ describe('ViewContainerRef', () => { factory: () => directiveInstance = new DirectiveWithVCRef( directiveInject(ViewContainerRef as any), injectComponentFactoryResolver()), - inputs: {tplRef: 'tplRef'} + inputs: {tplRef: 'tplRef', name: 'name'} }); // TODO(issue/24571): remove '!'. tplRef !: TemplateRef<{}>; + name: string = ''; + // injecting a ViewContainerRef to create a dynamic container in which embedded views will be // created constructor(public vcref: ViewContainerRef, public cfr: ComponentFactoryResolver) {} diff --git a/packages/platform-server/test/integration_spec.ts b/packages/platform-server/test/integration_spec.ts index a9d0c6978c..3bac6084ad 100644 --- a/packages/platform-server/test/integration_spec.ts +++ b/packages/platform-server/test/integration_spec.ts @@ -17,7 +17,7 @@ import {MockBackend, MockConnection} from '@angular/http/testing'; import {BrowserModule, DOCUMENT, Title, TransferState, makeStateKey} from '@angular/platform-browser'; import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter'; import {BEFORE_APP_SERIALIZED, INITIAL_CONFIG, PlatformState, ServerModule, ServerTransferStateModule, platformDynamicServer, renderModule, renderModuleFactory} from '@angular/platform-server'; -import {fixmeIvy, ivyEnabled} from '@angular/private/testing'; +import {fixmeIvy, ivyEnabled, modifiedInIvy} from '@angular/private/testing'; import {Observable} from 'rxjs'; import {first} from 'rxjs/operators'; @@ -99,7 +99,7 @@ class TitleApp { class TitleAppModule { } -@Component({selector: 'app', template: '{{text}}

'}) +@Component({selector: 'app', template: '{{text}}

'}) class MyAsyncServerApp { text = ''; h1 = ''; @@ -276,6 +276,19 @@ class MyHostComponent { class FalseAttributesModule { } +@Component({selector: 'app', template: '
'}) +class InnerTextComponent { + foo = 'Some text'; +} + +@NgModule({ + declarations: [InnerTextComponent], + bootstrap: [InnerTextComponent], + imports: [ServerModule, BrowserModule.withServerTransition({appId: 'inner-text'})] +}) +class InnerTextModule { +} + @Component({selector: 'app', template: ''}) class MyInputComponent { @Input() @@ -528,7 +541,7 @@ class HiddenModule { let doc: string; let called: boolean; let expectedOutput = - 'Works!

fine

'; + 'Works!

fine

'; beforeEach(() => { // PlatformConfig takes in a parsed document so that it can be cached across requests. @@ -567,6 +580,15 @@ class HiddenModule { }); })); + modifiedInIvy('Will not support binding to innerText in Ivy since domino does not') + .it('should support binding to innerText', async(() => { + renderModule(InnerTextModule, {document: doc}).then(output => { + expect(output).toBe( + '
Some text
'); + called = true; + }); + })); + it('using renderModuleFactory should work', async(inject([PlatformRef], (defaultPlatform: PlatformRef) => { const compilerFactory: CompilerFactory = diff --git a/tools/material-ci/angular_material_test_blocklist.js b/tools/material-ci/angular_material_test_blocklist.js index ab5d5878bc..f31fd2b7dd 100644 --- a/tools/material-ci/angular_material_test_blocklist.js +++ b/tools/material-ci/angular_material_test_blocklist.js @@ -953,6 +953,58 @@ window.testBlocklist = { "error": "Error: Expected undefined to be truthy.", "notes": "Unknown" }, + "MatButton should apply class based on color attribute": { + "error": "Template error: Can't bind to 'disabled' since it isn't a known property of 'a'", + "notes": "FW-1037: Host bindings for host objects in metadata are inherited" + }, + "MatButton should not clear previous defined classes": { + "error": "Template error: Can't bind to 'disabled' since it isn't a known property of 'a'", + "notes": "FW-1037: Host bindings for host objects in metadata are inherited" + }, + "MatButton button[mat-fab] should have accent palette by default": { + "error": "Template error: Can't bind to 'disabled' since it isn't a known property of 'a'", + "notes": "FW-1037: Host bindings for host objects in metadata are inherited" + }, + "MatButton button[mat-mini-fab] should have accent palette by default": { + "error": "Template error: Can't bind to 'disabled' since it isn't a known property of 'a'", + "notes": "FW-1037: Host bindings for host objects in metadata are inherited" + }, + "MatButton button[mat-button] should not increment if disabled": { + "error": "Template error: Can't bind to 'disabled' since it isn't a known property of 'a'", + "notes": "FW-1037: Host bindings for host objects in metadata are inherited" + }, + "MatButton button[mat-button] should disable the native button element": { + "error": "Template error: Can't bind to 'disabled' since it isn't a known property of 'a'", + "notes": "FW-1037: Host bindings for host objects in metadata are inherited" + }, + "MatButton a[mat-button] should not redirect if disabled": { + "error": "Template error: Can't bind to 'disabled' since it isn't a known property of 'a'", + "notes": "FW-1037: Host bindings for host objects in metadata are inherited" + }, + "MatButton a[mat-button] should remove tabindex if disabled": { + "error": "Template error: Can't bind to 'disabled' since it isn't a known property of 'a'", + "notes": "FW-1037: Host bindings for host objects in metadata are inherited" + }, + "MatButton a[mat-button] should add aria-disabled attribute if disabled": { + "error": "Template error: Can't bind to 'disabled' since it isn't a known property of 'a'", + "notes": "FW-1037: Host bindings for host objects in metadata are inherited" + }, + "MatButton a[mat-button] should not add aria-disabled attribute if disabled is false": { + "error": "Template error: Can't bind to 'disabled' since it isn't a known property of 'a'", + "notes": "FW-1037: Host bindings for host objects in metadata are inherited" + }, + "MatButton a[mat-button] should be able to set a custom tabindex": { + "error": "Template error: Can't bind to 'disabled' since it isn't a known property of 'a'", + "notes": "FW-1037: Host bindings for host objects in metadata are inherited" + }, + "MatButton button ripples should disable the ripple if matRippleDisabled input is set": { + "error": "Template error: Can't bind to 'disabled' since it isn't a known property of 'a'", + "notes": "FW-1037: Host bindings for host objects in metadata are inherited" + }, + "MatButton button ripples should disable the ripple when the button is disabled": { + "error": "Template error: Can't bind to 'disabled' since it isn't a known property of 'a'", + "notes": "FW-1037: Host bindings for host objects in metadata are inherited" + }, "MatTabHeader focusing should initialize to the selected index": { "error": "TypeError: Cannot read property 'nativeElement' of undefined", "notes": "FW-1019: Design new API to replace static queries"