Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "none",
"comment": "chore: prepare package for publish",
"packageName": "@microsoft/fast-test-harness",
"email": "863023+radium-v@users.noreply.github.com",
"dependentChangeType": "none"
}
12 changes: 9 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

42 changes: 24 additions & 18 deletions packages/fast-test-harness/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@

The `fast-test-harness` package is a Playwright testing harness for FAST Element web components with CSR and SSR support.

## Requirements

- Node.js 22.18 or later
- Playwright 1.56 or later

## Installation

To install `fast-test-harness` using `npm`:
Expand All @@ -12,6 +17,21 @@ To install `fast-test-harness` using `npm`:
npm install --save-dev @microsoft/fast-test-harness
```

## Test directory setup

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.

```
test/
β”œβ”€β”€ index.html # CSR: loads main.ts
β”œβ”€β”€ ssr.html # SSR: template with comment placeholders
β”œβ”€β”€ vite.config.ts # Vite config (shared by both modes)
└── src/
β”œβ”€β”€ main.ts # CSR: registers components, applies theme
β”œβ”€β”€ entry-client.ts # SSR: registers components for hydration
└── entry-server.ts # SSR: exports render() for fixture generation
```

## Writing tests

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.
Expand Down Expand Up @@ -67,21 +87,6 @@ await expect(element).toHaveCustomState("checked");
| `waitFor` | `string[]` | `[]` | Additional elements to wait for before testing |
| `ssr` | `boolean` | `false` | Use SSR mode (or set `PLAYWRIGHT_TEST_SSR=true`) |

## Test directory setup

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.

```
test/
β”œβ”€β”€ index.html # CSR: loads main.ts
β”œβ”€β”€ ssr.html # SSR: template with comment placeholders
β”œβ”€β”€ vite.config.ts # Vite config (shared by both modes)
└── src/
β”œβ”€β”€ main.ts # CSR: registers components, applies theme
β”œβ”€β”€ entry-client.ts # SSR: registers components for hydration
└── entry-server.ts # SSR: exports render() for fixture generation
```

### CSR files

**`index.html`** loads a script that registers your components:
Expand Down Expand Up @@ -258,10 +263,11 @@ CLI flags take precedence over environment variables.

| Specifier | Contents |
|-----------|----------|
| `@microsoft/fast-test-harness` | `test`, `expect`, `CSRFixture`, `SSRFixture`, `createSSRRenderer`, build utilities |
| `@microsoft/fast-test-harness/server.mjs` | `startServer` |
| `@microsoft/fast-test-harness/ssr/render.js` | `createSSRRenderer`, `ComponentRegistration`, `RenderResult`, `SSRRendererOptions` |
| `@microsoft/fast-test-harness` | `test`, `expect`, `CSRFixture`, `SSRFixture`, `toHaveCustomState`, `installDomShim`, `createSSRRenderer` |
| `@microsoft/fast-test-harness/build/*.js` | `installDomShim`, `generateStylesheets`, `generateFTemplates`, `generateWebuiTemplates` |
| `@microsoft/fast-test-harness/fixtures/*.js` | `CSRFixture`, `SSRFixture`, `toHaveCustomState`, extended `test` and `expect` |
| `@microsoft/fast-test-harness/ssr/render.js` | `createSSRRenderer`, `renderTemplate`, `buildEntryHtml`, `buildState`, `parseDefaultValue` |
| `@microsoft/fast-test-harness/server.mjs` | `startServer` |
| `@microsoft/fast-test-harness/playwright.config.mjs` | Shared Playwright configuration |
| `@microsoft/fast-test-harness/vite.config.mjs` | Shared Vite configuration |
| `@microsoft/fast-test-harness/public/*` | Static assets (base CSS) |
38 changes: 24 additions & 14 deletions packages/fast-test-harness/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@
"types": "./dist/dts/build/*.d.ts",
"default": "./dist/esm/build/*.js"
},
"./fixtures/*.js": {
"types": "./dist/dts/fixtures/*.d.ts",
"default": "./dist/esm/fixtures/*.js"
},
"./ssr/*.js": {
"types": "./dist/dts/ssr/*.d.ts",
"default": "./dist/esm/ssr/*.js"
Expand All @@ -48,36 +52,42 @@
"./package.json": "./package.json"
},
"scripts": {
"clean": "clean dist temp test-results",
"clean": "clean dist temp test-results test/temp",
"build": "npm run build:tsc",
"build:tsc": "tsgo -p tsconfig.build.json",
"test": "npm run test:node && npm run test:playwright",
"lint": "biome-changed",
"lint:fix": "biome-changed -- --fix",
"prepublishOnly": "npm run clean && npm run build",
"test": "npm run lint && npm run test:node && npm run test:playwright",
"test:node": "node --test --experimental-test-isolation=none \"**/*.test.ts\"",
"test:playwright": "playwright test"
"test:playwright": "playwright test",
"test:chromium": "playwright test --project=chromium"
},
"files": [
"dist",
"playwright.config.mjs",
"playwright.config.d.ts",
"public",
"server.mjs",
"start.mjs",
"vite.config.mjs",
"vite.config.d.ts"
"*.mjs",
"*.d.ts"
],
"devDependencies": {
"@microsoft/fast-element": "^2.10.4",
"@microsoft/fast-build": "^0.6.0",
"@microsoft/fast-html": "^1.0.0-alpha.52"
},
"dependencies": {
"cheerio": "1.2.0"
},
"devDependencies": {
"@microsoft/fast-html": "*"
},
"peerDependencies": {
"@microsoft/fast-build": ">=0.6.0 <1.0.0",
"@microsoft/fast-html": ">=1.0.0-alpha.52 <1.0.0",
"@microsoft/fast-element": "^2.10.4 || ^3.0.0",
"@microsoft/fast-build": "^0.6.0",
"@microsoft/fast-html": ">=1.0.0-alpha.52",
"@playwright/test": ">=1.40.0",
"vite": ">=7.0.0"
},
"peerDependenciesMeta": {
"@microsoft/fast-element": {
"optional": true
},
"@microsoft/fast-html": {
"optional": true
}
Expand Down
28 changes: 28 additions & 0 deletions packages/fast-test-harness/src/fixtures/csr-fixture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,19 @@ import type { Locator, Page } from "@playwright/test";

export type ThemeTokens = Record<string, string | number | boolean>;

/**
* The initial attributes for the fixture's template, where boolean attributes are
* represented as `true` and omitted when `false`. This allows for a more intuitive
* configuration of boolean attributes in the template options.
*/
export type InitialTemplateAttributes = Record<string, string | true>;

/**
* The attributes for the fixture's template, where boolean attributes can be represented as `true`
* or `false`. When `true`, the attribute will be included without a value (e.g., `disabled`), and
* when `false`, the attribute will be omitted entirely. This type is used for updating the
* template, allowing for both adding and removing boolean attributes.
*/
export type TemplateAttributes = Record<string, string | boolean>;

/**
Expand All @@ -14,6 +25,10 @@ export type InitialTemplateOptions = {
innerHTML?: string;
};

/**
* The options for updating the fixture's template, where `attributes` can include boolean values to
* add or remove attributes from the element.
*/
export type FixtureOptions = Omit<InitialTemplateOptions, "attributes"> & {
attributes?: TemplateAttributes;
};
Expand Down Expand Up @@ -45,6 +60,19 @@ export class CSRFixture {

/**
* Additional custom elements to wait for before running the test.
*
* @remarks
* This is useful for fixtures that depend on multiple custom elements being defined
* and stable before the test can run. By specifying additional tag names here, the
* fixture will wait for these elements to be defined before proceeding. Ensure that
* any elements specified here are included on the page and properly defined to
* prevent test timeouts.
*
* @example
* test.use({
* tagName: "fast-dropdown",
* waitFor: ["fast-listbox", "fast-option"],
* });
*/
protected readonly waitFor: string[];

Expand Down
10 changes: 10 additions & 0 deletions packages/fast-test-harness/src/fixtures/ssr-fixture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,16 @@ export class SSRFixture extends CSRFixture {
*/
private templateRendered = false;

/**
* Creates an instance of the SSRFixture.
*
* @param page - The Playwright page object.
* @param tagName - The tag name of the custom element.
* @param innerHTML - The inner HTML of the custom element.
* @param waitFor - Additional custom elements to wait for.
* @param testId - The test ID for the SSR fixture.
* @param testTitle - The test title for the SSR fixture.
*/
constructor(
page: Page,
tagName: string,
Expand Down
7 changes: 5 additions & 2 deletions packages/fast-test-harness/src/ssr/render.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -276,15 +276,18 @@ test.describe("createSSRRenderer", () => {
);
});

test("should return empty preloadLinks without a theme", () => {
test("should not include theme link in preloadLinks when themeStylesheet is omitted", () => {
const { render } = createSSRRenderer({
tagPrefix: "test",
components: [{ name: "widget", packageName: "@microsoft/fast-test-harness" }],
});

const result = render({ tagName: "test-widget" });

assert.strictEqual(result.preloadLinks, "");
assert.ok(
!result.preloadLinks.includes('rel="stylesheet"'),
`should not have a stylesheet link, got: ${result.preloadLinks}`,
);
});

test("should handle raw HTML via the html key", () => {
Expand Down
41 changes: 38 additions & 3 deletions packages/fast-test-harness/src/ssr/render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -476,11 +476,17 @@ export function createSSRRenderer(options: SSRRendererOptions): {
// Concatenate all f-templates (with styles) for client hydration.
const allFTemplates = [...fTemplatesByName.values()].join("\n");

// Resolve theme stylesheet if provided.
let preloadLinks = "";
// Build preload links: theme stylesheet + component stylesheets.
const preloadParts: string[] = [];
if (options.themeStylesheet) {
preloadLinks = `<link rel="stylesheet" href="${options.themeStylesheet}">`;
preloadParts.push(`<link rel="stylesheet" href="${options.themeStylesheet}">`);
}
for (const stylesUrl of styleUrlsByName.values()) {
if (stylesUrl) {
preloadParts.push(`<link rel="preload" href="${stylesUrl}" as="style">`);
}
}
const preloadLinks = preloadParts.join("\n");

return {
render(queryObj: Record<string, string> = {}): RenderResult {
Expand All @@ -505,6 +511,35 @@ export function createSSRRenderer(options: SSRRendererOptions): {
// Extract body content from the rendered document.
const bodyMatch = rendered.match(/<body[^>]*>([\s\S]*?)<\/body>/i);
fixture = bodyMatch?.[1] ?? entryHtml;

// The WASM renderer only injects DSD for top-level
// custom elements. Inject DSD for any nested custom
// elements that have templates but weren't rendered.
for (const nestedTag of Object.keys(templatesMap)) {
// Match opening tags that don't already have a
// DSD <template> as their first child.
const openTagRe = new RegExp(
`(<${nestedTag}(?=[\\s>/])[^>]*>)(?!\\s*<template[\\s>])`,
"g",
);

fixture = fixture.replace(openTagRe, (_, open: string) => {
// Render this element in isolation.
const solo: string = wasm.render_with_templates(
`${open}</${nestedTag}>`,
templatesJson,
JSON.stringify(defaultStateByTag.get(nestedTag) ?? {}),
"camelCase",
);
// Extract the DSD that was injected.
const dsdStart = solo.indexOf("<template shadowrootmode");
const dsdEnd = solo.lastIndexOf("</template>");
if (dsdStart !== -1 && dsdEnd > dsdStart) {
return `${open}${solo.slice(dsdStart, dsdEnd + "</template>".length)}`;
}
return open;
});
}
} catch (e) {
// Fall back to the raw entry HTML if rendering fails.
console.error("WASM render failed:", e);
Expand Down
3 changes: 2 additions & 1 deletion packages/fast-test-harness/tsconfig.build.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,6 @@
"sourceMap": false,
"outDir": "dist/esm"
},
"include": ["src/**/*.ts"]
"include": ["src/**/*.ts"],
"exclude": ["src/**/*.test.ts", "src/**/*.spec.ts"]
}
Loading