Skip to content

Commit 0a827f9

Browse files
committed
refactor(zone.js): Add internal implementation for auto ticking fakeAsync (#62135)
Benefits of auto-ticking mock clocks have been described in other PRs, such as jasmine/jasmine#2042 and sinonjs/fake-timers#509. In short, `fakeAsync` cannot work when some tasks are required to be truly async, such as XHRs or observers like ResizeObserver. In addition, auto ticking mock clocks can be applied to tests without the tests then needing to update everything to manually flush timers. PR Close#62135
1 parent 124dcc0 commit 0a827f9

2 files changed

Lines changed: 215 additions & 4 deletions

File tree

‎packages/zone.js/lib/zone-spec/fake-async-test.ts‎

Lines changed: 135 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ interface MacroTaskOptions {
3434
callbackArgs?: any;
3535
}
3636

37+
// Need this because mock clocks might be installed (other than fakeAsync!)
38+
constoriginalSetImmediate=global.setImmediate;
39+
constoriginalTimeout=global.setTimeout;
3740
constOriginalDate=global.Date;
3841
// Since when we compile this file to `es2015`, and if we define
3942
// this `FakeDate` as `class FakeDate`, and then set `FakeDate.prototype`
@@ -274,6 +277,19 @@ class Scheduler {
274277
}
275278
}
276279

280+
executeNextTask(doTick?: (elapsed: number)=>void): void{
281+
constcurrent=this._schedulerQueue.shift();
282+
if(current===undefined){
283+
return;
284+
}
285+
doTick?.(current.endTime-this._currentTickTime);
286+
this._currentTickTime=current.endTime;
287+
current.func.apply(
288+
global,
289+
current.isRequestAnimationFrame ? [this._currentTickTime] : current.args,
290+
);
291+
}
292+
277293
flushOnlyPendingTimers(doTick?: (elapsed: number)=>void): number{
278294
if(this._schedulerQueue.length===0){
279295
return0;
@@ -547,6 +563,99 @@ class FakeAsyncTestZoneSpec implements ZoneSpec {
547563
FakeAsyncTestZoneSpec.resetDate();
548564
}
549565

566+
privatetickMode: {counter: number;mode: 'manual'|'automatic'}={
567+
counter: 0,
568+
mode: 'manual',
569+
};
570+
571+
/** @experimental */
572+
setTickMode(mode: 'manual'|'automatic',doTick?: (elapsed: number)=>void){
573+
if(mode===this.tickMode.mode){
574+
return;
575+
}
576+
this.tickMode.counter++;
577+
this.tickMode.mode=mode;
578+
if(mode==='automatic'){
579+
this.advanceUntilModeChanges(doTick);
580+
}
581+
}
582+
583+
privateadvanceUntilModeChanges(doTick?: (elapsed: number)=>void): void{
584+
FakeAsyncTestZoneSpec.assertInZone();
585+
constspecZone=Zone.current;
586+
const{counter}=this.tickMode;
587+
588+
Zone.root.run(async()=>{
589+
// autoTick with fakeAsync is a bit awkward because microtasks are
590+
// controlled by the scheduler as well. This means that we have to
591+
// manually flush microtasks before allowing real macrotasks to execute.
592+
// Waiting for a macrotask would otherwise allow the browser to execute
593+
// other macrotasks before the currently scheduled microtasks are flushed.
594+
awaitsafeAsync(async()=>{
595+
awaitvoid0;
596+
specZone.run(()=>{
597+
this.flushMicrotasks();
598+
});
599+
});
600+
601+
if(this.tickMode.counter!==counter){
602+
return;
603+
}
604+
605+
while(true){
606+
awaitsafeAsync(()=>this.newMacrotask(specZone));
607+
608+
if(this.tickMode.counter!==counter){
609+
return;
610+
}
611+
612+
awaitsafeAsync(()=>
613+
specZone.run(()=>{
614+
this._scheduler.executeNextTask(doTick);
615+
}),
616+
);
617+
}
618+
});
619+
}
620+
621+
// Waits until a new macro task.
622+
//
623+
// Used with autoTick(), which is meant to act when the test is waiting, we
624+
// need to insert ourselves in the macro task queue.
625+
//
626+
// @return {!Promise<undefined>}
627+
privateasyncnewMacrotask(specZone: Zone){
628+
if(originalSetImmediate){
629+
// setImmediate is much faster than setTimeout in node
630+
awaitnewPromise((resolve)=>{
631+
originalSetImmediate(resolve);
632+
});
633+
}else{
634+
// MessageChannel ensures that setTimeout is not throttled to 4ms.
635+
// https://developer.mozilla.org/en-US/docs/Web/API/setTimeout#reasons_for_delays_longer_than_specified
636+
// https://stackblitz.com/edit/stackblitz-starters-qtlpcc
637+
// Note: This trick does not work in Safari, which will still throttle the
638+
// setTimeout
639+
constchannel=newMessageChannel();
640+
awaitnewPromise((resolve)=>{
641+
channel.port1.onmessage=resolve;
642+
channel.port2.postMessage(undefined);
643+
});
644+
channel.port1.close();
645+
channel.port2.close();
646+
// setTimeout ensures that we interleave with other setTimeouts.
647+
awaitnewPromise((resolve)=>{
648+
originalTimeout(resolve);
649+
});
650+
}
651+
652+
// flush any microtasks that were scheduled from the tasks that ran during
653+
// the timeout.
654+
specZone.run(()=>{
655+
this.flushMicrotasks();
656+
});
657+
}
658+
550659
tickToNext(
551660
steps: number=1,
552661
doTick?: (elapsed: number)=>void,
@@ -676,10 +785,16 @@ class FakeAsyncTestZoneSpec implements ZoneSpec {
676785
);
677786
break;
678787
case'XMLHttpRequest.send':
679-
thrownewError(
680-
'Cannot make XHRs from within a fake async test. Request URL: '+
681-
(task.dataasany)['url'],
682-
);
788+
if(this.tickMode.mode==='manual'){
789+
thrownewError(
790+
'Cannot make XHRs from within a fake async test. Request URL: '+
791+
(task.dataasany)['url'],
792+
);
793+
}
794+
// When using automatic ticking, we allow the XHR to be handled in a truly async form
795+
// by the parent/delegate Zone because auto ticking FakeAsync is not strictly synchronous.
796+
task=delegate.scheduleTask(target,task);
797+
break;
683798
case'requestAnimationFrame':
684799
case'webkitRequestAnimationFrame':
685800
case'mozRequestAnimationFrame':
@@ -1034,3 +1149,19 @@ export function patchFakeAsyncTest(Zone: ZoneType): void {
10341149

10351150
Scheduler.nextId=Scheduler.getNextId();
10361151
}
1152+
1153+
asyncfunctionsafeAsync(fn: ()=>Promise<void>): Promise<void>{
1154+
try{
1155+
returnawaitfn();
1156+
}catch(e){
1157+
hostReportError(e);
1158+
}
1159+
}
1160+
1161+
functionhostReportError(e: unknown){
1162+
Zone.root.run(()=>{
1163+
originalTimeout(()=>{
1164+
throwe;
1165+
});
1166+
});
1167+
}

‎packages/zone.js/test/zone-spec/fake-async-test.spec.ts‎

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1152,6 +1152,86 @@ describe('FakeAsyncTestZoneSpec', () => {
11521152
emptyRun,
11531153
),
11541154
);
1155+
1156+
describe('setTickMode',()=>{
1157+
it('should execute timers automatically when mode is set to automatic',async()=>{
1158+
letran=false;
1159+
awaitnewPromise<void>((resolve)=>{
1160+
fakeAsyncTestZone.run(()=>{
1161+
setTimeout(()=>{
1162+
ran=true;
1163+
resolve();
1164+
},10);
1165+
testZoneSpec.setTickMode('automatic');
1166+
});
1167+
});
1168+
1169+
expect(ran).toBe(true);
1170+
fakeAsyncTestZone.run(()=>{
1171+
testZoneSpec.setTickMode('manual');
1172+
});
1173+
});
1174+
1175+
it('should execute multiple timers automatically',async()=>{
1176+
constlog: string[]=[];
1177+
constpromise=newPromise<void>((resolve)=>{
1178+
fakeAsyncTestZone.run(()=>{
1179+
setTimeout(()=>{
1180+
log.push('timer A');
1181+
},10);
1182+
setTimeout(()=>{
1183+
log.push('timer B');
1184+
resolve();
1185+
},20);
1186+
testZoneSpec.setTickMode('automatic');
1187+
});
1188+
});
1189+
1190+
awaitpromise;
1191+
expect(log).toEqual(['timer A','timer B']);
1192+
fakeAsyncTestZone.run(()=>{
1193+
testZoneSpec.setTickMode('manual');
1194+
});
1195+
});
1196+
1197+
it('should allow mutation observers to execute between timers',async()=>{
1198+
if(isNode){
1199+
return;
1200+
}
1201+
constlog: string[]=[];
1202+
constel=document.createElement('div');
1203+
letobserver: MutationObserver;
1204+
1205+
awaitnewPromise<void>((resolve)=>{
1206+
fakeAsyncTestZone.run(()=>{
1207+
document.body.appendChild(el);
1208+
observer=newMutationObserver(()=>{
1209+
log.push('mutation');
1210+
});
1211+
observer.observe(el,{attributes: true});
1212+
1213+
setTimeout(()=>{
1214+
log.push('timer A');
1215+
el.style.width='100px';// trigger mutation observer
1216+
},10);
1217+
setTimeout(()=>{
1218+
debugger;
1219+
log.push('timer B');
1220+
resolve();
1221+
},10);
1222+
1223+
testZoneSpec.setTickMode('automatic');
1224+
});
1225+
});
1226+
1227+
expect(log).toEqual(['timer A','mutation','timer B']);
1228+
fakeAsyncTestZone.run(()=>{
1229+
testZoneSpec.setTickMode('manual');
1230+
observer.disconnect();
1231+
});
1232+
document.body.removeChild(el);
1233+
});
1234+
});
11551235
});
11561236

11571237
classLog<T>{

0 commit comments

Comments
 (0)