fix(ivy): unify checkNoChanges logic with the view engine (#28366)

This commit unifies handling of the "check no changes" mode between
ngIvy and the view engine. More specifically:
- check no changes can be invoked before change detection in ivy;
- `undefined` values are considered equal `NO_CHANGES` for the "check no changes"
mode purposes.

Chanes in this commit enables several tests that were previously running only in ivy
or only in the view engine.

PR Close #28366
This commit is contained in:
Pawel Kozlowski 2019-01-25 15:36:46 +01:00 committed by Jason Aden
parent 3d5a919ac5
commit 99886bd159
3 changed files with 59 additions and 61 deletions

View File

@ -7,11 +7,10 @@
*/ */
import {devModeEqual} from '../change_detection/change_detection_util'; import {devModeEqual} from '../change_detection/change_detection_util';
import {assertDataInRange, assertLessThan, assertNotEqual} from '../util/assert'; import {assertDataInRange, assertLessThan, assertNotEqual} from '../util/assert';
import {throwErrorIfNoChangesMode} from './errors'; import {throwErrorIfNoChangesMode} from './errors';
import {BINDING_INDEX, LView} from './interfaces/view'; import {LView} from './interfaces/view';
import {getCheckNoChangesMode, isCreationMode} from './state'; import {getCheckNoChangesMode} from './state';
import {NO_CHANGE} from './tokens'; import {NO_CHANGE} from './tokens';
import {isDifferent} from './util'; import {isDifferent} from './util';
@ -38,20 +37,21 @@ export function bindingUpdated(lView: LView, bindingIndex: number, value: any):
ngDevMode && ngDevMode &&
assertLessThan(bindingIndex, lView.length, `Slot should have been initialized to NO_CHANGE`); assertLessThan(bindingIndex, lView.length, `Slot should have been initialized to NO_CHANGE`);
if (lView[bindingIndex] === NO_CHANGE) { const oldValue = lView[bindingIndex];
// initial pass if (isDifferent(oldValue, value)) {
lView[bindingIndex] = value;
} else if (isDifferent(lView[bindingIndex], value)) {
if (ngDevMode && getCheckNoChangesMode()) { if (ngDevMode && getCheckNoChangesMode()) {
if (!devModeEqual(lView[bindingIndex], value)) { // View engine didn't report undefined values as changed on the first checkNoChanges pass
throwErrorIfNoChangesMode(isCreationMode(lView), lView[bindingIndex], value); // (before the change detection was run).
const oldValueToCompare = oldValue !== NO_CHANGE ? oldValue : undefined;
if (!devModeEqual(oldValueToCompare, value)) {
throwErrorIfNoChangesMode(oldValue === NO_CHANGE, oldValueToCompare, value);
} }
} }
lView[bindingIndex] = value; lView[bindingIndex] = value;
} else {
return false;
}
return true; return true;
}
return false;
} }
/** Updates 2 bindings if changed, then returns whether either was updated. */ /** Updates 2 bindings if changed, then returns whether either was updated. */

View File

@ -9,7 +9,7 @@
import {assertDataInRange, assertDefined, assertGreaterThan, assertLessThan} from '../util/assert'; import {assertDataInRange, assertDefined, assertGreaterThan, assertLessThan} from '../util/assert';
import {global} from '../util/global'; import {global} from '../util/global';
import {ACTIVE_INDEX, LCONTAINER_LENGTH, LContainer} from './interfaces/container'; import {LCONTAINER_LENGTH, LContainer} from './interfaces/container';
import {LContext, MONKEY_PATCH_KEY_NAME} from './interfaces/context'; import {LContext, MONKEY_PATCH_KEY_NAME} from './interfaces/context';
import {ComponentDef, DirectiveDef} from './interfaces/definition'; import {ComponentDef, DirectiveDef} from './interfaces/definition';
import {NO_PARENT_INJECTOR, RelativeInjectorLocation, RelativeInjectorLocationFlags} from './interfaces/injector'; import {NO_PARENT_INJECTOR, RelativeInjectorLocation, RelativeInjectorLocationFlags} from './interfaces/injector';

View File

@ -13,7 +13,7 @@ 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';
import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter'; import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter';
import {expect} from '@angular/platform-browser/testing/src/matchers'; import {expect} from '@angular/platform-browser/testing/src/matchers';
import {fixmeIvy, ivyEnabled, modifiedInIvy, onlyInIvy} from '@angular/private/testing'; import {fixmeIvy, ivyEnabled, modifiedInIvy} from '@angular/private/testing';
export function createUrlResolverWithoutPackagePrefix(): UrlResolver { export function createUrlResolverWithoutPackagePrefix(): UrlResolver {
return new UrlResolver(); return new UrlResolver();
@ -1183,8 +1183,7 @@ const TEST_COMPILER_PROVIDERS: Provider[] = [
}); });
describe('enforce no new changes', () => { describe('enforce no new changes', () => {
modifiedInIvy('checkNoChanges has no effect before first change detection run') it('should throw when a record gets changed after it has been checked', fakeAsync(() => {
.it('should throw when a record gets changed after it has been checked', fakeAsync(() => {
@Directive({selector: '[changed]'}) @Directive({selector: '[changed]'})
class ChangingDirective { class ChangingDirective {
@Input() changed: any; @Input() changed: any;
@ -1195,15 +1194,14 @@ const TEST_COMPILER_PROVIDERS: Provider[] = [
const ctx = createCompFixture('<div [someProp]="a" [changed]="b"></div>', TestData); const ctx = createCompFixture('<div [someProp]="a" [changed]="b"></div>', TestData);
ctx.componentInstance.b = 1; ctx.componentInstance.b = 1;
const errMsgRegExp = ivyEnabled ?
expect(() => ctx.checkNoChanges()) /Previous value: 'undefined'\. Current value: '1'/g :
.toThrowError( /Previous value: 'changed: undefined'\. Current value: 'changed: 1'/g;
/Previous value: 'changed: undefined'\. Current value: 'changed: 1'/g); expect(() => ctx.checkNoChanges()).toThrowError(errMsgRegExp);
})); }));
onlyInIvy('checkNoChanges has no effect before first change detection run') it('should throw when a record gets changed after the first change detection pass',
.it('should throw when a record gets changed after the first change detection pass',
fakeAsync(() => { fakeAsync(() => {
@Directive({selector: '[changed]'}) @Directive({selector: '[changed]'})
class ChangingDirective { class ChangingDirective {
@ -1215,23 +1213,24 @@ const TEST_COMPILER_PROVIDERS: Provider[] = [
const ctx = createCompFixture('<div [someProp]="a" [changed]="b"></div>', TestData); const ctx = createCompFixture('<div [someProp]="a" [changed]="b"></div>', TestData);
ctx.componentInstance.b = 1; ctx.componentInstance.b = 1;
// should not throw here as change detection didn't run yet
ctx.checkNoChanges();
ctx.detectChanges(); ctx.detectChanges();
ctx.componentInstance.b = 2; ctx.componentInstance.b = 2;
expect(() => ctx.checkNoChanges()) const errMsgRegExp = ivyEnabled ?
.toThrowError(/Previous value: '1'\. Current value: '2'/g); /Previous value: '1'\. Current value: '2'/g :
/Previous value: 'changed: 1'\. Current value: 'changed: 2'/g;
expect(() => ctx.checkNoChanges()).toThrowError(errMsgRegExp);
})); }));
fixmeIvy('FW-831: Views created in a cd hooks throw in view engine') it('should warn when the view has been created in a cd hook', fakeAsync(() => {
.it('should warn when the view has been created in a cd hook', fakeAsync(() => {
const ctx = createCompFixture('<div *gh9882>{{ a }}</div>', TestData); const ctx = createCompFixture('<div *gh9882>{{ a }}</div>', TestData);
ctx.componentInstance.a = 1; ctx.componentInstance.a = 1;
expect(() => ctx.detectChanges()) expect(() => ctx.detectChanges())
.toThrowError( .toThrowError(
/It seems like the view has been created after its parent and its children have been dirty checked/); /It seems like the view has been created after its parent and its children have been dirty checked/);
// subsequent change detection should run without issues
ctx.detectChanges();
})); }));
it('should not throw when two arrays are structurally the same', fakeAsync(() => { it('should not throw when two arrays are structurally the same', fakeAsync(() => {
@ -1242,8 +1241,7 @@ const TEST_COMPILER_PROVIDERS: Provider[] = [
expect(() => ctx.checkNoChanges()).not.toThrow(); expect(() => ctx.checkNoChanges()).not.toThrow();
})); }));
modifiedInIvy('checkNoChanges has no effect before first change detection run') it('should not break the next run', fakeAsync(() => {
.it('should not break the next run', fakeAsync(() => {
const ctx = _bindSimpleValue('a', TestData); const ctx = _bindSimpleValue('a', TestData);
ctx.componentInstance.a = 'value'; ctx.componentInstance.a = 'value';
expect(() => ctx.checkNoChanges()).toThrow(); expect(() => ctx.checkNoChanges()).toThrow();