@@ -34,6 +34,9 @@ interface MacroTaskOptions {
3434 callbackArgs ?: any ;
3535}
3636
37+ // Need this because mock clocks might be installed (other than fakeAsync!)
38+ const originalSetImmediate = global . setImmediate ;
39+ const originalTimeout = global . setTimeout ;
3740const OriginalDate = 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+ const current = 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 return 0 ;
@@ -547,6 +563,99 @@ class FakeAsyncTestZoneSpec implements ZoneSpec {
547563 FakeAsyncTestZoneSpec . resetDate ( ) ;
548564 }
549565
566+ private tickMode : { 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+ private advanceUntilModeChanges ( doTick ?: ( elapsed : number ) => void ) : void {
584+ FakeAsyncTestZoneSpec . assertInZone ( ) ;
585+ const specZone = 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+ await safeAsync ( async ( ) => {
595+ await void 0 ;
596+ specZone . run ( ( ) => {
597+ this . flushMicrotasks ( ) ;
598+ } ) ;
599+ } ) ;
600+
601+ if ( this . tickMode . counter !== counter ) {
602+ return ;
603+ }
604+
605+ while ( true ) {
606+ await safeAsync ( ( ) => this . newMacrotask ( specZone ) ) ;
607+
608+ if ( this . tickMode . counter !== counter ) {
609+ return ;
610+ }
611+
612+ await safeAsync ( ( ) =>
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+ private async newMacrotask ( specZone : Zone ) {
628+ if ( originalSetImmediate ) {
629+ // setImmediate is much faster than setTimeout in node
630+ await new Promise ( ( 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+ const channel = new MessageChannel ( ) ;
640+ await new Promise ( ( 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+ await new Promise ( ( 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- throw new Error (
680- 'Cannot make XHRs from within a fake async test. Request URL: ' +
681- ( task . data as any ) [ 'url' ] ,
682- ) ;
788+ if ( this . tickMode . mode === 'manual' ) {
789+ throw new Error (
790+ 'Cannot make XHRs from within a fake async test. Request URL: ' +
791+ ( task . data as any ) [ '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+ async function safeAsync ( fn : ( ) => Promise < void > ) : Promise < void > {
1154+ try {
1155+ return await fn ( ) ;
1156+ } catch ( e ) {
1157+ hostReportError ( e ) ;
1158+ }
1159+ }
1160+
1161+ function hostReportError ( e : unknown ) {
1162+ Zone . root . run ( ( ) => {
1163+ originalTimeout ( ( ) => {
1164+ throw e ;
1165+ } ) ;
1166+ } ) ;
1167+ }
0 commit comments