fix(ivy): throw on bindings to unknown properties (#28537)

This commit adds a devMode-only check which will throw if a user
attempts to bind a property that does not match a directive
input or a known HTML property.

Example:
```
<div [unknownProp]="someValue"></div>
```

The above will throw because "unknownProp" is not a known
property of HTMLDivElement.

This check is similar to the check executed in View Engine during
template parsing, but occurs at runtime instead of compile-time.

Note: This change uncovered an existing bug with host binding
inheritance, so some Material tests had to be turned off. They
will be fixed in an upcoming PR.

PR Close #28537
This commit is contained in:
Kara Erickson 2019-02-04 21:42:55 -08:00 committed by Miško Hevery
parent 7660d0d74a
commit 1950e2d9ba
13 changed files with 262 additions and 160 deletions

View File

@ -10,7 +10,7 @@ import {InjectFlags, InjectionToken, Injector} from '../di';
import {resolveForwardRef} from '../di/forward_ref'; import {resolveForwardRef} from '../di/forward_ref';
import {ErrorHandler} from '../error_handler'; import {ErrorHandler} from '../error_handler';
import {Type} from '../interface/type'; import {Type} from '../interface/type';
import {validateAttribute, validateProperty} from '../sanitization/sanitization'; import {validateAgainstEventAttributes, validateAgainstEventProperties} from '../sanitization/sanitization';
import {Sanitizer} from '../sanitization/security'; import {Sanitizer} from '../sanitization/security';
import {StyleSanitizeFn} from '../sanitization/style_sanitizer'; import {StyleSanitizeFn} from '../sanitization/style_sanitizer';
import {assertDataInRange, assertDefined, assertEqual, assertLessThan, assertNotEqual} from '../util/assert'; 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 {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 {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 {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 {NO_CHANGE} from './tokens';
import {INTERPOLATION_DELIMITER, findComponentView, getComponentViewByIndex, getNativeByIndex, getNativeByTNode, getRootContext, getRootView, getTNode, isComponent, isComponentDef, isContentQueryHost, loadInternal, readElementValue, readPatchedLView, renderStringify} from './util'; 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, index: number, name: string, value: any, sanitizer?: SanitizerFn | null,
namespace?: string): void { namespace?: string): void {
if (value !== NO_CHANGE) { if (value !== NO_CHANGE) {
ngDevMode && validateAttribute(name); ngDevMode && validateAgainstEventAttributes(name);
const lView = getLView(); const lView = getLView();
const renderer = lView[RENDERER]; const renderer = lView[RENDERER];
const element = getNativeByIndex(index, lView); const element = getNativeByIndex(index, lView);
@ -1193,7 +1193,8 @@ function elementPropertyInternal<T>(
} }
} else if (tNode.type === TNodeType.Element) { } else if (tNode.type === TNodeType.Element) {
if (ngDevMode) { if (ngDevMode) {
validateProperty(propName); validateAgainstEventProperties(propName);
validateAgainstUnknownProperties(element, propName, tNode);
ngDevMode.rendererSetProperty++; ngDevMode.rendererSetProperty++;
} }
@ -1212,6 +1213,18 @@ function elementPropertyInternal<T>(
} }
} }
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. * Stores debugging data for this property binding on first template pass.
* This enables features like DebugElement.properties. * This enables features like DebugElement.properties.

View File

@ -20,7 +20,7 @@ import {getTNode} from '../util';
import {CorePlayerHandler} from './core_player_handler'; import {CorePlayerHandler} from './core_player_handler';
const ANIMATION_PROP_PREFIX = '@'; export const ANIMATION_PROP_PREFIX = '@';
export function createEmptyStylingContext( export function createEmptyStylingContext(
element?: RElement | null, sanitizer?: StyleSanitizeFn | null, element?: RElement | null, sanitizer?: StyleSanitizeFn | null,

View File

@ -178,7 +178,7 @@ export const defaultStyleSanitizer = (function(prop: string, value?: string): st
return sanitizeStyle(value); return sanitizeStyle(value);
} as StyleSanitizeFn); } as StyleSanitizeFn);
export function validateProperty(name: string) { export function validateAgainstEventProperties(name: string) {
if (name.toLowerCase().startsWith('on')) { if (name.toLowerCase().startsWith('on')) {
const msg = `Binding to event property '${name}' is disallowed for security reasons, ` + const msg = `Binding to event property '${name}' is disallowed for security reasons, ` +
`please use (${name.slice(2)})=...` + `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')) { if (name.toLowerCase().startsWith('on')) {
const msg = `Binding to event attribute '${name}' is disallowed for security reasons, ` + const msg = `Binding to event attribute '${name}' is disallowed for security reasons, ` +
`please use (${name.slice(2)})=...`; `please use (${name.slice(2)})=...`;

View File

@ -6,8 +6,8 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {DomElementSchemaRegistry, ElementSchemaRegistry, ResourceLoader, UrlResolver} from '@angular/compiler'; import {ResourceLoader, UrlResolver} from '@angular/compiler';
import {MockResourceLoader, MockSchemaRegistry} from '@angular/compiler/testing'; 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 {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 {ComponentFixture, TestBed, fakeAsync} from '@angular/core/testing';
import {By} from '@angular/platform-browser/src/dom/debug/by'; import {By} from '@angular/platform-browser/src/dom/debug/by';
@ -20,14 +20,12 @@ export function createUrlResolverWithoutPackagePrefix(): UrlResolver {
} }
const TEST_COMPILER_PROVIDERS: Provider[] = [ const TEST_COMPILER_PROVIDERS: Provider[] = [
{provide: ElementSchemaRegistry, useValue: new MockSchemaRegistry({}, {}, {}, [], [])},
{provide: ResourceLoader, useClass: MockResourceLoader, deps: []}, {provide: ResourceLoader, useClass: MockResourceLoader, deps: []},
{provide: UrlResolver, useFactory: createUrlResolverWithoutPackagePrefix, deps: []} {provide: UrlResolver, useFactory: createUrlResolverWithoutPackagePrefix, deps: []}
]; ];
(function() { (function() {
let elSchema: MockSchemaRegistry;
let renderLog: RenderLog; let renderLog: RenderLog;
let directiveLog: DirectiveLog; let directiveLog: DirectiveLog;
@ -43,10 +41,8 @@ const TEST_COMPILER_PROVIDERS: Provider[] = [
} }
function initHelpers(): void { function initHelpers(): void {
elSchema = TestBed.get(ElementSchemaRegistry);
renderLog = TestBed.get(RenderLog); renderLog = TestBed.get(RenderLog);
directiveLog = TestBed.get(DirectiveLog); directiveLog = TestBed.get(DirectiveLog);
elSchema.existingProperties['someProp'] = true;
patchLoggingRenderer2(TestBed.get(RendererFactory2), renderLog); patchLoggingRenderer2(TestBed.get(RendererFactory2), renderLog);
} }
@ -67,7 +63,7 @@ const TEST_COMPILER_PROVIDERS: Provider[] = [
function _bindSimpleValue<T>(expression: any, compType: Type<T>): ComponentFixture<T>; function _bindSimpleValue<T>(expression: any, compType: Type<T>): ComponentFixture<T>;
function _bindSimpleValue<T>( function _bindSimpleValue<T>(
expression: any, compType: Type<T> = <any>TestComponent): ComponentFixture<T> { expression: any, compType: Type<T> = <any>TestComponent): ComponentFixture<T> {
return _bindSimpleProp(`[someProp]='${expression}'`, compType); return _bindSimpleProp(`[id]='${expression}'`, compType);
} }
function _bindAndCheckSimpleValue( function _bindAndCheckSimpleValue(
@ -117,145 +113,125 @@ const TEST_COMPILER_PROVIDERS: Provider[] = [
describe('expressions', () => { describe('expressions', () => {
it('should support literals', it('should support literals',
fakeAsync(() => { expect(_bindAndCheckSimpleValue(10)).toEqual(['someProp=10']); })); fakeAsync(() => { expect(_bindAndCheckSimpleValue(10)).toEqual(['id=10']); }));
it('should strip quotes from literals', 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(() => { it('should support newlines in literals',
expect(_bindAndCheckSimpleValue('"a\n\nb"')).toEqual(['someProp=a\n\nb']); fakeAsync(() => { expect(_bindAndCheckSimpleValue('"a\n\nb"')).toEqual(['id=a\n\nb']); }));
}));
it('should support + operations', it('should support + operations',
fakeAsync(() => { expect(_bindAndCheckSimpleValue('10 + 2')).toEqual(['someProp=12']); })); fakeAsync(() => { expect(_bindAndCheckSimpleValue('10 + 2')).toEqual(['id=12']); }));
it('should support - operations', it('should support - operations',
fakeAsync(() => { expect(_bindAndCheckSimpleValue('10 - 2')).toEqual(['someProp=8']); })); fakeAsync(() => { expect(_bindAndCheckSimpleValue('10 - 2')).toEqual(['id=8']); }));
it('should support * operations', 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(() => { 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 })); // dart exp=5.0, js exp=5
it('should support % operations', 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(() => { it('should support == operations on identical',
expect(_bindAndCheckSimpleValue('1 == 1')).toEqual(['someProp=true']); fakeAsync(() => { expect(_bindAndCheckSimpleValue('1 == 1')).toEqual(['id=true']); }));
}));
it('should support != operations', fakeAsync(() => { it('should support != operations',
expect(_bindAndCheckSimpleValue('1 != 1')).toEqual(['someProp=false']); fakeAsync(() => { expect(_bindAndCheckSimpleValue('1 != 1')).toEqual(['id=false']); }));
}));
it('should support == operations on coerceible', fakeAsync(() => { it('should support == operations on coerceible',
expect(_bindAndCheckSimpleValue('1 == true')).toEqual([`someProp=true`]); fakeAsync(() => { expect(_bindAndCheckSimpleValue('1 == true')).toEqual([`id=true`]); }));
}));
it('should support === operations on identical', fakeAsync(() => { it('should support === operations on identical',
expect(_bindAndCheckSimpleValue('1 === 1')).toEqual(['someProp=true']); fakeAsync(() => { expect(_bindAndCheckSimpleValue('1 === 1')).toEqual(['id=true']); }));
}));
it('should support !== operations', fakeAsync(() => { it('should support !== operations',
expect(_bindAndCheckSimpleValue('1 !== 1')).toEqual(['someProp=false']); fakeAsync(() => { expect(_bindAndCheckSimpleValue('1 !== 1')).toEqual(['id=false']); }));
}));
it('should support === operations on coerceible', fakeAsync(() => { 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(() => { it('should support true < operations',
expect(_bindAndCheckSimpleValue('1 < 2')).toEqual(['someProp=true']); fakeAsync(() => { expect(_bindAndCheckSimpleValue('1 < 2')).toEqual(['id=true']); }));
}));
it('should support false < operations', fakeAsync(() => { it('should support false < operations',
expect(_bindAndCheckSimpleValue('2 < 1')).toEqual(['someProp=false']); fakeAsync(() => { expect(_bindAndCheckSimpleValue('2 < 1')).toEqual(['id=false']); }));
}));
it('should support false > operations', fakeAsync(() => { it('should support false > operations',
expect(_bindAndCheckSimpleValue('1 > 2')).toEqual(['someProp=false']); fakeAsync(() => { expect(_bindAndCheckSimpleValue('1 > 2')).toEqual(['id=false']); }));
}));
it('should support true > operations', fakeAsync(() => { it('should support true > operations',
expect(_bindAndCheckSimpleValue('2 > 1')).toEqual(['someProp=true']); fakeAsync(() => { expect(_bindAndCheckSimpleValue('2 > 1')).toEqual(['id=true']); }));
}));
it('should support true <= operations', fakeAsync(() => { it('should support true <= operations',
expect(_bindAndCheckSimpleValue('1 <= 2')).toEqual(['someProp=true']); fakeAsync(() => { expect(_bindAndCheckSimpleValue('1 <= 2')).toEqual(['id=true']); }));
}));
it('should support equal <= operations', fakeAsync(() => { it('should support equal <= operations',
expect(_bindAndCheckSimpleValue('2 <= 2')).toEqual(['someProp=true']); fakeAsync(() => { expect(_bindAndCheckSimpleValue('2 <= 2')).toEqual(['id=true']); }));
}));
it('should support false <= operations', fakeAsync(() => { it('should support false <= operations',
expect(_bindAndCheckSimpleValue('2 <= 1')).toEqual(['someProp=false']); fakeAsync(() => { expect(_bindAndCheckSimpleValue('2 <= 1')).toEqual(['id=false']); }));
}));
it('should support true >= operations', fakeAsync(() => { it('should support true >= operations',
expect(_bindAndCheckSimpleValue('2 >= 1')).toEqual(['someProp=true']); fakeAsync(() => { expect(_bindAndCheckSimpleValue('2 >= 1')).toEqual(['id=true']); }));
}));
it('should support equal >= operations', fakeAsync(() => { it('should support equal >= operations',
expect(_bindAndCheckSimpleValue('2 >= 2')).toEqual(['someProp=true']); fakeAsync(() => { expect(_bindAndCheckSimpleValue('2 >= 2')).toEqual(['id=true']); }));
}));
it('should support false >= operations', fakeAsync(() => { it('should support false >= operations',
expect(_bindAndCheckSimpleValue('1 >= 2')).toEqual(['someProp=false']); fakeAsync(() => { expect(_bindAndCheckSimpleValue('1 >= 2')).toEqual(['id=false']); }));
}));
it('should support true && operations', fakeAsync(() => { 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(() => { 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(() => { 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(() => { 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(() => { it('should support negate',
expect(_bindAndCheckSimpleValue('!true')).toEqual(['someProp=false']); fakeAsync(() => { expect(_bindAndCheckSimpleValue('!true')).toEqual(['id=false']); }));
}));
it('should support double negate', fakeAsync(() => { it('should support double negate',
expect(_bindAndCheckSimpleValue('!!true')).toEqual(['someProp=true']); fakeAsync(() => { expect(_bindAndCheckSimpleValue('!!true')).toEqual(['id=true']); }));
}));
it('should support true conditionals', fakeAsync(() => { it('should support true conditionals',
expect(_bindAndCheckSimpleValue('1 < 2 ? 1 : 2')).toEqual(['someProp=1']); fakeAsync(() => { expect(_bindAndCheckSimpleValue('1 < 2 ? 1 : 2')).toEqual(['id=1']); }));
}));
it('should support false conditionals', fakeAsync(() => { it('should support false conditionals',
expect(_bindAndCheckSimpleValue('1 > 2 ? 1 : 2')).toEqual(['someProp=2']); fakeAsync(() => { expect(_bindAndCheckSimpleValue('1 > 2 ? 1 : 2')).toEqual(['id=2']); }));
}));
it('should support keyed access to a list item', fakeAsync(() => { 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(() => { 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', it('should report all changes on the first run including uninitialized values',
fakeAsync(() => { 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(() => { it('should report all changes on the first run including null values', fakeAsync(() => {
const ctx = _bindSimpleValue('a', TestData); const ctx = _bindSimpleValue('a', TestData);
ctx.componentInstance.a = null; ctx.componentInstance.a = null;
ctx.detectChanges(false); ctx.detectChanges(false);
expect(renderLog.log).toEqual(['someProp=null']); expect(renderLog.log).toEqual(['id=null']);
})); }));
it('should support simple chained property access', fakeAsync(() => { it('should support simple chained property access', fakeAsync(() => {
@ -263,7 +239,7 @@ const TEST_COMPILER_PROVIDERS: Provider[] = [
ctx.componentInstance.name = 'Victor'; ctx.componentInstance.name = 'Victor';
ctx.componentInstance.address = new Address('Grenoble'); ctx.componentInstance.address = new Address('Grenoble');
ctx.detectChanges(false); ctx.detectChanges(false);
expect(renderLog.log).toEqual(['someProp=Grenoble']); expect(renderLog.log).toEqual(['id=Grenoble']);
})); }));
describe('safe navigation operator', () => { describe('safe navigation operator', () => {
@ -271,54 +247,54 @@ const TEST_COMPILER_PROVIDERS: Provider[] = [
const ctx = _bindSimpleValue('address?.city', Person); const ctx = _bindSimpleValue('address?.city', Person);
ctx.componentInstance.address = null !; ctx.componentInstance.address = null !;
ctx.detectChanges(false); ctx.detectChanges(false);
expect(renderLog.log).toEqual(['someProp=null']); expect(renderLog.log).toEqual(['id=null']);
})); }));
it('should support calling methods on nulls', fakeAsync(() => { it('should support calling methods on nulls', fakeAsync(() => {
const ctx = _bindSimpleValue('address?.toString()', Person); const ctx = _bindSimpleValue('address?.toString()', Person);
ctx.componentInstance.address = null !; ctx.componentInstance.address = null !;
ctx.detectChanges(false); ctx.detectChanges(false);
expect(renderLog.log).toEqual(['someProp=null']); expect(renderLog.log).toEqual(['id=null']);
})); }));
it('should support reading properties on non nulls', fakeAsync(() => { it('should support reading properties on non nulls', fakeAsync(() => {
const ctx = _bindSimpleValue('address?.city', Person); const ctx = _bindSimpleValue('address?.city', Person);
ctx.componentInstance.address = new Address('MTV'); ctx.componentInstance.address = new Address('MTV');
ctx.detectChanges(false); ctx.detectChanges(false);
expect(renderLog.log).toEqual(['someProp=MTV']); expect(renderLog.log).toEqual(['id=MTV']);
})); }));
it('should support calling methods on non nulls', fakeAsync(() => { it('should support calling methods on non nulls', fakeAsync(() => {
const ctx = _bindSimpleValue('address?.toString()', Person); const ctx = _bindSimpleValue('address?.toString()', Person);
ctx.componentInstance.address = new Address('MTV'); ctx.componentInstance.address = new Address('MTV');
ctx.detectChanges(false); ctx.detectChanges(false);
expect(renderLog.log).toEqual(['someProp=MTV']); expect(renderLog.log).toEqual(['id=MTV']);
})); }));
it('should support short-circuting safe navigation', fakeAsync(() => { it('should support short-circuting safe navigation', fakeAsync(() => {
const ctx = _bindSimpleValue('value?.address.city', PersonHolder); const ctx = _bindSimpleValue('value?.address.city', PersonHolder);
ctx.componentInstance.value = null !; ctx.componentInstance.value = null !;
ctx.detectChanges(false); ctx.detectChanges(false);
expect(renderLog.log).toEqual(['someProp=null']); expect(renderLog.log).toEqual(['id=null']);
})); }));
it('should support nested short-circuting safe navigation', fakeAsync(() => { it('should support nested short-circuting safe navigation', fakeAsync(() => {
const ctx = _bindSimpleValue('value.value?.address.city', PersonHolderHolder); const ctx = _bindSimpleValue('value.value?.address.city', PersonHolderHolder);
ctx.componentInstance.value = new PersonHolder(); ctx.componentInstance.value = new PersonHolder();
ctx.detectChanges(false); ctx.detectChanges(false);
expect(renderLog.log).toEqual(['someProp=null']); expect(renderLog.log).toEqual(['id=null']);
})); }));
it('should support chained short-circuting safe navigation', fakeAsync(() => { it('should support chained short-circuting safe navigation', fakeAsync(() => {
const ctx = _bindSimpleValue('value?.value?.address.city', PersonHolderHolder); const ctx = _bindSimpleValue('value?.value?.address.city', PersonHolderHolder);
ctx.detectChanges(false); ctx.detectChanges(false);
expect(renderLog.log).toEqual(['someProp=null']); expect(renderLog.log).toEqual(['id=null']);
})); }));
it('should support short-circuting array index operations', fakeAsync(() => { it('should support short-circuting array index operations', fakeAsync(() => {
const ctx = _bindSimpleValue('value?.phones[0]', PersonHolder); const ctx = _bindSimpleValue('value?.phones[0]', PersonHolder);
ctx.detectChanges(false); 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(() => { 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(() => { it('should support method calls', fakeAsync(() => {
const ctx = _bindSimpleValue('sayHi("Jim")', Person); const ctx = _bindSimpleValue('sayHi("Jim")', Person);
ctx.detectChanges(false); ctx.detectChanges(false);
expect(renderLog.log).toEqual(['someProp=Hi, Jim']); expect(renderLog.log).toEqual(['id=Hi, Jim']);
})); }));
it('should support function calls', fakeAsync(() => { it('should support function calls', fakeAsync(() => {
const ctx = _bindSimpleValue('a()(99)', TestData); const ctx = _bindSimpleValue('a()(99)', TestData);
ctx.componentInstance.a = () => (a: any) => a; ctx.componentInstance.a = () => (a: any) => a;
ctx.detectChanges(false); ctx.detectChanges(false);
expect(renderLog.log).toEqual(['someProp=99']); expect(renderLog.log).toEqual(['id=99']);
})); }));
it('should support chained method calls', fakeAsync(() => { it('should support chained method calls', fakeAsync(() => {
const ctx = _bindSimpleValue('address.toString()', Person); const ctx = _bindSimpleValue('address.toString()', Person);
ctx.componentInstance.address = new Address('MTV'); ctx.componentInstance.address = new Address('MTV');
ctx.detectChanges(false); ctx.detectChanges(false);
expect(renderLog.log).toEqual(['someProp=MTV']); expect(renderLog.log).toEqual(['id=MTV']);
})); }));
it('should support NaN', fakeAsync(() => { it('should support NaN', fakeAsync(() => {
@ -357,7 +333,7 @@ const TEST_COMPILER_PROVIDERS: Provider[] = [
ctx.componentInstance.age = NaN; ctx.componentInstance.age = NaN;
ctx.detectChanges(false); ctx.detectChanges(false);
expect(renderLog.log).toEqual(['someProp=NaN']); expect(renderLog.log).toEqual(['id=NaN']);
renderLog.clear(); renderLog.clear();
ctx.detectChanges(false); ctx.detectChanges(false);
@ -369,7 +345,7 @@ const TEST_COMPILER_PROVIDERS: Provider[] = [
ctx.componentInstance.name = 'misko'; ctx.componentInstance.name = 'misko';
ctx.detectChanges(false); ctx.detectChanges(false);
expect(renderLog.log).toEqual(['someProp=misko']); expect(renderLog.log).toEqual(['id=misko']);
renderLog.clear(); renderLog.clear();
ctx.detectChanges(false); ctx.detectChanges(false);
@ -378,7 +354,7 @@ const TEST_COMPILER_PROVIDERS: Provider[] = [
ctx.componentInstance.name = 'Misko'; ctx.componentInstance.name = 'Misko';
ctx.detectChanges(false); ctx.detectChanges(false);
expect(renderLog.log).toEqual(['someProp=Misko']); expect(renderLog.log).toEqual(['id=Misko']);
})); }));
it('should support literal array made of literals', fakeAsync(() => { it('should support literal array made of literals', fakeAsync(() => {
@ -445,7 +421,7 @@ const TEST_COMPILER_PROVIDERS: Provider[] = [
it('should ignore empty bindings', fakeAsync(() => { it('should ignore empty bindings', fakeAsync(() => {
const ctx = _bindSimpleProp('[someProp]', TestData); const ctx = _bindSimpleProp('[id]', TestData);
ctx.componentInstance.a = 'value'; ctx.componentInstance.a = 'value';
ctx.detectChanges(false); ctx.detectChanges(false);
@ -453,23 +429,23 @@ const TEST_COMPILER_PROVIDERS: Provider[] = [
})); }));
it('should support interpolation', fakeAsync(() => { 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.componentInstance.a = 'value';
ctx.detectChanges(false); 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(() => { 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.componentInstance.a = null;
ctx.detectChanges(false); ctx.detectChanges(false);
expect(renderLog.log).toEqual(['someProp=BA']); expect(renderLog.log).toEqual(['id=BA']);
})); }));
it('should escape values in literals that indicate interpolation', it('should escape values in literals that indicate interpolation',
fakeAsync(() => { expect(_bindAndCheckSimpleValue('"$"')).toEqual(['someProp=$']); })); fakeAsync(() => { expect(_bindAndCheckSimpleValue('"$"')).toEqual(['id=$']); }));
it('should read locals', fakeAsync(() => { it('should read locals', fakeAsync(() => {
const ctx = createCompFixture( const ctx = createCompFixture(
@ -515,7 +491,7 @@ const TEST_COMPILER_PROVIDERS: Provider[] = [
ctx.detectChanges(false); ctx.detectChanges(false);
expect(renderLog.log).toEqual(['someProp=Megatron']); expect(renderLog.log).toEqual(['id=Megatron']);
renderLog.clear(); renderLog.clear();
ctx.detectChanges(false); ctx.detectChanges(false);
@ -528,12 +504,12 @@ const TEST_COMPILER_PROVIDERS: Provider[] = [
ctx.detectChanges(false); ctx.detectChanges(false);
expect(renderLog.log).toEqual(['someProp=Megatron']); expect(renderLog.log).toEqual(['id=Megatron']);
renderLog.clear(); renderLog.clear();
ctx.detectChanges(false); ctx.detectChanges(false);
expect(renderLog.log).toEqual(['someProp=Megatron']); expect(renderLog.log).toEqual(['id=Megatron']);
})); }));
it('should record unwrapped values via ngOnChanges', fakeAsync(() => { 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', .it('should call pure pipes that are used multiple times only when the arguments change and share state between pipe instances',
fakeAsync(() => { fakeAsync(() => {
const ctx = createCompFixture( const ctx = createCompFixture(
`<div [someProp]="name | countingPipe"></div><div [someProp]="age | countingPipe"></div>` + `<div [id]="name | countingPipe"></div><div [id]="age | countingPipe"></div>` +
'<div *ngFor="let x of [1,2]" [someProp]="address.city | countingPipe"></div>', '<div *ngFor="let x of [1,2]" [id]="address.city | countingPipe"></div>',
Person); Person);
ctx.componentInstance.name = 'a'; ctx.componentInstance.name = 'a';
ctx.componentInstance.age = 10; 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', it('should call pure pipes that are used multiple times only when the arguments change',
fakeAsync(() => { fakeAsync(() => {
const ctx = createCompFixture( const ctx = createCompFixture(
`<div [someProp]="name | countingPipe"></div><div [someProp]="age | countingPipe"></div>` + `<div [id]="name | countingPipe"></div><div [id]="age | countingPipe"></div>` +
'<div *ngFor="let x of [1,2]" [someProp]="address.city | countingPipe"></div>', '<div *ngFor="let x of [1,2]" [id]="address.city | countingPipe"></div>',
Person); Person);
ctx.componentInstance.name = 'a'; ctx.componentInstance.name = 'a';
ctx.componentInstance.age = 10; ctx.componentInstance.age = 10;
@ -731,7 +707,7 @@ const TEST_COMPILER_PROVIDERS: Provider[] = [
describe('reading directives', () => { describe('reading directives', () => {
it('should read directive properties', fakeAsync(() => { it('should read directive properties', fakeAsync(() => {
const ctx = createCompFixture( const ctx = createCompFixture(
'<div testDirective [a]="42" ref-dir="testDirective" [someProp]="dir.a"></div>'); '<div testDirective [a]="42" ref-dir="testDirective" [id]="dir.a"></div>');
ctx.detectChanges(false); ctx.detectChanges(false);
expect(renderLog.loggedValues).toEqual([42]); expect(renderLog.loggedValues).toEqual([42]);
})); }));
@ -1191,7 +1167,7 @@ const TEST_COMPILER_PROVIDERS: Provider[] = [
TestBed.configureTestingModule({declarations: [ChangingDirective]}); TestBed.configureTestingModule({declarations: [ChangingDirective]});
const ctx = createCompFixture('<div [someProp]="a" [changed]="b"></div>', TestData); const ctx = createCompFixture('<div [id]="a" [changed]="b"></div>', TestData);
ctx.componentInstance.b = 1; ctx.componentInstance.b = 1;
const errMsgRegExp = ivyEnabled ? const errMsgRegExp = ivyEnabled ?
@ -1210,7 +1186,7 @@ const TEST_COMPILER_PROVIDERS: Provider[] = [
TestBed.configureTestingModule({declarations: [ChangingDirective]}); TestBed.configureTestingModule({declarations: [ChangingDirective]});
const ctx = createCompFixture('<div [someProp]="a" [changed]="b"></div>', TestData); const ctx = createCompFixture('<div [id]="a" [changed]="b"></div>', TestData);
ctx.componentInstance.b = 1; ctx.componentInstance.b = 1;
ctx.detectChanges(); ctx.detectChanges();
@ -1532,13 +1508,7 @@ const TEST_COMPILER_PROVIDERS: Provider[] = [
} }
const ctx = const ctx =
TestBed TestBed.configureTestingModule({declarations: [Comp, SomeDir]}).createComponent(Comp);
.configureCompiler({
providers:
[{provide: ElementSchemaRegistry, useExisting: DomElementSchemaRegistry}]
})
.configureTestingModule({declarations: [Comp, SomeDir]})
.createComponent(Comp);
ctx.detectChanges(); ctx.detectChanges();

View File

@ -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 {DOCUMENT} from '@angular/platform-browser/src/dom/dom_tokens';
import {dispatchEvent, el} from '@angular/platform-browser/testing/src/browser_util'; import {dispatchEvent, el} from '@angular/platform-browser/testing/src/browser_util';
import {expect} from '@angular/platform-browser/testing/src/matchers'; 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'; import {stringify} from '../../src/util/stringify';
@ -1592,7 +1592,7 @@ function declareTests(config?: {useJit: boolean}) {
}); });
describe('Property bindings', () => { 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', () => { .it('should throw on bindings to unknown properties', () => {
TestBed.configureTestingModule({declarations: [MyComp]}); TestBed.configureTestingModule({declarations: [MyComp]});
const template = '<div unknown="{{ctxProp}}"></div>'; const template = '<div unknown="{{ctxProp}}"></div>';
@ -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 = '<div unknown="{{ctxProp}}"></div>';
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', it('should not throw for property binding to a non-existing property when there is a matching directive property',
() => { () => {
TestBed.configureTestingModule({declarations: [MyComp, MyDir]}); TestBed.configureTestingModule({declarations: [MyComp, MyDir]});

View File

@ -239,7 +239,7 @@ function declareTests(config?: {useJit: boolean}) {
}); });
describe('schemas', () => { 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', () => { .it('should error on unknown bound properties on custom elements by default', () => {
@Component({template: '<some-element [someUnknownProp]="true"></some-element>'}) @Component({template: '<some-element [someUnknownProp]="true"></some-element>'})
class ComponentUsingInvalidProperty { class ComponentUsingInvalidProperty {
@ -252,19 +252,36 @@ function declareTests(config?: {useJit: boolean}) {
expect(() => createModule(SomeModule)).toThrowError(/Can't bind to 'someUnknownProp'/); 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', 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: '<some-element [someUnknownProp]="true"></some-element>'}) @Component({template: '<some-element [someUnknownProp]="true"></some-element>'})
class ComponentUsingInvalidProperty { class ComponentUsingInvalidProperty {
} }
@NgModule( @NgModule({declarations: [ComponentUsingInvalidProperty]})
{schemas: [CUSTOM_ELEMENTS_SCHEMA], declarations: [ComponentUsingInvalidProperty]}) class SomeModule {
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: '<some-element [someUnknownProp]="true"></some-element>'})
class ComponentUsingInvalidProperty {
}
@NgModule({
schemas: [CUSTOM_ELEMENTS_SCHEMA],
declarations: [ComponentUsingInvalidProperty]
})
class SomeModule {
}
expect(() => createModule(SomeModule)).not.toThrow();
});
}); });
describe('id', () => { describe('id', () => {

View File

@ -255,7 +255,7 @@ function declareTests(config?: {useJit: boolean}) {
expect(getDOM().getStyle(e, 'background')).not.toContain('javascript'); 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', () => { .it('should escape unsafe SVG attributes', () => {
const template = `<svg:circle [xlink:href]="ctxProp">Text</svg:circle>`; const template = `<svg:circle [xlink:href]="ctxProp">Text</svg:circle>`;
TestBed.overrideComponent(SecuredComponent, {set: {template}}); TestBed.overrideComponent(SecuredComponent, {set: {template}});
@ -264,6 +264,15 @@ function declareTests(config?: {useJit: boolean}) {
.toThrowError(/Can't bind to 'xlink:href'/); .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 = `<svg:circle [xlink:href]="ctxProp">Text</svg:circle>`;
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', () => { it('should escape unsafe HTML values', () => {
const template = `<div [innerHTML]="ctxProp">Text</div>`; const template = `<div [innerHTML]="ctxProp">Text</div>`;
TestBed.overrideComponent(SecuredComponent, {set: {template}}); TestBed.overrideComponent(SecuredComponent, {set: {template}});

View File

@ -182,11 +182,12 @@ describe('instructions', () => {
}); });
it('should not stringify non string values', () => { 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)); // Note: don't use 'hidden' here because IE10 does not support the hidden property
// The hidden property would be true if `false` was stringified into `"false"`. t.update(() => elementProperty(0, 'required', false));
expect((t.hostElement as HTMLElement).querySelector('div') !.hidden).toEqual(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({ expect(ngDevMode).toHaveProperties({
firstTemplatePass: 1, firstTemplatePass: 1,
tNode: 2, // 1 for div, 1 for host element tNode: 2, // 1 for div, 1 for host element

View File

@ -443,7 +443,8 @@ describe('lifecycles', () => {
factory: () => new Component(), template, factory: () => new Component(), template,
consts: consts, consts: consts,
vars: vars, vars: vars,
directives: directives directives: directives,
inputs: {val: 'val'}
}); });
}; };
} }
@ -2665,10 +2666,10 @@ describe('lifecycles', () => {
element(0, 'div'); element(0, 'div');
} }
if (rf & RenderFlags.Update) { if (rf & RenderFlags.Update) {
elementProperty(0, 'data-a', bind(ctx.a)); elementProperty(0, 'id', bind(ctx.a));
} }
}, },
selectors: [['mycomp']], selectors: [['my-comp']],
inputs: { inputs: {
value: 'value', value: 'value',
}, },
@ -2683,7 +2684,7 @@ describe('lifecycles', () => {
const App = createComponent('app', function(rf: RenderFlags, ctx: any) { const App = createComponent('app', function(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) { if (rf & RenderFlags.Create) {
element(0, 'mycomp'); element(0, 'my-comp');
} }
if (rf & RenderFlags.Update) { if (rf & RenderFlags.Update) {
elementProperty(0, 'value', bind(1)); elementProperty(0, 'value', bind(1));

View File

@ -170,12 +170,12 @@ describe('pipe', () => {
elementEnd(); elementEnd();
} }
if (rf & RenderFlags.Update) { 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); renderToHtml(Template, person, 2, 3, null, [IdentityPipe], rendererFactory2);
expect(renderLog.log).toEqual(['someProp=Megatron']); expect(renderLog.log).toEqual(['id=Megatron']);
renderLog.clear(); renderLog.clear();
renderToHtml(Template, person, 2, 3, null, pipes, rendererFactory2); renderToHtml(Template, person, 2, 3, null, pipes, rendererFactory2);
@ -255,8 +255,8 @@ describe('pipe', () => {
container(4); container(4);
} }
if (rf & RenderFlags.Update) { if (rf & RenderFlags.Update) {
elementProperty(0, 'someProp', bind(pipeBind1(1, 2, true))); elementProperty(0, 'id', bind(pipeBind1(1, 2, true)));
elementProperty(2, 'someProp', bind(pipeBind1(3, 4, true))); elementProperty(2, 'id', bind(pipeBind1(3, 4, true)));
pipeInstances.push(load<CountingImpurePipe>(1), load(3)); pipeInstances.push(load<CountingImpurePipe>(1), load(3));
containerRefreshStart(4); containerRefreshStart(4);
{ {
@ -269,7 +269,7 @@ describe('pipe', () => {
elementEnd(); elementEnd();
} }
if (rf1 & RenderFlags.Update) { if (rf1 & RenderFlags.Update) {
elementProperty(0, 'someProp', bind(pipeBind1(1, 1, true))); elementProperty(0, 'id', bind(pipeBind1(1, 1, true)));
pipeInstances.push(load<CountingImpurePipe>(1)); pipeInstances.push(load<CountingImpurePipe>(1));
} }
} }

View File

@ -42,12 +42,14 @@ describe('ViewContainerRef', () => {
factory: () => directiveInstance = new DirectiveWithVCRef( factory: () => directiveInstance = new DirectiveWithVCRef(
directiveInject(ViewContainerRef as any), injectComponentFactoryResolver()), directiveInject(ViewContainerRef as any), injectComponentFactoryResolver()),
inputs: {tplRef: 'tplRef'} inputs: {tplRef: 'tplRef', name: 'name'}
}); });
// TODO(issue/24571): remove '!'. // TODO(issue/24571): remove '!'.
tplRef !: TemplateRef<{}>; tplRef !: TemplateRef<{}>;
name: string = '';
// injecting a ViewContainerRef to create a dynamic container in which embedded views will be // injecting a ViewContainerRef to create a dynamic container in which embedded views will be
// created // created
constructor(public vcref: ViewContainerRef, public cfr: ComponentFactoryResolver) {} constructor(public vcref: ViewContainerRef, public cfr: ComponentFactoryResolver) {}

View File

@ -17,7 +17,7 @@ import {MockBackend, MockConnection} from '@angular/http/testing';
import {BrowserModule, DOCUMENT, Title, TransferState, makeStateKey} from '@angular/platform-browser'; import {BrowserModule, DOCUMENT, Title, TransferState, makeStateKey} from '@angular/platform-browser';
import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter'; 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 {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 {Observable} from 'rxjs';
import {first} from 'rxjs/operators'; import {first} from 'rxjs/operators';
@ -99,7 +99,7 @@ class TitleApp {
class TitleAppModule { class TitleAppModule {
} }
@Component({selector: 'app', template: '{{text}}<h1 [innerText]="h1"></h1>'}) @Component({selector: 'app', template: '{{text}}<h1 [textContent]="h1"></h1>'})
class MyAsyncServerApp { class MyAsyncServerApp {
text = ''; text = '';
h1 = ''; h1 = '';
@ -276,6 +276,19 @@ class MyHostComponent {
class FalseAttributesModule { class FalseAttributesModule {
} }
@Component({selector: 'app', template: '<div [innerText]="foo"></div>'})
class InnerTextComponent {
foo = 'Some text';
}
@NgModule({
declarations: [InnerTextComponent],
bootstrap: [InnerTextComponent],
imports: [ServerModule, BrowserModule.withServerTransition({appId: 'inner-text'})]
})
class InnerTextModule {
}
@Component({selector: 'app', template: '<input [name]="name">'}) @Component({selector: 'app', template: '<input [name]="name">'})
class MyInputComponent { class MyInputComponent {
@Input() @Input()
@ -528,7 +541,7 @@ class HiddenModule {
let doc: string; let doc: string;
let called: boolean; let called: boolean;
let expectedOutput = let expectedOutput =
'<html><head></head><body><app ng-version="0.0.0-PLACEHOLDER">Works!<h1 innertext="fine">fine</h1></app></body></html>'; '<html><head></head><body><app ng-version="0.0.0-PLACEHOLDER">Works!<h1 textcontent="fine">fine</h1></app></body></html>';
beforeEach(() => { beforeEach(() => {
// PlatformConfig takes in a parsed document so that it can be cached across requests. // 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(
'<html><head></head><body><app ng-version="0.0.0-PLACEHOLDER"><div innertext="Some text">Some text</div></app></body></html>');
called = true;
});
}));
it('using renderModuleFactory should work', it('using renderModuleFactory should work',
async(inject([PlatformRef], (defaultPlatform: PlatformRef) => { async(inject([PlatformRef], (defaultPlatform: PlatformRef) => {
const compilerFactory: CompilerFactory = const compilerFactory: CompilerFactory =

View File

@ -953,6 +953,58 @@ window.testBlocklist = {
"error": "Error: Expected undefined to be truthy.", "error": "Error: Expected undefined to be truthy.",
"notes": "Unknown" "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": { "MatTabHeader focusing should initialize to the selected index": {
"error": "TypeError: Cannot read property 'nativeElement' of undefined", "error": "TypeError: Cannot read property 'nativeElement' of undefined",
"notes": "FW-1019: Design new API to replace static queries" "notes": "FW-1019: Design new API to replace static queries"