@microsoft/fast-test-harness 0.1.0 → 0.2.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.
Files changed (36) hide show
  1. package/README.md +24 -18
  2. package/dist/dts/build/generate-templates.d.ts +35 -0
  3. package/dist/dts/build/generate-templates.d.ts.map +1 -1
  4. package/dist/dts/build/generate-webui-templates.d.ts +13 -0
  5. package/dist/dts/build/generate-webui-templates.d.ts.map +1 -1
  6. package/dist/dts/fixtures/csr-fixture.d.ts +28 -0
  7. package/dist/dts/fixtures/csr-fixture.d.ts.map +1 -1
  8. package/dist/dts/fixtures/ssr-fixture.d.ts +19 -0
  9. package/dist/dts/fixtures/ssr-fixture.d.ts.map +1 -1
  10. package/dist/dts/ssr/render.d.ts.map +1 -1
  11. package/dist/esm/build/generate-templates.js +61 -1
  12. package/dist/esm/build/generate-webui-templates.js +8 -25
  13. package/dist/esm/fixtures/ssr-fixture.js +19 -1
  14. package/dist/esm/ssr/render.js +38 -5
  15. package/package.json +26 -15
  16. package/dist/dts/build/dom-shim.test.d.ts +0 -2
  17. package/dist/dts/build/dom-shim.test.d.ts.map +0 -1
  18. package/dist/dts/build/generate-stylesheets.test.d.ts +0 -2
  19. package/dist/dts/build/generate-stylesheets.test.d.ts.map +0 -1
  20. package/dist/dts/build/generate-templates.test.d.ts +0 -2
  21. package/dist/dts/build/generate-templates.test.d.ts.map +0 -1
  22. package/dist/dts/build/generate-webui-templates.test.d.ts +0 -2
  23. package/dist/dts/build/generate-webui-templates.test.d.ts.map +0 -1
  24. package/dist/dts/fixtures/csr-fixture.pw.spec.d.ts +0 -2
  25. package/dist/dts/fixtures/csr-fixture.pw.spec.d.ts.map +0 -1
  26. package/dist/dts/fixtures/ssr-fixture.pw.spec.d.ts +0 -2
  27. package/dist/dts/fixtures/ssr-fixture.pw.spec.d.ts.map +0 -1
  28. package/dist/dts/ssr/render.test.d.ts +0 -2
  29. package/dist/dts/ssr/render.test.d.ts.map +0 -1
  30. package/dist/esm/build/dom-shim.test.js +0 -202
  31. package/dist/esm/build/generate-stylesheets.test.js +0 -74
  32. package/dist/esm/build/generate-templates.test.js +0 -231
  33. package/dist/esm/build/generate-webui-templates.test.js +0 -179
  34. package/dist/esm/fixtures/csr-fixture.pw.spec.js +0 -137
  35. package/dist/esm/fixtures/ssr-fixture.pw.spec.js +0 -189
  36. package/dist/esm/ssr/render.test.js +0 -236
package/README.md CHANGED
@@ -4,6 +4,11 @@
4
4
 
5
5
  The `fast-test-harness` package is a Playwright testing harness for FAST Element web components with CSR and SSR support.
6
6
 
7
+ ## Requirements
8
+
9
+ - Node.js 22.18 or later
10
+ - Playwright 1.56 or later
11
+
7
12
  ## Installation
8
13
 
9
14
  To install `fast-test-harness` using `npm`:
@@ -12,6 +17,21 @@ To install `fast-test-harness` using `npm`:
12
17
  npm install --save-dev @microsoft/fast-test-harness
13
18
  ```
14
19
 
20
+ ## Test directory setup
21
+
22
+ The harness serves a Vite dev server from a `test/` directory in your project. CSR and SSR modes use different entry points from the same directory.
23
+
24
+ ```
25
+ test/
26
+ ├── index.html # CSR: loads main.ts
27
+ ├── ssr.html # SSR: template with comment placeholders
28
+ ├── vite.config.ts # Vite config (shared by both modes)
29
+ └── src/
30
+ ├── main.ts # CSR: registers components, applies theme
31
+ ├── entry-client.ts # SSR: registers components for hydration
32
+ └── entry-server.ts # SSR: exports render() for fixture generation
33
+ ```
34
+
15
35
  ## Writing tests
16
36
 
17
37
  Import `test` and `expect` from the harness. Configure the component tag name with `test.use()`, then call `fastPage.setTemplate()` in each test to render it.
@@ -67,21 +87,6 @@ await expect(element).toHaveCustomState("checked");
67
87
  | `waitFor` | `string[]` | `[]` | Additional elements to wait for before testing |
68
88
  | `ssr` | `boolean` | `false` | Use SSR mode (or set `PLAYWRIGHT_TEST_SSR=true`) |
69
89
 
70
- ## Test directory setup
71
-
72
- The harness serves a Vite dev server from a `test/` directory in your project. CSR and SSR modes use different entry points from the same directory.
73
-
74
- ```
75
- test/
76
- ├── index.html # CSR: loads main.ts
77
- ├── ssr.html # SSR: template with comment placeholders
78
- ├── vite.config.ts # Vite config (shared by both modes)
79
- └── src/
80
- ├── main.ts # CSR: registers components, applies theme
81
- ├── entry-client.ts # SSR: registers components for hydration
82
- └── entry-server.ts # SSR: exports render() for fixture generation
83
- ```
84
-
85
90
  ### CSR files
86
91
 
87
92
  **`index.html`** loads a script that registers your components:
@@ -258,10 +263,11 @@ CLI flags take precedence over environment variables.
258
263
 
259
264
  | Specifier | Contents |
260
265
  |-----------|----------|
261
- | `@microsoft/fast-test-harness` | `test`, `expect`, `CSRFixture`, `SSRFixture`, `createSSRRenderer`, build utilities |
266
+ | `@microsoft/fast-test-harness` | `test`, `expect`, `CSRFixture`, `SSRFixture`, `toHaveCustomState`, `installDomShim`, `createSSRRenderer` |
267
+ | `@microsoft/fast-test-harness/build/*.js` | `installDomShim`, `generateStylesheets`, `generateFTemplates`, `generateWebuiTemplates`, `definitionAsyncResolver`, `shadowOptionsToAttributes`, `ShadowOptionsResolver` |
268
+ | `@microsoft/fast-test-harness/fixtures/*.js` | `CSRFixture`, `SSRFixture`, `toHaveCustomState`, extended `test` and `expect` |
269
+ | `@microsoft/fast-test-harness/ssr/render.js` | `createSSRRenderer`, `renderTemplate`, `buildEntryHtml`, `buildState`, `parseDefaultValue` |
262
270
  | `@microsoft/fast-test-harness/server.mjs` | `startServer` |
263
- | `@microsoft/fast-test-harness/ssr/render.js` | `createSSRRenderer`, `ComponentRegistration`, `RenderResult`, `SSRRendererOptions` |
264
- | `@microsoft/fast-test-harness/build/*.js` | `installDomShim`, `generateStylesheets`, `generateFTemplates`, `generateWebuiTemplates` |
265
271
  | `@microsoft/fast-test-harness/playwright.config.mjs` | Shared Playwright configuration |
266
272
  | `@microsoft/fast-test-harness/vite.config.mjs` | Shared Vite configuration |
267
273
  | `@microsoft/fast-test-harness/public/*` | Static assets (base CSS) |
@@ -45,7 +45,37 @@ export interface GenerateFTemplatesOptions {
45
45
  * Optional formatter function applied to generated HTML before writing.
46
46
  */
47
47
  format?: (html: string, filePath: string) => string | Promise<string>;
48
+ /**
49
+ * Resolves shadow DOM options for a given template module path.
50
+ * Returns a `shadowOptions` object (e.g. `{ delegatesFocus: true }`) or
51
+ * `undefined` if the component has no special shadow options.
52
+ *
53
+ * Defaults to {@link definitionAsyncResolver}, which loads a companion
54
+ * `*.definition-async.js` module next to each template. Set to `null`
55
+ * to disable shadow options resolution.
56
+ *
57
+ * @default definitionAsyncResolver
58
+ */
59
+ resolveShadowOptions?: ShadowOptionsResolver | null;
48
60
  }
61
+ /**
62
+ * A function that resolves shadow DOM options for a template module.
63
+ * Receives the absolute path to the compiled `*.template.js` file and
64
+ * returns shadow options or `undefined`.
65
+ */
66
+ export type ShadowOptionsResolver = (templateJsPath: string) => Record<string, unknown> | undefined | Promise<Record<string, unknown> | undefined>;
67
+ /**
68
+ * Convention-based resolver that loads a companion `*.definition-async.js`
69
+ * module next to the template module and returns its `shadowOptions`.
70
+ *
71
+ * For a template at `dist/textarea/textarea.template.js`, this looks for
72
+ * `dist/textarea/textarea.definition-async.js`.
73
+ *
74
+ * This is the default resolver used by {@link generateFTemplates} and
75
+ * {@link generateWebuiTemplates} when `resolveShadowOptions` is not
76
+ * specified.
77
+ */
78
+ export declare function definitionAsyncResolver(templateJsPath: string): Promise<Record<string, unknown> | undefined>;
49
79
  export interface ViewTemplate {
50
80
  html: string | HTMLTemplateElement;
51
81
  factories: Record<string, Factory>;
@@ -64,6 +94,11 @@ interface Factory {
64
94
  * f-template HTML string.
65
95
  */
66
96
  export declare function convertTemplate(viewTemplate: ViewTemplate, componentName: string): string | null;
97
+ /**
98
+ * Convert a `shadowOptions` object (e.g. `{ delegatesFocus: true }`) into
99
+ * DSD template attribute entries (e.g. `{ shadowrootdelegatesfocus: "" }`).
100
+ */
101
+ export declare function shadowOptionsToAttributes(shadowOptions: Record<string, unknown> | undefined): Record<string, string>;
67
102
  export declare function generateFTemplates(options?: GenerateFTemplatesOptions): Promise<void>;
68
103
  export {};
69
104
  //# sourceMappingURL=generate-templates.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"generate-templates.d.ts","sourceRoot":"","sources":["../../../src/build/generate-templates.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AA8BH,MAAM,WAAW,yBAAyB;IACtC;;OAEG;IACH,GAAG,CAAC,EAAE,MAAM,CAAC;IAEb;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IAEjB;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IAEjB;;;;;OAKG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAEhB;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB;;OAEG;IACH,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;CACzE;AAED,MAAM,WAAW,YAAY;IACzB,IAAI,EAAE,MAAM,GAAG,mBAAmB,CAAC;IACnC,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACtC;AAED,UAAU,OAAO;IACb,WAAW,EAAE;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;IAC9B,OAAO,CAAC,EAAE,GAAG,CAAC;IACd,WAAW,CAAC,EAAE;QACV,QAAQ,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,GAAG,CAAC;KACrC,CAAC;CACL;AAmFD;;;GAGG;AACH,wBAAgB,eAAe,CAC3B,YAAY,EAAE,YAAY,EAC1B,aAAa,EAAE,MAAM,GACtB,MAAM,GAAG,IAAI,CA4Hf;AAED,wBAAsB,kBAAkB,CACpC,OAAO,GAAE,yBAA8B,GACxC,OAAO,CAAC,IAAI,CAAC,CAkEf"}
1
+ {"version":3,"file":"generate-templates.d.ts","sourceRoot":"","sources":["../../../src/build/generate-templates.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AA8BH,MAAM,WAAW,yBAAyB;IACtC;;OAEG;IACH,GAAG,CAAC,EAAE,MAAM,CAAC;IAEb;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IAEjB;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IAEjB;;;;;OAKG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAEhB;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB;;OAEG;IACH,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAEtE;;;;;;;;;;OAUG;IACH,oBAAoB,CAAC,EAAE,qBAAqB,GAAG,IAAI,CAAC;CACvD;AAED;;;;GAIG;AACH,MAAM,MAAM,qBAAqB,GAAG,CAChC,cAAc,EAAE,MAAM,KACrB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAAC,CAAC;AAExF;;;;;;;;;;GAUG;AACH,wBAAsB,uBAAuB,CACzC,cAAc,EAAE,MAAM,GACvB,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAAC,CAY9C;AAED,MAAM,WAAW,YAAY;IACzB,IAAI,EAAE,MAAM,GAAG,mBAAmB,CAAC;IACnC,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACtC;AAED,UAAU,OAAO;IACb,WAAW,EAAE;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;IAC9B,OAAO,CAAC,EAAE,GAAG,CAAC;IACd,WAAW,CAAC,EAAE;QACV,QAAQ,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,GAAG,CAAC;KACrC,CAAC;CACL;AAmFD;;;GAGG;AACH,wBAAgB,eAAe,CAC3B,YAAY,EAAE,YAAY,EAC1B,aAAa,EAAE,MAAM,GACtB,MAAM,GAAG,IAAI,CA4Hf;AAED;;;GAGG;AACH,wBAAgB,yBAAyB,CACrC,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,GACnD,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAmBxB;AAED,wBAAsB,kBAAkB,CACpC,OAAO,GAAE,yBAA8B,GACxC,OAAO,CAAC,IAAI,CAAC,CAgFf"}
@@ -17,6 +17,7 @@
17
17
  * await generateWebuiTemplates({ cwd: process.cwd(), tagPrefix: "mai" });
18
18
  * ```
19
19
  */
20
+ import { type ShadowOptionsResolver } from "./generate-templates.js";
20
21
  export interface GenerateWebuiTemplatesOptions {
21
22
  /**
22
23
  * Root directory of the package. Defaults to `process.cwd()`.
@@ -49,6 +50,18 @@ export interface GenerateWebuiTemplatesOptions {
49
50
  * Optional formatter function applied to generated HTML before writing.
50
51
  */
51
52
  format?: (html: string, filePath: string) => string | Promise<string>;
53
+ /**
54
+ * Resolves shadow DOM options for a given template module path.
55
+ * Returns a `shadowOptions` object (e.g. `{ delegatesFocus: true }`) or
56
+ * `undefined` if the component has no special shadow options.
57
+ *
58
+ * Defaults to {@link definitionAsyncResolver}, which loads a companion
59
+ * `*.definition-async.js` module next to each template. Set to `null`
60
+ * to disable shadow options resolution.
61
+ *
62
+ * @default definitionAsyncResolver
63
+ */
64
+ resolveShadowOptions?: ShadowOptionsResolver | null;
52
65
  }
53
66
  export declare function generateWebuiTemplates(options?: GenerateWebuiTemplatesOptions): Promise<void>;
54
67
  //# sourceMappingURL=generate-webui-templates.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"generate-webui-templates.d.ts","sourceRoot":"","sources":["../../../src/build/generate-webui-templates.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAgBH,MAAM,WAAW,6BAA6B;IAC1C;;OAEG;IACH,GAAG,CAAC,EAAE,MAAM,CAAC;IAEb;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IAEjB;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IAEjB;;;;;OAKG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAEhB;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB;;OAEG;IACH,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;CACzE;AAuED,wBAAsB,sBAAsB,CACxC,OAAO,GAAE,6BAAkC,GAC5C,OAAO,CAAC,IAAI,CAAC,CAmEf"}
1
+ {"version":3,"file":"generate-webui-templates.d.ts","sourceRoot":"","sources":["../../../src/build/generate-webui-templates.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAQH,OAAO,EAGH,KAAK,qBAAqB,EAG7B,MAAM,yBAAyB,CAAC;AAQjC,MAAM,WAAW,6BAA6B;IAC1C;;OAEG;IACH,GAAG,CAAC,EAAE,MAAM,CAAC;IAEb;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IAEjB;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IAEjB;;;;;OAKG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAEhB;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB;;OAEG;IACH,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAEtE;;;;;;;;;;OAUG;IACH,oBAAoB,CAAC,EAAE,qBAAqB,GAAG,IAAI,CAAC;CACvD;AA4CD,wBAAsB,sBAAsB,CACxC,OAAO,GAAE,6BAAkC,GAC5C,OAAO,CAAC,IAAI,CAAC,CA6Ef"}
@@ -1,6 +1,17 @@
1
1
  import type { Locator, Page } from "@playwright/test";
2
2
  export type ThemeTokens = Record<string, string | number | boolean>;
3
+ /**
4
+ * The initial attributes for the fixture's template, where boolean attributes are
5
+ * represented as `true` and omitted when `false`. This allows for a more intuitive
6
+ * configuration of boolean attributes in the template options.
7
+ */
3
8
  export type InitialTemplateAttributes = Record<string, string | true>;
9
+ /**
10
+ * The attributes for the fixture's template, where boolean attributes can be represented as `true`
11
+ * or `false`. When `true`, the attribute will be included without a value (e.g., `disabled`), and
12
+ * when `false`, the attribute will be omitted entirely. This type is used for updating the
13
+ * template, allowing for both adding and removing boolean attributes.
14
+ */
4
15
  export type TemplateAttributes = Record<string, string | boolean>;
5
16
  /**
6
17
  * The options for configuring the fixture's template.
@@ -9,6 +20,10 @@ export type InitialTemplateOptions = {
9
20
  attributes?: InitialTemplateAttributes;
10
21
  innerHTML?: string;
11
22
  };
23
+ /**
24
+ * The options for updating the fixture's template, where `attributes` can include boolean values to
25
+ * add or remove attributes from the element.
26
+ */
12
27
  export type FixtureOptions = Omit<InitialTemplateOptions, "attributes"> & {
13
28
  attributes?: TemplateAttributes;
14
29
  };
@@ -36,6 +51,19 @@ export declare class CSRFixture {
36
51
  protected readonly innerHTML: string;
37
52
  /**
38
53
  * Additional custom elements to wait for before running the test.
54
+ *
55
+ * @remarks
56
+ * This is useful for fixtures that depend on multiple custom elements being defined
57
+ * and stable before the test can run. By specifying additional tag names here, the
58
+ * fixture will wait for these elements to be defined before proceeding. Ensure that
59
+ * any elements specified here are included on the page and properly defined to
60
+ * prevent test timeouts.
61
+ *
62
+ * @example
63
+ * test.use({
64
+ * tagName: "fast-dropdown",
65
+ * waitFor: ["fast-listbox", "fast-option"],
66
+ * });
39
67
  */
40
68
  protected readonly waitFor: string[];
41
69
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"csr-fixture.d.ts","sourceRoot":"","sources":["../../../src/fixtures/csr-fixture.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAEtD,MAAM,MAAM,WAAW,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,CAAC;AAEpE,MAAM,MAAM,yBAAyB,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC,CAAC;AAEtE,MAAM,MAAM,kBAAkB,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC;AAElE;;GAEG;AACH,MAAM,MAAM,sBAAsB,GAAG;IACjC,UAAU,CAAC,EAAE,yBAAyB,CAAC;IACvC,SAAS,CAAC,EAAE,MAAM,CAAC;CACtB,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG,IAAI,CAAC,sBAAsB,EAAE,YAAY,CAAC,GAAG;IACtE,UAAU,CAAC,EAAE,kBAAkB,CAAC;CACnC,CAAC;AAEF;;;GAGG;AACH,MAAM,MAAM,iBAAiB,GAAG,sBAAsB,GAAG,MAAM,CAAC;AAEhE;;GAEG;AACH,qBAAa,UAAU;aA8BC,IAAI,EAAE,IAAI;IA7B9B;;OAEG;IACH,SAAgB,OAAO,EAAE,OAAO,CAAC;IAEjC;;OAEG;IACH,SAAS,CAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IAEnC;;OAEG;IACH,SAAS,CAAC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAErC;;OAEG;IACH,SAAS,CAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,CAAC;IAErC;;;;;;;OAOG;IACH,YACoB,IAAI,EAAE,IAAI,EAC1B,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM,EACjB,OAAO,GAAE,MAAM,EAAO,EAMzB;IAED;;;;;OAKG;IACG,WAAW,CAAC,OAAO,EAAE,UAAU,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAE5E;IAED;;;;OAIG;IACG,IAAI,CAAC,GAAG,GAAE,MAAY,iBAE3B;IAED;;;;;OAKG;IACG,WAAW,CAAC,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CASpD;IAED;;OAEG;IACH,OAAO,CAAC,eAAe;IAkBvB;;;;;;;;;;;;;;OAcG;IACG,WAAW,CAAC,iBAAiB,CAAC,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CAmBtE;IAED;;;;;OAKG;IACH,UAAgB,gBAAgB,IAAI,OAAO,CAAC,IAAI,CAAC,CAqBhD;IAED;;;;;;OAMG;IACG,cAAc,CAChB,OAAO,EAAE,MAAM,GAAG,OAAO,EACzB,OAAO,EAAE,cAAc,GACxB,OAAO,CAAC,IAAI,CAAC,CAuBf;IAED;;;;;;OAMG;IACG,oBAAoB,CACtB,OAAO,GAAE,MAAqB,EAC9B,GAAG,QAAQ,EAAE,MAAM,EAAE,GACtB,OAAO,CAAC,IAAI,CAAC,CAUf;CACJ"}
1
+ {"version":3,"file":"csr-fixture.d.ts","sourceRoot":"","sources":["../../../src/fixtures/csr-fixture.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAEtD,MAAM,MAAM,WAAW,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,CAAC;AAEpE;;;;GAIG;AACH,MAAM,MAAM,yBAAyB,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC,CAAC;AAEtE;;;;;GAKG;AACH,MAAM,MAAM,kBAAkB,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC;AAElE;;GAEG;AACH,MAAM,MAAM,sBAAsB,GAAG;IACjC,UAAU,CAAC,EAAE,yBAAyB,CAAC;IACvC,SAAS,CAAC,EAAE,MAAM,CAAC;CACtB,CAAC;AAEF;;;GAGG;AACH,MAAM,MAAM,cAAc,GAAG,IAAI,CAAC,sBAAsB,EAAE,YAAY,CAAC,GAAG;IACtE,UAAU,CAAC,EAAE,kBAAkB,CAAC;CACnC,CAAC;AAEF;;;GAGG;AACH,MAAM,MAAM,iBAAiB,GAAG,sBAAsB,GAAG,MAAM,CAAC;AAEhE;;GAEG;AACH,qBAAa,UAAU;aA2CC,IAAI,EAAE,IAAI;IA1C9B;;OAEG;IACH,SAAgB,OAAO,EAAE,OAAO,CAAC;IAEjC;;OAEG;IACH,SAAS,CAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IAEnC;;OAEG;IACH,SAAS,CAAC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAErC;;;;;;;;;;;;;;;OAeG;IACH,SAAS,CAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,CAAC;IAErC;;;;;;;OAOG;IACH,YACoB,IAAI,EAAE,IAAI,EAC1B,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM,EACjB,OAAO,GAAE,MAAM,EAAO,EAMzB;IAED;;;;;OAKG;IACG,WAAW,CAAC,OAAO,EAAE,UAAU,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAE5E;IAED;;;;OAIG;IACG,IAAI,CAAC,GAAG,GAAE,MAAY,iBAE3B;IAED;;;;;OAKG;IACG,WAAW,CAAC,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CASpD;IAED;;OAEG;IACH,OAAO,CAAC,eAAe;IAkBvB;;;;;;;;;;;;;;OAcG;IACG,WAAW,CAAC,iBAAiB,CAAC,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CAmBtE;IAED;;;;;OAKG;IACH,UAAgB,gBAAgB,IAAI,OAAO,CAAC,IAAI,CAAC,CAqBhD;IAED;;;;;;OAMG;IACG,cAAc,CAChB,OAAO,EAAE,MAAM,GAAG,OAAO,EACzB,OAAO,EAAE,cAAc,GACxB,OAAO,CAAC,IAAI,CAAC,CAuBf;IAED;;;;;;OAMG;IACG,oBAAoB,CACtB,OAAO,GAAE,MAAqB,EAC9B,GAAG,QAAQ,EAAE,MAAM,EAAE,GACtB,OAAO,CAAC,IAAI,CAAC,CAUf;CACJ"}
@@ -11,6 +11,25 @@ export declare class SSRFixture extends CSRFixture {
11
11
  * Whether the template has been rendered.
12
12
  */
13
13
  private templateRendered;
14
+ /**
15
+ * The internal reference to the generated fixture URL.
16
+ */
17
+ private _url?;
18
+ /**
19
+ * The URL of the generated SSR fixture page. This is set after {@link setTemplate}
20
+ * is called and the page navigates to the generated fixture.
21
+ */
22
+ get url(): string | undefined;
23
+ /**
24
+ * Creates an instance of the SSRFixture.
25
+ *
26
+ * @param page - The Playwright page object.
27
+ * @param tagName - The tag name of the custom element.
28
+ * @param innerHTML - The inner HTML of the custom element.
29
+ * @param waitFor - Additional custom elements to wait for.
30
+ * @param testId - The test ID for the SSR fixture.
31
+ * @param testTitle - The test title for the SSR fixture.
32
+ */
14
33
  constructor(page: Page, tagName: string, innerHTML?: string, waitFor?: string[], testId?: string, testTitle?: string);
15
34
  /**
16
35
  * Buffers style tags added before {@link setTemplate} so they can be
@@ -1 +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"}
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;IAuClC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC;IACxB,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAC;IAvC/B;;OAEG;IACH,OAAO,CAAC,aAAa,CAA4C;IAEjE;;OAEG;IACH,OAAO,CAAC,gBAAgB,CAAS;IAEjC;;OAEG;IACH,OAAO,CAAC,IAAI,CAAC,CAAS;IAEtB;;;OAGG;IACH,IAAW,GAAG,IAAI,MAAM,GAAG,SAAS,CAEnC;IAED;;;;;;;;;OASG;IACH,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,CAqE/E;IAED;;OAEG;IACH,OAAO,CAAC,eAAe;CAqB1B"}
@@ -1 +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"}
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,CAkMA"}
@@ -29,6 +29,30 @@ function wrapDefaultExpression(expression) {
29
29
  function attributeDirective(name, value) {
30
30
  return `${attributeDirectivePrefix}${name}="${wrapClientExpression(value)}"`;
31
31
  }
32
+ /**
33
+ * Convention-based resolver that loads a companion `*.definition-async.js`
34
+ * module next to the template module and returns its `shadowOptions`.
35
+ *
36
+ * For a template at `dist/textarea/textarea.template.js`, this looks for
37
+ * `dist/textarea/textarea.definition-async.js`.
38
+ *
39
+ * This is the default resolver used by {@link generateFTemplates} and
40
+ * {@link generateWebuiTemplates} when `resolveShadowOptions` is not
41
+ * specified.
42
+ */
43
+ export async function definitionAsyncResolver(templateJsPath) {
44
+ const dir = path.dirname(templateJsPath);
45
+ const base = path.basename(templateJsPath, ".template.js");
46
+ const defAsyncPath = path.resolve(dir, `${base}.definition-async.js`);
47
+ try {
48
+ const mod = await import(pathToFileURL(defAsyncPath).href);
49
+ const definition = mod.definition ?? mod.default;
50
+ return definition?.shadowOptions;
51
+ }
52
+ catch {
53
+ return undefined;
54
+ }
55
+ }
32
56
  /**
33
57
  * Extract a readable binding expression from a factory's evaluate function.
34
58
  */
@@ -199,6 +223,29 @@ export function convertTemplate(viewTemplate, componentName) {
199
223
  fInner = fInner.replace(/(<template[^>]*>)/, `$1${stylesMarker}`);
200
224
  return `<f-template name="${componentName}" shadowrootmode="open">\n${fInner}\n</f-template>\n`;
201
225
  }
226
+ /**
227
+ * Convert a `shadowOptions` object (e.g. `{ delegatesFocus: true }`) into
228
+ * DSD template attribute entries (e.g. `{ shadowrootdelegatesfocus: "" }`).
229
+ */
230
+ export function shadowOptionsToAttributes(shadowOptions) {
231
+ const attrs = {};
232
+ if (!shadowOptions) {
233
+ return attrs;
234
+ }
235
+ for (const [key, value] of Object.entries(shadowOptions)) {
236
+ if (key === "mode") {
237
+ continue;
238
+ }
239
+ const attrName = `shadowroot${key.toLowerCase()}`;
240
+ if (value === true) {
241
+ attrs[attrName] = "";
242
+ }
243
+ else if (typeof value === "string" && value !== "") {
244
+ attrs[attrName] = value;
245
+ }
246
+ }
247
+ return attrs;
248
+ }
202
249
  export async function generateFTemplates(options = {}) {
203
250
  installDomShim();
204
251
  const cwd = options.cwd ?? process.cwd();
@@ -220,7 +267,19 @@ export async function generateFTemplates(options = {}) {
220
267
  if (!fTemplateHtml) {
221
268
  continue;
222
269
  }
270
+ // Resolve shadow options and inject them into the <f-template> tag.
271
+ const resolver = options.resolveShadowOptions === null
272
+ ? undefined
273
+ : (options.resolveShadowOptions ?? definitionAsyncResolver);
274
+ const shadowOpts = resolver ? await resolver(jsFilePath) : undefined;
275
+ const shadowAttrs = shadowOptionsToAttributes(shadowOpts);
223
276
  let html = fTemplateHtml;
277
+ if (Object.keys(shadowAttrs).length > 0) {
278
+ const extraAttrs = Object.entries(shadowAttrs)
279
+ .map(([k, v]) => (v ? ` ${k}="${v}"` : ` ${k}`))
280
+ .join("");
281
+ html = html.replace(/(<f-template[^>]*)(>)/, `$1${extraAttrs}$2`);
282
+ }
224
283
  if (options.format) {
225
284
  try {
226
285
  html = await options.format(html, jsFilePath);
@@ -229,8 +288,9 @@ export async function generateFTemplates(options = {}) {
229
288
  console.warn(styleText(["yellow", "bold"], "⚠"), `Format failed for ${componentName}:`, formatError.message);
230
289
  }
231
290
  }
291
+ const relativeDir = path.relative(distDir, path.dirname(jsFilePath));
232
292
  const fTemplatePath = outDir
233
- ? path.resolve(outDir, `${componentBaseName}.template.html`)
293
+ ? path.resolve(outDir, relativeDir, `${componentBaseName}.template.html`)
234
294
  : path.resolve(path.dirname(jsFilePath), `${componentBaseName}.template.html`);
235
295
  await mkdir(path.dirname(fTemplatePath), { recursive: true });
236
296
  await writeFile(fTemplatePath, html, "utf8");
@@ -23,31 +23,9 @@ import { pathToFileURL } from "node:url";
23
23
  import { styleText } from "node:util";
24
24
  import { closeExpression, openExpression } from "@microsoft/fast-html/syntax.js";
25
25
  import { installDomShim } from "./dom-shim.js";
26
- import { convertTemplate } from "./generate-templates.js";
26
+ import { convertTemplate, definitionAsyncResolver, shadowOptionsToAttributes, } from "./generate-templates.js";
27
27
  const stylesMarker = `${openExpression}styles${closeExpression}`;
28
28
  const escapedStylesMarker = new RegExp(stylesMarker.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "g");
29
- /**
30
- * Try to load shadow options from a companion `*.definition-async.js`
31
- * module next to the template module. Returns an object with template
32
- * attribute strings to add (e.g. `shadowrootdelegatesfocus`).
33
- */
34
- async function loadShadowAttributes(templateJsPath) {
35
- const dir = path.dirname(templateJsPath);
36
- const base = path.basename(templateJsPath, ".template.js");
37
- const defAsyncPath = path.resolve(dir, `${base}.definition-async.js`);
38
- const attrs = {};
39
- try {
40
- const mod = await import(pathToFileURL(defAsyncPath).href);
41
- const definition = mod.definition ?? mod.default;
42
- if (definition?.shadowOptions?.delegatesFocus) {
43
- attrs.shadowrootdelegatesfocus = "";
44
- }
45
- }
46
- catch {
47
- // No definition-async module or it failed to load — skip.
48
- }
49
- return attrs;
50
- }
51
29
  /**
52
30
  * Transform an f-template string into a webui template by replacing the
53
31
  * `<f-template>` wrapper with `<template shadowrootmode="open">` and
@@ -97,7 +75,11 @@ export async function generateWebuiTemplates(options = {}) {
97
75
  if (!fTemplateHtml) {
98
76
  continue;
99
77
  }
100
- const shadowAttrs = await loadShadowAttributes(jsFilePath);
78
+ const resolver = options.resolveShadowOptions === null
79
+ ? undefined
80
+ : (options.resolveShadowOptions ?? definitionAsyncResolver);
81
+ const shadowOpts = resolver ? await resolver(jsFilePath) : undefined;
82
+ const shadowAttrs = shadowOptionsToAttributes(shadowOpts);
101
83
  let html = fTemplateToWebui(fTemplateHtml, shadowAttrs);
102
84
  if (options.format) {
103
85
  try {
@@ -107,8 +89,9 @@ export async function generateWebuiTemplates(options = {}) {
107
89
  console.warn(styleText(["yellow", "bold"], "⚠"), `Format failed for ${componentName}:`, formatError.message);
108
90
  }
109
91
  }
92
+ const relativeDir = path.relative(distDir, path.dirname(jsFilePath));
110
93
  const webuiPath = outDir
111
- ? path.resolve(outDir, `${componentBaseName}.template-webui.html`)
94
+ ? path.resolve(outDir, relativeDir, `${componentBaseName}.template-webui.html`)
112
95
  : path.resolve(path.dirname(jsFilePath), `${componentBaseName}.template-webui.html`);
113
96
  await mkdir(path.dirname(webuiPath), { recursive: true });
114
97
  await writeFile(webuiPath, html, "utf8");
@@ -1,5 +1,22 @@
1
1
  import { CSRFixture } from "./csr-fixture.js";
2
2
  export class SSRFixture extends CSRFixture {
3
+ /**
4
+ * The URL of the generated SSR fixture page. This is set after {@link setTemplate}
5
+ * is called and the page navigates to the generated fixture.
6
+ */
7
+ get url() {
8
+ return this._url;
9
+ }
10
+ /**
11
+ * Creates an instance of the SSRFixture.
12
+ *
13
+ * @param page - The Playwright page object.
14
+ * @param tagName - The tag name of the custom element.
15
+ * @param innerHTML - The inner HTML of the custom element.
16
+ * @param waitFor - Additional custom elements to wait for.
17
+ * @param testId - The test ID for the SSR fixture.
18
+ * @param testTitle - The test title for the SSR fixture.
19
+ */
3
20
  constructor(page, tagName, innerHTML = "", waitFor = [], testId, testTitle) {
4
21
  super(page, tagName, innerHTML, waitFor);
5
22
  this.testId = testId;
@@ -86,7 +103,8 @@ export class SSRFixture extends CSRFixture {
86
103
  if (!result.url) {
87
104
  throw new Error(`Invalid response from server: ${JSON.stringify(result)}`);
88
105
  }
89
- await this.page.goto(result.url);
106
+ this._url = result.url;
107
+ await this.page.goto(this._url);
90
108
  await this.waitForStability();
91
109
  this.templateRendered = true;
92
110
  this.pendingStyles.length = 0;
@@ -309,7 +309,6 @@ export function createSSRRenderer(options) {
309
309
  // Populate maps and collect default state from CEM.
310
310
  const defaultStateByTag = new Map();
311
311
  for (const art of artifacts) {
312
- const tagName = `${tagPrefix}-${art.componentName}`;
313
312
  if (art.fTemplate) {
314
313
  fTemplatesByName.set(art.componentName, art.fTemplate);
315
314
  }
@@ -339,7 +338,16 @@ export function createSSRRenderer(options) {
339
338
  const parsed = JSON.parse(wasm.parse_f_templates(styled));
340
339
  const entry = parsed.find((t) => t.name !== null);
341
340
  if (entry) {
342
- templatesMap[`${tagPrefix}-${name}`] = entry.content;
341
+ const tagName = `${tagPrefix}-${name}`;
342
+ const attrs = entry.shadowrootAttributes ?? [];
343
+ // Use the object format when there are extra shadowroot attributes
344
+ // beyond the default shadowrootmode, so the WASM renderer can
345
+ // propagate them (e.g. shadowrootdelegatesfocus) onto the DSD
346
+ // <template> element.
347
+ const hasExtraAttrs = attrs.some(a => a.name !== "shadowrootmode" && a.name !== "shadowroot");
348
+ templatesMap[tagName] = hasExtraAttrs
349
+ ? { content: entry.content, shadowrootAttributes: attrs }
350
+ : entry.content;
343
351
  }
344
352
  else {
345
353
  console.warn(`No named template found for ${tagPrefix}-${name}`);
@@ -348,11 +356,17 @@ export function createSSRRenderer(options) {
348
356
  const templatesJson = JSON.stringify(templatesMap);
349
357
  // Concatenate all f-templates (with styles) for client hydration.
350
358
  const allFTemplates = [...fTemplatesByName.values()].join("\n");
351
- // Resolve theme stylesheet if provided.
352
- let preloadLinks = "";
359
+ // Build preload links: theme stylesheet + component stylesheets.
360
+ const preloadParts = [];
353
361
  if (options.themeStylesheet) {
354
- preloadLinks = `<link rel="stylesheet" href="${options.themeStylesheet}">`;
362
+ preloadParts.push(`<link rel="stylesheet" href="${options.themeStylesheet}">`);
355
363
  }
364
+ for (const stylesUrl of styleUrlsByName.values()) {
365
+ if (stylesUrl) {
366
+ preloadParts.push(`<link rel="preload" href="${stylesUrl}" as="style">`);
367
+ }
368
+ }
369
+ const preloadLinks = preloadParts.join("\n");
356
370
  return {
357
371
  render(queryObj = {}) {
358
372
  const entryHtml = buildEntryHtml(queryObj);
@@ -368,6 +382,25 @@ export function createSSRRenderer(options) {
368
382
  // Extract body content from the rendered document.
369
383
  const bodyMatch = rendered.match(/<body[^>]*>([\s\S]*?)<\/body>/i);
370
384
  fixture = bodyMatch?.[1] ?? entryHtml;
385
+ // The WASM renderer only injects DSD for top-level
386
+ // custom elements. Inject DSD for any nested custom
387
+ // elements that have templates but weren't rendered.
388
+ for (const nestedTag of Object.keys(templatesMap)) {
389
+ // Match opening tags that don't already have a
390
+ // DSD <template> as their first child.
391
+ const openTagRe = new RegExp(`(<${nestedTag}(?=[\\s>/])[^>]*>)(?!\\s*<template[\\s>])`, "g");
392
+ fixture = fixture.replace(openTagRe, (_, open) => {
393
+ // Render this element in isolation.
394
+ const solo = wasm.render_with_templates(`${open}</${nestedTag}>`, templatesJson, JSON.stringify(defaultStateByTag.get(nestedTag) ?? {}), "camelCase");
395
+ // Extract the DSD that was injected.
396
+ const dsdStart = solo.indexOf("<template shadowrootmode");
397
+ const dsdEnd = solo.lastIndexOf("</template>");
398
+ if (dsdStart !== -1 && dsdEnd > dsdStart) {
399
+ return `${open}${solo.slice(dsdStart, dsdEnd + "</template>".length)}`;
400
+ }
401
+ return open;
402
+ });
403
+ }
371
404
  }
372
405
  catch (e) {
373
406
  // Fall back to the raw entry HTML if rendering fails.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@microsoft/fast-test-harness",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "author": {
5
5
  "name": "Microsoft",
6
6
  "url": "https://discord.gg/FcSNfg4"
@@ -28,6 +28,10 @@
28
28
  "types": "./dist/dts/build/*.d.ts",
29
29
  "default": "./dist/esm/build/*.js"
30
30
  },
31
+ "./fixtures/*.js": {
32
+ "types": "./dist/dts/fixtures/*.d.ts",
33
+ "default": "./dist/esm/fixtures/*.js"
34
+ },
31
35
  "./ssr/*.js": {
32
36
  "types": "./dist/dts/ssr/*.d.ts",
33
37
  "default": "./dist/esm/ssr/*.js"
@@ -38,6 +42,7 @@
38
42
  },
39
43
  "./public/*": "./public/*",
40
44
  "./template.html": "./test/src/test-widget/test-widget.template.html",
45
+ "./input/template.html": "./test/src/test-widget/input/test-input.template.html",
41
46
  "./styles.css": "./test/src/test-widget/test-widget.styles.css",
42
47
  "./server.mjs": "./server.mjs",
43
48
  "./start.mjs": "./start.mjs",
@@ -48,36 +53,42 @@
48
53
  "./package.json": "./package.json"
49
54
  },
50
55
  "scripts": {
51
- "clean": "clean dist temp test-results",
56
+ "clean": "clean dist temp test-results test/temp",
52
57
  "build": "npm run build:tsc",
53
58
  "build:tsc": "tsgo -p tsconfig.build.json",
54
- "test": "npm run test:node && npm run test:playwright",
59
+ "lint": "biome-changed",
60
+ "lint:fix": "biome-changed -- --fix",
61
+ "prepublishOnly": "npm run clean && npm run build",
62
+ "test": "npm run lint && npm run test:node && npm run test:playwright",
55
63
  "test:node": "node --test --experimental-test-isolation=none \"**/*.test.ts\"",
56
- "test:playwright": "playwright test"
64
+ "test:playwright": "playwright test",
65
+ "test:chromium": "playwright test --project=chromium"
57
66
  },
58
67
  "files": [
59
68
  "dist",
60
- "playwright.config.mjs",
61
- "playwright.config.d.ts",
62
69
  "public",
63
- "server.mjs",
64
- "start.mjs",
65
- "vite.config.mjs",
66
- "vite.config.d.ts"
70
+ "*.mjs",
71
+ "*.d.ts"
67
72
  ],
73
+ "devDependencies": {
74
+ "@microsoft/fast-element": "^2.10.4",
75
+ "@microsoft/fast-build": "^0.7.0",
76
+ "@microsoft/fast-html": "^1.0.0-alpha.53"
77
+ },
68
78
  "dependencies": {
69
79
  "cheerio": "1.2.0"
70
80
  },
71
- "devDependencies": {
72
- "@microsoft/fast-html": "*"
73
- },
74
81
  "peerDependencies": {
75
- "@microsoft/fast-build": ">=0.6.0 <1.0.0",
76
- "@microsoft/fast-html": ">=1.0.0-alpha.52 <1.0.0",
82
+ "@microsoft/fast-element": "^2.10.4 || ^3.0.0",
83
+ "@microsoft/fast-build": "^0.7.0",
84
+ "@microsoft/fast-html": ">=1.0.0-alpha.53 <1.0.0",
77
85
  "@playwright/test": ">=1.40.0",
78
86
  "vite": ">=7.0.0"
79
87
  },
80
88
  "peerDependenciesMeta": {
89
+ "@microsoft/fast-element": {
90
+ "optional": true
91
+ },
81
92
  "@microsoft/fast-html": {
82
93
  "optional": true
83
94
  }
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=dom-shim.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"dom-shim.test.d.ts","sourceRoot":"","sources":["../../../src/build/dom-shim.test.ts"],"names":[],"mappings":""}
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=generate-stylesheets.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"generate-stylesheets.test.d.ts","sourceRoot":"","sources":["../../../src/build/generate-stylesheets.test.ts"],"names":[],"mappings":""}
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=generate-templates.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"generate-templates.test.d.ts","sourceRoot":"","sources":["../../../src/build/generate-templates.test.ts"],"names":[],"mappings":""}
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=generate-webui-templates.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"generate-webui-templates.test.d.ts","sourceRoot":"","sources":["../../../src/build/generate-webui-templates.test.ts"],"names":[],"mappings":""}
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=csr-fixture.pw.spec.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"csr-fixture.pw.spec.d.ts","sourceRoot":"","sources":["../../../src/fixtures/csr-fixture.pw.spec.ts"],"names":[],"mappings":""}