@microsoft/fast-test-harness 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +267 -0
- package/dist/dts/build/dom-shim.d.ts +10 -0
- package/dist/dts/build/dom-shim.d.ts.map +1 -0
- package/dist/dts/build/dom-shim.test.d.ts +2 -0
- package/dist/dts/build/dom-shim.test.d.ts.map +1 -0
- package/dist/dts/build/generate-stylesheets.d.ts +62 -0
- package/dist/dts/build/generate-stylesheets.d.ts.map +1 -0
- package/dist/dts/build/generate-stylesheets.test.d.ts +2 -0
- package/dist/dts/build/generate-stylesheets.test.d.ts.map +1 -0
- package/dist/dts/build/generate-templates.d.ts +69 -0
- package/dist/dts/build/generate-templates.d.ts.map +1 -0
- package/dist/dts/build/generate-templates.test.d.ts +2 -0
- package/dist/dts/build/generate-templates.test.d.ts.map +1 -0
- package/dist/dts/build/generate-webui-templates.d.ts +54 -0
- package/dist/dts/build/generate-webui-templates.d.ts.map +1 -0
- package/dist/dts/build/generate-webui-templates.test.d.ts +2 -0
- package/dist/dts/build/generate-webui-templates.test.d.ts.map +1 -0
- package/dist/dts/fixtures/assertions.d.ts +19 -0
- package/dist/dts/fixtures/assertions.d.ts.map +1 -0
- package/dist/dts/fixtures/csr-fixture.d.ts +114 -0
- package/dist/dts/fixtures/csr-fixture.d.ts.map +1 -0
- package/dist/dts/fixtures/csr-fixture.pw.spec.d.ts +2 -0
- package/dist/dts/fixtures/csr-fixture.pw.spec.d.ts.map +1 -0
- package/dist/dts/fixtures/index.d.ts +30 -0
- package/dist/dts/fixtures/index.d.ts.map +1 -0
- package/dist/dts/fixtures/ssr-fixture.d.ts +42 -0
- package/dist/dts/fixtures/ssr-fixture.d.ts.map +1 -0
- package/dist/dts/fixtures/ssr-fixture.pw.spec.d.ts +2 -0
- package/dist/dts/fixtures/ssr-fixture.pw.spec.d.ts.map +1 -0
- package/dist/dts/index.d.ts +7 -0
- package/dist/dts/index.d.ts.map +1 -0
- package/dist/dts/ssr/entry-client.d.ts +2 -0
- package/dist/dts/ssr/entry-client.d.ts.map +1 -0
- package/dist/dts/ssr/render.d.ts +127 -0
- package/dist/dts/ssr/render.d.ts.map +1 -0
- package/dist/dts/ssr/render.test.d.ts +2 -0
- package/dist/dts/ssr/render.test.d.ts.map +1 -0
- package/dist/esm/build/dom-shim.js +142 -0
- package/dist/esm/build/dom-shim.test.js +202 -0
- package/dist/esm/build/generate-stylesheets.js +70 -0
- package/dist/esm/build/generate-stylesheets.test.js +74 -0
- package/dist/esm/build/generate-templates.js +243 -0
- package/dist/esm/build/generate-templates.test.js +231 -0
- package/dist/esm/build/generate-webui-templates.js +121 -0
- package/dist/esm/build/generate-webui-templates.test.js +179 -0
- package/dist/esm/fixtures/assertions.js +49 -0
- package/dist/esm/fixtures/csr-fixture.js +153 -0
- package/dist/esm/fixtures/csr-fixture.pw.spec.js +137 -0
- package/dist/esm/fixtures/index.js +48 -0
- package/dist/esm/fixtures/ssr-fixture.js +113 -0
- package/dist/esm/fixtures/ssr-fixture.pw.spec.js +189 -0
- package/dist/esm/index.js +6 -0
- package/dist/esm/ssr/entry-client.js +2 -0
- package/dist/esm/ssr/render.js +381 -0
- package/dist/esm/ssr/render.test.js +236 -0
- package/package.json +88 -0
- package/playwright.config.d.ts +4 -0
- package/playwright.config.mjs +38 -0
- package/public/styles.css +15 -0
- package/server.mjs +317 -0
- package/start.mjs +244 -0
- package/vite.config.d.ts +4 -0
- package/vite.config.mjs +35 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { toHaveCustomState } from "./assertions.js";
|
|
2
|
+
import { CSRFixture } from "./csr-fixture.js";
|
|
3
|
+
import { SSRFixture } from "./ssr-fixture.js";
|
|
4
|
+
type FixtureOptions = {
|
|
5
|
+
/**
|
|
6
|
+
* Additional HTML to insert into the element.
|
|
7
|
+
*/
|
|
8
|
+
innerHTML: string;
|
|
9
|
+
/**
|
|
10
|
+
* Indicates if the test is running in SSR mode.
|
|
11
|
+
*/
|
|
12
|
+
ssr: boolean;
|
|
13
|
+
/**
|
|
14
|
+
* The tag name of the custom element to test.
|
|
15
|
+
*/
|
|
16
|
+
tagName: string;
|
|
17
|
+
/**
|
|
18
|
+
* Additional custom elements to wait for before running the test.
|
|
19
|
+
*/
|
|
20
|
+
waitFor: string[];
|
|
21
|
+
};
|
|
22
|
+
export type Fixtures = {
|
|
23
|
+
fastPage: CSRFixture | SSRFixture;
|
|
24
|
+
};
|
|
25
|
+
export declare const test: import("@playwright/test").TestType<import("@playwright/test").PlaywrightTestArgs & import("@playwright/test").PlaywrightTestOptions & Fixtures & FixtureOptions, import("@playwright/test").PlaywrightWorkerArgs & import("@playwright/test").PlaywrightWorkerOptions>;
|
|
26
|
+
export declare const expect: import("@playwright/test").Expect<{
|
|
27
|
+
toHaveCustomState: typeof toHaveCustomState;
|
|
28
|
+
}>;
|
|
29
|
+
export {};
|
|
30
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/fixtures/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AACpD,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAI9C,KAAK,cAAc,GAAG;IAClB;;OAEG;IACH,SAAS,EAAE,MAAM,CAAC;IAElB;;OAEG;IACH,GAAG,EAAE,OAAO,CAAC;IAEb;;OAEG;IACH,OAAO,EAAE,MAAM,CAAC;IAEhB;;OAEG;IACH,OAAO,EAAE,MAAM,EAAE,CAAC;CACrB,CAAC;AAEF,MAAM,MAAM,QAAQ,GAAG;IACnB,QAAQ,EAAE,UAAU,GAAG,UAAU,CAAC;CACrC,CAAC;AAEF,eAAO,MAAM,IAAI,yQAmDf,CAAC;AAEH,eAAO,MAAM,MAAM;;EAEjB,CAAC"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { Page } from "@playwright/test";
|
|
2
|
+
import { CSRFixture, type TemplateOrOptions } from "./csr-fixture.js";
|
|
3
|
+
export declare class SSRFixture extends CSRFixture {
|
|
4
|
+
private readonly testId?;
|
|
5
|
+
private readonly testTitle?;
|
|
6
|
+
/**
|
|
7
|
+
* Styles buffered before {@link setTemplate} is called.
|
|
8
|
+
*/
|
|
9
|
+
private pendingStyles;
|
|
10
|
+
/**
|
|
11
|
+
* Whether the template has been rendered.
|
|
12
|
+
*/
|
|
13
|
+
private templateRendered;
|
|
14
|
+
constructor(page: Page, tagName: string, innerHTML?: string, waitFor?: string[], testId?: string, testTitle?: string);
|
|
15
|
+
/**
|
|
16
|
+
* Buffers style tags added before {@link setTemplate} so they can be
|
|
17
|
+
* included in the generated SSR page. After the template has been
|
|
18
|
+
* rendered, calls pass through to the page directly.
|
|
19
|
+
*
|
|
20
|
+
* @param options - The options for the style tag.
|
|
21
|
+
* @see {@link Page.addStyleTag}
|
|
22
|
+
*/
|
|
23
|
+
addStyleTag(options: Parameters<Page["addStyleTag"]>[0]): Promise<void>;
|
|
24
|
+
/**
|
|
25
|
+
* Sets up the test fixture by posting the template or configuration
|
|
26
|
+
* to the SSR generation endpoint.
|
|
27
|
+
*
|
|
28
|
+
* This method constructs a request body based on the provided
|
|
29
|
+
* `templateOrOptions` and the current test context (like `testId`
|
|
30
|
+
* and `testTitle`). It sends this data to the `/generate-fixture`
|
|
31
|
+
* endpoint to create a server-side rendered fixture, navigates the
|
|
32
|
+
* page to the resulting URL, and waits for the page to stabilize.
|
|
33
|
+
*
|
|
34
|
+
* @param templateOrOptions - The template configuration.
|
|
35
|
+
*/
|
|
36
|
+
setTemplate(templateOrOptions?: TemplateOrOptions): Promise<void>;
|
|
37
|
+
/**
|
|
38
|
+
* Formats the test title based on the provided test ID.
|
|
39
|
+
*/
|
|
40
|
+
private formatTestTitle;
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=ssr-fixture.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ssr-fixture.d.ts","sourceRoot":"","sources":["../../../src/fixtures/ssr-fixture.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAC7C,OAAO,EAAE,UAAU,EAAE,KAAK,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAEtE,qBAAa,UAAW,SAAQ,UAAU;IAgBlC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC;IACxB,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAC;IAhB/B;;OAEG;IACH,OAAO,CAAC,aAAa,CAA4C;IAEjE;;OAEG;IACH,OAAO,CAAC,gBAAgB,CAAS;IAEjC,YACI,IAAI,EAAE,IAAI,EACV,OAAO,EAAE,MAAM,EACf,SAAS,GAAE,MAAW,EACtB,OAAO,GAAE,MAAM,EAAO,EACL,MAAM,CAAC,EAAE,MAAM,EACf,SAAS,CAAC,EAAE,MAAM,EAGtC;IAED;;;;;;;OAOG;IACY,WAAW,CACtB,OAAO,EAAE,UAAU,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC,GAC5C,OAAO,CAAC,IAAI,CAAC,CAMf;IAED;;;;;;;;;;;OAWG;IACY,WAAW,CAAC,iBAAiB,CAAC,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CAoE/E;IAED;;OAEG;IACH,OAAO,CAAC,eAAe;CAqB1B"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ssr-fixture.pw.spec.d.ts","sourceRoot":"","sources":["../../../src/fixtures/ssr-fixture.pw.spec.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { installDomShim } from "./build/dom-shim.js";
|
|
2
|
+
export { toHaveCustomState } from "./fixtures/assertions.js";
|
|
3
|
+
export { CSRFixture, type FixtureOptions, type InitialTemplateAttributes, type InitialTemplateOptions, type TemplateAttributes, type TemplateOrOptions, type ThemeTokens, } from "./fixtures/csr-fixture.js";
|
|
4
|
+
export { expect, type Fixtures, test } from "./fixtures/index.js";
|
|
5
|
+
export { SSRFixture } from "./fixtures/ssr-fixture.js";
|
|
6
|
+
export { type ComponentRegistration, createSSRRenderer, type RenderResult, type SSRRendererOptions, } from "./ssr/render.js";
|
|
7
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AACrD,OAAO,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAC7D,OAAO,EACH,UAAU,EACV,KAAK,cAAc,EACnB,KAAK,yBAAyB,EAC9B,KAAK,sBAAsB,EAC3B,KAAK,kBAAkB,EACvB,KAAK,iBAAiB,EACtB,KAAK,WAAW,GACnB,MAAM,2BAA2B,CAAC;AACnC,OAAO,EAAE,MAAM,EAAE,KAAK,QAAQ,EAAE,IAAI,EAAE,MAAM,qBAAqB,CAAC;AAClE,OAAO,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAC;AACvD,OAAO,EACH,KAAK,qBAAqB,EAC1B,iBAAiB,EACjB,KAAK,YAAY,EACjB,KAAK,kBAAkB,GAC1B,MAAM,iBAAiB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"entry-client.d.ts","sourceRoot":"","sources":["../../../src/ssr/entry-client.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SSR rendering powered by `@microsoft/fast-build`'s WASM renderer.
|
|
3
|
+
*
|
|
4
|
+
* Provides a high-level `createSSRRenderer()` factory that scans for
|
|
5
|
+
* component build artifacts (f-templates, stylesheets) and returns a
|
|
6
|
+
* `render()` function compatible with the test harness server's
|
|
7
|
+
* `entry-server.ts` contract.
|
|
8
|
+
*
|
|
9
|
+
* Supports two layout modes:
|
|
10
|
+
*
|
|
11
|
+
* **Multi-component package**: one package contains all
|
|
12
|
+
* components in subdirectories.
|
|
13
|
+
* ```ts
|
|
14
|
+
* const { render } = createSSRRenderer({
|
|
15
|
+
* packageName: "@fluentui/web-components",
|
|
16
|
+
* tagPrefix: "fluent",
|
|
17
|
+
* });
|
|
18
|
+
* ```
|
|
19
|
+
*
|
|
20
|
+
* **Per-component packages**: each component is a separate npm package with
|
|
21
|
+
* flat exports.
|
|
22
|
+
* ```ts
|
|
23
|
+
* const { render } = createSSRRenderer({
|
|
24
|
+
* tagPrefix: "mai",
|
|
25
|
+
* components: [
|
|
26
|
+
* { name: "button", packageName: "@mai-ui/button" },
|
|
27
|
+
* { name: "checkbox", packageName: "@mai-ui/checkbox" },
|
|
28
|
+
* ],
|
|
29
|
+
* });
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
export interface ComponentRegistration {
|
|
33
|
+
/**
|
|
34
|
+
* Component name (e.g., "button"). Combined with `tagPrefix` to
|
|
35
|
+
* form the tag name.
|
|
36
|
+
*/
|
|
37
|
+
name: string;
|
|
38
|
+
/**
|
|
39
|
+
* The npm package name for this component
|
|
40
|
+
* (e.g., "@mai-ui/button").
|
|
41
|
+
*/
|
|
42
|
+
packageName: string;
|
|
43
|
+
}
|
|
44
|
+
export interface SSRRendererOptions {
|
|
45
|
+
/**
|
|
46
|
+
* Tag name prefix for custom elements (e.g., "fluent", "mai").
|
|
47
|
+
*/
|
|
48
|
+
tagPrefix: string;
|
|
49
|
+
/**
|
|
50
|
+
* The npm package name used to resolve component build artifacts
|
|
51
|
+
* when all components live in subdirectories of a single package
|
|
52
|
+
* (Fluent-style). Mutually exclusive with `components`.
|
|
53
|
+
*/
|
|
54
|
+
packageName?: string;
|
|
55
|
+
/**
|
|
56
|
+
* Explicit list of per-component packages (MAI-style). Each entry
|
|
57
|
+
* maps a component name to its npm package. Mutually exclusive
|
|
58
|
+
* with `packageName`.
|
|
59
|
+
*/
|
|
60
|
+
components?: ComponentRegistration[];
|
|
61
|
+
/**
|
|
62
|
+
* Directory containing compiled build artifacts, relative to the
|
|
63
|
+
* package root. Only used with `packageName`.
|
|
64
|
+
* @default "dist/esm"
|
|
65
|
+
*/
|
|
66
|
+
distDir?: string;
|
|
67
|
+
/**
|
|
68
|
+
* Stylesheet URL or server-relative path for a global theme
|
|
69
|
+
* stylesheet to include in every SSR fixture page.
|
|
70
|
+
*
|
|
71
|
+
* The value is used directly as the `<link>` tag's `href`.
|
|
72
|
+
* Callers should provide a fully qualified URL or a
|
|
73
|
+
* server-relative path.
|
|
74
|
+
*/
|
|
75
|
+
themeStylesheet?: string;
|
|
76
|
+
}
|
|
77
|
+
export interface RenderResult {
|
|
78
|
+
/** All f-template definitions concatenated, with styles injected. */
|
|
79
|
+
template: string;
|
|
80
|
+
/** The rendered fixture HTML with DSD injected. */
|
|
81
|
+
fixture: string;
|
|
82
|
+
/** Preload link tags (empty string when using fast-build). */
|
|
83
|
+
preloadLinks: string;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Parse a JavaScript default value string from CEM into a JSON-safe value.
|
|
87
|
+
* @internal
|
|
88
|
+
*/
|
|
89
|
+
export declare function parseDefaultValue(raw: string): unknown;
|
|
90
|
+
/**
|
|
91
|
+
* Replace the `{{styles}}` placeholder in an f-template with a
|
|
92
|
+
* stylesheet `<link>` tag. Falls back to injecting after the opening
|
|
93
|
+
* `<template>` tag if the placeholder is absent.
|
|
94
|
+
* @internal
|
|
95
|
+
*/
|
|
96
|
+
export declare function renderTemplate(rawTemplate: string, styles: string): string;
|
|
97
|
+
/**
|
|
98
|
+
* Build the entry HTML that the WASM renderer processes.
|
|
99
|
+
* Constructs either a raw HTML fixture or a single custom element
|
|
100
|
+
* from the query parameters.
|
|
101
|
+
* @internal
|
|
102
|
+
*/
|
|
103
|
+
export declare function buildEntryHtml(queryObj: Record<string, string>): string;
|
|
104
|
+
/**
|
|
105
|
+
* Build the state JSON for the WASM renderer from the query
|
|
106
|
+
* parameters. Includes attribute values and normalised (hyphen-
|
|
107
|
+
* stripped) variants so bindings like `{{arialabel}}` resolve for
|
|
108
|
+
* an `aria-label` HTML attribute.
|
|
109
|
+
* @internal
|
|
110
|
+
*/
|
|
111
|
+
export declare function buildState(queryObj: Record<string, string>): Record<string, unknown>;
|
|
112
|
+
/**
|
|
113
|
+
* Create an SSR renderer that uses `@microsoft/fast-build`'s WASM
|
|
114
|
+
* module to render f-templates into declarative shadow DOM on each
|
|
115
|
+
* request, with full expression evaluation and nested component
|
|
116
|
+
* support.
|
|
117
|
+
*
|
|
118
|
+
* Supports two modes:
|
|
119
|
+
* - **`packageName`**: Scans a monolithic package's dist directory for
|
|
120
|
+
* components in subdirectories (Fluent-style).
|
|
121
|
+
* - **`components`**: Uses an explicit list of per-component packages
|
|
122
|
+
* with flat exports (MAI-style).
|
|
123
|
+
*/
|
|
124
|
+
export declare function createSSRRenderer(options: SSRRendererOptions): {
|
|
125
|
+
render: (queryObj: Record<string, string>) => RenderResult;
|
|
126
|
+
};
|
|
127
|
+
//# sourceMappingURL=render.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"render.d.ts","sourceRoot":"","sources":["../../../src/ssr/render.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAOH,MAAM,WAAW,qBAAqB;IAClC;;;OAGG;IACH,IAAI,EAAE,MAAM,CAAC;IAEb;;;OAGG;IACH,WAAW,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,kBAAkB;IAC/B;;OAEG;IACH,SAAS,EAAE,MAAM,CAAC;IAElB;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB;;;;OAIG;IACH,UAAU,CAAC,EAAE,qBAAqB,EAAE,CAAC;IAErC;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IAEjB;;;;;;;OAOG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED,MAAM,WAAW,YAAY;IACzB,qEAAqE;IACrE,QAAQ,EAAE,MAAM,CAAC;IACjB,mDAAmD;IACnD,OAAO,EAAE,MAAM,CAAC;IAChB,8DAA8D;IAC9D,YAAY,EAAE,MAAM,CAAC;CACxB;AA4CD;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CA6BtD;AA0FD;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,WAAW,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAa1E;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,CAwCvE;AAED;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAsBpF;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,kBAAkB,GAAG;IAC5D,MAAM,EAAE,CAAC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KAAK,YAAY,CAAC;CAC9D,CA4IA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"render.test.d.ts","sourceRoot":"","sources":["../../../src/ssr/render.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal DOM shim for running FAST Element's `css` and `html` tagged
|
|
3
|
+
* templates in Node.js. Provides just enough of the DOM API to resolve
|
|
4
|
+
* `ElementStyles.toString()` and compile `html` templates.
|
|
5
|
+
*
|
|
6
|
+
* This module is idempotent — if `globalThis.window` is already defined,
|
|
7
|
+
* no shims are applied.
|
|
8
|
+
*/
|
|
9
|
+
class ShimNode extends EventTarget {
|
|
10
|
+
}
|
|
11
|
+
class ShimElement extends ShimNode {
|
|
12
|
+
}
|
|
13
|
+
class ShimHTMLElement extends ShimElement {
|
|
14
|
+
constructor() {
|
|
15
|
+
super(...arguments);
|
|
16
|
+
this._shadowRoot = null;
|
|
17
|
+
}
|
|
18
|
+
static { this.elementAttributes = new WeakMap(); }
|
|
19
|
+
get attributes() {
|
|
20
|
+
return Array.from(ShimHTMLElement.elementAttributes.get(this) ?? []).map(([name, value]) => ({ name, value }));
|
|
21
|
+
}
|
|
22
|
+
get shadowRoot() {
|
|
23
|
+
return this._shadowRoot;
|
|
24
|
+
}
|
|
25
|
+
setAttribute(name, value) {
|
|
26
|
+
let attrs = ShimHTMLElement.elementAttributes.get(this);
|
|
27
|
+
if (!attrs) {
|
|
28
|
+
attrs = new Map();
|
|
29
|
+
ShimHTMLElement.elementAttributes.set(this, attrs);
|
|
30
|
+
}
|
|
31
|
+
attrs.set(name, value);
|
|
32
|
+
}
|
|
33
|
+
removeAttribute(name) {
|
|
34
|
+
ShimHTMLElement.elementAttributes.get(this)?.delete(name);
|
|
35
|
+
}
|
|
36
|
+
hasAttribute(name) {
|
|
37
|
+
return ShimHTMLElement.elementAttributes.get(this)?.has(name) ?? false;
|
|
38
|
+
}
|
|
39
|
+
getAttribute(name) {
|
|
40
|
+
const v = ShimHTMLElement.elementAttributes.get(this)?.get(name);
|
|
41
|
+
return v === undefined ? null : v;
|
|
42
|
+
}
|
|
43
|
+
attachShadow(init) {
|
|
44
|
+
const sr = { host: this };
|
|
45
|
+
if (init?.mode === "open") {
|
|
46
|
+
this._shadowRoot = sr;
|
|
47
|
+
}
|
|
48
|
+
return sr;
|
|
49
|
+
}
|
|
50
|
+
get classList() {
|
|
51
|
+
return {
|
|
52
|
+
add() { },
|
|
53
|
+
remove() { },
|
|
54
|
+
contains() {
|
|
55
|
+
return false;
|
|
56
|
+
},
|
|
57
|
+
toggle() { },
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
get part() {
|
|
61
|
+
return this.classList;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
class ShimCSSStyleSheet {
|
|
65
|
+
constructor() {
|
|
66
|
+
this.cssRules = [];
|
|
67
|
+
}
|
|
68
|
+
replace() { }
|
|
69
|
+
insertRule(rule, index = 0) {
|
|
70
|
+
this.cssRules.splice(index, 0, { selectorText: rule.split("{")[0] });
|
|
71
|
+
return index;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
class ShimCustomElementRegistry {
|
|
75
|
+
constructor() {
|
|
76
|
+
this.__definitions = new Map();
|
|
77
|
+
}
|
|
78
|
+
define(name, ctor) {
|
|
79
|
+
this.__definitions.set(name, {
|
|
80
|
+
ctor,
|
|
81
|
+
observedAttributes: ctor.observedAttributes ?? [],
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
get(name) {
|
|
85
|
+
return this.__definitions.get(name)?.ctor;
|
|
86
|
+
}
|
|
87
|
+
whenDefined() {
|
|
88
|
+
return Promise.resolve();
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
class ShimDocument extends ShimNode {
|
|
92
|
+
constructor() {
|
|
93
|
+
super(...arguments);
|
|
94
|
+
this.adoptedStyleSheets = [];
|
|
95
|
+
}
|
|
96
|
+
createTreeWalker() {
|
|
97
|
+
return {};
|
|
98
|
+
}
|
|
99
|
+
createTextNode() {
|
|
100
|
+
return {};
|
|
101
|
+
}
|
|
102
|
+
createElement() {
|
|
103
|
+
return new ShimHTMLElement();
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
class ShimMutationObserver {
|
|
107
|
+
observe() { }
|
|
108
|
+
disconnect() { }
|
|
109
|
+
}
|
|
110
|
+
class ShimMediaQueryList {
|
|
111
|
+
constructor() {
|
|
112
|
+
this.matches = false;
|
|
113
|
+
}
|
|
114
|
+
addEventListener() { }
|
|
115
|
+
removeEventListener() { }
|
|
116
|
+
}
|
|
117
|
+
export function installDomShim() {
|
|
118
|
+
if (globalThis.window !== undefined) {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
globalThis.Node = ShimNode;
|
|
122
|
+
globalThis.Element = ShimElement;
|
|
123
|
+
globalThis.HTMLElement = ShimHTMLElement;
|
|
124
|
+
globalThis.Document = ShimDocument;
|
|
125
|
+
globalThis.CustomEvent = class extends Event {
|
|
126
|
+
constructor(type, init) {
|
|
127
|
+
super(type, init);
|
|
128
|
+
this.detail = init?.detail ?? null;
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
globalThis.CSSStyleSheet = ShimCSSStyleSheet;
|
|
132
|
+
globalThis.ShadowRoot = class {
|
|
133
|
+
};
|
|
134
|
+
globalThis.CustomElementRegistry = ShimCustomElementRegistry;
|
|
135
|
+
globalThis.MutationObserver = ShimMutationObserver;
|
|
136
|
+
globalThis.MediaQueryList = ShimMediaQueryList;
|
|
137
|
+
globalThis.matchMedia = () => new ShimMediaQueryList();
|
|
138
|
+
globalThis.document = new ShimDocument();
|
|
139
|
+
globalThis.customElements = new ShimCustomElementRegistry();
|
|
140
|
+
globalThis.window = globalThis;
|
|
141
|
+
globalThis.CSS ??= { supports: () => true };
|
|
142
|
+
}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { test } from "node:test";
|
|
3
|
+
// Each test needs a clean globalThis, so we tear down the shim between tests.
|
|
4
|
+
function teardownShim() {
|
|
5
|
+
for (const key of [
|
|
6
|
+
"Node",
|
|
7
|
+
"Element",
|
|
8
|
+
"HTMLElement",
|
|
9
|
+
"Document",
|
|
10
|
+
"CustomEvent",
|
|
11
|
+
"CSSStyleSheet",
|
|
12
|
+
"ShadowRoot",
|
|
13
|
+
"CustomElementRegistry",
|
|
14
|
+
"MutationObserver",
|
|
15
|
+
"MediaQueryList",
|
|
16
|
+
"matchMedia",
|
|
17
|
+
"document",
|
|
18
|
+
"customElements",
|
|
19
|
+
"window",
|
|
20
|
+
"CSS",
|
|
21
|
+
]) {
|
|
22
|
+
delete globalThis[key];
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
test.describe("installDomShim", () => {
|
|
26
|
+
test.beforeEach(() => {
|
|
27
|
+
teardownShim();
|
|
28
|
+
});
|
|
29
|
+
async function loadShim() {
|
|
30
|
+
// Dynamic import so each test gets a fresh evaluation context
|
|
31
|
+
// after globalThis is cleaned.
|
|
32
|
+
const { installDomShim } = await import("@microsoft/fast-test-harness/build/dom-shim.js");
|
|
33
|
+
installDomShim();
|
|
34
|
+
}
|
|
35
|
+
test("should assign globals on first call", async () => {
|
|
36
|
+
await loadShim();
|
|
37
|
+
assert.ok(globalThis.window !== undefined);
|
|
38
|
+
assert.ok(globalThis.document !== undefined);
|
|
39
|
+
assert.ok(globalThis.customElements !== undefined);
|
|
40
|
+
assert.ok(globalThis.Node !== undefined);
|
|
41
|
+
assert.ok(globalThis.Element !== undefined);
|
|
42
|
+
assert.ok(globalThis.HTMLElement !== undefined);
|
|
43
|
+
assert.ok(globalThis.CSSStyleSheet !== undefined);
|
|
44
|
+
assert.ok(globalThis.MutationObserver !== undefined);
|
|
45
|
+
assert.ok(globalThis.matchMedia !== undefined);
|
|
46
|
+
});
|
|
47
|
+
test("should be idempotent when window is already defined", async () => {
|
|
48
|
+
const sentinel = { __sentinel: true };
|
|
49
|
+
globalThis.window = sentinel;
|
|
50
|
+
await loadShim();
|
|
51
|
+
assert.strictEqual(globalThis.window, sentinel);
|
|
52
|
+
assert.strictEqual(globalThis.document, undefined);
|
|
53
|
+
});
|
|
54
|
+
test("should set window to globalThis", async () => {
|
|
55
|
+
await loadShim();
|
|
56
|
+
assert.strictEqual(globalThis.window, globalThis);
|
|
57
|
+
});
|
|
58
|
+
test("should provide CSS.supports that returns true", async () => {
|
|
59
|
+
await loadShim();
|
|
60
|
+
assert.strictEqual(globalThis.CSS.supports("display", "flex"), true);
|
|
61
|
+
});
|
|
62
|
+
test("should not overwrite an existing CSS global", async () => {
|
|
63
|
+
const existing = { supports: () => false };
|
|
64
|
+
globalThis.CSS = existing;
|
|
65
|
+
await loadShim();
|
|
66
|
+
assert.strictEqual(globalThis.CSS, existing);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
test.describe("ShimHTMLElement", () => {
|
|
70
|
+
test.beforeEach(async () => {
|
|
71
|
+
teardownShim();
|
|
72
|
+
const { installDomShim } = await import("@microsoft/fast-test-harness/build/dom-shim.js");
|
|
73
|
+
installDomShim();
|
|
74
|
+
});
|
|
75
|
+
test("should support setAttribute / getAttribute / hasAttribute", () => {
|
|
76
|
+
const el = new globalThis.HTMLElement();
|
|
77
|
+
assert.strictEqual(el.hasAttribute("id"), false);
|
|
78
|
+
assert.strictEqual(el.getAttribute("id"), null);
|
|
79
|
+
el.setAttribute("id", "test");
|
|
80
|
+
assert.strictEqual(el.hasAttribute("id"), true);
|
|
81
|
+
assert.strictEqual(el.getAttribute("id"), "test");
|
|
82
|
+
});
|
|
83
|
+
test("should support removeAttribute", () => {
|
|
84
|
+
const el = new globalThis.HTMLElement();
|
|
85
|
+
el.setAttribute("class", "foo");
|
|
86
|
+
assert.strictEqual(el.hasAttribute("class"), true);
|
|
87
|
+
el.removeAttribute("class");
|
|
88
|
+
assert.strictEqual(el.hasAttribute("class"), false);
|
|
89
|
+
assert.strictEqual(el.getAttribute("class"), null);
|
|
90
|
+
});
|
|
91
|
+
test("should return attributes as an array of {name, value}", () => {
|
|
92
|
+
const el = new globalThis.HTMLElement();
|
|
93
|
+
el.setAttribute("role", "button");
|
|
94
|
+
el.setAttribute("aria-label", "Close");
|
|
95
|
+
const attrs = el.attributes;
|
|
96
|
+
assert.strictEqual(attrs.length, 2);
|
|
97
|
+
assert.deepStrictEqual(attrs.map((a) => a.name).sort(), [
|
|
98
|
+
"aria-label",
|
|
99
|
+
"role",
|
|
100
|
+
]);
|
|
101
|
+
});
|
|
102
|
+
test("should support attachShadow with open mode", () => {
|
|
103
|
+
const el = new globalThis.HTMLElement();
|
|
104
|
+
const sr = el.attachShadow({ mode: "open" });
|
|
105
|
+
assert.ok(sr);
|
|
106
|
+
assert.strictEqual(sr.host, el);
|
|
107
|
+
assert.strictEqual(el.shadowRoot, sr);
|
|
108
|
+
});
|
|
109
|
+
test("should not expose shadowRoot for closed mode", () => {
|
|
110
|
+
const el = new globalThis.HTMLElement();
|
|
111
|
+
el.attachShadow({ mode: "closed" });
|
|
112
|
+
assert.strictEqual(el.shadowRoot, null);
|
|
113
|
+
});
|
|
114
|
+
test("should provide a classList stub", () => {
|
|
115
|
+
const el = new globalThis.HTMLElement();
|
|
116
|
+
const cl = el.classList;
|
|
117
|
+
assert.strictEqual(cl.contains("foo"), false);
|
|
118
|
+
// Should not throw
|
|
119
|
+
cl.add("foo");
|
|
120
|
+
cl.remove("foo");
|
|
121
|
+
cl.toggle("foo");
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
test.describe("ShimCSSStyleSheet", () => {
|
|
125
|
+
test.beforeEach(async () => {
|
|
126
|
+
teardownShim();
|
|
127
|
+
const { installDomShim } = await import("@microsoft/fast-test-harness/build/dom-shim.js");
|
|
128
|
+
installDomShim();
|
|
129
|
+
});
|
|
130
|
+
test("should support insertRule", () => {
|
|
131
|
+
const sheet = new globalThis.CSSStyleSheet();
|
|
132
|
+
const idx = sheet.insertRule(".foo { color: red }", 0);
|
|
133
|
+
assert.strictEqual(idx, 0);
|
|
134
|
+
assert.strictEqual(sheet.cssRules.length, 1);
|
|
135
|
+
assert.strictEqual(sheet.cssRules[0].selectorText, ".foo ");
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
test.describe("ShimCustomElementRegistry", () => {
|
|
139
|
+
test.beforeEach(async () => {
|
|
140
|
+
teardownShim();
|
|
141
|
+
const { installDomShim } = await import("@microsoft/fast-test-harness/build/dom-shim.js");
|
|
142
|
+
installDomShim();
|
|
143
|
+
});
|
|
144
|
+
test("should support define and get", () => {
|
|
145
|
+
class MyEl {
|
|
146
|
+
}
|
|
147
|
+
globalThis.customElements.define("my-el", MyEl);
|
|
148
|
+
assert.strictEqual(globalThis.customElements.get("my-el"), MyEl);
|
|
149
|
+
});
|
|
150
|
+
test("should return undefined for unknown elements", () => {
|
|
151
|
+
assert.strictEqual(globalThis.customElements.get("unknown-el"), undefined);
|
|
152
|
+
});
|
|
153
|
+
test("should resolve whenDefined immediately", async () => {
|
|
154
|
+
const result = await globalThis.customElements.whenDefined("any-el");
|
|
155
|
+
assert.strictEqual(result, undefined);
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
test.describe("ShimDocument", () => {
|
|
159
|
+
test.beforeEach(async () => {
|
|
160
|
+
teardownShim();
|
|
161
|
+
const { installDomShim } = await import("@microsoft/fast-test-harness/build/dom-shim.js");
|
|
162
|
+
installDomShim();
|
|
163
|
+
});
|
|
164
|
+
test("should create elements via createElement", () => {
|
|
165
|
+
const el = globalThis.document.createElement("div");
|
|
166
|
+
assert.ok(el);
|
|
167
|
+
assert.strictEqual(typeof el.setAttribute, "function");
|
|
168
|
+
});
|
|
169
|
+
test("should support adoptedStyleSheets", () => {
|
|
170
|
+
assert.ok(Array.isArray(globalThis.document.adoptedStyleSheets));
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
test.describe("ShimCustomEvent", () => {
|
|
174
|
+
test.beforeEach(async () => {
|
|
175
|
+
teardownShim();
|
|
176
|
+
const { installDomShim } = await import("@microsoft/fast-test-harness/build/dom-shim.js");
|
|
177
|
+
installDomShim();
|
|
178
|
+
});
|
|
179
|
+
test("should carry detail", () => {
|
|
180
|
+
const evt = new globalThis.CustomEvent("test", { detail: 42 });
|
|
181
|
+
assert.strictEqual(evt.detail, 42);
|
|
182
|
+
assert.strictEqual(evt.type, "test");
|
|
183
|
+
});
|
|
184
|
+
test("should default detail to null", () => {
|
|
185
|
+
const evt = new globalThis.CustomEvent("test");
|
|
186
|
+
assert.strictEqual(evt.detail, null);
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
test.describe("matchMedia", () => {
|
|
190
|
+
test.beforeEach(async () => {
|
|
191
|
+
teardownShim();
|
|
192
|
+
const { installDomShim } = await import("@microsoft/fast-test-harness/build/dom-shim.js");
|
|
193
|
+
installDomShim();
|
|
194
|
+
});
|
|
195
|
+
test("should return a MediaQueryList with matches = false", () => {
|
|
196
|
+
const mql = globalThis.matchMedia("(prefers-color-scheme: dark)");
|
|
197
|
+
assert.strictEqual(mql.matches, false);
|
|
198
|
+
// Should not throw
|
|
199
|
+
mql.addEventListener("change", () => { });
|
|
200
|
+
mql.removeEventListener("change", () => { });
|
|
201
|
+
});
|
|
202
|
+
});
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Style extraction — converts compiled FAST ElementStyles JS modules
|
|
3
|
+
* into plain CSS files.
|
|
4
|
+
*
|
|
5
|
+
* Usage as a module:
|
|
6
|
+
* ```ts
|
|
7
|
+
* import { generateStylesheets } from "@microsoft/fast-test-harness/build/generate-stylesheets.js";
|
|
8
|
+
*
|
|
9
|
+
* await generateStylesheets({ cwd: process.cwd() });
|
|
10
|
+
* ```
|
|
11
|
+
*
|
|
12
|
+
* Usage as a Lage worker:
|
|
13
|
+
* ```ts
|
|
14
|
+
* import { generateStylesheets } from "@microsoft/fast-test-harness/build/generate-stylesheets.js";
|
|
15
|
+
*
|
|
16
|
+
* export default async function init({ target }) {
|
|
17
|
+
* await generateStylesheets({ cwd: target.cwd });
|
|
18
|
+
* }
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
import { glob, mkdir, writeFile } from "node:fs/promises";
|
|
22
|
+
import path from "node:path";
|
|
23
|
+
import { pathToFileURL } from "node:url";
|
|
24
|
+
import { styleText } from "node:util";
|
|
25
|
+
import { installDomShim } from "./dom-shim.js";
|
|
26
|
+
function flattenStyles(style) {
|
|
27
|
+
if (typeof style === "string") {
|
|
28
|
+
return [style];
|
|
29
|
+
}
|
|
30
|
+
if (Array.isArray(style.styles)) {
|
|
31
|
+
return style.styles.flatMap(flattenStyles);
|
|
32
|
+
}
|
|
33
|
+
return [style.toString?.() ?? ""];
|
|
34
|
+
}
|
|
35
|
+
export async function generateStylesheets(options = {}) {
|
|
36
|
+
installDomShim();
|
|
37
|
+
const cwd = options.cwd ?? process.cwd();
|
|
38
|
+
const distDir = path.resolve(cwd, options.distDir ?? "dist");
|
|
39
|
+
const outDir = options.outDir ? path.resolve(cwd, options.outDir) : null;
|
|
40
|
+
const pattern = options.pattern ?? "**/*.styles.js";
|
|
41
|
+
for await (const jsFile of glob(pattern, { cwd: distDir })) {
|
|
42
|
+
const jsFilePath = path.resolve(distDir, jsFile);
|
|
43
|
+
const baseName = path.basename(jsFile, ".js") + ".css";
|
|
44
|
+
const cssFilePath = outDir
|
|
45
|
+
? path.resolve(outDir, baseName)
|
|
46
|
+
: path.resolve(path.dirname(jsFilePath), baseName);
|
|
47
|
+
try {
|
|
48
|
+
const mod = await import(pathToFileURL(jsFilePath).href);
|
|
49
|
+
const stylesheet = mod.styles ?? mod.default;
|
|
50
|
+
if (!stylesheet?.styles) {
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
let css = stylesheet.styles.flatMap(flattenStyles).join("\n");
|
|
54
|
+
if (options.format) {
|
|
55
|
+
try {
|
|
56
|
+
css = await options.format(css, cssFilePath);
|
|
57
|
+
}
|
|
58
|
+
catch (formatError) {
|
|
59
|
+
console.warn(styleText(["yellow", "bold"], "⚠"), `Format failed for ${path.relative(cwd, cssFilePath)}:`, formatError.message);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
await mkdir(path.dirname(cssFilePath), { recursive: true });
|
|
63
|
+
await writeFile(cssFilePath, css, "utf8");
|
|
64
|
+
console.log(styleText(["green", "bold"], "✔"), "Style:", styleText("dim", path.relative(cwd, jsFilePath)), "→", styleText("bold", path.relative(cwd, cssFilePath)));
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
console.error(styleText(["red", "bold"], "✘"), `Failed: ${path.relative(cwd, jsFilePath)}`, error.message);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|