2019-05-31 11:56:07 -04:00
|
|
|
/**
|
|
|
|
* @license
|
2020-05-19 15:08:49 -04:00
|
|
|
* Copyright Google LLC All Rights Reserved.
|
2019-05-31 11:56:07 -04:00
|
|
|
*
|
|
|
|
* 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
|
|
|
|
*/
|
|
|
|
|
|
|
|
(function(global: any) {
|
2020-04-13 19:40:21 -04:00
|
|
|
interface ScheduledFunction {
|
|
|
|
endTime: number;
|
|
|
|
id: number;
|
|
|
|
func: Function;
|
|
|
|
args: any[];
|
|
|
|
delay: number;
|
|
|
|
isPeriodic: boolean;
|
|
|
|
isRequestAnimationFrame: boolean;
|
|
|
|
}
|
|
|
|
|
|
|
|
interface MicroTaskScheduledFunction {
|
|
|
|
func: Function;
|
|
|
|
args?: any[];
|
|
|
|
target: any;
|
|
|
|
}
|
|
|
|
|
|
|
|
interface MacroTaskOptions {
|
|
|
|
source: string;
|
|
|
|
isPeriodic?: boolean;
|
|
|
|
callbackArgs?: any;
|
|
|
|
}
|
|
|
|
|
|
|
|
const OriginalDate = global.Date;
|
feat(zone.js): upgrade zone.js to angular package format(APF) (#36540)
Close #35157
In the current version of zone.js, zone.js uses it's own package format, and it is not following the rule
of Angualr package format(APF), so it is not easily to be consumed by Angular CLI or other bundle tools.
For example, zone.js npm package has two bundles,
1. zone.js/dist/zone.js, this is a `es5` bundle.
2. zone.js/dist/zone-evergreen.js, this is a `es2015` bundle.
And Angular CLI has to add some hard-coding code to handle this case, ohttps://github.com/angular/angular-cli/blob/5376a8b1392ac7bd252782d8474161ce03a4d1cb/packages/schematics/angular/application/files/src/polyfills.ts.template#L55-L58
This PR upgrade zone.js npm package format to follow APF rule, https://docs.google.com/document/d/1CZC2rcpxffTDfRDs6p1cfbmKNLA6x5O-NtkJglDaBVs/edit#heading=h.k0mh3o8u5hx
The updated points are:
1. in package.json, update all bundle related properties
```
"main": "./bundles/zone.umd.js",
"module": "./fesm2015/zone.js",
"es2015": "./fesm2015/zone.js",
"fesm2015": "./fesm2015/zone.js",
```
2. re-organize dist folder, for example for `zone.js` bundle, now we have
```
dist/
bundles/
zone.js // this is the es5 bundle
fesm2015/
zone.js // this is the es2015 bundle (in the old version is `zone-evergreen.js`)
```
3. have several sub-packages.
1. `zone-testing`, provide zone-testing bundles include zone.js and testing libraries
2. `zone-node`, provide zone.js implemention for NodeJS
3. `zone-mix`, provide zone.js patches for both Browser and NodeJS
All those sub-packages will have their own `package.json` and the bundle will reference `bundles(es5)` and `fesm2015(es2015)`.
4. keep backward compatibility, still keep the `zone.js/dist` folder, and all bundles will be redirected to `zone.js/bundles` or `zone.js/fesm2015` folders.
PR Close #36540
2020-05-16 21:53:03 -04:00
|
|
|
// Since when we compile this file to `es2015`, and if we define
|
|
|
|
// this `FakeDate` as `class FakeDate`, and then set `FakeDate.prototype`
|
|
|
|
// there will be an error which is `Cannot assign to read only property 'prototype'`
|
|
|
|
// so we need to use function implementation here.
|
|
|
|
function FakeDate() {
|
|
|
|
if (arguments.length === 0) {
|
|
|
|
const d = new OriginalDate();
|
|
|
|
d.setTime(FakeDate.now());
|
|
|
|
return d;
|
|
|
|
} else {
|
|
|
|
const args = Array.prototype.slice.call(arguments);
|
|
|
|
return new OriginalDate(...args);
|
2019-05-31 11:56:07 -04:00
|
|
|
}
|
feat(zone.js): upgrade zone.js to angular package format(APF) (#36540)
Close #35157
In the current version of zone.js, zone.js uses it's own package format, and it is not following the rule
of Angualr package format(APF), so it is not easily to be consumed by Angular CLI or other bundle tools.
For example, zone.js npm package has two bundles,
1. zone.js/dist/zone.js, this is a `es5` bundle.
2. zone.js/dist/zone-evergreen.js, this is a `es2015` bundle.
And Angular CLI has to add some hard-coding code to handle this case, ohttps://github.com/angular/angular-cli/blob/5376a8b1392ac7bd252782d8474161ce03a4d1cb/packages/schematics/angular/application/files/src/polyfills.ts.template#L55-L58
This PR upgrade zone.js npm package format to follow APF rule, https://docs.google.com/document/d/1CZC2rcpxffTDfRDs6p1cfbmKNLA6x5O-NtkJglDaBVs/edit#heading=h.k0mh3o8u5hx
The updated points are:
1. in package.json, update all bundle related properties
```
"main": "./bundles/zone.umd.js",
"module": "./fesm2015/zone.js",
"es2015": "./fesm2015/zone.js",
"fesm2015": "./fesm2015/zone.js",
```
2. re-organize dist folder, for example for `zone.js` bundle, now we have
```
dist/
bundles/
zone.js // this is the es5 bundle
fesm2015/
zone.js // this is the es2015 bundle (in the old version is `zone-evergreen.js`)
```
3. have several sub-packages.
1. `zone-testing`, provide zone-testing bundles include zone.js and testing libraries
2. `zone-node`, provide zone.js implemention for NodeJS
3. `zone-mix`, provide zone.js patches for both Browser and NodeJS
All those sub-packages will have their own `package.json` and the bundle will reference `bundles(es5)` and `fesm2015(es2015)`.
4. keep backward compatibility, still keep the `zone.js/dist` folder, and all bundles will be redirected to `zone.js/bundles` or `zone.js/fesm2015` folders.
PR Close #36540
2020-05-16 21:53:03 -04:00
|
|
|
}
|
2019-05-31 11:56:07 -04:00
|
|
|
|
2020-10-05 23:45:16 -04:00
|
|
|
FakeDate.now = function(this: unknown) {
|
feat(zone.js): upgrade zone.js to angular package format(APF) (#36540)
Close #35157
In the current version of zone.js, zone.js uses it's own package format, and it is not following the rule
of Angualr package format(APF), so it is not easily to be consumed by Angular CLI or other bundle tools.
For example, zone.js npm package has two bundles,
1. zone.js/dist/zone.js, this is a `es5` bundle.
2. zone.js/dist/zone-evergreen.js, this is a `es2015` bundle.
And Angular CLI has to add some hard-coding code to handle this case, ohttps://github.com/angular/angular-cli/blob/5376a8b1392ac7bd252782d8474161ce03a4d1cb/packages/schematics/angular/application/files/src/polyfills.ts.template#L55-L58
This PR upgrade zone.js npm package format to follow APF rule, https://docs.google.com/document/d/1CZC2rcpxffTDfRDs6p1cfbmKNLA6x5O-NtkJglDaBVs/edit#heading=h.k0mh3o8u5hx
The updated points are:
1. in package.json, update all bundle related properties
```
"main": "./bundles/zone.umd.js",
"module": "./fesm2015/zone.js",
"es2015": "./fesm2015/zone.js",
"fesm2015": "./fesm2015/zone.js",
```
2. re-organize dist folder, for example for `zone.js` bundle, now we have
```
dist/
bundles/
zone.js // this is the es5 bundle
fesm2015/
zone.js // this is the es2015 bundle (in the old version is `zone-evergreen.js`)
```
3. have several sub-packages.
1. `zone-testing`, provide zone-testing bundles include zone.js and testing libraries
2. `zone-node`, provide zone.js implemention for NodeJS
3. `zone-mix`, provide zone.js patches for both Browser and NodeJS
All those sub-packages will have their own `package.json` and the bundle will reference `bundles(es5)` and `fesm2015(es2015)`.
4. keep backward compatibility, still keep the `zone.js/dist` folder, and all bundles will be redirected to `zone.js/bundles` or `zone.js/fesm2015` folders.
PR Close #36540
2020-05-16 21:53:03 -04:00
|
|
|
const fakeAsyncTestZoneSpec = Zone.current.get('FakeAsyncTestZoneSpec');
|
|
|
|
if (fakeAsyncTestZoneSpec) {
|
2020-10-05 23:45:16 -04:00
|
|
|
return fakeAsyncTestZoneSpec.getFakeSystemTime();
|
2020-04-13 19:40:21 -04:00
|
|
|
}
|
feat(zone.js): upgrade zone.js to angular package format(APF) (#36540)
Close #35157
In the current version of zone.js, zone.js uses it's own package format, and it is not following the rule
of Angualr package format(APF), so it is not easily to be consumed by Angular CLI or other bundle tools.
For example, zone.js npm package has two bundles,
1. zone.js/dist/zone.js, this is a `es5` bundle.
2. zone.js/dist/zone-evergreen.js, this is a `es2015` bundle.
And Angular CLI has to add some hard-coding code to handle this case, ohttps://github.com/angular/angular-cli/blob/5376a8b1392ac7bd252782d8474161ce03a4d1cb/packages/schematics/angular/application/files/src/polyfills.ts.template#L55-L58
This PR upgrade zone.js npm package format to follow APF rule, https://docs.google.com/document/d/1CZC2rcpxffTDfRDs6p1cfbmKNLA6x5O-NtkJglDaBVs/edit#heading=h.k0mh3o8u5hx
The updated points are:
1. in package.json, update all bundle related properties
```
"main": "./bundles/zone.umd.js",
"module": "./fesm2015/zone.js",
"es2015": "./fesm2015/zone.js",
"fesm2015": "./fesm2015/zone.js",
```
2. re-organize dist folder, for example for `zone.js` bundle, now we have
```
dist/
bundles/
zone.js // this is the es5 bundle
fesm2015/
zone.js // this is the es2015 bundle (in the old version is `zone-evergreen.js`)
```
3. have several sub-packages.
1. `zone-testing`, provide zone-testing bundles include zone.js and testing libraries
2. `zone-node`, provide zone.js implemention for NodeJS
3. `zone-mix`, provide zone.js patches for both Browser and NodeJS
All those sub-packages will have their own `package.json` and the bundle will reference `bundles(es5)` and `fesm2015(es2015)`.
4. keep backward compatibility, still keep the `zone.js/dist` folder, and all bundles will be redirected to `zone.js/bundles` or `zone.js/fesm2015` folders.
PR Close #36540
2020-05-16 21:53:03 -04:00
|
|
|
return OriginalDate.now.apply(this, arguments);
|
2020-10-05 23:45:16 -04:00
|
|
|
};
|
2020-04-13 19:40:21 -04:00
|
|
|
|
2020-10-05 23:45:16 -04:00
|
|
|
FakeDate.UTC = OriginalDate.UTC;
|
feat(zone.js): upgrade zone.js to angular package format(APF) (#36540)
Close #35157
In the current version of zone.js, zone.js uses it's own package format, and it is not following the rule
of Angualr package format(APF), so it is not easily to be consumed by Angular CLI or other bundle tools.
For example, zone.js npm package has two bundles,
1. zone.js/dist/zone.js, this is a `es5` bundle.
2. zone.js/dist/zone-evergreen.js, this is a `es2015` bundle.
And Angular CLI has to add some hard-coding code to handle this case, ohttps://github.com/angular/angular-cli/blob/5376a8b1392ac7bd252782d8474161ce03a4d1cb/packages/schematics/angular/application/files/src/polyfills.ts.template#L55-L58
This PR upgrade zone.js npm package format to follow APF rule, https://docs.google.com/document/d/1CZC2rcpxffTDfRDs6p1cfbmKNLA6x5O-NtkJglDaBVs/edit#heading=h.k0mh3o8u5hx
The updated points are:
1. in package.json, update all bundle related properties
```
"main": "./bundles/zone.umd.js",
"module": "./fesm2015/zone.js",
"es2015": "./fesm2015/zone.js",
"fesm2015": "./fesm2015/zone.js",
```
2. re-organize dist folder, for example for `zone.js` bundle, now we have
```
dist/
bundles/
zone.js // this is the es5 bundle
fesm2015/
zone.js // this is the es2015 bundle (in the old version is `zone-evergreen.js`)
```
3. have several sub-packages.
1. `zone-testing`, provide zone-testing bundles include zone.js and testing libraries
2. `zone-node`, provide zone.js implemention for NodeJS
3. `zone-mix`, provide zone.js patches for both Browser and NodeJS
All those sub-packages will have their own `package.json` and the bundle will reference `bundles(es5)` and `fesm2015(es2015)`.
4. keep backward compatibility, still keep the `zone.js/dist` folder, and all bundles will be redirected to `zone.js/bundles` or `zone.js/fesm2015` folders.
PR Close #36540
2020-05-16 21:53:03 -04:00
|
|
|
FakeDate.parse = OriginalDate.parse;
|
2020-04-13 19:40:21 -04:00
|
|
|
|
|
|
|
// keep a reference for zone patched timer function
|
|
|
|
const timers = {
|
|
|
|
setTimeout: global.setTimeout,
|
|
|
|
setInterval: global.setInterval,
|
|
|
|
clearTimeout: global.clearTimeout,
|
|
|
|
clearInterval: global.clearInterval
|
|
|
|
};
|
|
|
|
|
|
|
|
class Scheduler {
|
|
|
|
// Next scheduler id.
|
|
|
|
public static nextId: number = 1;
|
|
|
|
|
|
|
|
// Scheduler queue with the tuple of end time and callback function - sorted by end time.
|
|
|
|
private _schedulerQueue: ScheduledFunction[] = [];
|
|
|
|
// Current simulated time in millis.
|
2020-10-05 23:45:16 -04:00
|
|
|
private _currentTickTime: number = 0;
|
|
|
|
// Current fake system base time in millis.
|
|
|
|
private _currentFakeBaseSystemTime: number = OriginalDate.now();
|
2020-04-13 19:40:21 -04:00
|
|
|
// track requeuePeriodicTimer
|
|
|
|
private _currentTickRequeuePeriodicEntries: any[] = [];
|
|
|
|
|
|
|
|
constructor() {}
|
|
|
|
|
2020-10-05 23:45:16 -04:00
|
|
|
getCurrentTickTime() {
|
|
|
|
return this._currentTickTime;
|
2020-04-13 19:40:21 -04:00
|
|
|
}
|
2019-05-31 11:56:07 -04:00
|
|
|
|
2020-10-05 23:45:16 -04:00
|
|
|
getFakeSystemTime() {
|
|
|
|
return this._currentFakeBaseSystemTime + this._currentTickTime;
|
2020-04-13 19:40:21 -04:00
|
|
|
}
|
2019-12-02 10:56:56 -05:00
|
|
|
|
2020-10-05 23:45:16 -04:00
|
|
|
setFakeBaseSystemTime(fakeBaseSystemTime: number) {
|
|
|
|
this._currentFakeBaseSystemTime = fakeBaseSystemTime;
|
2020-04-13 19:40:21 -04:00
|
|
|
}
|
2019-05-31 11:56:07 -04:00
|
|
|
|
2020-09-22 09:52:20 -04:00
|
|
|
getRealSystemTime() {
|
|
|
|
return OriginalDate.now();
|
|
|
|
}
|
|
|
|
|
2020-04-13 19:40:21 -04:00
|
|
|
scheduleFunction(cb: Function, delay: number, options?: {
|
|
|
|
args?: any[],
|
|
|
|
isPeriodic?: boolean,
|
|
|
|
isRequestAnimationFrame?: boolean,
|
|
|
|
id?: number,
|
|
|
|
isRequeuePeriodic?: boolean
|
|
|
|
}): number {
|
|
|
|
options = {
|
|
|
|
...{
|
|
|
|
args: [],
|
|
|
|
isPeriodic: false,
|
|
|
|
isRequestAnimationFrame: false,
|
|
|
|
id: -1,
|
|
|
|
isRequeuePeriodic: false
|
|
|
|
},
|
|
|
|
...options
|
|
|
|
};
|
|
|
|
let currentId = options.id! < 0 ? Scheduler.nextId++ : options.id!;
|
2020-10-05 23:45:16 -04:00
|
|
|
let endTime = this._currentTickTime + delay;
|
2020-04-13 19:40:21 -04:00
|
|
|
|
|
|
|
// Insert so that scheduler queue remains sorted by end time.
|
|
|
|
let newEntry: ScheduledFunction = {
|
|
|
|
endTime: endTime,
|
|
|
|
id: currentId,
|
|
|
|
func: cb,
|
|
|
|
args: options.args!,
|
|
|
|
delay: delay,
|
|
|
|
isPeriodic: options.isPeriodic!,
|
|
|
|
isRequestAnimationFrame: options.isRequestAnimationFrame!
|
|
|
|
};
|
|
|
|
if (options.isRequeuePeriodic!) {
|
|
|
|
this._currentTickRequeuePeriodicEntries.push(newEntry);
|
|
|
|
}
|
|
|
|
let i = 0;
|
|
|
|
for (; i < this._schedulerQueue.length; i++) {
|
|
|
|
let currentEntry = this._schedulerQueue[i];
|
|
|
|
if (newEntry.endTime < currentEntry.endTime) {
|
|
|
|
break;
|
2019-05-31 11:56:07 -04:00
|
|
|
}
|
|
|
|
}
|
2020-04-13 19:40:21 -04:00
|
|
|
this._schedulerQueue.splice(i, 0, newEntry);
|
|
|
|
return currentId;
|
|
|
|
}
|
2019-05-31 11:56:07 -04:00
|
|
|
|
2020-04-13 19:40:21 -04:00
|
|
|
removeScheduledFunctionWithId(id: number): void {
|
|
|
|
for (let i = 0; i < this._schedulerQueue.length; i++) {
|
|
|
|
if (this._schedulerQueue[i].id == id) {
|
|
|
|
this._schedulerQueue.splice(i, 1);
|
|
|
|
break;
|
2019-05-31 11:56:07 -04:00
|
|
|
}
|
|
|
|
}
|
2020-04-13 19:40:21 -04:00
|
|
|
}
|
2019-05-31 11:56:07 -04:00
|
|
|
|
2020-09-22 09:52:20 -04:00
|
|
|
removeAll(): void {
|
|
|
|
this._schedulerQueue = [];
|
|
|
|
}
|
|
|
|
|
|
|
|
getTimerCount(): number {
|
|
|
|
return this._schedulerQueue.length;
|
|
|
|
}
|
|
|
|
|
|
|
|
tickToNext(step: number = 1, doTick?: (elapsed: number) => void, tickOptions?: {
|
|
|
|
processNewMacroTasksSynchronously: boolean
|
|
|
|
}) {
|
|
|
|
if (this._schedulerQueue.length < step) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
// Find the last task currently queued in the scheduler queue and tick
|
|
|
|
// till that time.
|
2020-10-05 23:45:16 -04:00
|
|
|
const startTime = this._currentTickTime;
|
2020-09-22 09:52:20 -04:00
|
|
|
const targetTask = this._schedulerQueue[step - 1];
|
|
|
|
this.tick(targetTask.endTime - startTime, doTick, tickOptions);
|
|
|
|
}
|
|
|
|
|
2020-04-13 19:40:21 -04:00
|
|
|
tick(millis: number = 0, doTick?: (elapsed: number) => void, tickOptions?: {
|
|
|
|
processNewMacroTasksSynchronously: boolean
|
|
|
|
}): void {
|
2020-10-05 23:45:16 -04:00
|
|
|
let finalTime = this._currentTickTime + millis;
|
2020-04-13 19:40:21 -04:00
|
|
|
let lastCurrentTime = 0;
|
|
|
|
tickOptions = Object.assign({processNewMacroTasksSynchronously: true}, tickOptions);
|
|
|
|
// we need to copy the schedulerQueue so nested timeout
|
|
|
|
// will not be wrongly called in the current tick
|
|
|
|
// https://github.com/angular/angular/issues/33799
|
|
|
|
const schedulerQueue = tickOptions.processNewMacroTasksSynchronously ?
|
|
|
|
this._schedulerQueue :
|
|
|
|
this._schedulerQueue.slice();
|
|
|
|
if (schedulerQueue.length === 0 && doTick) {
|
|
|
|
doTick(millis);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
while (schedulerQueue.length > 0) {
|
|
|
|
// clear requeueEntries before each loop
|
|
|
|
this._currentTickRequeuePeriodicEntries = [];
|
|
|
|
let current = schedulerQueue[0];
|
|
|
|
if (finalTime < current.endTime) {
|
|
|
|
// Done processing the queue since it's sorted by endTime.
|
|
|
|
break;
|
|
|
|
} else {
|
|
|
|
// Time to run scheduled function. Remove it from the head of queue.
|
|
|
|
let current = schedulerQueue.shift()!;
|
|
|
|
if (!tickOptions.processNewMacroTasksSynchronously) {
|
|
|
|
const idx = this._schedulerQueue.indexOf(current);
|
|
|
|
if (idx >= 0) {
|
|
|
|
this._schedulerQueue.splice(idx, 1);
|
|
|
|
}
|
2019-05-31 11:56:07 -04:00
|
|
|
}
|
2020-10-05 23:45:16 -04:00
|
|
|
lastCurrentTime = this._currentTickTime;
|
|
|
|
this._currentTickTime = current.endTime;
|
2019-05-31 11:56:07 -04:00
|
|
|
if (doTick) {
|
2020-10-05 23:45:16 -04:00
|
|
|
doTick(this._currentTickTime - lastCurrentTime);
|
2019-05-31 11:56:07 -04:00
|
|
|
}
|
2020-04-13 19:40:21 -04:00
|
|
|
let retval = current.func.apply(
|
2020-10-05 23:45:16 -04:00
|
|
|
global, current.isRequestAnimationFrame ? [this._currentTickTime] : current.args);
|
2019-05-31 11:56:07 -04:00
|
|
|
if (!retval) {
|
|
|
|
// Uncaught exception in the current scheduled function. Stop processing the queue.
|
|
|
|
break;
|
|
|
|
}
|
2020-04-13 19:40:21 -04:00
|
|
|
|
|
|
|
// check is there any requeue periodic entry is added in
|
|
|
|
// current loop, if there is, we need to add to current loop
|
|
|
|
if (!tickOptions.processNewMacroTasksSynchronously) {
|
|
|
|
this._currentTickRequeuePeriodicEntries.forEach(newEntry => {
|
|
|
|
let i = 0;
|
|
|
|
for (; i < schedulerQueue.length; i++) {
|
|
|
|
const currentEntry = schedulerQueue[i];
|
|
|
|
if (newEntry.endTime < currentEntry.endTime) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
schedulerQueue.splice(i, 0, newEntry);
|
|
|
|
});
|
|
|
|
}
|
2019-05-31 11:56:07 -04:00
|
|
|
}
|
2020-04-13 19:40:21 -04:00
|
|
|
}
|
2020-10-05 23:45:16 -04:00
|
|
|
lastCurrentTime = this._currentTickTime;
|
|
|
|
this._currentTickTime = finalTime;
|
2020-04-13 19:40:21 -04:00
|
|
|
if (doTick) {
|
2020-10-05 23:45:16 -04:00
|
|
|
doTick(this._currentTickTime - lastCurrentTime);
|
2019-05-31 11:56:07 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-09-22 09:52:20 -04:00
|
|
|
flushOnlyPendingTimers(doTick?: (elapsed: number) => void): number {
|
|
|
|
if (this._schedulerQueue.length === 0) {
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
// Find the last task currently queued in the scheduler queue and tick
|
|
|
|
// till that time.
|
2020-10-05 23:45:16 -04:00
|
|
|
const startTime = this._currentTickTime;
|
2020-09-22 09:52:20 -04:00
|
|
|
const lastTask = this._schedulerQueue[this._schedulerQueue.length - 1];
|
|
|
|
this.tick(lastTask.endTime - startTime, doTick, {processNewMacroTasksSynchronously: false});
|
2020-10-05 23:45:16 -04:00
|
|
|
return this._currentTickTime - startTime;
|
2020-09-22 09:52:20 -04:00
|
|
|
}
|
|
|
|
|
2020-04-13 19:40:21 -04:00
|
|
|
flush(limit = 20, flushPeriodic = false, doTick?: (elapsed: number) => void): number {
|
|
|
|
if (flushPeriodic) {
|
|
|
|
return this.flushPeriodic(doTick);
|
|
|
|
} else {
|
|
|
|
return this.flushNonPeriodic(limit, doTick);
|
2019-05-31 11:56:07 -04:00
|
|
|
}
|
2020-04-13 19:40:21 -04:00
|
|
|
}
|
2019-05-31 11:56:07 -04:00
|
|
|
|
2020-04-13 19:40:21 -04:00
|
|
|
private flushPeriodic(doTick?: (elapsed: number) => void): number {
|
|
|
|
if (this._schedulerQueue.length === 0) {
|
|
|
|
return 0;
|
2019-05-31 11:56:07 -04:00
|
|
|
}
|
2020-04-13 19:40:21 -04:00
|
|
|
// Find the last task currently queued in the scheduler queue and tick
|
|
|
|
// till that time.
|
2020-10-05 23:45:16 -04:00
|
|
|
const startTime = this._currentTickTime;
|
2020-04-13 19:40:21 -04:00
|
|
|
const lastTask = this._schedulerQueue[this._schedulerQueue.length - 1];
|
|
|
|
this.tick(lastTask.endTime - startTime, doTick);
|
2020-10-05 23:45:16 -04:00
|
|
|
return this._currentTickTime - startTime;
|
2020-04-13 19:40:21 -04:00
|
|
|
}
|
2019-05-31 11:56:07 -04:00
|
|
|
|
2020-04-13 19:40:21 -04:00
|
|
|
private flushNonPeriodic(limit: number, doTick?: (elapsed: number) => void): number {
|
2020-10-05 23:45:16 -04:00
|
|
|
const startTime = this._currentTickTime;
|
2020-04-13 19:40:21 -04:00
|
|
|
let lastCurrentTime = 0;
|
|
|
|
let count = 0;
|
|
|
|
while (this._schedulerQueue.length > 0) {
|
|
|
|
count++;
|
|
|
|
if (count > limit) {
|
|
|
|
throw new Error(
|
|
|
|
'flush failed after reaching the limit of ' + limit +
|
|
|
|
' tasks. Does your code use a polling timeout?');
|
|
|
|
}
|
2019-05-31 11:56:07 -04:00
|
|
|
|
2020-04-13 19:40:21 -04:00
|
|
|
// flush only non-periodic timers.
|
|
|
|
// If the only remaining tasks are periodic(or requestAnimationFrame), finish flushing.
|
|
|
|
if (this._schedulerQueue.filter(task => !task.isPeriodic && !task.isRequestAnimationFrame)
|
|
|
|
.length === 0) {
|
|
|
|
break;
|
|
|
|
}
|
2019-05-31 11:56:07 -04:00
|
|
|
|
2020-04-13 19:40:21 -04:00
|
|
|
const current = this._schedulerQueue.shift()!;
|
2020-10-05 23:45:16 -04:00
|
|
|
lastCurrentTime = this._currentTickTime;
|
|
|
|
this._currentTickTime = current.endTime;
|
2020-04-13 19:40:21 -04:00
|
|
|
if (doTick) {
|
|
|
|
// Update any secondary schedulers like Jasmine mock Date.
|
2020-10-05 23:45:16 -04:00
|
|
|
doTick(this._currentTickTime - lastCurrentTime);
|
2020-04-13 19:40:21 -04:00
|
|
|
}
|
|
|
|
const retval = current.func.apply(global, current.args);
|
|
|
|
if (!retval) {
|
|
|
|
// Uncaught exception in the current scheduled function. Stop processing the queue.
|
|
|
|
break;
|
2019-05-31 11:56:07 -04:00
|
|
|
}
|
|
|
|
}
|
2020-10-05 23:45:16 -04:00
|
|
|
return this._currentTickTime - startTime;
|
2020-04-13 19:40:21 -04:00
|
|
|
}
|
|
|
|
}
|
2019-05-31 11:56:07 -04:00
|
|
|
|
2020-04-13 19:40:21 -04:00
|
|
|
class FakeAsyncTestZoneSpec implements ZoneSpec {
|
|
|
|
static assertInZone(): void {
|
|
|
|
if (Zone.current.get('FakeAsyncTestZoneSpec') == null) {
|
|
|
|
throw new Error('The code should be running in the fakeAsync zone to call this function');
|
2019-05-31 11:56:07 -04:00
|
|
|
}
|
2020-04-13 19:40:21 -04:00
|
|
|
}
|
2019-05-31 11:56:07 -04:00
|
|
|
|
2020-04-13 19:40:21 -04:00
|
|
|
private _scheduler: Scheduler = new Scheduler();
|
|
|
|
private _microtasks: MicroTaskScheduledFunction[] = [];
|
|
|
|
private _lastError: Error|null = null;
|
|
|
|
private _uncaughtPromiseErrors: {rejection: any}[] =
|
|
|
|
(Promise as any)[(Zone as any).__symbol__('uncaughtPromiseErrors')];
|
2019-05-31 11:56:07 -04:00
|
|
|
|
2020-04-13 19:40:21 -04:00
|
|
|
pendingPeriodicTimers: number[] = [];
|
|
|
|
pendingTimers: number[] = [];
|
|
|
|
|
|
|
|
private patchDateLocked = false;
|
|
|
|
|
|
|
|
constructor(
|
|
|
|
namePrefix: string, private trackPendingRequestAnimationFrame = false,
|
|
|
|
private macroTaskOptions?: MacroTaskOptions[]) {
|
|
|
|
this.name = 'fakeAsyncTestZone for ' + namePrefix;
|
|
|
|
// in case user can't access the construction of FakeAsyncTestSpec
|
|
|
|
// user can also define macroTaskOptions by define a global variable.
|
|
|
|
if (!this.macroTaskOptions) {
|
|
|
|
this.macroTaskOptions = global[Zone.__symbol__('FakeAsyncTestMacroTask')];
|
2019-05-31 11:56:07 -04:00
|
|
|
}
|
2020-04-13 19:40:21 -04:00
|
|
|
}
|
2019-05-31 11:56:07 -04:00
|
|
|
|
2020-04-13 19:40:21 -04:00
|
|
|
private _fnAndFlush(fn: Function, completers: {onSuccess?: Function, onError?: Function}):
|
|
|
|
Function {
|
|
|
|
return (...args: any[]): boolean => {
|
|
|
|
fn.apply(global, args);
|
|
|
|
|
|
|
|
if (this._lastError === null) { // Success
|
|
|
|
if (completers.onSuccess != null) {
|
|
|
|
completers.onSuccess.apply(global);
|
|
|
|
}
|
|
|
|
// Flush microtasks only on success.
|
|
|
|
this.flushMicrotasks();
|
|
|
|
} else { // Failure
|
|
|
|
if (completers.onError != null) {
|
|
|
|
completers.onError.apply(global);
|
|
|
|
}
|
2019-05-31 11:56:07 -04:00
|
|
|
}
|
2020-04-13 19:40:21 -04:00
|
|
|
// Return true if there were no errors, false otherwise.
|
|
|
|
return this._lastError === null;
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
private static _removeTimer(timers: number[], id: number): void {
|
|
|
|
let index = timers.indexOf(id);
|
|
|
|
if (index > -1) {
|
|
|
|
timers.splice(index, 1);
|
2019-05-31 11:56:07 -04:00
|
|
|
}
|
2020-04-13 19:40:21 -04:00
|
|
|
}
|
2019-05-31 11:56:07 -04:00
|
|
|
|
2020-04-13 19:40:21 -04:00
|
|
|
private _dequeueTimer(id: number): Function {
|
|
|
|
return () => {
|
2019-05-31 11:56:07 -04:00
|
|
|
FakeAsyncTestZoneSpec._removeTimer(this.pendingTimers, id);
|
2020-04-13 19:40:21 -04:00
|
|
|
};
|
|
|
|
}
|
2019-05-31 11:56:07 -04:00
|
|
|
|
2020-04-13 19:40:21 -04:00
|
|
|
private _requeuePeriodicTimer(fn: Function, interval: number, args: any[], id: number): Function {
|
|
|
|
return () => {
|
|
|
|
// Requeue the timer callback if it's not been canceled.
|
|
|
|
if (this.pendingPeriodicTimers.indexOf(id) !== -1) {
|
|
|
|
this._scheduler.scheduleFunction(
|
|
|
|
fn, interval, {args, isPeriodic: true, id, isRequeuePeriodic: true});
|
|
|
|
}
|
|
|
|
};
|
|
|
|
}
|
2019-05-31 11:56:07 -04:00
|
|
|
|
2020-04-13 19:40:21 -04:00
|
|
|
private _dequeuePeriodicTimer(id: number): Function {
|
|
|
|
return () => {
|
|
|
|
FakeAsyncTestZoneSpec._removeTimer(this.pendingPeriodicTimers, id);
|
|
|
|
};
|
|
|
|
}
|
2019-05-31 11:56:07 -04:00
|
|
|
|
2020-04-13 19:40:21 -04:00
|
|
|
private _setTimeout(fn: Function, delay: number, args: any[], isTimer = true): number {
|
|
|
|
let removeTimerFn = this._dequeueTimer(Scheduler.nextId);
|
|
|
|
// Queue the callback and dequeue the timer on success and error.
|
|
|
|
let cb = this._fnAndFlush(fn, {onSuccess: removeTimerFn, onError: removeTimerFn});
|
|
|
|
let id = this._scheduler.scheduleFunction(cb, delay, {args, isRequestAnimationFrame: !isTimer});
|
|
|
|
if (isTimer) {
|
|
|
|
this.pendingTimers.push(id);
|
2019-05-31 11:56:07 -04:00
|
|
|
}
|
2020-04-13 19:40:21 -04:00
|
|
|
return id;
|
|
|
|
}
|
2019-05-31 11:56:07 -04:00
|
|
|
|
2020-04-13 19:40:21 -04:00
|
|
|
private _clearTimeout(id: number): void {
|
|
|
|
FakeAsyncTestZoneSpec._removeTimer(this.pendingTimers, id);
|
|
|
|
this._scheduler.removeScheduledFunctionWithId(id);
|
|
|
|
}
|
2019-05-31 11:56:07 -04:00
|
|
|
|
2020-04-13 19:40:21 -04:00
|
|
|
private _setInterval(fn: Function, interval: number, args: any[]): number {
|
|
|
|
let id = Scheduler.nextId;
|
|
|
|
let completers = {onSuccess: null as any, onError: this._dequeuePeriodicTimer(id)};
|
|
|
|
let cb = this._fnAndFlush(fn, completers);
|
2019-05-31 11:56:07 -04:00
|
|
|
|
2020-04-13 19:40:21 -04:00
|
|
|
// Use the callback created above to requeue on success.
|
|
|
|
completers.onSuccess = this._requeuePeriodicTimer(cb, interval, args, id);
|
2019-05-31 11:56:07 -04:00
|
|
|
|
2020-04-13 19:40:21 -04:00
|
|
|
// Queue the callback and dequeue the periodic timer only on error.
|
|
|
|
this._scheduler.scheduleFunction(cb, interval, {args, isPeriodic: true});
|
|
|
|
this.pendingPeriodicTimers.push(id);
|
|
|
|
return id;
|
|
|
|
}
|
2019-05-31 11:56:07 -04:00
|
|
|
|
2020-04-13 19:40:21 -04:00
|
|
|
private _clearInterval(id: number): void {
|
|
|
|
FakeAsyncTestZoneSpec._removeTimer(this.pendingPeriodicTimers, id);
|
|
|
|
this._scheduler.removeScheduledFunctionWithId(id);
|
|
|
|
}
|
2019-05-31 11:56:07 -04:00
|
|
|
|
2020-04-13 19:40:21 -04:00
|
|
|
private _resetLastErrorAndThrow(): void {
|
|
|
|
let error = this._lastError || this._uncaughtPromiseErrors[0];
|
|
|
|
this._uncaughtPromiseErrors.length = 0;
|
|
|
|
this._lastError = null;
|
|
|
|
throw error;
|
|
|
|
}
|
2019-05-31 11:56:07 -04:00
|
|
|
|
2020-10-05 23:45:16 -04:00
|
|
|
getCurrentTickTime() {
|
|
|
|
return this._scheduler.getCurrentTickTime();
|
2020-04-13 19:40:21 -04:00
|
|
|
}
|
|
|
|
|
2020-10-05 23:45:16 -04:00
|
|
|
getFakeSystemTime() {
|
|
|
|
return this._scheduler.getFakeSystemTime();
|
2020-04-13 19:40:21 -04:00
|
|
|
}
|
2019-05-31 11:56:07 -04:00
|
|
|
|
2020-10-05 23:45:16 -04:00
|
|
|
setFakeBaseSystemTime(realTime: number) {
|
|
|
|
this._scheduler.setFakeBaseSystemTime(realTime);
|
2020-04-13 19:40:21 -04:00
|
|
|
}
|
|
|
|
|
2020-09-22 09:52:20 -04:00
|
|
|
getRealSystemTime() {
|
|
|
|
return this._scheduler.getRealSystemTime();
|
|
|
|
}
|
|
|
|
|
2020-04-13 19:40:21 -04:00
|
|
|
static patchDate() {
|
|
|
|
if (!!global[Zone.__symbol__('disableDatePatching')]) {
|
|
|
|
// we don't want to patch global Date
|
|
|
|
// because in some case, global Date
|
|
|
|
// is already being patched, we need to provide
|
|
|
|
// an option to let user still use their
|
|
|
|
// own version of Date.
|
|
|
|
return;
|
2019-05-31 11:56:07 -04:00
|
|
|
}
|
|
|
|
|
2020-04-13 19:40:21 -04:00
|
|
|
if (global['Date'] === FakeDate) {
|
|
|
|
// already patched
|
|
|
|
return;
|
2019-05-31 11:56:07 -04:00
|
|
|
}
|
2020-04-13 19:40:21 -04:00
|
|
|
global['Date'] = FakeDate;
|
|
|
|
FakeDate.prototype = OriginalDate.prototype;
|
2019-05-31 11:56:07 -04:00
|
|
|
|
2020-04-13 19:40:21 -04:00
|
|
|
// try check and reset timers
|
|
|
|
// because jasmine.clock().install() may
|
|
|
|
// have replaced the global timer
|
|
|
|
FakeAsyncTestZoneSpec.checkTimerPatch();
|
|
|
|
}
|
|
|
|
|
|
|
|
static resetDate() {
|
|
|
|
if (global['Date'] === FakeDate) {
|
|
|
|
global['Date'] = OriginalDate;
|
2019-05-31 11:56:07 -04:00
|
|
|
}
|
2020-04-13 19:40:21 -04:00
|
|
|
}
|
2019-05-31 11:56:07 -04:00
|
|
|
|
2020-04-13 19:40:21 -04:00
|
|
|
static checkTimerPatch() {
|
|
|
|
if (global.setTimeout !== timers.setTimeout) {
|
|
|
|
global.setTimeout = timers.setTimeout;
|
|
|
|
global.clearTimeout = timers.clearTimeout;
|
2019-05-31 11:56:07 -04:00
|
|
|
}
|
2020-04-13 19:40:21 -04:00
|
|
|
if (global.setInterval !== timers.setInterval) {
|
|
|
|
global.setInterval = timers.setInterval;
|
|
|
|
global.clearInterval = timers.clearInterval;
|
2019-05-31 11:56:07 -04:00
|
|
|
}
|
2020-04-13 19:40:21 -04:00
|
|
|
}
|
2019-05-31 11:56:07 -04:00
|
|
|
|
2020-04-13 19:40:21 -04:00
|
|
|
lockDatePatch() {
|
|
|
|
this.patchDateLocked = true;
|
|
|
|
FakeAsyncTestZoneSpec.patchDate();
|
|
|
|
}
|
|
|
|
unlockDatePatch() {
|
|
|
|
this.patchDateLocked = false;
|
|
|
|
FakeAsyncTestZoneSpec.resetDate();
|
|
|
|
}
|
2019-05-31 11:56:07 -04:00
|
|
|
|
2020-09-22 09:52:20 -04:00
|
|
|
tickToNext(steps: number = 1, doTick?: (elapsed: number) => void, tickOptions: {
|
|
|
|
processNewMacroTasksSynchronously: boolean
|
|
|
|
} = {processNewMacroTasksSynchronously: true}): void {
|
|
|
|
if (steps <= 0) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
FakeAsyncTestZoneSpec.assertInZone();
|
|
|
|
this.flushMicrotasks();
|
|
|
|
this._scheduler.tickToNext(steps, doTick, tickOptions);
|
|
|
|
if (this._lastError !== null) {
|
|
|
|
this._resetLastErrorAndThrow();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-04-13 19:40:21 -04:00
|
|
|
tick(millis: number = 0, doTick?: (elapsed: number) => void, tickOptions: {
|
|
|
|
processNewMacroTasksSynchronously: boolean
|
|
|
|
} = {processNewMacroTasksSynchronously: true}): void {
|
|
|
|
FakeAsyncTestZoneSpec.assertInZone();
|
|
|
|
this.flushMicrotasks();
|
|
|
|
this._scheduler.tick(millis, doTick, tickOptions);
|
|
|
|
if (this._lastError !== null) {
|
|
|
|
this._resetLastErrorAndThrow();
|
2019-05-31 11:56:07 -04:00
|
|
|
}
|
2020-04-13 19:40:21 -04:00
|
|
|
}
|
2019-05-31 11:56:07 -04:00
|
|
|
|
2020-04-13 19:40:21 -04:00
|
|
|
flushMicrotasks(): void {
|
|
|
|
FakeAsyncTestZoneSpec.assertInZone();
|
|
|
|
const flushErrors = () => {
|
|
|
|
if (this._lastError !== null || this._uncaughtPromiseErrors.length) {
|
|
|
|
// If there is an error stop processing the microtask queue and rethrow the error.
|
2019-05-31 11:56:07 -04:00
|
|
|
this._resetLastErrorAndThrow();
|
|
|
|
}
|
2020-04-13 19:40:21 -04:00
|
|
|
};
|
|
|
|
while (this._microtasks.length > 0) {
|
|
|
|
let microtask = this._microtasks.shift()!;
|
|
|
|
microtask.func.apply(microtask.target, microtask.args);
|
|
|
|
}
|
|
|
|
flushErrors();
|
|
|
|
}
|
|
|
|
|
|
|
|
flush(limit?: number, flushPeriodic?: boolean, doTick?: (elapsed: number) => void): number {
|
|
|
|
FakeAsyncTestZoneSpec.assertInZone();
|
|
|
|
this.flushMicrotasks();
|
|
|
|
const elapsed = this._scheduler.flush(limit, flushPeriodic, doTick);
|
|
|
|
if (this._lastError !== null) {
|
|
|
|
this._resetLastErrorAndThrow();
|
2019-05-31 11:56:07 -04:00
|
|
|
}
|
2020-04-13 19:40:21 -04:00
|
|
|
return elapsed;
|
|
|
|
}
|
2019-05-31 11:56:07 -04:00
|
|
|
|
2020-09-22 09:52:20 -04:00
|
|
|
flushOnlyPendingTimers(doTick?: (elapsed: number) => void): number {
|
|
|
|
FakeAsyncTestZoneSpec.assertInZone();
|
|
|
|
this.flushMicrotasks();
|
|
|
|
const elapsed = this._scheduler.flushOnlyPendingTimers(doTick);
|
|
|
|
if (this._lastError !== null) {
|
|
|
|
this._resetLastErrorAndThrow();
|
|
|
|
}
|
|
|
|
return elapsed;
|
|
|
|
}
|
|
|
|
|
|
|
|
removeAllTimers() {
|
|
|
|
FakeAsyncTestZoneSpec.assertInZone();
|
|
|
|
this._scheduler.removeAll();
|
|
|
|
this.pendingPeriodicTimers = [];
|
|
|
|
this.pendingTimers = [];
|
|
|
|
}
|
|
|
|
|
|
|
|
getTimerCount() {
|
|
|
|
return this._scheduler.getTimerCount() + this._microtasks.length;
|
|
|
|
}
|
|
|
|
|
2020-04-13 19:40:21 -04:00
|
|
|
// ZoneSpec implementation below.
|
2019-05-31 11:56:07 -04:00
|
|
|
|
2020-04-13 19:40:21 -04:00
|
|
|
name: string;
|
2019-05-31 11:56:07 -04:00
|
|
|
|
2020-04-13 19:40:21 -04:00
|
|
|
properties: {[key: string]: any} = {'FakeAsyncTestZoneSpec': this};
|
2019-05-31 11:56:07 -04:00
|
|
|
|
2020-04-13 19:40:21 -04:00
|
|
|
onScheduleTask(delegate: ZoneDelegate, current: Zone, target: Zone, task: Task): Task {
|
|
|
|
switch (task.type) {
|
|
|
|
case 'microTask':
|
|
|
|
let args = task.data && (task.data as any).args;
|
|
|
|
// should pass additional arguments to callback if have any
|
|
|
|
// currently we know process.nextTick will have such additional
|
|
|
|
// arguments
|
|
|
|
let additionalArgs: any[]|undefined;
|
|
|
|
if (args) {
|
|
|
|
let callbackIndex = (task.data as any).cbIdx;
|
|
|
|
if (typeof args.length === 'number' && args.length > callbackIndex + 1) {
|
|
|
|
additionalArgs = Array.prototype.slice.call(args, callbackIndex + 1);
|
2019-05-31 11:56:07 -04:00
|
|
|
}
|
2020-04-13 19:40:21 -04:00
|
|
|
}
|
|
|
|
this._microtasks.push({
|
|
|
|
func: task.invoke,
|
|
|
|
args: additionalArgs,
|
|
|
|
target: task.data && (task.data as any).target
|
|
|
|
});
|
|
|
|
break;
|
|
|
|
case 'macroTask':
|
|
|
|
switch (task.source) {
|
|
|
|
case 'setTimeout':
|
|
|
|
task.data!['handleId'] = this._setTimeout(
|
|
|
|
task.invoke, task.data!['delay']!,
|
|
|
|
Array.prototype.slice.call((task.data as any)['args'], 2));
|
|
|
|
break;
|
|
|
|
case 'setImmediate':
|
|
|
|
task.data!['handleId'] = this._setTimeout(
|
|
|
|
task.invoke, 0, Array.prototype.slice.call((task.data as any)['args'], 1));
|
|
|
|
break;
|
|
|
|
case 'setInterval':
|
|
|
|
task.data!['handleId'] = this._setInterval(
|
|
|
|
task.invoke, task.data!['delay']!,
|
|
|
|
Array.prototype.slice.call((task.data as any)['args'], 2));
|
|
|
|
break;
|
|
|
|
case 'XMLHttpRequest.send':
|
|
|
|
throw new Error(
|
|
|
|
'Cannot make XHRs from within a fake async test. Request URL: ' +
|
|
|
|
(task.data as any)['url']);
|
|
|
|
case 'requestAnimationFrame':
|
|
|
|
case 'webkitRequestAnimationFrame':
|
|
|
|
case 'mozRequestAnimationFrame':
|
|
|
|
// Simulate a requestAnimationFrame by using a setTimeout with 16 ms.
|
|
|
|
// (60 frames per second)
|
|
|
|
task.data!['handleId'] = this._setTimeout(
|
|
|
|
task.invoke, 16, (task.data as any)['args'],
|
|
|
|
this.trackPendingRequestAnimationFrame);
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
// user can define which macroTask they want to support by passing
|
|
|
|
// macroTaskOptions
|
|
|
|
const macroTaskOption = this.findMacroTaskOption(task);
|
|
|
|
if (macroTaskOption) {
|
|
|
|
const args = task.data && (task.data as any)['args'];
|
|
|
|
const delay = args && args.length > 1 ? args[1] : 0;
|
|
|
|
let callbackArgs = macroTaskOption.callbackArgs ? macroTaskOption.callbackArgs : args;
|
|
|
|
if (!!macroTaskOption.isPeriodic) {
|
|
|
|
// periodic macroTask, use setInterval to simulate
|
|
|
|
task.data!['handleId'] = this._setInterval(task.invoke, delay, callbackArgs);
|
|
|
|
task.data!.isPeriodic = true;
|
|
|
|
} else {
|
|
|
|
// not periodic, use setTimeout to simulate
|
|
|
|
task.data!['handleId'] = this._setTimeout(task.invoke, delay, callbackArgs);
|
2019-05-31 11:56:07 -04:00
|
|
|
}
|
2020-04-13 19:40:21 -04:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
throw new Error('Unknown macroTask scheduled in fake async test: ' + task.source);
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
case 'eventTask':
|
|
|
|
task = delegate.scheduleTask(target, task);
|
|
|
|
break;
|
2019-05-31 11:56:07 -04:00
|
|
|
}
|
2020-04-13 19:40:21 -04:00
|
|
|
return task;
|
|
|
|
}
|
2019-05-31 11:56:07 -04:00
|
|
|
|
2020-04-13 19:40:21 -04:00
|
|
|
onCancelTask(delegate: ZoneDelegate, current: Zone, target: Zone, task: Task): any {
|
|
|
|
switch (task.source) {
|
|
|
|
case 'setTimeout':
|
|
|
|
case 'requestAnimationFrame':
|
|
|
|
case 'webkitRequestAnimationFrame':
|
|
|
|
case 'mozRequestAnimationFrame':
|
|
|
|
return this._clearTimeout(<number>task.data!['handleId']);
|
|
|
|
case 'setInterval':
|
|
|
|
return this._clearInterval(<number>task.data!['handleId']);
|
|
|
|
default:
|
|
|
|
// user can define which macroTask they want to support by passing
|
|
|
|
// macroTaskOptions
|
|
|
|
const macroTaskOption = this.findMacroTaskOption(task);
|
|
|
|
if (macroTaskOption) {
|
|
|
|
const handleId: number = <number>task.data!['handleId'];
|
|
|
|
return macroTaskOption.isPeriodic ? this._clearInterval(handleId) :
|
|
|
|
this._clearTimeout(handleId);
|
|
|
|
}
|
|
|
|
return delegate.cancelTask(target, task);
|
2019-05-31 11:56:07 -04:00
|
|
|
}
|
2020-04-13 19:40:21 -04:00
|
|
|
}
|
2019-05-31 11:56:07 -04:00
|
|
|
|
2020-04-13 19:40:21 -04:00
|
|
|
onInvoke(
|
|
|
|
delegate: ZoneDelegate, current: Zone, target: Zone, callback: Function, applyThis: any,
|
|
|
|
applyArgs?: any[], source?: string): any {
|
|
|
|
try {
|
|
|
|
FakeAsyncTestZoneSpec.patchDate();
|
|
|
|
return delegate.invoke(target, callback, applyThis, applyArgs, source);
|
|
|
|
} finally {
|
|
|
|
if (!this.patchDateLocked) {
|
|
|
|
FakeAsyncTestZoneSpec.resetDate();
|
2019-05-31 11:56:07 -04:00
|
|
|
}
|
|
|
|
}
|
2020-04-13 19:40:21 -04:00
|
|
|
}
|
2019-05-31 11:56:07 -04:00
|
|
|
|
2020-04-13 19:40:21 -04:00
|
|
|
findMacroTaskOption(task: Task) {
|
|
|
|
if (!this.macroTaskOptions) {
|
2019-05-31 11:56:07 -04:00
|
|
|
return null;
|
|
|
|
}
|
2020-04-13 19:40:21 -04:00
|
|
|
for (let i = 0; i < this.macroTaskOptions.length; i++) {
|
|
|
|
const macroTaskOption = this.macroTaskOptions[i];
|
|
|
|
if (macroTaskOption.source === task.source) {
|
|
|
|
return macroTaskOption;
|
|
|
|
}
|
2019-05-31 11:56:07 -04:00
|
|
|
}
|
2020-04-13 19:40:21 -04:00
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
onHandleError(parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, error: any):
|
|
|
|
boolean {
|
|
|
|
this._lastError = error;
|
|
|
|
return false; // Don't propagate error to parent zone.
|
2019-05-31 11:56:07 -04:00
|
|
|
}
|
2020-04-13 19:40:21 -04:00
|
|
|
}
|
2019-05-31 11:56:07 -04:00
|
|
|
|
2020-04-13 19:40:21 -04:00
|
|
|
// Export the class so that new instances can be created with proper
|
|
|
|
// constructor params.
|
|
|
|
(Zone as any)['FakeAsyncTestZoneSpec'] = FakeAsyncTestZoneSpec;
|
2019-07-08 14:30:29 -04:00
|
|
|
})(typeof window === 'object' && window || typeof self === 'object' && self || global);
|
2020-07-01 17:35:19 -04:00
|
|
|
|
|
|
|
Zone.__load_patch('fakeasync', (global: any, Zone: ZoneType, api: _ZonePrivate) => {
|
|
|
|
const FakeAsyncTestZoneSpec = Zone && (Zone as any)['FakeAsyncTestZoneSpec'];
|
|
|
|
type ProxyZoneSpecType = {
|
|
|
|
setDelegate(delegateSpec: ZoneSpec): void; getDelegate(): ZoneSpec; resetDelegate(): void;
|
|
|
|
};
|
|
|
|
|
|
|
|
function getProxyZoneSpec(): {get(): ProxyZoneSpecType; assertPresent: () => ProxyZoneSpecType} {
|
|
|
|
return Zone && (Zone as any)['ProxyZoneSpec'];
|
|
|
|
}
|
|
|
|
|
|
|
|
let _fakeAsyncTestZoneSpec: any = null;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Clears out the shared fake async zone for a test.
|
|
|
|
* To be called in a global `beforeEach`.
|
|
|
|
*
|
|
|
|
* @experimental
|
|
|
|
*/
|
|
|
|
function resetFakeAsyncZone() {
|
|
|
|
if (_fakeAsyncTestZoneSpec) {
|
|
|
|
_fakeAsyncTestZoneSpec.unlockDatePatch();
|
|
|
|
}
|
|
|
|
_fakeAsyncTestZoneSpec = null;
|
|
|
|
// in node.js testing we may not have ProxyZoneSpec in which case there is nothing to reset.
|
|
|
|
getProxyZoneSpec() && getProxyZoneSpec().assertPresent().resetDelegate();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Wraps a function to be executed in the fakeAsync zone:
|
|
|
|
* - microtasks are manually executed by calling `flushMicrotasks()`,
|
|
|
|
* - timers are synchronous, `tick()` simulates the asynchronous passage of time.
|
|
|
|
*
|
|
|
|
* If there are any pending timers at the end of the function, an exception will be thrown.
|
|
|
|
*
|
|
|
|
* Can be used to wrap inject() calls.
|
|
|
|
*
|
|
|
|
* ## Example
|
|
|
|
*
|
|
|
|
* {@example core/testing/ts/fake_async.ts region='basic'}
|
|
|
|
*
|
|
|
|
* @param fn
|
|
|
|
* @returns The function wrapped to be executed in the fakeAsync zone
|
|
|
|
*
|
|
|
|
* @experimental
|
|
|
|
*/
|
|
|
|
function fakeAsync(fn: Function): (...args: any[]) => any {
|
|
|
|
// Not using an arrow function to preserve context passed from call site
|
|
|
|
const fakeAsyncFn: any = function(this: unknown, ...args: any[]) {
|
|
|
|
const ProxyZoneSpec = getProxyZoneSpec();
|
|
|
|
if (!ProxyZoneSpec) {
|
|
|
|
throw new Error(
|
|
|
|
'ProxyZoneSpec is needed for the async() test helper but could not be found. ' +
|
2021-02-17 02:46:48 -05:00
|
|
|
'Please make sure that your environment includes zone.js/plugins/proxy');
|
2020-07-01 17:35:19 -04:00
|
|
|
}
|
|
|
|
const proxyZoneSpec = ProxyZoneSpec.assertPresent();
|
|
|
|
if (Zone.current.get('FakeAsyncTestZoneSpec')) {
|
|
|
|
throw new Error('fakeAsync() calls can not be nested');
|
|
|
|
}
|
|
|
|
try {
|
|
|
|
// in case jasmine.clock init a fakeAsyncTestZoneSpec
|
|
|
|
if (!_fakeAsyncTestZoneSpec) {
|
|
|
|
if (proxyZoneSpec.getDelegate() instanceof FakeAsyncTestZoneSpec) {
|
|
|
|
throw new Error('fakeAsync() calls can not be nested');
|
|
|
|
}
|
|
|
|
|
|
|
|
_fakeAsyncTestZoneSpec = new FakeAsyncTestZoneSpec();
|
|
|
|
}
|
|
|
|
|
|
|
|
let res: any;
|
|
|
|
const lastProxyZoneSpec = proxyZoneSpec.getDelegate();
|
|
|
|
proxyZoneSpec.setDelegate(_fakeAsyncTestZoneSpec);
|
|
|
|
_fakeAsyncTestZoneSpec.lockDatePatch();
|
|
|
|
try {
|
|
|
|
res = fn.apply(this, args);
|
|
|
|
flushMicrotasks();
|
|
|
|
} finally {
|
|
|
|
proxyZoneSpec.setDelegate(lastProxyZoneSpec);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (_fakeAsyncTestZoneSpec.pendingPeriodicTimers.length > 0) {
|
|
|
|
throw new Error(
|
|
|
|
`${_fakeAsyncTestZoneSpec.pendingPeriodicTimers.length} ` +
|
|
|
|
`periodic timer(s) still in the queue.`);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (_fakeAsyncTestZoneSpec.pendingTimers.length > 0) {
|
|
|
|
throw new Error(
|
|
|
|
`${_fakeAsyncTestZoneSpec.pendingTimers.length} timer(s) still in the queue.`);
|
|
|
|
}
|
|
|
|
return res;
|
|
|
|
} finally {
|
|
|
|
resetFakeAsyncZone();
|
|
|
|
}
|
|
|
|
};
|
|
|
|
(fakeAsyncFn as any).isFakeAsync = true;
|
|
|
|
return fakeAsyncFn;
|
|
|
|
}
|
|
|
|
|
|
|
|
function _getFakeAsyncZoneSpec(): any {
|
|
|
|
if (_fakeAsyncTestZoneSpec == null) {
|
|
|
|
_fakeAsyncTestZoneSpec = Zone.current.get('FakeAsyncTestZoneSpec');
|
|
|
|
if (_fakeAsyncTestZoneSpec == null) {
|
|
|
|
throw new Error('The code should be running in the fakeAsync zone to call this function');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return _fakeAsyncTestZoneSpec;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Simulates the asynchronous passage of time for the timers in the fakeAsync zone.
|
|
|
|
*
|
|
|
|
* The microtasks queue is drained at the very start of this function and after any timer callback
|
|
|
|
* has been executed.
|
|
|
|
*
|
|
|
|
* ## Example
|
|
|
|
*
|
|
|
|
* {@example core/testing/ts/fake_async.ts region='basic'}
|
|
|
|
*
|
|
|
|
* @experimental
|
|
|
|
*/
|
|
|
|
function tick(millis: number = 0, ignoreNestedTimeout = false): void {
|
|
|
|
_getFakeAsyncZoneSpec().tick(millis, null, ignoreNestedTimeout);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Simulates the asynchronous passage of time for the timers in the fakeAsync zone by
|
|
|
|
* draining the macrotask queue until it is empty. The returned value is the milliseconds
|
|
|
|
* of time that would have been elapsed.
|
|
|
|
*
|
|
|
|
* @param maxTurns
|
|
|
|
* @returns The simulated time elapsed, in millis.
|
|
|
|
*
|
|
|
|
* @experimental
|
|
|
|
*/
|
|
|
|
function flush(maxTurns?: number): number {
|
|
|
|
return _getFakeAsyncZoneSpec().flush(maxTurns);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Discard all remaining periodic tasks.
|
|
|
|
*
|
|
|
|
* @experimental
|
|
|
|
*/
|
|
|
|
function discardPeriodicTasks(): void {
|
|
|
|
const zoneSpec = _getFakeAsyncZoneSpec();
|
|
|
|
const pendingTimers = zoneSpec.pendingPeriodicTimers;
|
|
|
|
zoneSpec.pendingPeriodicTimers.length = 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Flush any pending microtasks.
|
|
|
|
*
|
|
|
|
* @experimental
|
|
|
|
*/
|
|
|
|
function flushMicrotasks(): void {
|
|
|
|
_getFakeAsyncZoneSpec().flushMicrotasks();
|
|
|
|
}
|
|
|
|
(Zone as any)[api.symbol('fakeAsyncTest')] =
|
|
|
|
{resetFakeAsyncZone, flushMicrotasks, discardPeriodicTasks, tick, flush, fakeAsync};
|
|
|
|
}, true);
|