feat(ExpressionChangedAfterItHasBeenCheckedException): more meaningful error message

fixes #9882
This commit is contained in:
Victor Berchet 2016-07-07 17:49:01 -07:00
parent eacc9e6541
commit 2de8364de2
3 changed files with 44 additions and 18 deletions

View File

@ -11,7 +11,9 @@ import {isPrimitive, looseIdentical} from '../facade/lang';
export {looseIdentical} from '../facade/lang'; export {looseIdentical} from '../facade/lang';
export const UNINITIALIZED = new Object(); export const UNINITIALIZED = {
toString: () => 'CD_INIT_VALUE'
};
export function devModeEqual(a: any, b: any): boolean { export function devModeEqual(a: any, b: any): boolean {
if (isListLikeIterable(a) && isListLikeIterable(b)) { if (isListLikeIterable(a) && isListLikeIterable(b)) {

View File

@ -6,8 +6,10 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {UNINITIALIZED} from '../change_detection/change_detection_util';
import {BaseException, WrappedException} from '../facade/exceptions'; import {BaseException, WrappedException} from '../facade/exceptions';
/** /**
* An error thrown if application changes model breaking the top-down data flow. * An error thrown if application changes model breaking the top-down data flow.
* *
@ -44,9 +46,14 @@ import {BaseException, WrappedException} from '../facade/exceptions';
*/ */
export class ExpressionChangedAfterItHasBeenCheckedException extends BaseException { export class ExpressionChangedAfterItHasBeenCheckedException extends BaseException {
constructor(oldValue: any, currValue: any, context: any) { constructor(oldValue: any, currValue: any, context: any) {
super( let msg =
`Expression has changed after it was checked. ` + `Expression has changed after it was checked. Previous value: '${oldValue}'. Current value: '${currValue}'.`;
`Previous value: '${oldValue}'. Current value: '${currValue}'`); if (oldValue === UNINITIALIZED) {
msg +=
` It seems like the view has been created after its parent and its children have been dirty checked.` +
` Has it been created in a change detection hook ?`;
}
super(msg);
} }
} }

View File

@ -1002,15 +1002,22 @@ export function main() {
describe('enforce no new changes', () => { describe('enforce no new changes', () => {
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(() => {
var ctx = createCompFixture('<div [someProp]="a"></div>', TestData); const ctx = createCompFixture('<div [someProp]="a"></div>', TestData);
ctx.componentInstance.a = 1; ctx.componentInstance.a = 1;
expect(() => ctx.checkNoChanges()) expect(() => ctx.checkNoChanges())
.toThrowError(/:0:5[\s\S]*Expression has changed after it was checked./g); .toThrowError(/:0:5[\s\S]*Expression has changed after it was checked./g);
})); }));
it('should warn when the view has been created in a cd hook', fakeAsync(() => {
const ctx = createCompFixture('<div *gh9882>{{ a }}</div>', TestData);
ctx.componentInstance.a = 1;
expect(() => ctx.detectChanges())
.toThrowError(
/It seems like the view has been created after its parent and its children have been dirty checked/);
}));
it('should not throw when two arrays are structurally the same', fakeAsync(() => { it('should not throw when two arrays are structurally the same', fakeAsync(() => {
var ctx = _bindSimpleValue('a', TestData); const ctx = _bindSimpleValue('a', TestData);
ctx.componentInstance.a = ['value']; ctx.componentInstance.a = ['value'];
ctx.detectChanges(false); ctx.detectChanges(false);
ctx.componentInstance.a = ['value']; ctx.componentInstance.a = ['value'];
@ -1018,9 +1025,8 @@ export function main() {
})); }));
it('should not break the next run', fakeAsync(() => { it('should not break the next run', fakeAsync(() => {
var 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();
ctx.detectChanges(); ctx.detectChanges();
@ -1093,17 +1099,28 @@ export function main() {
} }
const ALL_DIRECTIVES = /*@ts2dart_const*/[ const ALL_DIRECTIVES = /*@ts2dart_const*/[
forwardRef(() => TestDirective), forwardRef(() => TestComponent), forwardRef(() => TestDirective),
forwardRef(() => AnotherComponent), forwardRef(() => TestLocals), forwardRef(() => CompWithRef), forwardRef(() => TestComponent),
forwardRef(() => EmitterDirective), forwardRef(() => PushComp), forwardRef(() => AnotherComponent),
forwardRef(() => OrderCheckDirective2), forwardRef(() => OrderCheckDirective0), forwardRef(() => TestLocals),
forwardRef(() => OrderCheckDirective1), NgFor forwardRef(() => CompWithRef),
forwardRef(() => EmitterDirective),
forwardRef(() => PushComp),
forwardRef(() => OrderCheckDirective2),
forwardRef(() => OrderCheckDirective0),
forwardRef(() => OrderCheckDirective1),
forwardRef(() => Gh9882),
NgFor,
]; ];
const ALL_PIPES = /*@ts2dart_const*/[ const ALL_PIPES = /*@ts2dart_const*/[
forwardRef(() => CountingPipe), forwardRef(() => CountingImpurePipe), forwardRef(() => CountingPipe),
forwardRef(() => MultiArgPipe), forwardRef(() => PipeWithOnDestroy), forwardRef(() => CountingImpurePipe),
forwardRef(() => IdentityPipe), forwardRef(() => WrappedPipe), AsyncPipe forwardRef(() => MultiArgPipe),
forwardRef(() => PipeWithOnDestroy),
forwardRef(() => IdentityPipe),
forwardRef(() => WrappedPipe),
AsyncPipe,
]; ];
@Injectable() @Injectable()
@ -1259,7 +1276,7 @@ class EmitterDirective {
@Output('event') emitter = new EventEmitter<string>(); @Output('event') emitter = new EventEmitter<string>();
} }
@Directive({selector: '[gh-9882]'}) @Directive({selector: '[gh9882]'})
class Gh9882 implements AfterContentInit { class Gh9882 implements AfterContentInit {
constructor(private _viewContainer: ViewContainerRef, private _templateRef: TemplateRef<Object>) { constructor(private _viewContainer: ViewContainerRef, private _templateRef: TemplateRef<Object>) {
} }