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 11 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
23 changes: 16 additions & 7 deletions .github/workflows/ci.yml
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,18 +126,24 @@ jobs:
- name: Install
run: pnpm i

- name: Install Playwright Dependencies
run: sudo pnpm playwright install-deps

- name: Build
run: pnpm run build

- name: Test Browser
run: pnpm run browser:test

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

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

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

Expand All @@ -149,13 +155,16 @@ jobs:
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
run: sudo BROWSER=safari pnpm run browser:test

- name: Test Browser (playwright)
run: sudo BROWSER=webkit pnpm run browser:playwright:test
7 changes: 5 additions & 2 deletions docs/config/index.md
Expand Up @@ -1025,8 +1025,11 @@ Run all tests inside a browser by default. Can be overriden with [`poolMatchGlob
- **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. If not specified, tries to find a browser automatically. Possible options in different providers:

- `webdriverio`: `firefox`, `chrome`, `edge`, `safari`
- `playwright`: `firefox`, `webkit`, `chromium`
- custom: path to the custom provider

#### browser.headless

Expand All @@ -1050,7 +1053,7 @@ Configure options for Vite server that serves code in the browser. Does not affe
- **Default:** `'webdriverio'`
- **CLI:** `--browser.provider=./custom-provider.ts`

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`. Provider should be exported using `default` export and have this shape:

```ts
export interface BrowserProvider {
Expand Down
3 changes: 2 additions & 1 deletion package.json
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"
"browser:test": "npm -C test/browser run test",
"browser:playwright:test": "npm -C test/browser run test:playwright"
},
"devDependencies": {
"@antfu/eslint-config": "^0.34.1",
Expand Down
4 changes: 4 additions & 0 deletions packages/vitest/package.json
Expand Up @@ -123,6 +123,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 +189,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
Expand Up @@ -53,6 +53,7 @@ const external = [
'inspector',
'webdriverio',
'safaridriver',
'playwright',
'vite-node/source-map',
'vite-node/client',
'vite-node/server',
Expand Down
14 changes: 12 additions & 2 deletions packages/vitest/src/integrations/browser.ts
@@ -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
119 changes: 119 additions & 0 deletions packages/vitest/src/node/browser/playwright.ts
@@ -0,0 +1,119 @@
import { promisify } from 'util'
import type { Page } from 'playwright'
import type { Awaitable } from '@vitest/utils'
// @ts-expect-error doesn't have types
import detectBrowser from 'x-default-browser'
import { createDefer } from '@vitest/utils'
import { relative } from 'pathe'
import type { BrowserProvider } from '../../types/browser'
import { ensurePackageInstalled } from '../pkg'
import type { Vitest } from '../core'

export class PlaywrightBrowserProvider implements BrowserProvider {
private supportedBrowsers = ['firefox', 'webkit', 'chromium'] as const
private cachedBrowser: Page | null = null
private testDefers = new Map<string, ReturnType<typeof createDefer>>()
private host = ''
private browser!: typeof this.supportedBrowsers[number]
private ctx!: Vitest

async initialize(ctx: Vitest) {
this.ctx = ctx
this.host = `http://${ctx.config.browser.api?.host || 'localhost'}:${ctx.browser.config.server.port}`

const root = this.ctx.config.root
const browser = await this.getBrowserName()

this.browser = browser as any
sheremet-va marked this conversation as resolved.
Show resolved Hide resolved

if (browser === 'unknown' || !browser)
throw new Error('Cannot detect browser. Please specify it in the config file.')

if (!this.supportedBrowsers.includes(this.browser))
throw new Error(`Playwright provider does not support this browser, and only supports these browsers: ${this.supportedBrowsers.join(', ')}`)

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

private async resolveBrowserName(): Promise<string> {
const browser = await promisify(detectBrowser)()
return browser.commonName
}

async getBrowserName(): Promise<string> {
return this.ctx.config.browser.name ?? await this.resolveBrowserName()
}

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
}

testFinished(id: string): Awaitable<void> {
this.testDefers.get(id)?.resolve(true)
}

private waitForTest(id: string) {
const defer = createDefer()
this.testDefers.set(id, defer)
return defer
}

createPool() {
const close = async () => {
this.testDefers.clear()
await Promise.all([
this.cachedBrowser?.close(),
])
// TODO: right now process can only exit with timeout, if we use browser
// needs investigating
process.exit()
}

const runTests = async (files: string[]) => {
const paths = files.map(file => relative(this.ctx.config.root, file))
const browserInstance = await this.openBrowser()

const isolate = this.ctx.config.isolate
if (isolate) {
for (const path of paths) {
const url = new URL(this.host)
url.searchParams.append('path', path)
url.searchParams.set('id', path)
await browserInstance.goto(url.toString())
await this.waitForTest(path)
}
}
else {
const url = new URL(this.host)
url.searchParams.set('id', 'no-isolate')
paths.forEach(path => url.searchParams.append('path', path))
await browserInstance.goto(url.toString())
await this.waitForTest('no-isolate')
}
browserInstance.on('close', () => {
// if the user closes the browser, then close vitest too
close()
})
}

return {
runTests,
close,
}
}
}
8 changes: 6 additions & 2 deletions packages/vitest/src/node/browser/webdriver.ts
Expand Up @@ -10,11 +10,12 @@ import { ensurePackageInstalled } from '../pkg'
import type { Vitest } from '../core'

export class WebdriverBrowserProvider implements BrowserProvider {
private supportedBrowsers = ['firefox', 'chrome', 'edge', 'safari'] as const
private cachedBrowser: Browser | null = null
private testDefers = new Map<string, ReturnType<typeof createDefer>>()
private stopSafari: () => void = () => {}
private host = ''
private browser = 'unknown'
private browser!: typeof this.supportedBrowsers[number]
private ctx!: Vitest

async initialize(ctx: Vitest) {
Expand All @@ -24,11 +25,14 @@ export class WebdriverBrowserProvider implements BrowserProvider {
const root = this.ctx.config.root
const browser = await this.getBrowserName()

this.browser = browser
this.browser = browser as any

if (browser === 'unknown' || !browser)
throw new Error('Cannot detect browser. Please specify it in the config file.')

if (!this.supportedBrowsers.includes(this.browser))
throw new Error(`Webdriver provider does not support this browser, and only supports these browsers: ${this.supportedBrowsers.join(', ')}`)

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

Expand Down
6 changes: 3 additions & 3 deletions packages/vitest/src/types/browser.ts
Expand Up @@ -26,14 +26,14 @@ export interface BrowserConfigOptions {
*
* @default tries to find the first available browser
*/
name?: 'firefox' | 'chrome' | 'edge' | 'safari'
name?: 'firefox' | 'chrome' | 'edge' | 'safari' | 'webkit' | 'chromium' | (string & {})

/**
* browser provider
*
* @default 'webdriver'
* @default 'webdriverio'
*/
provider?: string
provider?: 'webdriverio' | 'playwright' | (string & {})

/**
* enable headless mode
Expand Down
2 changes: 2 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions test/browser/package.json
Expand Up @@ -4,6 +4,7 @@
"module": "true",
"scripts": {
"test": "node --test specs/",
"test:playwright": "PROVIDER=playwright node --test specs/",
"coverage": "vitest run --coverage"
},
"devDependencies": {
Expand Down
2 changes: 1 addition & 1 deletion test/browser/specs/runner.test.mjs
Expand Up @@ -30,7 +30,7 @@ test('logs are redirected to stdout', async () => {
assert.match(stdout, /hello from console.debug/, 'prints console.debug')
assert.match(stdout, /{ hello: 'from dir' }/, 'prints console.dir')
assert.match(stdout, /{ hello: 'from dirxml' }/, 'prints console.dixml')
assert.match(stdout, /hello from console.trace\s+\w+/, 'prints console.trace')
assert.match(stdout, /hello from console.trace\s*\w*/, 'prints console.trace')
sheremet-va marked this conversation as resolved.
Show resolved Hide resolved
assert.match(stdout, /dom <div \/>/, 'prints dom')
assert.match(stdout, /default: 1/, 'prints first default count')
assert.match(stdout, /default: 2/, 'prints second default count')
Expand Down
2 changes: 2 additions & 0 deletions test/browser/vitest.config.ts
@@ -1,6 +1,7 @@
import { defineConfig } from 'vitest/config'

const noop = () => {}
const isPlaywright = process.env.PROVIDER === 'playwright'

export default defineConfig({
test: {
Expand All @@ -9,6 +10,7 @@ export default defineConfig({
enabled: true,
name: 'chrome',
headless: true,
provider: isPlaywright ? 'playwright' : 'webdriverio',
},
open: false,
isolate: false,
Expand Down