/
githubmirror
/
angular
Обзор
Документация
Войти
/
githubmirror
/
angular
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
packages/private/testing/src/utils.ts
300 строк
8 KB
Alan Agius
build: consolidate domino bundling in platform-server
25 мар 2026, 23:31
25 мар 2026, 23:31
b40d11e
Код
Авторство
О чём код?
/** * @license * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.dev/license */ import {TestBed} from '../../../core/testing'; import {ɵresetJitOptions as resetJitOptions} from '@angular/core'; /** * Wraps a function in a new function which sets up document and HTML for running a test. * * This function wraps an existing testing function. The wrapper adds HTML to the `body` element of * the `document` and subsequently tears it down. * * This function can be used with `async await` and `Promise`s. If the wrapped function returns a * promise (or is `async`) then the teardown is delayed until that `Promise` is resolved. * * In the NodeJS environment this function detects if `document` is present and if not, it creates * one by loading `domino` and installing it. * * Example: * * ```ts * describe('something', () => { * it('should do something', withBody('<app-root></app-root>', async () => { * const fixture = TestBed.createComponent(MyApp); * fixture.detectChanges(); * expect(fixture.nativeElement.textContent).toEqual('Hello World!'); * })); * }); * ``` * * @param html HTML which should be inserted into the `body` of the `document`. * @param blockFn function to wrap. The function can return promise or be `async`. */ export function withBody( html: string, blockFn: () => Promise<unknown> | void, ): jasmine.ImplementationCallback { return wrapTestFn(() => document.body, html, blockFn); } /** * Wraps a function in a new function which sets up document and HTML for running a test. * * This function wraps an existing testing function. The wrapper adds HTML to the `head` element of * the `document` and subsequently tears it down. * * This function can be used with `async await` and `Promise`s. If the wrapped function returns a * promise (or is `async`) then the teardown is delayed until that `Promise` is resolved. * * In the NodeJS environment this function detects if `document` is present and if not, it creates * one by loading `domino` and installing it. * * Example: * * ```ts * describe('something', () => { * it('should do something', withHead('<link rel="preconnect" href="...">', async () => { * // ... * })); * }); * ``` * * @param html HTML which should be inserted into the `head` of the `document`. * @param blockFn function to wrap. The function can return promise or be `async`. */ export function withHead( html: string, blockFn: () => Promise<unknown> | void, ): jasmine.ImplementationCallback { return wrapTestFn(() => document.head, html, blockFn); } /** * Wraps provided function (which typically contains the code of a test) into a new function that * performs the necessary setup of the environment. */ function wrapTestFn( elementGetter: () => HTMLElement, html: string, blockFn: () => Promise<unknown> | void, ): jasmine.ImplementationCallback { return () => { elementGetter().innerHTML = html; return blockFn(); }; } let savedDocument: Document | undefined = undefined; let savedRequestAnimationFrame: ((callback: FrameRequestCallback) => number) | undefined = undefined; let savedNode: typeof Node | undefined = undefined; let requestAnimationFrameCount = 0; let domino: | (typeof import('../../../platform-server/third_party/domino/bundled-domino'))['default'] | null | undefined = undefined; async function loadDominoOrNull(): Promise< (typeof import('../../../platform-server/third_party/domino/bundled-domino'))['default'] | null > { if (domino !== undefined) { return domino; } try { return (domino = (await import('../../../platform-server/third_party/domino/bundled-domino')) .default); } catch { return (domino = null); } } /** * Ensure that global has `Document` if we are in node.js */ export async function ensureDocument(): Promise<void> { if ((global as any).isBrowser) { return; } const domino = await loadDominoOrNull(); if (domino === null) { return; } // we are in node.js. const window = domino.createWindow('', 'http://localhost'); savedDocument = (global as any).document; (global as any).window = window; (global as any).document = window.document; savedNode = (global as any).Node; // Domino types do not type `impl`, but it's a documented field. // See: https://www.npmjs.com/package/domino#usage. (global as any).Event = (domino as typeof domino & {impl: any}).impl.Event; (global as any).Node = (domino as typeof domino & {impl: any}).impl.Node; savedRequestAnimationFrame = (global as any).requestAnimationFrame; (global as any).requestAnimationFrame = function (cb: () => void): number { setTimeout(cb, 0); return requestAnimationFrameCount++; }; } /** * Restore the state of `Document` between tests. * @publicApi */ export function cleanupDocument(): void { if (savedDocument) { (global as any).document = savedDocument; (global as any).window = undefined; savedDocument = undefined; } if (savedNode) { (global as any).Node = savedNode; savedNode = undefined; } if (savedRequestAnimationFrame) { (global as any).requestAnimationFrame = savedRequestAnimationFrame; savedRequestAnimationFrame = undefined; } } if (typeof beforeEach == 'function') beforeEach(ensureDocument); if (typeof afterEach == 'function') afterEach(cleanupDocument); if (typeof afterEach === 'function') afterEach(resetJitOptions); /** * Returns a promise that resolves after the specified time. * * @param ms - Time to wait in milliseconds. Defaults to 0. * * @example * ```ts * await timeout(100); // Wait 100ms * ``` */ export async function timeout(ms?: number): Promise<void> { return new Promise((resolve) => { setTimeout(resolve, ms); }); } /** * Installs Jasmine's fake clock with auto-tick enabled for all tests in the describe block. * Call at the top level of a describe block to automatically advance time for async operations. * * @example * ```ts * describe('MyComponent', () => { * useAutoTick(); * * it('should handle timers', () => { * // setTimeout, setInterval, etc. will execute synchronously * }); * }); * ``` */ export function useAutoTick() { beforeEach(() => { jasmine.clock().install(); jasmine.clock().autoTick(); }); afterEach(() => { jasmine.clock().uninstall(); }); } export interface WaitForOptions { timeout?: number; interval?: number; } export interface ExpectTextOptions extends WaitForOptions { container?: HTMLElement; } /** * Returns a promise that resolves when the provided element's text content matches the expected text. * * @param element - The element or fixture to check. * @param text - The expected text content. * * @example * ```ts * await (fixture, 'Hello'); * ``` */ /** * Returns a promise that resolves when the text content is found on the screen. * * @param text - The expected text content, regex, or matcher function. * * @example * ```ts * await expectText('Hello'); * await expectText(/Hello/); * ``` */ export async function expectText( text: string | RegExp, options: ExpectTextOptions = {}, ): Promise<void> { const container = options.container || TestBed.getLastFixture().nativeElement; await waitFor(() => { const content = container.textContent || ''; if (typeof text === 'string') { throwUnless(content).toContain(text); } else { throwUnless(text.test(content)).toBeTrue(); } }, options); } // Intentionally does not participate in fake clocks. const realNow = performance.now.bind(performance); const realSetTimeout = setTimeout; export async function waitFor<T>( callback: () => Promise<T> | T, options: WaitForOptions = {}, ): Promise<T> { const waitTime = options.timeout ?? 100; const interval = options.interval ?? 0; const stack = new Error().stack; const deadline = realNow() + waitTime; let i = 0; let lastError: any | undefined; while (true) { try { return await callback(); } catch (cause) { lastError = cause; } i++; if (deadline < realNow()) { throw Object.assign( new Error( `Timed out after ${waitTime}ms and ${i} attempts. ` + `Last error: ${lastError?.message ?? 'condition returned false'}`, ), { stack: stack + `Last error: ${lastError?.stack ?? 'condition returned false'}`, }, ); } // Guarantee a macro-task between retries. await new Promise((resolve) => void realSetTimeout(resolve, interval)); } }