feat(core): view engine - add `WrappedValue` support (#14216)

Part of #14013
This commit is contained in:
Tobias Bosch 2017-01-31 11:08:29 -08:00 committed by Miško Hevery
parent 1bc5368ea0
commit 08ff67ea11
9 changed files with 327 additions and 104 deletions

View File

@ -10,7 +10,7 @@ import {isDevMode} from '../application_ref';
import {SecurityContext} from '../security';
import {BindingDef, BindingType, DebugContext, DisposableFn, ElementData, ElementOutputDef, EntryAction, NodeData, NodeDef, NodeFlags, NodeType, QueryValueType, ViewData, ViewDefinition, ViewFlags, asElementData} from './types';
import {checkAndUpdateBinding, entryAction, setBindingDebugInfo, setCurrentNode, sliceErrorStack} from './util';
import {checkAndUpdateBinding, entryAction, setBindingDebugInfo, setCurrentNode, sliceErrorStack, unwrapValue} from './util';
export function anchorDef(
flags: NodeFlags, matchedQueries: [string, QueryValueType][], ngContentIndex: number,
@ -248,6 +248,7 @@ function checkAndUpdateElementValue(view: ViewData, def: NodeDef, bindingIdx: nu
if (!checkAndUpdateBinding(view, def, bindingIdx, value)) {
return;
}
value = unwrapValue(value);
const binding = def.bindings[bindingIdx];
const name = binding.name;

View File

@ -17,7 +17,7 @@ import {Renderer} from '../render/api';
import {queryDef} from './query';
import {BindingDef, BindingType, DepDef, DepFlags, DisposableFn, EntryAction, NodeData, NodeDef, NodeFlags, NodeType, ProviderData, ProviderOutputDef, QueryBindingType, QueryDef, QueryValueType, Services, ViewData, ViewDefinition, ViewFlags, asElementData, asProviderData} from './types';
import {checkAndUpdateBinding, checkAndUpdateBindingWithChange, entryAction, setBindingDebugInfo, setCurrentNode} from './util';
import {checkAndUpdateBinding, checkAndUpdateBindingWithChange, entryAction, setBindingDebugInfo, setCurrentNode, unwrapValue} from './util';
const _tokenKeyCache = new Map<any, string>();
@ -278,6 +278,7 @@ function checkAndUpdateProp(
changed = checkAndUpdateBinding(view, def, bindingIdx, value);
}
if (changed) {
value = unwrapValue(value);
const binding = def.bindings[bindingIdx];
const propName = binding.name;
// Note: This is still safe with Closure Compiler as

View File

@ -8,7 +8,7 @@
import {resolveDep, tokenKey} from './provider';
import {BindingDef, BindingType, DepDef, DepFlags, NodeData, NodeDef, NodeType, ProviderData, PureExpressionData, PureExpressionType, ViewData, asPureExpressionData} from './types';
import {checkAndUpdateBinding} from './util';
import {checkAndUpdateBinding, unwrapValue} from './util';
export function purePipeDef(pipeToken: any, argCount: number): NodeDef {
return _pureExpressionDef(
@ -99,6 +99,17 @@ export function checkAndUpdatePureExpressionInline(
}
if (changed) {
v0 = unwrapValue(v0);
v1 = unwrapValue(v1);
v2 = unwrapValue(v2);
v3 = unwrapValue(v3);
v4 = unwrapValue(v4);
v5 = unwrapValue(v5);
v6 = unwrapValue(v6);
v7 = unwrapValue(v7);
v8 = unwrapValue(v8);
v9 = unwrapValue(v9);
const data = asPureExpressionData(view, def.index);
let value: any;
switch (def.pureExpression.type) {
@ -121,7 +132,7 @@ export function checkAndUpdatePureExpressionInline(
case 4:
value[3] = v3;
case 3:
value[2] = v2;
value[3] = v2;
case 2:
value[1] = v1;
case 1:
@ -208,16 +219,23 @@ export function checkAndUpdatePureExpressionDynamic(view: ViewData, def: NodeDef
let value: any;
switch (def.pureExpression.type) {
case PureExpressionType.Array:
value = values;
value = new Array(values.length);
for (let i = 0; i < values.length; i++) {
value[i] = unwrapValue(values[i]);
}
break;
case PureExpressionType.Object:
value = {};
for (let i = 0; i < values.length; i++) {
value[bindings[i].name] = values[i];
value[bindings[i].name] = unwrapValue(values[i]);
}
break;
case PureExpressionType.Pipe:
value = data.pipe.transform(values[0], ...values.slice(1));
const params = new Array(values.length);
for (let i = 0; i < values.length; i++) {
params[i] = unwrapValue(values[i]);
}
value = data.pipe.transform(params[0], ...params.slice(1));
break;
}
data.value = value;

View File

@ -10,7 +10,7 @@ import {isDevMode} from '../application_ref';
import {looseIdentical} from '../facade/lang';
import {BindingDef, BindingType, DebugContext, NodeData, NodeDef, NodeFlags, NodeType, Services, TextData, ViewData, ViewFlags, asElementData, asTextData} from './types';
import {checkAndUpdateBinding, sliceErrorStack} from './util';
import {checkAndUpdateBinding, sliceErrorStack, unwrapValue} from './util';
export function textDef(ngContentIndex: number, constants: string[]): NodeDef {
// skip the call to sliceErrorStack itself + the call to this function.
@ -156,6 +156,7 @@ export function checkAndUpdateTextDynamic(view: ViewData, def: NodeDef, values:
}
function _addInterpolationPart(value: any, binding: BindingDef): string {
value = unwrapValue(value);
const valueStr = value != null ? value.toString() : '';
return valueStr + binding.suffix;
}

View File

@ -7,7 +7,7 @@
*/
import {isDevMode} from '../application_ref';
import {devModeEqual} from '../change_detection/change_detection';
import {WrappedValue, devModeEqual} from '../change_detection/change_detection';
import {SimpleChange} from '../change_detection/change_detection_util';
import {looseIdentical} from '../facade/lang';
import {Renderer} from '../render/api';
@ -63,6 +63,13 @@ export function checkAndUpdateBindingWithChange(
return null;
}
export function unwrapValue(value: any): any {
if (value instanceof WrappedValue) {
value = value.wrapped;
}
return value;
}
export function declaredViewContainer(view: ViewData): ElementData {
if (view.parent) {
const parentView = view.parent;

View File

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import {RenderComponentType, RootRenderer, Sanitizer, SecurityContext, ViewEncapsulation, getDebugNode} from '@angular/core';
import {RenderComponentType, RootRenderer, Sanitizer, SecurityContext, ViewEncapsulation, WrappedValue, getDebugNode} from '@angular/core';
import {BindingType, DebugContext, DefaultServices, NodeDef, NodeFlags, Services, ViewData, ViewDefinition, ViewFlags, ViewHandleEventFn, ViewUpdateFn, anchorDef, asElementData, checkAndUpdateView, checkNoChangesView, checkNodeDynamic, checkNodeInline, createRootView, destroyView, elementDef, rootRenderNodes, setCurrentNode, textDef, viewDef} from '@angular/core/src/view/index';
import {inject} from '@angular/core/testing';
import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter';
@ -197,6 +197,43 @@ function defineTests(config: {directDom: boolean, viewFlags: number}) {
});
});
describe('general binding behavior', () => {
INLINE_DYNAMIC_VALUES.forEach((inlineDynamic) => {
it(`should unwrap values with ${InlineDynamic[inlineDynamic]}`, () => {
let bindingValue: any;
const {view, rootNodes} = createAndGetRootNodes(compViewDef(
[
elementDef(
NodeFlags.None, null, null, 0, 'input', null,
[
[BindingType.ElementProperty, 'title', SecurityContext.NONE],
]),
],
(view) => {
setCurrentNode(view, 0);
checkNodeInlineOrDynamic(inlineDynamic, [bindingValue]);
}));
const setterSpy = jasmine.createSpy('set');
Object.defineProperty(rootNodes[0], 'title', {set: setterSpy});
bindingValue = 'v1';
checkAndUpdateView(view);
expect(setterSpy).toHaveBeenCalledWith('v1');
setterSpy.calls.reset();
checkAndUpdateView(view);
expect(setterSpy).not.toHaveBeenCalled();
setterSpy.calls.reset();
bindingValue = WrappedValue.wrap('v1');
checkAndUpdateView(view);
expect(setterSpy).toHaveBeenCalledWith('v1');
});
});
});
if (getDOM().supportsDOMEvents()) {
describe('listen to DOM events', () => {
let removeNodes: Node[];

View File

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import {AfterContentChecked, AfterContentInit, AfterViewChecked, AfterViewInit, DoCheck, ElementRef, EventEmitter, OnChanges, OnDestroy, OnInit, RenderComponentType, Renderer, RootRenderer, Sanitizer, SecurityContext, SimpleChange, TemplateRef, ViewContainerRef, ViewEncapsulation, getDebugNode} from '@angular/core';
import {AfterContentChecked, AfterContentInit, AfterViewChecked, AfterViewInit, DoCheck, ElementRef, EventEmitter, OnChanges, OnDestroy, OnInit, RenderComponentType, Renderer, RootRenderer, Sanitizer, SecurityContext, SimpleChange, TemplateRef, ViewContainerRef, ViewEncapsulation, WrappedValue, getDebugNode} from '@angular/core';
import {BindingType, DebugContext, DefaultServices, NodeDef, NodeFlags, Services, ViewData, ViewDefinition, ViewFlags, ViewHandleEventFn, ViewUpdateFn, anchorDef, asElementData, asProviderData, checkAndUpdateView, checkNoChangesView, checkNodeDynamic, checkNodeInline, createRootView, destroyView, elementDef, providerDef, rootRenderNodes, setCurrentNode, textDef, viewDef} from '@angular/core/src/view/index';
import {inject} from '@angular/core/testing';
import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter';
@ -234,6 +234,39 @@ function defineTests(config: {directDom: boolean, viewFlags: number}) {
expect(getDOM().getAttribute(el, 'ng-reflect-a')).toBe('v1');
}
});
it(`should unwrap values with ${InlineDynamic[inlineDynamic]}`, () => {
let bindingValue: any;
let setterSpy = jasmine.createSpy('set');
class SomeService {
set a(value: any) { setterSpy(value); }
}
const {view, rootNodes} = createAndGetRootNodes(compViewDef(
[
elementDef(NodeFlags.None, null, null, 1, 'span'),
providerDef(NodeFlags.None, null, 0, SomeService, [], {a: [0, 'a']})
],
(view) => {
setCurrentNode(view, 1);
checkNodeInlineOrDynamic(inlineDynamic, [bindingValue]);
}));
bindingValue = 'v1';
checkAndUpdateView(view);
expect(setterSpy).toHaveBeenCalledWith('v1');
setterSpy.calls.reset();
checkAndUpdateView(view);
expect(setterSpy).not.toHaveBeenCalled();
setterSpy.calls.reset();
bindingValue = WrappedValue.wrap('v1');
checkAndUpdateView(view);
expect(setterSpy).toHaveBeenCalledWith('v1');
});
});
});

View File

@ -6,8 +6,8 @@
* found in the LICENSE file at https://angular.io/license
*/
import {PipeTransform, RenderComponentType, RootRenderer, Sanitizer, SecurityContext, ViewEncapsulation} from '@angular/core';
import {DefaultServices, NodeDef, NodeFlags, Services, ViewData, ViewDefinition, ViewFlags, ViewHandleEventFn, ViewUpdateFn, anchorDef, asProviderData, checkAndUpdateView, checkNoChangesView, checkNodeDynamic, checkNodeInline, createRootView, elementDef, providerDef, pureArrayDef, pureObjectDef, purePipeDef, rootRenderNodes, setCurrentNode, textDef, viewDef} from '@angular/core/src/view/index';
import {PipeTransform, RenderComponentType, RootRenderer, Sanitizer, SecurityContext, ViewEncapsulation, WrappedValue} from '@angular/core';
import {DefaultServices, NodeDef, NodeFlags, Services, ViewData, ViewDefinition, ViewFlags, ViewHandleEventFn, ViewUpdateFn, anchorDef, asProviderData, asPureExpressionData, checkAndUpdateView, checkNoChangesView, checkNodeDynamic, checkNodeInline, createRootView, elementDef, providerDef, pureArrayDef, pureObjectDef, purePipeDef, rootRenderNodes, setCurrentNode, textDef, viewDef} from '@angular/core/src/view/index';
import {inject} from '@angular/core/testing';
import {INLINE_DYNAMIC_VALUES, InlineDynamic, checkNodeInlineOrDynamic} from './helper';
@ -39,113 +39,209 @@ export function main() {
data: any;
}
INLINE_DYNAMIC_VALUES.forEach((inlineDynamic) => {
it(`should support pure arrays in ${InlineDynamic[inlineDynamic]} bindings`, () => {
let values: any[];
describe('pure arrays', () => {
const {view, rootNodes} = createAndGetRootNodes(compViewDef(
[
elementDef(NodeFlags.None, null, null, 2, 'span'), pureArrayDef(2),
providerDef(NodeFlags.None, null, 0, Service, [], {data: [0, 'data']})
],
(view) => {
setCurrentNode(view, 1);
const pureValue = checkNodeInlineOrDynamic(inlineDynamic, values);
setCurrentNode(view, 2);
checkNodeInlineOrDynamic(inlineDynamic, [pureValue]);
}));
const service = asProviderData(view, 2).instance;
INLINE_DYNAMIC_VALUES.forEach((inlineDynamic) => {
it(`should support ${InlineDynamic[inlineDynamic]} bindings`, () => {
let values: any[];
values = [1, 2];
checkAndUpdateView(view);
const arr0 = service.data;
expect(arr0).toEqual([1, 2]);
const {view, rootNodes} = createAndGetRootNodes(compViewDef(
[
elementDef(NodeFlags.None, null, null, 2, 'span'), pureArrayDef(2),
providerDef(NodeFlags.None, null, 0, Service, [], {data: [0, 'data']})
],
(view) => {
setCurrentNode(view, 1);
const pureValue = checkNodeInlineOrDynamic(inlineDynamic, values);
setCurrentNode(view, 2);
checkNodeInlineOrDynamic(inlineDynamic, [pureValue]);
}));
const service = asProviderData(view, 2).instance;
// instance should not change
// if the values don't change
checkAndUpdateView(view);
expect(service.data).toBe(arr0);
values = [1, 2];
checkAndUpdateView(view);
const arr0 = service.data;
expect(arr0).toEqual([1, 2]);
values = [3, 2];
checkAndUpdateView(view);
const arr1 = service.data;
expect(arr1).not.toBe(arr0);
expect(arr1).toEqual([3, 2]);
// instance should not change
// if the values don't change
checkAndUpdateView(view);
expect(service.data).toBe(arr0);
values = [3, 2];
checkAndUpdateView(view);
const arr1 = service.data;
expect(arr1).not.toBe(arr0);
expect(arr1).toEqual([3, 2]);
});
it(`should unwrap values with ${InlineDynamic[inlineDynamic]}`, () => {
let bindingValue: any;
const {view, rootNodes} = createAndGetRootNodes(compViewDef(
[
elementDef(NodeFlags.None, null, null, 1, 'span'),
pureArrayDef(1),
],
(view) => {
setCurrentNode(view, 1);
checkNodeInlineOrDynamic(inlineDynamic, [bindingValue]);
}));
const exprData = asPureExpressionData(view, 1);
bindingValue = 'v1';
checkAndUpdateView(view);
const v1Arr = exprData.value;
expect(v1Arr).toEqual(['v1']);
checkAndUpdateView(view);
expect(exprData.value).toBe(v1Arr);
bindingValue = WrappedValue.wrap('v1');
checkAndUpdateView(view);
expect(exprData.value).not.toBe(v1Arr);
expect(exprData.value).toEqual(['v1']);
});
});
});
describe('pure objects', () => {
INLINE_DYNAMIC_VALUES.forEach((inlineDynamic) => {
it(`should support ${InlineDynamic[inlineDynamic]} bindings`, () => {
let values: any[];
const {view, rootNodes} = createAndGetRootNodes(compViewDef(
[
elementDef(NodeFlags.None, null, null, 2, 'span'), pureObjectDef(['a', 'b']),
providerDef(NodeFlags.None, null, 0, Service, [], {data: [0, 'data']})
],
(view) => {
setCurrentNode(view, 1);
const pureValue = checkNodeInlineOrDynamic(inlineDynamic, values);
setCurrentNode(view, 2);
checkNodeInlineOrDynamic(inlineDynamic, [pureValue]);
}));
const service = asProviderData(view, 2).instance;
values = [1, 2];
checkAndUpdateView(view);
const obj0 = service.data;
expect(obj0).toEqual({a: 1, b: 2});
// instance should not change
// if the values don't change
checkAndUpdateView(view);
expect(service.data).toBe(obj0);
values = [3, 2];
checkAndUpdateView(view);
const obj1 = service.data;
expect(obj1).not.toBe(obj0);
expect(obj1).toEqual({a: 3, b: 2});
});
it(`should unwrap values with ${InlineDynamic[inlineDynamic]}`, () => {
let bindingValue: any;
const {view, rootNodes} = createAndGetRootNodes(compViewDef(
[
elementDef(NodeFlags.None, null, null, 1, 'span'),
pureObjectDef(['a']),
],
(view) => {
setCurrentNode(view, 1);
checkNodeInlineOrDynamic(inlineDynamic, [bindingValue]);
}));
const exprData = asPureExpressionData(view, 1);
bindingValue = 'v1';
checkAndUpdateView(view);
const v1Obj = exprData.value;
expect(v1Obj).toEqual({'a': 'v1'});
checkAndUpdateView(view);
expect(exprData.value).toBe(v1Obj);
bindingValue = WrappedValue.wrap('v1');
checkAndUpdateView(view);
expect(exprData.value).not.toBe(v1Obj);
expect(exprData.value).toEqual({'a': 'v1'});
});
});
});
INLINE_DYNAMIC_VALUES.forEach((inlineDynamic) => {
it(`should support pure objects in ${InlineDynamic[inlineDynamic]} bindings`, () => {
let values: any[];
describe('pure pipes', () => {
INLINE_DYNAMIC_VALUES.forEach((inlineDynamic) => {
it(`should support ${InlineDynamic[inlineDynamic]} bindings`, () => {
class SomePipe implements PipeTransform {
transform(v1: any, v2: any) { return [v1 + 10, v2 + 20]; }
}
const {view, rootNodes} = createAndGetRootNodes(compViewDef(
[
elementDef(NodeFlags.None, null, null, 2, 'span'), pureObjectDef(['a', 'b']),
providerDef(NodeFlags.None, null, 0, Service, [], {data: [0, 'data']})
],
(view) => {
setCurrentNode(view, 1);
const pureValue = checkNodeInlineOrDynamic(inlineDynamic, values);
setCurrentNode(view, 2);
checkNodeInlineOrDynamic(inlineDynamic, [pureValue]);
}));
const service = asProviderData(view, 2).instance;
let values: any[];
values = [1, 2];
checkAndUpdateView(view);
const obj0 = service.data;
expect(obj0).toEqual({a: 1, b: 2});
const {view, rootNodes} = createAndGetRootNodes(compViewDef(
[
elementDef(NodeFlags.None, null, null, 3, 'span'),
providerDef(NodeFlags.None, null, 0, SomePipe, []), purePipeDef(SomePipe, 2),
providerDef(NodeFlags.None, null, 0, Service, [], {data: [0, 'data']})
],
(view) => {
setCurrentNode(view, 2);
const pureValue = checkNodeInlineOrDynamic(inlineDynamic, values);
setCurrentNode(view, 3);
checkNodeInlineOrDynamic(inlineDynamic, [pureValue]);
}));
const service = asProviderData(view, 3).instance;
// instance should not change
// if the values don't change
checkAndUpdateView(view);
expect(service.data).toBe(obj0);
values = [1, 2];
checkAndUpdateView(view);
const obj0 = service.data;
expect(obj0).toEqual([11, 22]);
values = [3, 2];
checkAndUpdateView(view);
const obj1 = service.data;
expect(obj1).not.toBe(obj0);
expect(obj1).toEqual({a: 3, b: 2});
});
});
// instance should not change
// if the values don't change
checkAndUpdateView(view);
expect(service.data).toBe(obj0);
INLINE_DYNAMIC_VALUES.forEach((inlineDynamic) => {
it(`should support pure pipes in ${InlineDynamic[inlineDynamic]} bindings`, () => {
class SomePipe implements PipeTransform {
transform(v1: any, v2: any) { return [v1 + 10, v2 + 20]; }
}
values = [3, 2];
checkAndUpdateView(view);
const obj1 = service.data;
expect(obj1).not.toBe(obj0);
expect(obj1).toEqual([13, 22]);
});
let values: any[];
it(`should unwrap values with ${InlineDynamic[inlineDynamic]}`, () => {
let bindingValue: any;
let transformSpy = jasmine.createSpy('transform');
const {view, rootNodes} = createAndGetRootNodes(compViewDef(
[
elementDef(NodeFlags.None, null, null, 3, 'span'),
providerDef(NodeFlags.None, null, 0, SomePipe, []), purePipeDef(SomePipe, 2),
providerDef(NodeFlags.None, null, 0, Service, [], {data: [0, 'data']})
],
(view) => {
setCurrentNode(view, 2);
const pureValue = checkNodeInlineOrDynamic(inlineDynamic, values);
setCurrentNode(view, 3);
checkNodeInlineOrDynamic(inlineDynamic, [pureValue]);
}));
const service = asProviderData(view, 3).instance;
class SomePipe implements PipeTransform {
transform = transformSpy;
}
values = [1, 2];
checkAndUpdateView(view);
const obj0 = service.data;
expect(obj0).toEqual([11, 22]);
const {view, rootNodes} = createAndGetRootNodes(compViewDef(
[
elementDef(NodeFlags.None, null, null, 2, 'span'),
providerDef(NodeFlags.None, null, 0, SomePipe, []),
purePipeDef(SomePipe, 1),
],
(view) => {
setCurrentNode(view, 2);
checkNodeInlineOrDynamic(inlineDynamic, [bindingValue]);
}));
// instance should not change
// if the values don't change
checkAndUpdateView(view);
expect(service.data).toBe(obj0);
bindingValue = 'v1';
checkAndUpdateView(view);
expect(transformSpy).toHaveBeenCalledWith('v1');
values = [3, 2];
checkAndUpdateView(view);
const obj1 = service.data;
expect(obj1).not.toBe(obj0);
expect(obj1).toEqual([13, 22]);
transformSpy.calls.reset();
checkAndUpdateView(view);
expect(transformSpy).not.toHaveBeenCalled();
bindingValue = WrappedValue.wrap('v1');
checkAndUpdateView(view);
expect(transformSpy).toHaveBeenCalledWith('v1');
});
});
});
});

View File

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import {RenderComponentType, RootRenderer, Sanitizer, SecurityContext, ViewEncapsulation, getDebugNode} from '@angular/core';
import {RenderComponentType, RootRenderer, Sanitizer, SecurityContext, ViewEncapsulation, WrappedValue, getDebugNode} from '@angular/core';
import {DebugContext, DefaultServices, NodeDef, NodeFlags, Services, ViewData, ViewDefinition, ViewFlags, ViewHandleEventFn, ViewUpdateFn, anchorDef, asTextData, checkAndUpdateView, checkNoChangesView, checkNodeDynamic, checkNodeInline, createRootView, elementDef, rootRenderNodes, setCurrentNode, textDef, viewDef} from '@angular/core/src/view/index';
import {inject} from '@angular/core/testing';
import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter';
@ -97,6 +97,35 @@ function defineTests(config: {directDom: boolean, viewFlags: number}) {
const node = rootNodes[0];
expect(getDOM().getText(rootNodes[0])).toBe('0a1b2');
});
it(`should unwrap values with ${InlineDynamic[inlineDynamic]}`, () => {
let bindingValue: any;
const {view, rootNodes} = createAndGetRootNodes(compViewDef(
[
textDef(null, ['', '']),
],
(view: ViewData) => {
setCurrentNode(view, 0);
checkNodeInlineOrDynamic(inlineDynamic, [bindingValue]);
}));
const setterSpy = jasmine.createSpy('set');
Object.defineProperty(rootNodes[0], 'nodeValue', {set: setterSpy});
bindingValue = 'v1';
checkAndUpdateView(view);
expect(setterSpy).toHaveBeenCalledWith('v1');
setterSpy.calls.reset();
checkAndUpdateView(view);
expect(setterSpy).not.toHaveBeenCalled();
setterSpy.calls.reset();
bindingValue = WrappedValue.wrap('v1');
checkAndUpdateView(view);
expect(setterSpy).toHaveBeenCalledWith('v1');
});
});
});