mirror of
https://github.com/angular/angular
synced 2026-05-24 09:28:37 +00:00
Move the domino bundling logic and related shims into a centralized third_party directory within packages/platform-server. This avoids duplication of the bundling logic and ensures consistent shimming across the platform-server package and its entry points.
Following a conversation with OSS licensing, this change also includes the domino LICENSE file in the generated npm package to comply with licensing requirements for bundled third-party code.
```
├── fesm2022
│ ├── init.mjs
│ ├── init.mjs.map
│ ├── platform-server.mjs
│ ├── platform-server.mjs.map
│ ├── _server-chunk.mjs
│ ├── _server-chunk.mjs.map
│ ├── testing.mjs
│ └── testing.mjs.map
├── LICENSE
├── package.json
├── README.md
├── third_party
│ └── domino
│ ├── bundled-domino.d.ts
│ ├── bundled-domino.mjs
│ ├── bundled-domino.mjs.map
│ └── LICENSE
└── types
├── init.d.ts
├── platform-server.d.ts
└── testing.d.ts
```
300 lines
8.5 KiB
TypeScript
300 lines
8.5 KiB
TypeScript
/**
|
|
* @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));
|
|
}
|
|
}
|