bdce7698fc
As with regular Angular components, Angular elements are expected to
have their views update when inputs change.
Previously, Angular Elements views were not updated if the underlying
component used the `OnPush` change detection strategy.
This commit fixes this by calling `markForCheck()` on the component
view's `ChangeDetectorRef`.
NOTE:
This is similar to how `@angular/upgrade` does it:
3236ae0ee1/packages/upgrade/src/common/src/downgrade_component_adapter.ts (L146)
.
Fixes #38948
PR Close #39452
506 lines
20 KiB
TypeScript
506 lines
20 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright Google LLC All Rights Reserved.
|
|
*
|
|
* Use of this source code is governed by an MIT-style license that can be
|
|
* found in the LICENSE file at https://angular.io/license
|
|
*/
|
|
|
|
import {ApplicationRef, ChangeDetectorRef, ComponentFactory, ComponentFactoryResolver, ComponentRef, Injector, NgModuleRef, NgZone, SimpleChange, SimpleChanges, Type} from '@angular/core';
|
|
import {fakeAsync, tick} from '@angular/core/testing';
|
|
import {Subject} from 'rxjs';
|
|
|
|
import {ComponentNgElementStrategy, ComponentNgElementStrategyFactory} from '../src/component-factory-strategy';
|
|
import {NgElementStrategyEvent} from '../src/element-strategy';
|
|
|
|
describe('ComponentFactoryNgElementStrategy', () => {
|
|
let factory: FakeComponentFactory<typeof FakeComponent>;
|
|
let strategy: ComponentNgElementStrategy;
|
|
|
|
let injector: any;
|
|
let componentRef: any;
|
|
let applicationRef: any;
|
|
let ngZone: any;
|
|
|
|
let injectables: Map<unknown, unknown>;
|
|
|
|
beforeEach(() => {
|
|
factory = new FakeComponentFactory(FakeComponent);
|
|
componentRef = factory.componentRef;
|
|
|
|
applicationRef = jasmine.createSpyObj('applicationRef', ['attachView']);
|
|
|
|
ngZone = jasmine.createSpyObj('ngZone', ['run']);
|
|
ngZone.run.and.callFake((fn: () => unknown) => fn());
|
|
|
|
injector = jasmine.createSpyObj('injector', ['get']);
|
|
injector.get.and.callFake((token: unknown) => {
|
|
if (!injectables.has(token)) {
|
|
throw new Error(`Failed to get injectable from mock injector: ${token}`);
|
|
}
|
|
return injectables.get(token);
|
|
});
|
|
|
|
injectables = new Map<unknown, unknown>([
|
|
[ApplicationRef, applicationRef],
|
|
[NgZone, ngZone],
|
|
]);
|
|
|
|
strategy = new ComponentNgElementStrategy(factory, injector);
|
|
ngZone.run.calls.reset();
|
|
});
|
|
|
|
it('should create a new strategy from the factory', () => {
|
|
const factoryResolver = jasmine.createSpyObj('factoryResolver', ['resolveComponentFactory']);
|
|
factoryResolver.resolveComponentFactory.and.returnValue(factory);
|
|
injectables.set(ComponentFactoryResolver, factoryResolver);
|
|
|
|
const strategyFactory = new ComponentNgElementStrategyFactory(FakeComponent, injector);
|
|
expect(strategyFactory.create(injector)).toBeTruthy();
|
|
});
|
|
|
|
describe('before connected', () => {
|
|
it('should allow subscribing to output events', () => {
|
|
const events: NgElementStrategyEvent[] = [];
|
|
strategy.events.subscribe(e => events.push(e));
|
|
|
|
// No events before connecting (since `componentRef` is not even on the strategy yet).
|
|
componentRef.instance.output1.next('output-1a');
|
|
componentRef.instance.output1.next('output-1b');
|
|
componentRef.instance.output2.next('output-2a');
|
|
expect(events).toEqual([]);
|
|
|
|
// No events upon connecting (since events are not cached/played back).
|
|
strategy.connect(document.createElement('div'));
|
|
expect(events).toEqual([]);
|
|
|
|
// Events emitted once connected.
|
|
componentRef.instance.output1.next('output-1c');
|
|
componentRef.instance.output1.next('output-1d');
|
|
componentRef.instance.output2.next('output-2b');
|
|
expect(events).toEqual([
|
|
{name: 'templateOutput1', value: 'output-1c'},
|
|
{name: 'templateOutput1', value: 'output-1d'},
|
|
{name: 'templateOutput2', value: 'output-2b'},
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe('after connected', () => {
|
|
beforeEach(() => {
|
|
// Set up an initial value to make sure it is passed to the component
|
|
strategy.setInputValue('fooFoo', 'fooFoo-1');
|
|
strategy.setInputValue('falsyUndefined', undefined);
|
|
strategy.setInputValue('falsyNull', null);
|
|
strategy.setInputValue('falsyEmpty', '');
|
|
strategy.setInputValue('falsyFalse', false);
|
|
strategy.setInputValue('falsyZero', 0);
|
|
strategy.connect(document.createElement('div'));
|
|
});
|
|
|
|
it('should attach the component to the view', () => {
|
|
expect(applicationRef.attachView).toHaveBeenCalledWith(componentRef.hostView);
|
|
});
|
|
|
|
it('should detect changes', () => {
|
|
expect(componentRef.changeDetectorRef.detectChanges).toHaveBeenCalled();
|
|
});
|
|
|
|
it('should listen to output events', () => {
|
|
const events: NgElementStrategyEvent[] = [];
|
|
strategy.events.subscribe(e => events.push(e));
|
|
|
|
componentRef.instance.output1.next('output-1a');
|
|
componentRef.instance.output1.next('output-1b');
|
|
componentRef.instance.output2.next('output-2a');
|
|
expect(events).toEqual([
|
|
{name: 'templateOutput1', value: 'output-1a'},
|
|
{name: 'templateOutput1', value: 'output-1b'},
|
|
{name: 'templateOutput2', value: 'output-2a'},
|
|
]);
|
|
});
|
|
|
|
it('should initialize the component with initial values', () => {
|
|
expect(strategy.getInputValue('fooFoo')).toBe('fooFoo-1');
|
|
expect(componentRef.instance.fooFoo).toBe('fooFoo-1');
|
|
});
|
|
|
|
it('should initialize the component with falsy initial values', () => {
|
|
expect(strategy.getInputValue('falsyUndefined')).toEqual(undefined);
|
|
expect(componentRef.instance.falsyUndefined).toEqual(undefined);
|
|
expect(strategy.getInputValue('falsyNull')).toEqual(null);
|
|
expect(componentRef.instance.falsyNull).toEqual(null);
|
|
expect(strategy.getInputValue('falsyEmpty')).toEqual('');
|
|
expect(componentRef.instance.falsyEmpty).toEqual('');
|
|
expect(strategy.getInputValue('falsyFalse')).toEqual(false);
|
|
expect(componentRef.instance.falsyFalse).toEqual(false);
|
|
expect(strategy.getInputValue('falsyZero')).toEqual(0);
|
|
expect(componentRef.instance.falsyZero).toEqual(0);
|
|
});
|
|
|
|
it('should call ngOnChanges with the change', () => {
|
|
expectSimpleChanges(componentRef.instance.simpleChanges[0], {
|
|
fooFoo: new SimpleChange(undefined, 'fooFoo-1', true),
|
|
falsyUndefined: new SimpleChange(undefined, undefined, true),
|
|
falsyNull: new SimpleChange(undefined, null, true),
|
|
falsyEmpty: new SimpleChange(undefined, '', true),
|
|
falsyFalse: new SimpleChange(undefined, false, true),
|
|
falsyZero: new SimpleChange(undefined, 0, true),
|
|
});
|
|
});
|
|
|
|
it('should call ngOnChanges with proper firstChange value', fakeAsync(() => {
|
|
strategy.setInputValue('fooFoo', 'fooFoo-2');
|
|
strategy.setInputValue('barBar', 'barBar-1');
|
|
strategy.setInputValue('falsyUndefined', 'notanymore');
|
|
tick(16); // scheduler waits 16ms if RAF is unavailable
|
|
(strategy as any).detectChanges();
|
|
expectSimpleChanges(componentRef.instance.simpleChanges[1], {
|
|
fooFoo: new SimpleChange('fooFoo-1', 'fooFoo-2', false),
|
|
barBar: new SimpleChange(undefined, 'barBar-1', true),
|
|
falsyUndefined: new SimpleChange(undefined, 'notanymore', false),
|
|
});
|
|
}));
|
|
});
|
|
|
|
describe('when inputs change and not connected', () => {
|
|
it('should cache the value', () => {
|
|
strategy.setInputValue('fooFoo', 'fooFoo-1');
|
|
expect(strategy.getInputValue('fooFoo')).toBe('fooFoo-1');
|
|
|
|
// Sanity check: componentRef isn't changed since its not even on the strategy
|
|
expect(componentRef.instance.fooFoo).toBe(undefined);
|
|
});
|
|
|
|
it('should not detect changes', fakeAsync(() => {
|
|
strategy.setInputValue('fooFoo', 'fooFoo-1');
|
|
tick(16); // scheduler waits 16ms if RAF is unavailable
|
|
expect(componentRef.changeDetectorRef.detectChanges).not.toHaveBeenCalled();
|
|
}));
|
|
});
|
|
|
|
describe('when inputs change and is connected', () => {
|
|
let viewChangeDetectorRef: ChangeDetectorRef;
|
|
|
|
beforeEach(() => {
|
|
strategy.connect(document.createElement('div'));
|
|
viewChangeDetectorRef = componentRef.injector.get(ChangeDetectorRef);
|
|
});
|
|
|
|
it('should be set on the component instance', () => {
|
|
strategy.setInputValue('fooFoo', 'fooFoo-1');
|
|
expect(componentRef.instance.fooFoo).toBe('fooFoo-1');
|
|
expect(strategy.getInputValue('fooFoo')).toBe('fooFoo-1');
|
|
});
|
|
|
|
it('should detect changes', fakeAsync(() => {
|
|
// Connect detected changes automatically
|
|
expect(componentRef.changeDetectorRef.detectChanges).toHaveBeenCalledTimes(1);
|
|
|
|
strategy.setInputValue('fooFoo', 'fooFoo-1');
|
|
tick(16); // scheduler waits 16ms if RAF is unavailable
|
|
expect(componentRef.changeDetectorRef.detectChanges).toHaveBeenCalledTimes(2);
|
|
}));
|
|
|
|
it('should detect changes once for multiple input changes', fakeAsync(() => {
|
|
// Connect detected changes automatically
|
|
expect(componentRef.changeDetectorRef.detectChanges).toHaveBeenCalledTimes(1);
|
|
|
|
strategy.setInputValue('fooFoo', 'fooFoo-1');
|
|
strategy.setInputValue('barBar', 'barBar-1');
|
|
tick(16); // scheduler waits 16ms if RAF is unavailable
|
|
expect(componentRef.changeDetectorRef.detectChanges).toHaveBeenCalledTimes(2);
|
|
}));
|
|
|
|
it('should not detect changes if the input is set to the same value', fakeAsync(() => {
|
|
(componentRef.changeDetectorRef.detectChanges as jasmine.Spy).calls.reset();
|
|
|
|
strategy.setInputValue('fooFoo', 'fooFoo-1');
|
|
strategy.setInputValue('barBar', 'barBar-1');
|
|
tick(16); // scheduler waits 16ms if RAF is unavailable
|
|
expect(componentRef.changeDetectorRef.detectChanges).toHaveBeenCalledTimes(1);
|
|
|
|
(componentRef.changeDetectorRef.detectChanges as jasmine.Spy).calls.reset();
|
|
|
|
strategy.setInputValue('fooFoo', 'fooFoo-1');
|
|
strategy.setInputValue('barBar', 'barBar-1');
|
|
tick(16); // scheduler waits 16ms if RAF is unavailable
|
|
expect(componentRef.changeDetectorRef.detectChanges).not.toHaveBeenCalled();
|
|
}));
|
|
|
|
it('should call ngOnChanges', fakeAsync(() => {
|
|
strategy.setInputValue('fooFoo', 'fooFoo-1');
|
|
tick(16); // scheduler waits 16ms if RAF is unavailable
|
|
expectSimpleChanges(
|
|
componentRef.instance.simpleChanges[0],
|
|
{fooFoo: new SimpleChange(undefined, 'fooFoo-1', true)});
|
|
}));
|
|
|
|
it('should call ngOnChanges once for multiple input changes', fakeAsync(() => {
|
|
strategy.setInputValue('fooFoo', 'fooFoo-1');
|
|
strategy.setInputValue('barBar', 'barBar-1');
|
|
tick(16); // scheduler waits 16ms if RAF is unavailable
|
|
expectSimpleChanges(componentRef.instance.simpleChanges[0], {
|
|
fooFoo: new SimpleChange(undefined, 'fooFoo-1', true),
|
|
barBar: new SimpleChange(undefined, 'barBar-1', true)
|
|
});
|
|
}));
|
|
|
|
it('should call ngOnChanges twice for changes in different rounds with previous values',
|
|
fakeAsync(() => {
|
|
strategy.setInputValue('fooFoo', 'fooFoo-1');
|
|
strategy.setInputValue('barBar', 'barBar-1');
|
|
tick(16); // scheduler waits 16ms if RAF is unavailable
|
|
expectSimpleChanges(componentRef.instance.simpleChanges[0], {
|
|
fooFoo: new SimpleChange(undefined, 'fooFoo-1', true),
|
|
barBar: new SimpleChange(undefined, 'barBar-1', true)
|
|
});
|
|
|
|
strategy.setInputValue('fooFoo', 'fooFoo-2');
|
|
strategy.setInputValue('barBar', 'barBar-2');
|
|
tick(16); // scheduler waits 16ms if RAF is unavailable
|
|
expectSimpleChanges(componentRef.instance.simpleChanges[1], {
|
|
fooFoo: new SimpleChange('fooFoo-1', 'fooFoo-2', false),
|
|
barBar: new SimpleChange('barBar-1', 'barBar-2', false)
|
|
});
|
|
}));
|
|
|
|
it('should not call ngOnChanges if the inout is set to the same value', fakeAsync(() => {
|
|
const ngOnChangesSpy = spyOn(componentRef.instance, 'ngOnChanges');
|
|
|
|
strategy.setInputValue('fooFoo', 'fooFoo-1');
|
|
strategy.setInputValue('barBar', 'barBar-1');
|
|
tick(16); // scheduler waits 16ms if RAF is unavailable
|
|
expect(ngOnChangesSpy).toHaveBeenCalledTimes(1);
|
|
|
|
ngOnChangesSpy.calls.reset();
|
|
|
|
strategy.setInputValue('fooFoo', 'fooFoo-1');
|
|
strategy.setInputValue('barBar', 'barBar-1');
|
|
tick(16); // scheduler waits 16ms if RAF is unavailable
|
|
expect(ngOnChangesSpy).not.toHaveBeenCalled();
|
|
}));
|
|
|
|
it('should not try to call ngOnChanges if not present on the component', fakeAsync(() => {
|
|
const factory2 = new FakeComponentFactory(FakeComponentWithoutNgOnChanges);
|
|
const strategy2 = new ComponentNgElementStrategy(factory2, injector);
|
|
const changeDetectorRef2 = factory2.componentRef.changeDetectorRef;
|
|
|
|
strategy2.connect(document.createElement('div'));
|
|
changeDetectorRef2.detectChanges.calls.reset();
|
|
|
|
strategy2.setInputValue('fooFoo', 'fooFoo-1');
|
|
expect(() => tick(16)).not.toThrow(); // scheduler waits 16ms if RAF is unavailable
|
|
|
|
// If the strategy would have tried to call `component.ngOnChanges()`, an error would have
|
|
// been thrown and `changeDetectorRef2.detectChanges()` would not have been called.
|
|
expect(changeDetectorRef2.detectChanges).toHaveBeenCalledTimes(1);
|
|
}));
|
|
|
|
it('should mark the view for check', fakeAsync(() => {
|
|
expect(viewChangeDetectorRef.markForCheck).not.toHaveBeenCalled();
|
|
|
|
strategy.setInputValue('fooFoo', 'fooFoo-1');
|
|
tick(16); // scheduler waits 16ms if RAF is unavailable
|
|
|
|
expect(viewChangeDetectorRef.markForCheck).toHaveBeenCalledTimes(1);
|
|
}));
|
|
|
|
it('should mark the view for check once for multiple input changes', fakeAsync(() => {
|
|
strategy.setInputValue('fooFoo', 'fooFoo-1');
|
|
strategy.setInputValue('barBar', 'barBar-1');
|
|
tick(16); // scheduler waits 16ms if RAF is unavailable
|
|
|
|
expect(viewChangeDetectorRef.markForCheck).toHaveBeenCalledTimes(1);
|
|
}));
|
|
|
|
it('should mark the view for check twice for changes in different rounds with previous values',
|
|
fakeAsync(() => {
|
|
strategy.setInputValue('fooFoo', 'fooFoo-1');
|
|
strategy.setInputValue('barBar', 'barBar-1');
|
|
tick(16); // scheduler waits 16ms if RAF is unavailable
|
|
|
|
expect(viewChangeDetectorRef.markForCheck).toHaveBeenCalledTimes(1);
|
|
|
|
strategy.setInputValue('fooFoo', 'fooFoo-2');
|
|
strategy.setInputValue('barBar', 'barBar-2');
|
|
tick(16); // scheduler waits 16ms if RAF is unavailable
|
|
|
|
expect(viewChangeDetectorRef.markForCheck).toHaveBeenCalledTimes(2);
|
|
}));
|
|
|
|
it('should mark the view for check even if ngOnChanges is not present on the component',
|
|
fakeAsync(() => {
|
|
const factory2 = new FakeComponentFactory(FakeComponentWithoutNgOnChanges);
|
|
const strategy2 = new ComponentNgElementStrategy(factory2, injector);
|
|
const viewChangeDetectorRef2 = factory2.componentRef.injector.get(ChangeDetectorRef);
|
|
|
|
strategy2.connect(document.createElement('div'));
|
|
(viewChangeDetectorRef2.markForCheck as jasmine.Spy).calls.reset();
|
|
|
|
strategy2.setInputValue('fooFoo', 'fooFoo-1');
|
|
expect(() => tick(16)).not.toThrow(); // scheduler waits 16ms if RAF is unavailable
|
|
|
|
// If the strategy would have tried to call `component.ngOnChanges()`, an error would have
|
|
// been thrown and `viewChangeDetectorRef2.markForCheck()` would not have been called.
|
|
expect(viewChangeDetectorRef2.markForCheck).toHaveBeenCalledTimes(1);
|
|
}));
|
|
|
|
it('should not mark the view for check if the input is set to the same value', fakeAsync(() => {
|
|
(viewChangeDetectorRef.markForCheck as jasmine.Spy).calls.reset();
|
|
|
|
strategy.setInputValue('fooFoo', 'fooFoo-1');
|
|
strategy.setInputValue('barBar', 'barBar-1');
|
|
tick(16); // scheduler waits 16ms if RAF is unavailable
|
|
expect(viewChangeDetectorRef.markForCheck).toHaveBeenCalledTimes(1);
|
|
|
|
(viewChangeDetectorRef.markForCheck as jasmine.Spy).calls.reset();
|
|
|
|
strategy.setInputValue('fooFoo', 'fooFoo-1');
|
|
strategy.setInputValue('barBar', 'barBar-1');
|
|
tick(16); // scheduler waits 16ms if RAF is unavailable
|
|
expect(viewChangeDetectorRef.markForCheck).not.toHaveBeenCalled();
|
|
}));
|
|
});
|
|
|
|
describe('disconnect', () => {
|
|
it('should be able to call if not connected', fakeAsync(() => {
|
|
strategy.disconnect();
|
|
|
|
// Sanity check: the strategy doesn't have an instance of the componentRef anyways
|
|
expect(componentRef.destroy).not.toHaveBeenCalled();
|
|
}));
|
|
|
|
it('should destroy the component after the destroy delay', fakeAsync(() => {
|
|
strategy.connect(document.createElement('div'));
|
|
strategy.disconnect();
|
|
expect(componentRef.destroy).not.toHaveBeenCalled();
|
|
|
|
tick(10);
|
|
expect(componentRef.destroy).toHaveBeenCalledTimes(1);
|
|
}));
|
|
|
|
it('should be able to call it multiple times but only destroy once', fakeAsync(() => {
|
|
strategy.connect(document.createElement('div'));
|
|
strategy.disconnect();
|
|
strategy.disconnect();
|
|
expect(componentRef.destroy).not.toHaveBeenCalled();
|
|
|
|
tick(10);
|
|
expect(componentRef.destroy).toHaveBeenCalledTimes(1);
|
|
|
|
strategy.disconnect();
|
|
expect(componentRef.destroy).toHaveBeenCalledTimes(1);
|
|
}));
|
|
});
|
|
|
|
describe('runInZone', () => {
|
|
const param = 'foofoo';
|
|
const fn = () => param;
|
|
|
|
it('should run the callback directly when invoked in element\'s zone', () => {
|
|
expect(strategy['runInZone'](fn)).toEqual('foofoo');
|
|
expect(ngZone.run).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should run the callback inside the element\'s zone when invoked in a different zone',
|
|
() => {
|
|
expect(Zone.root.run(() => (strategy['runInZone'](fn)))).toEqual('foofoo');
|
|
expect(ngZone.run).toHaveBeenCalledWith(fn);
|
|
});
|
|
|
|
it('should run the callback directly when called without zone.js loaded', () => {
|
|
// simulate no zone.js loaded
|
|
(strategy as any)['elementZone'] = null;
|
|
|
|
expect(Zone.root.run(() => (strategy['runInZone'](fn)))).toEqual('foofoo');
|
|
expect(ngZone.run).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
});
|
|
|
|
export class FakeComponentWithoutNgOnChanges {
|
|
output1 = new Subject();
|
|
output2 = new Subject();
|
|
}
|
|
|
|
export class FakeComponent {
|
|
output1 = new Subject();
|
|
output2 = new Subject();
|
|
|
|
// Keep track of the simple changes passed to ngOnChanges
|
|
simpleChanges: SimpleChanges[] = [];
|
|
|
|
ngOnChanges(simpleChanges: SimpleChanges) {
|
|
this.simpleChanges.push(simpleChanges);
|
|
}
|
|
}
|
|
|
|
export class FakeComponentFactory<T extends Type<any>> extends ComponentFactory<T> {
|
|
componentRef: any = jasmine.createSpyObj(
|
|
'componentRef',
|
|
// Method spies.
|
|
['destroy'],
|
|
// Property spies.
|
|
{
|
|
changeDetectorRef: jasmine.createSpyObj('changeDetectorRef', ['detectChanges']),
|
|
hostView: {},
|
|
injector: jasmine.createSpyObj('injector', {
|
|
get: jasmine.createSpyObj('viewChangeDetectorRef', ['markForCheck']),
|
|
}),
|
|
instance: new this.ComponentClass(),
|
|
});
|
|
|
|
get selector(): string {
|
|
return 'fake-component';
|
|
}
|
|
get componentType(): Type<any> {
|
|
return this.ComponentClass;
|
|
}
|
|
get ngContentSelectors(): string[] {
|
|
return ['content-1', 'content-2'];
|
|
}
|
|
get inputs(): {propName: string; templateName: string}[] {
|
|
return [
|
|
{propName: 'fooFoo', templateName: 'fooFoo'},
|
|
{propName: 'barBar', templateName: 'my-bar-bar'},
|
|
{propName: 'falsyUndefined', templateName: 'falsyUndefined'},
|
|
{propName: 'falsyNull', templateName: 'falsyNull'},
|
|
{propName: 'falsyEmpty', templateName: 'falsyEmpty'},
|
|
{propName: 'falsyFalse', templateName: 'falsyFalse'},
|
|
{propName: 'falsyZero', templateName: 'falsyZero'},
|
|
];
|
|
}
|
|
get outputs(): {propName: string; templateName: string}[] {
|
|
return [
|
|
{propName: 'output1', templateName: 'templateOutput1'},
|
|
{propName: 'output2', templateName: 'templateOutput2'},
|
|
];
|
|
}
|
|
|
|
constructor(private ComponentClass: T) {
|
|
super();
|
|
}
|
|
|
|
create(
|
|
injector: Injector, projectableNodes?: any[][], rootSelectorOrNode?: string|any,
|
|
ngModule?: NgModuleRef<any>): ComponentRef<any> {
|
|
return this.componentRef;
|
|
}
|
|
}
|
|
|
|
function expectSimpleChanges(actual: SimpleChanges, expected: SimpleChanges) {
|
|
Object.keys(actual).forEach(key => {
|
|
expect(expected[key]).toBeTruthy(`Change included additional key ${key}`);
|
|
});
|
|
|
|
Object.keys(expected).forEach(key => {
|
|
expect(actual[key]).toBeTruthy(`Change should have included key ${key}`);
|
|
if (actual[key]) {
|
|
expect(actual[key].previousValue).toBe(expected[key].previousValue, `${key}.previousValue`);
|
|
expect(actual[key].currentValue).toBe(expected[key].currentValue, `${key}.currentValue`);
|
|
expect(actual[key].firstChange).toBe(expected[key].firstChange, `${key}.firstChange`);
|
|
}
|
|
});
|
|
}
|