diff --git a/crates/webui-press/README.md b/crates/webui-press/README.md index 7ed1d36b5..33c23d77e 100644 --- a/crates/webui-press/README.md +++ b/crates/webui-press/README.md @@ -658,6 +658,13 @@ Generated root and page entries also receive build-time ` + diff --git a/docs/.webui-press/components/benchmark-preview/benchmark-preview.test.ts b/docs/.webui-press/components/benchmark-preview/benchmark-preview.test.ts index 342991ec5..dc2f66e13 100644 --- a/docs/.webui-press/components/benchmark-preview/benchmark-preview.test.ts +++ b/docs/.webui-press/components/benchmark-preview/benchmark-preview.test.ts @@ -99,7 +99,7 @@ test('homepage evidence preserves unavailable and measured-zero values', () => { } }); -test('homepage notes use the chart metric order and preserve sampling and measurement limitations', () => { +test('benchmark notes use the chart metric order and preserve sampling and measurement limitations', () => { const evidence = prepareBenchmarkEvidence(source()); const previews = buildBenchmarkPreviews(evidence); const notes = buildBenchmarkNotes(evidence); @@ -183,7 +183,8 @@ test('the homepage defaults to RPS and switches four ordered metrics without mou await expect(preview.locator('.value')).toHaveText(metric.values); await expect(preview.locator('.chart-caption')).toContainText(metric.caption); await expect(preview.locator('.direction')).toHaveText(metric.direction); - await expect(page.locator(`#benchmark-note-${metric.id}`)).toContainText(metric.sampling); + await expect(preview.locator('.chart-caption sup a')).toHaveAttribute( + 'href', `/webui/reference/benchmarks/#benchmark-note-${metric.id}`); await expect(preview.locator('.scale')).toContainText(metric.maximum); const widths = await preview.locator('.bar').evaluateAll(elements => elements.map(element => Number(element.getAttribute('width')))); @@ -199,13 +200,13 @@ test('the homepage defaults to RPS and switches four ordered metrics without mou assert.ok(shape.graphics && shape.width > 0 && shape.height > 0, 'Each selected bar must actually paint, not merely carry numeric attributes.'); } await expect(preview.locator('.details-link')).toHaveAttribute('href', '/webui/reference/benchmarks/'); - await expect(preview.getByRole('link', { name: 'Full data and methodology', exact: true })).toBeVisible(); + await expect(preview.getByRole('link', { name: 'More frameworks and methodology', exact: true })).toBeVisible(); } assert.deepEqual(errors, []); assert.equal(await page.evaluate(() => customElements.get('benchmark-explorer') !== undefined), false); }); -test('metric notes are native document fragment targets with and without JavaScript', async t => { +test('metric notes link to the benchmark page with and without JavaScript', async t => { const server = await startWebsite(); t.after(() => server.close()); const browser = await chromium.launch({ headless: true }); @@ -213,9 +214,10 @@ test('metric notes are native document fragment targets with and without JavaScr for (const javaScriptEnabled of [false, true]) { const page = await browser.newPage({ javaScriptEnabled }); await page.route('https://**', route => route.abort()); - await page.goto(`${server.origin}/webui/`); const preview = page.locator('benchmark-preview'); for (const metric of javaScriptEnabled ? metrics : metrics.slice(0, 1)) { + await page.goto(`${server.origin}/webui/`); + await expect(page.locator('.benchmark-notes')).toHaveCount(0); if (javaScriptEnabled) { const tab = preview.getByRole('tab', { name: metric.label, exact: true }); await expect(tab).toBeEnabled(); @@ -224,7 +226,7 @@ test('metric notes are native document fragment targets with and without JavaScr const link = preview.locator('.chart-caption sup a'); await expect(link).toHaveAccessibleName(/measurement notes/); await link.click(); - await expect(page).toHaveURL(`${server.origin}/webui/#benchmark-note-${metric.id}`); + await expect(page).toHaveURL(`${server.origin}/webui/reference/benchmarks/#benchmark-note-${metric.id}`); const note = page.locator(`#benchmark-note-${metric.id}`); assert.equal(await note.evaluate(element => element.getRootNode() === document), true); await expect(note).toBeInViewport(); diff --git a/docs/.webui-press/components/install-command/install-command.ts b/docs/.webui-press/components/install-command/install-command.ts index 683d5f635..b6b9be775 100644 --- a/docs/.webui-press/components/install-command/install-command.ts +++ b/docs/.webui-press/components/install-command/install-command.ts @@ -12,12 +12,15 @@ interface InstallOption { type CopyState = "idle" | "copying" | "copied" | "failed" | "unavailable"; +const packageManagerKey = "webui-press:tabs:package-manager"; + export class InstallCommand extends WebUIElement { @observable installOptions: InstallOption[] = []; @observable installReady = false; @observable copyState: CopyState = "idle"; private copyGeneration = 0; + private preferenceStorage?: Storage; protected hydratedCallback(): void { let selected = 0; @@ -29,9 +32,31 @@ export class InstallCommand extends WebUIElement { console.error("[WebUI docs] Install commands require exactly one selected command."); return; } + this.restoreManager(); this.installReady = true; } + private restoreManager(): void { + let storage: Storage; + let stored: string | null; + try { + const available = this.ownerDocument.defaultView?.localStorage; + if (!available) { + console.warn("[WebUI docs] Package-manager preferences require localStorage; selection remains page-local."); + return; + } + storage = available; + stored = storage.getItem(packageManagerKey); + } catch (error) { + console.warn("[WebUI docs] Cannot restore package-manager preferences; selection remains page-local.", error); + return; + } + if (stored !== null && this.installOptions.some(option => option.id === stored)) { + this.selectManager(stored); + } + this.preferenceStorage = storage; + } + selectManager(id: string): void { if (!this.installOptions.some(option => option.id === id)) { this.copyState = "unavailable"; @@ -45,6 +70,14 @@ export class InstallCommand extends WebUIElement { selected: option.id === id, })); if (this.copyState !== "copying") this.copyState = "idle"; + if (this.preferenceStorage) { + try { + this.preferenceStorage.setItem(packageManagerKey, id); + } catch (error) { + this.preferenceStorage = undefined; + console.warn("[WebUI docs] Cannot save package-manager preferences; selection remains page-local.", error); + } + } } async copy(): Promise { diff --git a/docs/.webui-press/regions/home-features.html b/docs/.webui-press/regions/home-features.html index 089a4eb5c..b3bfceb04 100644 --- a/docs/.webui-press/regions/home-features.html +++ b/docs/.webui-press/regions/home-features.html @@ -137,23 +137,3 @@

Make your first page

Start building - -
-

Benchmark notes

-

- Official campaign results for a 100-todo app, not a current framework ranking. - The featured WebUI configuration uses Light DOM with Rust and Actix. -

-
    - -
  1. - {{note.label}}. {{note.description}} {{note.samplingLabel}} -
  2. -
    -
-

- The campaign covers all 15 configurations from one clean source revision. - The download is a sanitized extract, not a complete raw-run archive. - Full data and methodology -

-
diff --git a/docs/.webui-press/scripts/prepare-benchmarks.mjs b/docs/.webui-press/scripts/prepare-benchmarks.mjs index df117dc24..b8b3e69eb 100644 --- a/docs/.webui-press/scripts/prepare-benchmarks.mjs +++ b/docs/.webui-press/scripts/prepare-benchmarks.mjs @@ -57,10 +57,12 @@ try { ); } const fullView = buildBenchmarkView(evidence); - const renderedState = { benchmarks: fullView }; + const renderedState = { + benchmarks: fullView, + benchmarkNotes: buildBenchmarkNotes(evidence), + }; const previewState = { previewMetrics: buildBenchmarkPreviews(evidence), - benchmarkNotes: buildBenchmarkNotes(evidence), }; for (const [file, content] of [[download, evidence], [state, renderedState], [homeState, previewState]]) { try { diff --git a/docs/ai.md b/docs/ai.md index acfbb3085..99d506539 100644 --- a/docs/ai.md +++ b/docs/ai.md @@ -1527,6 +1527,11 @@ Install `@microsoft/webui-press` at the same version as `@microsoft/webui`. `webui press build` and `webui press serve` invoke the native Press sidecar; Press does not support `--format json`. +For page-local synchronized tabs, give related `` controls +the same `sync` name and their `` children matching `value` +attributes. Add `storage="local"` to remember a category across page loads. +See [tabbed content](/reference/webui-press#tabbed-content). + Set `"outline": true` in `.webui-press/config.json` for the built-in page outline; it is off by default. Markdown frontmatter and object-form custom pages can override it with the same boolean. Eligible `doc`/`page` layouts get diff --git a/docs/build/desktop.md b/docs/build/desktop.md index 643c03655..4a26528d7 100644 --- a/docs/build/desktop.md +++ b/docs/build/desktop.md @@ -13,12 +13,43 @@ The target machine still needs the platform's webview and runtime prerequisites. ## Get started -For an npm project, opt in to desktop support with matching package versions: +For a JavaScript project, opt in to desktop support with matching package versions: + + +npm +pnpm +Yarn +Bun + ```bash npm install @microsoft/webui @microsoft/webui-desktop ``` + + + +```bash +pnpm add @microsoft/webui @microsoft/webui-desktop +``` + + + + +```bash +yarn add @microsoft/webui @microsoft/webui-desktop +``` + + + + +```bash +bun add @microsoft/webui @microsoft/webui-desktop +``` + + + + The desktop package installs the native sidecar for your platform without adding webview dependencies to the base `@microsoft/webui` package. Do not disable optional dependencies. A Rust CLI installation can instead use a @@ -1010,10 +1041,42 @@ Check platform capabilities before requesting menus, tray, or effects. Enable the SDK's `application-ipc` feature and install the browser runtime: + +npm +pnpm +Yarn +Bun + + +```bash +npm install @microsoft/webui-desktop +``` + + + + ```bash pnpm add @microsoft/webui-desktop ``` + + + +```bash +yarn add @microsoft/webui-desktop +``` + + + + +```bash +bun add @microsoft/webui-desktop +``` + + + + + Define one proto3 contract for your app. For example: ```proto diff --git a/docs/build/hydration.md b/docs/build/hydration.md index f23e5c6f6..0688437ec 100644 --- a/docs/build/hydration.md +++ b/docs/build/hydration.md @@ -435,10 +435,41 @@ projection build reports an error rather than dropping inherited state. The supported esbuild plugin is exported from `@microsoft/webui/projection.js`: + +npm +pnpm +Yarn +Bun + + ```bash npm install -D esbuild typescript ``` + + + +```bash +pnpm add -D esbuild typescript +``` + + + + +```bash +yarn add --dev esbuild typescript +``` + + + + +```bash +bun add --dev esbuild typescript +``` + + + + ```js // build-client.mjs import * as esbuild from 'esbuild'; diff --git a/docs/build/integrations/dotnet.md b/docs/build/integrations/dotnet.md index 4524ebfac..04f56c272 100644 --- a/docs/build/integrations/dotnet.md +++ b/docs/build/integrations/dotnet.md @@ -12,6 +12,11 @@ then dispose native-backed objects when their work is complete. ## Installation +**Check package availability first.** NuGet artifacts are staged by the release +pipeline, but automatic NuGet.org publishing is not enabled pending an approved +signing path. Obtain matching packages from an available release or approved +feed; the command below requires that feed to be configured. + ```bash dotnet add package Microsoft.WebUI ``` diff --git a/docs/build/integrations/node.md b/docs/build/integrations/node.md index d005dd1e9..a3173c2db 100644 --- a/docs/build/integrations/node.md +++ b/docs/build/integrations/node.md @@ -14,11 +14,42 @@ and pass JSON state for each render. Buffered output is a UTF-8 `Buffer`. ## Installation + +npm +pnpm +Yarn +Bun + + ```bash npm install @microsoft/webui ``` -Keep optional dependencies enabled so npm can install the matching native + + + +```bash +pnpm add @microsoft/webui +``` + + + + +```bash +yarn add @microsoft/webui +``` + + + + +```bash +bun add @microsoft/webui +``` + + + + +Keep optional dependencies enabled so your manager can install the matching native package. This API needs native-addon support; it is not a browser or JavaScript-only edge-runtime package. diff --git a/docs/build/integrations/rust.md b/docs/build/integrations/rust.md index 6ca31ad5a..49cb6c10d 100644 --- a/docs/build/integrations/rust.md +++ b/docs/build/integrations/rust.md @@ -20,6 +20,14 @@ and `/`. Attribute names passed to `write_escaped_attribute` must be trusted. ## Installation +To install the standalone compiler without npm, use Rust 1.99.1 or newer: + +```bash +cargo install microsoft-webui-cli +``` + +For a Rust application host, add the rendering library: + ```toml [dependencies] microsoft-webui = "*" # see https://crates.io/crates/microsoft-webui for latest version diff --git a/docs/build/routing.md b/docs/build/routing.md index 816fc4ca9..e984aabcf 100644 --- a/docs/build/routing.md +++ b/docs/build/routing.md @@ -16,10 +16,41 @@ server's JavaScript engine. ## Installation + +npm +pnpm +Yarn +Bun + + ```bash npm install @microsoft/webui-router ``` + + + +```bash +pnpm add @microsoft/webui-router +``` + + + + +```bash +yarn add @microsoft/webui-router +``` + + + + +```bash +bun add @microsoft/webui-router +``` + + + + Only needed when your app has client-side navigation. Server-only apps with full page loads don't need it. ## Quick Start diff --git a/docs/index.md b/docs/index.md index c1795fb6c..216c46c8b 100644 --- a/docs/index.md +++ b/docs/index.md @@ -22,4 +22,8 @@ state: label: Yarn command: "yarn add -D @microsoft/webui" selected: false + - id: bun + label: Bun + command: "bun add -D @microsoft/webui" + selected: false --- diff --git a/docs/learn/how-it-works.md b/docs/learn/how-it-works.md index 446a24954..692b71a16 100644 --- a/docs/learn/how-it-works.md +++ b/docs/learn/how-it-works.md @@ -30,10 +30,41 @@ A small interactive app has these inputs: into `protocol.bin`. It also prepares the chosen CSS output. Rebuild when those sources change, not for each request. + +npm +pnpm +Yarn +Bun + + ```bash npx webui build ./src --out ./dist --plugin webui ``` + + + +```bash +pnpm exec webui build ./src --out ./dist --plugin webui +``` + + + + +```bash +yarn run webui build ./src --out ./dist --plugin webui +``` + + + + +```bash +bun run webui build ./src --out ./dist --plugin webui +``` + + + + This command **does not bundle application TypeScript**. Use a JavaScript bundler for the browser entry and load its output from the page. The [Add interaction lesson](/start/add-interactivity#bundle-the-browser-entry) provides a diff --git a/docs/reference/benchmarks.md b/docs/reference/benchmarks.md index 84df88feb..2794e465f 100644 --- a/docs/reference/benchmarks.md +++ b/docs/reference/benchmarks.md @@ -18,6 +18,25 @@ Browser metrics describe **10 isolated runs per configuration**. +
+

Benchmark notes

+

+ Official campaign results for a 100-todo app, not a current framework ranking. + The featured WebUI configuration uses Light DOM with Rust and Actix. +

+
    + +
  1. + {{note.label}}. {{note.description}} {{note.samplingLabel}} +
  2. +
    +
+

+ The campaign covers all 15 configurations from one clean source revision. + The download is a sanitized extract, not a complete raw-run archive. +

+
+ ## What this snapshot describes The recorded server workload uses 100 todos, 13 component types, 32 warm diff --git a/docs/reference/cli/index.md b/docs/reference/cli/index.md index 9f356bdf8..2227180ee 100644 --- a/docs/reference/cli/index.md +++ b/docs/reference/cli/index.md @@ -19,19 +19,81 @@ produces the protocol and CSS; it does not replace your application bundler. ## Installation -Install via npm: +Install with your package manager: + + +npm +pnpm +Yarn +Bun + ```bash npm install @microsoft/webui ``` + + + +```bash +pnpm add @microsoft/webui +``` + + + + +```bash +yarn add @microsoft/webui +``` + + + + +```bash +bun add @microsoft/webui +``` + + + + Install WebUI Press separately when you need the documentation/static-site generator CLI: + +npm +pnpm +Yarn +Bun + + ```bash npm install @microsoft/webui-press ``` + + + +```bash +pnpm add @microsoft/webui-press +``` + + + + +```bash +yarn add @microsoft/webui-press +``` + + + + +```bash +bun add @microsoft/webui-press +``` + + + + Or install via Cargo for standalone CLI use: ```bash @@ -624,13 +686,44 @@ Install the matching desktop support package; ordinary web builds do not need it. A standalone CLI can find the `webui-desktop` sidecar beside its binary or on `PATH`; `WEBUI_DESKTOP_BINARY` overrides discovery. -For a new npm project, install matching versions of both packages with optional +For a new JavaScript project, install matching versions of both packages with optional dependencies enabled: + +npm +pnpm +Yarn +Bun + + ```bash npm install @microsoft/webui @microsoft/webui-desktop ``` + + + +```bash +pnpm add @microsoft/webui @microsoft/webui-desktop +``` + + + + +```bash +yarn add @microsoft/webui @microsoft/webui-desktop +``` + + + + +```bash +bun add @microsoft/webui @microsoft/webui-desktop +``` + + + + `webui desktop` invokes the native Rust sidecar directly, without a Node wrapper. Running it without desktop support exits with a hint to install `@microsoft/webui-desktop`. A sidecar from a different WebUI version fails diff --git a/docs/reference/webui-press.md b/docs/reference/webui-press.md index 6979dde8a..6c32c54f3 100644 --- a/docs/reference/webui-press.md +++ b/docs/reference/webui-press.md @@ -8,10 +8,41 @@ output that can be deployed to GitHub Pages, a CDN, or any static host. Install the native CLI package: + +npm +pnpm +Yarn +Bun + + ```bash npm install @microsoft/webui @microsoft/webui-press ``` + + + +```bash +pnpm add @microsoft/webui @microsoft/webui-press +``` + + + + +```bash +yarn add @microsoft/webui @microsoft/webui-press +``` + + + + +```bash +bun add @microsoft/webui @microsoft/webui-press +``` + + + + The package installs the matching platform-specific binary automatically for Windows, macOS, or Linux on x64 and arm64. After installation, run `webui press build` or `webui press serve` directly without compiling the Rust @@ -34,6 +65,55 @@ second-stop behavior, stdin restrictions, and platform limits. watched inputs change. Reading source files, including test discovery and component imports, does not trigger a rebuild or browser reload. +## Tabbed content + +Use the built-in tab controls around Markdown or HTML content. Mark the initial +tab and its panel `active` so that content is visible before JavaScript loads: + +```html + + npm + pnpm + + + npm install @microsoft/webui + + + + + pnpm add @microsoft/webui + + + +``` + +Give related groups the same `sync` name and each tab a unique, nonempty `value`. +Selecting `pnpm` in one group selects that value in the other groups on the +page, even when their tabs are in a different order. A group without the +selected value keeps its current tab. Groups without `sync`, or with another +name, remain independent. + +By default, the selection belongs to the current document and is not saved +across page loads. The first connected group supplies the initial selection; +groups connected later adopt the current value. + +Add `storage="local"` to opt a category into browser persistence: + +```html + +``` + +The category uses `webui-press:tabs:` in localStorage on the current +origin. Later page loads restore that value, including in another browser tab; +already-open documents keep their own selection until reloaded. If a group +does not offer the saved value, it keeps its authored active tab. A new +selection replaces the saved value for that category only. + +Set the storage option consistently on related groups. A group that enables +storage later does not overwrite a choice already made on that page. +If browser policy or storage limits prevent reading or saving preferences, +Press logs a warning and the controls continue with page-local selection. + ## Content-only sites The native `webui-press` binary supports the same display modes for static diff --git a/docs/start/add-interactivity.md b/docs/start/add-interactivity.md index b3e569dff..8baa1c6f6 100644 --- a/docs/start/add-interactivity.md +++ b/docs/start/add-interactivity.md @@ -16,11 +16,45 @@ Continue in the `hello-webui` project from The CLI renders HTML. Interactive components also need the browser runtime and a bundler for their TypeScript: + +npm +pnpm +Yarn +Bun + + ```bash npm install @microsoft/webui-framework npm install --save-dev esbuild ``` + + + +```bash +pnpm add @microsoft/webui-framework +pnpm add -D esbuild +``` + + + + +```bash +yarn add @microsoft/webui-framework +yarn add --dev esbuild +``` + + + + +```bash +bun add @microsoft/webui-framework +bun add --dev esbuild +``` + + + + Keep WebUI packages on matching releases. If your package manager reports blocked scripts for `esbuild`, review and approve that package separately using the @@ -180,11 +214,45 @@ Keep your dependencies, script approvals, and other fields. ## Run the counter + +npm +pnpm +Yarn +Bun + + ```bash npm run build npm run dev ``` + + + +```bash +pnpm run build +pnpm run dev +``` + + + + +```bash +yarn run build +yarn run dev +``` + + + + +```bash +bun run build +bun run dev +``` + + + + `dist/index.js` contains the counter and browser runtime. The WebUI build emits `dist/protocol.bin` and the paired component CSS. `--servedir ./dist` makes the browser bundle available at `/index.js`; the preview server handles component diff --git a/docs/start/first-component.md b/docs/start/first-component.md index 80d59843c..3627b8918 100644 --- a/docs/start/first-component.md +++ b/docs/start/first-component.md @@ -100,10 +100,41 @@ authored browser entry or a TypeScript class. ## Run it + +npm +pnpm +Yarn +Bun + + ```bash npm run dev ``` + + + +```bash +pnpm run dev +``` + + + + +```bash +yarn run dev +``` + + + + +```bash +bun run dev +``` + + + + Open **http://127.0.0.1:3000/**. Below the greeting, the page displays **Clicks: 0**. It still renders with browser JavaScript disabled. diff --git a/docs/start/first-page.md b/docs/start/first-page.md index 8d6f0840e..5b9b4b1eb 100644 --- a/docs/start/first-page.md +++ b/docs/start/first-page.md @@ -70,12 +70,42 @@ so the same HTML can display different data. ## Previewing with `webui serve` + +npm +pnpm +Yarn +Bun + + ```bash npm run dev ``` -Use `pnpm run dev`, `yarn run dev`, or `bun run dev` if you installed with -that manager. Open **http://127.0.0.1:3000/**: + + + +```bash +pnpm run dev +``` + + + + +```bash +yarn run dev +``` + + + + +```bash +bun run dev +``` + + + + +Open **http://127.0.0.1:3000/**: ```text Hello, Ada! diff --git a/docs/start/installation.md b/docs/start/installation.md index e0de9e761..07cdefc7f 100644 --- a/docs/start/installation.md +++ b/docs/start/installation.md @@ -1,31 +1,23 @@ --- -description: Install the native WebUI CLI with npm, pnpm, Yarn, or Bun, approve package-specific setup scripts, and troubleshoot registry policies. +description: Install the WebUI CLI, verify your setup, and choose a backend integration when you are ready to deploy. --- # Installation -Set up the `hello-webui` project used by the -[getting-started lessons](/start/#start-here). At the end of this page, the -local `webui` command should display its help. You will create the page itself -in the next lesson. - -Use a current Node.js LTS release for the installation scripts and Node-based -tooling in the examples. Keep optional dependencies enabled: they carry the -native binary for your platform. Your package manager does not determine your -production rendering host. +Set up the `hello-webui` project for the [getting-started lessons](/start/). +You need a current Node.js LTS release for the installation scripts and tooling. +Your production backend can use any of the [deep integrations](#deep-integrations). ## Install the CLI -For the lessons, create a directory and work inside it: +Create a project directory: ```bash mkdir hello-webui cd hello-webui ``` -Create this file in your editor. If you are adding WebUI to an existing app, -use that app's directory and merge the scripts into its existing package file -instead. +Create this file, or merge the scripts into your existing package file: **`package.json`** @@ -41,89 +33,116 @@ instead. } ``` -Choose one package manager and keep its lockfile: +Choose a package manager. Your browser remembers the selection for the homepage +installer and package-manager tabs across these guides: -| Package manager | Install the compiler | -| --- | --- | -| npm | `npm install --save-dev @microsoft/webui` | -| pnpm | `pnpm add -D @microsoft/webui` | -| Yarn | `yarn add --dev @microsoft/webui` | -| Bun | `bun add --dev @microsoft/webui` | + +npm +pnpm +Yarn +Bun + + +```bash +npm install --save-dev @microsoft/webui +``` -**If setup scripts are blocked, complete the [package-specific approval -steps](#installation-policies) before continuing.** A downloaded package is -not necessarily a working CLI when its setup script was skipped. + + -Then verify the project-local command: +```bash +pnpm add -D @microsoft/webui +``` -| Package manager | Verify the CLI | -| --- | --- | -| npm | `npm exec --no -- webui --help` | -| pnpm | `pnpm exec webui --help` | -| Yarn | `yarn run webui --help` | -| Bun | `bun run webui --help` | + + -The npm `--no` option refuses an implicit package installation if the local -command is missing. Do not fetch an unrelated package named `webui` to repair -an incomplete `@microsoft/webui` installation. +```bash +yarn add --dev @microsoft/webui +``` -`@microsoft/webui` installs the native `webui` command for supported platforms. -It also exports a Node build/render API (`build`, `Protocol`) if you later -choose Node as your host. For an application that imports that API at runtime, -install it as a regular dependency rather than only a development dependency. + + - +```bash +bun add --dev @microsoft/webui +``` + + + + +Keep optional dependencies enabled: they provide native binaries for Windows, +macOS, and Linux on x64 and ARM64. A supported prebuilt install needs no Rust +toolchain. Commit your lockfile and keep WebUI package versions aligned. + +## Verify the CLI -### Next: render a page + +npm +pnpm +Yarn +Bun + -Continue with [Your first page](/start/first-page) in this same directory. -It creates the template and data files used by `npm run dev`. The lessons use -npm script commands; `pnpm run`, `yarn run`, and `bun run` accept the same -script names. +```bash +npm exec --no -- webui --help +``` -### Cross-Platform Support + + -Platform packages provide native binaries for Windows, macOS, and Linux on -x64 and ARM64. A supported prebuilt install does not need a Rust toolchain. -If the CLI is missing after installation, check for blocked setup scripts, -omitted optional dependencies, or an OS/architecture without a matching package. +```bash +pnpm exec webui --help +``` -Commit the package lockfile and keep compiler and browser-runtime versions -aligned. Do not substitute an unrelated globally installed CLI when diagnosing -a project-specific build problem. + + -### Desktop support (opt-in) +```bash +yarn run webui --help +``` -Desktop support is a separate install, not a requirement for a website: + + ```bash -npm install @microsoft/webui @microsoft/webui-desktop -npx webui desktop init ./my-app -npx webui desktop run ./my-app/src +bun run webui --help ``` -Use matching versions. The desktop package installs a native sidecar through -its optional platform dependencies; the base CLI does not include it. -See [Desktop Apps](/build/desktop) for platform requirements, host -capabilities, and packaging. + + + +The command should display its help. npm's `--no` refuses an implicit package +installation if the local command is missing. Do not install an unrelated +package named `webui` or a global CLI to repair this project. + + + +## Create your first page + +Continue with [Your first page](/start/first-page) in this directory. It creates +the HTML template and JSON data used by the `dev` script. No browser runtime +or production server integration is needed for that lesson. ## Installation policies -Downloading a package and permitting its install scripts are separate decisions. -`@microsoft/webui` uses a `postinstall` script to prepare the native CLI. -Review the installed package and your team's policy before approving it. -Do not enable all dependency scripts just to make one package work. +
+Setup scripts blocked? -Check your manager's version with `npm --version`, `pnpm --version`, -`yarn --version`, or `bun --version`. The commands below approve the published -`@microsoft/webui` package in a local project, not a global installation. -In a workspace, use the project root that owns the script-approval policy. +Downloading a package and permitting its install scripts are separate decisions. +`@microsoft/webui` uses `postinstall` to prepare the native CLI. Review the +package and your team's policy before approving it. In a workspace, use the +root that owns the script-approval policy. -### npm + +npm +pnpm +Yarn +Bun + **npm 12 and later** block unapproved dependency install scripts by default. -After installing, list pending scripts, approve the package you reviewed, -then run its setup: +List pending scripts, approve the package you reviewed, then run its setup: ```bash npm install-scripts ls @@ -132,21 +151,22 @@ npm rebuild @microsoft/webui ``` Approval updates `allowScripts` in `package.json`, pinned to the installed -version by default. Review the approval again after upgrading that version. -The rebuild runs the approved package's lifecycle scripts and links its command. +version by default. Review it again after upgrading. The rebuild runs the +approved lifecycle scripts and links the command. -**npm 11 and earlier** normally run dependency scripts unless disabled; they -do not provide `npm install-scripts`. An explicit `ignore-scripts` setting or -an existing denial still needs to be resolved through your project's policy. -Do not use `--dangerously-allow-all-scripts` as the default install command. +**npm 11 and earlier** normally run dependency scripts unless disabled and +do not provide `npm install-scripts`. Resolve an explicit `ignore-scripts` +setting or denial through your project's policy. Do not use +`--dangerously-allow-all-scripts` as the default install command. See [npm install-script approvals](https://docs.npmjs.com/cli/v12/commands/npm-install-scripts/) and [npm rebuild](https://docs.npmjs.com/cli/v12/commands/npm-rebuild/). -### pnpm + + **pnpm 12** requires a decision for dependency build scripts. An install can -exit with `ERR_PNPM_IGNORED_BUILDS` while packages are awaiting approval. +exit with `ERR_PNPM_IGNORED_BUILDS` while packages await approval. After reviewing the package: ```bash @@ -154,25 +174,24 @@ pnpm approve-builds @microsoft/webui pnpm install ``` -The named approval leaves other packages untouched and rebuilds this package -if it is awaiting approval. Rerunning the install completes any interrupted -installation. Decisions are recorded in `pnpm-workspace.yaml` under -`allowBuilds`, or under `permissions` when that policy is already configured -in pnpm 12.11 or later. +Named approval leaves other packages untouched and rebuilds this package if +it is awaiting approval. Rerunning the install completes an interrupted setup. +Decisions are recorded in `pnpm-workspace.yaml` under `allowBuilds`, or under +`permissions` when that policy is configured in pnpm 12.11 or later. For **pnpm 10.1 and later versions without named approvals**, run -`pnpm approve-builds` interactively and select only the packages you reviewed. -Do not use `--all` or turn off build-script enforcement to bypass the decision. +`pnpm approve-builds` interactively and select only reviewed packages. +Do not use `--all` or disable build-script enforcement. See [pnpm approve-builds](https://pnpm.io/cli/approve-builds) and the [pnpm 10 approval workflow](https://pnpm.io/10.x/cli/approve-builds). -### Yarn + + -**Modern Yarn (2 and later)** controls third-party setup scripts through -`enableScripts` and `dependenciesMeta`. If scripts are disabled, merge this -package-specific approval into the root `package.json`, preserving any existing -entries: +**Modern Yarn (2 and later)** controls setup scripts through `enableScripts` +and `dependenciesMeta`. If scripts are disabled, merge this approval into the +root `package.json`, preserving existing entries: ```json { @@ -191,188 +210,84 @@ yarn install yarn rebuild @microsoft/webui ``` -This allows the reviewed package without setting `enableScripts: true` for -every dependency. Do not override an intentional package denial without -reviewing it with the project owner. +This allows the reviewed package without enabling scripts for every dependency. +Do not override an intentional denial without reviewing it with the project owner. -**Yarn Classic (1.x)** normally runs dependency scripts unless disabled with -settings such as `ignore-scripts`. It does not use this modern Yarn approval -and rebuild workflow. +**Yarn Classic (1.x)** normally runs dependency scripts unless disabled. +It does not use this modern Yarn approval and rebuild workflow. See [Yarn script policy](https://yarnpkg.com/configuration/yarnrc#enableScripts), [per-package build permissions](https://yarnpkg.com/configuration/manifest#dependenciesMeta.built), and [yarn rebuild](https://yarnpkg.com/cli/rebuild). -### Bun + + -Bun runs dependency lifecycle scripts only for trusted packages. If it reports -that WebUI's script was blocked, inspect pending packages and trust only the -package you reviewed: +Bun runs lifecycle scripts only for trusted packages. Inspect pending packages +and trust only the package you reviewed: ```bash bun pm untrusted bun pm trust @microsoft/webui ``` -The trust command runs its pending scripts and records the package in -`trustedDependencies` in `package.json`. Review the resulting list: an explicit -`trustedDependencies` list replaces Bun's built-in default list rather than -extending it. Preserve other reviewed entries; do not use `bun pm trust --all`. +The trust command runs pending scripts and records the package in +`trustedDependencies` in `package.json`. An explicit list replaces Bun's +built-in default list, so preserve other reviewed entries. +Do not use `bun pm trust --all`. See [Bun lifecycle scripts](https://bun.sh/docs/pm/lifecycle) and [bun pm trust](https://bun.sh/docs/pm/cli/pm#trust). -### Other tools and CI + + -Bundlers such as `esbuild` may also need script approval. Review each reported -package separately and apply the matching manager's approval process; approving -`@microsoft/webui` does not approve every dependency. +Bundlers such as `esbuild` may need separate approval. Commit the reviewed +settings and lockfile for teammates and CI. Do not enable all dependency +scripts to make one package work. -Commit the resulting project approval settings and lockfile so teammates and -CI use the reviewed policy. An explicit script-disable setting can still -prevent setup; resolve that policy deliberately instead of adding a global -override. +
-### Download restrictions +
+Download, registry, or proxy errors? -Script approval does not fix a rejected download, authentication failure, or -proxy error. +Script approval does not fix a rejected download or authentication failure. +Check the exact error, manager version, configured registry, and tarball +hostname. Follow your organization's registry and proxy policy. In **npm 12**, `allow-remote` defaults to `none`, but tarballs on the configured -registry's hostname are still allowed. An ordinary install from that registry -does not require `--allow-remote=all`. Direct tarball URL dependencies and -registries that serve tarballs from a different hostname can require additional -configuration. - -If npm reports a remote-URL rejection, check the exact error, the configured -registry (including any `@microsoft` scope override), and the tarball hostname. -Use your organization's supported registry configuration or ask its registry -administrator to review the mismatch. Do not add `--allow-remote=all` to every -install command. See npm's [allow-remote](https://docs.npmjs.com/cli/v12/using-npm/config/#allow-remote) -and [replace-registry-host](https://docs.npmjs.com/cli/v12/using-npm/config/#replace-registry-host) -settings. - -The npm flag is not a shared option for pnpm, Yarn, or Bun. Those managers have -their own registry, proxy, offline, and trust settings. Diagnose the reported -download error separately from script approvals. When requesting help, include -the manager version, sanitized error, and registry/tarball hostnames, not tokens -or an unredacted configuration file. - -## Rust - -Building from source requires Rust 1.99.1 or newer. -Rust users can install the same compiler through Cargo: - -```bash -cargo install microsoft-webui-cli -webui build ./src --out ./dist -``` +registry's hostname are allowed. Direct URL dependencies or registries serving +tarballs from another hostname can need additional configuration. +Do not add `--allow-remote=all` to every install command. +See [allow-remote](https://docs.npmjs.com/cli/v12/using-npm/config/#allow-remote) +and [replace-registry-host](https://docs.npmjs.com/cli/v12/using-npm/config/#replace-registry-host). +This npm flag is not shared by pnpm, Yarn, or Bun. -This is an alternative to the npm compiler install, not another first-app step. -The [Rust integration guide](/build/integrations/rust) covers rendering in a -production Rust service. See the [CLI reference](/reference/cli/) for command options. +When requesting help, share a sanitized error and hostnames, not tokens or +an unredacted configuration file. -## .NET +
-The managed binding is `Microsoft.WebUI`. It loads compiled protocols and -uses platform-specific `Microsoft.WebUI.Runtime.*` native assets. +## Deep integrations -**Check package availability before choosing an install command.** The release -pipeline stages NuGet artifacts, but automatic NuGet.org publishing is not -enabled pending an approved signing path. Obtain the packages from an available -release or approved feed; do not assume a public restore will succeed. +Choose the backend that fits your application. These integrations all use +the native renderer; the package manager used above does not choose your host. +Each guide covers installation, protocol lifetime, and server integration. -The [.NET integration guide](/build/integrations/dotnet) covers supported -targets, protocol lifetime, and ASP.NET response handling. - -## Python - -The native `microsoft-webui` binding is **not published to PyPI yet**. Install -a matching wheel from a GitHub Release, or build from source with `maturin`: - -```bash -pip install ./microsoft_webui--cp311-abi3-.whl -``` - -Replace the placeholder with the wheel you downloaded for your platform. -The binding supports CPython 3.11+ and renders protocols; it does not compile -templates. Build `protocol.bin` with the npm or Cargo CLI first. -See [Python integration](/build/integrations/python) for installation details -and WSGI/ASGI examples. - -## WebUI Framework (Client-Side Interactivity) - -Install the browser runtime only when the app needs browser behavior or -browser-applied state: - -```bash -npm install @microsoft/webui-framework -``` - -It supplies `WebUIElement`, `@attr`, and `@observable`, and connects interactive -components to server-rendered HTML. It is separate from the compiler and server -binding. - -A component that only renders data can remain HTML and CSS. Do not add an -empty TypeScript class just to display bindings. When you do add one, configure -the decorators and browser bundler as shown in -[Add interaction](/start/add-interactivity#bundle-the-browser-entry). - -## Client-Side Router (Optional) - -Ordinary document navigation needs no client router. For soft navigation: - -```bash -npm install @microsoft/webui-router -``` - -The router is a separate browser package, with server-side route and partial -response requirements. It works with WebUI Framework and FAST 3.x components. -Follow [Routing](/build/routing) when the application needs it, not as -a prerequisite to the counter tutorial. - -## AI Coding Agents - -`@microsoft/webui` includes the version-matched `ai.md` authoring reference. -The `webui-reference` [agent skill](https://agentskills.io) tells supported -coding agents to read that file from the application's installed dependencies. - -After installing `@microsoft/webui`, install the loader: - -```bash -npx skills add microsoft/webui --skill webui-reference -``` - -For pnpm, use `pnpm dlx skills add microsoft/webui --skill webui-reference`; -for modern Yarn, use `yarn dlx skills add microsoft/webui --skill webui-reference`; -for Bun, use `bunx skills add microsoft/webui --skill webui-reference`. - -The loader lives in `.agents/skills/webui-reference/`, with agent-specific -installation locations where needed. Commit a project-local installation if -you want to share it with your team. - -| Option | Purpose | +| Backend | Guide | | --- | --- | -| `-g` | Install in the agent's user scope rather than this project | -| `-a ` | Target an agent, for example `-a github-copilot` | -| `-l` | List available skills without installing them | +| Rust | [Build and render with the Rust API](/build/integrations/rust) | +| Node.js and Bun | [Use the native addon](/build/integrations/node) | +| .NET | [Integrate with ASP.NET](/build/integrations/dotnet) | +| Python | [Integrate with WSGI or ASGI](/build/integrations/python) | +| C and other languages | [Use the C ABI](/build/integrations/ffi) | +| WebAssembly | [Render in a browser host](/build/integrations/wasm) | -Upgrading `@microsoft/webui` updates its reference; ask an active agent to reread -it. Rerun the install command only when you want to update the loader itself. -If you previously installed the full reference as a skill, reinstall in the -same scope to replace it with the loader. +## Add what your app needs -The loader resolves the package from the target app, including hoisted -dependencies, even when the skill is global. In the WebUI source checkout it -reads `docs/ai.md` instead. If a matching installed reference is missing, it -reports that rather than silently using a newer release's rules. - -To try the loader for one Copilot session: - -```bash -npx skills use microsoft/webui@webui-reference | copilot -``` + -You can also read the installed file yourself. The website's -[AI Reference](/ai) follows the current release and may be newer than your -application. +- [Browser interaction](/start/add-interactivity) adds the optional component runtime. +- [Routing](/build/routing) adds client-side navigation. +- [Desktop apps](/build/desktop) adds a native system-webview host. +- [AI authoring reference](/ai) explains the version-matched reference and agent skill. diff --git a/docs/start/todo-app.md b/docs/start/todo-app.md index d5af4a03d..bad3a97b0 100644 --- a/docs/start/todo-app.md +++ b/docs/start/todo-app.md @@ -43,11 +43,45 @@ cd todo-webui } ``` + +npm +pnpm +Yarn +Bun + + ```bash npm install @microsoft/webui-framework npm install --save-dev @microsoft/webui esbuild ``` + + + +```bash +pnpm add @microsoft/webui-framework +pnpm add -D @microsoft/webui esbuild +``` + + + + +```bash +yarn add @microsoft/webui-framework +yarn add --dev @microsoft/webui esbuild +``` + + + + +```bash +bun add @microsoft/webui-framework +bun add --dev @microsoft/webui esbuild +``` + + + + Keep WebUI packages on matching releases and keep optional dependencies enabled. If setup scripts for `@microsoft/webui` or `esbuild` are blocked, review and approve each package before building. See @@ -394,11 +428,45 @@ await build({ }); ``` + +npm +pnpm +Yarn +Bun + + ```bash npm run build npm run dev ``` + + + +```bash +pnpm run build +pnpm run dev +``` + + + + +```bash +yarn run build +yarn run dev +``` + + + + +```bash +bun run build +bun run dev +``` + + + + Open **http://127.0.0.1:3000/**. You should see three tasks and **Remaining tasks: 2**. Toggle "Write documentation" to see **Remaining tasks: 1**. Add a task to return to **2**, then delete that task to return to **1**. diff --git a/docs/start/tutorials.test.ts b/docs/start/tutorials.test.ts index 2a0b1d3c0..f6d652160 100644 --- a/docs/start/tutorials.test.ts +++ b/docs/start/tutorials.test.ts @@ -12,7 +12,7 @@ import type { TestContext } from 'node:test'; import { build as buildPress, fixture as pressFixture, removeFixtureRoot, unusedPort, write, } from '../../packages/webui-press/tests/native-fixture.js'; -import { docsDist } from '../../packages/webui-press/tests/website-fixture.js'; +import { docsDist, startWebsite } from '../../packages/webui-press/tests/website-fixture.js'; const docs = path.dirname(docsDist); const workspace = path.dirname(docs); @@ -109,6 +109,122 @@ test('installation scripts match the scriptless first app', () => { assert.equal(JSON.parse(interactive).private, JSON.parse(packageJson).private); }); +test('package-manager command tabs render and switch across the documentation', async t => { + const server = await startWebsite(); + t.after(() => server.close()); + const browser = await chromium.launch({ headless: true }); + t.after(() => browser.close()); + const page = await browser.newPage(); + const errors: string[] = []; + page.on('pageerror', error => errors.push(error.message)); + const pages = [ + ['start/installation', 3], + ['start/first-page', 1], + ['start/first-component', 1], + ['start/add-interactivity', 2], + ['start/todo-app', 2], + ['learn/how-it-works', 1], + ['build/desktop', 2], + ['build/hydration', 1], + ['build/routing', 1], + ['build/integrations/node', 1], + ['reference/cli', 3], + ['reference/webui-press', 1], + ] as const; + for (const [pathname, count] of pages) { + await page.goto(`${server.origin}/webui/${pathname}/`); + if (pathname === 'start/installation') { + const policies = page.locator('details').filter({ hasText: 'Setup scripts blocked?' }); + await expect(policies).not.toHaveAttribute('open', ''); + await policies.locator('summary').click(); + } + const groups = page.locator('webui-press-tabs').filter({ + has: page.locator('webui-press-tab', { hasText: /^npm$/ }), + }); + await expect(groups).toHaveCount(count); + await expect(page.locator('webui-press-tabs[sync="package-manager"][storage="local"]')).toHaveCount(count); + const restored = pathname === 'start/installation' ? 'npm' : 'bun'; + await expect(groups.locator(`:scope > webui-press-tab[value="${restored}"][active]`)) + .toHaveCount(count); + for (const group of await groups.all()) { + const tabs = group.locator(':scope > webui-press-tab'); + const panels = group.locator(':scope > webui-press-tab-panel'); + await expect(tabs).toHaveText(['npm', 'pnpm', 'Yarn', 'Bun']); + await expect(panels).toHaveCount(4); + const commands = [ + ['npm', /^(?:npm|npx)\b/], + ['pnpm', /^pnpm\b/], + ['Yarn', /^yarn\b/], + ['Bun', /^(?:bun|bunx)\b/], + ] as const; + for (const [index, [label, command]] of commands.entries()) { + await tabs.nth(index).getByRole('button', { name: label, exact: true }).click(); + await expect(groups.locator(`:scope > webui-press-tab[value="${label.toLowerCase()}"][active]`), + `${pathname}: selecting ${label} must synchronize every package-manager group`) + .toHaveCount(count); + await expect(panels.nth(index)).toBeVisible(); + await expect(panels.nth(index).locator('code-block').last().locator('code')).toContainText(command); + await expect(group.locator(':scope > webui-press-tab-panel[active]')).toHaveCount(1); + for (let other = 0; other < 4; other++) { + if (other !== index) await expect(panels.nth(other)).toBeHidden(); + } + } + } + } + assert.deepEqual(errors, []); +}); + +test('code-only tabs have no outer code-block gaps while standalone blocks retain their spacing', async t => { + const server = await startWebsite(); + t.after(() => server.close()); + const browser = await chromium.launch({ headless: true }); + t.after(() => browser.close()); + for (const javaScriptEnabled of [false, true]) { + for (const colorScheme of ['light', 'dark'] as const) { + const page = await browser.newPage({ javaScriptEnabled, colorScheme, viewport: { width: 390, height: 844 } }); + await page.goto(`${server.origin}/webui/start/installation/`); + const group = page.locator('webui-press-tabs').first(); + const pre = group.locator('webui-press-tab-panel[active] > code-block:only-child > pre'); + await expect(pre).toHaveCSS('margin-top', '0px'); + await expect(pre).toHaveCSS('margin-bottom', '0px'); + await expect(pre).toHaveCSS('border-radius', '0px'); + await expect(pre).toHaveCSS('padding-top', '16px'); + const gaps = await group.evaluate(element => { + const bar = element.shadowRoot?.querySelector('.tab-bar'); + const code = element.querySelector('webui-press-tab-panel[active] pre'); + if (!bar || !code) throw new Error('Missing tab geometry'); + return { + top: code.getBoundingClientRect().top - bar.getBoundingClientRect().bottom, + bottom: element.getBoundingClientRect().bottom - code.getBoundingClientRect().bottom, + }; + }); + assert.ok(Math.abs(gaps.top) < 0.1, 'code must meet the tab bar without an empty strip'); + assert.ok(Math.abs(gaps.bottom - 1) < 0.1, 'only the tab container border may follow the code'); + await expect(page.locator('.doc-content > code-block > pre').first()).toHaveCSS('margin-bottom', '16px'); + await page.close(); + } + } +}); + +test('installation keeps first-page setup separate from deep integrations', async t => { + const server = await startWebsite(); + t.after(() => server.close()); + const browser = await chromium.launch({ headless: true }); + t.after(() => browser.close()); + const page = await browser.newPage({ javaScriptEnabled: false }); + await page.goto(`${server.origin}/webui/start/installation/`); + const content = page.locator('.doc-content'); + await expect(content.getByRole('heading', { level: 2 })).toHaveText([ + 'Install the CLI #', 'Verify the CLI #', 'Create your first page #', + 'Installation policies #', 'Deep integrations #', 'Add what your app needs #', + ]); + for (const host of ['rust', 'node', 'dotnet', 'python', 'ffi', 'wasm']) { + await expect(content.locator(`a[href="/webui/build/integrations/${host}"]`)).toHaveCount(1); + } + await expect(content.locator('a[href="/webui/start/first-page"]')).toBeVisible(); + await expect(content.locator('details[open]')).toHaveCount(0); +}); + test('introductory page frontmatter builds with WebUI Press', t => { const f = pressFixture(); t.after(() => removeFixtureRoot(f.root)); diff --git a/packages/webui-press/README.md b/packages/webui-press/README.md index b52fa609c..9c93c4b68 100644 --- a/packages/webui-press/README.md +++ b/packages/webui-press/README.md @@ -646,6 +646,13 @@ Generated root and page entries also receive build-time ` { @@ -50,19 +51,14 @@ test('the entry page serves the install command and qualified fixed-case preview await expect(preview.locator('tbody tr[data-webui]')).toHaveAttribute('data-case', 'webui-rust-actix'); await expect(preview.locator('.chart-caption')).toContainText('Complete responses (req/s)'); await expect(preview.locator('.direction')).toHaveText('Higher is better'); - const notes = page.locator('.benchmark-notes'); - await expect(notes).toContainText('Official campaign results for a 100-todo app'); - await expect(notes).toContainText('1 measured round (20 s)'); - await expect(notes).toContainText('variability is not established'); - await expect(notes).toContainText('Some samples were unavailable'); - await expect(notes).toContainText('not a complete raw-run archive'); + await expect(page.locator('.benchmark-notes')).toHaveCount(0); await expect(preview.locator('.scale')).toContainText('1,741.15'); const widths = await preview.locator('.bar').evaluateAll(elements => elements.map(element => Number(element.getAttribute('width')))); for (const [index, expected] of [100, 28.173908049277774, 8.683915802774028].entries()) { assert.ok(Math.abs(widths[index] - expected) < 1e-10, 'SSR must preserve the full-view bar ratios.'); } - const fullData = preview.getByRole('link', { name: 'Full data and methodology', exact: true }); + const fullData = preview.getByRole('link', { name: 'More frameworks and methodology', exact: true }); await expect(fullData).toHaveAttribute('href', '/webui/reference/benchmarks/'); await expect(fullData).toBeVisible(); await expect(preview.locator('details')).toHaveCount(0); @@ -87,6 +83,12 @@ test('the entry page serves the install command and qualified fixed-case preview await expect(fullData).toHaveCSS('outline-style', 'solid'); await page.keyboard.press('Enter'); await expect(page).toHaveURL(`${server.origin}/webui/reference/benchmarks/`); + const notes = page.locator('.benchmark-notes'); + await expect(notes).toContainText('Official campaign results for a 100-todo app'); + await expect(notes).toContainText('1 measured round (20 s)'); + await expect(notes).toContainText('variability is not established'); + await expect(notes).toContainText('Some samples were unavailable'); + await expect(notes).toContainText('not a complete raw-run archive'); }); test('install choices copy exact commands and support native keyboard selection', async t => { @@ -123,6 +125,79 @@ test('install choices copy exact commands and support native keyboard selection' await expect(install.locator('code')).toHaveText(commands[1][1]); }); +test('homepage package-manager choice shares the documentation category across navigation', async t => { + const server = await startWebsite(); + t.after(() => server.close()); + const browser = await chromium.launch({ headless: true }); + t.after(() => browser.close()); + const page = await browser.newPage(); + await page.goto(`${server.origin}/webui/`); + const install = page.locator('install-command'); + await expect(install.getByRole('button', { name: 'Copy install command' })).toBeEnabled(); + await install.getByText('pnpm', { exact: true }).click(); + expect(await page.evaluate(() => localStorage.getItem('webui-press:tabs:package-manager'))).toBe('pnpm'); + await page.goto(`${server.origin}/webui/start/first-page/`); + const tabs = page.locator('webui-press-tabs[sync="package-manager"][storage="local"]'); + await expect(tabs.locator('webui-press-tab[value="pnpm"][active]')).toHaveCount(1); + await tabs.getByRole('button', { name: 'Bun', exact: true }).click(); + await page.goto(`${server.origin}/webui/`); + await expect(install.getByRole('radio', { name: 'Bun', exact: true })).toBeChecked(); + await expect(install.locator('code')).toHaveText('bun add -D @microsoft/webui'); + await page.reload(); + await expect(install.getByRole('radio', { name: 'Bun', exact: true })).toBeChecked(); + await install.getByText('Yarn', { exact: true }).click(); + await page.goto(`${server.origin}/webui/start/installation/`); + await expect(page.locator('webui-press-tabs[sync="package-manager"] > webui-press-tab[value="yarn"][active]')) + .toHaveCount(3); +}); + +test('homepage package-manager preferences tolerate stale values and storage failures', async t => { + const server = await startWebsite(); + t.after(() => server.close()); + const browser = await chromium.launch({ headless: true }); + t.after(() => browser.close()); + for (const failure of ['read', 'write', 'stale'] as const) { + const page = await browser.newPage(); + const errors: string[] = []; + const warnings: string[] = []; + page.on('pageerror', error => errors.push(error.message)); + page.on('console', message => { + if (message.type() === 'warning' && message.text().startsWith('[WebUI docs]')) { + warnings.push(message.text()); + } + }); + await page.addInitScript(mode => { + const key = 'webui-press:tabs:package-manager'; + if (mode === 'stale') { + localStorage.setItem(key, 'removed-manager'); + } else if (mode === 'read') { + const getItem = Storage.prototype.getItem; + Storage.prototype.getItem = function (name: string) { + if (name === key) throw new DOMException('Read blocked', 'SecurityError'); + return getItem.call(this, name); + }; + } else { + const setItem = Storage.prototype.setItem; + Storage.prototype.setItem = function (name: string, value: string) { + if (name === key) throw new DOMException('Storage full', 'QuotaExceededError'); + setItem.call(this, name, value); + }; + } + }, failure); + await page.goto(`${server.origin}/webui/`); + const install = page.locator('install-command'); + await expect(install.getByRole('button', { name: 'Copy install command' })).toBeEnabled(); + await expect(install.locator('code')).toHaveText(commands[0][1]); + await install.getByText('pnpm', { exact: true }).click(); + await expect(install.locator('code')).toHaveText(commands[1][1]); + await install.getByText('Yarn', { exact: true }).click(); + await expect(install.locator('code')).toHaveText(commands[2][1]); + expect(warnings).toHaveLength(failure === 'stale' ? 0 : 1); + expect(errors).toEqual([]); + await page.close(); + } +}); + test('clipboard denial and unavailable APIs show a manual-copy recovery instead of success', async t => { const server = await startWebsite(); t.after(() => server.close()); diff --git a/packages/webui-press/tests/tabs.test.ts b/packages/webui-press/tests/tabs.test.ts new file mode 100644 index 000000000..192d7318c --- /dev/null +++ b/packages/webui-press/tests/tabs.test.ts @@ -0,0 +1,211 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { chromium, expect } from '@playwright/test'; +import path from 'node:path'; +import test from 'node:test'; +import { build, fixture, removeFixtureRoot, write } from './native-fixture.js'; +import { startWebsite } from './website-fixture.js'; + +function tabs(id: string, sync: string, values: string[], active = 'npm', stored = false): string { + return ` +${values.map(value => + `${value}`).join('\n')} +${values.map(value => + `

${id}:${value}

`).join('\n')} +
`; +} + +test('synchronized tabs share page-local values across order, subsets and reconnects', async t => { + const f = fixture(); + t.after(() => removeFixtureRoot(f.root)); + write(f.configFile, JSON.stringify({ ...f.config, regions: {} })); + write(path.join(f.site, 'content/index.md'), '# Tab control fixtures\n'); + write(path.join(f.site, 'content/tabs.md'), `# Tab selection + +${tabs('first', 'package-manager', ['npm', 'pnpm', 'yarn', 'bun'])} +${tabs('reordered', 'package-manager', ['bun', 'yarn', 'pnpm', 'npm'])} +${tabs('subset', 'package-manager', ['npm', 'bun'])} +${tabs('other', 'another-group', ['npm', 'pnpm'])} +${tabs('independent', '', ['npm', 'pnpm'], 'pnpm')} +${tabs('outer', '', ['npm', 'pnpm']).replace( + '

outer:npm

', tabs('nested', 'another-group', ['npm', 'pnpm']))} +`); + build(f.site); + const server = await startWebsite(path.join(f.site, 'dist'), '/fixture/'); + t.after(() => server.close()); + const browser = await chromium.launch({ headless: true }); + t.after(() => browser.close()); + const noScript = await browser.newPage({ javaScriptEnabled: false }); + await noScript.goto(`${server.origin}/fixture/tabs/`); + await expect(noScript.locator('#first webui-press-tab-panel[active]')).toHaveText('first:npm'); + await expect(noScript.locator('#independent webui-press-tab-panel[active]')).toHaveText('independent:pnpm'); + await noScript.close(); + + const page = await browser.newPage(); + const errors: string[] = []; + page.on('pageerror', error => errors.push(error.message)); + page.on('console', message => { + if (message.type() === 'error') errors.push(message.text()); + }); + await page.goto(`${server.origin}/fixture/tabs/`); + const first = page.locator('#first'); + const reordered = page.locator('#reordered'); + const subset = page.locator('#subset'); + await first.getByRole('button', { name: 'pnpm', exact: true }).click(); + await expect(reordered.locator('webui-press-tab-panel[active]')).toHaveText('reordered:pnpm'); + await expect(subset.locator('webui-press-tab-panel[active]')).toHaveText('subset:npm'); + await expect(page.locator('#other webui-press-tab-panel[active]')).toHaveText('other:npm'); + await expect(page.locator('#independent webui-press-tab-panel[active]')).toHaveText('independent:pnpm'); + await reordered.getByRole('button', { name: 'bun', exact: true }).click(); + await expect(first.locator('webui-press-tab-panel[active]')).toHaveText('first:bun'); + await expect(subset.locator('webui-press-tab-panel[active]')).toHaveText('subset:bun'); + + const detached = await reordered.elementHandle(); + if (!detached) throw new Error('Missing reordered tab group'); + await detached.evaluate(element => element.remove()); + await first.getByRole('button', { name: 'yarn', exact: true }).click(); + expect(await detached.evaluate(element => + element.querySelector('webui-press-tab-panel[active]')?.textContent)).toBe('reordered:bun'); + await detached.evaluate(element => document.body.append(element)); + await expect(reordered.locator('webui-press-tab-panel[active]')).toHaveText('reordered:yarn'); + await reordered.getByRole('button', { name: 'npm', exact: true }).click(); + await expect(first.locator('webui-press-tab-panel[active]')).toHaveText('first:npm'); + await page.locator('#independent').getByRole('button', { name: 'pnpm', exact: true }).click(); + await expect(first.locator('webui-press-tab-panel[active]')).toHaveText('first:npm'); + await page.locator('#nested').getByRole('button', { name: 'pnpm', exact: true }).click(); + await expect(page.locator('#other webui-press-tab-panel[active]')).toHaveText('other:pnpm'); + await expect(page.locator('#outer > webui-press-tab[active]')).toHaveText('npm'); + await first.getByRole('button', { name: 'pnpm', exact: true }).click(); + + const secondPage = await browser.newPage(); + await secondPage.goto(`${server.origin}/fixture/tabs/`); + await expect(secondPage.locator('#first webui-press-tab-panel[active]')).toHaveText('first:npm'); + await expect(first.locator('webui-press-tab-panel[active]')).toHaveText('first:pnpm'); + await expect(page.locator('#independent webui-press-tab-panel[active]')).toHaveText('independent:pnpm'); + expect(errors).toEqual([]); +}); + +test('local tab preferences survive navigation and reloads without affecting other categories', async t => { + const f = fixture(); + t.after(() => removeFixtureRoot(f.root)); + write(f.configFile, JSON.stringify({ ...f.config, regions: {} })); + write(path.join(f.site, 'content/index.md'), '# Tab control fixtures\n'); + const content = `# Stored tab selection + +${tabs('first', 'package-manager', ['npm', 'pnpm'], 'npm', true)} +${tabs('peer', 'package-manager', ['pnpm', 'npm'], 'npm', true)} +${tabs('subset', 'package-manager', ['npm'], 'npm', true)} +${tabs('language', 'language', ['rust', 'python'], 'rust', true)} +${tabs('ephemeral', 'temporary', ['npm', 'pnpm'])} +${tabs('early', 'late-category', ['npm', 'pnpm'])} +${tabs('late', 'late-category', ['npm', 'pnpm'])} +`; + for (const name of ['tabs', 'next']) { + write(path.join(f.site, 'content', `${name}.md`), content); + } + build(f.site); + const server = await startWebsite(path.join(f.site, 'dist'), '/fixture/'); + t.after(() => server.close()); + const browser = await chromium.launch({ headless: true }); + t.after(() => browser.close()); + const context = await browser.newContext(); + const page = await context.newPage(); + const errors: string[] = []; + page.on('pageerror', error => errors.push(error.message)); + await page.goto(`${server.origin}/fixture/tabs/`); + await page.locator('#first').getByRole('button', { name: 'pnpm', exact: true }).click(); + await page.locator('#language').getByRole('button', { name: 'python', exact: true }).click(); + expect(await page.evaluate(() => localStorage.getItem('webui-press:tabs:package-manager'))).toBe('pnpm'); + expect(await page.evaluate(() => localStorage.getItem('webui-press:tabs:language'))).toBe('python'); + await page.evaluate(() => localStorage.setItem('webui-press:tabs:temporary', 'pnpm')); + await page.goto(`${server.origin}/fixture/next/`); + await expect(page.locator('#first webui-press-tab-panel[active]')).toHaveText('first:pnpm'); + await expect(page.locator('#peer webui-press-tab-panel[active]')).toHaveText('peer:pnpm'); + await expect(page.locator('#subset webui-press-tab-panel[active]')).toHaveText('subset:npm'); + await expect(page.locator('#language webui-press-tab-panel[active]')).toHaveText('language:python'); + await expect(page.locator('#ephemeral webui-press-tab-panel[active]')).toHaveText('ephemeral:npm'); + + const secondPage = await page.context().newPage(); + await secondPage.goto(`${server.origin}/fixture/tabs/`); + await expect(secondPage.locator('#first webui-press-tab-panel[active]')).toHaveText('first:pnpm'); + await secondPage.locator('#first').getByRole('button', { name: 'npm', exact: true }).click(); + await expect(page.locator('#first webui-press-tab-panel[active]')).toHaveText('first:pnpm'); + await page.reload(); + await expect(page.locator('#first webui-press-tab-panel[active]')).toHaveText('first:npm'); + + await page.evaluate(() => localStorage.setItem('webui-press:tabs:package-manager', 'removed-manager')); + await page.reload(); + await expect(page.locator('#first webui-press-tab-panel[active]')).toHaveText('first:npm'); + await page.locator('#first').getByRole('button', { name: 'pnpm', exact: true }).click(); + expect(await page.evaluate(() => localStorage.getItem('webui-press:tabs:package-manager'))).toBe('pnpm'); + + const late = await page.locator('#late').elementHandle(); + if (!late) throw new Error('Missing late storage group'); + await late.evaluate(element => { + element.remove(); + element.setAttribute('storage', 'local'); + }); + await page.evaluate(() => localStorage.setItem('webui-press:tabs:late-category', 'npm')); + await page.locator('#early').getByRole('button', { name: 'pnpm', exact: true }).click(); + await late.evaluate(element => document.body.append(element)); + await expect(page.locator('#early webui-press-tab-panel[active]')).toHaveText('early:pnpm'); + await expect(page.locator('#late webui-press-tab-panel[active]')).toHaveText('late:pnpm'); + expect(await page.evaluate(() => localStorage.getItem('webui-press:tabs:late-category'))).toBe('pnpm'); + expect(errors).toEqual([]); +}); + +test('tab storage access, read and quota failures retain page-local synchronization with a warning', async t => { + const f = fixture(); + t.after(() => removeFixtureRoot(f.root)); + write(f.configFile, JSON.stringify({ ...f.config, regions: {} })); + write(path.join(f.site, 'content/index.md'), `# Storage failures + +${tabs('first', 'package-manager', ['npm', 'pnpm'], 'npm', true)} +${tabs('peer', 'package-manager', ['pnpm', 'npm'], 'npm', true)} +`); + build(f.site, '--show=content'); + const server = await startWebsite(path.join(f.site, 'dist'), '/fixture/'); + t.after(() => server.close()); + const browser = await chromium.launch({ headless: true }); + t.after(() => browser.close()); + for (const failure of ['access', 'read', 'write'] as const) { + const page = await browser.newPage(); + const warnings: string[] = []; + const errors: string[] = []; + page.on('console', message => { + if (message.type() === 'warning' && message.text().startsWith('[WebUI Press]')) { + warnings.push(message.text()); + } + }); + page.on('pageerror', error => errors.push(error.message)); + await page.addInitScript(mode => { + if (mode === 'access') { + Object.defineProperty(window, 'localStorage', { + get() { throw new DOMException('Storage blocked', 'SecurityError'); }, + }); + } else if (mode === 'read') { + const getItem = Storage.prototype.getItem; + Storage.prototype.getItem = function (key: string) { + if (key.startsWith('webui-press:tabs:')) throw new DOMException('Read blocked', 'SecurityError'); + return getItem.call(this, key); + }; + } else { + const setItem = Storage.prototype.setItem; + Storage.prototype.setItem = function (key: string, value: string) { + if (key.startsWith('webui-press:tabs:')) throw new DOMException('Storage full', 'QuotaExceededError'); + setItem.call(this, key, value); + }; + } + }, failure); + await page.goto(`${server.origin}/fixture/`); + await page.locator('#first').getByRole('button', { name: 'pnpm', exact: true }).click(); + await expect(page.locator('#peer webui-press-tab-panel[active]')).toHaveText('peer:pnpm'); + await page.locator('#peer').getByRole('button', { name: 'npm', exact: true }).click(); + await expect(page.locator('#first webui-press-tab-panel[active]')).toHaveText('first:npm'); + expect(warnings).toHaveLength(1); + expect(warnings[0]).toContain(failure === 'write' ? 'Cannot save tab preferences' : 'Cannot restore tab preferences'); + expect(errors).toEqual([]); + await page.close(); + } +}); diff --git a/packages/webui-press/tests/website.test.ts b/packages/webui-press/tests/website.test.ts index 6f9a46631..71ede0d44 100644 --- a/packages/webui-press/tests/website.test.ts +++ b/packages/webui-press/tests/website.test.ts @@ -111,8 +111,11 @@ test('current runtime guidance keeps Node.js and Bun without Deno recommendation await page.goto(`${server.origin}/webui/build/integrations/node/`); await expect(page.getByRole('heading', { level: 1 })).toHaveText('Node.js and Bun #'); await expect(page.locator('#nodejs-bun-and-deno')).toHaveCount(1); - await expect(page.locator('webui-press-tab[slot="tab"]')).toHaveText(['Node.js', 'Bun']); - await expect(page.locator('webui-press-tab-panel')).toHaveCount(2); + const runtimeTabs = page.locator('webui-press-tabs').filter({ + has: page.locator('webui-press-tab', { hasText: 'Node.js' }), + }); + await expect(runtimeTabs.locator('webui-press-tab[slot="tab"]')).toHaveText(['Node.js', 'Bun']); + await expect(runtimeTabs.locator('webui-press-tab-panel')).toHaveCount(2); await expect(page.getByRole('link', { name: 'Node / Bun', exact: true })).toBeVisible(); await expect(page.locator('.doc-content')).not.toContainText(/deno/i); for (const pathname of ['build/integrations/', 'build/performance/']) {