Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: playwright as browser provider #3079

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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
32 changes: 19 additions & 13 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -106,12 +106,12 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
browser: [chrome, firefox, edge]
browser: [[chrome, chromium], [firefox, firefox], [edge, webkit]]

timeout-minutes: 10

env:
BROWSER: ${{ matrix.browser }}
BROWSER: ${{ matrix.browser[0] }}
steps:
- uses: actions/checkout@v3

Expand All @@ -126,36 +126,42 @@ jobs:
- name: Install
run: pnpm i

- name: Install Playwright Dependencies
run: pnpx playwright install-deps

- name: Build
run: pnpm run build

- name: Test Browser
run: pnpm run browser:test
- name: Test Browser (webdriverio)
run: pnpm run test:browser:webdriverio

- name: Test Browser (playwright)
run: pnpm run test:browser:playwright
env:
BROWSER: ${{ matrix.browser[1] }}

test-browser-safari:
runs-on: macos-latest
timeout-minutes: 10

env:
BROWSER: safari
steps:
- uses: actions/checkout@v3

- uses: ./.github/actions/setup-and-cache
with:
node-version: 18

- name: Info
run: system_profiler SPSoftwareDataType

- name: Install
run: pnpm i
run: sudo pnpm i

- name: Build
run: pnpm run build
run: sudo pnpm run build

- name: Enable
run: sudo safaridriver --enable

- name: Test Browser
run: sudo pnpm run browser:test
- name: Test Browser (webdriverio)
run: sudo BROWSER=safari pnpm run test:browser:webdriverio

- name: Test Browser (playwright)
run: sudo BROWSER=webkit pnpm run test:browser:playwright
26 changes: 13 additions & 13 deletions docs/config/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -1001,7 +1001,7 @@ Listen to port and serve API. When set to true, the default port is 51204
- **Version:** Since Vitest 0.29.4
- **CLI:** `--browser`, `--browser=<name>`, `--browser.name=chrome --browser.headless`

Run Vitest tests in a browser. If the browser name is not specified, Vitest will try to determine your default browser automatically. We use [WebdriverIO](https://webdriver.io/) for running tests by default, but it can be configured with [browser.provider](/config/#browser-provider) option.
Run Vitest tests in a browser. We use [WebdriverIO](https://webdriver.io/) for running tests by default, but it can be configured with [browser.provider](/config/#browser-provider) option.

::: tip NOTE
Read more about testing in a real browser in the [guide page](/guide/browser).
Expand All @@ -1022,11 +1022,13 @@ Run all tests inside a browser by default. Can be overriden with [`poolMatchGlob
#### browser&#46;name

- **Type:** `string`
- **Default:** _tries to find default browser automatically_
- **CLI:** `--browser=safari`

Run all tests in a specific browser. If not specified, tries to find a browser automatically.
Run all tests in a specific browser. Possible options in different providers:

- `webdriverio`: `firefox`, `chrome`, `edge`, `safari`
- `playwright`: `firefox`, `webkit`, `chromium`
- custom: any string that will be passed to the provider

#### browser.headless

Expand All @@ -1046,21 +1048,19 @@ Configure options for Vite server that serves code in the browser. Does not affe

#### browser.provider

- **Type:** `string`
- **Type:** `'webdriverio' | 'playwright' | string`
- **Default:** `'webdriverio'`
- **CLI:** `--browser.provider=./custom-provider.ts`
- **CLI:** `--browser.provider=playwright`

Path to a provider that will be used when running browser tests. Provider should be exported using `default` export and have this shape:
Path to a provider that will be used when running browser tests. Vitest provides two providers which are `webdriverio` (default) and `playwright`. Custom providers should be exported using `default` export and have this shape:

```ts
export interface BrowserProvider {
initialize(ctx: Vitest): Awaitable<void>
createPool(): {
runTests: (files: string[], invalidated: string[]) => void
close: () => Awaited<void>
}
// signals that test file stopped running, if it was opened with `id=` query
testFinished?(testId: string): Awaitable<void>
name: string
getSupportedBrowsers(): readonly string[]
initialize(ctx: Vitest, options: { browser: string }): Awaitable<void>
openPage(url: string): Awaitable<void>
close(): Awaitable<void>
}
```

Expand Down
36 changes: 15 additions & 21 deletions docs/guide/browser.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
---
title: Browser mode | Guide
title: Browser Mode | Guide
---

# Browser mode (experimental)
# Browser Mode (experimental)

This page provides information about the experimental browser mode feature in the Vitest API, which allows you to run your tests in the browser natively, providing access to browser globals like window and document. This feature is currently under development, and APIs may change in the future.

Expand All @@ -14,32 +14,26 @@ To activate browser mode in your Vitest configuration, you can use the `--browse
export default defineConfig({
test: {
browser: {
enabled: true
enabled: true,
name: 'chrome', // browser name is required
},
}
})
```

## Browser Option Types:

The browser option in Vitest can be set to either a boolean or a string type. If set to `true`, Vitest will try to automatically find your default browser. You can also specify a browser by providing its name as a `string`. The available browser options are:
- `firefox`
- `chrome`
- `edge`
- `safari`

Here's an example configuration setting chrome as the browser option:
The browser option in Vitest depends on the provider. Vitest will fail, if you pass `--browser` and don't specify its name in the config file. Available options:

```ts
export default defineConfig({
test: {
browser: {
enabled: true,
name: 'chrome',
},
}
})
```
- `webdriverio` (default) supports these browsers:
- `firefox`
- `chrome`
- `edge`
- `safari`
- `playwright` supports these browsers:
- `firefox`
- `webkit`
- `chromium`

## Cross-browser Testing:

Expand All @@ -58,7 +52,7 @@ npx vitest --browser.name=chrome --browser.headless
```

::: tip NOTE
When using the Safari browser option, the `safaridriver` needs to be activated by running `sudo safaridriver --enable` on your device.
When using the Safari browser option with WebdriverIO, the `safaridriver` needs to be activated by running `sudo safaridriver --enable` on your device.

Additionally, when running your tests, Vitest will attempt to install some drivers for compatibility with `safaridriver`.
:::
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@
"ui:build": "vite build packages/ui",
"ui:dev": "vite packages/ui",
"ui:test": "npm -C packages/ui run test:run",
"browser:test": "npm -C test/browser run test"
"test:browser:webdriverio": "npm -C test/browser run test:webdriverio",
"test:browser:playwright": "npm -C test/browser run test:playwright"
},
"devDependencies": {
"@antfu/eslint-config": "^0.34.1",
Expand Down
9 changes: 8 additions & 1 deletion packages/vitest/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,10 @@
"@vitest/browser": "*",
"@vitest/ui": "*",
"happy-dom": "*",
"jsdom": "*"
"jsdom": "*",
"playwright": "*",
"safaridriver": "*",
"webdriverio": "*"
},
"peerDependenciesMeta": {
"@vitest/ui": {
Expand All @@ -123,6 +126,9 @@
"safaridriver": {
"optional": true
},
"playwright": {
Aslemammad marked this conversation as resolved.
Show resolved Hide resolved
"optional": true
},
"@edge-runtime/vm": {
"optional": true
}
Expand Down Expand Up @@ -186,6 +192,7 @@
"natural-compare": "^1.4.0",
"p-limit": "^4.0.0",
"pkg-types": "^1.0.1",
"playwright": "^1.28.0",
"pretty-format": "^27.5.1",
"prompts": "^2.4.2",
"rollup": "^2.79.1",
Expand Down
1 change: 1 addition & 0 deletions packages/vitest/rollup.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ const external = [
'inspector',
'webdriverio',
'safaridriver',
'playwright',
'vite-node/source-map',
'vite-node/client',
'vite-node/server',
Expand Down
2 changes: 1 addition & 1 deletion packages/vitest/src/api/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export function setup(ctx: Vitest, server?: ViteDevServer) {
const rpc = createBirpc<WebSocketEvents, WebSocketHandlers>(
{
async onDone(testId) {
await ctx.browserProvider?.testFinished?.(testId)
return ctx.state.browserTestPromises.get(testId)?.resolve(true)
},
async onCollected(files) {
ctx.state.collectFiles(files)
Expand Down
6 changes: 0 additions & 6 deletions packages/vitest/src/defaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,6 @@ export const coverageConfigDefaults: ResolvedCoverageOptions = {
extension: ['.js', '.cjs', '.mjs', '.ts', '.mts', '.cts', '.tsx', '.jsx', '.vue', '.svelte'],
}

export const browserConfigDefaults = {
enabled: false,
headless: isCI,
} as const

export const fakeTimersDefaults = {
loopLimit: 10_000,
shouldClearNativeTimers: true,
Expand Down Expand Up @@ -73,7 +68,6 @@ const config = {
hookTimeout: 10000,
teardownTimeout: 10000,
isolate: true,
browser: browserConfigDefaults,
watchExclude: ['**/node_modules/**', '**/dist/**'],
forceRerunTriggers: [
'**/package.json/**',
Expand Down
14 changes: 12 additions & 2 deletions packages/vitest/src/integrations/browser.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { PlaywrightBrowserProvider } from '../node/browser/playwright'
import { WebdriverBrowserProvider } from '../node/browser/webdriver'
import type { BrowserProviderModule, ResolvedBrowserOptions } from '../types/browser'

Expand All @@ -6,8 +7,17 @@ interface Loader {
}

export async function getBrowserProvider(options: ResolvedBrowserOptions, loader: Loader): Promise<BrowserProviderModule> {
if (!options.provider || options.provider === 'webdriverio')
return WebdriverBrowserProvider
switch (options.provider) {
case undefined:
case 'webdriverio':
return WebdriverBrowserProvider

case 'playwright':
return PlaywrightBrowserProvider

default:
break
}

let customProviderModule

Expand Down
63 changes: 63 additions & 0 deletions packages/vitest/src/node/browser/playwright.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import type { Page } from 'playwright'
import type { BrowserProvider, BrowserProviderOptions } from '../../types/browser'
import { ensurePackageInstalled } from '../pkg'
import type { Vitest } from '../core'

export const playwrightBrowsers = ['firefox', 'webkit', 'chromium'] as const
export type PlaywrightBrowser = typeof playwrightBrowsers[number]

export interface PlaywrightProviderOptions extends BrowserProviderOptions {
browser: PlaywrightBrowser
}

export class PlaywrightBrowserProvider implements BrowserProvider {
public name = 'playwright'

private cachedBrowser: Page | null = null
private browser!: PlaywrightBrowser
private ctx!: Vitest

getSupportedBrowsers() {
return playwrightBrowsers
}

async initialize(ctx: Vitest, { browser }: PlaywrightProviderOptions) {
this.ctx = ctx
this.browser = browser

const root = this.ctx.config.root

if (!await ensurePackageInstalled('playwright', root))
throw new Error('Cannot find "playwright" package. Please install it manually.')
}

async openBrowser() {
if (this.cachedBrowser)
return this.cachedBrowser

const options = this.ctx.config.browser

const playwright = await import('playwright')

const playwrightInstance = await playwright[this.browser].launch({ headless: options.headless })
this.cachedBrowser = await playwrightInstance.newPage()

this.cachedBrowser.on('close', () => {
playwrightInstance.close()
})

return this.cachedBrowser
}

async openPage(url: string) {
const browserInstance = await this.openBrowser()
await browserInstance.goto(url)
}

async close() {
await this.cachedBrowser?.close()
// TODO: right now process can only exit with timeout, if we use browser
// needs investigating
process.exit()
}
}