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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions docs/config/browser/playwright.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,3 +104,35 @@ await userEvent.click(page.getByRole('button'), {
timeout: 1_000,
})
```

## `persistentContext`
Comment thread
hi-ogawa marked this conversation as resolved.
Outdated

- **Type:** `boolean | string`
- **Default:** `false`

When enabled, Vitest uses Playwright's [persistent context](https://playwright.dev/docs/api/class-browsertype#browser-type-launch-persistent-context) instead of a regular browser context. This allows browser state (cookies, localStorage, DevTools settings, etc.) to persist between test runs.

::: warning
This option is ignored when [`headless`](/config/browser/headless) is enabled. In headless mode, Vitest runs tests in parallel sessions which is incompatible with persistent context.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe it should depend on fileParallelism instead of headless, but mention that headless enabled file parallelism

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That sounds right, but maybe I saw persistent context with headless didn't work. I'll double check.

If it works, we should aim for checking getThreadsCount and maxWorkers here

function getThreadsCount(project: TestProject) {
const config = project.config.browser
if (
!config.headless
|| !config.fileParallelism
|| !project.browser!.provider.supportsParallelism
) {
return 1
}
if (project.config.maxWorkers) {
return project.config.maxWorkers
}
return threadsCount
}

// open the minimum amount of tabs
// if there is only 1 file running, we don't need 8 tabs running
const workerCount = Math.min(
this.options.maxWorkers - this.orchestrators.size,
files.length,
)

Copy link
Copy Markdown
Member

@sheremet-va sheremet-va Jan 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The note makes it sounds like it's about parallelism. If it's just about headless, we need to clarify that and remove the parallelisation mention I think

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I confirmed headless works fine. I updated to pass around initial workerCount > 1 condition from browser pool to playwright provider.

:::

- When set to `true`, the user data is stored in `./node_modules/.cache/vitest-playwright-user-data`
- When set to a string, the value is used as the path to the user data directory

```ts [vitest.config.js]
import { playwright } from '@vitest/browser-playwright'
import { defineConfig } from 'vitest/config'

export default defineConfig({
test: {
browser: {
provider: playwright({
persistentContext: true,
// or specify a custom directory:
// persistentContext: './my-browser-data',
}),
instances: [{ browser: 'chromium' }],
},
},
})
```
62 changes: 51 additions & 11 deletions packages/browser-playwright/src/playwright.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,18 @@ export interface PlaywrightProviderOptions {
* @default 0 (no timeout)
*/
actionTimeout?: number

/**
* Use a persistent context instead of a regular browser context.
* This allows browser state (cookies, localStorage, DevTools settings, etc.) to persist between test runs.
* When set to `true`, the user data is stored in `./node_modules/.cache/vitest-playwright-user-data`.
* When set to a string, the value is used as the path to the user data directory.
*
* Note: This option is ignored when `headless` is enabled because headless mode runs tests in parallel sessions.
* @default false
* @see {@link https://playwright.dev/docs/api/class-browsertype#browser-type-launch-persistent-context}
*/
persistentContext?: boolean | string
}

export function playwright(options: PlaywrightProviderOptions = {}): BrowserProviderOption<PlaywrightProviderOptions> {
Expand All @@ -94,6 +106,7 @@ export class PlaywrightBrowserProvider implements BrowserProvider {
public supportsParallelism = true

public browser: Browser | null = null
public persistentContext: BrowserContext | null = null

public contexts: Map<string, BrowserContext> = new Map()
public pages: Map<string, Page> = new Map()
Expand Down Expand Up @@ -202,7 +215,24 @@ export class PlaywrightBrowserProvider implements BrowserProvider {
}

debug?.('[%s] initializing the browser with launch options: %O', this.browserName, launchOptions)
this.browser = await playwright[this.browserName].launch(launchOptions)
if (this.options.persistentContext && !options.headless) {
const userDataDir
= typeof this.options.persistentContext === 'string'
? this.options.persistentContext
: './node_modules/.cache/vitest-playwright-user-data'
// TODO: how to avoid default "about" page?
this.persistentContext = await playwright[this.browserName].launchPersistentContext(
userDataDir,
{
...launchOptions,
...this.getContextOptions(),
},
)
this.browser = this.persistentContext.browser()!
}
else {
this.browser = await playwright[this.browserName].launch(launchOptions)
}
this.browserPromise = null
return this.browser
})()
Expand Down Expand Up @@ -357,20 +387,13 @@ export class PlaywrightBrowserProvider implements BrowserProvider {
const browser = await this.openBrowser()
await this._throwIfClosing(browser)
const actionTimeout = this.options.actionTimeout
const contextOptions = this.options.contextOptions ?? {}
const options = {
...contextOptions,
ignoreHTTPSErrors: true,
} satisfies BrowserContextOptions
if (this.project.config.browser.ui) {
options.viewport = null
}
const options = this.getContextOptions()
// TODO: investigate the consequences for Vitest 5
// else {
// if UI is disabled, keep the iframe scale to 1
// options.viewport ??= this.project.config.browser.viewport
// }
const context = await browser.newContext(options)
const context = this.persistentContext ?? await browser.newContext(options)
await this._throwIfClosing(context)
if (actionTimeout != null) {
context.setDefaultTimeout(actionTimeout)
Expand All @@ -380,6 +403,18 @@ export class PlaywrightBrowserProvider implements BrowserProvider {
return context
}

private getContextOptions(): BrowserContextOptions {
const contextOptions = this.options.contextOptions ?? {}
const options = {
...contextOptions,
ignoreHTTPSErrors: true,
} satisfies BrowserContextOptions
if (this.project.config.browser.ui) {
options.viewport = null
}
return options
}

public getPage(sessionId: string): Page {
const page = this.pages.get(sessionId)
if (!page) {
Expand Down Expand Up @@ -504,7 +539,12 @@ export class PlaywrightBrowserProvider implements BrowserProvider {
this.browser = null
await Promise.all([...this.pages.values()].map(p => p.close()))
this.pages.clear()
await Promise.all([...this.contexts.values()].map(c => c.close()))
if (this.persistentContext) {
await this.persistentContext.close()
}
else {
await Promise.all([...this.contexts.values()].map(c => c.close()))
}
this.contexts.clear()
await browser?.close()
debug?.('[%s] provider is closed', this.browserName)
Expand Down
Loading