diff --git a/dotnet/docs/api/class-page.mdx b/dotnet/docs/api/class-page.mdx
index 0075e64141..08e3980e78 100644
--- a/dotnet/docs/api/class-page.mdx
+++ b/dotnet/docs/api/class-page.mdx
@@ -1214,6 +1214,11 @@ Returns the main resource response. In case of multiple redirects, the navigatio
Navigate to the previous page in history.
+:::warning
+
+**Testing Back/Forward Cache (BFCache) is not supported.** By default, Playwright disables the Back/Forward Cache across all browsers. Even if explicitly enabled, Playwright's internal state relies on network-level navigation events. Because BFCache restores unfreeze the DOM without firing these events, using `page.goBack()` or `page.goForward()` to trigger a BFCache restore will result in timeouts and a desynchronized `Page` state.
+:::
+
**Usage**
```csharp
@@ -1246,6 +1251,11 @@ Returns the main resource response. In case of multiple redirects, the navigatio
Navigate to the next page in history.
+:::warning
+
+**Testing Back/Forward Cache (BFCache) is not supported.** By default, Playwright disables the Back/Forward Cache across all browsers. Even if explicitly enabled, Playwright's internal state relies on network-level navigation events. Because BFCache restores unfreeze the DOM without firing these events, using `page.goBack()` or `page.goForward()` to trigger a BFCache restore will result in timeouts and a desynchronized `Page` state.
+:::
+
**Usage**
```csharp
diff --git a/dotnet/docs/navigations.mdx b/dotnet/docs/navigations.mdx
index 0d44a3c7aa..829d099812 100644
--- a/dotnet/docs/navigations.mdx
+++ b/dotnet/docs/navigations.mdx
@@ -60,6 +60,16 @@ await page.GetByText("Click me").ClickAsync();
await page.WaitForURL("**/login");
```
+## Back/Forward Cache (BFCache)
+
+Modern browsers utilize a Back/Forward Cache (BFCache) to instantly load a page when a user navigates back or forward. This is achieved by freezing the page's DOM and JavaScript heap in memory, and thawing it upon return.
+
+By default, Playwright disables the BFCache across all browsers to ensure consistent, clean testing environments.
+
+Even if you explicitly enable BFCache, **testing BFCache restorations is not supported**. Because a BFCache restore skips the network fetch phase, the browser does not fire standard navigation lifecycle events (such as `commit`, `domcontentloaded`, or `load`). Playwright's internal `Page` state heavily relies on these network-level events to stay synchronized.
+
+Consequently, triggering a BFCache restore (e.g., via `page.goBack()`) will bypass Playwright's lifecycle tracking, resulting in timeouts and a completely desynchronized `Page` object where subsequent interactions will fail.
+
## Navigation events
Playwright splits the process of showing a new document in a page into **navigation** and **loading**.
diff --git a/java/docs/api/class-page.mdx b/java/docs/api/class-page.mdx
index 937d0287e3..d1ad77c016 100644
--- a/java/docs/api/class-page.mdx
+++ b/java/docs/api/class-page.mdx
@@ -1218,6 +1218,11 @@ Returns the main resource response. In case of multiple redirects, the navigatio
Navigate to the previous page in history.
+:::warning
+
+**Testing Back/Forward Cache (BFCache) is not supported.** By default, Playwright disables the Back/Forward Cache across all browsers. Even if explicitly enabled, Playwright's internal state relies on network-level navigation events. Because BFCache restores unfreeze the DOM without firing these events, using `page.goBack()` or `page.goForward()` to trigger a BFCache restore will result in timeouts and a desynchronized `Page` state.
+:::
+
**Usage**
```java
@@ -1251,6 +1256,11 @@ Returns the main resource response. In case of multiple redirects, the navigatio
Navigate to the next page in history.
+:::warning
+
+**Testing Back/Forward Cache (BFCache) is not supported.** By default, Playwright disables the Back/Forward Cache across all browsers. Even if explicitly enabled, Playwright's internal state relies on network-level navigation events. Because BFCache restores unfreeze the DOM without firing these events, using `page.goBack()` or `page.goForward()` to trigger a BFCache restore will result in timeouts and a desynchronized `Page` state.
+:::
+
**Usage**
```java
diff --git a/java/docs/navigations.mdx b/java/docs/navigations.mdx
index ca04efa537..5a7f3a90fd 100644
--- a/java/docs/navigations.mdx
+++ b/java/docs/navigations.mdx
@@ -60,6 +60,16 @@ page.getByText("Click me").click();
page.waitForURL("**/login");
```
+## Back/Forward Cache (BFCache)
+
+Modern browsers utilize a Back/Forward Cache (BFCache) to instantly load a page when a user navigates back or forward. This is achieved by freezing the page's DOM and JavaScript heap in memory, and thawing it upon return.
+
+By default, Playwright disables the BFCache across all browsers to ensure consistent, clean testing environments.
+
+Even if you explicitly enable BFCache, **testing BFCache restorations is not supported**. Because a BFCache restore skips the network fetch phase, the browser does not fire standard navigation lifecycle events (such as `commit`, `domcontentloaded`, or `load`). Playwright's internal `Page` state heavily relies on these network-level events to stay synchronized.
+
+Consequently, triggering a BFCache restore (e.g., via `page.goBack()`) will bypass Playwright's lifecycle tracking, resulting in timeouts and a completely desynchronized `Page` object where subsequent interactions will fail.
+
## Navigation events
Playwright splits the process of showing a new document in a page into **navigation** and **loading**.
diff --git a/nodejs/docs/api/class-browsercontext.mdx b/nodejs/docs/api/class-browsercontext.mdx
index bda17d41ce..b3c6646892 100644
--- a/nodejs/docs/api/class-browsercontext.mdx
+++ b/nodejs/docs/api/class-browsercontext.mdx
@@ -121,6 +121,10 @@ The order of evaluation of multiple scripts installed via [browserContext.addIni
- `arg` [Serializable] *(optional)*#
Optional argument to pass to [script](/api/class-browsercontext.mdx#browser-context-add-init-script-option-script) (only supported when passing a function).
+- `options` [Object] *(optional)*
+ - `exposeFunctions` [boolean] *(optional)* Added in: v1.62#
+
+ When set to `true`, functions passed inside [arg](/api/class-browsercontext.mdx#browser-context-add-init-script-option-arg) are exposed in the page and can be called from the init script. Calling one returns a [Promise] of its result. Under the hood, each function is exposed via [page.exposeFunction()](/api/class-page.mdx#page-expose-function), so it is technically accessible from all frames and worlds of the page. Unlike functions passed to [page.evaluate()](/api/class-page.mdx#page-evaluate), functions passed to an init script are exposed in every new document, so they survive navigations. Defaults to `false`, in which case functions are not serializable and are silently dropped.
**Returns**
- [Promise]<[Disposable]>#
diff --git a/nodejs/docs/api/class-cdpsession.mdx b/nodejs/docs/api/class-cdpsession.mdx
index 9ea3409d98..5f114a4883 100644
--- a/nodejs/docs/api/class-cdpsession.mdx
+++ b/nodejs/docs/api/class-cdpsession.mdx
@@ -100,8 +100,8 @@ Emitted for every CDP event received from the session. Allows subscribing to all
**Usage**
```js
-session.on('event', ({ name, params }) => {
- console.log(`CDP event: ${name}`, params);
+session.on('event', ({ method, params }) => {
+ console.log(`CDP event: ${method}`, params);
});
```
diff --git a/nodejs/docs/api/class-fixtures.mdx b/nodejs/docs/api/class-fixtures.mdx
index 2a73900a9d..85681cd365 100644
--- a/nodejs/docs/api/class-fixtures.mdx
+++ b/nodejs/docs/api/class-fixtures.mdx
@@ -24,6 +24,54 @@ Given the test above, Playwright Test will set up the `page` fixture before runn
Playwright Test comes with builtin fixtures listed below, and you can add your own fixtures as well. Playwright Test also [provides options][TestOptions] to configure [fixtures.browser](/api/class-fixtures.mdx#fixtures-browser), [fixtures.context](/api/class-fixtures.mdx#fixtures-context) and [fixtures.page](/api/class-fixtures.mdx#fixtures-page).
+---
+
+## Methods
+
+### mount {/* #fixtures-mount */}
+
+Added in: v1.62fixtures.mount
+
+Mounts a component story and returns a [Locator] pointing to the root element the story was rendered into. Scope your queries from the returned locator: `component.getByRole('button')`, not `page.getByRole('button')`.
+
+A **story** is a small wrapper component that embeds the component under test in one specific scenario: hard-coded props, mock data, providers, recorded callbacks. Stories are rendered by a **gallery** page that you implement and serve at [testOptions.baseURL](/api/class-testoptions.mdx#test-options-base-url). The gallery exposes `window.mount(params)` and `window.unmount()` functions that render a story into its root element. Each call to [fixtures.mount()](/api/class-fixtures.mdx#fixtures-mount) navigates to [testOptions.baseURL](/api/class-testoptions.mdx#test-options-base-url) and calls `window.mount()` with the story id and props, so tests are fully isolated from each other.
+
+**Usage**
+
+```js
+test('click should expand', async ({ mount }) => {
+ const component = await mount('components/Expandable/Stateful');
+ await component.getByRole('button').click();
+ await expect(component.getByTestId('expanded')).toHaveValue('true');
+});
+```
+
+Pass the story type as a template argument to type-check the props:
+
+```js
+import type { WithTitle } from './Button.story';
+
+test('renders the title', async ({ mount }) => {
+ const component = await mount('Button/WithTitle', { title: 'Hello' });
+ await expect(component).toContainText('Hello');
+});
+```
+
+The returned locator is augmented with two methods:
+* `update(props)` - re-renders the same story with new props without remounting, preserving component state;
+* `unmount()` - unmounts the story.
+
+**Arguments**
+- `storyId` [string]#
+
+ Identifier of the story to mount, as resolved by the gallery page. Conventionally, the story file path plus the exported story name, for example `'components/Button/Primary'`.
+- `props` [Object] *(optional)*#
+
+ Optional plain, serializable props passed to the story.
+
+**Returns**
+- [Locator]#
+
---
## Properties
diff --git a/nodejs/docs/api/class-frame.mdx b/nodejs/docs/api/class-frame.mdx
index ca029255b8..7484809317 100644
--- a/nodejs/docs/api/class-frame.mdx
+++ b/nodejs/docs/api/class-frame.mdx
@@ -253,7 +253,7 @@ await bodyHandle.dispose();
- `options` [Object] *(optional)*
- `exposeFunctions` [boolean] *(optional)* Added in: v1.62#
- When set to `true`, functions passed inside [arg](/api/class-frame.mdx#frame-evaluate-option-arg) are exposed in the page and can be called from the page function. Calling one returns a [Promise] of its result. The page-side functions are scoped to the execution context they were passed to and disappear when the page navigates. Defaults to `false`, in which case functions are not serializable and passing one throws an error, as before.
+ When set to `true`, functions passed inside [arg](/api/class-frame.mdx#frame-evaluate-option-arg) are exposed in the page and can be called from the page function. Calling one returns a [Promise] of its result. Under the hood, each function is exposed via [page.exposeFunction()](/api/class-page.mdx#page-expose-function), so it is technically accessible from all frames and worlds of the page. Exposed functions are cleared upon the top-level navigation. Defaults to `false`, in which case functions are not serializable and passing one throws an error.
**Returns**
- [Promise]<[Serializable]>#
@@ -304,7 +304,7 @@ await resultHandle.dispose();
- `options` [Object] *(optional)*
- `exposeFunctions` [boolean] *(optional)* Added in: v1.62#
- When set to `true`, functions passed inside [arg](/api/class-frame.mdx#frame-evaluate-handle-option-arg) are exposed in the page and can be called from the page function. Calling one returns a [Promise] of its result. The page-side functions are scoped to the execution context they were passed to and disappear when the page navigates. Defaults to `false`, in which case functions are not serializable and passing one throws an error, as before.
+ When set to `true`, functions passed inside [arg](/api/class-frame.mdx#frame-evaluate-handle-option-arg) are exposed in the page and can be called from the page function. Calling one returns a [Promise] of its result. Under the hood, each function is exposed via [page.exposeFunction()](/api/class-page.mdx#page-expose-function), so it is technically accessible from all frames and worlds of the page. Exposed functions are cleared upon the top-level navigation. Defaults to `false`, in which case functions are not serializable and passing one throws an error.
**Returns**
- [Promise]<[JSHandle]>#
diff --git a/nodejs/docs/api/class-jshandle.mdx b/nodejs/docs/api/class-jshandle.mdx
index abbff64d2b..2672c8dd61 100644
--- a/nodejs/docs/api/class-jshandle.mdx
+++ b/nodejs/docs/api/class-jshandle.mdx
@@ -84,7 +84,7 @@ expect(await tweetHandle.evaluate(node => node.innerText)).toBe('10 retweets');
- `options` [Object] *(optional)*
- `exposeFunctions` [boolean] *(optional)* Added in: v1.62#
- When set to `true`, functions passed inside [arg](/api/class-jshandle.mdx#js-handle-evaluate-option-arg) are exposed in the page and can be called from the page function. Calling one returns a [Promise] of its result. The page-side functions are scoped to the execution context they were passed to and disappear when the page navigates. Defaults to `false`, in which case functions are not serializable and passing one throws an error, as before.
+ When set to `true`, functions passed inside [arg](/api/class-jshandle.mdx#js-handle-evaluate-option-arg) are exposed in the page and can be called from the page function. Calling one returns a [Promise] of its result. Under the hood, each function is exposed via [page.exposeFunction()](/api/class-page.mdx#page-expose-function), so it is technically accessible from all frames and worlds of the page. Exposed functions are cleared upon the top-level navigation. Defaults to `false`, in which case functions are not serializable and passing one throws an error.
**Returns**
- [Promise]<[Serializable]>#
@@ -122,7 +122,7 @@ await jsHandle.evaluateHandle(pageFunction, arg, options);
- `options` [Object] *(optional)*
- `exposeFunctions` [boolean] *(optional)* Added in: v1.62#
- When set to `true`, functions passed inside [arg](/api/class-jshandle.mdx#js-handle-evaluate-handle-option-arg) are exposed in the page and can be called from the page function. Calling one returns a [Promise] of its result. The page-side functions are scoped to the execution context they were passed to and disappear when the page navigates. Defaults to `false`, in which case functions are not serializable and passing one throws an error, as before.
+ When set to `true`, functions passed inside [arg](/api/class-jshandle.mdx#js-handle-evaluate-handle-option-arg) are exposed in the page and can be called from the page function. Calling one returns a [Promise] of its result. Under the hood, each function is exposed via [page.exposeFunction()](/api/class-page.mdx#page-expose-function), so it is technically accessible from all frames and worlds of the page. Exposed functions are cleared upon the top-level navigation. Defaults to `false`, in which case functions are not serializable and passing one throws an error.
**Returns**
- [Promise]<[JSHandle]>#
diff --git a/nodejs/docs/api/class-locator.mdx b/nodejs/docs/api/class-locator.mdx
index 1252521b5e..b74c62456a 100644
--- a/nodejs/docs/api/class-locator.mdx
+++ b/nodejs/docs/api/class-locator.mdx
@@ -849,7 +849,7 @@ console.log(result); // prints "myId text 56"
- `options` [Object] *(optional)*
- `exposeFunctions` [boolean] *(optional)* Added in: v1.62#
- When set to `true`, functions passed inside [arg](/api/class-locator.mdx#locator-evaluate-option-arg) are exposed in the page and can be called from the page function. Calling one returns a [Promise] of its result. The page-side functions are scoped to the execution context they were passed to and disappear when the page navigates. Defaults to `false`, in which case functions are not serializable and passing one throws an error, as before.
+ When set to `true`, functions passed inside [arg](/api/class-locator.mdx#locator-evaluate-option-arg) are exposed in the page and can be called from the page function. Calling one returns a [Promise] of its result. Under the hood, each function is exposed via [page.exposeFunction()](/api/class-page.mdx#page-expose-function), so it is technically accessible from all frames and worlds of the page. Exposed functions are cleared upon the top-level navigation. Defaults to `false`, in which case functions are not serializable and passing one throws an error.
- `signal` [AbortSignal] *(optional)* Added in: v1.62#
Allows to cancel the operation using an [`AbortSignal`](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal). If the signal is aborted, the operation will be aborted and throw an error. Note that providing a signal does not disable the default timeout, which can be changed using [browserContext.setDefaultTimeout()](/api/class-browsercontext.mdx#browser-context-set-default-timeout) or [page.setDefaultTimeout()](/api/class-page.mdx#page-set-default-timeout); pass `timeout: 0` to disable the timeout entirely.
@@ -927,7 +927,7 @@ await locator.evaluateHandle(pageFunction, arg, options);
- `options` [Object] *(optional)*
- `exposeFunctions` [boolean] *(optional)* Added in: v1.62#
- When set to `true`, functions passed inside [arg](/api/class-locator.mdx#locator-evaluate-handle-option-arg) are exposed in the page and can be called from the page function. Calling one returns a [Promise] of its result. The page-side functions are scoped to the execution context they were passed to and disappear when the page navigates. Defaults to `false`, in which case functions are not serializable and passing one throws an error, as before.
+ When set to `true`, functions passed inside [arg](/api/class-locator.mdx#locator-evaluate-handle-option-arg) are exposed in the page and can be called from the page function. Calling one returns a [Promise] of its result. Under the hood, each function is exposed via [page.exposeFunction()](/api/class-page.mdx#page-expose-function), so it is technically accessible from all frames and worlds of the page. Exposed functions are cleared upon the top-level navigation. Defaults to `false`, in which case functions are not serializable and passing one throws an error.
- `signal` [AbortSignal] *(optional)* Added in: v1.62#
Allows to cancel the operation using an [`AbortSignal`](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal). If the signal is aborted, the operation will be aborted and throw an error. Note that providing a signal does not disable the default timeout, which can be changed using [browserContext.setDefaultTimeout()](/api/class-browsercontext.mdx#browser-context-set-default-timeout) or [page.setDefaultTimeout()](/api/class-page.mdx#page-set-default-timeout); pass `timeout: 0` to disable the timeout entirely.
diff --git a/nodejs/docs/api/class-page.mdx b/nodejs/docs/api/class-page.mdx
index 0c997395d8..9cff3523d8 100644
--- a/nodejs/docs/api/class-page.mdx
+++ b/nodejs/docs/api/class-page.mdx
@@ -95,6 +95,10 @@ The order of evaluation of multiple scripts installed via [browserContext.addIni
- `arg` [Serializable] *(optional)*#
Optional argument to pass to [script](/api/class-page.mdx#page-add-init-script-option-script) (only supported when passing a function).
+- `options` [Object] *(optional)*
+ - `exposeFunctions` [boolean] *(optional)* Added in: v1.62#
+
+ When set to `true`, functions passed inside [arg](/api/class-page.mdx#page-add-init-script-option-arg) are exposed in the page and can be called from the init script. Calling one returns a [Promise] of its result. Under the hood, each function is exposed via [page.exposeFunction()](/api/class-page.mdx#page-expose-function), so it is technically accessible from all frames and worlds of the page. Unlike functions passed to [page.evaluate()](/api/class-page.mdx#page-evaluate), functions passed to an init script are exposed in every new document, so they survive navigations. Defaults to `false`, in which case functions are not serializable and are silently dropped.
**Returns**
- [Promise]<[Disposable]>#
@@ -636,7 +640,7 @@ await bodyHandle.dispose();
- `options` [Object] *(optional)*
- `exposeFunctions` [boolean] *(optional)* Added in: v1.62#
- When set to `true`, functions passed inside [arg](/api/class-page.mdx#page-evaluate-option-arg) are exposed in the page and can be called from the page function. Calling one returns a [Promise] of its result. The page-side functions are scoped to the execution context they were passed to and disappear when the page navigates. Defaults to `false`, in which case functions are not serializable and passing one throws an error, as before.
+ When set to `true`, functions passed inside [arg](/api/class-page.mdx#page-evaluate-option-arg) are exposed in the page and can be called from the page function. Calling one returns a [Promise] of its result. Under the hood, each function is exposed via [page.exposeFunction()](/api/class-page.mdx#page-expose-function), so it is technically accessible from all frames and worlds of the page. Exposed functions are cleared upon the top-level navigation. Defaults to `false`, in which case functions are not serializable and passing one throws an error.
**Returns**
- [Promise]<[Serializable]>#
@@ -685,7 +689,7 @@ await resultHandle.dispose();
- `options` [Object] *(optional)*
- `exposeFunctions` [boolean] *(optional)* Added in: v1.62#
- When set to `true`, functions passed inside [arg](/api/class-page.mdx#page-evaluate-handle-option-arg) are exposed in the page and can be called from the page function. Calling one returns a [Promise] of its result. The page-side functions are scoped to the execution context they were passed to and disappear when the page navigates. Defaults to `false`, in which case functions are not serializable and passing one throws an error, as before.
+ When set to `true`, functions passed inside [arg](/api/class-page.mdx#page-evaluate-handle-option-arg) are exposed in the page and can be called from the page function. Calling one returns a [Promise] of its result. Under the hood, each function is exposed via [page.exposeFunction()](/api/class-page.mdx#page-expose-function), so it is technically accessible from all frames and worlds of the page. Exposed functions are cleared upon the top-level navigation. Defaults to `false`, in which case functions are not serializable and passing one throws an error.
**Returns**
- [Promise]<[JSHandle]>#
@@ -1218,6 +1222,11 @@ Returns the main resource response. In case of multiple redirects, the navigatio
Navigate to the previous page in history.
+:::warning
+
+**Testing Back/Forward Cache (BFCache) is not supported.** By default, Playwright disables the Back/Forward Cache across all browsers. Even if explicitly enabled, Playwright's internal state relies on network-level navigation events. Because BFCache restores unfreeze the DOM without firing these events, using `page.goBack()` or `page.goForward()` to trigger a BFCache restore will result in timeouts and a desynchronized `Page` state.
+:::
+
**Usage**
```js
@@ -1254,6 +1263,11 @@ Returns the main resource response. In case of multiple redirects, the navigatio
Navigate to the next page in history.
+:::warning
+
+**Testing Back/Forward Cache (BFCache) is not supported.** By default, Playwright disables the Back/Forward Cache across all browsers. Even if explicitly enabled, Playwright's internal state relies on network-level navigation events. Because BFCache restores unfreeze the DOM without firing these events, using `page.goBack()` or `page.goForward()` to trigger a BFCache restore will result in timeouts and a desynchronized `Page` state.
+:::
+
**Usage**
```js
diff --git a/nodejs/docs/api/class-testconfig.mdx b/nodejs/docs/api/class-testconfig.mdx
index 3c82e2bd99..9c37e27207 100644
--- a/nodejs/docs/api/class-testconfig.mdx
+++ b/nodejs/docs/api/class-testconfig.mdx
@@ -359,104 +359,6 @@ export default defineConfig({
---
-### httpCache {/* #test-config-http-cache */}
-
-Added in: v1.62testConfig.httpCache
-
-Records network responses to disk and replays them on later runs, so large static dependencies are downloaded from a remote server once instead of on every run. This is most useful against a slow or remote environment such as staging.
-
-When `httpCache` is set, Playwright starts a caching proxy for the run and routes all browser traffic through it. On the first run, eligible responses are recorded under `dir`; on subsequent runs they are served from disk without reaching the network. A single proxy is shared by all workers, so a resource fetched by one worker is a cache hit for the rest, and the cache persists across runs until you delete `dir`.
-
-Loopback traffic (`localhost`, `127.0.0.1`) is never cached — a local dev server already serves from disk, so there is nothing to optimize. The cache targets remote origins.
-
-**What is cached by default**
-
-With no `match`, the cache stores only **shared static assets**: successful `GET` requests the browser makes for a static subresource — a script, stylesheet, image, font, or media element — as reported by the request's `Sec-Fetch-Dest` metadata. These bytes do not depend on who is signed in, so replaying them into a fresh browser context is always safe, which keeps tests that each create their own context isolated by construction.
-
-The following are therefore **not** cached by default:
-* `fetch`/`XMLHttpRequest` (API) requests and top-level documents — their request destination is not a static subresource. This is the dynamic, per-user surface.
-* Any response marked `Cache-Control: no-store`.
-* Any response carrying a personalization signal: `Cache-Control: private`, a `Set-Cookie` header, or `Vary: Cookie`/`Vary: Authorization`.
-
-The `Authorization` and `Cookie` request headers are deliberately ignored when deciding what to cache. On a gated staging environment these are a shared environment credential attached to every request, not a per-user identity, so caching on their presence would be wrong.
-
-Freshness directives (`max-age`, `no-cache`, `Expires`) are ignored: once a response is recorded it is replayed until `dir` is deleted, keeping runs deterministic. `Vary` is honored — responses are keyed by the request-header values they vary on, and `Vary: *` is never stored.
-
-**Customizing with `match`**
-
-A string or [RegExp] restricts caching to requests whose URL matches; other requests pass straight through to the network. For full control, pass a callback that returns a decision object per request:
-* `disposition` — `'cache'` force-stores the response and serves it back, `'no-cache'` bypasses the cache entirely, and `'default'` (or an empty object) applies the rules above.
-* `identity` — a stable principal id (such as a session token) that partitions the cache. Entries recorded under one identity are never served to a request with a different one, so per-user content can be cached without leaking across contexts. The value is hashed into the cache key and never written to disk.
-
-Set `proxy` to fetch cache misses through an upstream proxy — for example, to reach a staging environment that is only accessible behind one. Browsers connect to the caching proxy, which chains to `proxy` for anything not served from disk.
-
-**Usage**
-
-Cache shared static assets from a staging server with zero configuration:
-
-```js title="playwright.config.ts"
-import { defineConfig } from '@playwright/test';
-
-export default defineConfig({
- httpCache: { dir: './.network-cache' },
-});
-```
-
-Fetch cache misses through an upstream proxy:
-
-```js title="playwright.config.ts"
-import { defineConfig } from '@playwright/test';
-
-export default defineConfig({
- httpCache: { dir: './.network-cache', proxy: { server: 'http://myproxy.com:3128' } },
-});
-```
-
-Take control per request — force-cache a per-user API response with session isolation, and bypass the cache for others:
-
-```js title="playwright.config.ts"
-import { defineConfig } from '@playwright/test';
-
-export default defineConfig({
- httpCache: {
- dir: './.network-cache',
- match: request => {
- if (request.url.includes('/api/config'))
- return { disposition: 'cache', identity: request.headers.get('authorization') };
- if (request.url.includes('/telemetry'))
- return { disposition: 'no-cache' };
- return {};
- },
- },
-});
-```
-
-**Type**
-- [Object]
- - `dir` [string]
-
- Directory where the cache is stored, resolved relative to the configuration file.
- - `match` [string] | [RegExp] | [HttpCachePolicy] *(optional)*
-
- Limits or customizes what is cached. A glob pattern or regular expression restricts caching to requests whose URL matches; a callback returns a per-request decision (see [HttpCachePolicy]). When omitted, every request is considered with the default behavior.
- - `proxy` [Object] *(optional)*
- - `server` [string]
-
- Proxy to be used for all requests. HTTP and SOCKS proxies are supported, for example `http://myproxy.com:3128` or `socks5://myproxy.com:3128`. Short form `myproxy.com:3128` is considered an HTTP proxy.
- - `bypass` [string] *(optional)*
-
- Optional comma-separated domains to bypass proxy.
- - `username` [string] *(optional)*
-
- Optional username to use if HTTP proxy requires authentication.
- - `password` [string] *(optional)*
-
- Optional password to use if HTTP proxy requires authentication.
-
- Upstream proxy for cache misses.
-
----
-
### ignoreSnapshots {/* #test-config-ignore-snapshots */}
Added in: v1.26testConfig.ignoreSnapshots
diff --git a/nodejs/docs/api/class-testoptions.mdx b/nodejs/docs/api/class-testoptions.mdx
index cec293d807..61917bfb7b 100644
--- a/nodejs/docs/api/class-testoptions.mdx
+++ b/nodejs/docs/api/class-testoptions.mdx
@@ -1133,6 +1133,55 @@ export default defineConfig({
page height in pixels.
+---
+
+## Deprecated
+
+### reuseContext {/* #test-options-reuse-context */}
+
+Added in: v1.62testOptions.reuseContext
+
+:::warning[Discouraged]
+
+This option trades test isolation for speed and is intended for component tests that drive a story gallery. Leave it unset for end-to-end tests - a fresh browser context per test is one of the core guarantees of Playwright Test.
+
+:::
+
+
+**Experimental.** When set to `true`, all tests in a worker process run in a single browser context that is reused between tests, instead of getting a brand new context per test. Defaults to `false`.
+
+Between tests, Playwright resets the state that component tests typically touch: it clears cookies, cache, local storage and IndexedDB of visited origins, unregisters service workers, closes extra pages, removes routes, bindings and init scripts, and re-applies the configured storage state, viewport and emulation options.
+
+This reset is best-effort, not a guarantee of isolation. State that is **not** reset includes:
+* Permissions granted with [browserContext.grantPermissions()](/api/class-browsercontext.mdx#browser-context-grant-permissions) during a test.
+* Runtime changes made through [browserContext.setGeolocation()](/api/class-browsercontext.mdx#browser-context-set-geolocation), [browserContext.setOffline()](/api/class-browsercontext.mdx#browser-context-set-offline) and [browserContext.setExtraHTTPHeaders()](/api/class-browsercontext.mdx#browser-context-set-extra-http-headers).
+* Browsing history, `window.name` and any browser-process-wide state.
+
+Additional restrictions:
+* The option is ignored when [testOptions.video](/api/class-testoptions.mdx#test-options-video) recording is enabled.
+* Only a few context options may differ between consecutive tests: `colorScheme`, `forcedColors`, `reducedMotion`, `contrast`, `screen`, `userAgent`, `viewport` and `testIdAttribute`. Changing any other option in [test.use()](/api/class-test.mdx#test-use), for example `locale` or `storageState`, silently forces a fresh context and negates the speedup.
+* Do not combine with [testOptions.connectOptions](/api/class-testoptions.mdx#test-options-connect-options) pointing multiple workers at a shared browser - workers would compete for the single reusable context.
+* `recordHar` in [testOptions.contextOptions](/api/class-testoptions.mdx#test-options-context-options) is not supported and produces no HAR file.
+
+**Usage**
+
+```js title="playwright.config.ts"
+import { defineConfig } from '@playwright/test';
+
+export default defineConfig({
+ projects: [
+ {
+ name: 'components',
+ testDir: './tests/components',
+ use: { reuseContext: true },
+ },
+ ],
+});
+```
+
+**Type**
+- [boolean]
+
[APIRequest]: /api/class-apirequest.mdx "APIRequest"
[APIRequestContext]: /api/class-apirequestcontext.mdx "APIRequestContext"
diff --git a/nodejs/docs/navigations.mdx b/nodejs/docs/navigations.mdx
index e168544c6d..99d58c60fe 100644
--- a/nodejs/docs/navigations.mdx
+++ b/nodejs/docs/navigations.mdx
@@ -60,6 +60,16 @@ await page.getByText('Click me').click();
await page.waitForURL('**/login');
```
+## Back/Forward Cache (BFCache)
+
+Modern browsers utilize a Back/Forward Cache (BFCache) to instantly load a page when a user navigates back or forward. This is achieved by freezing the page's DOM and JavaScript heap in memory, and thawing it upon return.
+
+By default, Playwright disables the BFCache across all browsers to ensure consistent, clean testing environments.
+
+Even if you explicitly enable BFCache, **testing BFCache restorations is not supported**. Because a BFCache restore skips the network fetch phase, the browser does not fire standard navigation lifecycle events (such as `commit`, `domcontentloaded`, or `load`). Playwright's internal `Page` state heavily relies on these network-level events to stay synchronized.
+
+Consequently, triggering a BFCache restore (e.g., via `page.goBack()`) will bypass Playwright's lifecycle tracking, resulting in timeouts and a completely desynchronized `Page` object where subsequent interactions will fail.
+
## Navigation events
Playwright splits the process of showing a new document in a page into **navigation** and **loading**.
diff --git a/nodejs/docs/test-components.mdx b/nodejs/docs/test-components.mdx
index ccbf5ac10b..1f80d1a2c1 100644
--- a/nodejs/docs/test-components.mdx
+++ b/nodejs/docs/test-components.mdx
@@ -1,399 +1,195 @@
---
id: test-components
-title: "Components (experimental)"
+title: "Component testing"
---
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
import HTMLCard from '@site/src/components/HTMLCard';
-import LiteYouTube from '@site/src/components/LiteYouTube';
-
## Introduction
-Playwright Test can now test your components.
-
-
-
-## Example
-
-Here is what a typical component test looks like:
+Playwright Test can test the components of your web application in isolation. A component test is a regular Playwright end-to-end test that runs against a small **story gallery** page served by your own dev server. There is no dedicated component-testing runtime, no bundler integration and no extra npm packages — the built-in [fixtures.mount()](/api/class-fixtures.mdx#fixtures-mount) fixture of `@playwright/test` drives it all.
```js
-test('event should work', async ({ mount }) => {
- let clicked = false;
-
- // Mount a component. Returns locator pointing to the component.
- const component = await mount(
-
- );
+import { test, expect } from '@playwright/test';
- // As with any Playwright test, assert locator text.
- await expect(component).toContainText('Submit');
-
- // Perform locator click. This will trigger the event.
- await component.click();
-
- // Assert that respective events have been fired.
- expect(clicked).toBeTruthy();
+test('click should expand', async ({ mount }) => {
+ const component = await mount('components/Expandable/Stateful');
+ await component.getByRole('button').click();
+ await expect(component.getByTestId('expanded')).toHaveValue('true');
});
```
-## How to get started
-
-Adding Playwright Test to an existing project is easy. Below are the steps to enable Playwright Test for a React or Vue project.
-
-### Step 1: Install Playwright Test for components for your respective framework
-
-
-
-
-
-```bash
-npm init playwright@latest -- --ct
-```
-
-
-
-
-
-```bash
-yarn create playwright --ct
-```
-
-
+Tests run in Node.js while components run in a real browser: real clicks are triggered, real layout is executed, visual regression is possible. At the same time, tests get everything Playwright Test offers: parallelism, parametrization, retries and post-mortem tracing.
-
+:::note
-```bash
-pnpm create playwright --ct
-```
+This guide replaces the experimental `@playwright/experimental-ct-react` and `@playwright/experimental-ct-vue` packages. If you are using them today, see the [migration guide](#migration-from-the-experimental-packages) below.
+:::
-
+## Why a framework-agnostic approach
-
+The `@playwright/experimental-ct-*` packages let tests write JSX inline — `mount()`. To make that possible, Playwright had to control the entire pipeline: scan the tests for components, compile a bundle with its own copy of Vite and its own config, serve it from its own server, and marshal props and callbacks across the Node.js/browser boundary.
-This step creates several files in your workspace:
+That design kept the packages experimental forever:
+- **It only worked when your setup matched ours.** Path aliases, plugins and CSS handling had to be mirrored into `ctViteConfig` by hand. Projects on webpack, Next.js or custom pipelines could not use their own build at all. Every framework needed its own package with its own runtime glue, and every new framework meant yet another package.
+- **The Node.js/browser boundary leaked.** JSX written in a test was compiled in Node.js and reassembled in the browser. Live objects could not cross, callbacks only half-worked through marshalling, and module mocks silently did not apply.
-```html title="playwright/index.html"
-
-
-
-
-
-
-```
-
-This file defines an html file that will be used to render components during testing. It must contain element with `id="root"`, that's where components are mounted. It must also link the script called `playwright/index.{js,ts,jsx,tsx}`.
+The replacement inverts the control:
+- **You own the pipeline.** Components are built and served by your own dev server, with your plugins, your aliases and your CSS. Playwright does not compile or serve anything — it navigates to a page, like in any other test.
+- **It is framework-agnostic.** The only framework-specific piece is the gallery page — a small module that you own. React, Vue, Svelte, Solid or anything else: if your dev server can render it, Playwright can test it.
+- **It is stable.** Tests import `test` and `expect` from plain `@playwright/test`, and [fixtures.mount()](/api/class-fixtures.mdx#fixtures-mount) is a documented built-in fixture. There is no experimental package to depend on and no separate config dialect.
-You can include stylesheets, apply theme and inject code into the page where component is mounted using this script. It can be either a `.js`, `.ts`, `.jsx` or `.tsx` file.
+## How it works
-```js title="playwright/index.ts"
-// Apply theme here, add anything your component needs at runtime here.
-```
+Three concepts make up the whole model:
+- A **story** is a tiny wrapper component that embeds the component under test in one specific scenario: hard-coded props, mock data, providers, recorded callbacks. Stories live next to the component in `*.story.tsx` (or `.ts`/`.jsx`/`.js`/`.vue`) files; each named export is one story.
+- The **gallery** is a single page, served by your dev server, that exposes `window.mount(params)` and `window.unmount()` functions rendering a story — resolved from your story files — into a `#root` element. It is framework-specific and yours to own.
+- The [fixtures.mount()](/api/class-fixtures.mdx#fixtures-mount) fixture navigates to the gallery ([testOptions.baseURL](/api/class-testoptions.mdx#test-options-base-url)), calls `window.mount()` with the story id and props, and returns a [Locator] for the gallery root. Scope your queries from it: `component.getByRole('button').click()`.
-### Step 2. Create a test file `src/App.spec.{ts,tsx}`
+Everything the component needs is set up *inside the story*, which runs in the browser. Everything the test asserts is observable *through the page*: DOM, URL, network.
-
+## Getting started
-
+### Step 1: Point your coding agent at the skill
-```js title="app.spec.tsx"
-import { test, expect } from '@playwright/experimental-ct-react';
-import App from './App';
+The gallery is application code — it belongs to you, not to Playwright. The fastest way to get one is to not write it yourself: Playwright ships this entire methodology as an agent skill. Install the skills and ask your coding agent (Claude Code, GitHub Copilot or similar) to do the setup:
-test('should work', async ({ mount }) => {
- const component = await mount();
- await expect(component).toContainText('Learn React');
-});
-```
-
-
-
-
-
-```js title="app.spec.ts"
-import { test, expect } from '@playwright/experimental-ct-vue';
-import App from './App.vue';
-
-test('should work', async ({ mount }) => {
- const component = await mount(App);
- await expect(component).toContainText('Learn Vue');
-});
-```
-
-```js title="app.spec.tsx"
-import { test, expect } from '@playwright/experimental-ct-vue';
-import App from './App.vue';
-
-test('should work', async ({ mount }) => {
- const component = await mount();
- await expect(component).toContainText('Learn Vue');
-});
-```
-
-If using TypeScript and Vue make sure to add a `vue.d.ts` file to your project:
-
-```js
-declare module '*.vue';
-```
-
-
-
-
-
-### Step 3. Run the tests
-
-You can run tests using the [VS Code extension](./getting-started-vscode.mdx) or the command line.
-
-```sh
-npm run test-ct
-```
-
-### Further reading: configure reporting, browsers, tracing
-
-Refer to [Playwright config](./test-configuration.mdx) for configuring your project.
-
-## Test stories
-
-When Playwright Test is used to test web components, tests run in Node.js, while components run in the real browser. This brings together the best of both worlds: components run in the real browser environment, real clicks are triggered, real layout is executed, visual regression is possible. At the same time, test can use all the powers of Node.js as well as all the Playwright Test features. As a result, the same parallel, parametrized tests with the same post-mortem Tracing story are available during component testing.
-
-This however, is introducing a number of limitations:
-- You can't pass complex live objects to your component. Only plain JavaScript objects and built-in types like strings, numbers, dates etc. can be passed.
-
-```js
-test('this will work', async ({ mount }) => {
- const component = await mount();
-});
-
-test('this will not work', async ({ mount }) => {
- // `process` is a Node object, we can't pass it to the browser and expect it to work.
- const component = await mount();
-});
-```
-
-- You can't pass data to your component synchronously in a callback:
-
-```js
-test('this will not work', async ({ mount }) => {
- // () => 'red' callback lives in Node. If `ColorPicker` component in the browser calls the parameter function
- // `colorGetter` it won't get result synchronously. It'll be able to get it via await, but that is not how
- // components are typically built.
- const component = await mount( 'red'}/>);
-});
-```
-
-Working around these and other limitations is quick and elegant: for every use case of the tested component, create a wrapper of this component designed specifically for test. Not only it will mitigate the limitations, but it will also offer powerful abstractions for testing where you would be able to define environment, theme and other aspects of your component rendering.
-
-Let's say you'd like to test following component:
-
-```js title="input-media.tsx"
-import React from 'react';
-
-type InputMediaProps = {
- // Media is a complex browser object we can't send to Node while testing.
- onChange(media: Media): void;
-};
-
-export function InputMedia(props: InputMediaProps) {
- return <>> as any;
-}
-```
-
-Create a story file for your component:
-
-```js title="input-media.story.tsx"
-import React from 'react';
-import InputMedia from './import-media';
-
-type InputMediaForTestProps = {
- onMediaChange(mediaName: string): void;
-};
-
-export function InputMediaForTest(props: InputMediaForTestProps) {
- // Instead of sending a complex `media` object to the test, send the media name.
- return props.onMediaChange(media.name)} />;
-}
-// Export more stories here.
+```bash
+npx playwright init-skills
```
-Then test the component via testing the story:
-
-```js title="input-media.spec.tsx"
-import { test, expect } from '@playwright/experimental-ct-react';
-import { InputMediaForTest } from './input-media.story.tsx';
-
-test('changes the image', async ({ mount }) => {
- let mediaSelected: string | null = null;
-
- const component = await mount(
- {
- mediaSelected = mediaName;
- }}
- />
- );
- await component
- .getByTestId('imageInput')
- .setInputFiles('src/assets/logo.png');
-
- await expect(component.getByAltText(/selected image/i)).toBeVisible();
- await expect.poll(() => mediaSelected).toBe('logo.png');
-});
+```txt
+Set up component testing using the playwright-component-testing skill.
```
-As a result, for every component you'll have a story file that exports all the stories that are actually tested. These stories live in the browser and "convert" complex object into the simple objects that can be accessed in the test.
-
-## Under the hood
+The agent detects your framework and bundler, implements the gallery for your stack, adds a Playwright project to the config, and writes the first story and spec.
-Here is how component testing works:
-- Once the tests are executed, Playwright creates a list of components that the tests need.
-- It then compiles a bundle that includes these components and serves it using a local static web server.
-- Upon the `mount` call within the test, Playwright navigates to the facade page `/playwright/index.html` of this bundle and tells it to render the component.
-- Events are marshalled back to the Node.js environment to allow verification.
+The contract the gallery fulfills is small and worth knowing, even if you never open the file:
+- It is a single page under `playwright/gallery/`, served by **your own dev server** — Vite apps serve it with the dev server they already run; other setups run a small standalone Vite server next to the app.
+- It discovers your `*.story.*` files and exposes two functions: `window.mount({ story, props })` renders the story with the given id into a `#root` element, and `window.unmount()` tears it down. An unknown story or a render error rejects, which surfaces as the test's `mount()` call throwing.
+- It reuses the rendering root across calls, so `component.update(props)` reconciles instead of remounting and component state is preserved.
+- It imports your global CSS the same way the app entry does, and the body of `window.mount` is the natural place for app-wide setup — the equivalent of the old `beforeMount`/`afterMount` hooks.
-Playwright is using [Vite](https://vitejs.dev/) to create the components bundle and serve it.
+If you prefer to write the gallery by hand, the installed skill contains the full specification with worked React and Vue examples in `references/gallery-spec.md` — the whole page is a few dozen lines.
-## Best practices and pitfalls
+### Step 2: Configure Playwright
-Component tests are most reliable when they embrace the fact that the test runs in Node.js while the mounted component runs in the browser.
+Add a project to your `playwright.config.ts` and point `baseURL` at the gallery:
-### Prefer mounting inside each test
+```js title="playwright.config.ts"
+import { defineConfig, devices } from '@playwright/test';
-Keep `mount()` close to the assertions that use it. Mounting in `beforeEach` makes it harder to see which component state belongs to which test and tends to hide accidental coupling between tests.
-
-```js
-test('renders the product name', async ({ mount }) => {
- const component = await mount();
- await expect(component).toContainText('Playwright');
+export default defineConfig({
+ projects: [
+ {
+ name: 'components',
+ testDir: './tests/components',
+ use: {
+ ...devices['Desktop Chrome'],
+ baseURL: 'http://localhost:5173/playwright/gallery/index.html',
+ serviceWorkers: 'block',
+ reuseContext: true,
+ },
+ },
+ ],
+ webServer: {
+ command: 'npm run dev',
+ url: 'http://localhost:5173/playwright/gallery/index.html',
+ reuseExistingServer: !process.env.CI,
+ },
});
```
-### Module mocks do not cross the Node/browser boundary
-
-Module-level mocks such as `vi.mock()` or `jest.mock()` run in the test process. The component bundle runs in the browser, so those mocks do not automatically affect what the component imports at runtime. Prefer passing test-specific behavior through [`hooksConfig`](#hooks) and configuring it in `playwright/index.{js,ts,jsx,tsx}` with `beforeMount`.
-
-### Reset browser state when a component depends on globals
+`mount` navigates to `baseURL`, so it must point at the gallery. `serviceWorkers: 'block'` keeps the app's own service worker from serving cached responses that would shadow your `page.route()` mocks. `reuseContext: true` reuses the browser context between tests in a worker — a large speedup for component suites, and the same optimization the experimental packages applied implicitly.
-Component testing may reuse the browser `context` and `page` between tests as a performance optimization. If a component depends on global browser state such as `localStorage`, cookies, singleton services, or router state, reset that state in your test setup or in [`beforeMount`](#hooks) so each test starts from a known baseline.
+### Step 3: Write a story
-## API reference
+Stories live next to the component they exercise. Each named export is one scenario:
-### props
+```js title="src/components/Button.story.tsx"
+import { Button } from './Button';
-Provide props to a component when mounted.
+export const Primary = () => ;
-
-
-
-
-```js title="component.spec.tsx"
-import { test } from '@playwright/experimental-ct-react';
-
-test('props', async ({ mount }) => {
- const component = await mount();
-});
+export const Disabled = () => ;
```
-
-
-
+### Step 4: Write a test
-```js title="component.spec.ts"
-import { test } from '@playwright/experimental-ct-vue';
+```js title="tests/components/button.spec.ts"
+import { test, expect } from '@playwright/test';
-test('props', async ({ mount }) => {
- const component = await mount(Component, { props: { msg: 'greetings' } });
+test('renders primary button', async ({ mount }) => {
+ const component = await mount('components/Button/Primary');
+ await expect(component.getByRole('button')).toHaveText('Submit');
});
-```
-
-```js title="component.spec.tsx"
-// Or alternatively, using the `jsx` style
-import { test } from '@playwright/experimental-ct-vue';
-test('props', async ({ mount }) => {
- const component = await mount();
+test('disabled button is disabled', async ({ mount }) => {
+ const component = await mount('components/Button/Disabled');
+ await expect(component.getByRole('button')).toBeDisabled();
});
```
-
-
-
-
-### callbacks / events
+### Step 5: Run
-Provide callbacks/events to a component when mounted.
-
-
-
-
-
-```js title="component.spec.tsx"
-import { test } from '@playwright/experimental-ct-react';
-
-test('callback', async ({ mount }) => {
- const component = await mount( {}} />);
-});
-```
-
-
-
-
-
-```js title="component.spec.ts"
-import { test } from '@playwright/experimental-ct-vue';
-
-test('event', async ({ mount }) => {
- const component = await mount(Component, { on: { click() {} } });
-});
+```sh
+npx playwright test --project=components
```
-```js title="component.spec.tsx"
-// Or alternatively, using the `jsx` style
-import { test } from '@playwright/experimental-ct-vue';
+## Stories as a methodology
-test('event', async ({ mount }) => {
- const component = await mount( {}} />);
-});
-```
+Stories are not just a testing workaround — they are greppable, reviewable documentation of your component states, and the conventions keep them that way:
+- **One export per scenario.** Prefer a new story export over parameterizing an existing one. `Button.story.tsx` exporting `Primary`, `Disabled`, `WithLongTitle` reads as a specification of the component.
+- **Stories live next to the component.** `src/components/Button.story.tsx` documents `src/components/Button.tsx`. Renames and refactors touch both together.
+- **Story ids are derived from the file path**: path under `src/` without the `.story.*` extension, plus the export name — `components/Button/Primary`. Any unique suffix works too: `mount('Button/Primary')`.
+- **The story owns everything the component needs**: providers, mock data, state, callbacks. The test owns nothing but interactions and assertions.
-
+Because every story is a named, addressable page state, the gallery doubles as a living catalog: open the gallery URL in a browser and render any story to inspect it by eye.
-
+## Testing patterns
-### children / slots
+### Record state for assertions
-Provide children/slots to a component when mounted.
+Components take callbacks; tests want to assert they fired. Instead of marshalling callbacks between Node.js and the browser, **the story owns the state and provides the callbacks** — and records the observable outcome into a hidden form next to the component:
-```js title="component.spec.tsx"
-import { test } from '@playwright/experimental-ct-react';
+```js title="src/components/Expandable.story.tsx"
+import { useState } from 'react';
+import { Expandable } from './Expandable';
-test('children', async ({ mount }) => {
- const component = await mount(Child);
-});
+export const Stateful = () => {
+ const [expanded, setExpanded] = useState(false);
+ return <>
+ Details
+
+ >;
+};
```
-```js title="component.spec.ts"
-import { test } from '@playwright/experimental-ct-vue';
+```js title="src/components/Expandable.story.ts"
+import { defineComponent, h, ref } from 'vue';
+import Expandable from './Expandable.vue';
-test('slot', async ({ mount }) => {
- const component = await mount(Component, { slots: { default: 'Slot' } });
-});
-```
-
-```js title="component.spec.tsx"
-// Or alternatively, using the `jsx` style
-import { test } from '@playwright/experimental-ct-vue';
-
-test('children', async ({ mount }) => {
- const component = await mount(Child);
+export const Stateful = defineComponent(() => {
+ const expanded = ref(false);
+ return () => h('div', [
+ h(Expandable, {
+ 'expanded': expanded.value,
+ 'onUpdate:expanded': (value: boolean) => expanded.value = value,
+ 'title': 'Title',
+ }),
+ h('form', { hidden: true }, [
+ h('input', { 'data-testid': 'expanded', 'readonly': true, 'value': String(expanded.value) }),
+ ]),
+ ]);
});
```
@@ -401,384 +197,153 @@ test('children', async ({ mount }) => {
-### hooks
-
-You can use `beforeMount` and `afterMount` hooks to configure your app. This lets you set up things like your app router, fake server etc. giving you the flexibility you need. You can also pass custom configuration from the `mount` call from a test, which is accessible from the `hooksConfig` fixture. This includes any config that needs to be run before or after mounting the component. An example of configuring a router is provided below:
-
-
-
-
-
-```js title="playwright/index.tsx"
-import { beforeMount, afterMount } from '@playwright/experimental-ct-react/hooks';
-import { BrowserRouter } from 'react-router-dom';
-
-export type HooksConfig = {
- enableRouting?: boolean;
-}
-
-beforeMount(async ({ App, hooksConfig }) => {
- if (hooksConfig?.enableRouting)
- return ;
-});
-```
-
-```js title="src/pages/ProductsPage.spec.tsx"
-import { test, expect } from '@playwright/experimental-ct-react';
-import type { HooksConfig } from '../playwright';
-import { ProductsPage } from './pages/ProductsPage';
-
-test('configure routing through hooks config', async ({ page, mount }) => {
- const component = await mount(, {
- hooksConfig: { enableRouting: true },
- });
- await expect(component.getByRole('link')).toHaveAttribute('href', '/products/42');
+```js title="tests/components/expandable.spec.ts"
+test('click should expand', async ({ mount }) => {
+ const component = await mount('components/Expandable/Stateful');
+ await component.getByRole('button').click();
+ await expect(component.getByTestId('expanded')).toHaveValue('true');
});
```
-
+This pattern is the heart of the methodology:
+- The whole scenario runs in the browser — no callback marshalling, no Node.js/browser boundary to leak through.
+- `toHaveValue()` is a web-first assertion: it retries until the state lands, so there is nothing to await or poll manually.
+- Record each observed value in its own `data-testid` input — `String(...)` for scalars, `JSON.stringify(...)` for payloads. The negative direction works the same way: perform the operation, then assert the value did **not** change.
+- The recorded state is *visible when you open the story in the gallery*. Click the component by hand and watch the values change — the story doubles as a manual test page for the exact scenario the automated test covers. Keep the form `hidden` for a clean screenshot baseline, or drop the `hidden` attribute while developing to see the state live next to the component.
-
+### Per-test props
-```js title="playwright/index.ts"
-import { beforeMount, afterMount } from '@playwright/experimental-ct-vue/hooks';
-import { router } from '../src/router';
+When a scenario is genuinely parametric — a boundary-value sweep, a text matrix — pass plain serializable props as the second argument to `mount`. The gallery hands them to the story as its props:
-export type HooksConfig = {
- enableRouting?: boolean;
-}
+```js title="src/components/Button.story.tsx"
+import { Button } from './Button';
-beforeMount(async ({ app, hooksConfig }) => {
- if (hooksConfig?.enableRouting)
- app.use(router);
-});
+export const WithTitle = ({ title = 'Default' }: { title?: string }) =>
+ ;
```
-```js title="src/pages/ProductsPage.spec.ts"
-import { test, expect } from '@playwright/experimental-ct-vue';
-import type { HooksConfig } from '../playwright';
-import ProductsPage from './pages/ProductsPage.vue';
+```js title="tests/components/button.spec.ts"
+import type { WithTitle } from '../../src/components/Button.story';
-test('configure routing through hooks config', async ({ page, mount }) => {
- const component = await mount(ProductsPage, {
- hooksConfig: { enableRouting: true },
- });
- await expect(component.getByRole('link')).toHaveAttribute('href', '/products/42');
-});
+const component = await mount('Button/WithTitle', { title: 'Hello' });
```
-
+`mount` is generic over the story: pass the story type as a template argument and the props (and `update()`) are type-checked against the story signature. Keep props to plain serializable data — callbacks belong inside the story.
-
+### Prop transitions with `update()`
-### unmount
+To test how a component reacts to a prop change **without remounting** — state preserved — call `component.update(newProps)`. It re-renders the same story with new props on the existing root:
-Unmount the mounted component from the DOM. This is useful for testing the component's behavior upon unmounting. Use cases include testing an "Are you sure you want to leave?" modal or ensuring proper cleanup of event handlers to prevent memory leaks.
-
-
-
-
-
-```js title="component.spec.tsx"
-import { test } from '@playwright/experimental-ct-react';
-
-test('unmount', async ({ mount }) => {
- const component = await mount();
- await component.unmount();
-});
-```
-
-
-
-
-
-```js title="component.spec.ts"
-import { test } from '@playwright/experimental-ct-vue';
-
-test('unmount', async ({ mount }) => {
- const component = await mount(Component);
- await component.unmount();
-});
-```
-
-```js title="component.spec.tsx"
-// Or alternatively, using the `jsx` style
-import { test } from '@playwright/experimental-ct-vue';
-
-test('unmount', async ({ mount }) => {
- const component = await mount();
- await component.unmount();
-});
-```
-
-
-
-
-
-### update
-
-Update props, slots/children, and/or events/callbacks of a mounted component. These component inputs can change at any time and are typically provided by the parent component, but sometimes it is necessary to ensure that your components behave appropriately to new inputs.
-
-
-
-
-
-```js title="component.spec.tsx"
-import { test } from '@playwright/experimental-ct-react';
-
-test('update', async ({ mount }) => {
- const component = await mount();
- await component.update(
- {}}>Child
- );
-});
+```js
+const component = await mount('components/Counter/Default', { value: 1 });
+await expect(component.getByTestId('value')).toHaveText('1');
+await component.update({ value: 2 });
+await expect(component.getByTestId('value')).toHaveText('2');
```
-
-
-
-
-```js title="component.spec.ts"
-import { test } from '@playwright/experimental-ct-vue';
-
-test('update', async ({ mount }) => {
- const component = await mount(Component);
- await component.update({
- props: { msg: 'greetings' },
- on: { click() {} },
- slots: { default: 'Child' }
- });
-});
-```
+### Multiple states and visual comparison
-```js title="component.spec.tsx"
-// Or alternatively, using the `jsx` style
-import { test } from '@playwright/experimental-ct-vue';
+Each `mount()` navigates fresh, so tests are fully isolated and mounting several stories in one test is cheap:
-test('update', async ({ mount }) => {
- const component = await mount();
- await component.update(
- {}}>Child
- );
-});
+```js
+await expect(await mount('Button/Primary')).toHaveScreenshot('primary.png');
+await expect(await mount('Button/Disabled')).toHaveScreenshot('disabled.png');
```
-
-
-
+Screenshot the returned root locator, not the page, to avoid asserting on browser chrome.
### Handling network requests
-Playwright provides an **experimental** `router` fixture to intercept and handle network requests. There are two ways to use the `router` fixture:
-* Call `router.route(url, handler)` that behaves similarly to [page.route()](/api/class-page.mdx#page-route). See the [network mocking guide](./mock.mdx) for more details.
-* Call `router.use(handlers)` and pass [MSW library](https://mswjs.io/) request handlers to it.
-
-Here is an example of reusing your existing MSW handlers in the test.
+Use [page.route()](/api/class-page.mdx#page-route) as usual — register routes before `mount()`, since mounting navigates:
```js
-import { handlers } from '@src/mocks/handlers';
-
-test.beforeEach(async ({ router }) => {
- // install common handlers before each test
- await router.use(...handlers);
-});
-
-test('example test', async ({ mount }) => {
- // test as usual, your handlers are active
- // ...
+test('renders the error state', async ({ page, mount }) => {
+ await page.route('**/api/items', route => route.fulfill({ status: 500 }));
+ const component = await mount('components/ItemList/Default');
+ await expect(component.getByRole('alert')).toContainText('Something went wrong');
});
```
-You can also introduce a one-off handler for a specific test.
+The `serviceWorkers: 'block'` option from the config keeps the app's own service worker from serving cached responses that would shadow the routes. Teams with [MSW](https://mswjs.io/) handler libraries can start the worker inside a story or decorator instead.
-```js
-import { http, HttpResponse } from 'msw';
-
-test('example test', async ({ mount, router }) => {
- await router.use(http.get('/data', async ({ request }) => {
- return HttpResponse.json({ value: 'mocked' });
- }));
+### Debugging stories
- // test as usual, your handler is active
- // ...
-});
-```
+Open the gallery URL in a browser and call `await window.mount({ story: 'components/Button/Primary' })` from the DevTools console — that is exactly what the `mount` fixture does. An unknown story or a render error rejects `window.mount`, which surfaces as the test's `mount()` throwing with a real stack. To browse without the console, give your gallery an optional index page listing all discovered stories.
-## Frequently asked questions
+## Migration from the experimental packages
-### What's the difference between `@playwright/test` and `@playwright/experimental-ct-{react,vue}`?
+The experimental packages compiled JSX in the test file and marshalled it into the browser. The gallery pattern moves the scenario into a story export that runs natively in the browser. Here is how the concepts map:
-```js
-test('…', async ({ mount, page, context }) => {
- // …
-});
-```
+| `@playwright/experimental-ct-*` | Story gallery |
+|---|---|
+| `mount()` | Stateful story: the story provides `onClick` and records the effect into a hidden input; the test asserts with `toHaveValue()` |
+| Plain data props from the test | Unchanged in spirit: `mount(id, props)` |
+| JSX children / slots from the test | One story export per composition (Vue: a `.story.vue` file for slot-heavy scenarios) |
+| `component.update()` | `component.update({ count: 2 })` |
+| `component.unmount()` | `component.unmount()` |
+| `beforeMount` / `afterMount` hooks | The body of the gallery's `window.mount` (global), or story decorators (per-story) |
+| `hooksConfig` per-test variation | Props: `mount('App/Routing', { route: '/dashboard' })`, interpreted by the story |
+| `router` fixture / MSW handlers in Node.js | [page.route()](/api/class-page.mdx#page-route) in the test, or MSW `setupWorker` inside a story |
+| `playwright/index.html` (styles, theme) | The gallery's `index.html` and entry module imports |
+| `ctViteConfig`, `ctPort`, `ctTemplateDir` | Gone — the gallery runs through your own dev server; the port lives in `webServer` and `baseURL` |
+| `defineConfig` from the ct package | Plain `defineConfig` from `@playwright/test` |
-`@playwright/experimental-ct-{react,vue}` wrap `@playwright/test` to provide an additional built-in component-testing specific fixture called `mount`:
+A typical spec migrates like this:
-
-
-
-
-```js
+```js title="Before: button.spec.tsx"
import { test, expect } from '@playwright/experimental-ct-react';
-import HelloWorld from './HelloWorld';
-
-test.use({ viewport: { width: 500, height: 500 } });
-
-test('should work', async ({ mount }) => {
- const component = await mount();
- await expect(component).toContainText('Greetings');
-});
-```
-
-
-
-
-
-```js
-import { test, expect } from '@playwright/experimental-ct-vue';
-import HelloWorld from './HelloWorld.vue';
-
-test.use({ viewport: { width: 500, height: 500 } });
+import Button from '../src/components/Button';
-test('should work', async ({ mount }) => {
- const component = await mount(HelloWorld, {
- props: {
- msg: 'Greetings',
- },
- });
- await expect(component).toContainText('Greetings');
+test('counts clicks', async ({ mount }) => {
+ let clicks = 0;
+ const component = await mount(
-
-
-
-Additionally, it adds some config options you can use in your `playwright-ct.config.{ts,js}`.
+```js title="After: src/components/Button.story.tsx"
+import { useState } from 'react';
+import { Button } from './Button';
-Finally, under the hood, each test re-uses the `context` and `page` fixture as a speed optimization for Component Testing. It resets them in between each test so it should be functionally equivalent to `@playwright/test`'s guarantee that you get a new, isolated `context` and `page` fixture per-test.
-
-### I have a project that already uses Vite. Can I reuse the config?
-
-At this point, Playwright is bundler-agnostic, so it is not reusing your existing Vite config. Your config might have a lot of things we won't be able to reuse. So for now, you would copy your path mappings and other high level settings into the `ctViteConfig` property of Playwright config.
-
-```js
-import { defineConfig } from '@playwright/experimental-ct-react';
-
-export default defineConfig({
- use: {
- ctViteConfig: {
- // ...
- },
- },
-});
+export const CountsClicks = () => {
+ const [clicks, setClicks] = useState(0);
+ return <>
+