
`AsyncPipe` only uses `WrappedValue` when the latest value from the `Promise` or `Observable` is different from the previous one. This is already enough to trigger change detection so the `WrappedValue` is not necessary. Fixes #29927 BREAKING CHANGE: This change could result in ExpressionChangedAfterItHasBeenChecked errors that were not detected before. The error could previously have gone undetected because two WrappedValues are considered "equal" in all cases for the purposes of the check, even if their respective unwrapped values are not. Additionally, `[val]=(observable | async).someProperty` will no longer trigger change detection if the value of `someProperty` is identical to the value in the previous emit. If you need to force change detection, either update the binding to use an object whose reference changes or subscribe to the observable and call markForCheck as needed. PR Close #36633
229 lines
7.4 KiB
TypeScript
229 lines
7.4 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright Google Inc. 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 {AsyncPipe, ɵgetDOM as getDOM} from '@angular/common';
|
|
import {EventEmitter} from '@angular/core';
|
|
import {AsyncTestCompleter, beforeEach, describe, expect, inject, it} from '@angular/core/testing/src/testing_internal';
|
|
import {browserDetection} from '@angular/platform-browser/testing/src/browser_util';
|
|
|
|
import {SpyChangeDetectorRef} from '../spies';
|
|
|
|
{
|
|
describe('AsyncPipe', () => {
|
|
describe('Observable', () => {
|
|
let emitter: EventEmitter<any>;
|
|
let pipe: AsyncPipe;
|
|
let ref: any;
|
|
const message = {};
|
|
|
|
beforeEach(() => {
|
|
emitter = new EventEmitter();
|
|
ref = new SpyChangeDetectorRef();
|
|
pipe = new AsyncPipe(ref);
|
|
});
|
|
|
|
describe('transform', () => {
|
|
it('should return null when subscribing to an observable', () => {
|
|
expect(pipe.transform(emitter)).toBe(null);
|
|
});
|
|
|
|
it('should return the latest available value',
|
|
inject([AsyncTestCompleter], (async: AsyncTestCompleter) => {
|
|
pipe.transform(emitter);
|
|
emitter.emit(message);
|
|
|
|
setTimeout(() => {
|
|
expect(pipe.transform(emitter)).toEqual(message);
|
|
async.done();
|
|
}, 0);
|
|
}));
|
|
|
|
|
|
it('should return same value when nothing has changed since the last call',
|
|
inject([AsyncTestCompleter], (async: AsyncTestCompleter) => {
|
|
pipe.transform(emitter);
|
|
emitter.emit(message);
|
|
|
|
setTimeout(() => {
|
|
pipe.transform(emitter);
|
|
expect(pipe.transform(emitter)).toBe(message);
|
|
async.done();
|
|
}, 0);
|
|
}));
|
|
|
|
it('should dispose of the existing subscription when subscribing to a new observable',
|
|
inject([AsyncTestCompleter], (async: AsyncTestCompleter) => {
|
|
pipe.transform(emitter);
|
|
|
|
const newEmitter = new EventEmitter();
|
|
expect(pipe.transform(newEmitter)).toBe(null);
|
|
emitter.emit(message);
|
|
|
|
// this should not affect the pipe
|
|
setTimeout(() => {
|
|
expect(pipe.transform(newEmitter)).toBe(null);
|
|
async.done();
|
|
}, 0);
|
|
}));
|
|
|
|
it('should request a change detection check upon receiving a new value',
|
|
inject([AsyncTestCompleter], (async: AsyncTestCompleter) => {
|
|
pipe.transform(emitter);
|
|
emitter.emit(message);
|
|
|
|
setTimeout(() => {
|
|
expect(ref.spy('markForCheck')).toHaveBeenCalled();
|
|
async.done();
|
|
}, 10);
|
|
}));
|
|
|
|
it('should return value for unchanged NaN', () => {
|
|
const emitter = new EventEmitter<any>();
|
|
emitter.emit(null);
|
|
pipe.transform(emitter);
|
|
emitter.next(NaN);
|
|
const firstResult = pipe.transform(emitter);
|
|
const secondResult = pipe.transform(emitter);
|
|
expect(firstResult).toBeNaN();
|
|
expect(secondResult).toBeNaN();
|
|
});
|
|
});
|
|
|
|
describe('ngOnDestroy', () => {
|
|
it('should do nothing when no subscription', () => {
|
|
expect(() => pipe.ngOnDestroy()).not.toThrow();
|
|
});
|
|
|
|
it('should dispose of the existing subscription',
|
|
inject([AsyncTestCompleter], (async: AsyncTestCompleter) => {
|
|
pipe.transform(emitter);
|
|
pipe.ngOnDestroy();
|
|
emitter.emit(message);
|
|
|
|
setTimeout(() => {
|
|
expect(pipe.transform(emitter)).toBe(null);
|
|
async.done();
|
|
}, 0);
|
|
}));
|
|
});
|
|
});
|
|
|
|
describe('Promise', () => {
|
|
const message = {};
|
|
let pipe: AsyncPipe;
|
|
let resolve: (result: any) => void;
|
|
let reject: (error: any) => void;
|
|
let promise: Promise<any>;
|
|
let ref: SpyChangeDetectorRef;
|
|
// adds longer timers for passing tests in IE
|
|
const timer = (getDOM() && browserDetection.isIE) ? 50 : 10;
|
|
|
|
beforeEach(() => {
|
|
promise = new Promise((res, rej) => {
|
|
resolve = res;
|
|
reject = rej;
|
|
});
|
|
ref = new SpyChangeDetectorRef();
|
|
pipe = new AsyncPipe(<any>ref);
|
|
});
|
|
|
|
describe('transform', () => {
|
|
it('should return null when subscribing to a promise', () => {
|
|
expect(pipe.transform(promise)).toBe(null);
|
|
});
|
|
|
|
it('should return the latest available value',
|
|
inject([AsyncTestCompleter], (async: AsyncTestCompleter) => {
|
|
pipe.transform(promise);
|
|
|
|
resolve(message);
|
|
|
|
setTimeout(() => {
|
|
expect(pipe.transform(promise)).toEqual(message);
|
|
async.done();
|
|
}, timer);
|
|
}));
|
|
|
|
it('should return value when nothing has changed since the last call',
|
|
inject([AsyncTestCompleter], (async: AsyncTestCompleter) => {
|
|
pipe.transform(promise);
|
|
resolve(message);
|
|
|
|
setTimeout(() => {
|
|
pipe.transform(promise);
|
|
expect(pipe.transform(promise)).toBe(message);
|
|
async.done();
|
|
}, timer);
|
|
}));
|
|
|
|
it('should dispose of the existing subscription when subscribing to a new promise',
|
|
inject([AsyncTestCompleter], (async: AsyncTestCompleter) => {
|
|
pipe.transform(promise);
|
|
|
|
promise = new Promise<any>(() => {});
|
|
expect(pipe.transform(promise)).toBe(null);
|
|
|
|
resolve(message);
|
|
|
|
setTimeout(() => {
|
|
expect(pipe.transform(promise)).toBe(null);
|
|
async.done();
|
|
}, timer);
|
|
}));
|
|
|
|
it('should request a change detection check upon receiving a new value',
|
|
inject([AsyncTestCompleter], (async: AsyncTestCompleter) => {
|
|
const markForCheck = ref.spy('markForCheck');
|
|
pipe.transform(promise);
|
|
resolve(message);
|
|
|
|
setTimeout(() => {
|
|
expect(markForCheck).toHaveBeenCalled();
|
|
async.done();
|
|
}, timer);
|
|
}));
|
|
|
|
describe('ngOnDestroy', () => {
|
|
it('should do nothing when no source', () => {
|
|
expect(() => pipe.ngOnDestroy()).not.toThrow();
|
|
});
|
|
|
|
it('should dispose of the existing source',
|
|
inject([AsyncTestCompleter], (async: AsyncTestCompleter) => {
|
|
pipe.transform(promise);
|
|
expect(pipe.transform(promise)).toBe(null);
|
|
resolve(message);
|
|
|
|
|
|
setTimeout(() => {
|
|
expect(pipe.transform(promise)).toEqual(message);
|
|
pipe.ngOnDestroy();
|
|
expect(pipe.transform(promise)).toBe(null);
|
|
async.done();
|
|
}, timer);
|
|
}));
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('null', () => {
|
|
it('should return null when given null', () => {
|
|
const pipe = new AsyncPipe(null as any);
|
|
expect(pipe.transform(null)).toEqual(null);
|
|
});
|
|
});
|
|
|
|
describe('other types', () => {
|
|
it('should throw when given an invalid object', () => {
|
|
const pipe = new AsyncPipe(null as any);
|
|
expect(() => pipe.transform(<any>'some bogus object')).toThrowError();
|
|
});
|
|
});
|
|
});
|
|
}
|