mirror of
https://github.com/angular/angular
synced 2026-05-24 09:28:37 +00:00
995 lines
31 KiB
TypeScript
995 lines
31 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.dev/license
|
|
*/
|
|
|
|
import {isNode} from '../../lib/common/utils';
|
|
import {ifEnvSupports} from '../test-util';
|
|
|
|
declare const global: any;
|
|
|
|
class MicroTaskQueueZoneSpec implements ZoneSpec {
|
|
name: string = 'MicroTaskQueue';
|
|
queue: MicroTask[] = [];
|
|
properties = {queue: this.queue, flush: this.flush.bind(this)};
|
|
|
|
flush() {
|
|
while (this.queue.length) {
|
|
const task = this.queue.shift();
|
|
task!.invoke();
|
|
}
|
|
}
|
|
|
|
onScheduleTask(delegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, task: Task): any {
|
|
this.queue.push(task as MicroTask);
|
|
}
|
|
}
|
|
|
|
function flushMicrotasks() {
|
|
Zone.current.get('flush')();
|
|
}
|
|
|
|
class TestRejection {
|
|
prop1?: string;
|
|
prop2?: string;
|
|
}
|
|
|
|
describe(
|
|
'Promise',
|
|
ifEnvSupports('Promise', function () {
|
|
if (!global.Promise) return;
|
|
let log: string[];
|
|
let queueZone: Zone;
|
|
let testZone: Zone;
|
|
let pZone: Zone;
|
|
|
|
beforeEach(() => {
|
|
testZone = Zone.current.fork({name: 'TestZone'});
|
|
|
|
pZone = Zone.current.fork({
|
|
name: 'promise-zone',
|
|
onScheduleTask: (
|
|
parentZoneDelegate: ZoneDelegate,
|
|
currentZone: Zone,
|
|
targetZone: Zone,
|
|
task: Task,
|
|
): any => {
|
|
log.push('scheduleTask');
|
|
parentZoneDelegate.scheduleTask(targetZone, task);
|
|
},
|
|
});
|
|
|
|
queueZone = Zone.current.fork(new MicroTaskQueueZoneSpec());
|
|
|
|
log = [];
|
|
});
|
|
|
|
it('should pretend to be a native code', () => {
|
|
expect(String(Promise).indexOf('[native code]') >= 0).toBe(true);
|
|
});
|
|
|
|
it('should use native toString for promise instance', () => {
|
|
expect(Object.prototype.toString.call(Promise.resolve())).toEqual('[object Promise]');
|
|
});
|
|
|
|
it('should make sure that new Promise is instance of Promise', () => {
|
|
expect(Promise.resolve(123) instanceof Promise).toBe(true);
|
|
expect(new Promise(() => null) instanceof Promise).toBe(true);
|
|
});
|
|
|
|
it('Promise.resolve(subPromise) should equal to subPromise', () => {
|
|
const p1 = Promise.resolve(1);
|
|
const p2 = Promise.resolve(p1);
|
|
expect(p1).toBe(p2);
|
|
});
|
|
|
|
xit('should ensure that Promise this is instanceof Promise', () => {
|
|
expect(() => {
|
|
Promise.call({} as any, () => null);
|
|
}).toThrowError('Must be an instanceof Promise.');
|
|
});
|
|
|
|
it('should allow subclassing without Symbol.species', () => {
|
|
class MyPromise extends Promise<any> {
|
|
constructor(fn: any) {
|
|
super(fn);
|
|
}
|
|
}
|
|
expect(new MyPromise(() => {}).then(() => null) instanceof MyPromise).toBe(true);
|
|
});
|
|
|
|
it('should allow subclassing without Symbol.species if properties are copied (SystemJS case)', () => {
|
|
let value: any = null;
|
|
const promise = Promise.resolve();
|
|
const systemjsModule = Object.create(null);
|
|
|
|
// We only copy properties from the `promise` instance onto the `systemjsModule` object.
|
|
// This is what SystemJS is doing internally:
|
|
// https://github.com/systemjs/systemjs/blob/main/src/system-core.js#L107-L113
|
|
for (const property in promise) {
|
|
const value: any = promise[property as keyof typeof promise];
|
|
if (!(value in systemjsModule) || systemjsModule[property] !== value) {
|
|
systemjsModule[property] = value;
|
|
}
|
|
}
|
|
|
|
queueZone.run(() => {
|
|
Promise.resolve()
|
|
.then(() => systemjsModule)
|
|
.then((v) => (value = v));
|
|
flushMicrotasks();
|
|
// Note: we want to ensure that the promise has been resolved. In this specific case
|
|
// the promise may resolve to different values in the browser and on the Node.js side.
|
|
// SystemJS runs only in the browser and it only needs the promise to be resolved.
|
|
expect(value).not.toEqual(null);
|
|
});
|
|
});
|
|
|
|
it('should allow subclassing with Symbol.species', () => {
|
|
class MyPromise extends Promise<any> {
|
|
constructor(fn: any) {
|
|
super(fn);
|
|
}
|
|
|
|
static override get [Symbol.species]() {
|
|
return MyPromise;
|
|
}
|
|
}
|
|
expect(new MyPromise(() => {}).then(() => null) instanceof MyPromise).toBe(true);
|
|
});
|
|
|
|
it('should allow subclassing with own then', (done: DoneFn) => {
|
|
class MyPromise extends Promise<any> {
|
|
constructor(private sub: Promise<any>) {
|
|
super((resolve) => {
|
|
resolve(null);
|
|
});
|
|
}
|
|
|
|
override then(onFulfilled: any, onRejected: any) {
|
|
return this.sub.then(onFulfilled, onRejected);
|
|
}
|
|
}
|
|
const p = Promise.resolve(1);
|
|
new MyPromise(p).then(
|
|
(v: any) => {
|
|
expect(v).toBe(1);
|
|
done();
|
|
},
|
|
() => done(),
|
|
);
|
|
});
|
|
|
|
it('Symbol.species should return ZoneAwarePromise', () => {
|
|
const empty = function () {};
|
|
const promise = Promise.resolve(1);
|
|
const FakePromise = (((promise.constructor = {} as any) as any)[Symbol.species] = function (
|
|
exec: any,
|
|
) {
|
|
exec(empty, empty);
|
|
});
|
|
expect(promise.then(empty) instanceof FakePromise).toBe(true);
|
|
});
|
|
|
|
it('should intercept scheduling of resolution and then', (done) => {
|
|
pZone.run(() => {
|
|
let p: Promise<any> = new Promise(function (resolve, reject) {
|
|
expect(resolve('RValue')).toBe(undefined);
|
|
});
|
|
expect(log).toEqual([]);
|
|
expect(p instanceof Promise).toBe(true);
|
|
p = p.then((v) => {
|
|
log.push(v);
|
|
expect(v).toBe('RValue');
|
|
expect(log).toEqual(['scheduleTask', 'RValue']);
|
|
return 'second value';
|
|
});
|
|
expect(p instanceof Promise).toBe(true);
|
|
expect(log).toEqual(['scheduleTask']);
|
|
p = p.then((v) => {
|
|
log.push(v);
|
|
expect(log).toEqual(['scheduleTask', 'RValue', 'scheduleTask', 'second value']);
|
|
done();
|
|
});
|
|
expect(p instanceof Promise).toBe(true);
|
|
expect(log).toEqual(['scheduleTask']);
|
|
});
|
|
});
|
|
|
|
it('should allow sync resolution of promises', () => {
|
|
queueZone.run(() => {
|
|
const flush = Zone.current.get('flush');
|
|
const queue = Zone.current.get('queue');
|
|
const p = new Promise<string>(function (resolve, reject) {
|
|
resolve('RValue');
|
|
})
|
|
.then((v: string) => {
|
|
log.push(v);
|
|
return 'second value';
|
|
})
|
|
.then((v: string) => {
|
|
log.push(v);
|
|
});
|
|
expect(queue.length).toEqual(1);
|
|
expect(log).toEqual([]);
|
|
flush();
|
|
expect(log).toEqual(['RValue', 'second value']);
|
|
});
|
|
});
|
|
|
|
it('should allow sync resolution of promises returning promises', () => {
|
|
queueZone.run(() => {
|
|
const flush = Zone.current.get('flush');
|
|
const queue = Zone.current.get('queue');
|
|
const p = new Promise<string>(function (resolve, reject) {
|
|
resolve(Promise.resolve('RValue'));
|
|
})
|
|
.then((v: string) => {
|
|
log.push(v);
|
|
return Promise.resolve('second value');
|
|
})
|
|
.then((v: string) => {
|
|
log.push(v);
|
|
});
|
|
expect(queue.length).toEqual(1);
|
|
expect(log).toEqual([]);
|
|
flush();
|
|
expect(log).toEqual(['RValue', 'second value']);
|
|
});
|
|
});
|
|
|
|
describe('Promise API', function () {
|
|
it('should work with .then', function (done) {
|
|
let resolve: Function | null = null;
|
|
|
|
testZone.run(function () {
|
|
new Promise(function (resolveFn) {
|
|
resolve = resolveFn;
|
|
}).then(function () {
|
|
expect(Zone.current).toBe(testZone);
|
|
done();
|
|
});
|
|
});
|
|
|
|
resolve!();
|
|
});
|
|
|
|
it('should work with .catch', function (done) {
|
|
let reject: (() => void) | null = null;
|
|
|
|
testZone.run(function () {
|
|
new Promise(function (resolveFn, rejectFn) {
|
|
reject = rejectFn;
|
|
})['catch'](function () {
|
|
expect(Zone.current).toBe(testZone);
|
|
done();
|
|
});
|
|
});
|
|
|
|
expect(reject!()).toBe(undefined);
|
|
});
|
|
|
|
it('should work with .finally with resolved promise', function (done) {
|
|
let resolve: Function | null = null;
|
|
|
|
testZone.run(function () {
|
|
(
|
|
new Promise(function (resolveFn) {
|
|
resolve = resolveFn;
|
|
}) as any
|
|
).finally(function () {
|
|
expect(arguments.length).toBe(0);
|
|
expect(Zone.current).toBe(testZone);
|
|
done();
|
|
});
|
|
});
|
|
|
|
resolve!('value');
|
|
});
|
|
|
|
it('should work with .finally with rejected promise', function (done) {
|
|
let reject: Function | null = null;
|
|
|
|
testZone.run(function () {
|
|
(
|
|
new Promise(function (_, rejectFn) {
|
|
reject = rejectFn;
|
|
}) as any
|
|
).finally(function () {
|
|
expect(arguments.length).toBe(0);
|
|
expect(Zone.current).toBe(testZone);
|
|
done();
|
|
});
|
|
});
|
|
|
|
reject!('error');
|
|
});
|
|
|
|
it('should work with Promise.resolve', () => {
|
|
queueZone.run(() => {
|
|
let value: any = null;
|
|
Promise.resolve('resolveValue').then((v) => (value = v));
|
|
expect(Zone.current.get('queue').length).toEqual(1);
|
|
flushMicrotasks();
|
|
expect(value).toEqual('resolveValue');
|
|
});
|
|
});
|
|
|
|
it('should work with Promise.reject', () => {
|
|
queueZone.run(() => {
|
|
let value: any = null;
|
|
Promise.reject('rejectReason')['catch']((v) => (value = v));
|
|
expect(Zone.current.get('queue').length).toEqual(1);
|
|
flushMicrotasks();
|
|
expect(value).toEqual('rejectReason');
|
|
});
|
|
});
|
|
|
|
describe('reject', () => {
|
|
it('should reject promise', () => {
|
|
queueZone.run(() => {
|
|
let value: any = null;
|
|
Promise.reject('rejectReason')['catch']((v) => (value = v));
|
|
flushMicrotasks();
|
|
expect(value).toEqual('rejectReason');
|
|
});
|
|
});
|
|
|
|
it('should re-reject promise', () => {
|
|
queueZone.run(() => {
|
|
let value: any = null;
|
|
Promise.reject('rejectReason')
|
|
['catch']((v) => {
|
|
throw v;
|
|
})
|
|
['catch']((v) => (value = v));
|
|
flushMicrotasks();
|
|
expect(value).toEqual('rejectReason');
|
|
});
|
|
});
|
|
|
|
it('should reject and recover promise', () => {
|
|
queueZone.run(() => {
|
|
let value: any = null;
|
|
Promise.reject('rejectReason')
|
|
['catch']((v) => v)
|
|
.then((v) => (value = v));
|
|
flushMicrotasks();
|
|
expect(value).toEqual('rejectReason');
|
|
});
|
|
});
|
|
|
|
it('should reject if chained promise does not catch promise', () => {
|
|
queueZone.run(() => {
|
|
let value: any = null;
|
|
Promise.reject('rejectReason')
|
|
.then((v) => fail('should not get here'))
|
|
.then(null, (v) => (value = v));
|
|
flushMicrotasks();
|
|
expect(value).toEqual('rejectReason');
|
|
});
|
|
});
|
|
|
|
it('should output error to console if ignoreConsoleErrorUncaughtError is false', async () => {
|
|
await jasmine.spyOnGlobalErrorsAsync(() => {
|
|
const originalConsoleError = console.error;
|
|
Zone.current.fork({name: 'promise-error'}).run(() => {
|
|
(Zone as any)[Zone.__symbol__('ignoreConsoleErrorUncaughtError')] = false;
|
|
console.error = jasmine.createSpy('consoleErr');
|
|
const p = new Promise((resolve, reject) => {
|
|
throw new Error('promise error');
|
|
});
|
|
});
|
|
return new Promise((res) => {
|
|
setTimeout(() => {
|
|
expect(console.error).toHaveBeenCalled();
|
|
console.error = originalConsoleError;
|
|
res();
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
it('should not output error to console if ignoreConsoleErrorUncaughtError is true', async () => {
|
|
await jasmine.spyOnGlobalErrorsAsync(() => {
|
|
const originalConsoleError = console.error;
|
|
Zone.current.fork({name: 'promise-error'}).run(() => {
|
|
(Zone as any)[Zone.__symbol__('ignoreConsoleErrorUncaughtError')] = true;
|
|
console.error = jasmine.createSpy('consoleErr');
|
|
const p = new Promise((resolve, reject) => {
|
|
throw new Error('promise error');
|
|
});
|
|
});
|
|
return new Promise((res) => {
|
|
setTimeout(() => {
|
|
expect(console.error).not.toHaveBeenCalled();
|
|
console.error = originalConsoleError;
|
|
(Zone as any)[Zone.__symbol__('ignoreConsoleErrorUncaughtError')] = false;
|
|
res();
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
it('should notify Zone.onHandleError if no one catches promise', (done) => {
|
|
let promiseError: Error | null = null;
|
|
let zone: Zone | null = null;
|
|
let task: Task | null = null;
|
|
let error: Error | null = null;
|
|
queueZone
|
|
.fork({
|
|
name: 'promise-error',
|
|
onHandleError: (
|
|
delegate: ZoneDelegate,
|
|
current: Zone,
|
|
target: Zone,
|
|
error: any,
|
|
): boolean => {
|
|
promiseError = error;
|
|
delegate.handleError(target, error);
|
|
return false;
|
|
},
|
|
})
|
|
.run(() => {
|
|
zone = Zone.current;
|
|
task = Zone.currentTask;
|
|
error = new Error('rejectedErrorShouldBeHandled');
|
|
try {
|
|
// throw so that the stack trace is captured
|
|
throw error;
|
|
} catch (e) {}
|
|
Promise.reject(error);
|
|
expect(promiseError).toBe(null);
|
|
});
|
|
setTimeout((): any => null);
|
|
setTimeout(() => {
|
|
expect(promiseError!.message).toBe(
|
|
'Uncaught (in promise): ' + error + (error!.stack ? '\n' + error!.stack : ''),
|
|
);
|
|
expect((promiseError as any)['rejection']).toBe(error);
|
|
expect((promiseError as any)['zone']).toBe(zone);
|
|
expect((promiseError as any)['task']).toBe(task);
|
|
done();
|
|
});
|
|
});
|
|
|
|
it('should print readable information when throw a not error object', (done) => {
|
|
let promiseError: Error | null = null;
|
|
let zone: Zone | null = null;
|
|
let task: Task | null = null;
|
|
let rejectObj: TestRejection;
|
|
queueZone
|
|
.fork({
|
|
name: 'promise-error',
|
|
onHandleError: (
|
|
delegate: ZoneDelegate,
|
|
current: Zone,
|
|
target: Zone,
|
|
error: any,
|
|
): boolean => {
|
|
promiseError = error;
|
|
delegate.handleError(target, error);
|
|
return false;
|
|
},
|
|
})
|
|
.run(() => {
|
|
zone = Zone.current;
|
|
task = Zone.currentTask;
|
|
rejectObj = new TestRejection();
|
|
rejectObj.prop1 = 'value1';
|
|
rejectObj.prop2 = 'value2';
|
|
Promise.reject(rejectObj);
|
|
expect(promiseError).toBe(null);
|
|
});
|
|
setTimeout((): any => null);
|
|
setTimeout(() => {
|
|
expect(promiseError!.message).toMatch(
|
|
/Uncaught \(in promise\):.*: {"prop1":"value1","prop2":"value2"}/,
|
|
);
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Promise.race', () => {
|
|
it('should reject the value', () => {
|
|
queueZone.run(() => {
|
|
let value: any = null;
|
|
(Promise as any)
|
|
.race([Promise.reject('rejection1'), 'v1'])
|
|
['catch']((v: any) => (value = v));
|
|
flushMicrotasks();
|
|
expect(value).toEqual('rejection1');
|
|
});
|
|
});
|
|
|
|
it('should resolve the value', () => {
|
|
queueZone.run(() => {
|
|
let value: any = null;
|
|
(Promise as any)
|
|
.race([Promise.resolve('resolution'), 'v1'])
|
|
.then((v: any) => (value = v));
|
|
flushMicrotasks();
|
|
expect(value).toEqual('resolution');
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Promise.all', () => {
|
|
it('should reject the value', () => {
|
|
queueZone.run(() => {
|
|
let value: any = null;
|
|
Promise.all([Promise.reject('rejection'), 'v1'])['catch']((v: any) => (value = v));
|
|
flushMicrotasks();
|
|
expect(value).toEqual('rejection');
|
|
});
|
|
});
|
|
|
|
it('should resolve with the sync then operation', () => {
|
|
queueZone.run(() => {
|
|
let value: any = null;
|
|
const p1 = {
|
|
then: function (thenCallback: Function) {
|
|
return thenCallback('p1');
|
|
},
|
|
};
|
|
const p2 = {
|
|
then: function (thenCallback: Function) {
|
|
return thenCallback('p2');
|
|
},
|
|
};
|
|
Promise.all([p1, 'v1', p2]).then((v: any) => (value = v));
|
|
flushMicrotasks();
|
|
expect(value).toEqual(['p1', 'v1', 'p2']);
|
|
});
|
|
});
|
|
|
|
it(
|
|
'should resolve generators',
|
|
ifEnvSupports(
|
|
() => {
|
|
return isNode;
|
|
},
|
|
() => {
|
|
const generators: any = function* () {
|
|
yield Promise.resolve(1);
|
|
yield Promise.resolve(2);
|
|
return;
|
|
};
|
|
queueZone.run(() => {
|
|
let value: any = null;
|
|
Promise.all(generators()).then((val) => {
|
|
value = val;
|
|
});
|
|
flushMicrotasks();
|
|
expect(value).toEqual([1, 2]);
|
|
});
|
|
},
|
|
),
|
|
);
|
|
|
|
it('should handle object with a truthy `then property`', () => {
|
|
queueZone.run(() => {
|
|
let value: any = null;
|
|
Promise.all([{then: 123}]).then((v: any) => (value = v));
|
|
flushMicrotasks();
|
|
expect(value).toEqual([jasmine.objectContaining({then: 123})]);
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Promise subclasses', function () {
|
|
class MyPromise<T> {
|
|
private _promise: Promise<any>;
|
|
constructor(init: any) {
|
|
this._promise = new Promise(init);
|
|
}
|
|
|
|
catch<TResult = never>(
|
|
onrejected?: ((reason: any) => TResult | PromiseLike<TResult>) | undefined | null,
|
|
): Promise<T | TResult> {
|
|
return this._promise.catch.call(this._promise, onrejected);
|
|
}
|
|
|
|
then<TResult1 = T, TResult2 = never>(
|
|
onfulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | undefined | null,
|
|
onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | undefined | null,
|
|
): Promise<any> {
|
|
return this._promise.then.call(this._promise, onfulfilled, onrejected);
|
|
}
|
|
}
|
|
|
|
const setPrototypeOf =
|
|
(Object as any).setPrototypeOf ||
|
|
function (obj: any, proto: any) {
|
|
obj.__proto__ = proto;
|
|
return obj;
|
|
};
|
|
|
|
setPrototypeOf(MyPromise.prototype, Promise.prototype);
|
|
|
|
it('should reject if the Promise subclass rejects', function () {
|
|
const myPromise = new MyPromise(function (resolve: any, reject: any): void {
|
|
reject('foo');
|
|
});
|
|
|
|
return Promise.resolve()
|
|
.then(function () {
|
|
return myPromise;
|
|
})
|
|
.then(
|
|
function () {
|
|
throw new Error('Unexpected resolution');
|
|
},
|
|
function (result) {
|
|
expect(result).toBe('foo');
|
|
},
|
|
);
|
|
});
|
|
|
|
function testPromiseSubClass(done?: Function) {
|
|
const myPromise = new MyPromise(function (resolve: any, reject: Function) {
|
|
resolve('foo');
|
|
});
|
|
|
|
return Promise.resolve()
|
|
.then(function () {
|
|
return myPromise;
|
|
})
|
|
.then(function (result) {
|
|
expect(result).toBe('foo');
|
|
done && done();
|
|
});
|
|
}
|
|
|
|
it(
|
|
'should resolve if the Promise subclass resolves',
|
|
jasmine
|
|
? function (done) {
|
|
testPromiseSubClass(done);
|
|
}
|
|
: function () {
|
|
testPromiseSubClass();
|
|
},
|
|
);
|
|
});
|
|
|
|
describe('Promise.any', () => {
|
|
const any = (Promise as any).any;
|
|
it('undefined parameters', (done: DoneFn) => {
|
|
any().then(
|
|
() => {
|
|
fail('should not get a resolved promise.');
|
|
},
|
|
(err: any) => {
|
|
expect(err.message).toEqual('All promises were rejected');
|
|
expect(err.errors).toEqual([]);
|
|
done();
|
|
},
|
|
);
|
|
});
|
|
it('invalid iterable', (done: DoneFn) => {
|
|
const invalidIterable: any = {};
|
|
invalidIterable[Symbol.iterator] = () => 2;
|
|
any(invalidIterable).then(
|
|
() => {
|
|
fail('should not get a resolved promise.');
|
|
},
|
|
(err: any) => {
|
|
expect(err.message).toEqual('All promises were rejected');
|
|
expect(err.errors).toEqual([]);
|
|
done();
|
|
},
|
|
);
|
|
});
|
|
it('empty parameters', (done: DoneFn) => {
|
|
any([]).then(
|
|
() => {
|
|
fail('should not get a resolved promise.');
|
|
},
|
|
(err: any) => {
|
|
expect(err.message).toEqual('All promises were rejected');
|
|
expect(err.errors).toEqual([]);
|
|
done();
|
|
},
|
|
);
|
|
});
|
|
it('non promises parameters', (done: DoneFn) => {
|
|
any([1, 'test']).then(
|
|
(v: any) => {
|
|
expect(v).toBe(1);
|
|
done();
|
|
},
|
|
(err: any) => {
|
|
fail('should not get a rejected promise.');
|
|
},
|
|
);
|
|
});
|
|
it('mixed parameters, non promise first', (done: DoneFn) => {
|
|
any([1, Promise.resolve(2)]).then(
|
|
(v: any) => {
|
|
expect(v).toBe(1);
|
|
done();
|
|
},
|
|
(err: any) => {
|
|
fail('should not get a rejected promise.');
|
|
},
|
|
);
|
|
});
|
|
it('mixed parameters, promise first', (done: DoneFn) => {
|
|
any([Promise.resolve(1), 2]).then(
|
|
(v: any) => {
|
|
expect(v).toBe(1);
|
|
done();
|
|
},
|
|
(err: any) => {
|
|
fail('should not get a rejected promise.');
|
|
},
|
|
);
|
|
});
|
|
it('all ok promises', (done: DoneFn) => {
|
|
any([Promise.resolve(1), Promise.resolve(2)]).then(
|
|
(v: any) => {
|
|
expect(v).toBe(1);
|
|
done();
|
|
},
|
|
(err: any) => {
|
|
fail('should not get a rejected promise.');
|
|
},
|
|
);
|
|
});
|
|
it('all promises, first rejected', (done: DoneFn) => {
|
|
any([Promise.reject('error'), Promise.resolve(2)]).then(
|
|
(v: any) => {
|
|
expect(v).toBe(2);
|
|
done();
|
|
},
|
|
(err: any) => {
|
|
fail('should not get a rejected promise.');
|
|
},
|
|
);
|
|
});
|
|
it('all promises, second rejected', (done: DoneFn) => {
|
|
any([Promise.resolve(1), Promise.reject('error')]).then(
|
|
(v: any) => {
|
|
expect(v).toBe(1);
|
|
done();
|
|
},
|
|
(err: any) => {
|
|
fail('should not get a rejected promise.');
|
|
},
|
|
);
|
|
});
|
|
it('all rejected promises', (done: DoneFn) => {
|
|
any([Promise.reject('error1'), Promise.reject('error2')]).then(
|
|
(v: any) => {
|
|
fail('should not get a resolved promise.');
|
|
},
|
|
(err: any) => {
|
|
expect(err.message).toEqual('All promises were rejected');
|
|
expect(err.errors).toEqual(['error1', 'error2']);
|
|
done();
|
|
},
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('Promise.allSettled', () => {
|
|
const yes = function makeFulfilledResult(value: any) {
|
|
return {status: 'fulfilled', value: value};
|
|
};
|
|
const no = function makeRejectedResult(reason: any) {
|
|
return {status: 'rejected', reason: reason};
|
|
};
|
|
const a = {};
|
|
const b = {};
|
|
const c = {};
|
|
const allSettled = (Promise as any).allSettled;
|
|
it('no promise values', (done: DoneFn) => {
|
|
allSettled([a, b, c]).then((results: any[]) => {
|
|
expect(results).toEqual([yes(a), yes(b), yes(c)]);
|
|
done();
|
|
});
|
|
});
|
|
it('all fulfilled', (done: DoneFn) => {
|
|
allSettled([Promise.resolve(a), Promise.resolve(b), Promise.resolve(c)]).then(
|
|
(results: any[]) => {
|
|
expect(results).toEqual([yes(a), yes(b), yes(c)]);
|
|
done();
|
|
},
|
|
);
|
|
});
|
|
it('all rejected', (done: DoneFn) => {
|
|
allSettled([Promise.reject(a), Promise.reject(b), Promise.reject(c)]).then(
|
|
(results: any[]) => {
|
|
expect(results).toEqual([no(a), no(b), no(c)]);
|
|
done();
|
|
},
|
|
);
|
|
});
|
|
it('mixed', (done: DoneFn) => {
|
|
allSettled([a, Promise.resolve(b), Promise.reject(c)]).then((results: any[]) => {
|
|
expect(results).toEqual([yes(a), yes(b), no(c)]);
|
|
done();
|
|
});
|
|
});
|
|
it('mixed should in zone', (done: DoneFn) => {
|
|
const zone = Zone.current.fork({name: 'settled'});
|
|
const bPromise = Promise.resolve(b);
|
|
const cPromise = Promise.reject(c);
|
|
zone.run(() => {
|
|
allSettled([a, bPromise, cPromise]).then((results: any[]) => {
|
|
expect(results).toEqual([yes(a), yes(b), no(c)]);
|
|
expect(Zone.current.name).toEqual(zone.name);
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
it('poisoned .then', (done: DoneFn) => {
|
|
const promise = new Promise(function () {});
|
|
promise.then = function () {
|
|
throw new EvalError();
|
|
};
|
|
allSettled([promise]).then(
|
|
() => {
|
|
fail('should not reach here');
|
|
},
|
|
(reason: any) => {
|
|
expect(reason instanceof EvalError).toBe(true);
|
|
done();
|
|
},
|
|
);
|
|
});
|
|
const Subclass = (function () {
|
|
try {
|
|
// eslint-disable-next-line no-new-func
|
|
return Function(
|
|
'class Subclass extends Promise { constructor(...args) { super(...args); this.thenArgs = []; } then(...args) { Subclass.thenArgs.push(args); this.thenArgs.push(args); return super.then(...args); } } Subclass.thenArgs = []; return Subclass;',
|
|
)();
|
|
} catch (e) {
|
|
/**/
|
|
}
|
|
|
|
return false;
|
|
})();
|
|
|
|
describe('inheritance', () => {
|
|
it('preserves correct subclass', () => {
|
|
const promise = allSettled.call(Subclass, [1]);
|
|
expect(promise instanceof Subclass).toBe(true);
|
|
expect(promise.constructor).toEqual(Subclass);
|
|
});
|
|
|
|
it('invoke the subclass', () => {
|
|
Subclass.thenArgs.length = 0;
|
|
|
|
const original = Subclass.resolve();
|
|
expect(Subclass.thenArgs.length).toBe(0);
|
|
expect(original.thenArgs.length).toBe(0);
|
|
|
|
allSettled.call(Subclass, [original]);
|
|
|
|
expect(original.thenArgs.length).toBe(1);
|
|
expect(Subclass.thenArgs.length).toBe(1);
|
|
});
|
|
});
|
|
|
|
describe('resolve/reject multiple times', () => {
|
|
it('should ignore second resolve', (done) => {
|
|
const nested = new Promise((res) => setTimeout(() => res('nested')));
|
|
const p = new Promise((res) => {
|
|
res(1);
|
|
res(nested);
|
|
});
|
|
p.then((v) => {
|
|
expect(v).toBe(1);
|
|
done();
|
|
});
|
|
});
|
|
it('should ignore second reject', (done) => {
|
|
const p = new Promise((res, rej) => {
|
|
rej(1);
|
|
rej(2);
|
|
});
|
|
p.then(
|
|
(v) => {
|
|
fail('should not get here');
|
|
},
|
|
(err) => {
|
|
expect(err).toBe(1);
|
|
done();
|
|
},
|
|
);
|
|
});
|
|
it('should ignore resolve after reject', (done) => {
|
|
const p = new Promise((res, rej) => {
|
|
rej(1);
|
|
res(2);
|
|
});
|
|
p.then(
|
|
(v) => {
|
|
fail('should not get here');
|
|
},
|
|
(err) => {
|
|
expect(err).toBe(1);
|
|
done();
|
|
},
|
|
);
|
|
});
|
|
it('should ignore reject after resolve', (done) => {
|
|
const nested = new Promise((res) => setTimeout(() => res('nested')));
|
|
const p = new Promise((res, rej) => {
|
|
res(nested);
|
|
rej(1);
|
|
});
|
|
p.then(
|
|
(v) => {
|
|
expect(v).toBe('nested');
|
|
done();
|
|
},
|
|
(err) => {
|
|
fail('should not be here');
|
|
},
|
|
);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Promise.withResolvers', () => {
|
|
it('should resolve', (done: DoneFn) => {
|
|
const {promise, resolve, reject} = (Promise as any).withResolvers();
|
|
promise.then((v: any) => {
|
|
expect(v).toBe(1);
|
|
done();
|
|
});
|
|
resolve(1);
|
|
});
|
|
it('should reject', (done: DoneFn) => {
|
|
const {promise, resolve, reject} = (Promise as any).withResolvers();
|
|
const error = new Error('test');
|
|
promise.catch((e: any) => {
|
|
expect(e).toBe(error);
|
|
done();
|
|
});
|
|
reject(error);
|
|
});
|
|
});
|
|
|
|
describe('Promise.try', () => {
|
|
it('should resolve', (done: DoneFn) => {
|
|
(Promise as any)
|
|
.try(() => 1)
|
|
.then((v: any) => {
|
|
expect(v).toBe(1);
|
|
done();
|
|
});
|
|
});
|
|
it('should reject on throw', (done: DoneFn) => {
|
|
const error = new Error('test');
|
|
(Promise as any)
|
|
.try(() => {
|
|
throw error;
|
|
})
|
|
.catch((e: any) => {
|
|
expect(e).toBe(error);
|
|
done();
|
|
});
|
|
});
|
|
it('should execute in the correct zone', (done: DoneFn) => {
|
|
const zone = Zone.current.fork({name: 'promise-try'});
|
|
zone.run(() => {
|
|
(Promise as any)
|
|
.try(() => 1)
|
|
.then(() => {
|
|
expect(Zone.current.name).toEqual(zone.name);
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
});
|
|
}),
|
|
);
|