--- name: testing description: Test patterns, fixture structure, and when to use E2E vs unit tests. --- # Testing Use this skill when writing or modifying tests in this repository. ## Rust crate tests Unit tests live alongside code in `#[cfg(test)]` modules. Integration tests go in each crate's `tests/` directory. ```bash cargo test -p microsoft-webui-handler # one crate cargo test --workspace # all crates cargo xtask test # via xtask (part of quality gate) ``` Every code change ships with tests: | Change type | Requirement | |-------------|-------------| | New public API | At least one unit test per function/method | | Bug fix | Regression test that fails without the fix | | Performance change | Benchmark comparison (before/after) | | Refactor | Existing tests pass unchanged | Never remove, weaken, or `#[ignore]` an existing test unless explicitly asked. ## Shared test utilities (`webui-test-utils`) The `webui-test-utils` crate provides common Rust test helpers, builders, and fixtures. Check it before writing new helpers - avoid duplicating across crates. ```toml [dev-dependencies] webui-test-utils = { path = "../webui-test-utils" } ``` ## webui-framework E2E fixtures The `packages/webui-framework` package uses Playwright for E2E tests. Each fixture is a mini WebUI app compiled and rendered by the real pipeline. See `tests/fixtures/README.md` for the full reference. ### Fixture structure ``` tests/fixtures// src/ index.html Page template (uses the component) / .html Component template (real WebUI syntax) .css Component CSS (optional) state.json Initial render state (all bound properties) element.ts Component class (NO template registration) .spec.ts Playwright tests webui.config.json Build options override (optional) ``` ### How it works The test server (`fixture-render.ts`) auto-discovers fixtures with `src/index.html`, calls `@microsoft/webui` `build()` + `render()` to produce SSR HTML with template IIFEs, hydration markers, and inventory. The result is served at `//fixture.html`. ### Creating a new fixture 1. Create `fixtures//src/index.html` — page template: ```html My Fixture ``` 2. Create `fixtures//src/test-widget/test-widget.html` — component template: ```html {{label}} {{count}} ``` 3. Create `fixtures//state.json` — initial state: ```json { "label": "Hello", "count": 0 } ``` 4. Create `fixtures//element.ts` — component class only: ```typescript // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. import { WebUIElement, attr, observable } from '../../../src/index.js'; export class TestWidget extends WebUIElement { @attr label = 'Hello'; @observable count = 0; increment(): void { this.count += 1; } } TestWidget.define('test-widget'); ``` 5. Create `fixtures//.spec.ts` — Playwright tests: ```typescript // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. import { expect, test } from '@playwright/test'; test.describe('widget fixture', () => { test.beforeEach(async ({ page }) => { await page.goto('//fixture.html'); await page.waitForFunction(() => { const el = document.querySelector('test-widget'); return el && (el as any).$ready === true; }); }); test('renders SSR content', async ({ page }) => { await expect(page.locator('test-widget .label')).toHaveText('Hello'); await expect(page.locator('test-widget .count')).toHaveText('0'); }); test('updates on click', async ({ page }) => { await page.locator('test-widget .inc').click(); await expect(page.locator('test-widget .count')).toHaveText('1'); }); }); ``` ### Template syntax quick reference | Feature | Syntax | |---------|--------| | Text binding | `{{propertyName}}` | | Event binding | `@click="{handler()}"` or `@click="{handler(e)}"` | | Boolean attribute | `?disabled="{{isDisabled}}"` | | Dynamic attribute | `href="{{url}}"` | | Mixed attribute | `href="/items/{{id}}"` | | Complex property | `:items="{{items}}"` | | Element ref | `w-ref="{myElement}"` | | Conditional | `...` | | Negation | `...` | | Comparison | `...` | | Compound | `...` | | Loop | `...` | | Nested loop | `......` | | Shadow DOM | `` | | Root event | `