@microsoft/fast-test-harness 0.1.0 → 0.3.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 (37) hide show
  1. package/README.md +25 -19
  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 +14 -1
  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 +11 -16
  11. package/dist/dts/ssr/render.d.ts.map +1 -1
  12. package/dist/esm/build/generate-templates.js +62 -2
  13. package/dist/esm/build/generate-webui-templates.js +9 -26
  14. package/dist/esm/fixtures/ssr-fixture.js +19 -1
  15. package/dist/esm/ssr/render.js +48 -104
  16. package/package.json +26 -15
  17. package/dist/dts/build/dom-shim.test.d.ts +0 -2
  18. package/dist/dts/build/dom-shim.test.d.ts.map +0 -1
  19. package/dist/dts/build/generate-stylesheets.test.d.ts +0 -2
  20. package/dist/dts/build/generate-stylesheets.test.d.ts.map +0 -1
  21. package/dist/dts/build/generate-templates.test.d.ts +0 -2
  22. package/dist/dts/build/generate-templates.test.d.ts.map +0 -1
  23. package/dist/dts/build/generate-webui-templates.test.d.ts +0 -2
  24. package/dist/dts/build/generate-webui-templates.test.d.ts.map +0 -1
  25. package/dist/dts/fixtures/csr-fixture.pw.spec.d.ts +0 -2
  26. package/dist/dts/fixtures/csr-fixture.pw.spec.d.ts.map +0 -1
  27. package/dist/dts/fixtures/ssr-fixture.pw.spec.d.ts +0 -2
  28. package/dist/dts/fixtures/ssr-fixture.pw.spec.d.ts.map +0 -1
  29. package/dist/dts/ssr/render.test.d.ts +0 -2
  30. package/dist/dts/ssr/render.test.d.ts.map +0 -1
  31. package/dist/esm/build/dom-shim.test.js +0 -202
  32. package/dist/esm/build/generate-stylesheets.test.js +0 -74
  33. package/dist/esm/build/generate-templates.test.js +0 -231
  34. package/dist/esm/build/generate-webui-templates.test.js +0 -179
  35. package/dist/esm/fixtures/csr-fixture.pw.spec.js +0 -137
  36. package/dist/esm/fixtures/ssr-fixture.pw.spec.js +0 -189
  37. package/dist/esm/ssr/render.test.js +0 -236
@@ -21,10 +21,10 @@
21
21
  * flat exports.
22
22
  * ```ts
23
23
  * const { render } = createSSRRenderer({
24
- * tagPrefix: "mai",
24
+ * tagPrefix: "contoso",
25
25
  * components: [
26
- * { name: "button", packageName: "@mai-ui/button" },
27
- * { name: "checkbox", packageName: "@mai-ui/checkbox" },
26
+ * { name: "button", packageName: "@contoso/button" },
27
+ * { name: "checkbox", packageName: "@contoso/checkbox" },
28
28
  * ],
29
29
  * });
30
30
  * ```
@@ -71,75 +71,6 @@ function resolvePackageRoot(packageName) {
71
71
  function toServerUrl(absolutePath, packageRoot) {
72
72
  return `/${relative(packageRoot, absolutePath).replace(/\\/g, "/")}`;
73
73
  }
74
- /**
75
- * Parse a JavaScript default value string from CEM into a JSON-safe value.
76
- * @internal
77
- */
78
- export function parseDefaultValue(raw) {
79
- const trimmed = raw.trim();
80
- if (trimmed === "" || trimmed === "undefined" || trimmed === "null") {
81
- return "";
82
- }
83
- if (trimmed === "true") {
84
- return true;
85
- }
86
- if (trimmed === "false") {
87
- return false;
88
- }
89
- if (trimmed.startsWith("'") || trimmed.startsWith('"')) {
90
- return trimmed.slice(1, -1);
91
- }
92
- const num = Number(trimmed);
93
- if (!Number.isNaN(num)) {
94
- return num;
95
- }
96
- try {
97
- return JSON.parse(trimmed);
98
- }
99
- catch {
100
- return "";
101
- }
102
- }
103
- /**
104
- * Load a Custom Elements Manifest and extract default state for
105
- * each custom element declaration. Returns a map of tag names to
106
- * their default property values.
107
- */
108
- function loadDefaultStateFromCEM(packageName, tagPrefix) {
109
- const cemPath = resolveSpecifier(`${packageName}/custom-elements.json`);
110
- if (!cemPath) {
111
- return new Map();
112
- }
113
- try {
114
- const cem = JSON.parse(readFileSync(cemPath, "utf8"));
115
- const result = new Map();
116
- for (const mod of cem.modules ?? []) {
117
- for (const decl of mod.declarations ?? []) {
118
- if (!decl.customElement) {
119
- continue;
120
- }
121
- const tagName = decl.tagName ?? `${tagPrefix}-${decl.name?.toLowerCase()}`;
122
- const state = {};
123
- for (const member of decl.members ?? []) {
124
- if (member.kind !== "field" || member.privacy !== "public") {
125
- continue;
126
- }
127
- state[member.name] =
128
- member.default != null
129
- ? parseDefaultValue(String(member.default))
130
- : "";
131
- }
132
- if (Object.keys(state).length > 0) {
133
- result.set(tagName, state);
134
- }
135
- }
136
- }
137
- return result;
138
- }
139
- catch {
140
- return new Map();
141
- }
142
- }
143
74
  /**
144
75
  * Load artifacts for a component from a monolithic package
145
76
  * (e.g., `@fluentui/web-components/button/template.html`).
@@ -155,7 +86,7 @@ function loadMonolithicComponent(packageName, componentDir, packageRoot) {
155
86
  }
156
87
  /**
157
88
  * Load artifacts for a component from a per-component package
158
- * (e.g., `@mai-ui/button/template.html`).
89
+ * (e.g., `@contoso/button/template.html`).
159
90
  */
160
91
  function loadPerPackageComponent(reg) {
161
92
  const fTemplatePath = resolveSpecifier(`${reg.packageName}/template.html`);
@@ -263,9 +194,9 @@ export function buildState(queryObj) {
263
194
  *
264
195
  * Supports two modes:
265
196
  * - **`packageName`**: Scans a monolithic package's dist directory for
266
- * components in subdirectories (Fluent-style).
197
+ * components in subdirectories (monolithic layout).
267
198
  * - **`components`**: Uses an explicit list of per-component packages
268
- * with flat exports (MAI-style).
199
+ * with flat exports.
269
200
  */
270
201
  export function createSSRRenderer(options) {
271
202
  const { tagPrefix } = options;
@@ -284,13 +215,13 @@ export function createSSRRenderer(options) {
284
215
  // Collect component artifacts from either mode.
285
216
  const artifacts = [];
286
217
  if (options.components) {
287
- // Per-component packages (MAI-style).
218
+ // Per-component packages
288
219
  for (const reg of options.components) {
289
220
  artifacts.push(loadPerPackageComponent(reg));
290
221
  }
291
222
  }
292
223
  else if (options.packageName) {
293
- // Monolithic package (Fluent-style).
224
+ // Monolithic package
294
225
  const packageRoot = resolvePackageRoot(options.packageName);
295
226
  const distDir = join(packageRoot, options.distDir ?? "dist/esm");
296
227
  const pattern = "**/*.template.html";
@@ -306,30 +237,13 @@ export function createSSRRenderer(options) {
306
237
  artifacts.push(loadMonolithicComponent(options.packageName, componentDir, packageRoot));
307
238
  }
308
239
  }
309
- // Populate maps and collect default state from CEM.
310
- const defaultStateByTag = new Map();
240
+ // Populate template and style maps from collected artifacts.
311
241
  for (const art of artifacts) {
312
- const tagName = `${tagPrefix}-${art.componentName}`;
313
242
  if (art.fTemplate) {
314
243
  fTemplatesByName.set(art.componentName, art.fTemplate);
315
244
  }
316
245
  styleUrlsByName.set(art.componentName, art.stylesUrl);
317
246
  }
318
- // Load CEM defaults per-package (each package may contain multiple elements).
319
- if (options.components) {
320
- for (const reg of options.components) {
321
- const cemDefaults = loadDefaultStateFromCEM(reg.packageName, tagPrefix);
322
- for (const [tag, state] of cemDefaults) {
323
- defaultStateByTag.set(tag, state);
324
- }
325
- }
326
- }
327
- else if (options.packageName) {
328
- const cemDefaults = loadDefaultStateFromCEM(options.packageName, tagPrefix);
329
- for (const [tag, state] of cemDefaults) {
330
- defaultStateByTag.set(tag, state);
331
- }
332
- }
333
247
  // Inject styles into f-templates, then parse into the WASM
334
248
  // templates map (tag-name → inner template content).
335
249
  const templatesMap = {};
@@ -339,7 +253,16 @@ export function createSSRRenderer(options) {
339
253
  const parsed = JSON.parse(wasm.parse_f_templates(styled));
340
254
  const entry = parsed.find((t) => t.name !== null);
341
255
  if (entry) {
342
- templatesMap[`${tagPrefix}-${name}`] = entry.content;
256
+ const tagName = `${tagPrefix}-${name}`;
257
+ const attrs = entry.shadowrootAttributes ?? [];
258
+ // Use the object format when there are extra shadowroot attributes
259
+ // beyond the default shadowrootmode, so the WASM renderer can
260
+ // propagate them (e.g. shadowrootdelegatesfocus) onto the DSD
261
+ // <template> element.
262
+ const hasExtraAttrs = attrs.some(a => a.name !== "shadowrootmode" && a.name !== "shadowroot");
263
+ templatesMap[tagName] = hasExtraAttrs
264
+ ? { content: entry.content, shadowrootAttributes: attrs }
265
+ : entry.content;
343
266
  }
344
267
  else {
345
268
  console.warn(`No named template found for ${tagPrefix}-${name}`);
@@ -348,19 +271,21 @@ export function createSSRRenderer(options) {
348
271
  const templatesJson = JSON.stringify(templatesMap);
349
272
  // Concatenate all f-templates (with styles) for client hydration.
350
273
  const allFTemplates = [...fTemplatesByName.values()].join("\n");
351
- // Resolve theme stylesheet if provided.
352
- let preloadLinks = "";
274
+ // Build preload links: theme stylesheet + component stylesheets.
275
+ const preloadParts = [];
353
276
  if (options.themeStylesheet) {
354
- preloadLinks = `<link rel="stylesheet" href="${options.themeStylesheet}">`;
277
+ preloadParts.push(`<link rel="stylesheet" href="${options.themeStylesheet}">`);
355
278
  }
279
+ for (const stylesUrl of styleUrlsByName.values()) {
280
+ if (stylesUrl) {
281
+ preloadParts.push(`<link rel="preload" href="${stylesUrl}" as="style">`);
282
+ }
283
+ }
284
+ const preloadLinks = preloadParts.join("\n");
356
285
  return {
357
286
  render(queryObj = {}) {
358
287
  const entryHtml = buildEntryHtml(queryObj);
359
- const requestState = buildState(queryObj);
360
- // Merge CEM default state (base) with request state (overrides).
361
- const tagName = String(queryObj.tagName ?? "");
362
- const defaults = defaultStateByTag.get(tagName) ?? {};
363
- const state = { ...defaults, ...requestState };
288
+ const state = buildState(queryObj);
364
289
  let fixture = "";
365
290
  if (entryHtml) {
366
291
  try {
@@ -368,6 +293,25 @@ export function createSSRRenderer(options) {
368
293
  // Extract body content from the rendered document.
369
294
  const bodyMatch = rendered.match(/<body[^>]*>([\s\S]*?)<\/body>/i);
370
295
  fixture = bodyMatch?.[1] ?? entryHtml;
296
+ // The WASM renderer only injects DSD for top-level
297
+ // custom elements. Inject DSD for any nested custom
298
+ // elements that have templates but weren't rendered.
299
+ for (const nestedTag of Object.keys(templatesMap)) {
300
+ // Match opening tags that don't already have a
301
+ // DSD <template> as their first child.
302
+ const openTagRe = new RegExp(`(<${nestedTag}(?=[\\s>/])[^>]*>)(?!\\s*<template[\\s>])`, "g");
303
+ fixture = fixture.replace(openTagRe, (_, open) => {
304
+ // Render this element in isolation.
305
+ const solo = wasm.render_with_templates(`${open}</${nestedTag}>`, templatesJson, "{}", "camelCase");
306
+ // Extract the DSD that was injected.
307
+ const dsdStart = solo.indexOf("<template shadowrootmode");
308
+ const dsdEnd = solo.lastIndexOf("</template>");
309
+ if (dsdStart !== -1 && dsdEnd > dsdStart) {
310
+ return `${open}${solo.slice(dsdStart, dsdEnd + "</template>".length)}`;
311
+ }
312
+ return open;
313
+ });
314
+ }
371
315
  }
372
316
  catch (e) {
373
317
  // 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.3.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":""}
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=ssr-fixture.pw.spec.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"ssr-fixture.pw.spec.d.ts","sourceRoot":"","sources":["../../../src/fixtures/ssr-fixture.pw.spec.ts"],"names":[],"mappings":""}
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=render.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"render.test.d.ts","sourceRoot":"","sources":["../../../src/ssr/render.test.ts"],"names":[],"mappings":""}
@@ -1,202 +0,0 @@
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
- });
@@ -1,74 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
3
- import { tmpdir } from "node:os";
4
- import { join } from "node:path";
5
- import { test } from "node:test";
6
- import { generateStylesheets } from "@microsoft/fast-test-harness/build/generate-stylesheets.js";
7
- test.describe("generateStylesheets", () => {
8
- let tempDir;
9
- test.beforeEach(async () => {
10
- tempDir = await mkdtemp(join(tmpdir(), "fast-styles-"));
11
- });
12
- test.afterEach(async () => {
13
- await rm(tempDir, { recursive: true, force: true });
14
- });
15
- test("should extract CSS from a styles module", async () => {
16
- const distDir = join(tempDir, "dist");
17
- await mkdir(distDir, { recursive: true });
18
- // Write a fake styles module that exports an ElementStyles-like object.
19
- await writeFile(join(distDir, "button.styles.js"), `export const styles = { styles: [":host { display: block; }", "span { color: red; }"] };`);
20
- await generateStylesheets({ cwd: tempDir });
21
- const css = await readFile(join(distDir, "button.styles.css"), "utf8");
22
- assert.ok(css.includes(":host { display: block; }"));
23
- assert.ok(css.includes("span { color: red; }"));
24
- });
25
- test("should write to outDir when specified", async () => {
26
- const distDir = join(tempDir, "dist");
27
- const outDir = join(tempDir, "out");
28
- await mkdir(distDir, { recursive: true });
29
- await writeFile(join(distDir, "card.styles.js"), `export const styles = { styles: [".card { padding: 8px; }"] };`);
30
- await generateStylesheets({ cwd: tempDir, outDir: "out" });
31
- const css = await readFile(join(outDir, "card.styles.css"), "utf8");
32
- assert.ok(css.includes(".card { padding: 8px; }"));
33
- });
34
- test("should apply a format function", async () => {
35
- const distDir = join(tempDir, "dist");
36
- await mkdir(distDir, { recursive: true });
37
- await writeFile(join(distDir, "link.styles.js"), `export const styles = { styles: ["a { color: blue; }"] };`);
38
- await generateStylesheets({
39
- cwd: tempDir,
40
- format: css => `/* formatted */\n${css}`,
41
- });
42
- const css = await readFile(join(distDir, "link.styles.css"), "utf8");
43
- assert.ok(css.startsWith("/* formatted */"));
44
- });
45
- test("should flatten nested styles arrays", async () => {
46
- const distDir = join(tempDir, "dist");
47
- await mkdir(distDir, { recursive: true });
48
- await writeFile(join(distDir, "nested.styles.js"), `export const styles = {
49
- styles: [
50
- { styles: [":host { display: flex; }", "div { margin: 0; }"] },
51
- "span { font-size: 14px; }"
52
- ]
53
- };`);
54
- await generateStylesheets({ cwd: tempDir });
55
- const css = await readFile(join(distDir, "nested.styles.css"), "utf8");
56
- assert.ok(css.includes(":host { display: flex; }"));
57
- assert.ok(css.includes("div { margin: 0; }"));
58
- assert.ok(css.includes("span { font-size: 14px; }"));
59
- });
60
- test("should skip modules without a styles export", async () => {
61
- const distDir = join(tempDir, "dist");
62
- await mkdir(distDir, { recursive: true });
63
- await writeFile(join(distDir, "empty.styles.js"), `export const template = "<div></div>";`);
64
- await generateStylesheets({ cwd: tempDir });
65
- // Should not create a CSS file
66
- try {
67
- await readFile(join(distDir, "empty.styles.css"), "utf8");
68
- assert.fail("Should not have created a CSS file");
69
- }
70
- catch (err) {
71
- assert.strictEqual(err.code, "ENOENT");
72
- }
73
- });
74
- });