/** * `fakeAsync` on Vitest — the patch `zone.js/testing` does not ship. * * `zone.js/testing` patches three runners: jasmine, mocha and jest. Vitest is not among them, so in * an Angular project on Vitest **every** `fakeAsync` fails with * `Expected to be running in 'ProxyZone', but it was not found` — a message about a zone, in a test * that never mentions one. Exactly one package does something about it today * (`@analogjs/vitest-angular`, as a side effect of importing `…/setup-zone`), which means a project * moving to the native `@angular/build:unit-test` builder loses the patch along with Analog. * * What `fakeAsync` needs is narrow: the callback it wraps must be running inside a zone that has a * `ProxyZoneSpec`, because that is the spec it swaps its own `FakeAsyncTestZoneSpec` into. So the * patch is "run every test and hook body inside a forked proxy zone", and the whole difficulty is * doing that without disturbing the runner: * * - **The wrapper must declare no parameters.** Vitest reads `fn.toString()` to discover fixtures, * and a `function (...args)` makes it fail with `FixtureParseError: The 1st argument inside a * fixture must use object destructuring pattern` — in every file, about code the author did not * write. Here the wrapper takes nothing and forwards `arguments`, and reports the *original* * source from `toString`, so fixtures keep working exactly as they did. * - **`fn.length` has to survive.** The runner reads it to decide how to call the callback; a * wrapper of arity 0 silently changes that decision. * - **`it.each(table)(…)` must keep its receiver.** `each` is a method that reads `this`; called * detached it returns `undefined` and the next line fails. A Proxy is what preserves it, and it * also means `it.skip`, `it.only`, `test.each` and the rest are covered without naming them. * * Nothing here imports zone.js. The patch reads `globalThis.Zone`, which the consumer has already * loaded (the Angular builder loads `zone.js/testing` from its own entry point, before any setup * file runs) — and says so when it has not. */ /** * How many proxy zones a spec gets. * * - `'shared'` — one, for every callback of the run. This is what Angular's own jasmine patch does, * and what the ecosystem is written against: a component built in `beforeEach` schedules from its * constructor, and `tick()` inside a `fakeAsync` test has to see those timers. * - `'callback'` — a fresh fork per callback. Correct in the abstract and required by * `test.concurrent`, where two callbacks are in flight at once and would otherwise swap the same * `ProxyZoneSpec` delegate under one another. */ type ProxyZoneScope = 'callback' | 'shared'; /** Options for {@link installProxyZonePatch}. */ interface ProxyZonePatchOptions { /** @default 'shared' */ scope?: ProxyZoneScope; } /** * Make `fakeAsync` and `waitForAsync` work in this run. * * ```ts * // vitest.setup.ts — after zone.js is loaded, before the suites run * import 'vitest-auto-spy/zone'; * ``` * * Every test and hook body then runs inside a proxy zone — **one and the same zone**, which is what * Angular's own jasmine patch does and what the ecosystem is written against: * * ```ts * beforeEach(() => { * fixture = TestBed.createComponent(GamificationComponent); // the constructor schedules * }); * * it('loads', fakeAsync(() => { * fixture.detectChanges(); * tick(200); // must see what `beforeEach` scheduled * expect(component.levels()).toEqual(levels); * })); * ``` * * With a fresh fork per callback that `tick` drives the clock of *its* zone, the timer scheduled in * `beforeEach` belongs to another, and the assertion fails with `expected [] to deeply equal […]` — * a message with no zone in it, in a spec that reads like every Angular spec ever written. Measured * on a suite of 1688 files: per-callback forking failed 7 tests in 2 files that pass under jasmine. * * `scope: 'callback'` restores the per-callback fork. It is the right choice for `test.concurrent`, * where two callbacks are in flight at once and would otherwise swap the same `ProxyZoneSpec` * delegate under one another. * * @returns The undo, which puts the untouched globals back. Mostly useful to this library's own * tests; a run that installs the patch keeps it for the whole worker. */ declare function installProxyZonePatch({ scope }?: ProxyZonePatchOptions): () => void; export { type ProxyZonePatchOptions, type ProxyZoneScope, installProxyZonePatch };