fix(ivy): constant object literals shared across element and component instances (#33705)
Currently if a consumer does something like the following, the object literal will be shared across the two elements and any instances of the component template. The same applies to array literals: ``` <div [someDirective]="{}"></div> <div [someDirective]="{}"></div> ``` These changes make it so that we generate a pure function even if an object is constant so that each instance gets its own object. Note that the original design for this fix included moving the pure function factories into the `consts` array. In the process of doing so I realized that pure function are also used inside of directive host bindings which means that we don't have access to the `consts`. These changes also: * Fix an issue that meant that the `pureFunction0` instruction could only be run during creation mode. * Make the `getConstant` utility slightly more convenient to use. This isn't strictly required for these changes to work, but I had made it as a part of a larger refactor that I ended up reverting. PR Close #33705
This commit is contained in:
parent
9a68f23dd2
commit
fcdada53f1
|
@ -365,10 +365,10 @@ describe('compiler compliance', () => {
|
||||||
template: function MyComponent_Template(rf, ctx) {
|
template: function MyComponent_Template(rf, ctx) {
|
||||||
if (rf & 1) {
|
if (rf & 1) {
|
||||||
$r3$.ɵɵelement(0, "div", 0);
|
$r3$.ɵɵelement(0, "div", 0);
|
||||||
$r3$.ɵɵpipe(1,"pipe");
|
$r3$.ɵɵpipe(1, "pipe");
|
||||||
}
|
}
|
||||||
if (rf & 2) {
|
if (rf & 2) {
|
||||||
$r3$.ɵɵproperty("ternary", ctx.cond ? $r3$.ɵɵpureFunction1(8, $c0$, ctx.a): $c1$)("pipe", $r3$.ɵɵpipeBind3(1, 4, ctx.value, 1, 2))("and", ctx.cond && $r3$.ɵɵpureFunction1(10, $c0$, ctx.b))("or", ctx.cond || $r3$.ɵɵpureFunction1(12, $c0$, ctx.c));
|
$r3$.ɵɵproperty("ternary", ctx.cond ? $r3$.ɵɵpureFunction1(8, $c0$, ctx.a): $r3$.ɵɵpureFunction0(10, $c1$))("pipe", $r3$.ɵɵpipeBind3(1, 4, ctx.value, 1, 2))("and", ctx.cond && $r3$.ɵɵpureFunction1(11, $c0$, ctx.b))("or", ctx.cond || $r3$.ɵɵpureFunction1(13, $c0$, ctx.c));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
@ -1082,25 +1082,24 @@ describe('compiler compliance', () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const MyAppDefinition = `
|
const MyAppDefinition = `
|
||||||
const $c0$ = {opacity: 0, duration: 0};
|
const $c0$ = function () { return {opacity: 0, duration: 0}; };
|
||||||
const $e0_ff$ = function ($v$) { return {opacity: 1, duration: $v$}; };
|
const $e0_ff$ = function ($v$) { return {opacity: 1, duration: $v$}; };
|
||||||
const $e0_ff_1$ = function ($v$) { return [$c0$, $v$]; };
|
const $e0_ff_1$ = function ($v1$, $v2$) { return [$v1$, $v2$]; };
|
||||||
const $e0_ff_2$ = function ($v1$, $v2$) { return {animation: $v1$, actions: $v2$}; };
|
const $e0_ff_2$ = function ($v1$, $v2$) { return {animation: $v1$, actions: $v2$}; };
|
||||||
…
|
…
|
||||||
MyApp.ɵcmp = $r3$.ɵɵdefineComponent({
|
MyApp.ɵcmp = $r3$.ɵɵdefineComponent({
|
||||||
type: MyApp,
|
type: MyApp,
|
||||||
selectors: [["my-app"]],
|
selectors: [["my-app"]],
|
||||||
decls: 1,
|
decls: 1,
|
||||||
vars: 8,
|
vars: 10,
|
||||||
consts: [[${AttributeMarker.Bindings}, "config"]],
|
consts: [[${AttributeMarker.Bindings}, "config"]],
|
||||||
template: function MyApp_Template(rf, ctx) {
|
template: function MyApp_Template(rf, ctx) {
|
||||||
if (rf & 1) {
|
if (rf & 1) {
|
||||||
$r3$.ɵɵelement(0, "nested-comp", 0);
|
$r3$.ɵɵelement(0, "nested-comp", 0);
|
||||||
}
|
}
|
||||||
if (rf & 2) {
|
if (rf & 2) {
|
||||||
$r3$.ɵɵproperty(
|
$r3$.ɵɵproperty("config",
|
||||||
"config",
|
$r3$.ɵɵpureFunction2(7, $e0_ff_2$, ctx.name, $r3$.ɵɵpureFunction2(4, $e0_ff_1$, $r3$.ɵɵpureFunction0(1, $c0$), $r3$.ɵɵpureFunction1(2, $e0_ff$, ctx.duration))));
|
||||||
$r3$.ɵɵpureFunction2(5, $e0_ff_2$, ctx.name, $r3$.ɵɵpureFunction1(3, $e0_ff_1$, $r3$.ɵɵpureFunction1(1, $e0_ff$, ctx.duration))));
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
directives: [NestedComp],
|
directives: [NestedComp],
|
||||||
|
@ -2975,6 +2974,88 @@ describe('compiler compliance', () => {
|
||||||
expectEmit(result.source, expectedOutput, 'Invalid directive definition');
|
expectEmit(result.source, expectedOutput, 'Invalid directive definition');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should generate a pure function for constant object literals', () => {
|
||||||
|
const files = {
|
||||||
|
app: {
|
||||||
|
'spec.ts': `
|
||||||
|
import {Component} from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
template: '<some-comp [prop]="{}" [otherProp]="{a: 1, b: 2}"></some-comp>'
|
||||||
|
})
|
||||||
|
export class MyApp {
|
||||||
|
}
|
||||||
|
`
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const MyAppDeclaration = `
|
||||||
|
const $c0$ = function () { return {}; };
|
||||||
|
const $c1$ = function () { return { a: 1, b: 2 }; };
|
||||||
|
…
|
||||||
|
MyApp.ɵcmp = $r3$.ɵɵdefineComponent({
|
||||||
|
type: MyApp,
|
||||||
|
selectors: [["ng-component"]],
|
||||||
|
decls: 1,
|
||||||
|
vars: 4,
|
||||||
|
consts: [[${AttributeMarker.Bindings}, "prop", "otherProp"]],
|
||||||
|
template: function MyApp_Template(rf, ctx) {
|
||||||
|
if (rf & 1) {
|
||||||
|
$r3$.ɵɵelement(0, "some-comp", 0);
|
||||||
|
}
|
||||||
|
if (rf & 2) {
|
||||||
|
$r3$.ɵɵproperty("prop", $r3$.ɵɵpureFunction0(2, $c0$))("otherProp", $r3$.ɵɵpureFunction0(3, $c1$));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
encapsulation: 2
|
||||||
|
});
|
||||||
|
`;
|
||||||
|
|
||||||
|
const result = compile(files, angularFiles);
|
||||||
|
expectEmit(result.source, MyAppDeclaration, 'Invalid component definition');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should generate a pure function for constant array literals', () => {
|
||||||
|
const files = {
|
||||||
|
app: {
|
||||||
|
'spec.ts': `
|
||||||
|
import {Component} from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
template: '<some-comp [prop]="[]" [otherProp]="[0, 1, 2]"></some-comp>'
|
||||||
|
})
|
||||||
|
export class MyApp {
|
||||||
|
}
|
||||||
|
`
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const MyAppDeclaration = `
|
||||||
|
const $c0$ = function () { return []; };
|
||||||
|
const $c1$ = function () { return [0, 1, 2]; };
|
||||||
|
…
|
||||||
|
MyApp.ɵcmp = $r3$.ɵɵdefineComponent({
|
||||||
|
type: MyApp,
|
||||||
|
selectors: [["ng-component"]],
|
||||||
|
decls: 1,
|
||||||
|
vars: 4,
|
||||||
|
consts: [[${AttributeMarker.Bindings}, "prop", "otherProp"]],
|
||||||
|
template: function MyApp_Template(rf, ctx) {
|
||||||
|
if (rf & 1) {
|
||||||
|
$r3$.ɵɵelement(0, "some-comp", 0);
|
||||||
|
}
|
||||||
|
if (rf & 2) {
|
||||||
|
$r3$.ɵɵproperty("prop", $r3$.ɵɵpureFunction0(2, $c0$))("otherProp", $r3$.ɵɵpureFunction0(3, $c1$));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
encapsulation: 2
|
||||||
|
});
|
||||||
|
`;
|
||||||
|
|
||||||
|
const result = compile(files, angularFiles);
|
||||||
|
expectEmit(result.source, MyAppDeclaration, 'Invalid component definition');
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('inherited base classes', () => {
|
describe('inherited base classes', () => {
|
||||||
|
|
|
@ -915,7 +915,7 @@ describe('compiler compliance: styling', () => {
|
||||||
if (rf & 2) {
|
if (rf & 2) {
|
||||||
$r3$.ɵɵstyleSanitizer($r3$.ɵɵdefaultStyleSanitizer);
|
$r3$.ɵɵstyleSanitizer($r3$.ɵɵdefaultStyleSanitizer);
|
||||||
$r3$.ɵɵstyleMap($r3$.ɵɵpipeBind2(1, 8, $ctx$.myStyleExp, 1000));
|
$r3$.ɵɵstyleMap($r3$.ɵɵpipeBind2(1, 8, $ctx$.myStyleExp, 1000));
|
||||||
$r3$.ɵɵclassMap($e2_styling$);
|
$r3$.ɵɵclassMap($r3$.ɵɵpureFunction0(20, _c0));
|
||||||
$r3$.ɵɵstyleProp("bar", $r3$.ɵɵpipeBind2(2, 11, $ctx$.barExp, 3000));
|
$r3$.ɵɵstyleProp("bar", $r3$.ɵɵpipeBind2(2, 11, $ctx$.barExp, 3000));
|
||||||
$r3$.ɵɵstyleProp("baz", $r3$.ɵɵpipeBind2(3, 14, $ctx$.bazExp, 4000));
|
$r3$.ɵɵstyleProp("baz", $r3$.ɵɵpipeBind2(3, 14, $ctx$.bazExp, 4000));
|
||||||
$r3$.ɵɵclassProp("foo", $r3$.ɵɵpipeBind2(4, 17, $ctx$.fooExp, 2000));
|
$r3$.ɵɵclassProp("foo", $r3$.ɵɵpipeBind2(4, 17, $ctx$.fooExp, 2000));
|
||||||
|
|
|
@ -1420,11 +1420,9 @@ export class ValueConverter extends AstMemoryEfficientTransformer {
|
||||||
array.span, array.sourceSpan, this.visitAll(array.expressions), values => {
|
array.span, array.sourceSpan, this.visitAll(array.expressions), values => {
|
||||||
// If the literal has calculated (non-literal) elements transform it into
|
// If the literal has calculated (non-literal) elements transform it into
|
||||||
// calls to literal factories that compose the literal and will cache intermediate
|
// calls to literal factories that compose the literal and will cache intermediate
|
||||||
// values. Otherwise, just return an literal array that contains the values.
|
// values.
|
||||||
const literal = o.literalArr(values);
|
const literal = o.literalArr(values);
|
||||||
return values.every(a => a.isConstant()) ?
|
return getLiteralFactory(this.constantPool, literal, this.allocatePureFunctionSlots);
|
||||||
this.constantPool.getConstLiteral(literal, true) :
|
|
||||||
getLiteralFactory(this.constantPool, literal, this.allocatePureFunctionSlots);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1432,12 +1430,10 @@ export class ValueConverter extends AstMemoryEfficientTransformer {
|
||||||
return new BuiltinFunctionCall(map.span, map.sourceSpan, this.visitAll(map.values), values => {
|
return new BuiltinFunctionCall(map.span, map.sourceSpan, this.visitAll(map.values), values => {
|
||||||
// If the literal has calculated (non-literal) elements transform it into
|
// If the literal has calculated (non-literal) elements transform it into
|
||||||
// calls to literal factories that compose the literal and will cache intermediate
|
// calls to literal factories that compose the literal and will cache intermediate
|
||||||
// values. Otherwise, just return an literal array that contains the values.
|
// values.
|
||||||
const literal = o.literalMap(values.map(
|
const literal = o.literalMap(values.map(
|
||||||
(value, index) => ({key: map.keys[index].key, value, quoted: map.keys[index].quoted})));
|
(value, index) => ({key: map.keys[index].key, value, quoted: map.keys[index].quoted})));
|
||||||
return values.every(a => a.isConstant()) ?
|
return getLiteralFactory(this.constantPool, literal, this.allocatePureFunctionSlots);
|
||||||
this.constantPool.getConstLiteral(literal, true) :
|
|
||||||
getLiteralFactory(this.constantPool, literal, this.allocatePureFunctionSlots);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1484,15 +1480,11 @@ function getLiteralFactory(
|
||||||
const {literalFactory, literalFactoryArguments} = constantPool.getLiteralFactory(literal);
|
const {literalFactory, literalFactoryArguments} = constantPool.getLiteralFactory(literal);
|
||||||
// Allocate 1 slot for the result plus 1 per argument
|
// Allocate 1 slot for the result plus 1 per argument
|
||||||
const startSlot = allocateSlots(1 + literalFactoryArguments.length);
|
const startSlot = allocateSlots(1 + literalFactoryArguments.length);
|
||||||
literalFactoryArguments.length > 0 || error(`Expected arguments to a literal factory function`);
|
|
||||||
const {identifier, isVarLength} = pureFunctionCallInfo(literalFactoryArguments);
|
const {identifier, isVarLength} = pureFunctionCallInfo(literalFactoryArguments);
|
||||||
|
|
||||||
// Literal factories are pure functions that only need to be re-invoked when the parameters
|
// Literal factories are pure functions that only need to be re-invoked when the parameters
|
||||||
// change.
|
// change.
|
||||||
const args = [
|
const args = [o.literal(startSlot), literalFactory];
|
||||||
o.literal(startSlot),
|
|
||||||
literalFactory,
|
|
||||||
];
|
|
||||||
|
|
||||||
if (isVarLength) {
|
if (isVarLength) {
|
||||||
args.push(o.literalArr(literalFactoryArguments));
|
args.push(o.literalArr(literalFactoryArguments));
|
||||||
|
|
|
@ -73,8 +73,8 @@ export function ɵɵtemplate(
|
||||||
|
|
||||||
// TODO: consider a separate node type for templates
|
// TODO: consider a separate node type for templates
|
||||||
const tContainerNode = containerInternal(
|
const tContainerNode = containerInternal(
|
||||||
lView, index, tagName || null, getConstant(tViewConsts, attrsIndex) as TAttributes);
|
lView, index, tagName || null, getConstant<TAttributes>(tViewConsts, attrsIndex));
|
||||||
const localRefs = getConstant(tViewConsts, localRefsIndex) as string[];
|
const localRefs = getConstant<string[]>(tViewConsts, localRefsIndex);
|
||||||
if (tView.firstCreatePass) {
|
if (tView.firstCreatePass) {
|
||||||
ngDevMode && ngDevMode.firstCreatePass++;
|
ngDevMode && ngDevMode.firstCreatePass++;
|
||||||
resolveDirectives(tView, lView, tContainerNode, localRefs);
|
resolveDirectives(tView, lView, tContainerNode, localRefs);
|
||||||
|
|
|
@ -46,8 +46,8 @@ export function ɵɵelementStart(
|
||||||
const lView = getLView();
|
const lView = getLView();
|
||||||
const tView = lView[TVIEW];
|
const tView = lView[TVIEW];
|
||||||
const tViewConsts = tView.consts;
|
const tViewConsts = tView.consts;
|
||||||
const attrs = getConstant(tViewConsts, attrsIndex) as TAttributes;
|
const attrs = getConstant<TAttributes>(tViewConsts, attrsIndex);
|
||||||
const localRefs = getConstant(tViewConsts, localRefsIndex) as string[];
|
const localRefs = getConstant<string[]>(tViewConsts, localRefsIndex);
|
||||||
ngDevMode && assertEqual(
|
ngDevMode && assertEqual(
|
||||||
getBindingIndex(), tView.bindingStartIndex,
|
getBindingIndex(), tView.bindingStartIndex,
|
||||||
'elements should be created before any bindings');
|
'elements should be created before any bindings');
|
||||||
|
|
|
@ -43,8 +43,8 @@ export function ɵɵelementContainerStart(
|
||||||
const renderer = lView[RENDERER];
|
const renderer = lView[RENDERER];
|
||||||
const tagName = 'ng-container';
|
const tagName = 'ng-container';
|
||||||
const tViewConsts = tView.consts;
|
const tViewConsts = tView.consts;
|
||||||
const attrs = getConstant(tViewConsts, attrsIndex) as TAttributes;
|
const attrs = getConstant<TAttributes>(tViewConsts, attrsIndex);
|
||||||
const localRefs = getConstant(tViewConsts, localRefsIndex) as string[];
|
const localRefs = getConstant<string[]>(tViewConsts, localRefsIndex);
|
||||||
ngDevMode && assertEqual(
|
ngDevMode && assertEqual(
|
||||||
getBindingIndex(), tView.bindingStartIndex,
|
getBindingIndex(), tView.bindingStartIndex,
|
||||||
'element containers should be created before any bindings');
|
'element containers should be created before any bindings');
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
|
|
||||||
import {bindingUpdated, bindingUpdated2, bindingUpdated3, bindingUpdated4, getBinding, updateBinding} from './bindings';
|
import {bindingUpdated, bindingUpdated2, bindingUpdated3, bindingUpdated4, getBinding, updateBinding} from './bindings';
|
||||||
import {getBindingRoot, getLView} from './state';
|
import {getBindingRoot, getLView} from './state';
|
||||||
import {isCreationMode} from './util/view_utils';
|
import {NO_CHANGE} from './tokens';
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -44,7 +44,7 @@ export function ɵɵpureFunction0<T>(slotOffset: number, pureFn: () => T, thisAr
|
||||||
// TODO(kara): use bindingRoot instead of bindingStartIndex when implementing host bindings
|
// TODO(kara): use bindingRoot instead of bindingStartIndex when implementing host bindings
|
||||||
const bindingIndex = getBindingRoot() + slotOffset;
|
const bindingIndex = getBindingRoot() + slotOffset;
|
||||||
const lView = getLView();
|
const lView = getLView();
|
||||||
return isCreationMode(lView) ?
|
return lView[bindingIndex] === NO_CHANGE ?
|
||||||
updateBinding(lView, bindingIndex, thisArg ? pureFn.call(thisArg) : pureFn()) :
|
updateBinding(lView, bindingIndex, thisArg ? pureFn.call(thisArg) : pureFn()) :
|
||||||
getBinding(lView, bindingIndex);
|
getBinding(lView, bindingIndex);
|
||||||
}
|
}
|
||||||
|
|
|
@ -176,8 +176,9 @@ export function viewAttachedToContainer(view: LView): boolean {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Returns a constant from `TConstants` instance. */
|
/** Returns a constant from `TConstants` instance. */
|
||||||
export function getConstant(consts: TConstants | null, index: number | null | undefined) {
|
export function getConstant<T>(consts: TConstants | null, index: number | null | undefined): T|
|
||||||
return consts === null || index == null ? null : consts[index];
|
null {
|
||||||
|
return consts === null || index == null ? null : consts[index] as unknown as T;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -6,9 +6,10 @@
|
||||||
* found in the LICENSE file at https://angular.io/license
|
* found in the LICENSE file at https://angular.io/license
|
||||||
*/
|
*/
|
||||||
import {CommonModule} from '@angular/common';
|
import {CommonModule} from '@angular/common';
|
||||||
import {Component, Input} from '@angular/core';
|
import {Component, Directive, Input, QueryList, ViewChild, ViewChildren} from '@angular/core';
|
||||||
import {TestBed} from '@angular/core/testing';
|
import {TestBed} from '@angular/core/testing';
|
||||||
import {By} from '@angular/platform-browser';
|
import {By} from '@angular/platform-browser';
|
||||||
|
import {onlyInIvy} from '@angular/private/testing';
|
||||||
|
|
||||||
describe('components using pure function instructions internally', () => {
|
describe('components using pure function instructions internally', () => {
|
||||||
describe('with array literals', () => {
|
describe('with array literals', () => {
|
||||||
|
@ -479,4 +480,84 @@ describe('components using pure function instructions internally', () => {
|
||||||
expect(objectComps[1].config).toEqual({opacity: 1, duration: 600});
|
expect(objectComps[1].config).toEqual({opacity: 1, duration: 600});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
onlyInIvy('issue has only been fixed for Ivy').describe('identical literals', () => {
|
||||||
|
@Directive({selector: '[dir]'})
|
||||||
|
class Dir {
|
||||||
|
@Input('dir') value: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
it('should not share object literals across elements', () => {
|
||||||
|
@Component({
|
||||||
|
template: `
|
||||||
|
<div [dir]="{}"></div>
|
||||||
|
<div [dir]="{}"></div>
|
||||||
|
`
|
||||||
|
})
|
||||||
|
class App {
|
||||||
|
@ViewChildren(Dir) directives !: QueryList<Dir>;
|
||||||
|
}
|
||||||
|
|
||||||
|
TestBed.configureTestingModule({declarations: [Dir, App]});
|
||||||
|
const fixture = TestBed.createComponent(App);
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
const directives = fixture.componentInstance.directives.toArray();
|
||||||
|
expect(directives[0].value).not.toBe(directives[1].value);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not share array literals across elements', () => {
|
||||||
|
@Component({
|
||||||
|
template: `
|
||||||
|
<div [dir]="[]"></div>
|
||||||
|
<div [dir]="[]"></div>
|
||||||
|
`
|
||||||
|
})
|
||||||
|
class App {
|
||||||
|
@ViewChildren(Dir) directives !: QueryList<Dir>;
|
||||||
|
}
|
||||||
|
|
||||||
|
TestBed.configureTestingModule({declarations: [Dir, App]});
|
||||||
|
const fixture = TestBed.createComponent(App);
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
const directives = fixture.componentInstance.directives.toArray();
|
||||||
|
expect(directives[0].value).not.toBe(directives[1].value);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not share object literals across component instances', () => {
|
||||||
|
@Component({template: `<div [dir]="{}"></div>`})
|
||||||
|
class App {
|
||||||
|
@ViewChild(Dir) directive !: Dir;
|
||||||
|
}
|
||||||
|
|
||||||
|
TestBed.configureTestingModule({declarations: [Dir, App]});
|
||||||
|
const firstFixture = TestBed.createComponent(App);
|
||||||
|
firstFixture.detectChanges();
|
||||||
|
|
||||||
|
const secondFixture = TestBed.createComponent(App);
|
||||||
|
secondFixture.detectChanges();
|
||||||
|
|
||||||
|
expect(firstFixture.componentInstance.directive.value)
|
||||||
|
.not.toBe(secondFixture.componentInstance.directive.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not share array literals across component instances', () => {
|
||||||
|
@Component({template: `<div [dir]="[]"></div>`})
|
||||||
|
class App {
|
||||||
|
@ViewChild(Dir) directive !: Dir;
|
||||||
|
}
|
||||||
|
|
||||||
|
TestBed.configureTestingModule({declarations: [Dir, App]});
|
||||||
|
const firstFixture = TestBed.createComponent(App);
|
||||||
|
firstFixture.detectChanges();
|
||||||
|
|
||||||
|
const secondFixture = TestBed.createComponent(App);
|
||||||
|
secondFixture.detectChanges();
|
||||||
|
|
||||||
|
expect(firstFixture.componentInstance.directive.value)
|
||||||
|
.not.toBe(secondFixture.componentInstance.directive.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in New Issue