
committed by
Kara Erickson

parent
7b3bcc23af
commit
5eb7426216
149
packages/zone.js/lib/zone-spec/async-test.ts
Normal file
149
packages/zone.js/lib/zone-spec/async-test.ts
Normal file
@ -0,0 +1,149 @@
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
(function(_global: any) {
|
||||
class AsyncTestZoneSpec implements ZoneSpec {
|
||||
static symbolParentUnresolved = Zone.__symbol__('parentUnresolved');
|
||||
|
||||
_pendingMicroTasks: boolean = false;
|
||||
_pendingMacroTasks: boolean = false;
|
||||
_alreadyErrored: boolean = false;
|
||||
_isSync: boolean = false;
|
||||
runZone = Zone.current;
|
||||
unresolvedChainedPromiseCount = 0;
|
||||
|
||||
supportWaitUnresolvedChainedPromise = false;
|
||||
|
||||
constructor(
|
||||
private finishCallback: Function, private failCallback: Function, namePrefix: string) {
|
||||
this.name = 'asyncTestZone for ' + namePrefix;
|
||||
this.properties = {'AsyncTestZoneSpec': this};
|
||||
this.supportWaitUnresolvedChainedPromise =
|
||||
_global[Zone.__symbol__('supportWaitUnResolvedChainedPromise')] === true;
|
||||
}
|
||||
|
||||
isUnresolvedChainedPromisePending() { return this.unresolvedChainedPromiseCount > 0; }
|
||||
|
||||
_finishCallbackIfDone() {
|
||||
if (!(this._pendingMicroTasks || this._pendingMacroTasks ||
|
||||
(this.supportWaitUnresolvedChainedPromise &&
|
||||
this.isUnresolvedChainedPromisePending()))) {
|
||||
// We do this because we would like to catch unhandled rejected promises.
|
||||
this.runZone.run(() => {
|
||||
setTimeout(() => {
|
||||
if (!this._alreadyErrored && !(this._pendingMicroTasks || this._pendingMacroTasks)) {
|
||||
this.finishCallback();
|
||||
}
|
||||
}, 0);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
patchPromiseForTest() {
|
||||
if (!this.supportWaitUnresolvedChainedPromise) {
|
||||
return;
|
||||
}
|
||||
const patchPromiseForTest = (Promise as any)[Zone.__symbol__('patchPromiseForTest')];
|
||||
if (patchPromiseForTest) {
|
||||
patchPromiseForTest();
|
||||
}
|
||||
}
|
||||
|
||||
unPatchPromiseForTest() {
|
||||
if (!this.supportWaitUnresolvedChainedPromise) {
|
||||
return;
|
||||
}
|
||||
const unPatchPromiseForTest = (Promise as any)[Zone.__symbol__('unPatchPromiseForTest')];
|
||||
if (unPatchPromiseForTest) {
|
||||
unPatchPromiseForTest();
|
||||
}
|
||||
}
|
||||
|
||||
// ZoneSpec implementation below.
|
||||
|
||||
name: string;
|
||||
|
||||
properties: {[key: string]: any};
|
||||
|
||||
onScheduleTask(delegate: ZoneDelegate, current: Zone, target: Zone, task: Task): Task {
|
||||
if (task.type !== 'eventTask') {
|
||||
this._isSync = false;
|
||||
}
|
||||
if (task.type === 'microTask' && task.data && task.data instanceof Promise) {
|
||||
// check whether the promise is a chained promise
|
||||
if ((task.data as any)[AsyncTestZoneSpec.symbolParentUnresolved] === true) {
|
||||
// chained promise is being scheduled
|
||||
this.unresolvedChainedPromiseCount--;
|
||||
}
|
||||
}
|
||||
return delegate.scheduleTask(target, task);
|
||||
}
|
||||
|
||||
onInvokeTask(
|
||||
delegate: ZoneDelegate, current: Zone, target: Zone, task: Task, applyThis: any,
|
||||
applyArgs: any) {
|
||||
if (task.type !== 'eventTask') {
|
||||
this._isSync = false;
|
||||
}
|
||||
return delegate.invokeTask(target, task, applyThis, applyArgs);
|
||||
}
|
||||
|
||||
onCancelTask(delegate: ZoneDelegate, current: Zone, target: Zone, task: Task) {
|
||||
if (task.type !== 'eventTask') {
|
||||
this._isSync = false;
|
||||
}
|
||||
return delegate.cancelTask(target, task);
|
||||
}
|
||||
|
||||
// Note - we need to use onInvoke at the moment to call finish when a test is
|
||||
// fully synchronous. TODO(juliemr): remove this when the logic for
|
||||
// onHasTask changes and it calls whenever the task queues are dirty.
|
||||
// updated by(JiaLiPassion), only call finish callback when no task
|
||||
// was scheduled/invoked/canceled.
|
||||
onInvoke(
|
||||
parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, delegate: Function,
|
||||
applyThis: any, applyArgs?: any[], source?: string): any {
|
||||
let previousTaskCounts: any = null;
|
||||
try {
|
||||
this._isSync = true;
|
||||
return parentZoneDelegate.invoke(targetZone, delegate, applyThis, applyArgs, source);
|
||||
} finally {
|
||||
const afterTaskCounts: any = (parentZoneDelegate as any)._taskCounts;
|
||||
if (this._isSync) {
|
||||
this._finishCallbackIfDone();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onHandleError(
|
||||
parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone,
|
||||
error: any): boolean {
|
||||
// Let the parent try to handle the error.
|
||||
const result = parentZoneDelegate.handleError(targetZone, error);
|
||||
if (result) {
|
||||
this.failCallback(error);
|
||||
this._alreadyErrored = true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
onHasTask(delegate: ZoneDelegate, current: Zone, target: Zone, hasTaskState: HasTaskState) {
|
||||
delegate.hasTask(target, hasTaskState);
|
||||
if (hasTaskState.change == 'microTask') {
|
||||
this._pendingMicroTasks = hasTaskState.microTask;
|
||||
this._finishCallbackIfDone();
|
||||
} else if (hasTaskState.change == 'macroTask') {
|
||||
this._pendingMacroTasks = hasTaskState.macroTask;
|
||||
this._finishCallbackIfDone();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Export the class so that new instances can be created with proper
|
||||
// constructor params.
|
||||
(Zone as any)['AsyncTestZoneSpec'] = AsyncTestZoneSpec;
|
||||
})(global);
|
560
packages/zone.js/lib/zone-spec/fake-async-test.ts
Normal file
560
packages/zone.js/lib/zone-spec/fake-async-test.ts
Normal file
@ -0,0 +1,560 @@
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
|
||||
(function(global: any) {
|
||||
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;
|
||||
class FakeDate {
|
||||
constructor() {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
static now() {
|
||||
const fakeAsyncTestZoneSpec = Zone.current.get('FakeAsyncTestZoneSpec');
|
||||
if (fakeAsyncTestZoneSpec) {
|
||||
return fakeAsyncTestZoneSpec.getCurrentRealTime() + fakeAsyncTestZoneSpec.getCurrentTime();
|
||||
}
|
||||
return OriginalDate.now.apply(this, arguments);
|
||||
}
|
||||
}
|
||||
|
||||
(FakeDate as any).UTC = OriginalDate.UTC;
|
||||
(FakeDate as any).parse = OriginalDate.parse;
|
||||
|
||||
// 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.
|
||||
private _currentTime: number = 0;
|
||||
// Current real time in millis.
|
||||
private _currentRealTime: number = OriginalDate.now();
|
||||
|
||||
constructor() {}
|
||||
|
||||
getCurrentTime() { return this._currentTime; }
|
||||
|
||||
getCurrentRealTime() { return this._currentRealTime; }
|
||||
|
||||
setCurrentRealTime(realTime: number) { this._currentRealTime = realTime; }
|
||||
|
||||
scheduleFunction(
|
||||
cb: Function, delay: number, args: any[] = [], isPeriodic: boolean = false,
|
||||
isRequestAnimationFrame: boolean = false, id: number = -1): number {
|
||||
let currentId: number = id < 0 ? Scheduler.nextId++ : id;
|
||||
let endTime = this._currentTime + delay;
|
||||
|
||||
// Insert so that scheduler queue remains sorted by end time.
|
||||
let newEntry: ScheduledFunction = {
|
||||
endTime: endTime,
|
||||
id: currentId,
|
||||
func: cb,
|
||||
args: args,
|
||||
delay: delay,
|
||||
isPeriodic: isPeriodic,
|
||||
isRequestAnimationFrame: isRequestAnimationFrame
|
||||
};
|
||||
let i = 0;
|
||||
for (; i < this._schedulerQueue.length; i++) {
|
||||
let currentEntry = this._schedulerQueue[i];
|
||||
if (newEntry.endTime < currentEntry.endTime) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
this._schedulerQueue.splice(i, 0, newEntry);
|
||||
return currentId;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tick(millis: number = 0, doTick?: (elapsed: number) => void): void {
|
||||
let finalTime = this._currentTime + millis;
|
||||
let lastCurrentTime = 0;
|
||||
if (this._schedulerQueue.length === 0 && doTick) {
|
||||
doTick(millis);
|
||||
return;
|
||||
}
|
||||
while (this._schedulerQueue.length > 0) {
|
||||
let current = this._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 = this._schedulerQueue.shift() !;
|
||||
lastCurrentTime = this._currentTime;
|
||||
this._currentTime = current.endTime;
|
||||
if (doTick) {
|
||||
doTick(this._currentTime - lastCurrentTime);
|
||||
}
|
||||
let retval = current.func.apply(
|
||||
global, current.isRequestAnimationFrame ? [this._currentTime] : current.args);
|
||||
if (!retval) {
|
||||
// Uncaught exception in the current scheduled function. Stop processing the queue.
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
lastCurrentTime = this._currentTime;
|
||||
this._currentTime = finalTime;
|
||||
if (doTick) {
|
||||
doTick(this._currentTime - lastCurrentTime);
|
||||
}
|
||||
}
|
||||
|
||||
flush(limit = 20, flushPeriodic = false, doTick?: (elapsed: number) => void): number {
|
||||
if (flushPeriodic) {
|
||||
return this.flushPeriodic(doTick);
|
||||
} else {
|
||||
return this.flushNonPeriodic(limit, doTick);
|
||||
}
|
||||
}
|
||||
|
||||
private flushPeriodic(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.
|
||||
const startTime = this._currentTime;
|
||||
const lastTask = this._schedulerQueue[this._schedulerQueue.length - 1];
|
||||
this.tick(lastTask.endTime - startTime, doTick);
|
||||
return this._currentTime - startTime;
|
||||
}
|
||||
|
||||
private flushNonPeriodic(limit: number, doTick?: (elapsed: number) => void): number {
|
||||
const startTime = this._currentTime;
|
||||
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?');
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
const current = this._schedulerQueue.shift() !;
|
||||
lastCurrentTime = this._currentTime;
|
||||
this._currentTime = current.endTime;
|
||||
if (doTick) {
|
||||
// Update any secondary schedulers like Jasmine mock Date.
|
||||
doTick(this._currentTime - lastCurrentTime);
|
||||
}
|
||||
const retval = current.func.apply(global, current.args);
|
||||
if (!retval) {
|
||||
// Uncaught exception in the current scheduled function. Stop processing the queue.
|
||||
break;
|
||||
}
|
||||
}
|
||||
return this._currentTime - startTime;
|
||||
}
|
||||
}
|
||||
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
||||
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')];
|
||||
|
||||
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')];
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
private _dequeueTimer(id: number): Function {
|
||||
return () => { FakeAsyncTestZoneSpec._removeTimer(this.pendingTimers, id); };
|
||||
}
|
||||
|
||||
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, true, false, id);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private _dequeuePeriodicTimer(id: number): Function {
|
||||
return () => { FakeAsyncTestZoneSpec._removeTimer(this.pendingPeriodicTimers, id); };
|
||||
}
|
||||
|
||||
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, false, !isTimer);
|
||||
if (isTimer) {
|
||||
this.pendingTimers.push(id);
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
private _clearTimeout(id: number): void {
|
||||
FakeAsyncTestZoneSpec._removeTimer(this.pendingTimers, id);
|
||||
this._scheduler.removeScheduledFunctionWithId(id);
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
// Use the callback created above to requeue on success.
|
||||
completers.onSuccess = this._requeuePeriodicTimer(cb, interval, args, id);
|
||||
|
||||
// Queue the callback and dequeue the periodic timer only on error.
|
||||
this._scheduler.scheduleFunction(cb, interval, args, true);
|
||||
this.pendingPeriodicTimers.push(id);
|
||||
return id;
|
||||
}
|
||||
|
||||
private _clearInterval(id: number): void {
|
||||
FakeAsyncTestZoneSpec._removeTimer(this.pendingPeriodicTimers, id);
|
||||
this._scheduler.removeScheduledFunctionWithId(id);
|
||||
}
|
||||
|
||||
private _resetLastErrorAndThrow(): void {
|
||||
let error = this._lastError || this._uncaughtPromiseErrors[0];
|
||||
this._uncaughtPromiseErrors.length = 0;
|
||||
this._lastError = null;
|
||||
throw error;
|
||||
}
|
||||
|
||||
getCurrentTime() { return this._scheduler.getCurrentTime(); }
|
||||
|
||||
getCurrentRealTime() { return this._scheduler.getCurrentRealTime(); }
|
||||
|
||||
setCurrentRealTime(realTime: number) { this._scheduler.setCurrentRealTime(realTime); }
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
if (global['Date'] === FakeDate) {
|
||||
// already patched
|
||||
return;
|
||||
}
|
||||
global['Date'] = FakeDate;
|
||||
FakeDate.prototype = OriginalDate.prototype;
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
static checkTimerPatch() {
|
||||
if (global.setTimeout !== timers.setTimeout) {
|
||||
global.setTimeout = timers.setTimeout;
|
||||
global.clearTimeout = timers.clearTimeout;
|
||||
}
|
||||
if (global.setInterval !== timers.setInterval) {
|
||||
global.setInterval = timers.setInterval;
|
||||
global.clearInterval = timers.clearInterval;
|
||||
}
|
||||
}
|
||||
|
||||
lockDatePatch() {
|
||||
this.patchDateLocked = true;
|
||||
FakeAsyncTestZoneSpec.patchDate();
|
||||
}
|
||||
unlockDatePatch() {
|
||||
this.patchDateLocked = false;
|
||||
FakeAsyncTestZoneSpec.resetDate();
|
||||
}
|
||||
|
||||
tick(millis: number = 0, doTick?: (elapsed: number) => void): void {
|
||||
FakeAsyncTestZoneSpec.assertInZone();
|
||||
this.flushMicrotasks();
|
||||
this._scheduler.tick(millis, doTick);
|
||||
if (this._lastError !== null) {
|
||||
this._resetLastErrorAndThrow();
|
||||
}
|
||||
}
|
||||
|
||||
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.
|
||||
this._resetLastErrorAndThrow();
|
||||
}
|
||||
};
|
||||
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();
|
||||
}
|
||||
return elapsed;
|
||||
}
|
||||
|
||||
// ZoneSpec implementation below.
|
||||
|
||||
name: string;
|
||||
|
||||
properties: {[key: string]: any} = {'FakeAsyncTestZoneSpec': this};
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
break;
|
||||
}
|
||||
throw new Error('Unknown macroTask scheduled in fake async test: ' + task.source);
|
||||
}
|
||||
break;
|
||||
case 'eventTask':
|
||||
task = delegate.scheduleTask(target, task);
|
||||
break;
|
||||
}
|
||||
return task;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
findMacroTaskOption(task: Task) {
|
||||
if (!this.macroTaskOptions) {
|
||||
return null;
|
||||
}
|
||||
for (let i = 0; i < this.macroTaskOptions.length; i++) {
|
||||
const macroTaskOption = this.macroTaskOptions[i];
|
||||
if (macroTaskOption.source === task.source) {
|
||||
return macroTaskOption;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
onHandleError(
|
||||
parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone,
|
||||
error: any): boolean {
|
||||
this._lastError = error;
|
||||
return false; // Don't propagate error to parent zone.
|
||||
}
|
||||
}
|
||||
|
||||
// Export the class so that new instances can be created with proper
|
||||
// constructor params.
|
||||
(Zone as any)['FakeAsyncTestZoneSpec'] = FakeAsyncTestZoneSpec;
|
||||
})(global);
|
183
packages/zone.js/lib/zone-spec/long-stack-trace.ts
Normal file
183
packages/zone.js/lib/zone-spec/long-stack-trace.ts
Normal file
@ -0,0 +1,183 @@
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
/**
|
||||
* @fileoverview
|
||||
* @suppress {globalThis}
|
||||
*/
|
||||
|
||||
const NEWLINE = '\n';
|
||||
const IGNORE_FRAMES: {[k: string]: true} = {};
|
||||
const creationTrace = '__creationTrace__';
|
||||
const ERROR_TAG = 'STACKTRACE TRACKING';
|
||||
const SEP_TAG = '__SEP_TAG__';
|
||||
let sepTemplate: string = SEP_TAG + '@[native]';
|
||||
|
||||
class LongStackTrace {
|
||||
error: Error = getStacktrace();
|
||||
timestamp: Date = new Date();
|
||||
}
|
||||
|
||||
function getStacktraceWithUncaughtError(): Error {
|
||||
return new Error(ERROR_TAG);
|
||||
}
|
||||
|
||||
function getStacktraceWithCaughtError(): Error {
|
||||
try {
|
||||
throw getStacktraceWithUncaughtError();
|
||||
} catch (err) {
|
||||
return err;
|
||||
}
|
||||
}
|
||||
|
||||
// Some implementations of exception handling don't create a stack trace if the exception
|
||||
// isn't thrown, however it's faster not to actually throw the exception.
|
||||
const error = getStacktraceWithUncaughtError();
|
||||
const caughtError = getStacktraceWithCaughtError();
|
||||
const getStacktrace = error.stack ?
|
||||
getStacktraceWithUncaughtError :
|
||||
(caughtError.stack ? getStacktraceWithCaughtError : getStacktraceWithUncaughtError);
|
||||
|
||||
function getFrames(error: Error): string[] {
|
||||
return error.stack ? error.stack.split(NEWLINE) : [];
|
||||
}
|
||||
|
||||
function addErrorStack(lines: string[], error: Error): void {
|
||||
let trace: string[] = getFrames(error);
|
||||
for (let i = 0; i < trace.length; i++) {
|
||||
const frame = trace[i];
|
||||
// Filter out the Frames which are part of stack capturing.
|
||||
if (!IGNORE_FRAMES.hasOwnProperty(frame)) {
|
||||
lines.push(trace[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function renderLongStackTrace(frames: LongStackTrace[], stack?: string): string {
|
||||
const longTrace: string[] = [stack ? stack.trim() : ''];
|
||||
|
||||
if (frames) {
|
||||
let timestamp = new Date().getTime();
|
||||
for (let i = 0; i < frames.length; i++) {
|
||||
const traceFrames: LongStackTrace = frames[i];
|
||||
const lastTime = traceFrames.timestamp;
|
||||
let separator =
|
||||
`____________________Elapsed ${timestamp - lastTime.getTime()} ms; At: ${lastTime}`;
|
||||
separator = separator.replace(/[^\w\d]/g, '_');
|
||||
longTrace.push(sepTemplate.replace(SEP_TAG, separator));
|
||||
addErrorStack(longTrace, traceFrames.error);
|
||||
|
||||
timestamp = lastTime.getTime();
|
||||
}
|
||||
}
|
||||
|
||||
return longTrace.join(NEWLINE);
|
||||
}
|
||||
|
||||
(Zone as any)['longStackTraceZoneSpec'] = <ZoneSpec>{
|
||||
name: 'long-stack-trace',
|
||||
longStackTraceLimit: 10, // Max number of task to keep the stack trace for.
|
||||
// add a getLongStackTrace method in spec to
|
||||
// handle handled reject promise error.
|
||||
getLongStackTrace: function(error: Error): string |
|
||||
undefined {
|
||||
if (!error) {
|
||||
return undefined;
|
||||
}
|
||||
const trace = (error as any)[(Zone as any).__symbol__('currentTaskTrace')];
|
||||
if (!trace) {
|
||||
return error.stack;
|
||||
}
|
||||
return renderLongStackTrace(trace, error.stack);
|
||||
},
|
||||
|
||||
onScheduleTask: function(
|
||||
parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, task: Task): any {
|
||||
if (Error.stackTraceLimit > 0) {
|
||||
// if Error.stackTraceLimit is 0, means stack trace
|
||||
// is disabled, so we don't need to generate long stack trace
|
||||
// this will improve performance in some test(some test will
|
||||
// set stackTraceLimit to 0, https://github.com/angular/zone.js/issues/698
|
||||
const currentTask = Zone.currentTask;
|
||||
let trace = currentTask && currentTask.data && (currentTask.data as any)[creationTrace] || [];
|
||||
trace = [new LongStackTrace()].concat(trace);
|
||||
if (trace.length > this.longStackTraceLimit) {
|
||||
trace.length = this.longStackTraceLimit;
|
||||
}
|
||||
if (!task.data) task.data = {};
|
||||
if (task.type === 'eventTask') {
|
||||
// Fix issue https://github.com/angular/zone.js/issues/1195,
|
||||
// For event task of browser, by default, all task will share a
|
||||
// singleton instance of data object, we should create a new one here
|
||||
|
||||
// The cast to `any` is required to workaround a closure bug which wrongly applies
|
||||
// URL sanitization rules to .data access.
|
||||
(task.data as any) = {...(task.data as any)};
|
||||
}
|
||||
(task.data as any)[creationTrace] = trace;
|
||||
}
|
||||
return parentZoneDelegate.scheduleTask(targetZone, task);
|
||||
},
|
||||
|
||||
onHandleError: function(
|
||||
parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, error: any): boolean {
|
||||
if (Error.stackTraceLimit > 0) {
|
||||
// if Error.stackTraceLimit is 0, means stack trace
|
||||
// is disabled, so we don't need to generate long stack trace
|
||||
// this will improve performance in some test(some test will
|
||||
// set stackTraceLimit to 0, https://github.com/angular/zone.js/issues/698
|
||||
const parentTask = Zone.currentTask || error.task;
|
||||
if (error instanceof Error && parentTask) {
|
||||
const longStack =
|
||||
renderLongStackTrace(parentTask.data && parentTask.data[creationTrace], error.stack);
|
||||
try {
|
||||
error.stack = (error as any).longStack = longStack;
|
||||
} catch (err) {
|
||||
}
|
||||
}
|
||||
}
|
||||
return parentZoneDelegate.handleError(targetZone, error);
|
||||
}
|
||||
};
|
||||
|
||||
function captureStackTraces(stackTraces: string[][], count: number): void {
|
||||
if (count > 0) {
|
||||
stackTraces.push(getFrames((new LongStackTrace()).error));
|
||||
captureStackTraces(stackTraces, count - 1);
|
||||
}
|
||||
}
|
||||
|
||||
function computeIgnoreFrames() {
|
||||
if (Error.stackTraceLimit <= 0) {
|
||||
return;
|
||||
}
|
||||
const frames: string[][] = [];
|
||||
captureStackTraces(frames, 2);
|
||||
const frames1 = frames[0];
|
||||
const frames2 = frames[1];
|
||||
for (let i = 0; i < frames1.length; i++) {
|
||||
const frame1 = frames1[i];
|
||||
if (frame1.indexOf(ERROR_TAG) == -1) {
|
||||
let match = frame1.match(/^\s*at\s+/);
|
||||
if (match) {
|
||||
sepTemplate = match[0] + SEP_TAG + ' (http://localhost)';
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < frames1.length; i++) {
|
||||
const frame1 = frames1[i];
|
||||
const frame2 = frames2[i];
|
||||
if (frame1 === frame2) {
|
||||
IGNORE_FRAMES[frame1] = true;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
computeIgnoreFrames();
|
195
packages/zone.js/lib/zone-spec/proxy.ts
Normal file
195
packages/zone.js/lib/zone-spec/proxy.ts
Normal file
@ -0,0 +1,195 @@
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
class ProxyZoneSpec implements ZoneSpec {
|
||||
name: string = 'ProxyZone';
|
||||
|
||||
private _delegateSpec: ZoneSpec|null = null;
|
||||
|
||||
properties: {[k: string]: any} = {'ProxyZoneSpec': this};
|
||||
propertyKeys: string[]|null = null;
|
||||
|
||||
lastTaskState: HasTaskState|null = null;
|
||||
isNeedToTriggerHasTask = false;
|
||||
|
||||
private tasks: Task[] = [];
|
||||
|
||||
static get(): ProxyZoneSpec { return Zone.current.get('ProxyZoneSpec'); }
|
||||
|
||||
static isLoaded(): boolean { return ProxyZoneSpec.get() instanceof ProxyZoneSpec; }
|
||||
|
||||
static assertPresent(): ProxyZoneSpec {
|
||||
if (!ProxyZoneSpec.isLoaded()) {
|
||||
throw new Error(`Expected to be running in 'ProxyZone', but it was not found.`);
|
||||
}
|
||||
return ProxyZoneSpec.get();
|
||||
}
|
||||
|
||||
constructor(private defaultSpecDelegate: ZoneSpec|null = null) {
|
||||
this.setDelegate(defaultSpecDelegate);
|
||||
}
|
||||
|
||||
setDelegate(delegateSpec: ZoneSpec|null) {
|
||||
const isNewDelegate = this._delegateSpec !== delegateSpec;
|
||||
this._delegateSpec = delegateSpec;
|
||||
this.propertyKeys && this.propertyKeys.forEach((key) => delete this.properties[key]);
|
||||
this.propertyKeys = null;
|
||||
if (delegateSpec && delegateSpec.properties) {
|
||||
this.propertyKeys = Object.keys(delegateSpec.properties);
|
||||
this.propertyKeys.forEach((k) => this.properties[k] = delegateSpec.properties ![k]);
|
||||
}
|
||||
// if set a new delegateSpec, shoulde check whether need to
|
||||
// trigger hasTask or not
|
||||
if (isNewDelegate && this.lastTaskState &&
|
||||
(this.lastTaskState.macroTask || this.lastTaskState.microTask)) {
|
||||
this.isNeedToTriggerHasTask = true;
|
||||
}
|
||||
}
|
||||
|
||||
getDelegate() { return this._delegateSpec; }
|
||||
|
||||
|
||||
resetDelegate() {
|
||||
const delegateSpec = this.getDelegate();
|
||||
this.setDelegate(this.defaultSpecDelegate);
|
||||
}
|
||||
|
||||
tryTriggerHasTask(parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone) {
|
||||
if (this.isNeedToTriggerHasTask && this.lastTaskState) {
|
||||
// last delegateSpec has microTask or macroTask
|
||||
// should call onHasTask in current delegateSpec
|
||||
this.isNeedToTriggerHasTask = false;
|
||||
this.onHasTask(parentZoneDelegate, currentZone, targetZone, this.lastTaskState);
|
||||
}
|
||||
}
|
||||
|
||||
removeFromTasks(task: Task) {
|
||||
if (!this.tasks) {
|
||||
return;
|
||||
}
|
||||
for (let i = 0; i < this.tasks.length; i++) {
|
||||
if (this.tasks[i] === task) {
|
||||
this.tasks.splice(i, 1);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getAndClearPendingTasksInfo() {
|
||||
if (this.tasks.length === 0) {
|
||||
return '';
|
||||
}
|
||||
const taskInfo = this.tasks.map((task: Task) => {
|
||||
const dataInfo = task.data &&
|
||||
Object.keys(task.data)
|
||||
.map((key: string) => { return key + ':' + (task.data as any)[key]; })
|
||||
.join(',');
|
||||
return `type: ${task.type}, source: ${task.source}, args: {${dataInfo}}`;
|
||||
});
|
||||
const pendingTasksInfo = '--Pendng async tasks are: [' + taskInfo + ']';
|
||||
// clear tasks
|
||||
this.tasks = [];
|
||||
|
||||
return pendingTasksInfo;
|
||||
}
|
||||
|
||||
onFork(parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, zoneSpec: ZoneSpec):
|
||||
Zone {
|
||||
if (this._delegateSpec && this._delegateSpec.onFork) {
|
||||
return this._delegateSpec.onFork(parentZoneDelegate, currentZone, targetZone, zoneSpec);
|
||||
} else {
|
||||
return parentZoneDelegate.fork(targetZone, zoneSpec);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
onIntercept(
|
||||
parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, delegate: Function,
|
||||
source: string): Function {
|
||||
if (this._delegateSpec && this._delegateSpec.onIntercept) {
|
||||
return this._delegateSpec.onIntercept(
|
||||
parentZoneDelegate, currentZone, targetZone, delegate, source);
|
||||
} else {
|
||||
return parentZoneDelegate.intercept(targetZone, delegate, source);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
onInvoke(
|
||||
parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, delegate: Function,
|
||||
applyThis: any, applyArgs?: any[], source?: string): any {
|
||||
this.tryTriggerHasTask(parentZoneDelegate, currentZone, targetZone);
|
||||
if (this._delegateSpec && this._delegateSpec.onInvoke) {
|
||||
return this._delegateSpec.onInvoke(
|
||||
parentZoneDelegate, currentZone, targetZone, delegate, applyThis, applyArgs, source);
|
||||
} else {
|
||||
return parentZoneDelegate.invoke(targetZone, delegate, applyThis, applyArgs, source);
|
||||
}
|
||||
}
|
||||
|
||||
onHandleError(parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, error: any):
|
||||
boolean {
|
||||
if (this._delegateSpec && this._delegateSpec.onHandleError) {
|
||||
return this._delegateSpec.onHandleError(parentZoneDelegate, currentZone, targetZone, error);
|
||||
} else {
|
||||
return parentZoneDelegate.handleError(targetZone, error);
|
||||
}
|
||||
}
|
||||
|
||||
onScheduleTask(parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, task: Task):
|
||||
Task {
|
||||
if (task.type !== 'eventTask') {
|
||||
this.tasks.push(task);
|
||||
}
|
||||
if (this._delegateSpec && this._delegateSpec.onScheduleTask) {
|
||||
return this._delegateSpec.onScheduleTask(parentZoneDelegate, currentZone, targetZone, task);
|
||||
} else {
|
||||
return parentZoneDelegate.scheduleTask(targetZone, task);
|
||||
}
|
||||
}
|
||||
|
||||
onInvokeTask(
|
||||
parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, task: Task,
|
||||
applyThis: any, applyArgs: any): any {
|
||||
if (task.type !== 'eventTask') {
|
||||
this.removeFromTasks(task);
|
||||
}
|
||||
this.tryTriggerHasTask(parentZoneDelegate, currentZone, targetZone);
|
||||
if (this._delegateSpec && this._delegateSpec.onInvokeTask) {
|
||||
return this._delegateSpec.onInvokeTask(
|
||||
parentZoneDelegate, currentZone, targetZone, task, applyThis, applyArgs);
|
||||
} else {
|
||||
return parentZoneDelegate.invokeTask(targetZone, task, applyThis, applyArgs);
|
||||
}
|
||||
}
|
||||
|
||||
onCancelTask(parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, task: Task):
|
||||
any {
|
||||
if (task.type !== 'eventTask') {
|
||||
this.removeFromTasks(task);
|
||||
}
|
||||
this.tryTriggerHasTask(parentZoneDelegate, currentZone, targetZone);
|
||||
if (this._delegateSpec && this._delegateSpec.onCancelTask) {
|
||||
return this._delegateSpec.onCancelTask(parentZoneDelegate, currentZone, targetZone, task);
|
||||
} else {
|
||||
return parentZoneDelegate.cancelTask(targetZone, task);
|
||||
}
|
||||
}
|
||||
|
||||
onHasTask(delegate: ZoneDelegate, current: Zone, target: Zone, hasTaskState: HasTaskState): void {
|
||||
this.lastTaskState = hasTaskState;
|
||||
if (this._delegateSpec && this._delegateSpec.onHasTask) {
|
||||
this._delegateSpec.onHasTask(delegate, current, target, hasTaskState);
|
||||
} else {
|
||||
delegate.hasTask(target, hasTaskState);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Export the class so that new instances can be created with proper
|
||||
// constructor params.
|
||||
(Zone as any)['ProxyZoneSpec'] = ProxyZoneSpec;
|
33
packages/zone.js/lib/zone-spec/sync-test.ts
Normal file
33
packages/zone.js/lib/zone-spec/sync-test.ts
Normal file
@ -0,0 +1,33 @@
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
|
||||
class SyncTestZoneSpec implements ZoneSpec {
|
||||
runZone = Zone.current;
|
||||
|
||||
constructor(namePrefix: string) { this.name = 'syncTestZone for ' + namePrefix; }
|
||||
|
||||
// ZoneSpec implementation below.
|
||||
|
||||
name: string;
|
||||
|
||||
onScheduleTask(delegate: ZoneDelegate, current: Zone, target: Zone, task: Task): Task {
|
||||
switch (task.type) {
|
||||
case 'microTask':
|
||||
case 'macroTask':
|
||||
throw new Error(`Cannot call ${task.source} from within a sync test.`);
|
||||
case 'eventTask':
|
||||
task = delegate.scheduleTask(target, task);
|
||||
break;
|
||||
}
|
||||
return task;
|
||||
}
|
||||
}
|
||||
|
||||
// Export the class so that new instances can be created with proper
|
||||
// constructor params.
|
||||
(Zone as any)['SyncTestZoneSpec'] = SyncTestZoneSpec;
|
80
packages/zone.js/lib/zone-spec/task-tracking.ts
Normal file
80
packages/zone.js/lib/zone-spec/task-tracking.ts
Normal file
@ -0,0 +1,80 @@
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
|
||||
/**
|
||||
* A `TaskTrackingZoneSpec` allows one to track all outstanding Tasks.
|
||||
*
|
||||
* This is useful in tests. For example to see which tasks are preventing a test from completing
|
||||
* or an automated way of releasing all of the event listeners at the end of the test.
|
||||
*/
|
||||
class TaskTrackingZoneSpec implements ZoneSpec {
|
||||
name = 'TaskTrackingZone';
|
||||
microTasks: Task[] = [];
|
||||
macroTasks: Task[] = [];
|
||||
eventTasks: Task[] = [];
|
||||
properties: {[key: string]: any} = {'TaskTrackingZone': this};
|
||||
|
||||
static get() { return Zone.current.get('TaskTrackingZone'); }
|
||||
|
||||
private getTasksFor(type: string): Task[] {
|
||||
switch (type) {
|
||||
case 'microTask':
|
||||
return this.microTasks;
|
||||
case 'macroTask':
|
||||
return this.macroTasks;
|
||||
case 'eventTask':
|
||||
return this.eventTasks;
|
||||
}
|
||||
throw new Error('Unknown task format: ' + type);
|
||||
}
|
||||
|
||||
onScheduleTask(parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, task: Task):
|
||||
Task {
|
||||
(task as any)['creationLocation'] = new Error(`Task '${task.type}' from '${task.source}'.`);
|
||||
const tasks = this.getTasksFor(task.type);
|
||||
tasks.push(task);
|
||||
return parentZoneDelegate.scheduleTask(targetZone, task);
|
||||
}
|
||||
|
||||
onCancelTask(parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, task: Task):
|
||||
any {
|
||||
const tasks = this.getTasksFor(task.type);
|
||||
for (let i = 0; i < tasks.length; i++) {
|
||||
if (tasks[i] == task) {
|
||||
tasks.splice(i, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return parentZoneDelegate.cancelTask(targetZone, task);
|
||||
}
|
||||
|
||||
onInvokeTask(
|
||||
parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, task: Task,
|
||||
applyThis: any, applyArgs: any): any {
|
||||
if (task.type === 'eventTask')
|
||||
return parentZoneDelegate.invokeTask(targetZone, task, applyThis, applyArgs);
|
||||
const tasks = this.getTasksFor(task.type);
|
||||
for (let i = 0; i < tasks.length; i++) {
|
||||
if (tasks[i] == task) {
|
||||
tasks.splice(i, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return parentZoneDelegate.invokeTask(targetZone, task, applyThis, applyArgs);
|
||||
}
|
||||
|
||||
clearEvents() {
|
||||
while (this.eventTasks.length) {
|
||||
Zone.current.cancelTask(this.eventTasks[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Export the class so that new instances can be created with proper
|
||||
// constructor params.
|
||||
(Zone as any)['TaskTrackingZoneSpec'] = TaskTrackingZoneSpec;
|
161
packages/zone.js/lib/zone-spec/wtf.ts
Normal file
161
packages/zone.js/lib/zone-spec/wtf.ts
Normal file
@ -0,0 +1,161 @@
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
/**
|
||||
* @fileoverview
|
||||
* @suppress {missingRequire}
|
||||
*/
|
||||
|
||||
(function(global: any) {
|
||||
interface Wtf {
|
||||
trace: WtfTrace;
|
||||
}
|
||||
interface WtfScope {}
|
||||
interface WtfRange {}
|
||||
interface WtfTrace {
|
||||
events: WtfEvents;
|
||||
leaveScope(scope: WtfScope, returnValue?: any): void;
|
||||
beginTimeRange(rangeType: string, action: string): WtfRange;
|
||||
endTimeRange(range: WtfRange): void;
|
||||
}
|
||||
interface WtfEvents {
|
||||
createScope(signature: string, flags?: any): WtfScopeFn;
|
||||
createInstance(signature: string, flags?: any): WtfEventFn;
|
||||
}
|
||||
|
||||
type WtfScopeFn = (...args: any[]) => WtfScope;
|
||||
type WtfEventFn = (...args: any[]) => any;
|
||||
|
||||
// Detect and setup WTF.
|
||||
let wtfTrace: WtfTrace|null = null;
|
||||
let wtfEvents: WtfEvents|null = null;
|
||||
const wtfEnabled: boolean = (function(): boolean {
|
||||
const wtf: Wtf = global['wtf'];
|
||||
if (wtf) {
|
||||
wtfTrace = wtf.trace;
|
||||
if (wtfTrace) {
|
||||
wtfEvents = wtfTrace.events;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
})();
|
||||
|
||||
class WtfZoneSpec implements ZoneSpec {
|
||||
name: string = 'WTF';
|
||||
|
||||
static forkInstance =
|
||||
wtfEnabled? wtfEvents !.createInstance('Zone:fork(ascii zone, ascii newZone)'): null;
|
||||
static scheduleInstance: {[key: string]: WtfEventFn} = {};
|
||||
static cancelInstance: {[key: string]: WtfEventFn} = {};
|
||||
static invokeScope: {[key: string]: WtfEventFn} = {};
|
||||
static invokeTaskScope: {[key: string]: WtfEventFn} = {};
|
||||
|
||||
onFork(
|
||||
parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone,
|
||||
zoneSpec: ZoneSpec): Zone {
|
||||
const retValue = parentZoneDelegate.fork(targetZone, zoneSpec);
|
||||
WtfZoneSpec.forkInstance !(zonePathName(targetZone), retValue.name);
|
||||
return retValue;
|
||||
}
|
||||
|
||||
onInvoke(
|
||||
parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, delegate: Function,
|
||||
applyThis: any, applyArgs?: any[], source?: string): any {
|
||||
const src = source || 'unknown';
|
||||
let scope = WtfZoneSpec.invokeScope[src];
|
||||
if (!scope) {
|
||||
scope = WtfZoneSpec.invokeScope[src] =
|
||||
wtfEvents !.createScope(`Zone:invoke:${source}(ascii zone)`);
|
||||
}
|
||||
return wtfTrace !.leaveScope(
|
||||
scope(zonePathName(targetZone)),
|
||||
parentZoneDelegate.invoke(targetZone, delegate, applyThis, applyArgs, source));
|
||||
}
|
||||
|
||||
|
||||
onHandleError(
|
||||
parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone,
|
||||
error: any): boolean {
|
||||
return parentZoneDelegate.handleError(targetZone, error);
|
||||
}
|
||||
|
||||
onScheduleTask(
|
||||
parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, task: Task): any {
|
||||
const key = task.type + ':' + task.source;
|
||||
let instance = WtfZoneSpec.scheduleInstance[key];
|
||||
if (!instance) {
|
||||
instance = WtfZoneSpec.scheduleInstance[key] =
|
||||
wtfEvents !.createInstance(`Zone:schedule:${key}(ascii zone, any data)`);
|
||||
}
|
||||
const retValue = parentZoneDelegate.scheduleTask(targetZone, task);
|
||||
instance(zonePathName(targetZone), shallowObj(task.data, 2));
|
||||
return retValue;
|
||||
}
|
||||
|
||||
|
||||
onInvokeTask(
|
||||
parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, task: Task,
|
||||
applyThis?: any, applyArgs?: any[]): any {
|
||||
const source = task.source;
|
||||
let scope = WtfZoneSpec.invokeTaskScope[source];
|
||||
if (!scope) {
|
||||
scope = WtfZoneSpec.invokeTaskScope[source] =
|
||||
wtfEvents !.createScope(`Zone:invokeTask:${source}(ascii zone)`);
|
||||
}
|
||||
return wtfTrace !.leaveScope(
|
||||
scope(zonePathName(targetZone)),
|
||||
parentZoneDelegate.invokeTask(targetZone, task, applyThis, applyArgs));
|
||||
}
|
||||
|
||||
onCancelTask(parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, task: Task):
|
||||
any {
|
||||
const key = task.source;
|
||||
let instance = WtfZoneSpec.cancelInstance[key];
|
||||
if (!instance) {
|
||||
instance = WtfZoneSpec.cancelInstance[key] =
|
||||
wtfEvents !.createInstance(`Zone:cancel:${key}(ascii zone, any options)`);
|
||||
}
|
||||
const retValue = parentZoneDelegate.cancelTask(targetZone, task);
|
||||
instance(zonePathName(targetZone), shallowObj(task.data, 2));
|
||||
return retValue;
|
||||
}
|
||||
}
|
||||
|
||||
function shallowObj(obj: {[k: string]: any} | undefined, depth: number): any {
|
||||
if (!obj || !depth) return null;
|
||||
const out: {[k: string]: any} = {};
|
||||
for (const key in obj) {
|
||||
if (obj.hasOwnProperty(key)) {
|
||||
let value = obj[key];
|
||||
switch (typeof value) {
|
||||
case 'object':
|
||||
const name = value && value.constructor && (<any>value.constructor).name;
|
||||
value = name == (<any>Object).name ? shallowObj(value, depth - 1) : name;
|
||||
break;
|
||||
case 'function':
|
||||
value = value.name || undefined;
|
||||
break;
|
||||
}
|
||||
out[key] = value;
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function zonePathName(zone: Zone) {
|
||||
let name: string = zone.name;
|
||||
let localZone = zone.parent;
|
||||
while (localZone != null) {
|
||||
name = localZone.name + '::' + name;
|
||||
localZone = localZone.parent;
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
(Zone as any)['wtfZoneSpec'] = !wtfEnabled ? null : new WtfZoneSpec();
|
||||
})(global);
|
Reference in New Issue
Block a user