From 27f3f8dbfd2bf9209c0e167e79b1da31ae0b22cc Mon Sep 17 00:00:00 2001 From: "s.v.zaytsev" Date: Tue, 20 May 2025 07:35:47 +0500 Subject: [PATCH] feat: add public delegate method --- src/internal/testing/TestScheduler.ts | 69 ++++++++++++++++++++------- 1 file changed, 51 insertions(+), 18 deletions(-) diff --git a/src/internal/testing/TestScheduler.ts b/src/internal/testing/TestScheduler.ts index 0045166740..6de3573619 100644 --- a/src/internal/testing/TestScheduler.ts +++ b/src/internal/testing/TestScheduler.ts @@ -637,19 +637,25 @@ export class TestScheduler extends VirtualTimeScheduler { } /** - * The `run` method performs the test in 'run mode' - in which schedulers - * used within the test automatically delegate to the `TestScheduler`. That - * is, in 'run mode' there is no need to explicitly pass a `TestScheduler` - * instance to observable creators or operators. + * Delegates all global timer and animation providers to the current {@link TestScheduler} instance. + * Intended for manual usage outside of {@link run}, it enables deterministic control over scheduling + * and time-based operations in tests. * - * @see {@link /guide/testing/marble-testing} + * Returns a `dispose` function that must be called to restore the original environment. + * Also implements `[Symbol.dispose]` for compatibility with the ECMAScript resource disposal protocol. */ - run(callback: (helpers: RunHelpers) => T): T { + delegate({ + frameTimeFactor = TestScheduler.frameTimeFactor, + maxFrames = Infinity, + }: { + frameTimeFactor?: number; + maxFrames?: number; + } = {}) { const prevFrameTimeFactor = TestScheduler.frameTimeFactor; const prevMaxFrames = this.maxFrames; - TestScheduler.frameTimeFactor = 1; - this.maxFrames = Infinity; + TestScheduler.frameTimeFactor = frameTimeFactor; + this.maxFrames = maxFrames; this.runMode = true; const animator = this.createAnimator(); @@ -662,6 +668,40 @@ export class TestScheduler extends VirtualTimeScheduler { timeoutProvider.delegate = delegates.timeout; performanceTimestampProvider.delegate = this; + const dispose = () => { + TestScheduler.frameTimeFactor = prevFrameTimeFactor; + this.maxFrames = prevMaxFrames; + this.runMode = false; + animationFrameProvider.delegate = undefined; + dateTimestampProvider.delegate = undefined; + immediateProvider.delegate = undefined; + intervalProvider.delegate = undefined; + timeoutProvider.delegate = undefined; + performanceTimestampProvider.delegate = undefined; + }; + + return { + dispose, + // @ts-expect-error for compatibility with the ECMAScript resource disposal protocol + [Symbol.dispose ?? 'dispose']: dispose, + animate: animator.animate, + }; + } + + /** + * The `run` method performs the test in 'run mode' - in which schedulers + * used within the test automatically delegate to the `TestScheduler`. That + * is, in 'run mode' there is no need to explicitly pass a `TestScheduler` + * instance to observable creators or operators. + * + * @see {@link /guide/testing/marble-testing} + */ + run(callback: (helpers: RunHelpers) => T): T { + const delegates = this.delegate({ + frameTimeFactor: 1, + maxFrames: Infinity, + }); + const helpers: RunHelpers = { cold: this.createColdObservable.bind(this), hot: this.createHotObservable.bind(this), @@ -669,22 +709,15 @@ export class TestScheduler extends VirtualTimeScheduler { time: this.createTime.bind(this), expectObservable: this.expectObservable.bind(this), expectSubscriptions: this.expectSubscriptions.bind(this), - animate: animator.animate, + animate: delegates.animate, }; + try { const ret = callback(helpers); this.flush(); return ret; } finally { - TestScheduler.frameTimeFactor = prevFrameTimeFactor; - this.maxFrames = prevMaxFrames; - this.runMode = false; - animationFrameProvider.delegate = undefined; - dateTimestampProvider.delegate = undefined; - immediateProvider.delegate = undefined; - intervalProvider.delegate = undefined; - timeoutProvider.delegate = undefined; - performanceTimestampProvider.delegate = undefined; + delegates.dispose(); } } }