Skip to content
This repository was archived by the owner on Feb 26, 2024. It is now read-only.

Commit 34159b4

Browse files
vikermanjuliemr
authored andcommitted
feat: Add a zone spec for fake async test zone. (#330)
1 parent 1d30d96 commit 34159b4

File tree

4 files changed

+572
-0
lines changed

4 files changed

+572
-0
lines changed

Diff for: gulpfile.js

+5
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,10 @@ gulp.task('build/async-test.js', function(cb) {
109109
return generateBrowserScript('./lib/zone-spec/async-test.ts', 'async-test.js', false, cb);
110110
});
111111

112+
gulp.task('build/fake-async-test.js', function(cb) {
113+
return generateBrowserScript('./lib/zone-spec/fake-async-test.ts', 'fake-async-test.js', false, cb);
114+
});
115+
112116
gulp.task('build/sync-test.js', function(cb) {
113117
return generateBrowserScript('./lib/zone-spec/sync-test.ts', 'sync-test.js', false, cb);
114118
});
@@ -125,6 +129,7 @@ gulp.task('build', [
125129
'build/wtf.js',
126130
'build/wtf.min.js',
127131
'build/async-test.js',
132+
'build/fake-async-test.js',
128133
'build/sync-test.js'
129134
]);
130135

Diff for: lib/zone-spec/fake-async-test.ts

+256
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
1+
(function() {
2+
interface ScheduledFunction {
3+
endTime: number;
4+
id: number,
5+
func: Function;
6+
args: any[];
7+
delay: number;
8+
}
9+
10+
class Scheduler {
11+
// Next scheduler id.
12+
public nextId: number = 0;
13+
14+
// Scheduler queue with the tuple of end time and callback function - sorted by end time.
15+
private _schedulerQueue: ScheduledFunction[] = [];
16+
// Current simulated time in millis.
17+
private _currentTime: number = 0;
18+
19+
constructor() {}
20+
21+
scheduleFunction(cb: Function, delay: number, args: any[] = [], id: number = -1) : number {
22+
let currentId: number = id < 0 ? this.nextId++ : id;
23+
let endTime = this._currentTime + delay;
24+
25+
// Insert so that scheduler queue remains sorted by end time.
26+
let newEntry: ScheduledFunction = {
27+
endTime: endTime,
28+
id: currentId,
29+
func: cb,
30+
args: args,
31+
delay: delay
32+
}
33+
let i = 0;
34+
for (; i < this._schedulerQueue.length; i++) {
35+
let currentEntry = this._schedulerQueue[i];
36+
if (newEntry.endTime < currentEntry.endTime) {
37+
break;
38+
}
39+
}
40+
this._schedulerQueue.splice(i, 0, newEntry);
41+
return currentId;
42+
}
43+
44+
removeScheduledFunctionWithId(id: number): void {
45+
for (let i = 0; i < this._schedulerQueue.length; i++) {
46+
if (this._schedulerQueue[i].id == id) {
47+
this._schedulerQueue.splice(i, 1);
48+
break;
49+
}
50+
}
51+
}
52+
53+
tick(millis: number = 0): void {
54+
this._currentTime += millis;
55+
while (this._schedulerQueue.length > 0) {
56+
let current = this._schedulerQueue[0];
57+
if (this._currentTime < current.endTime) {
58+
// Done processing the queue since it's sorted by endTime.
59+
break;
60+
} else {
61+
// Time to run scheduled function. Remove it from the head of queue.
62+
let current = this._schedulerQueue.shift();
63+
let retval = current.func.apply(global, current.args);
64+
if (!retval) {
65+
// Uncaught exception in the current scheduled function. Stop processing the queue.
66+
break;
67+
}
68+
}
69+
}
70+
}
71+
}
72+
73+
class FakeAsyncTestZoneSpec implements ZoneSpec {
74+
static assertInZone(): void {
75+
if (Zone.current.get('FakeAsyncTestZoneSpec') == null) {
76+
throw new Error('The code should be running in the fakeAsync zone to call this function');
77+
}
78+
}
79+
80+
private _scheduler: Scheduler = new Scheduler();
81+
private _microtasks: Function[] = [];
82+
private _lastError: Error = null;
83+
84+
pendingPeriodicTimers: number[] = [];
85+
pendingTimers: number[] = [];
86+
87+
constructor(namePrefix: string) {
88+
this.name = 'fakeAsyncTestZone for ' + namePrefix;
89+
}
90+
91+
private _fnAndFlush(fn: Function,
92+
completers: {onSuccess?: Function, onError?: Function}): Function {
93+
return (...args): boolean => {
94+
fn.apply(global, args);
95+
96+
if (this._lastError === null) { // Success
97+
if (completers.onSuccess != null) {
98+
completers.onSuccess.apply(global);
99+
}
100+
// Flush microtasks only on success.
101+
this.flushMicrotasks();
102+
} else { // Failure
103+
if (completers.onError != null) {
104+
completers.onError.apply(global);
105+
}
106+
}
107+
// Return true if there were no errors, false otherwise.
108+
return this._lastError === null;
109+
}
110+
}
111+
112+
private static _removeTimer(timers: number[], id:number): void {
113+
let index = timers.indexOf(id);
114+
if (index > -1) {
115+
timers.splice(index, 1);
116+
}
117+
}
118+
119+
private _dequeueTimer(id: number): Function {
120+
return () => {
121+
FakeAsyncTestZoneSpec._removeTimer(this.pendingTimers, id);
122+
};
123+
}
124+
125+
private _requeuePeriodicTimer(
126+
fn: Function, interval: number, args: any[], id: number): Function {
127+
return () => {
128+
// Requeue the timer callback if it's not been canceled.
129+
if (this.pendingPeriodicTimers.indexOf(id) !== -1) {
130+
this._scheduler.scheduleFunction(fn, interval, args, id);
131+
}
132+
}
133+
}
134+
135+
private _dequeuePeriodicTimer(id: number): Function {
136+
return () => {
137+
FakeAsyncTestZoneSpec._removeTimer(this.pendingPeriodicTimers, id);
138+
};
139+
}
140+
141+
private _setTimeout(fn: Function, delay: number, args: any[]): number {
142+
let removeTimerFn = this._dequeueTimer(this._scheduler.nextId);
143+
// Queue the callback and dequeue the timer on success and error.
144+
let cb = this._fnAndFlush(fn, {onSuccess: removeTimerFn, onError: removeTimerFn});
145+
let id = this._scheduler.scheduleFunction(cb, delay, args);
146+
this.pendingTimers.push(id);
147+
return id;
148+
}
149+
150+
private _clearTimeout(id: number): void {
151+
FakeAsyncTestZoneSpec._removeTimer(this.pendingTimers, id);
152+
this._scheduler.removeScheduledFunctionWithId(id);
153+
}
154+
155+
private _setInterval(fn: Function, interval: number, ...args): number {
156+
let id = this._scheduler.nextId;
157+
let completers = {onSuccess: null, onError: this._dequeuePeriodicTimer(id)};
158+
let cb = this._fnAndFlush(fn, completers);
159+
160+
// Use the callback created above to requeue on success.
161+
completers.onSuccess = this._requeuePeriodicTimer(cb, interval, args, id);
162+
163+
// Queue the callback and dequeue the periodic timer only on error.
164+
this._scheduler.scheduleFunction(cb, interval, args);
165+
this.pendingPeriodicTimers.push(id);
166+
return id;
167+
}
168+
169+
private _clearInterval(id: number): void {
170+
FakeAsyncTestZoneSpec._removeTimer(this.pendingPeriodicTimers, id);
171+
this._scheduler.removeScheduledFunctionWithId(id);
172+
}
173+
174+
private _resetLastErrorAndThrow(): void {
175+
let error = this._lastError;
176+
this._lastError = null;
177+
throw error;
178+
}
179+
180+
tick(millis: number = 0): void {
181+
FakeAsyncTestZoneSpec.assertInZone();
182+
this.flushMicrotasks();
183+
this._scheduler.tick(millis);
184+
if (this._lastError !== null) {
185+
this._resetLastErrorAndThrow();
186+
}
187+
}
188+
189+
flushMicrotasks(): void {
190+
FakeAsyncTestZoneSpec.assertInZone();
191+
while (this._microtasks.length > 0) {
192+
let microtask = this._microtasks.shift();
193+
microtask();
194+
if (this._lastError !== null) {
195+
// If there is an error stop processing the microtask queue and rethrow the error.
196+
this._resetLastErrorAndThrow();
197+
}
198+
}
199+
}
200+
201+
// ZoneSpec implementation below.
202+
203+
name: string;
204+
205+
properties: { [key: string]: any } = { 'FakeAsyncTestZoneSpec': this };
206+
207+
onScheduleTask(delegate: ZoneDelegate, current: Zone, target: Zone, task: Task): Task {
208+
switch (task.type) {
209+
case 'microTask':
210+
this._microtasks.push(task.invoke);
211+
break;
212+
case 'macroTask':
213+
switch (task.source) {
214+
case 'setTimeout':
215+
task.data['handleId'] =
216+
this._setTimeout(task.invoke, task.data['delay'], task.data['args']);
217+
break;
218+
case 'setInterval':
219+
task.data['handleId'] =
220+
this._setInterval(task.invoke, task.data['delay'], task.data['args']);
221+
break;
222+
case 'XMLHttpRequest.send':
223+
throw new Error('Cannot make XHRs from within a fake async test.');
224+
default:
225+
task = delegate.scheduleTask(target, task);
226+
}
227+
break;
228+
case 'eventTask':
229+
task = delegate.scheduleTask(target, task);
230+
break;
231+
}
232+
return task;
233+
}
234+
235+
onCancelTask(delegate: ZoneDelegate, current: Zone, target: Zone, task: Task): any {
236+
switch (task.source) {
237+
case 'setTimeout':
238+
return this._clearTimeout(task.data['handleId']);
239+
case 'setInterval':
240+
return this._clearInterval(task.data['handleId']);
241+
default:
242+
return delegate.cancelTask(target, task);
243+
}
244+
}
245+
246+
onHandleError(parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone,
247+
error: any): boolean {
248+
this._lastError = error;
249+
return false; // Don't propagate error to parent zone.
250+
}
251+
}
252+
253+
// Export the class so that new instances can be created with proper
254+
// constructor params.
255+
Zone['FakeAsyncTestZoneSpec'] = FakeAsyncTestZoneSpec;
256+
})();

Diff for: test/common_tests.ts

+1
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ import './common/setTimeout.spec';
77
import './zone-spec/long-stack-trace-zone.spec';
88
import './zone-spec/async-test.spec';
99
import './zone-spec/sync-test.spec';
10+
import './zone-spec/fake-async-test.spec';

0 commit comments

Comments
 (0)