+ *
+ *
+ *
+ * await expect.element(page.getByTestId('img-alt')).toHaveAccessibleName('Test alt')
+ * await expect.element(page.getByTestId('img-empty-alt')).not.toHaveAccessibleName()
+ * await expect.element(page.getByTestId('svg-title')).toHaveAccessibleName('Test title')
+ * await expect.element(page.getByTestId('button-img-alt')).toHaveAccessibleName()
+ * await expect.element(page.getByTestId('img-paragraph')).not.toHaveAccessibleName()
+ * await expect.element(page.getByTestId('svg-button')).toHaveAccessibleName()
+ * await expect.element(page.getByTestId('svg-without-title')).not.toHaveAccessibleName()
+ * await expect.element(page.getByTestId('input-title')).toHaveAccessibleName()
+ * @see https://vitest.dev/guide/browser/assertion-api#tohaveaccessiblename
+ */
+ toHaveAccessibleName(text?: string | RegExp | E): R
+ /**
+ * @description
+ * This allows you to assert that an element has the expected
+ * [role](https://www.w3.org/TR/html-aria/#docconformance).
+ *
+ * This is useful in cases where you already have access to an element via
+ * some query other than the role itself, and want to make additional
+ * assertions regarding its accessibility.
+ *
+ * The role can match either an explicit role (via the `role` attribute), or
+ * an implicit one via the [implicit ARIA
+ * semantics](https://www.w3.org/TR/html-aria/).
+ *
+ * Note: roles are matched literally by string equality, without inheriting
+ * from the ARIA role hierarchy. As a result, querying a superclass role
+ * like 'checkbox' will not include elements with a subclass role like
+ * 'switch'.
+ *
+ * @example
+ *
+ *
Continue
+ *
+ * About
+ * Invalid link
+ *
+ * await expect.element(page.getByTestId('button')).toHaveRole('button')
+ * await expect.element(page.getByTestId('button-explicit')).toHaveRole('button')
+ * await expect.element(page.getByTestId('button-explicit-multiple')).toHaveRole('button')
+ * await expect.element(page.getByTestId('button-explicit-multiple')).toHaveRole('switch')
+ * await expect.element(page.getByTestId('link')).toHaveRole('link')
+ * await expect.element(page.getByTestId('link-invalid')).not.toHaveRole('link')
+ * await expect.element(page.getByTestId('link-invalid')).toHaveRole('generic')
+ *
+ * @see https://vitest.dev/guide/browser/assertion-api#tohaverole
+ */
+ toHaveRole(
+ // Get autocomplete for ARIARole union types, while still supporting another string
+ // Ref: https://github.com/microsoft/TypeScript/issues/29729#issuecomment-567871939
+ role: ARIARole | (string & {}),
+ ): R
+ /**
+ * @description
+ * This allows you to check whether the given element is partially checked.
+ * It accepts an input of type checkbox and elements with a role of checkbox
+ * with a aria-checked="mixed", or input of type checkbox with indeterminate
+ * set to true
+ *
+ * @example
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ * const ariaCheckboxMixed = getByTestId('aria-checkbox-mixed')
+ * const inputCheckboxChecked = getByTestId('input-checkbox-checked')
+ * const inputCheckboxUnchecked = getByTestId('input-checkbox-unchecked')
+ * const ariaCheckboxChecked = getByTestId('aria-checkbox-checked')
+ * const ariaCheckboxUnchecked = getByTestId('aria-checkbox-unchecked')
+ * const inputCheckboxIndeterminate = getByTestId('input-checkbox-indeterminate')
+ *
+ * await expect.element(ariaCheckboxMixed).toBePartiallyChecked()
+ * await expect.element(inputCheckboxChecked).not.toBePartiallyChecked()
+ * await expect.element(inputCheckboxUnchecked).not.toBePartiallyChecked()
+ * await expect.element(ariaCheckboxChecked).not.toBePartiallyChecked()
+ * await expect.element(ariaCheckboxUnchecked).not.toBePartiallyChecked()
+ *
+ * inputCheckboxIndeterminate.indeterminate = true
+ * await expect.element(inputCheckboxIndeterminate).toBePartiallyChecked()
+ * @see https://vitest.dev/guide/browser/assertion-api#tobepartiallychecked
+ */
+ toBePartiallyChecked(): R
+ /**
+ * @description
+ * This allows to assert that an element has a
+ * [text selection](https://developer.mozilla.org/en-US/docs/Web/API/Selection).
+ *
+ * This is useful to check if text or part of the text is selected within an
+ * element. The element can be either an input of type text, a textarea, or any
+ * other element that contains text, such as a paragraph, span, div etc.
+ *
+ * NOTE: the expected selection is a string, it does not allow to check for
+ * selection range indices.
+ *
+ * @example
+ *
+ *
+ *
+ *
prev
+ *
text selected text
+ *
next
+ *
+ *
+ * page.getByTestId('text').element().setSelectionRange(5, 13)
+ * await expect.element(page.getByTestId('text')).toHaveSelection('selected')
+ *
+ * page.getByTestId('textarea').element().setSelectionRange(0, 5)
+ * await expect.element('textarea').toHaveSelection('text ')
+ *
+ * const selection = document.getSelection()
+ * const range = document.createRange()
+ * selection.removeAllRanges()
+ * selection.empty()
+ * selection.addRange(range)
+ *
+ * // selection of child applies to the parent as well
+ * range.selectNodeContents(page.getByTestId('child').element())
+ * await expect.element(page.getByTestId('child')).toHaveSelection('selected')
+ * await expect.element(page.getByTestId('parent')).toHaveSelection('selected')
+ *
+ * // selection that applies from prev all, parent text before child, and part child.
+ * range.setStart(page.getByTestId('prev').element(), 0)
+ * range.setEnd(page.getByTestId('child').element().childNodes[0], 3)
+ * await expect.element(page.queryByTestId('prev')).toHaveSelection('prev')
+ * await expect.element(page.queryByTestId('child')).toHaveSelection('sel')
+ * await expect.element(page.queryByTestId('parent')).toHaveSelection('text sel')
+ * await expect.element(page.queryByTestId('next')).not.toHaveSelection()
+ *
+ * // selection that applies from part child, parent text after child and part next.
+ * range.setStart(page.getByTestId('child').element().childNodes[0], 3)
+ * range.setEnd(page.getByTestId('next').element().childNodes[0], 2)
+ * await expect.element(page.queryByTestId('child')).toHaveSelection('ected')
+ * await expect.element(page.queryByTestId('parent')).toHaveSelection('ected text')
+ * await expect.element(page.queryByTestId('prev')).not.toHaveSelection()
+ * await expect.element(page.queryByTestId('next')).toHaveSelection('ne')
+ *
+ * @see https://vitest.dev/guide/browser/assertion-api#tohaveselection
+ */
+ toHaveSelection(selection?: string): R
+
+ /**
+ * @description
+ * This assertion allows you to perform visual regression testing by comparing
+ * screenshots of elements or pages against stored reference images.
+ *
+ * When differences are detected beyond the configured threshold, the test fails.
+ * To help identify the changes, the assertion generates:
+ *
+ * - The actual screenshot captured during the test
+ * - The expected reference screenshot
+ * - A diff image highlighting the differences (when possible)
+ *
+ * @example
+ *
+ *
+ * // basic usage, auto-generates screenshot name
+ * await expect.element(getByTestId('button')).toMatchScreenshot()
+ *
+ * // with custom name
+ * await expect.element(getByTestId('button')).toMatchScreenshot('fancy-button')
+ *
+ * // with options
+ * await expect.element(getByTestId('button')).toMatchScreenshot({
+ * comparatorName: 'pixelmatch',
+ * comparatorOptions: {
+ * allowedMismatchedPixelRatio: 0.01,
+ * },
+ * })
+ *
+ * // with both name and options
+ * await expect.element(getByTestId('button')).toMatchScreenshot('fancy-button', {
+ * comparatorName: 'pixelmatch',
+ * comparatorOptions: {
+ * allowedMismatchedPixelRatio: 0.01,
+ * },
+ * })
+ *
+ * @see https://vitest.dev/guide/browser/assertion-api#tomatchscreenshot
+ */
+ toMatchScreenshot(
+ options?: ScreenshotMatcherOptions,
+ ): Promise
+ toMatchScreenshot(
+ name?: string,
+ options?: ScreenshotMatcherOptions,
+ ): Promise
+}
diff --git a/packages/vitest/browser/matchers.d.ts b/packages/vitest/browser/matchers.d.ts
new file mode 100644
index 000000000000..36274e609b27
--- /dev/null
+++ b/packages/vitest/browser/matchers.d.ts
@@ -0,0 +1,29 @@
+import type { Locator } from 'vitest/browser'
+import type { TestingLibraryMatchers } from './jest-dom.js'
+import type { Assertion, ExpectPollOptions } from 'vitest'
+
+declare module 'vitest' {
+ interface JestAssertion extends TestingLibraryMatchers {}
+ interface AsymmetricMatchersContaining extends TestingLibraryMatchers {}
+
+ type Promisify = {
+ [K in keyof O]: O[K] extends (...args: infer A) => infer R
+ ? O extends R
+ ? Promisify
+ : (...args: A) => Promise
+ : O[K];
+ }
+
+ type PromisifyDomAssertion = Promisify>
+
+ interface ExpectStatic {
+ /**
+ * `expect.element(locator)` is a shorthand for `expect.poll(() => locator.element())`.
+ * You can set default timeout via `expect.poll.timeout` option in the config.
+ * @see {@link https://vitest.dev/api/expect#poll}
+ */
+ element: (element: T, options?: ExpectPollOptions) => PromisifyDomAssertion>
+ }
+}
+
+export {}
diff --git a/packages/vitest/package.json b/packages/vitest/package.json
index 36d6c3d14506..8c42a1fd3321 100644
--- a/packages/vitest/package.json
+++ b/packages/vitest/package.json
@@ -39,6 +39,10 @@
"default": "./index.cjs"
}
},
+ "./browser": {
+ "types": "./browser/context.d.ts",
+ "default": "./browser/context.js"
+ },
"./package.json": "./package.json",
"./optional-types.js": {
"types": "./optional-types.d.ts"
From 454e3280ac05b8992e1aab858d96a2ebca6fc8c4 Mon Sep 17 00:00:00 2001
From: Vladimir Sheremet
Date: Mon, 29 Sep 2025 10:57:50 +0200
Subject: [PATCH 03/43] fix: pass down factory options
---
packages/browser/src/node/index.ts | 34 +-
packages/vitest/src/node/pool.ts | 14 +-
packages/vitest/src/node/pools/browser.ts | 383 ++++++++++++++++++++++
packages/vitest/src/node/project.ts | 22 +-
packages/vitest/src/node/types/browser.ts | 18 +-
packages/vitest/src/public/node.ts | 2 +
6 files changed, 432 insertions(+), 41 deletions(-)
create mode 100644 packages/vitest/src/node/pools/browser.ts
diff --git a/packages/browser/src/node/index.ts b/packages/browser/src/node/index.ts
index 1c6312a376dd..81775d4ef331 100644
--- a/packages/browser/src/node/index.ts
+++ b/packages/browser/src/node/index.ts
@@ -1,25 +1,22 @@
-import type { Plugin } from 'vitest/config'
-import type { TestProject } from 'vitest/node'
+import type { BrowserServerFactory } from 'vitest/node'
import { MockerRegistry } from '@vitest/mocker'
import { interceptorPlugin } from '@vitest/mocker/node'
import c from 'tinyrainbow'
import { createViteLogger, createViteServer } from 'vitest/node'
import { version } from '../../package.json'
+import { distRoot } from './constants'
import BrowserPlugin from './plugin'
import { ParentBrowserProject } from './projectParent'
import { setupBrowserRpc } from './rpc'
-export { distRoot } from './constants'
export { createBrowserPool } from './pool'
export type { ProjectBrowser } from './project'
-export async function createBrowserServer(
- project: TestProject,
- configFile: string | undefined,
- prePlugins: Plugin[] = [],
- postPlugins: Plugin[] = [],
-): Promise {
+export const createBrowserServer: BrowserServerFactory = async (options) => {
+ const project = options.project
+ const configFile = project.vite.config.configFile
+
if (project.vitest.version !== version) {
project.vitest.logger.warn(
c.yellow(
@@ -42,6 +39,7 @@ export async function createBrowserServer(
const mockerRegistry = new MockerRegistry()
+ let cacheDir: string
const vite = await createViteServer({
...project.options, // spread project config inlined in root workspace config
base: '/',
@@ -71,11 +69,25 @@ export async function createBrowserServer(
},
cacheDir: project.vite.config.cacheDir,
plugins: [
- ...prePlugins,
+ {
+ name: 'vitest-internal:browser-cacheDir',
+ configResolved(config) {
+ cacheDir = config.cacheDir
+ },
+ },
+ ...options.mocksPlugins({
+ filter(id) {
+ if (id.includes(distRoot) || id.includes(cacheDir)) {
+ return false
+ }
+ return true
+ },
+ }),
+ options.metaEnvReplacer(),
...(project.options?.plugins || []),
BrowserPlugin(server),
interceptorPlugin({ registry: mockerRegistry }),
- ...postPlugins,
+ options.coveragePlugin(),
],
})
diff --git a/packages/vitest/src/node/pool.ts b/packages/vitest/src/node/pool.ts
index c7040b98b1ed..ce71d3bddafa 100644
--- a/packages/vitest/src/node/pool.ts
+++ b/packages/vitest/src/node/pool.ts
@@ -8,6 +8,7 @@ import { resolve } from 'pathe'
import { version as viteVersion } from 'vite'
import { rootDir } from '../paths'
import { isWindows } from '../utils/env'
+import { createBrowserPool } from './pools/browser'
import { createForksPool } from './pools/forks'
import { createThreadsPool } from './pools/threads'
import { createTypecheckPool } from './pools/typecheck'
@@ -175,13 +176,6 @@ export function createPool(ctx: Vitest): ProcessPool {
return getConcurrentPool(pool, () => resolveCustomPool(pool))
}
- function getBrowserPool() {
- return getConcurrentPool('browser', async () => {
- const { createBrowserPool } = await import('@vitest/browser')
- return createBrowserPool(ctx)
- })
- }
-
const groupedSpecifications: Record = {}
const groups = new Set()
@@ -191,6 +185,7 @@ export function createPool(ctx: Vitest): ProcessPool {
threads: specs => createThreadsPool(ctx, options, specs),
forks: specs => createForksPool(ctx, options, specs),
typescript: () => createTypecheckPool(ctx),
+ browser: () => createBrowserPool(ctx),
}
for (const spec of files) {
@@ -255,11 +250,6 @@ export function createPool(ctx: Vitest): ProcessPool {
return pools[pool]
}
- if (pool === 'browser') {
- pools.browser ??= await getBrowserPool()
- return pools.browser[method](specs, invalidate)
- }
-
const poolHandler = await getCustomPool(pool)
pools[poolHandler.name] ??= poolHandler
return poolHandler[method](specs, invalidate)
diff --git a/packages/vitest/src/node/pools/browser.ts b/packages/vitest/src/node/pools/browser.ts
new file mode 100644
index 000000000000..abf24d3e6b0c
--- /dev/null
+++ b/packages/vitest/src/node/pools/browser.ts
@@ -0,0 +1,383 @@
+import type { DeferPromise } from '@vitest/utils/helpers'
+import type { Vitest } from '../core'
+import type { ProcessPool } from '../pool'
+import type { TestProject } from '../project'
+import type { TestSpecification } from '../spec'
+import type { BrowserProvider } from '../types/browser'
+import crypto from 'node:crypto'
+import * as nodeos from 'node:os'
+import { performance } from 'node:perf_hooks'
+import { createDefer } from '@vitest/utils/helpers'
+import { stringify } from 'flatted'
+import { createDebugger } from '../../utils/debugger'
+
+const debug = createDebugger('vitest:browser:pool')
+
+export function createBrowserPool(vitest: Vitest): ProcessPool {
+ const providers = new Set()
+
+ const numCpus
+ = typeof nodeos.availableParallelism === 'function'
+ ? nodeos.availableParallelism()
+ : nodeos.cpus().length
+
+ const threadsCount = vitest.config.watch
+ ? Math.max(Math.floor(numCpus / 2), 1)
+ : Math.max(numCpus - 1, 1)
+
+ const projectPools = new WeakMap()
+
+ const ensurePool = (project: TestProject) => {
+ if (projectPools.has(project)) {
+ return projectPools.get(project)!
+ }
+
+ debug?.('creating pool for project %s', project.name)
+
+ const resolvedUrls = project.browser!.vite.resolvedUrls
+ const origin = resolvedUrls?.local[0] ?? resolvedUrls?.network[0]
+
+ if (!origin) {
+ throw new Error(
+ `Can't find browser origin URL for project "${project.name}"`,
+ )
+ }
+
+ const pool: BrowserPool = new BrowserPool(project, {
+ maxWorkers: getThreadsCount(project),
+ origin,
+ })
+ projectPools.set(project, pool)
+ vitest.onCancel(() => {
+ pool.cancel()
+ })
+
+ return pool
+ }
+
+ const runWorkspaceTests = async (method: 'run' | 'collect', specs: TestSpecification[]) => {
+ const groupedFiles = new Map()
+ for (const { project, moduleId } of specs) {
+ const files = groupedFiles.get(project) || []
+ files.push(moduleId)
+ groupedFiles.set(project, files)
+ }
+
+ let isCancelled = false
+ vitest.onCancel(() => {
+ isCancelled = true
+ })
+
+ const initialisedPools = await Promise.all([...groupedFiles.entries()].map(async ([project, files]) => {
+ await project._initBrowserProvider()
+
+ if (!project.browser) {
+ throw new TypeError(`The browser server was not initialized${project.name ? ` for the "${project.name}" project` : ''}. This is a bug in Vitest. Please, open a new issue with reproduction.`)
+ }
+
+ if (isCancelled) {
+ return
+ }
+
+ debug?.('provider is ready for %s project', project.name)
+
+ const pool = ensurePool(project)
+ vitest.state.clearFiles(project, files)
+ providers.add(project.browser!.provider)
+
+ return {
+ pool,
+ provider: project.browser!.provider,
+ runTests: () => pool.runTests(method, files),
+ }
+ }))
+
+ if (isCancelled) {
+ return
+ }
+
+ const parallelPools: (() => Promise)[] = []
+ const nonParallelPools: (() => Promise)[] = []
+
+ for (const pool of initialisedPools) {
+ if (!pool) {
+ // this means it was cancelled
+ return
+ }
+
+ if (pool.provider.mocker && pool.provider.supportsParallelism) {
+ parallelPools.push(pool.runTests)
+ }
+ else {
+ nonParallelPools.push(pool.runTests)
+ }
+ }
+
+ await Promise.all(parallelPools.map(runTests => runTests()))
+
+ for (const runTests of nonParallelPools) {
+ if (isCancelled) {
+ return
+ }
+
+ await runTests()
+ }
+ }
+
+ 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
+ }
+
+ return {
+ name: 'browser',
+ async close() {
+ await Promise.all([...providers].map(provider => provider.close()))
+ vitest._browserSessions.sessionIds.clear()
+ providers.clear()
+ vitest.projects.forEach((project) => {
+ project.browser?.state.orchestrators.forEach((orchestrator) => {
+ orchestrator.$close()
+ })
+ })
+ debug?.('browser pool closed all providers')
+ },
+ runTests: files => runWorkspaceTests('run', files),
+ collectTests: files => runWorkspaceTests('collect', files),
+ }
+}
+
+function escapePathToRegexp(path: string): string {
+ return path.replace(/[/\\.?*()^${}|[\]+]/g, '\\$&')
+}
+
+class BrowserPool {
+ private _queue: string[] = []
+ private _promise: DeferPromise | undefined
+ private _providedContext: string | undefined
+
+ private readySessions = new Set()
+
+ constructor(
+ private project: TestProject,
+ private options: {
+ maxWorkers: number
+ origin: string
+ },
+ ) {}
+
+ public cancel(): void {
+ this._queue = []
+ }
+
+ public reject(error: Error): void {
+ this._promise?.reject(error)
+ this._promise = undefined
+ this.cancel()
+ }
+
+ get orchestrators() {
+ return this.project.browser!.state.orchestrators
+ }
+
+ async runTests(method: 'run' | 'collect', files: string[]): Promise {
+ this._promise ??= createDefer()
+
+ if (!files.length) {
+ debug?.('no tests found, finishing test run immediately')
+ this._promise.resolve()
+ return this._promise
+ }
+
+ this._providedContext = stringify(this.project.getProvidedContext())
+
+ this._queue.push(...files)
+
+ this.readySessions.forEach((sessionId) => {
+ if (this._queue.length) {
+ this.readySessions.delete(sessionId)
+ this.runNextTest(method, sessionId)
+ }
+ })
+
+ if (this.orchestrators.size >= this.options.maxWorkers) {
+ debug?.('all orchestrators are ready, not creating more')
+ return this._promise
+ }
+
+ // 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,
+ )
+
+ const promises: Promise[] = []
+ for (let i = 0; i < workerCount; i++) {
+ const sessionId = crypto.randomUUID()
+ this.project.vitest._browserSessions.sessionIds.add(sessionId)
+ const project = this.project.name
+ debug?.('[%s] creating session for %s', sessionId, project)
+ const page = this.openPage(sessionId).then(() => {
+ // start running tests on the page when it's ready
+ this.runNextTest(method, sessionId)
+ })
+ promises.push(page)
+ }
+ await Promise.all(promises)
+ debug?.('all sessions are created')
+ return this._promise
+ }
+
+ private async openPage(sessionId: string) {
+ const sessionPromise = this.project.vitest._browserSessions.createSession(
+ sessionId,
+ this.project,
+ this,
+ )
+ const browser = this.project.browser!
+ const url = new URL('/__vitest_test__/', this.options.origin)
+ url.searchParams.set('sessionId', sessionId)
+ const pagePromise = browser.provider.openPage(
+ sessionId,
+ url.toString(),
+ )
+ await Promise.all([sessionPromise, pagePromise])
+ }
+
+ private getOrchestrator(sessionId: string) {
+ const orchestrator = this.orchestrators.get(sessionId)
+ if (!orchestrator) {
+ throw new Error(`Orchestrator not found for session ${sessionId}. This is a bug in Vitest. Please, open a new issue with reproduction.`)
+ }
+ return orchestrator
+ }
+
+ private finishSession(sessionId: string): void {
+ this.readySessions.add(sessionId)
+
+ // the last worker finished running tests
+ if (this.readySessions.size === this.orchestrators.size) {
+ this._promise?.resolve()
+ this._promise = undefined
+ debug?.('[%s] all tests finished running', sessionId)
+ }
+ else {
+ debug?.(
+ `did not finish sessions for ${sessionId}: |ready - %s| |overall - %s|`,
+ [...this.readySessions].join(', '),
+ [...this.orchestrators.keys()].join(', '),
+ )
+ }
+ }
+
+ private runNextTest(method: 'run' | 'collect', sessionId: string): void {
+ const file = this._queue.shift()
+
+ if (!file) {
+ debug?.('[%s] no more tests to run', sessionId)
+ const isolate = this.project.config.browser.isolate
+ // we don't need to cleanup testers if isolation is enabled,
+ // because cleanup is done at the end of every test
+ if (isolate) {
+ this.finishSession(sessionId)
+ return
+ }
+
+ // we need to cleanup testers first because there is only
+ // one iframe and it does the cleanup only after everything is completed
+ const orchestrator = this.getOrchestrator(sessionId)
+ orchestrator.cleanupTesters()
+ .catch(error => this.reject(error))
+ .finally(() => this.finishSession(sessionId))
+ return
+ }
+
+ if (!this._promise) {
+ throw new Error(`Unexpected empty queue`)
+ }
+ const startTime = performance.now()
+
+ const orchestrator = this.getOrchestrator(sessionId)
+ debug?.('[%s] run test %s', sessionId, file)
+
+ this.setBreakpoint(sessionId, file).then(() => {
+ // this starts running tests inside the orchestrator
+ orchestrator.createTesters(
+ {
+ method,
+ files: [file],
+ // this will be parsed by the test iframe, not the orchestrator
+ // so we need to stringify it first to avoid double serialization
+ providedContext: this._providedContext || '[{}]',
+ startTime,
+ },
+ )
+ .then(() => {
+ debug?.('[%s] test %s finished running', sessionId, file)
+ this.runNextTest(method, sessionId)
+ })
+ .catch((error) => {
+ // if user cancels the test run manually, ignore the error and exit gracefully
+ if (
+ this.project.vitest.isCancelling
+ && error instanceof Error
+ && error.message.startsWith('Browser connection was closed while running tests')
+ ) {
+ this.cancel()
+ this._promise?.resolve()
+ this._promise = undefined
+ debug?.('[%s] browser connection was closed', sessionId)
+ return
+ }
+ debug?.('[%s] error during %s test run: %s', sessionId, file, error)
+ this.reject(error)
+ })
+ }).catch(err => this.reject(err))
+ }
+
+ async setBreakpoint(sessionId: string, file: string) {
+ if (!this.project.config.inspector.waitForDebugger) {
+ return
+ }
+
+ const provider = this.project.browser!.provider
+ const browser = this.project.config.browser.name
+
+ if (shouldIgnoreDebugger(provider.name, browser)) {
+ debug?.('[$s] ignoring debugger in %s browser because it is not supported', sessionId, browser)
+ return
+ }
+
+ if (!provider.getCDPSession) {
+ throw new Error('Unable to set breakpoint, CDP not supported')
+ }
+
+ debug?.('[%s] set breakpoint for %s', sessionId, file)
+ const session = await provider.getCDPSession(sessionId)
+ await session.send('Debugger.enable', {})
+ await session.send('Debugger.setBreakpointByUrl', {
+ lineNumber: 0,
+ urlRegex: escapePathToRegexp(file),
+ })
+ }
+}
+
+function shouldIgnoreDebugger(provider: string, browser: string) {
+ if (provider === 'webdriverio') {
+ return browser !== 'chrome' && browser !== 'edge'
+ }
+ return browser !== 'chromium'
+}
diff --git a/packages/vitest/src/node/project.ts b/packages/vitest/src/node/project.ts
index 840c4658dcd1..846f5f222798 100644
--- a/packages/vitest/src/node/project.ts
+++ b/packages/vitest/src/node/project.ts
@@ -453,18 +453,16 @@ export class TestProject {
if (!this.isBrowserEnabled() || this._parentBrowser) {
return
}
- if (typeof this.config.browser.provider!.serverFactory !== 'function') {
- throw new TypeError(`The provider options do not return a "serverFactory" function.`)
- }
- const browser = await this.config.browser.provider!.serverFactory(
- this,
- this.vite.config.configFile,
- [
- ...MocksPlugins(), // TODO: inject cacheDir inside the server factory
- MetaEnvReplacerPlugin(),
- ],
- [CoverageTransform(this.vitest)],
- )
+ const provider = this.config.browser.provider!
+ if (typeof provider.serverFactory !== 'function') {
+ throw new TypeError(`The browser provider options do not return a "serverFactory" function. Are you using the latest "@vitest/browser-${provider?.name}" package?`)
+ }
+ const browser = await this.config.browser.provider!.serverFactory({
+ project: this,
+ mocksPlugins: options => MocksPlugins(options),
+ metaEnvReplacer: () => MetaEnvReplacerPlugin(),
+ coveragePlugin: () => CoverageTransform(this.vitest),
+ })
this._parentBrowser = browser
if (this.config.browser.ui) {
setup(this.vitest, browser.vite)
diff --git a/packages/vitest/src/node/types/browser.ts b/packages/vitest/src/node/types/browser.ts
index f90ffdb7b978..441b7dcd0e38 100644
--- a/packages/vitest/src/node/types/browser.ts
+++ b/packages/vitest/src/node/types/browser.ts
@@ -26,12 +26,18 @@ export interface BrowserProviderOption {
supportedBrowser?: ReadonlyArray
options: Options
providerFactory: (project: TestProject) => BrowserProvider
- serverFactory: (
- project: TestProject,
- configFile: string | undefined,
- prePlugins: Plugin[],
- postPlugins: Plugin[],
- ) => Promise
+ serverFactory: BrowserServerFactory
+}
+
+export interface BrowserServerOptions {
+ project: TestProject
+ coveragePlugin: () => Plugin
+ mocksPlugins: (options: { filter: (id: string) => boolean }) => Plugin[]
+ metaEnvReplacer: () => Plugin
+}
+
+export interface BrowserServerFactory {
+ (otpions: BrowserServerOptions): Promise
}
export interface BrowserProvider {
diff --git a/packages/vitest/src/public/node.ts b/packages/vitest/src/public/node.ts
index 0f2cfc903884..ff8b3fe4de6c 100644
--- a/packages/vitest/src/public/node.ts
+++ b/packages/vitest/src/public/node.ts
@@ -69,6 +69,8 @@ export type {
BrowserProvider,
BrowserProviderOption,
BrowserScript,
+ BrowserServerFactory,
+ BrowserServerOptions,
BrowserServerState,
BrowserServerStateSession,
CDPSession,
From ed6c594a2b40f45e31e07193173f0f28fa537608 Mon Sep 17 00:00:00 2001
From: Vladimir Sheremet
Date: Mon, 29 Sep 2025 11:07:16 +0200
Subject: [PATCH 04/43] feat: add wdio provider
---
packages/browser-playwright/src/index.ts | 2 +-
.../src/{provider.ts => playwright.ts} | 0
packages/browser-webdriverio/package.json | 52 +++
packages/browser-webdriverio/rollup.config.js | 62 ++++
packages/browser-webdriverio/src/index.ts | 5 +
.../browser-webdriverio/src/webdriverio.ts | 296 ++++++++++++++++++
packages/browser-webdriverio/tsconfig.json | 13 +
pnpm-lock.yaml | 19 ++
test/browser/package.json | 1 +
test/browser/settings.ts | 2 +-
test/browser/tsconfig.json | 1 -
test/browser/vitest.config.mts | 2 +-
tsconfig.base.json | 1 +
13 files changed, 452 insertions(+), 4 deletions(-)
rename packages/browser-playwright/src/{provider.ts => playwright.ts} (100%)
create mode 100644 packages/browser-webdriverio/package.json
create mode 100644 packages/browser-webdriverio/rollup.config.js
create mode 100644 packages/browser-webdriverio/src/index.ts
create mode 100644 packages/browser-webdriverio/src/webdriverio.ts
create mode 100644 packages/browser-webdriverio/tsconfig.json
diff --git a/packages/browser-playwright/src/index.ts b/packages/browser-playwright/src/index.ts
index 385b72c1693d..90757942d391 100644
--- a/packages/browser-playwright/src/index.ts
+++ b/packages/browser-playwright/src/index.ts
@@ -2,4 +2,4 @@ export {
playwright,
PlaywrightBrowserProvider,
type PlaywrightProviderOptions,
-} from './provider'
+} from './playwright'
diff --git a/packages/browser-playwright/src/provider.ts b/packages/browser-playwright/src/playwright.ts
similarity index 100%
rename from packages/browser-playwright/src/provider.ts
rename to packages/browser-playwright/src/playwright.ts
diff --git a/packages/browser-webdriverio/package.json b/packages/browser-webdriverio/package.json
new file mode 100644
index 000000000000..e7b6fe56c984
--- /dev/null
+++ b/packages/browser-webdriverio/package.json
@@ -0,0 +1,52 @@
+{
+ "name": "@vitest/browser-webdriverio",
+ "type": "module",
+ "version": "4.0.0-beta.13",
+ "description": "Browser running for Vitest using webdriverio",
+ "license": "MIT",
+ "funding": "https://opencollective.com/vitest",
+ "homepage": "https://vitest.dev/guide/browser/webdriverio",
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/vitest-dev/vitest.git",
+ "directory": "packages/browser-webdriverio"
+ },
+ "bugs": {
+ "url": "https://github.com/vitest-dev/vitest/issues"
+ },
+ "sideEffects": false,
+ "exports": {
+ ".": {
+ "types": "./dist/index.d.ts",
+ "default": "./dist/index.js"
+ },
+ "./package.json": "./package.json"
+ },
+ "main": "./dist/index.js",
+ "module": "./dist/index.js",
+ "types": "./dist/index.d.ts",
+ "files": [
+ "dist"
+ ],
+ "scripts": {
+ "build": "premove dist && pnpm rollup -c",
+ "dev": "rollup -c --watch --watch.include 'src/**'"
+ },
+ "peerDependencies": {
+ "vitest": "workspace:*",
+ "webdriverio": "*"
+ },
+ "peerDependenciesMeta": {
+ "webdriverio": {
+ "optional": false
+ }
+ },
+ "dependencies": {
+ "@vitest/browser": "workspace:*"
+ },
+ "devDependencies": {
+ "@wdio/types": "^9.19.2",
+ "vitest": "workspace:*",
+ "webdriverio": "^9.19.2"
+ }
+}
diff --git a/packages/browser-webdriverio/rollup.config.js b/packages/browser-webdriverio/rollup.config.js
new file mode 100644
index 000000000000..4a5ab941916b
--- /dev/null
+++ b/packages/browser-webdriverio/rollup.config.js
@@ -0,0 +1,62 @@
+import { createRequire } from 'node:module'
+import commonjs from '@rollup/plugin-commonjs'
+import json from '@rollup/plugin-json'
+import resolve from '@rollup/plugin-node-resolve'
+import { defineConfig } from 'rollup'
+import oxc from 'unplugin-oxc/rollup'
+import { createDtsUtils } from '../../scripts/build-utils.js'
+
+const require = createRequire(import.meta.url)
+const pkg = require('./package.json')
+
+const external = [
+ ...Object.keys(pkg.dependencies),
+ ...Object.keys(pkg.peerDependencies || {}),
+ /^@?vitest(\/|$)/,
+ '@vitest/browser/utils',
+ 'worker_threads',
+ 'node:worker_threads',
+ 'vite',
+ 'playwright-core/types/protocol',
+]
+
+const dtsUtils = createDtsUtils()
+
+const plugins = [
+ resolve({
+ preferBuiltins: true,
+ }),
+ json(),
+ commonjs(),
+ oxc({
+ transform: { target: 'node18' },
+ }),
+]
+
+export default () =>
+ defineConfig([
+ {
+ input: './src/index.ts',
+ output: {
+ dir: 'dist',
+ format: 'esm',
+ },
+ external,
+ context: 'null',
+ plugins: [
+ ...dtsUtils.isolatedDecl(),
+ ...plugins,
+ ],
+ },
+ {
+ input: dtsUtils.dtsInput('src/index.ts'),
+ output: {
+ dir: 'dist',
+ entryFileNames: '[name].d.ts',
+ format: 'esm',
+ },
+ watch: false,
+ external,
+ plugins: dtsUtils.dts(),
+ },
+ ])
diff --git a/packages/browser-webdriverio/src/index.ts b/packages/browser-webdriverio/src/index.ts
new file mode 100644
index 000000000000..2980265fbaef
--- /dev/null
+++ b/packages/browser-webdriverio/src/index.ts
@@ -0,0 +1,5 @@
+export {
+ WebdriverBrowserProvider,
+ webdriverio,
+ type WebdriverProviderOptions,
+} from './webdriverio'
diff --git a/packages/browser-webdriverio/src/webdriverio.ts b/packages/browser-webdriverio/src/webdriverio.ts
new file mode 100644
index 000000000000..70929a21943e
--- /dev/null
+++ b/packages/browser-webdriverio/src/webdriverio.ts
@@ -0,0 +1,296 @@
+import type {
+ ScreenshotComparatorRegistry,
+ ScreenshotMatcherOptions,
+} from '@vitest/browser/context'
+import type { Capabilities } from '@wdio/types'
+import type {
+ BrowserProvider,
+ BrowserProviderOption,
+ CDPSession,
+ TestProject,
+} from 'vitest/node'
+import type { ClickOptions, DragAndDropOptions, remote } from 'webdriverio'
+
+import { createDebugger } from 'vitest/node'
+
+const debug = createDebugger('vitest:browser:wdio')
+
+const webdriverBrowsers = ['firefox', 'chrome', 'edge', 'safari'] as const
+type WebdriverBrowser = (typeof webdriverBrowsers)[number]
+
+export interface WebdriverProviderOptions extends Partial<
+ Parameters[0]
+> {}
+
+export function webdriverio(options: WebdriverProviderOptions = {}): BrowserProviderOption {
+ return {
+ name: 'webdriverio',
+ supportedBrowser: webdriverBrowsers,
+ options,
+ providerFactory(project) {
+ return new WebdriverBrowserProvider(project, options)
+ },
+ // --browser.provider=webdriverio
+ // @ts-expect-error hidden way to bypass importing webdriverio
+ _cli: true,
+ }
+}
+
+export class WebdriverBrowserProvider implements BrowserProvider {
+ public name = 'webdriverio' as const
+ public supportsParallelism: boolean = false
+
+ public browser: WebdriverIO.Browser | null = null
+
+ private browserName!: WebdriverBrowser
+ private project!: TestProject
+
+ private options?: WebdriverProviderOptions
+
+ private closing = false
+ private iframeSwitched = false
+ private topLevelContext: string | undefined
+
+ getSupportedBrowsers(): readonly string[] {
+ return webdriverBrowsers
+ }
+
+ constructor(
+ project: TestProject,
+ options: WebdriverProviderOptions,
+ ) {
+ // increase shutdown timeout because WDIO takes some extra time to kill the driver
+ if (!project.vitest.state._data.timeoutIncreased) {
+ project.vitest.state._data.timeoutIncreased = true
+ project.vitest.config.teardownTimeout += 10_000
+ }
+
+ this.closing = false
+ this.project = project
+ this.browserName = project.config.browser.name as WebdriverBrowser
+ this.options = options
+ }
+
+ isIframeSwitched(): boolean {
+ return this.iframeSwitched
+ }
+
+ async switchToTestFrame(): Promise {
+ const browser = this.browser!
+ // support wdio@9
+ if (browser.switchFrame) {
+ await browser.switchFrame(browser.$('iframe[data-vitest]'))
+ }
+ else {
+ const iframe = await browser.findElement(
+ 'css selector',
+ 'iframe[data-vitest]',
+ )
+ await browser.switchToFrame(iframe)
+ }
+ this.iframeSwitched = true
+ }
+
+ async switchToMainFrame(): Promise {
+ const page = this.browser!
+ if (page.switchFrame) {
+ await page.switchFrame(null)
+ }
+ else {
+ await page.switchToParentFrame()
+ }
+ this.iframeSwitched = false
+ }
+
+ async setViewport(options: { width: number; height: number }): Promise {
+ if (this.topLevelContext == null || !this.browser) {
+ throw new Error(`The browser has no open pages.`)
+ }
+ await this.browser.send({
+ method: 'browsingContext.setViewport',
+ params: {
+ context: this.topLevelContext,
+ devicePixelRatio: 1,
+ viewport: options,
+ },
+ })
+ }
+
+ getCommandsContext(): {
+ browser: WebdriverIO.Browser | null
+ } {
+ return {
+ browser: this.browser,
+ }
+ }
+
+ async openBrowser(): Promise {
+ await this._throwIfClosing('opening the browser')
+
+ if (this.browser) {
+ debug?.('[%s] the browser is already opened, reusing it', this.browserName)
+ return this.browser
+ }
+
+ const options = this.project.config.browser
+
+ if (this.browserName === 'safari') {
+ if (options.headless) {
+ throw new Error(
+ 'You\'ve enabled headless mode for Safari but it doesn\'t currently support it.',
+ )
+ }
+ }
+
+ const { remote } = await import('webdriverio')
+
+ const remoteOptions: Capabilities.WebdriverIOConfig = {
+ logLevel: 'silent',
+ ...this.options,
+ capabilities: this.buildCapabilities(),
+ }
+
+ debug?.('[%s] opening the browser with options: %O', this.browserName, remoteOptions)
+ // TODO: close everything, if browser is closed from the outside
+ this.browser = await remote(remoteOptions)
+ await this._throwIfClosing()
+
+ return this.browser
+ }
+
+ private buildCapabilities() {
+ const capabilities: Capabilities.WebdriverIOConfig['capabilities'] = {
+ ...this.options?.capabilities,
+ browserName: this.browserName,
+ }
+
+ const headlessMap = {
+ chrome: ['goog:chromeOptions', ['headless', 'disable-gpu']],
+ firefox: ['moz:firefoxOptions', ['-headless']],
+ edge: ['ms:edgeOptions', ['--headless']],
+ } as const
+
+ const options = this.project.config.browser
+ const browser = this.browserName
+ if (browser !== 'safari' && options.headless) {
+ const [key, args] = headlessMap[browser]
+ const currentValues = (this.options?.capabilities as any)?.[key] || {}
+ const newArgs = [...(currentValues.args || []), ...args]
+ capabilities[key] = { ...currentValues, args: newArgs as any }
+ }
+
+ // start Vitest UI maximized only on supported browsers
+ if (options.ui && (browser === 'chrome' || browser === 'edge')) {
+ const key = browser === 'chrome'
+ ? 'goog:chromeOptions'
+ : 'ms:edgeOptions'
+ const args = capabilities[key]?.args || []
+ if (!args.includes('--start-maximized') && !args.includes('--start-fullscreen')) {
+ args.push('--start-maximized')
+ }
+ capabilities[key] ??= {}
+ capabilities[key]!.args = args
+ }
+
+ const inspector = this.project.vitest.config.inspector
+ if (inspector.enabled && (browser === 'chrome' || browser === 'edge')) {
+ const key = browser === 'chrome'
+ ? 'goog:chromeOptions'
+ : 'ms:edgeOptions'
+ const args = capabilities[key]?.args || []
+
+ // NodeJS equivalent defaults: https://nodejs.org/en/learn/getting-started/debugging#enable-inspector
+ const port = inspector.port || 9229
+ const host = inspector.host || '127.0.0.1'
+
+ args.push(`--remote-debugging-port=${port}`)
+ args.push(`--remote-debugging-address=${host}`)
+
+ this.project.vitest.logger.log(`Debugger listening on ws://${host}:${port}`)
+
+ capabilities[key] ??= {}
+ capabilities[key]!.args = args
+ }
+
+ return capabilities
+ }
+
+ async openPage(sessionId: string, url: string): Promise {
+ await this._throwIfClosing('creating the browser')
+ debug?.('[%s][%s] creating the browser page for %s', sessionId, this.browserName, url)
+ const browserInstance = await this.openBrowser()
+ debug?.('[%s][%s] browser page is created, opening %s', sessionId, this.browserName, url)
+ await browserInstance.url(url)
+ this.topLevelContext = await browserInstance.getWindowHandle()
+ await this._throwIfClosing('opening the url')
+ }
+
+ private async _throwIfClosing(action?: string) {
+ if (this.closing) {
+ debug?.(`[%s] provider was closed, cannot perform the action${action ? ` ${action}` : ''}`, this.browserName)
+ await (this.browser?.sessionId ? this.browser?.deleteSession?.() : null)
+ throw new Error(`[vitest] The provider was closed.`)
+ }
+ }
+
+ async close(): Promise {
+ debug?.('[%s] closing provider', this.browserName)
+ this.closing = true
+ const browser = this.browser
+ const sessionId = browser?.sessionId
+ if (!browser || !sessionId) {
+ return
+ }
+
+ // https://github.com/webdriverio/webdriverio/blob/ab1a2e82b13a9c7d0e275ae87e7357e1b047d8d3/packages/wdio-runner/src/index.ts#L486
+ await browser.deleteSession()
+ browser.sessionId = undefined as unknown as string
+ this.browser = null
+ }
+
+ async getCDPSession(_sessionId: string): Promise {
+ return {
+ send: (method: string, params: any) => {
+ if (!this.browser) {
+ throw new Error(`The environment was torn down.`)
+ }
+ return this.browser.sendCommandAndGetResult(method, params ?? {}).catch((error) => {
+ return Promise.reject(new Error(`Failed to execute "${method}" command.`, { cause: error }))
+ })
+ },
+ on: () => {
+ throw new Error(`webdriverio provider doesn't support cdp.on()`)
+ },
+ once: () => {
+ throw new Error(`webdriverio provider doesn't support cdp.once()`)
+ },
+ off: () => {
+ throw new Error(`webdriverio provider doesn't support cdp.off()`)
+ },
+ }
+ }
+}
+
+declare module 'vitest/node' {
+ export interface UserEventClickOptions extends ClickOptions {}
+
+ export interface UserEventDragOptions extends DragAndDropOptions {
+ sourceX?: number
+ sourceY?: number
+ targetX?: number
+ targetY?: number
+ }
+
+ export interface BrowserCommandContext {
+ browser: WebdriverIO.Browser
+ }
+
+ export interface ToMatchScreenshotOptions
+ extends Omit<
+ ScreenshotMatcherOptions,
+ 'comparatorName' | 'comparatorOptions'
+ > {}
+
+ export interface ToMatchScreenshotComparators
+ extends ScreenshotComparatorRegistry {}
+}
diff --git a/packages/browser-webdriverio/tsconfig.json b/packages/browser-webdriverio/tsconfig.json
new file mode 100644
index 000000000000..89c57bbd409f
--- /dev/null
+++ b/packages/browser-webdriverio/tsconfig.json
@@ -0,0 +1,13 @@
+{
+ "extends": "../../tsconfig.base.json",
+ "compilerOptions": {
+ "types": ["node", "vite/client"],
+ "isolatedDeclarations": true
+ },
+ "exclude": [
+ "dist",
+ "node_modules",
+ "**/vite.config.ts",
+ "src/client/**/*.ts"
+ ]
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index f082329befdc..62cdf2a8d4b7 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -531,6 +531,22 @@ importers:
specifier: workspace:*
version: link:../vitest
+ packages/browser-webdriverio:
+ dependencies:
+ '@vitest/browser':
+ specifier: workspace:*
+ version: link:../browser
+ devDependencies:
+ '@wdio/types':
+ specifier: ^9.19.2
+ version: 9.19.2
+ vitest:
+ specifier: workspace:*
+ version: link:../vitest
+ webdriverio:
+ specifier: ^9.19.2
+ version: 9.19.2
+
packages/coverage-istanbul:
dependencies:
'@istanbuljs/schema':
@@ -1126,6 +1142,9 @@ importers:
'@vitest/browser':
specifier: workspace:*
version: link:../../packages/browser
+ '@vitest/browser-playwright':
+ specifier: workspace:*
+ version: link:../../packages/browser-playwright
'@vitest/bundled-lib':
specifier: link:./bundled-lib
version: link:bundled-lib
diff --git a/test/browser/package.json b/test/browser/package.json
index 114b02163666..5e5ef6364f44 100644
--- a/test/browser/package.json
+++ b/test/browser/package.json
@@ -33,6 +33,7 @@
"@types/react": "^19.1.13",
"@vitejs/plugin-basic-ssl": "^2.1.0",
"@vitest/browser": "workspace:*",
+ "@vitest/browser-playwright": "workspace:*",
"@vitest/bundled-lib": "link:./bundled-lib",
"@vitest/cjs-lib": "link:./cjs-lib",
"playwright": "^1.55.0",
diff --git a/test/browser/settings.ts b/test/browser/settings.ts
index 3da09eed68b3..bb3b1754ed8d 100644
--- a/test/browser/settings.ts
+++ b/test/browser/settings.ts
@@ -1,5 +1,5 @@
import type { BrowserInstanceOption } from 'vitest/node'
-import { playwright } from '@vitest/browser/providers/playwright'
+import { playwright } from '@vitest/browser-playwright'
import { preview } from '@vitest/browser/providers/preview'
import { webdriverio } from '@vitest/browser/providers/webdriverio'
diff --git a/test/browser/tsconfig.json b/test/browser/tsconfig.json
index 7c0b0f12cfbd..dc59446e1fd5 100644
--- a/test/browser/tsconfig.json
+++ b/test/browser/tsconfig.json
@@ -9,7 +9,6 @@
},
"types": [
"vite/client",
- "@vitest/browser/providers/playwright",
"vitest-browser-react",
"vitest/import-meta"
],
diff --git a/test/browser/vitest.config.mts b/test/browser/vitest.config.mts
index bb01b21751cb..ea9f169c02d2 100644
--- a/test/browser/vitest.config.mts
+++ b/test/browser/vitest.config.mts
@@ -2,7 +2,7 @@ import type { BrowserCommand, BrowserInstanceOption } from 'vitest/node'
import { dirname, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
import * as util from 'node:util'
-import { playwright } from '@vitest/browser/providers/playwright'
+import { playwright } from '@vitest/browser-playwright'
import { preview } from '@vitest/browser/providers/preview'
import { webdriverio } from '@vitest/browser/providers/webdriverio'
import { defineConfig } from 'vitest/config'
diff --git a/tsconfig.base.json b/tsconfig.base.json
index da5ecf972430..b127f95f647c 100644
--- a/tsconfig.base.json
+++ b/tsconfig.base.json
@@ -19,6 +19,7 @@
"@vitest/mocker/browser": ["./packages/mocker/src/browser/index.ts"],
"@vitest/runner": ["./packages/runner/src/index.ts"],
"@vitest/runner/*": ["./packages/runner/src/*"],
+ "@vitest/browser-playwright": ["./packages/browser-playwright/src/index.ts"],
"@vitest/browser": ["./packages/browser/src/node/index.ts"],
"@vitest/browser/client": ["./packages/browser/src/client/client.ts"],
"~/*": ["./packages/ui/client/*"],
From c577be9807ac6d872c7c743452bad3aa9f14eb05 Mon Sep 17 00:00:00 2001
From: Vladimir Sheremet
Date: Mon, 29 Sep 2025 11:10:18 +0200
Subject: [PATCH 05/43] feat: add preview package
---
packages/browser-preview/package.json | 46 ++++++++++++++
packages/browser-preview/rollup.config.js | 62 +++++++++++++++++++
packages/browser-preview/src/index.ts | 4 ++
packages/browser-preview/src/preview.ts | 53 ++++++++++++++++
packages/browser-preview/tsconfig.json | 13 ++++
.../browser-webdriverio/src/webdriverio.ts | 5 +-
6 files changed, 180 insertions(+), 3 deletions(-)
create mode 100644 packages/browser-preview/package.json
create mode 100644 packages/browser-preview/rollup.config.js
create mode 100644 packages/browser-preview/src/index.ts
create mode 100644 packages/browser-preview/src/preview.ts
create mode 100644 packages/browser-preview/tsconfig.json
diff --git a/packages/browser-preview/package.json b/packages/browser-preview/package.json
new file mode 100644
index 000000000000..eee3fec8a839
--- /dev/null
+++ b/packages/browser-preview/package.json
@@ -0,0 +1,46 @@
+{
+ "name": "@vitest/browser-preview",
+ "type": "module",
+ "version": "4.0.0-beta.13",
+ "description": "Browser running for Vitest using your browser of choice",
+ "license": "MIT",
+ "funding": "https://opencollective.com/vitest",
+ "homepage": "https://vitest.dev/guide/browser",
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/vitest-dev/vitest.git",
+ "directory": "packages/browser-preview"
+ },
+ "bugs": {
+ "url": "https://github.com/vitest-dev/vitest/issues"
+ },
+ "sideEffects": false,
+ "exports": {
+ ".": {
+ "types": "./dist/index.d.ts",
+ "default": "./dist/index.js"
+ },
+ "./package.json": "./package.json"
+ },
+ "main": "./dist/index.js",
+ "module": "./dist/index.js",
+ "types": "./dist/index.d.ts",
+ "files": [
+ "dist"
+ ],
+ "scripts": {
+ "build": "premove dist && pnpm rollup -c",
+ "dev": "rollup -c --watch --watch.include 'src/**'"
+ },
+ "peerDependencies": {
+ "vitest": "workspace:*"
+ },
+ "dependencies": {
+ "@testing-library/dom": "^10.4.1",
+ "@testing-library/user-event": "^14.6.1",
+ "@vitest/browser": "workspace:*"
+ },
+ "devDependencies": {
+ "vitest": "workspace:*"
+ }
+}
diff --git a/packages/browser-preview/rollup.config.js b/packages/browser-preview/rollup.config.js
new file mode 100644
index 000000000000..4a5ab941916b
--- /dev/null
+++ b/packages/browser-preview/rollup.config.js
@@ -0,0 +1,62 @@
+import { createRequire } from 'node:module'
+import commonjs from '@rollup/plugin-commonjs'
+import json from '@rollup/plugin-json'
+import resolve from '@rollup/plugin-node-resolve'
+import { defineConfig } from 'rollup'
+import oxc from 'unplugin-oxc/rollup'
+import { createDtsUtils } from '../../scripts/build-utils.js'
+
+const require = createRequire(import.meta.url)
+const pkg = require('./package.json')
+
+const external = [
+ ...Object.keys(pkg.dependencies),
+ ...Object.keys(pkg.peerDependencies || {}),
+ /^@?vitest(\/|$)/,
+ '@vitest/browser/utils',
+ 'worker_threads',
+ 'node:worker_threads',
+ 'vite',
+ 'playwright-core/types/protocol',
+]
+
+const dtsUtils = createDtsUtils()
+
+const plugins = [
+ resolve({
+ preferBuiltins: true,
+ }),
+ json(),
+ commonjs(),
+ oxc({
+ transform: { target: 'node18' },
+ }),
+]
+
+export default () =>
+ defineConfig([
+ {
+ input: './src/index.ts',
+ output: {
+ dir: 'dist',
+ format: 'esm',
+ },
+ external,
+ context: 'null',
+ plugins: [
+ ...dtsUtils.isolatedDecl(),
+ ...plugins,
+ ],
+ },
+ {
+ input: dtsUtils.dtsInput('src/index.ts'),
+ output: {
+ dir: 'dist',
+ entryFileNames: '[name].d.ts',
+ format: 'esm',
+ },
+ watch: false,
+ external,
+ plugins: dtsUtils.dts(),
+ },
+ ])
diff --git a/packages/browser-preview/src/index.ts b/packages/browser-preview/src/index.ts
new file mode 100644
index 000000000000..0ba63aaa9fda
--- /dev/null
+++ b/packages/browser-preview/src/index.ts
@@ -0,0 +1,4 @@
+export {
+ preview,
+ PreviewBrowserProvider,
+} from './preview'
diff --git a/packages/browser-preview/src/preview.ts b/packages/browser-preview/src/preview.ts
new file mode 100644
index 000000000000..5956f4906240
--- /dev/null
+++ b/packages/browser-preview/src/preview.ts
@@ -0,0 +1,53 @@
+import type { BrowserProvider, BrowserProviderOption, TestProject } from 'vitest/node'
+import { createBrowserServer } from '@vitest/browser'
+
+export function preview(): BrowserProviderOption {
+ return {
+ name: 'preview',
+ options: {},
+ providerFactory(project) {
+ return new PreviewBrowserProvider(project)
+ },
+ serverFactory: createBrowserServer,
+ }
+}
+
+export class PreviewBrowserProvider implements BrowserProvider {
+ public name = 'preview' as const
+ public supportsParallelism: boolean = false
+ private project!: TestProject
+ private open = false
+
+ constructor(project: TestProject) {
+ this.project = project
+ this.open = false
+ if (project.config.browser.headless) {
+ throw new Error(
+ 'You\'ve enabled headless mode for "preview" provider but it doesn\'t support it. Use "playwright" or "webdriverio" instead: https://vitest.dev/guide/browser/#configuration',
+ )
+ }
+ project.vitest.logger.printBrowserBanner(project)
+ }
+
+ isOpen(): boolean {
+ return this.open
+ }
+
+ getCommandsContext() {
+ return {}
+ }
+
+ async openPage(_sessionId: string, url: string): Promise {
+ this.open = true
+ if (!this.project.browser) {
+ throw new Error('Browser is not initialized')
+ }
+ const options = this.project.browser.vite.config.server
+ const _open = options.open
+ options.open = url
+ this.project.browser.vite.openBrowser()
+ options.open = _open
+ }
+
+ async close(): Promise {}
+}
diff --git a/packages/browser-preview/tsconfig.json b/packages/browser-preview/tsconfig.json
new file mode 100644
index 000000000000..89c57bbd409f
--- /dev/null
+++ b/packages/browser-preview/tsconfig.json
@@ -0,0 +1,13 @@
+{
+ "extends": "../../tsconfig.base.json",
+ "compilerOptions": {
+ "types": ["node", "vite/client"],
+ "isolatedDeclarations": true
+ },
+ "exclude": [
+ "dist",
+ "node_modules",
+ "**/vite.config.ts",
+ "src/client/**/*.ts"
+ ]
+}
diff --git a/packages/browser-webdriverio/src/webdriverio.ts b/packages/browser-webdriverio/src/webdriverio.ts
index 70929a21943e..bc01107d6a93 100644
--- a/packages/browser-webdriverio/src/webdriverio.ts
+++ b/packages/browser-webdriverio/src/webdriverio.ts
@@ -10,6 +10,7 @@ import type {
TestProject,
} from 'vitest/node'
import type { ClickOptions, DragAndDropOptions, remote } from 'webdriverio'
+import { createBrowserServer } from '@vitest/browser'
import { createDebugger } from 'vitest/node'
@@ -30,9 +31,7 @@ export function webdriverio(options: WebdriverProviderOptions = {}): BrowserProv
providerFactory(project) {
return new WebdriverBrowserProvider(project, options)
},
- // --browser.provider=webdriverio
- // @ts-expect-error hidden way to bypass importing webdriverio
- _cli: true,
+ serverFactory: createBrowserServer,
}
}
From e36df43d981beedbee4be0f912fbbbd6fbf7cec1 Mon Sep 17 00:00:00 2001
From: Vladimir Sheremet
Date: Mon, 29 Sep 2025 11:22:03 +0200
Subject: [PATCH 06/43] fix: update imports
---
examples/lit/package.json | 2 +-
examples/lit/tsconfig.json | 1 -
examples/lit/vite.config.ts | 2 +-
package.json | 3 +
packages/browser/package.json | 4 -
packages/browser/providers.d.ts | 7 -
packages/browser/rollup.config.js | 6 +-
packages/browser/src/node/providers/index.ts | 3 -
.../browser/src/node/providers/playwright.ts | 592 ------------------
.../browser/src/node/providers/preview.ts | 54 --
.../browser/src/node/providers/webdriverio.ts | 296 ---------
packages/coverage-v8/tsconfig.json | 1 -
packages/ui/package.json | 1 +
packages/ui/vitest.config.ts | 2 +-
packages/vitest/src/create/browser/creator.ts | 8 +-
.../vitest/src/node/config/resolveConfig.ts | 2 +-
pnpm-lock.yaml | 63 +-
.../fixtures/trace-view/vitest.config.ts | 2 +-
test/browser/package.json | 2 +
test/browser/settings.ts | 4 +-
test/browser/specs/playwright-connect.test.ts | 2 +-
.../browser/specs/to-match-screenshot.test.ts | 4 +-
test/browser/vitest.config.mts | 4 +-
.../browser-multiple/vitest.config.ts | 2 +-
.../config-loader/browser/vitest.config.ts | 2 +-
test/cli/fixtures/list/vitest.config.ts | 2 +-
test/cli/fixtures/public-api/vitest.config.ts | 2 +-
test/cli/package.json | 1 +
test/cli/test/annotations.test.ts | 2 +-
test/cli/test/fails.test.ts | 2 +-
test/cli/test/init.test.ts | 2 +-
test/cli/test/scoped-fixtures.test.ts | 2 +-
.../vitest.config.correct.ts | 2 +-
...vitest.config.custom-transformIndexHtml.ts | 2 +-
...itest.config.default-transformIndexHtml.ts | 2 +-
.../vitest.config.error-hook.ts | 2 +-
.../vitest.config.non-existing.ts | 2 +-
test/config/package.json | 3 +
test/config/test/bail.test.ts | 2 +-
test/config/test/browser-configs.test.ts | 6 +-
test/config/test/failures.test.ts | 6 +-
test/coverage-test/package.json | 2 +-
test/coverage-test/utils.ts | 2 +-
test/dts-playwright/package.json | 2 +-
test/dts-playwright/tsconfig.json | 1 -
test/dts-playwright/vite.config.ts | 2 +-
test/watch/package.json | 3 +-
test/watch/test/config-watching.test.ts | 2 +-
test/watch/test/file-watching.test.ts | 2 +-
test/workspaces-browser/package.json | 2 +-
.../space_browser/vitest.config.ts | 2 +-
test/workspaces-browser/vitest.config.ts | 2 +-
52 files changed, 111 insertions(+), 1020 deletions(-)
delete mode 100644 packages/browser/providers.d.ts
delete mode 100644 packages/browser/src/node/providers/index.ts
delete mode 100644 packages/browser/src/node/providers/playwright.ts
delete mode 100644 packages/browser/src/node/providers/preview.ts
delete mode 100644 packages/browser/src/node/providers/webdriverio.ts
diff --git a/examples/lit/package.json b/examples/lit/package.json
index 7d684e6e5bc6..019e3e080b59 100644
--- a/examples/lit/package.json
+++ b/examples/lit/package.json
@@ -17,7 +17,7 @@
"lit": "^3.3.1"
},
"devDependencies": {
- "@vitest/browser": "latest",
+ "@vitest/browser-playwright": "latest",
"jsdom": "latest",
"playwright": "^1.55.0",
"vite": "latest",
diff --git a/examples/lit/tsconfig.json b/examples/lit/tsconfig.json
index 51f02ea20c1f..8f53fa03471a 100644
--- a/examples/lit/tsconfig.json
+++ b/examples/lit/tsconfig.json
@@ -5,7 +5,6 @@
"experimentalDecorators": true,
"module": "node16",
"moduleResolution": "Node16",
- "types": ["@vitest/browser/providers/playwright"],
"verbatimModuleSyntax": true
}
}
diff --git a/examples/lit/vite.config.ts b/examples/lit/vite.config.ts
index d4a0022e4b28..4c6060526bec 100644
--- a/examples/lit/vite.config.ts
+++ b/examples/lit/vite.config.ts
@@ -1,6 +1,6 @@
///
-import { playwright } from '@vitest/browser/providers/playwright'
+import { playwright } from '@vitest/browser-playwright'
import { defineConfig } from 'vite'
// https://vitejs.dev/config/
diff --git a/package.json b/package.json
index 4d6d1304ef1e..28a699f75548 100644
--- a/package.json
+++ b/package.json
@@ -71,6 +71,9 @@
"pnpm": {
"overrides": {
"@vitest/browser": "workspace:*",
+ "@vitest/browser-playwright": "workspace:*",
+ "@vitest/browser-preview": "workspace:*",
+ "@vitest/browser-webdriverio": "workspace:*",
"@vitest/ui": "workspace:*",
"acorn": "8.11.3",
"mlly": "^1.8.0",
diff --git a/packages/browser/package.json b/packages/browser/package.json
index 57c03ddcf674..ba9aa58c2f86 100644
--- a/packages/browser/package.json
+++ b/packages/browser/package.json
@@ -20,10 +20,6 @@
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
- "./providers/*": {
- "types": "./dist/providers/*.d.ts",
- "default": "./dist/providers/*.js"
- },
"./context": {
"types": "./context.d.ts",
"default": "./context.js"
diff --git a/packages/browser/providers.d.ts b/packages/browser/providers.d.ts
deleted file mode 100644
index 1106f2d6db06..000000000000
--- a/packages/browser/providers.d.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-import type { BrowserProviderModule } from 'vitest/node'
-
-declare const webdriverio: BrowserProviderModule
-declare const playwright: BrowserProviderModule
-declare const preview: BrowserProviderModule
-
-export { webdriverio, playwright, preview }
diff --git a/packages/browser/rollup.config.js b/packages/browser/rollup.config.js
index 6b56b70341aa..780c28294aca 100644
--- a/packages/browser/rollup.config.js
+++ b/packages/browser/rollup.config.js
@@ -17,7 +17,6 @@ const external = [
'worker_threads',
'node:worker_threads',
'vite',
- 'playwright-core/types/protocol',
]
const dtsUtils = createDtsUtils()
@@ -39,10 +38,7 @@ const plugins = [
]
const input = {
- 'index': './src/node/index.ts',
- 'providers/playwright': './src/node/providers/playwright.ts',
- 'providers/webdriverio': './src/node/providers/webdriverio.ts',
- 'providers/preview': './src/node/providers/preview.ts',
+ index: './src/node/index.ts',
}
export default () =>
diff --git a/packages/browser/src/node/providers/index.ts b/packages/browser/src/node/providers/index.ts
deleted file mode 100644
index a75c6cb36a28..000000000000
--- a/packages/browser/src/node/providers/index.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-export { playwright } from './playwright'
-export { preview } from './preview'
-export { webdriverio } from './webdriverio'
diff --git a/packages/browser/src/node/providers/playwright.ts b/packages/browser/src/node/providers/playwright.ts
deleted file mode 100644
index 95025fbbbc3b..000000000000
--- a/packages/browser/src/node/providers/playwright.ts
+++ /dev/null
@@ -1,592 +0,0 @@
-/* eslint-disable ts/method-signature-style */
-
-import type {
- ScreenshotComparatorRegistry,
- ScreenshotMatcherOptions,
-} from '@vitest/browser/context'
-import type { MockedModule } from '@vitest/mocker'
-import type {
- Browser,
- BrowserContext,
- BrowserContextOptions,
- ConnectOptions,
- Frame,
- FrameLocator,
- LaunchOptions,
- Page,
-} from 'playwright'
-import type { Protocol } from 'playwright-core/types/protocol'
-import type { SourceMap } from 'rollup'
-import type { ResolvedConfig } from 'vite'
-import type {
- BrowserModuleMocker,
- BrowserProvider,
- BrowserProviderOption,
- CDPSession,
- TestProject,
-} from 'vitest/node'
-import { createManualModuleSource } from '@vitest/mocker/node'
-import c from 'tinyrainbow'
-import { createDebugger, isCSSRequest } from 'vitest/node'
-
-const debug = createDebugger('vitest:browser:playwright')
-
-const playwrightBrowsers = ['firefox', 'webkit', 'chromium'] as const
-type PlaywrightBrowser = (typeof playwrightBrowsers)[number]
-
-export interface PlaywrightProviderOptions {
- /**
- * The options passed down to [`playwright.connect`](https://playwright.dev/docs/api/class-browsertype#browser-type-launch) method.
- * @see {@link https://playwright.dev/docs/api/class-browsertype#browser-type-launch}
- */
- launchOptions?: Omit<
- LaunchOptions,
- 'tracesDir'
- >
- /**
- * The options passed down to [`playwright.connect`](https://playwright.dev/docs/api/class-browsertype#browser-type-connect) method.
- *
- * This is used only if you connect remotely to the playwright instance via a WebSocket connection.
- * @see {@link https://playwright.dev/docs/api/class-browsertype#browser-type-connect}
- */
- connectOptions?: ConnectOptions & {
- wsEndpoint: string
- }
- /**
- * The options passed down to [`browser.newContext`](https://playwright.dev/docs/api/class-browser#browser-new-context) method.
- * @see {@link https://playwright.dev/docs/api/class-browser#browser-new-context}
- */
- contextOptions?: Omit<
- BrowserContextOptions,
- 'ignoreHTTPSErrors' | 'serviceWorkers'
- >
- /**
- * The maximum time in milliseconds to wait for `userEvent` action to complete.
- * @default 0 (no timeout)
- */
- actionTimeout?: number
-}
-
-export function playwright(options: PlaywrightProviderOptions = {}): BrowserProviderOption {
- return {
- name: 'playwright',
- supportedBrowser: playwrightBrowsers,
- options,
- providerFactory(project) {
- return new PlaywrightBrowserProvider(project, options)
- },
- // --browser.provider=playwright
- // @ts-expect-error hidden way to bypass importing playwright
- _cli: true,
- }
-}
-
-export class PlaywrightBrowserProvider implements BrowserProvider {
- public name = 'playwright' as const
- public supportsParallelism = true
-
- public browser: Browser | null = null
-
- public contexts: Map = new Map()
- public pages: Map = new Map()
- public mocker: BrowserModuleMocker
- public browserName: PlaywrightBrowser
-
- private browserPromise: Promise | null = null
- private closing = false
-
- public tracingContexts: Set = new Set()
- public pendingTraces: Map = new Map()
-
- constructor(
- private project: TestProject,
- private options: PlaywrightProviderOptions,
- ) {
- this.browserName = project.config.browser.name as PlaywrightBrowser
- this.mocker = this.createMocker()
-
- // make sure the traces are finished if the test hangs
- process.on('SIGTERM', () => {
- if (!this.browser) {
- return
- }
- const promises = []
- for (const [trace, contextId] of this.pendingTraces.entries()) {
- promises.push((() => {
- const context = this.contexts.get(contextId)
- return context?.tracing.stopChunk({ path: trace })
- })())
- }
- return Promise.allSettled(promises)
- })
- }
-
- private async openBrowser() {
- await this._throwIfClosing()
-
- if (this.browserPromise) {
- debug?.('[%s] the browser is resolving, reusing the promise', this.browserName)
- return this.browserPromise
- }
-
- if (this.browser) {
- debug?.('[%s] the browser is resolved, reusing it', this.browserName)
- return this.browser
- }
-
- this.browserPromise = (async () => {
- const options = this.project.config.browser
-
- const playwright = await import('playwright')
-
- if (this.options.connectOptions) {
- if (this.options.launchOptions) {
- this.project.vitest.logger.warn(
- c.yellow(`Found both ${c.bold(c.italic(c.yellow('connect')))} and ${c.bold(c.italic(c.yellow('launch')))} options in browser instance configuration.
- Ignoring ${c.bold(c.italic(c.yellow('launch')))} options and using ${c.bold(c.italic(c.yellow('connect')))} mode.
- You probably want to remove one of the two options and keep only the one you want to use.`),
- )
- }
- const browser = await playwright[this.browserName].connect(this.options.connectOptions.wsEndpoint, this.options.connectOptions)
- this.browser = browser
- this.browserPromise = null
- return this.browser
- }
-
- const launchOptions: LaunchOptions = {
- ...this.options.launchOptions,
- headless: options.headless,
- }
-
- if (typeof options.trace === 'object' && options.trace.tracesDir) {
- launchOptions.tracesDir = options.trace?.tracesDir
- }
-
- const inspector = this.project.vitest.config.inspector
- if (inspector.enabled) {
- // NodeJS equivalent defaults: https://nodejs.org/en/learn/getting-started/debugging#enable-inspector
- const port = inspector.port || 9229
- const host = inspector.host || '127.0.0.1'
-
- launchOptions.args ||= []
- launchOptions.args.push(`--remote-debugging-port=${port}`)
- launchOptions.args.push(`--remote-debugging-address=${host}`)
-
- this.project.vitest.logger.log(`Debugger listening on ws://${host}:${port}`)
- }
-
- // start Vitest UI maximized only on supported browsers
- if (this.project.config.browser.ui && this.browserName === 'chromium') {
- if (!launchOptions.args) {
- launchOptions.args = []
- }
- if (!launchOptions.args.includes('--start-maximized') && !launchOptions.args.includes('--start-fullscreen')) {
- launchOptions.args.push('--start-maximized')
- }
- }
-
- debug?.('[%s] initializing the browser with launch options: %O', this.browserName, launchOptions)
- this.browser = await playwright[this.browserName].launch(launchOptions)
- this.browserPromise = null
- return this.browser
- })()
-
- return this.browserPromise
- }
-
- private createMocker(): BrowserModuleMocker {
- const idPreficates = new Map boolean>()
- const sessionIds = new Map()
-
- function createPredicate(sessionId: string, url: string) {
- const moduleUrl = new URL(url, 'http://localhost')
- const predicate = (url: URL) => {
- if (url.searchParams.has('_vitest_original')) {
- return false
- }
-
- // different modules, ignore request
- if (url.pathname !== moduleUrl.pathname) {
- return false
- }
-
- url.searchParams.delete('t')
- url.searchParams.delete('v')
- url.searchParams.delete('import')
-
- // different search params, ignore request
- if (url.searchParams.size !== moduleUrl.searchParams.size) {
- return false
- }
-
- // check that all search params are the same
- for (const [param, value] of url.searchParams.entries()) {
- if (moduleUrl.searchParams.get(param) !== value) {
- return false
- }
- }
-
- return true
- }
- const ids = sessionIds.get(sessionId) || []
- ids.push(moduleUrl.href)
- sessionIds.set(sessionId, ids)
- idPreficates.set(predicateKey(sessionId, moduleUrl.href), predicate)
- return predicate
- }
-
- function predicateKey(sessionId: string, url: string) {
- return `${sessionId}:${url}`
- }
-
- return {
- register: async (sessionId: string, module: MockedModule): Promise => {
- const page = this.getPage(sessionId)
- await page.route(createPredicate(sessionId, module.url), async (route) => {
- if (module.type === 'manual') {
- const exports = Object.keys(await module.resolve())
- const body = createManualModuleSource(module.url, exports)
- return route.fulfill({
- body,
- headers: getHeaders(this.project.browser!.vite.config),
- })
- }
-
- // webkit doesn't support redirect responses
- // https://github.com/microsoft/playwright/issues/18318
- const isWebkit = this.browserName === 'webkit'
- if (isWebkit) {
- let url: string
- if (module.type === 'redirect') {
- const redirect = new URL(module.redirect)
- url = redirect.href.slice(redirect.origin.length)
- }
- else {
- const request = new URL(route.request().url())
- request.searchParams.set('mock', module.type)
- url = request.href.slice(request.origin.length)
- }
-
- const result = await this.project.browser!.vite.transformRequest(url).catch(() => null)
- if (!result) {
- return route.continue()
- }
- let content = result.code
- if (result.map && 'version' in result.map && result.map.mappings) {
- const type = isDirectCSSRequest(url) ? 'css' : 'js'
- content = getCodeWithSourcemap(type, content.toString(), result.map)
- }
- return route.fulfill({
- body: content,
- headers: getHeaders(this.project.browser!.vite.config),
- })
- }
-
- if (module.type === 'redirect') {
- return route.fulfill({
- status: 302,
- headers: {
- Location: module.redirect,
- },
- })
- }
- else if (module.type === 'automock' || module.type === 'autospy') {
- const url = new URL(route.request().url())
- url.searchParams.set('mock', module.type)
- return route.fulfill({
- status: 302,
- headers: {
- Location: url.href,
- },
- })
- }
- else {
- // all types are exhausted
- const _module: never = module
- }
- })
- },
- delete: async (sessionId: string, id: string): Promise => {
- const page = this.getPage(sessionId)
- const key = predicateKey(sessionId, id)
- const predicate = idPreficates.get(key)
- if (predicate) {
- await page.unroute(predicate).finally(() => idPreficates.delete(key))
- }
- },
- clear: async (sessionId: string): Promise => {
- const page = this.getPage(sessionId)
- const ids = sessionIds.get(sessionId) || []
- const promises = ids.map((id) => {
- const key = predicateKey(sessionId, id)
- const predicate = idPreficates.get(key)
- if (predicate) {
- return page.unroute(predicate).finally(() => idPreficates.delete(key))
- }
- return null
- })
- await Promise.all(promises).finally(() => sessionIds.delete(sessionId))
- },
- }
- }
-
- private async createContext(sessionId: string) {
- await this._throwIfClosing()
-
- if (this.contexts.has(sessionId)) {
- debug?.('[%s][%s] the context already exists, reusing it', sessionId, this.browserName)
- return this.contexts.get(sessionId)!
- }
-
- 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 context = await browser.newContext(options)
- await this._throwIfClosing(context)
- if (actionTimeout != null) {
- context.setDefaultTimeout(actionTimeout)
- }
- debug?.('[%s][%s] the context is ready', sessionId, this.browserName)
- this.contexts.set(sessionId, context)
- return context
- }
-
- public getPage(sessionId: string): Page {
- const page = this.pages.get(sessionId)
- if (!page) {
- throw new Error(`Page "${sessionId}" not found in ${this.browserName} browser.`)
- }
- return page
- }
-
- public getCommandsContext(sessionId: string): {
- page: Page
- context: BrowserContext
- frame: () => Promise
- readonly iframe: FrameLocator
- } {
- const page = this.getPage(sessionId)
- return {
- page,
- context: this.contexts.get(sessionId)!,
- frame(): Promise {
- return new Promise((resolve, reject) => {
- const frame = page.frame('vitest-iframe')
- if (frame) {
- return resolve(frame)
- }
-
- const timeout = setTimeout(() => {
- const err = new Error(`Cannot find "vitest-iframe" on the page. This is a bug in Vitest, please report it.`)
- reject(err)
- }, 1000).unref()
- page.on('frameattached', (frame) => {
- clearTimeout(timeout)
- resolve(frame)
- })
- })
- },
- get iframe(): FrameLocator {
- return page.frameLocator('[data-vitest="true"]')!
- },
- }
- }
-
- private async openBrowserPage(sessionId: string) {
- await this._throwIfClosing()
-
- if (this.pages.has(sessionId)) {
- debug?.('[%s][%s] the page already exists, closing the old one', sessionId, this.browserName)
- const page = this.pages.get(sessionId)!
- await page.close()
- this.pages.delete(sessionId)
- }
-
- const context = await this.createContext(sessionId)
- const page = await context.newPage()
- debug?.('[%s][%s] the page is ready', sessionId, this.browserName)
- await this._throwIfClosing(page)
- this.pages.set(sessionId, page)
-
- if (process.env.VITEST_PW_DEBUG) {
- page.on('requestfailed', (request) => {
- console.error(
- '[PW Error]',
- request.resourceType(),
- 'request failed for',
- request.url(),
- 'url:',
- request.failure()?.errorText,
- )
- })
- }
-
- return page
- }
-
- async openPage(sessionId: string, url: string, beforeNavigate?: () => Promise): Promise {
- debug?.('[%s][%s] creating the browser page for %s', sessionId, this.browserName, url)
- const browserPage = await this.openBrowserPage(sessionId)
- await beforeNavigate?.()
- debug?.('[%s][%s] browser page is created, opening %s', sessionId, this.browserName, url)
- await browserPage.goto(url, { timeout: 0 })
- await this._throwIfClosing(browserPage)
- }
-
- private async _throwIfClosing(disposable?: { close: () => Promise }) {
- if (this.closing) {
- debug?.('[%s] provider was closed, cannot perform the action on %s', this.browserName, String(disposable))
- await disposable?.close()
- this.pages.clear()
- this.contexts.clear()
- this.browser = null
- this.browserPromise = null
- throw new Error(`[vitest] The provider was closed.`)
- }
- }
-
- async getCDPSession(sessionid: string): Promise {
- const page = this.getPage(sessionid)
- const cdp = await page.context().newCDPSession(page)
- return {
- async send(method: string, params: any) {
- const result = await cdp.send(method as 'DOM.querySelector', params)
- return result as unknown
- },
- on(event: string, listener: (...args: any[]) => void) {
- cdp.on(event as 'Accessibility.loadComplete', listener)
- },
- off(event: string, listener: (...args: any[]) => void) {
- cdp.off(event as 'Accessibility.loadComplete', listener)
- },
- once(event: string, listener: (...args: any[]) => void) {
- cdp.once(event as 'Accessibility.loadComplete', listener)
- },
- }
- }
-
- async close(): Promise {
- debug?.('[%s] closing provider', this.browserName)
- this.closing = true
- if (this.browserPromise) {
- await this.browserPromise
- this.browserPromise = null
- }
- const browser = this.browser
- 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()))
- this.contexts.clear()
- await browser?.close()
- debug?.('[%s] provider is closed', this.browserName)
- }
-}
-
-function getHeaders(config: ResolvedConfig) {
- const headers: Record = {
- 'Content-Type': 'application/javascript',
- }
-
- for (const name in config.server.headers) {
- headers[name] = String(config.server.headers[name]!)
- }
- return headers
-}
-
-function getCodeWithSourcemap(
- type: 'js' | 'css',
- code: string,
- map: SourceMap,
-): string {
- if (type === 'js') {
- code += `\n//# sourceMappingURL=${genSourceMapUrl(map)}`
- }
- else if (type === 'css') {
- code += `\n/*# sourceMappingURL=${genSourceMapUrl(map)} */`
- }
-
- return code
-}
-
-function genSourceMapUrl(map: SourceMap | string): string {
- if (typeof map !== 'string') {
- map = JSON.stringify(map)
- }
- return `data:application/json;base64,${Buffer.from(map).toString('base64')}`
-}
-
-const directRequestRE = /[?&]direct\b/
-
-function isDirectCSSRequest(request: string): boolean {
- return isCSSRequest(request) && directRequestRE.test(request)
-}
-
-declare module 'vitest/node' {
- export interface BrowserCommandContext {
- page: Page
- frame(): Promise
- iframe: FrameLocator
- context: BrowserContext
- }
-
- export interface ToMatchScreenshotOptions
- extends Omit<
- ScreenshotMatcherOptions,
- 'comparatorName' | 'comparatorOptions'
- > {}
-
- export interface ToMatchScreenshotComparators
- extends ScreenshotComparatorRegistry {}
-}
-
-type PWHoverOptions = NonNullable[1]>
-type PWClickOptions = NonNullable[1]>
-type PWDoubleClickOptions = NonNullable[1]>
-type PWFillOptions = NonNullable[2]>
-type PWScreenshotOptions = NonNullable[0]>
-type PWSelectOptions = NonNullable[2]>
-type PWDragAndDropOptions = NonNullable[2]>
-type PWSetInputFiles = NonNullable[2]>
-
-declare module '@vitest/browser/context' {
- export interface UserEventHoverOptions extends PWHoverOptions {}
- export interface UserEventClickOptions extends PWClickOptions {}
- export interface UserEventDoubleClickOptions extends PWDoubleClickOptions {}
- export interface UserEventTripleClickOptions extends PWClickOptions {}
- export interface UserEventFillOptions extends PWFillOptions {}
- export interface UserEventSelectOptions extends PWSelectOptions {}
- export interface UserEventDragAndDropOptions extends PWDragAndDropOptions {}
- export interface UserEventUploadOptions extends PWSetInputFiles {}
-
- export interface ScreenshotOptions extends Omit {
- mask?: ReadonlyArray | undefined
- }
-
- export interface CDPSession {
- send(
- method: T,
- params?: Protocol.CommandParameters[T]
- ): Promise
- on(
- event: T,
- listener: (payload: Protocol.Events[T]) => void
- ): this
- once(
- event: T,
- listener: (payload: Protocol.Events[T]) => void
- ): this
- off(
- event: T,
- listener: (payload: Protocol.Events[T]) => void
- ): this
- }
-}
diff --git a/packages/browser/src/node/providers/preview.ts b/packages/browser/src/node/providers/preview.ts
deleted file mode 100644
index 7e648122c87f..000000000000
--- a/packages/browser/src/node/providers/preview.ts
+++ /dev/null
@@ -1,54 +0,0 @@
-import type { BrowserProvider, BrowserProviderOption, TestProject } from 'vitest/node'
-
-export function preview(): BrowserProviderOption {
- return {
- name: 'preview',
- options: {},
- providerFactory(project) {
- return new PreviewBrowserProvider(project)
- },
- // --browser.provider=preview
- // @ts-expect-error hidden way to bypass importing preview
- _cli: true,
- }
-}
-
-export class PreviewBrowserProvider implements BrowserProvider {
- public name = 'preview' as const
- public supportsParallelism: boolean = false
- private project!: TestProject
- private open = false
-
- constructor(project: TestProject) {
- this.project = project
- this.open = false
- if (project.config.browser.headless) {
- throw new Error(
- 'You\'ve enabled headless mode for "preview" provider but it doesn\'t support it. Use "playwright" or "webdriverio" instead: https://vitest.dev/guide/browser/#configuration',
- )
- }
- project.vitest.logger.printBrowserBanner(project)
- }
-
- isOpen(): boolean {
- return this.open
- }
-
- getCommandsContext() {
- return {}
- }
-
- async openPage(_sessionId: string, url: string): Promise {
- this.open = true
- if (!this.project.browser) {
- throw new Error('Browser is not initialized')
- }
- const options = this.project.browser.vite.config.server
- const _open = options.open
- options.open = url
- this.project.browser.vite.openBrowser()
- options.open = _open
- }
-
- async close(): Promise {}
-}
diff --git a/packages/browser/src/node/providers/webdriverio.ts b/packages/browser/src/node/providers/webdriverio.ts
deleted file mode 100644
index c5b985348231..000000000000
--- a/packages/browser/src/node/providers/webdriverio.ts
+++ /dev/null
@@ -1,296 +0,0 @@
-import type {
- ScreenshotComparatorRegistry,
- ScreenshotMatcherOptions,
-} from '@vitest/browser/context'
-import type { Capabilities } from '@wdio/types'
-import type {
- BrowserProvider,
- BrowserProviderOption,
- CDPSession,
- TestProject,
-} from 'vitest/node'
-import type { ClickOptions, DragAndDropOptions, remote } from 'webdriverio'
-
-import { createDebugger } from 'vitest/node'
-
-const debug = createDebugger('vitest:browser:wdio')
-
-const webdriverBrowsers = ['firefox', 'chrome', 'edge', 'safari'] as const
-type WebdriverBrowser = (typeof webdriverBrowsers)[number]
-
-interface WebdriverProviderOptions extends Partial<
- Parameters[0]
-> {}
-
-export function webdriverio(options: WebdriverProviderOptions = {}): BrowserProviderOption {
- return {
- name: 'webdriverio',
- supportedBrowser: webdriverBrowsers,
- options,
- providerFactory(project) {
- return new WebdriverBrowserProvider(project, options)
- },
- // --browser.provider=webdriverio
- // @ts-expect-error hidden way to bypass importing webdriverio
- _cli: true,
- }
-}
-
-export class WebdriverBrowserProvider implements BrowserProvider {
- public name = 'webdriverio' as const
- public supportsParallelism: boolean = false
-
- public browser: WebdriverIO.Browser | null = null
-
- private browserName!: WebdriverBrowser
- private project!: TestProject
-
- private options?: WebdriverProviderOptions
-
- private closing = false
- private iframeSwitched = false
- private topLevelContext: string | undefined
-
- getSupportedBrowsers(): readonly string[] {
- return webdriverBrowsers
- }
-
- constructor(
- project: TestProject,
- options: WebdriverProviderOptions,
- ) {
- // increase shutdown timeout because WDIO takes some extra time to kill the driver
- if (!project.vitest.state._data.timeoutIncreased) {
- project.vitest.state._data.timeoutIncreased = true
- project.vitest.config.teardownTimeout += 10_000
- }
-
- this.closing = false
- this.project = project
- this.browserName = project.config.browser.name as WebdriverBrowser
- this.options = options
- }
-
- isIframeSwitched(): boolean {
- return this.iframeSwitched
- }
-
- async switchToTestFrame(): Promise {
- const browser = this.browser!
- // support wdio@9
- if (browser.switchFrame) {
- await browser.switchFrame(browser.$('iframe[data-vitest]'))
- }
- else {
- const iframe = await browser.findElement(
- 'css selector',
- 'iframe[data-vitest]',
- )
- await browser.switchToFrame(iframe)
- }
- this.iframeSwitched = true
- }
-
- async switchToMainFrame(): Promise {
- const page = this.browser!
- if (page.switchFrame) {
- await page.switchFrame(null)
- }
- else {
- await page.switchToParentFrame()
- }
- this.iframeSwitched = false
- }
-
- async setViewport(options: { width: number; height: number }): Promise {
- if (this.topLevelContext == null || !this.browser) {
- throw new Error(`The browser has no open pages.`)
- }
- await this.browser.send({
- method: 'browsingContext.setViewport',
- params: {
- context: this.topLevelContext,
- devicePixelRatio: 1,
- viewport: options,
- },
- })
- }
-
- getCommandsContext(): {
- browser: WebdriverIO.Browser | null
- } {
- return {
- browser: this.browser,
- }
- }
-
- async openBrowser(): Promise {
- await this._throwIfClosing('opening the browser')
-
- if (this.browser) {
- debug?.('[%s] the browser is already opened, reusing it', this.browserName)
- return this.browser
- }
-
- const options = this.project.config.browser
-
- if (this.browserName === 'safari') {
- if (options.headless) {
- throw new Error(
- 'You\'ve enabled headless mode for Safari but it doesn\'t currently support it.',
- )
- }
- }
-
- const { remote } = await import('webdriverio')
-
- const remoteOptions: Capabilities.WebdriverIOConfig = {
- logLevel: 'silent',
- ...this.options,
- capabilities: this.buildCapabilities(),
- }
-
- debug?.('[%s] opening the browser with options: %O', this.browserName, remoteOptions)
- // TODO: close everything, if browser is closed from the outside
- this.browser = await remote(remoteOptions)
- await this._throwIfClosing()
-
- return this.browser
- }
-
- private buildCapabilities() {
- const capabilities: Capabilities.WebdriverIOConfig['capabilities'] = {
- ...this.options?.capabilities,
- browserName: this.browserName,
- }
-
- const headlessMap = {
- chrome: ['goog:chromeOptions', ['headless', 'disable-gpu']],
- firefox: ['moz:firefoxOptions', ['-headless']],
- edge: ['ms:edgeOptions', ['--headless']],
- } as const
-
- const options = this.project.config.browser
- const browser = this.browserName
- if (browser !== 'safari' && options.headless) {
- const [key, args] = headlessMap[browser]
- const currentValues = (this.options?.capabilities as any)?.[key] || {}
- const newArgs = [...(currentValues.args || []), ...args]
- capabilities[key] = { ...currentValues, args: newArgs as any }
- }
-
- // start Vitest UI maximized only on supported browsers
- if (options.ui && (browser === 'chrome' || browser === 'edge')) {
- const key = browser === 'chrome'
- ? 'goog:chromeOptions'
- : 'ms:edgeOptions'
- const args = capabilities[key]?.args || []
- if (!args.includes('--start-maximized') && !args.includes('--start-fullscreen')) {
- args.push('--start-maximized')
- }
- capabilities[key] ??= {}
- capabilities[key]!.args = args
- }
-
- const inspector = this.project.vitest.config.inspector
- if (inspector.enabled && (browser === 'chrome' || browser === 'edge')) {
- const key = browser === 'chrome'
- ? 'goog:chromeOptions'
- : 'ms:edgeOptions'
- const args = capabilities[key]?.args || []
-
- // NodeJS equivalent defaults: https://nodejs.org/en/learn/getting-started/debugging#enable-inspector
- const port = inspector.port || 9229
- const host = inspector.host || '127.0.0.1'
-
- args.push(`--remote-debugging-port=${port}`)
- args.push(`--remote-debugging-address=${host}`)
-
- this.project.vitest.logger.log(`Debugger listening on ws://${host}:${port}`)
-
- capabilities[key] ??= {}
- capabilities[key]!.args = args
- }
-
- return capabilities
- }
-
- async openPage(sessionId: string, url: string): Promise {
- await this._throwIfClosing('creating the browser')
- debug?.('[%s][%s] creating the browser page for %s', sessionId, this.browserName, url)
- const browserInstance = await this.openBrowser()
- debug?.('[%s][%s] browser page is created, opening %s', sessionId, this.browserName, url)
- await browserInstance.url(url)
- this.topLevelContext = await browserInstance.getWindowHandle()
- await this._throwIfClosing('opening the url')
- }
-
- private async _throwIfClosing(action?: string) {
- if (this.closing) {
- debug?.(`[%s] provider was closed, cannot perform the action${action ? ` ${action}` : ''}`, this.browserName)
- await (this.browser?.sessionId ? this.browser?.deleteSession?.() : null)
- throw new Error(`[vitest] The provider was closed.`)
- }
- }
-
- async close(): Promise {
- debug?.('[%s] closing provider', this.browserName)
- this.closing = true
- const browser = this.browser
- const sessionId = browser?.sessionId
- if (!browser || !sessionId) {
- return
- }
-
- // https://github.com/webdriverio/webdriverio/blob/ab1a2e82b13a9c7d0e275ae87e7357e1b047d8d3/packages/wdio-runner/src/index.ts#L486
- await browser.deleteSession()
- browser.sessionId = undefined as unknown as string
- this.browser = null
- }
-
- async getCDPSession(_sessionId: string): Promise {
- return {
- send: (method: string, params: any) => {
- if (!this.browser) {
- throw new Error(`The environment was torn down.`)
- }
- return this.browser.sendCommandAndGetResult(method, params ?? {}).catch((error) => {
- return Promise.reject(new Error(`Failed to execute "${method}" command.`, { cause: error }))
- })
- },
- on: () => {
- throw new Error(`webdriverio provider doesn't support cdp.on()`)
- },
- once: () => {
- throw new Error(`webdriverio provider doesn't support cdp.once()`)
- },
- off: () => {
- throw new Error(`webdriverio provider doesn't support cdp.off()`)
- },
- }
- }
-}
-
-declare module 'vitest/node' {
- export interface UserEventClickOptions extends ClickOptions {}
-
- export interface UserEventDragOptions extends DragAndDropOptions {
- sourceX?: number
- sourceY?: number
- targetX?: number
- targetY?: number
- }
-
- export interface BrowserCommandContext {
- browser: WebdriverIO.Browser
- }
-
- export interface ToMatchScreenshotOptions
- extends Omit<
- ScreenshotMatcherOptions,
- 'comparatorName' | 'comparatorOptions'
- > {}
-
- export interface ToMatchScreenshotComparators
- extends ScreenshotComparatorRegistry {}
-}
diff --git a/packages/coverage-v8/tsconfig.json b/packages/coverage-v8/tsconfig.json
index 8ff7219b1b7e..93e30d6aadd0 100644
--- a/packages/coverage-v8/tsconfig.json
+++ b/packages/coverage-v8/tsconfig.json
@@ -2,7 +2,6 @@
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"moduleResolution": "Bundler",
- "types": ["@vitest/browser/providers/playwright"],
"isolatedDeclarations": true
},
"include": ["./src/**/*.ts"],
diff --git a/packages/ui/package.json b/packages/ui/package.json
index 2b72cc701ea3..f8c551b06368 100644
--- a/packages/ui/package.json
+++ b/packages/ui/package.json
@@ -66,6 +66,7 @@
"@types/ws": "catalog:",
"@unocss/reset": "catalog:",
"@vitejs/plugin-vue": "catalog:",
+ "@vitest/browser-playwright": "workspace:*",
"@vitest/runner": "workspace:*",
"@vitest/ws-client": "workspace:*",
"@vue/test-utils": "^2.4.6",
diff --git a/packages/ui/vitest.config.ts b/packages/ui/vitest.config.ts
index 1c3e3e320adf..075a3a10d2de 100644
--- a/packages/ui/vitest.config.ts
+++ b/packages/ui/vitest.config.ts
@@ -1,4 +1,4 @@
-import { playwright } from '@vitest/browser/providers/playwright'
+import { playwright } from '@vitest/browser-playwright'
import { mergeConfig } from 'vite'
import { defineConfig } from 'vitest/config'
import viteConfig from './vite.config'
diff --git a/packages/vitest/src/create/browser/creator.ts b/packages/vitest/src/create/browser/creator.ts
index 6563c2868835..37e95b674f65 100644
--- a/packages/vitest/src/create/browser/creator.ts
+++ b/packages/vitest/src/create/browser/creator.ts
@@ -45,12 +45,12 @@ function getProviderPackageNames(provider: BrowserBuiltinProvider) {
switch (provider) {
case 'webdriverio':
return {
- types: '@vitest/browser/providers/webdriverio',
+ types: '@vitest/browser-webdriverio',
pkg: 'webdriverio',
}
case 'playwright':
return {
- types: '@vitest/browser/providers/playwright',
+ types: '@vitest/browser-playwright',
pkg: 'playwright',
}
case 'preview':
@@ -295,7 +295,7 @@ async function generateFrameworkConfigFile(options: {
const configContent = [
`import { defineConfig } from 'vitest/config'`,
- `import { ${options.provider} } from '@vitest/browser/providers/${options.provider}'`,
+ `import { ${options.provider} } from '@vitest/browser-${options.provider}'`,
options.frameworkPlugin ? frameworkImport : null,
``,
'export default defineConfig({',
@@ -433,7 +433,7 @@ export async function create(): Promise {
}
const dependenciesToInstall = [
- '@vitest/browser',
+ `@vitest/browser-${provider}`,
]
const frameworkPackage = getFrameworkTestPackage(framework)
diff --git a/packages/vitest/src/node/config/resolveConfig.ts b/packages/vitest/src/node/config/resolveConfig.ts
index b8e67fc636d3..1206363046de 100644
--- a/packages/vitest/src/node/config/resolveConfig.ts
+++ b/packages/vitest/src/node/config/resolveConfig.ts
@@ -755,7 +755,7 @@ export function resolveConfig(
}
if (typeof resolved.browser.provider === 'string') {
- const source = `@vitest/browser/providers/${resolved.browser.provider}`
+ const source = `@vitest/browser-${resolved.browser.provider}`
throw new TypeError(
'The `browser.provider` configuration was changed to accept a factory instead of a string. '
+ `Add an import of "${resolved.browser.provider}" from "${source}" instead. See: https://vitest.dev/guide/browser/config#provider`,
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 62cdf2a8d4b7..8bb4e9081954 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -120,6 +120,9 @@ catalogs:
overrides:
'@vitest/browser': workspace:*
+ '@vitest/browser-playwright': workspace:*
+ '@vitest/browser-preview': workspace:*
+ '@vitest/browser-webdriverio': workspace:*
'@vitest/ui': workspace:*
acorn: 8.11.3
mlly: ^1.8.0
@@ -351,9 +354,9 @@ importers:
specifier: ^3.3.1
version: 3.3.1
devDependencies:
- '@vitest/browser':
+ '@vitest/browser-playwright':
specifier: workspace:*
- version: link:../../packages/browser
+ version: link:../../packages/browser-playwright
jsdom:
specifier: latest
version: 27.0.0(postcss@8.5.6)
@@ -531,6 +534,22 @@ importers:
specifier: workspace:*
version: link:../vitest
+ packages/browser-preview:
+ dependencies:
+ '@testing-library/dom':
+ specifier: ^10.4.1
+ version: 10.4.1
+ '@testing-library/user-event':
+ specifier: ^14.6.1
+ version: 14.6.1(@testing-library/dom@10.4.1)
+ '@vitest/browser':
+ specifier: workspace:*
+ version: link:../browser
+ devDependencies:
+ vitest:
+ specifier: workspace:*
+ version: link:../vitest
+
packages/browser-webdriverio:
dependencies:
'@vitest/browser':
@@ -822,6 +841,9 @@ importers:
'@vitejs/plugin-vue':
specifier: 'catalog:'
version: 6.0.1(vite@7.1.5(@types/node@22.18.6)(jiti@2.5.1)(lightningcss@1.30.1)(sass-embedded@1.93.0)(sass@1.93.0)(terser@5.44.0)(tsx@4.20.5)(yaml@2.8.1))(vue@3.5.21(typescript@5.9.2))
+ '@vitest/browser-playwright':
+ specifier: workspace:*
+ version: link:../browser-playwright
'@vitest/runner':
specifier: workspace:*
version: link:../runner
@@ -1145,6 +1167,12 @@ importers:
'@vitest/browser-playwright':
specifier: workspace:*
version: link:../../packages/browser-playwright
+ '@vitest/browser-preview':
+ specifier: workspace:*
+ version: link:../../packages/browser-preview
+ '@vitest/browser-webdriverio':
+ specifier: workspace:*
+ version: link:../../packages/browser-webdriverio
'@vitest/bundled-lib':
specifier: link:./bundled-lib
version: link:bundled-lib
@@ -1190,6 +1218,9 @@ importers:
'@vitejs/plugin-basic-ssl':
specifier: ^2.1.0
version: 2.1.0(vite@7.1.5(@types/node@22.18.6)(jiti@2.5.1)(lightningcss@1.30.1)(sass-embedded@1.93.0)(sass@1.93.0)(terser@5.44.0)(tsx@4.20.5)(yaml@2.8.1))
+ '@vitest/browser-playwright':
+ specifier: workspace:*
+ version: link:../../packages/browser-playwright
'@vitest/runner':
specifier: workspace:^
version: link:../../packages/runner
@@ -1214,6 +1245,15 @@ importers:
test/config:
devDependencies:
+ '@vitest/browser-playwright':
+ specifier: workspace:*
+ version: link:../../packages/browser-playwright
+ '@vitest/browser-preview':
+ specifier: workspace:*
+ version: link:../../packages/browser-preview
+ '@vitest/browser-webdriverio':
+ specifier: workspace:*
+ version: link:../../packages/browser-webdriverio
'@vitest/test-dep-conditions':
specifier: file:./deps/test-dep-conditions
version: file:test/config/deps/test-dep-conditions
@@ -1337,9 +1377,9 @@ importers:
'@vitejs/plugin-vue':
specifier: latest
version: 6.0.1(vite@7.1.5(@types/node@22.18.6)(jiti@2.5.1)(lightningcss@1.30.1)(sass-embedded@1.93.0)(sass@1.93.0)(terser@5.44.0)(tsx@4.20.5)(yaml@2.8.1))(vue@3.5.21(typescript@5.9.2))
- '@vitest/browser':
+ '@vitest/browser-playwright':
specifier: workspace:*
- version: link:../../packages/browser
+ version: link:../../packages/browser-playwright
'@vitest/coverage-istanbul':
specifier: workspace:*
version: link:../../packages/coverage-istanbul
@@ -1397,9 +1437,9 @@ importers:
test/dts-playwright:
devDependencies:
- '@vitest/browser':
+ '@vitest/browser-playwright':
specifier: workspace:*
- version: link:../../packages/browser
+ version: link:../../packages/browser-playwright
vitest:
specifier: workspace:*
version: link:../../packages/vitest
@@ -1522,9 +1562,12 @@ importers:
test/watch:
devDependencies:
- '@vitest/browser':
+ '@vitest/browser-playwright':
specifier: workspace:*
- version: link:../../packages/browser
+ version: link:../../packages/browser-playwright
+ '@vitest/browser-webdriverio':
+ specifier: workspace:*
+ version: link:../../packages/browser-webdriverio
vite:
specifier: ^7.1.5
version: 7.1.5(@types/node@22.18.6)(jiti@2.5.1)(lightningcss@1.30.1)(sass-embedded@1.93.0)(sass@1.93.0)(terser@5.44.0)(tsx@4.20.5)(yaml@2.8.1)
@@ -1552,9 +1595,9 @@ importers:
test/workspaces-browser:
devDependencies:
- '@vitest/browser':
+ '@vitest/browser-playwright':
specifier: workspace:*
- version: link:../../packages/browser
+ version: link:../../packages/browser-playwright
vitest:
specifier: workspace:*
version: link:../../packages/vitest
diff --git a/test/browser/fixtures/trace-view/vitest.config.ts b/test/browser/fixtures/trace-view/vitest.config.ts
index e7a19d7a1cf4..ca61d24c72f1 100644
--- a/test/browser/fixtures/trace-view/vitest.config.ts
+++ b/test/browser/fixtures/trace-view/vitest.config.ts
@@ -1,6 +1,6 @@
import { fileURLToPath } from 'node:url'
import { defineConfig } from 'vitest/config'
-import { playwright } from '@vitest/browser/providers/playwright'
+import { playwright } from '@vitest/browser-playwright'
export default defineConfig({
cacheDir: fileURLToPath(new URL("./node_modules/.vite", import.meta.url)),
diff --git a/test/browser/package.json b/test/browser/package.json
index 5e5ef6364f44..b2c58d7d0015 100644
--- a/test/browser/package.json
+++ b/test/browser/package.json
@@ -34,6 +34,8 @@
"@vitejs/plugin-basic-ssl": "^2.1.0",
"@vitest/browser": "workspace:*",
"@vitest/browser-playwright": "workspace:*",
+ "@vitest/browser-preview": "workspace:*",
+ "@vitest/browser-webdriverio": "workspace:*",
"@vitest/bundled-lib": "link:./bundled-lib",
"@vitest/cjs-lib": "link:./cjs-lib",
"playwright": "^1.55.0",
diff --git a/test/browser/settings.ts b/test/browser/settings.ts
index bb3b1754ed8d..a23a43185ce2 100644
--- a/test/browser/settings.ts
+++ b/test/browser/settings.ts
@@ -1,7 +1,7 @@
import type { BrowserInstanceOption } from 'vitest/node'
import { playwright } from '@vitest/browser-playwright'
-import { preview } from '@vitest/browser/providers/preview'
-import { webdriverio } from '@vitest/browser/providers/webdriverio'
+import { preview } from '@vitest/browser-preview'
+import { webdriverio } from '@vitest/browser-webdriverio'
const providerName = (process.env.PROVIDER || 'playwright') as 'playwright' | 'webdriverio' | 'preview'
export const providers = {
diff --git a/test/browser/specs/playwright-connect.test.ts b/test/browser/specs/playwright-connect.test.ts
index 1d3cf6555726..7a1e8a715fc8 100644
--- a/test/browser/specs/playwright-connect.test.ts
+++ b/test/browser/specs/playwright-connect.test.ts
@@ -1,4 +1,4 @@
-import { playwright } from '@vitest/browser/providers/playwright'
+import { playwright } from '@vitest/browser-playwright'
import { chromium } from 'playwright'
import { expect, test } from 'vitest'
import { provider } from '../settings'
diff --git a/test/browser/specs/to-match-screenshot.test.ts b/test/browser/specs/to-match-screenshot.test.ts
index 887357abc34a..b410991cdc0a 100644
--- a/test/browser/specs/to-match-screenshot.test.ts
+++ b/test/browser/specs/to-match-screenshot.test.ts
@@ -1,8 +1,8 @@
-import type { ViteUserConfig } from 'vitest/config.js'
+import type { ViteUserConfig } from 'vitest/config'
import type { TestFsStructure } from '../../test-utils'
import { platform } from 'node:os'
import { resolve } from 'node:path'
-import { playwright } from '@vitest/browser/providers/playwright'
+import { playwright } from '@vitest/browser-playwright'
import { describe, expect, test } from 'vitest'
import { runVitestCli, useFS } from '../../test-utils'
import { extractToMatchScreenshotPaths } from '../fixtures/expect-dom/utils'
diff --git a/test/browser/vitest.config.mts b/test/browser/vitest.config.mts
index ea9f169c02d2..a8d4e56c7f6c 100644
--- a/test/browser/vitest.config.mts
+++ b/test/browser/vitest.config.mts
@@ -3,8 +3,8 @@ import { dirname, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
import * as util from 'node:util'
import { playwright } from '@vitest/browser-playwright'
-import { preview } from '@vitest/browser/providers/preview'
-import { webdriverio } from '@vitest/browser/providers/webdriverio'
+import { preview } from '@vitest/browser-preview'
+import { webdriverio } from '@vitest/browser-webdriverio'
import { defineConfig } from 'vitest/config'
const dir = dirname(fileURLToPath(import.meta.url))
diff --git a/test/cli/fixtures/browser-multiple/vitest.config.ts b/test/cli/fixtures/browser-multiple/vitest.config.ts
index 1df074e7bcef..c525c6f3258d 100644
--- a/test/cli/fixtures/browser-multiple/vitest.config.ts
+++ b/test/cli/fixtures/browser-multiple/vitest.config.ts
@@ -1,4 +1,4 @@
-import { playwright } from '@vitest/browser/providers/playwright';
+import { playwright } from '@vitest/browser-playwright';
import { resolve } from 'pathe';
import { defineConfig } from 'vitest/config';
diff --git a/test/cli/fixtures/config-loader/browser/vitest.config.ts b/test/cli/fixtures/config-loader/browser/vitest.config.ts
index 4a0041e1b70a..0bde499fc44e 100644
--- a/test/cli/fixtures/config-loader/browser/vitest.config.ts
+++ b/test/cli/fixtures/config-loader/browser/vitest.config.ts
@@ -1,6 +1,6 @@
import { defineConfig } from "vitest/config"
import "@test/test-dep-linked/ts";
-import { playwright } from '@vitest/browser/providers/playwright';
+import { playwright } from '@vitest/browser-playwright';
export default defineConfig({
test: {
diff --git a/test/cli/fixtures/list/vitest.config.ts b/test/cli/fixtures/list/vitest.config.ts
index 487485c4e133..a99b87bb6e7c 100644
--- a/test/cli/fixtures/list/vitest.config.ts
+++ b/test/cli/fixtures/list/vitest.config.ts
@@ -1,4 +1,4 @@
-import { playwright } from '@vitest/browser/providers/playwright'
+import { playwright } from '@vitest/browser-playwright'
import { fileURLToPath } from 'node:url'
import { defineConfig } from 'vitest/config'
diff --git a/test/cli/fixtures/public-api/vitest.config.ts b/test/cli/fixtures/public-api/vitest.config.ts
index 3fe34057ddff..8b5e2245d0e0 100644
--- a/test/cli/fixtures/public-api/vitest.config.ts
+++ b/test/cli/fixtures/public-api/vitest.config.ts
@@ -1,4 +1,4 @@
-import { playwright } from '@vitest/browser/providers/playwright'
+import { playwright } from '@vitest/browser-playwright'
import { defineConfig } from 'vitest/config'
export default defineConfig({
diff --git a/test/cli/package.json b/test/cli/package.json
index cb1dd8153597..608e02911c9b 100644
--- a/test/cli/package.json
+++ b/test/cli/package.json
@@ -12,6 +12,7 @@
"@types/debug": "catalog:",
"@types/ws": "catalog:",
"@vitejs/plugin-basic-ssl": "^2.1.0",
+ "@vitest/browser-playwright": "workspace:*",
"@vitest/runner": "workspace:^",
"@vitest/utils": "workspace:*",
"debug": "^4.4.3",
diff --git a/test/cli/test/annotations.test.ts b/test/cli/test/annotations.test.ts
index 414878805b3d..07eaa44ea414 100644
--- a/test/cli/test/annotations.test.ts
+++ b/test/cli/test/annotations.test.ts
@@ -1,5 +1,5 @@
import type { TestAnnotation } from 'vitest'
-import { playwright } from '@vitest/browser/providers/playwright'
+import { playwright } from '@vitest/browser-playwright'
import { describe, expect, test } from 'vitest'
import { runInlineTests } from '../../test-utils'
diff --git a/test/cli/test/fails.test.ts b/test/cli/test/fails.test.ts
index 7efc4b4f9431..6d4ea8255883 100644
--- a/test/cli/test/fails.test.ts
+++ b/test/cli/test/fails.test.ts
@@ -1,5 +1,5 @@
import type { TestCase } from 'vitest/node'
-import { playwright } from '@vitest/browser/providers/playwright'
+import { playwright } from '@vitest/browser-playwright'
import { resolve } from 'pathe'
import { glob } from 'tinyglobby'
diff --git a/test/cli/test/init.test.ts b/test/cli/test/init.test.ts
index c2c9f89ef3c1..dc3f725504ee 100644
--- a/test/cli/test/init.test.ts
+++ b/test/cli/test/init.test.ts
@@ -54,7 +54,7 @@ test('initializes project', async () => {
expect(await getFileContent('/vitest.browser.config.ts')).toMatchInlineSnapshot(`
"import { defineConfig } from 'vitest/config'
- import { preview } from '@vitest/browser/providers/preview'
+ import { preview } from '@vitest/browser-preview'
export default defineConfig({
test: {
diff --git a/test/cli/test/scoped-fixtures.test.ts b/test/cli/test/scoped-fixtures.test.ts
index 40ce7785f205..083edee8f713 100644
--- a/test/cli/test/scoped-fixtures.test.ts
+++ b/test/cli/test/scoped-fixtures.test.ts
@@ -4,7 +4,7 @@ import type { TestAPI } from 'vitest'
import type { ViteUserConfig } from 'vitest/config'
import type { TestSpecification, TestUserConfig } from 'vitest/node'
import type { TestFsStructure } from '../../test-utils'
-import { playwright } from '@vitest/browser/providers/playwright'
+import { playwright } from '@vitest/browser-playwright'
import { runInlineTests } from '../../test-utils'
interface TestContext {
diff --git a/test/config/fixtures/browser-custom-html/vitest.config.correct.ts b/test/config/fixtures/browser-custom-html/vitest.config.correct.ts
index cc6645a06060..cbd53f2d3615 100644
--- a/test/config/fixtures/browser-custom-html/vitest.config.correct.ts
+++ b/test/config/fixtures/browser-custom-html/vitest.config.correct.ts
@@ -1,4 +1,4 @@
-import { playwright } from '@vitest/browser/providers/playwright';
+import { playwright } from '@vitest/browser-playwright';
import { defineConfig } from 'vitest/config';
export default defineConfig({
diff --git a/test/config/fixtures/browser-custom-html/vitest.config.custom-transformIndexHtml.ts b/test/config/fixtures/browser-custom-html/vitest.config.custom-transformIndexHtml.ts
index 136f218a63ee..71c7a8a48d10 100644
--- a/test/config/fixtures/browser-custom-html/vitest.config.custom-transformIndexHtml.ts
+++ b/test/config/fixtures/browser-custom-html/vitest.config.custom-transformIndexHtml.ts
@@ -1,4 +1,4 @@
-import { playwright } from '@vitest/browser/providers/playwright';
+import { playwright } from '@vitest/browser-playwright';
import { defineConfig } from 'vitest/config';
export default defineConfig({
diff --git a/test/config/fixtures/browser-custom-html/vitest.config.default-transformIndexHtml.ts b/test/config/fixtures/browser-custom-html/vitest.config.default-transformIndexHtml.ts
index f8fe90543256..325eab8793a7 100644
--- a/test/config/fixtures/browser-custom-html/vitest.config.default-transformIndexHtml.ts
+++ b/test/config/fixtures/browser-custom-html/vitest.config.default-transformIndexHtml.ts
@@ -1,4 +1,4 @@
-import { playwright } from '@vitest/browser/providers/playwright';
+import { playwright } from '@vitest/browser-playwright';
import { defineConfig } from 'vitest/config';
export default defineConfig({
diff --git a/test/config/fixtures/browser-custom-html/vitest.config.error-hook.ts b/test/config/fixtures/browser-custom-html/vitest.config.error-hook.ts
index 4ad8f42e41fc..452b517cd766 100644
--- a/test/config/fixtures/browser-custom-html/vitest.config.error-hook.ts
+++ b/test/config/fixtures/browser-custom-html/vitest.config.error-hook.ts
@@ -1,4 +1,4 @@
-import { playwright } from '@vitest/browser/providers/playwright';
+import { playwright } from '@vitest/browser-playwright';
import { defineConfig } from 'vitest/config';
export default defineConfig({
diff --git a/test/config/fixtures/browser-custom-html/vitest.config.non-existing.ts b/test/config/fixtures/browser-custom-html/vitest.config.non-existing.ts
index 8a259fc9fff9..4e14f69e8593 100644
--- a/test/config/fixtures/browser-custom-html/vitest.config.non-existing.ts
+++ b/test/config/fixtures/browser-custom-html/vitest.config.non-existing.ts
@@ -1,4 +1,4 @@
-import { playwright } from '@vitest/browser/providers/playwright';
+import { playwright } from '@vitest/browser-playwright';
import { defineConfig } from 'vitest/config';
export default defineConfig({
diff --git a/test/config/package.json b/test/config/package.json
index dd7c8c7b2d1a..43318e591051 100644
--- a/test/config/package.json
+++ b/test/config/package.json
@@ -6,6 +6,9 @@
"test": "vitest --typecheck.enabled"
},
"devDependencies": {
+ "@vitest/browser-playwright": "workspace:*",
+ "@vitest/browser-preview": "workspace:*",
+ "@vitest/browser-webdriverio": "workspace:*",
"@vitest/test-dep-conditions": "file:./deps/test-dep-conditions",
"tinyexec": "^0.3.2",
"vite": "latest",
diff --git a/test/config/test/bail.test.ts b/test/config/test/bail.test.ts
index 62f509dcae9b..9127851bd59e 100644
--- a/test/config/test/bail.test.ts
+++ b/test/config/test/bail.test.ts
@@ -1,6 +1,6 @@
import type { TestUserConfig } from 'vitest/node'
-import { playwright } from '@vitest/browser/providers/playwright'
+import { playwright } from '@vitest/browser-playwright'
import { expect, test } from 'vitest'
import { runVitest } from '../../test-utils'
diff --git a/test/config/test/browser-configs.test.ts b/test/config/test/browser-configs.test.ts
index 87c6b004e8d8..89c9fa45a42f 100644
--- a/test/config/test/browser-configs.test.ts
+++ b/test/config/test/browser-configs.test.ts
@@ -2,9 +2,9 @@ import type { ViteUserConfig } from 'vitest/config'
import type { TestUserConfig, VitestOptions } from 'vitest/node'
import type { TestFsStructure } from '../../test-utils'
import crypto from 'node:crypto'
-import { playwright } from '@vitest/browser/providers/playwright'
-import { webdriverio } from '@vitest/browser/providers/webdriverio'
-import { preview } from '@vitest/browser/src/node/providers/preview.js'
+import { playwright } from '@vitest/browser-playwright'
+import { preview } from '@vitest/browser-preview'
+import { webdriverio } from '@vitest/browser-webdriverio'
import { resolve } from 'pathe'
import { describe, expect, onTestFinished, test } from 'vitest'
import { createVitest } from 'vitest/node'
diff --git a/test/config/test/failures.test.ts b/test/config/test/failures.test.ts
index cfb113b09900..c812fd928e8b 100644
--- a/test/config/test/failures.test.ts
+++ b/test/config/test/failures.test.ts
@@ -1,9 +1,9 @@
import type { UserConfig as ViteUserConfig } from 'vite'
import type { TestUserConfig } from 'vitest/node'
import type { VitestRunnerCLIOptions } from '../../test-utils'
-import { playwright } from '@vitest/browser/providers/playwright'
-import { preview } from '@vitest/browser/providers/preview'
-import { webdriverio } from '@vitest/browser/providers/webdriverio'
+import { playwright } from '@vitest/browser-playwright'
+import { preview } from '@vitest/browser-preview'
+import { webdriverio } from '@vitest/browser-webdriverio'
import { normalize, resolve } from 'pathe'
import { beforeEach, expect, test } from 'vitest'
import { version } from 'vitest/package.json'
diff --git a/test/coverage-test/package.json b/test/coverage-test/package.json
index 4e640f5fe105..f40c3ddc2aa2 100644
--- a/test/coverage-test/package.json
+++ b/test/coverage-test/package.json
@@ -10,7 +10,7 @@
"@types/istanbul-lib-coverage": "catalog:",
"@types/istanbul-lib-report": "catalog:",
"@vitejs/plugin-vue": "latest",
- "@vitest/browser": "workspace:*",
+ "@vitest/browser-playwright": "workspace:*",
"@vitest/coverage-istanbul": "workspace:*",
"@vitest/coverage-v8": "workspace:*",
"@vitest/web-worker": "workspace:*",
diff --git a/test/coverage-test/utils.ts b/test/coverage-test/utils.ts
index 764f13e2e0d3..d34f796566ee 100644
--- a/test/coverage-test/utils.ts
+++ b/test/coverage-test/utils.ts
@@ -7,7 +7,7 @@ import { unlink } from 'node:fs/promises'
import { resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
import { stripVTControlCharacters } from 'node:util'
-import { playwright } from '@vitest/browser/providers/playwright'
+import { playwright } from '@vitest/browser-playwright'
import libCoverage from 'istanbul-lib-coverage'
import { normalize } from 'pathe'
import { vi, describe as vitestDescribe, test as vitestTest } from 'vitest'
diff --git a/test/dts-playwright/package.json b/test/dts-playwright/package.json
index c53fbd8f5dc5..0b094aae7db7 100644
--- a/test/dts-playwright/package.json
+++ b/test/dts-playwright/package.json
@@ -6,7 +6,7 @@
"test": "tsc -b"
},
"devDependencies": {
- "@vitest/browser": "workspace:*",
+ "@vitest/browser-playwright": "workspace:*",
"vitest": "workspace:*"
}
}
diff --git a/test/dts-playwright/tsconfig.json b/test/dts-playwright/tsconfig.json
index 66b2370dff40..ef7768529b99 100644
--- a/test/dts-playwright/tsconfig.json
+++ b/test/dts-playwright/tsconfig.json
@@ -4,7 +4,6 @@
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "Bundler",
- "types": ["@vitest/browser/providers/playwright"],
"strict": true,
"noEmit": true
},
diff --git a/test/dts-playwright/vite.config.ts b/test/dts-playwright/vite.config.ts
index 0c32e8096600..f471d86a3ea6 100644
--- a/test/dts-playwright/vite.config.ts
+++ b/test/dts-playwright/vite.config.ts
@@ -1,4 +1,4 @@
-import { playwright } from '@vitest/browser/providers/playwright'
+import { playwright } from '@vitest/browser-playwright'
import { defineConfig } from 'vitest/config'
export default defineConfig({
diff --git a/test/watch/package.json b/test/watch/package.json
index d4fc79036824..48a6ddf442e2 100644
--- a/test/watch/package.json
+++ b/test/watch/package.json
@@ -6,7 +6,8 @@
"test": "vitest"
},
"devDependencies": {
- "@vitest/browser": "workspace:*",
+ "@vitest/browser-playwright": "workspace:*",
+ "@vitest/browser-webdriverio": "workspace:*",
"vite": "latest",
"vitest": "workspace:*"
}
diff --git a/test/watch/test/config-watching.test.ts b/test/watch/test/config-watching.test.ts
index 9edc214e3cf6..f4c8fc3d4220 100644
--- a/test/watch/test/config-watching.test.ts
+++ b/test/watch/test/config-watching.test.ts
@@ -1,4 +1,4 @@
-import { playwright } from '@vitest/browser/providers/playwright'
+import { playwright } from '@vitest/browser-playwright'
import { expect, test } from 'vitest'
import { runInlineTests } from '../../test-utils'
diff --git a/test/watch/test/file-watching.test.ts b/test/watch/test/file-watching.test.ts
index 451ec6aa37d1..dbed603471ac 100644
--- a/test/watch/test/file-watching.test.ts
+++ b/test/watch/test/file-watching.test.ts
@@ -1,5 +1,5 @@
import { existsSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs'
-import { webdriverio } from '@vitest/browser/providers/webdriverio'
+import { webdriverio } from '@vitest/browser-webdriverio'
import { afterEach, describe, expect, test } from 'vitest'
import * as testUtils from '../../test-utils'
diff --git a/test/workspaces-browser/package.json b/test/workspaces-browser/package.json
index 5cd40bf42095..1ef72c903e1c 100644
--- a/test/workspaces-browser/package.json
+++ b/test/workspaces-browser/package.json
@@ -6,7 +6,7 @@
"test": "vitest run"
},
"devDependencies": {
- "@vitest/browser": "workspace:^",
+ "@vitest/browser-playwright": "workspace:^",
"vitest": "workspace:*"
}
}
diff --git a/test/workspaces-browser/space_browser/vitest.config.ts b/test/workspaces-browser/space_browser/vitest.config.ts
index 246901f82f82..76fb1f7ea3ad 100644
--- a/test/workspaces-browser/space_browser/vitest.config.ts
+++ b/test/workspaces-browser/space_browser/vitest.config.ts
@@ -1,4 +1,4 @@
-import { playwright } from '@vitest/browser/providers/playwright'
+import { playwright } from '@vitest/browser-playwright'
import { defineProject } from 'vitest/config'
export default defineProject({
diff --git a/test/workspaces-browser/vitest.config.ts b/test/workspaces-browser/vitest.config.ts
index b550364fce73..dc590ffaf1fd 100644
--- a/test/workspaces-browser/vitest.config.ts
+++ b/test/workspaces-browser/vitest.config.ts
@@ -1,4 +1,4 @@
-import { playwright } from '@vitest/browser/providers/playwright'
+import { playwright } from '@vitest/browser-playwright'
import { defineConfig } from 'vitest/config'
if (process.env.TEST_WATCH) {
From b0fa5b4884d072d829b33ec74d064847cdbfce4e Mon Sep 17 00:00:00 2001
From: Vladimir Sheremet
Date: Mon, 29 Sep 2025 12:17:05 +0200
Subject: [PATCH 07/43] feat: register playwright commands
---
.../browser-playwright/src/commands/clear.ts | 11 ++
.../browser-playwright/src/commands/click.ts | 32 +++++
.../src/commands/dragAndDrop.ts | 16 +++
.../browser-playwright/src/commands/fill.ts | 13 ++
.../browser-playwright/src/commands/hover.ts | 10 ++
.../browser-playwright/src/commands/index.ts | 40 ++++++
.../src/commands/keyboard.ts | 133 ++++++++++++++++++
.../src/commands/screenshot.ts | 115 +++++++++++++++
.../browser-playwright/src/commands/select.ts | 27 ++++
.../browser-playwright/src/commands/tab.ts | 10 ++
.../browser-playwright/src/commands/trace.ts | 125 ++++++++++++++++
.../browser-playwright/src/commands/type.ts | 33 +++++
.../browser-playwright/src/commands/upload.ts | 33 +++++
.../browser-playwright/src/commands/utils.ts | 17 +++
packages/browser-playwright/src/playwright.ts | 8 +-
.../browser/src/node/commands/screenshot.ts | 16 ++-
packages/browser/src/node/index.ts | 3 +-
packages/browser/src/node/project.ts | 17 +++
packages/browser/src/node/rpc.ts | 5 +-
packages/browser/src/node/utils.ts | 39 ++++-
packages/vitest/src/node/types/browser.ts | 16 ++-
tsconfig.base.json | 1 +
22 files changed, 713 insertions(+), 7 deletions(-)
create mode 100644 packages/browser-playwright/src/commands/clear.ts
create mode 100644 packages/browser-playwright/src/commands/click.ts
create mode 100644 packages/browser-playwright/src/commands/dragAndDrop.ts
create mode 100644 packages/browser-playwright/src/commands/fill.ts
create mode 100644 packages/browser-playwright/src/commands/hover.ts
create mode 100644 packages/browser-playwright/src/commands/index.ts
create mode 100644 packages/browser-playwright/src/commands/keyboard.ts
create mode 100644 packages/browser-playwright/src/commands/screenshot.ts
create mode 100644 packages/browser-playwright/src/commands/select.ts
create mode 100644 packages/browser-playwright/src/commands/tab.ts
create mode 100644 packages/browser-playwright/src/commands/trace.ts
create mode 100644 packages/browser-playwright/src/commands/type.ts
create mode 100644 packages/browser-playwright/src/commands/upload.ts
create mode 100644 packages/browser-playwright/src/commands/utils.ts
diff --git a/packages/browser-playwright/src/commands/clear.ts b/packages/browser-playwright/src/commands/clear.ts
new file mode 100644
index 000000000000..6736b36f70eb
--- /dev/null
+++ b/packages/browser-playwright/src/commands/clear.ts
@@ -0,0 +1,11 @@
+import type { UserEvent } from 'vitest/browser'
+import type { UserEventCommand } from './utils'
+
+export const clear: UserEventCommand = async (
+ context,
+ selector,
+) => {
+ const { iframe } = context
+ const element = iframe.locator(selector)
+ await element.clear()
+}
diff --git a/packages/browser-playwright/src/commands/click.ts b/packages/browser-playwright/src/commands/click.ts
new file mode 100644
index 000000000000..75261a93aed3
--- /dev/null
+++ b/packages/browser-playwright/src/commands/click.ts
@@ -0,0 +1,32 @@
+import type { UserEvent } from 'vitest/browser'
+import type { UserEventCommand } from './utils'
+
+export const click: UserEventCommand = async (
+ context,
+ selector,
+ options = {},
+) => {
+ const tester = context.iframe
+ await tester.locator(selector).click(options)
+}
+
+export const dblClick: UserEventCommand = async (
+ context,
+ selector,
+ options = {},
+) => {
+ const tester = context.iframe
+ await tester.locator(selector).dblclick(options)
+}
+
+export const tripleClick: UserEventCommand = async (
+ context,
+ selector,
+ options = {},
+) => {
+ const tester = context.iframe
+ await tester.locator(selector).click({
+ ...options,
+ clickCount: 3,
+ })
+}
diff --git a/packages/browser-playwright/src/commands/dragAndDrop.ts b/packages/browser-playwright/src/commands/dragAndDrop.ts
new file mode 100644
index 000000000000..2febe3704101
--- /dev/null
+++ b/packages/browser-playwright/src/commands/dragAndDrop.ts
@@ -0,0 +1,16 @@
+import type { UserEvent } from 'vitest/browser'
+import type { UserEventCommand } from './utils'
+
+export const dragAndDrop: UserEventCommand = async (
+ context,
+ source,
+ target,
+ options_,
+) => {
+ const frame = await context.frame()
+ await frame.dragAndDrop(
+ source,
+ target,
+ options_,
+ )
+}
diff --git a/packages/browser-playwright/src/commands/fill.ts b/packages/browser-playwright/src/commands/fill.ts
new file mode 100644
index 000000000000..a0a6b2dd3612
--- /dev/null
+++ b/packages/browser-playwright/src/commands/fill.ts
@@ -0,0 +1,13 @@
+import type { UserEvent } from 'vitest/browser'
+import type { UserEventCommand } from './utils'
+
+export const fill: UserEventCommand = async (
+ context,
+ selector,
+ text,
+ options = {},
+) => {
+ const { iframe } = context
+ const element = iframe.locator(selector)
+ await element.fill(text, options)
+}
diff --git a/packages/browser-playwright/src/commands/hover.ts b/packages/browser-playwright/src/commands/hover.ts
new file mode 100644
index 000000000000..30afcd259073
--- /dev/null
+++ b/packages/browser-playwright/src/commands/hover.ts
@@ -0,0 +1,10 @@
+import type { UserEvent } from 'vitest/browser'
+import type { UserEventCommand } from './utils'
+
+export const hover: UserEventCommand = async (
+ context,
+ selector,
+ options = {},
+) => {
+ await context.iframe.locator(selector).hover(options)
+}
diff --git a/packages/browser-playwright/src/commands/index.ts b/packages/browser-playwright/src/commands/index.ts
new file mode 100644
index 000000000000..2714f873a023
--- /dev/null
+++ b/packages/browser-playwright/src/commands/index.ts
@@ -0,0 +1,40 @@
+import { clear } from './clear'
+import { click, dblClick, tripleClick } from './click'
+import { dragAndDrop } from './dragAndDrop'
+import { fill } from './fill'
+import { hover } from './hover'
+import { keyboard, keyboardCleanup } from './keyboard'
+import { screenshot } from './screenshot'
+import { selectOptions } from './select'
+import { tab } from './tab'
+import {
+ annotateTraces,
+ deleteTracing,
+ startChunkTrace,
+ startTracing,
+ stopChunkTrace,
+} from './trace'
+import { type } from './type'
+import { upload } from './upload'
+
+export default {
+ __vitest_upload: upload as typeof upload,
+ __vitest_click: click as typeof click,
+ __vitest_dblClick: dblClick as typeof dblClick,
+ __vitest_tripleClick: tripleClick as typeof tripleClick,
+ __vitest_screenshot: screenshot as typeof screenshot,
+ __vitest_type: type as typeof type,
+ __vitest_clear: clear as typeof clear,
+ __vitest_fill: fill as typeof fill,
+ __vitest_tab: tab as typeof tab,
+ __vitest_keyboard: keyboard as typeof keyboard,
+ __vitest_selectOptions: selectOptions as typeof selectOptions,
+ __vitest_dragAndDrop: dragAndDrop as typeof dragAndDrop,
+ __vitest_hover: hover as typeof hover,
+ __vitest_cleanup: keyboardCleanup as typeof keyboardCleanup,
+ __vitest_deleteTracing: deleteTracing as typeof deleteTracing,
+ __vitest_startChunkTrace: startChunkTrace as typeof startChunkTrace,
+ __vitest_startTracing: startTracing as typeof startTracing,
+ __vitest_stopChunkTrace: stopChunkTrace as typeof stopChunkTrace,
+ __vitest_annotateTraces: annotateTraces as typeof annotateTraces,
+}
diff --git a/packages/browser-playwright/src/commands/keyboard.ts b/packages/browser-playwright/src/commands/keyboard.ts
new file mode 100644
index 000000000000..70f776f7489b
--- /dev/null
+++ b/packages/browser-playwright/src/commands/keyboard.ts
@@ -0,0 +1,133 @@
+import type { BrowserProvider } from 'vitest/node'
+import type { PlaywrightBrowserProvider } from '../playwright'
+import type { UserEventCommand } from './utils'
+import { parseKeyDef } from '@vitest/browser'
+
+export interface KeyboardState {
+ unreleased: string[]
+}
+
+export const keyboard: UserEventCommand<(text: string, state: KeyboardState) => Promise<{ unreleased: string[] }>> = async (
+ context,
+ text,
+ state,
+) => {
+ const frame = await context.frame()
+ await frame.evaluate(focusIframe)
+
+ const pressed = new Set(state.unreleased)
+
+ await keyboardImplementation(
+ pressed,
+ context.provider,
+ context.sessionId,
+ text,
+ async () => {
+ const frame = await context.frame()
+ await frame.evaluate(selectAll)
+ },
+ true,
+ )
+
+ return {
+ unreleased: Array.from(pressed),
+ }
+}
+
+export const keyboardCleanup: UserEventCommand<(state: KeyboardState) => Promise> = async (
+ context,
+ state,
+) => {
+ const { provider, sessionId } = context
+ if (!state.unreleased) {
+ return
+ }
+ const page = (provider as PlaywrightBrowserProvider).getPage(sessionId)
+ for (const key of state.unreleased) {
+ await page.keyboard.up(key)
+ }
+}
+
+// fallback to insertText for non US key
+// https://github.com/microsoft/playwright/blob/50775698ae13642742f2a1e8983d1d686d7f192d/packages/playwright-core/src/server/input.ts#L95
+const VALID_KEYS = new Set(['Escape', 'F1', 'F2', 'F3', 'F4', 'F5', 'F6', 'F7', 'F8', 'F9', 'F10', 'F11', 'F12', 'Backquote', '`', '~', 'Digit1', '1', '!', 'Digit2', '2', '@', 'Digit3', '3', '#', 'Digit4', '4', '$', 'Digit5', '5', '%', 'Digit6', '6', '^', 'Digit7', '7', '&', 'Digit8', '8', '*', 'Digit9', '9', '(', 'Digit0', '0', ')', 'Minus', '-', '_', 'Equal', '=', '+', 'Backslash', '\\', '|', 'Backspace', 'Tab', 'KeyQ', 'q', 'Q', 'KeyW', 'w', 'W', 'KeyE', 'e', 'E', 'KeyR', 'r', 'R', 'KeyT', 't', 'T', 'KeyY', 'y', 'Y', 'KeyU', 'u', 'U', 'KeyI', 'i', 'I', 'KeyO', 'o', 'O', 'KeyP', 'p', 'P', 'BracketLeft', '[', '{', 'BracketRight', ']', '}', 'CapsLock', 'KeyA', 'a', 'A', 'KeyS', 's', 'S', 'KeyD', 'd', 'D', 'KeyF', 'f', 'F', 'KeyG', 'g', 'G', 'KeyH', 'h', 'H', 'KeyJ', 'j', 'J', 'KeyK', 'k', 'K', 'KeyL', 'l', 'L', 'Semicolon', ';', ':', 'Quote', '\'', '"', 'Enter', '\n', '\r', 'ShiftLeft', 'Shift', 'KeyZ', 'z', 'Z', 'KeyX', 'x', 'X', 'KeyC', 'c', 'C', 'KeyV', 'v', 'V', 'KeyB', 'b', 'B', 'KeyN', 'n', 'N', 'KeyM', 'm', 'M', 'Comma', ',', '<', 'Period', '.', '>', 'Slash', '/', '?', 'ShiftRight', 'ControlLeft', 'Control', 'MetaLeft', 'Meta', 'AltLeft', 'Alt', 'Space', ' ', 'AltRight', 'AltGraph', 'MetaRight', 'ContextMenu', 'ControlRight', 'PrintScreen', 'ScrollLock', 'Pause', 'PageUp', 'PageDown', 'Insert', 'Delete', 'Home', 'End', 'ArrowLeft', 'ArrowUp', 'ArrowRight', 'ArrowDown', 'NumLock', 'NumpadDivide', 'NumpadMultiply', 'NumpadSubtract', 'Numpad7', 'Numpad8', 'Numpad9', 'Numpad4', 'Numpad5', 'Numpad6', 'NumpadAdd', 'Numpad1', 'Numpad2', 'Numpad3', 'Numpad0', 'NumpadDecimal', 'NumpadEnter', 'ControlOrMeta'])
+
+export async function keyboardImplementation(
+ pressed: Set,
+ provider: BrowserProvider,
+ sessionId: string,
+ text: string,
+ selectAll: () => Promise,
+ skipRelease: boolean,
+): Promise<{ pressed: Set }> {
+ const page = (provider as PlaywrightBrowserProvider).getPage(sessionId)
+ const actions = parseKeyDef(text)
+
+ for (const { releasePrevious, releaseSelf, repeat, keyDef } of actions) {
+ const key = keyDef.key!
+
+ // TODO: instead of calling down/up for each key, join non special
+ // together, and call `type` once for all non special keys,
+ // and then `press` for special keys
+ if (pressed.has(key)) {
+ if (VALID_KEYS.has(key)) {
+ await page.keyboard.up(key)
+ }
+ pressed.delete(key)
+ }
+
+ if (!releasePrevious) {
+ if (key === 'selectall') {
+ await selectAll()
+ continue
+ }
+
+ for (let i = 1; i <= repeat; i++) {
+ if (VALID_KEYS.has(key)) {
+ await page.keyboard.down(key)
+ }
+ else {
+ await page.keyboard.insertText(key)
+ }
+ }
+
+ if (releaseSelf) {
+ if (VALID_KEYS.has(key)) {
+ await page.keyboard.up(key)
+ }
+ }
+ else {
+ pressed.add(key)
+ }
+ }
+ }
+
+ if (!skipRelease && pressed.size) {
+ for (const key of pressed) {
+ if (VALID_KEYS.has(key)) {
+ await page.keyboard.up(key)
+ }
+ }
+ }
+
+ return {
+ pressed,
+ }
+}
+
+function focusIframe() {
+ if (
+ !document.activeElement
+ || document.activeElement.ownerDocument !== document
+ || document.activeElement === document.body
+ ) {
+ window.focus()
+ }
+}
+
+function selectAll() {
+ const element = document.activeElement as HTMLInputElement
+ if (element && typeof element.select === 'function') {
+ element.select()
+ }
+}
diff --git a/packages/browser-playwright/src/commands/screenshot.ts b/packages/browser-playwright/src/commands/screenshot.ts
new file mode 100644
index 000000000000..7c1058260047
--- /dev/null
+++ b/packages/browser-playwright/src/commands/screenshot.ts
@@ -0,0 +1,115 @@
+import type { ScreenshotOptions } from 'vitest/browser'
+import type { BrowserCommand, BrowserCommandContext, ResolvedConfig } from 'vitest/node'
+import { mkdir } from 'node:fs/promises'
+import { basename, dirname, normalize, relative, resolve } from 'pathe'
+
+interface ScreenshotCommandOptions extends Omit {
+ element?: string
+ mask?: readonly string[]
+}
+
+export const screenshot: BrowserCommand<[string, ScreenshotCommandOptions]> = async (
+ context,
+ name: string,
+ options = {},
+) => {
+ options.save ??= true
+
+ if (!options.save) {
+ options.base64 = true
+ }
+
+ const { buffer, path } = await takeScreenshot(context, name, options)
+
+ return returnResult(options, path, buffer)
+}
+
+/**
+ * Takes a screenshot using the provided browser context and returns a buffer and the expected screenshot path.
+ *
+ * **Note**: the returned `path` indicates where the screenshot *might* be found.
+ * It is not guaranteed to exist, especially if `options.save` is `false`.
+ *
+ * @throws {Error} If the function is not called within a test or if the browser provider does not support screenshots.
+ */
+export async function takeScreenshot(
+ context: BrowserCommandContext,
+ name: string,
+ options: Omit,
+): Promise<{ buffer: Buffer; path: string }> {
+ if (!context.testPath) {
+ throw new Error(`Cannot take a screenshot without a test path`)
+ }
+
+ const path = resolveScreenshotPath(
+ context.testPath,
+ name,
+ context.project.config,
+ options.path,
+ )
+
+ // playwright does not need a screenshot path if we don't intend to save it
+ let savePath: string | undefined
+
+ if (options.save) {
+ savePath = normalize(path)
+
+ await mkdir(dirname(savePath), { recursive: true })
+ }
+
+ const mask = options.mask?.map(selector => context.iframe.locator(selector))
+
+ if (options.element) {
+ const { element: selector, ...config } = options
+ const element = context.iframe.locator(selector)
+ const buffer = await element.screenshot({
+ ...config,
+ mask,
+ path: savePath,
+ })
+ return { buffer, path }
+ }
+
+ const buffer = await context.iframe.locator('body').screenshot({
+ ...options,
+ mask,
+ path: savePath,
+ })
+ return { buffer, path }
+}
+
+function resolveScreenshotPath(
+ testPath: string,
+ name: string,
+ config: ResolvedConfig,
+ customPath: string | undefined,
+): string {
+ if (customPath) {
+ return resolve(dirname(testPath), customPath)
+ }
+ const dir = dirname(testPath)
+ const base = basename(testPath)
+ if (config.browser.screenshotDirectory) {
+ return resolve(
+ config.browser.screenshotDirectory,
+ relative(config.root, dir),
+ base,
+ name,
+ )
+ }
+ return resolve(dir, '__screenshots__', base, name)
+}
+
+function returnResult(
+ options: ScreenshotCommandOptions,
+ path: string,
+ buffer: Buffer,
+) {
+ if (!options.save) {
+ return buffer.toString('base64')
+ }
+ if (options.base64) {
+ return { path, base64: buffer.toString('base64') }
+ }
+ return path
+}
diff --git a/packages/browser-playwright/src/commands/select.ts b/packages/browser-playwright/src/commands/select.ts
new file mode 100644
index 000000000000..4fbcb972b87a
--- /dev/null
+++ b/packages/browser-playwright/src/commands/select.ts
@@ -0,0 +1,27 @@
+import type { ElementHandle } from 'playwright'
+import type { UserEvent } from 'vitest/browser'
+import type { UserEventCommand } from './utils'
+
+export const selectOptions: UserEventCommand = async (
+ context,
+ selector,
+ userValues,
+ options = {},
+) => {
+ const value = userValues as any as (string | { element: string })[]
+ const { iframe } = context
+ const selectElement = iframe.locator(selector)
+
+ const values = await Promise.all(value.map(async (v) => {
+ if (typeof v === 'string') {
+ return v
+ }
+ const elementHandler = await iframe.locator(v.element).elementHandle()
+ if (!elementHandler) {
+ throw new Error(`Element not found: ${v.element}`)
+ }
+ return elementHandler
+ })) as (readonly string[]) | (readonly ElementHandle[])
+
+ await selectElement.selectOption(values, options)
+}
diff --git a/packages/browser-playwright/src/commands/tab.ts b/packages/browser-playwright/src/commands/tab.ts
new file mode 100644
index 000000000000..8720e81088a7
--- /dev/null
+++ b/packages/browser-playwright/src/commands/tab.ts
@@ -0,0 +1,10 @@
+import type { UserEvent } from 'vitest/browser'
+import type { UserEventCommand } from './utils'
+
+export const tab: UserEventCommand = async (
+ context,
+ options = {},
+) => {
+ const page = context.page
+ await page.keyboard.press(options.shift === true ? 'Shift+Tab' : 'Tab')
+}
diff --git a/packages/browser-playwright/src/commands/trace.ts b/packages/browser-playwright/src/commands/trace.ts
new file mode 100644
index 000000000000..4542ebddda17
--- /dev/null
+++ b/packages/browser-playwright/src/commands/trace.ts
@@ -0,0 +1,125 @@
+import type { BrowserCommand, BrowserCommandContext } from 'vitest/node'
+import { unlink } from 'node:fs/promises'
+import { basename, dirname, relative, resolve } from 'pathe'
+import { PlaywrightBrowserProvider } from '../playwright'
+
+export const startTracing: BrowserCommand<[]> = async ({ context, project, provider, sessionId }) => {
+ if (provider instanceof PlaywrightBrowserProvider) {
+ if (provider.tracingContexts.has(sessionId)) {
+ return
+ }
+
+ provider.tracingContexts.add(sessionId)
+ const options = project.config.browser!.trace
+ await context.tracing.start({
+ screenshots: options.screenshots ?? true,
+ snapshots: options.snapshots ?? true,
+ // currently, PW shows sources in private methods
+ sources: false,
+ }).catch(() => {
+ provider.tracingContexts.delete(sessionId)
+ })
+ return
+ }
+ throw new TypeError(`The ${provider.name} provider does not support tracing.`)
+}
+
+export const startChunkTrace: BrowserCommand<[{ name: string; title: string }]> = async (
+ command,
+ { name, title },
+) => {
+ const { provider, sessionId, testPath, context } = command
+ if (!testPath) {
+ throw new Error(`stopChunkTrace cannot be called outside of the test file.`)
+ }
+ if (provider instanceof PlaywrightBrowserProvider) {
+ if (!provider.tracingContexts.has(sessionId)) {
+ await startTracing(command)
+ }
+ const path = resolveTracesPath(command, name)
+ provider.pendingTraces.set(path, sessionId)
+ await context.tracing.startChunk({ name, title })
+ return
+ }
+ throw new TypeError(`The ${provider.name} provider does not support tracing.`)
+}
+
+export const stopChunkTrace: BrowserCommand<[{ name: string }]> = async (
+ context,
+ { name },
+) => {
+ if (context.provider instanceof PlaywrightBrowserProvider) {
+ const path = resolveTracesPath(context, name)
+ context.provider.pendingTraces.delete(path)
+ await context.context.tracing.stopChunk({ path })
+ return { tracePath: path }
+ }
+ throw new TypeError(`The ${context.provider.name} provider does not support tracing.`)
+}
+
+function resolveTracesPath({ testPath, project }: BrowserCommandContext, name: string) {
+ if (!testPath) {
+ throw new Error(`This command can only be called inside a test file.`)
+ }
+ const options = project.config.browser!.trace
+ const sanitizedName = `${project.name.replace(/[^a-z0-9]/gi, '-')}-${name}.trace.zip`
+ if (options.tracesDir) {
+ return resolve(options.tracesDir, sanitizedName)
+ }
+ const dir = dirname(testPath)
+ const base = basename(testPath)
+ return resolve(
+ dir,
+ '__traces__',
+ base,
+ `${project.name.replace(/[^a-z0-9]/gi, '-')}-${name}.trace.zip`,
+ )
+}
+
+export const deleteTracing: BrowserCommand<[{ traces: string[] }]> = async (
+ context,
+ { traces },
+) => {
+ if (!context.testPath) {
+ throw new Error(`stopChunkTrace cannot be called outside of the test file.`)
+ }
+ if (context.provider instanceof PlaywrightBrowserProvider) {
+ return Promise.all(
+ traces.map(trace => unlink(trace).catch((err) => {
+ if (err.code === 'ENOENT') {
+ // Ignore the error if the file doesn't exist
+ return
+ }
+ // Re-throw other errors
+ throw err
+ })),
+ )
+ }
+
+ throw new Error(`provider ${context.provider.name} is not supported`)
+}
+
+export const annotateTraces: BrowserCommand<[{ traces: string[]; testId: string }]> = async (
+ { project },
+ { testId, traces },
+) => {
+ const vitest = project.vitest
+ await Promise.all(traces.map((trace) => {
+ const entity = vitest.state.getReportedEntityById(testId)
+ return vitest._testRun.annotate(testId, {
+ message: relative(project.config.root, trace),
+ type: 'traces',
+ attachment: {
+ path: trace,
+ contentType: 'application/octet-stream',
+ },
+ location: entity?.location
+ ? {
+ file: entity.module.moduleId,
+ line: entity.location.line,
+ column: entity.location.column,
+ }
+ : undefined,
+ })
+ }))
+}
diff --git a/packages/browser-playwright/src/commands/type.ts b/packages/browser-playwright/src/commands/type.ts
new file mode 100644
index 000000000000..2a3d6469f578
--- /dev/null
+++ b/packages/browser-playwright/src/commands/type.ts
@@ -0,0 +1,33 @@
+import type { UserEvent } from 'vitest/browser'
+import type { UserEventCommand } from './utils'
+import { keyboardImplementation } from './keyboard'
+
+export const type: UserEventCommand = async (
+ context,
+ selector,
+ text,
+ options = {},
+) => {
+ const { skipClick = false, skipAutoClose = false } = options
+ const unreleased = new Set(Reflect.get(options, 'unreleased') as string[] ?? [])
+
+ const { iframe } = context
+ const element = iframe.locator(selector)
+
+ if (!skipClick) {
+ await element.focus()
+ }
+
+ await keyboardImplementation(
+ unreleased,
+ context.provider,
+ context.sessionId,
+ text,
+ () => element.selectText(),
+ skipAutoClose,
+ )
+
+ return {
+ unreleased: Array.from(unreleased),
+ }
+}
diff --git a/packages/browser-playwright/src/commands/upload.ts b/packages/browser-playwright/src/commands/upload.ts
new file mode 100644
index 000000000000..2851dbd5169c
--- /dev/null
+++ b/packages/browser-playwright/src/commands/upload.ts
@@ -0,0 +1,33 @@
+import type { UserEventUploadOptions } from '@vitest/browser/context'
+import type { UserEventCommand } from './utils'
+import { resolve } from 'pathe'
+
+export const upload: UserEventCommand<(element: string, files: Array, options: UserEventUploadOptions) => void> = async (
+ context,
+ selector,
+ files,
+ options,
+) => {
+ const testPath = context.testPath
+ if (!testPath) {
+ throw new Error(`Cannot upload files outside of a test`)
+ }
+ const root = context.project.config.root
+
+ const { iframe } = context
+ const playwrightFiles = files.map((file) => {
+ if (typeof file === 'string') {
+ return resolve(root, file)
+ }
+ return {
+ name: file.name,
+ mimeType: file.mimeType,
+ buffer: Buffer.from(file.base64, 'base64'),
+ }
+ })
+ await iframe.locator(selector).setInputFiles(playwrightFiles as string[], options)
+}
diff --git a/packages/browser-playwright/src/commands/utils.ts b/packages/browser-playwright/src/commands/utils.ts
new file mode 100644
index 000000000000..454171f68b1d
--- /dev/null
+++ b/packages/browser-playwright/src/commands/utils.ts
@@ -0,0 +1,17 @@
+import type { Locator } from '@vitest/browser/context'
+import type { BrowserCommand } from 'vitest/node'
+
+export type UserEventCommand any> = BrowserCommand<
+ ConvertUserEventParameters>
+>
+
+type ConvertElementToLocator = T extends Element | Locator ? string : T
+type ConvertUserEventParameters = {
+ [K in keyof T]: ConvertElementToLocator;
+}
+
+export function defineBrowserCommand(
+ fn: BrowserCommand,
+): BrowserCommand {
+ return fn
+}
diff --git a/packages/browser-playwright/src/playwright.ts b/packages/browser-playwright/src/playwright.ts
index e3db6742d923..4529ac3ba2f9 100644
--- a/packages/browser-playwright/src/playwright.ts
+++ b/packages/browser-playwright/src/playwright.ts
@@ -19,6 +19,7 @@ import type { Protocol } from 'playwright-core/types/protocol'
import type { SourceMap } from 'rollup'
import type { ResolvedConfig } from 'vite'
import type {
+ BrowserCommand,
BrowserModuleMocker,
BrowserProvider,
BrowserProviderOption,
@@ -29,6 +30,7 @@ import { createBrowserServer } from '@vitest/browser'
import { createManualModuleSource } from '@vitest/mocker/node'
import c from 'tinyrainbow'
import { createDebugger, isCSSRequest } from 'vitest/node'
+import commands from './commands'
const debug = createDebugger('vitest:browser:playwright')
@@ -104,6 +106,10 @@ export class PlaywrightBrowserProvider implements BrowserProvider {
this.browserName = project.config.browser.name as PlaywrightBrowser
this.mocker = this.createMocker()
+ for (const [name, command] of Object.entries(commands)) {
+ project.browser!.registerCommand(name as any, command as BrowserCommand)
+ }
+
// make sure the traces are finished if the test hangs
process.on('SIGTERM', () => {
if (!this.browser) {
@@ -555,7 +561,7 @@ type PWSelectOptions = NonNullable[2]>
type PWDragAndDropOptions = NonNullable[2]>
type PWSetInputFiles = NonNullable[2]>
-declare module '@vitest/browser/context' {
+declare module 'vitest/browser' {
export interface UserEventHoverOptions extends PWHoverOptions {}
export interface UserEventClickOptions extends PWClickOptions {}
export interface UserEventDoubleClickOptions extends PWDoubleClickOptions {}
diff --git a/packages/browser/src/node/commands/screenshot.ts b/packages/browser/src/node/commands/screenshot.ts
index 14c9869142dd..aaa279ab4859 100644
--- a/packages/browser/src/node/commands/screenshot.ts
+++ b/packages/browser/src/node/commands/screenshot.ts
@@ -12,6 +12,18 @@ interface ScreenshotCommandOptions extends Omit Promise<{
+ buffer: Buffer
+ path: string
+ }>
+ }
+}
+
export const screenshot: BrowserCommand<[string, ScreenshotCommandOptions]> = async (
context,
name: string,
@@ -23,11 +35,13 @@ export const screenshot: BrowserCommand<[string, ScreenshotCommandOptions]> = as
options.base64 = true
}
- const { buffer, path } = await takeScreenshot(context, name, options)
+ const { buffer, path } = await context.triggerCommand('__vitest_takeScreenshot', name, options)
return returnResult(options, path, buffer)
}
+// TODO: remove this
+
/**
* Takes a screenshot using the provided browser context and returns a buffer and the expected screenshot path.
*
diff --git a/packages/browser/src/node/index.ts b/packages/browser/src/node/index.ts
index 81775d4ef331..703bfefb2a28 100644
--- a/packages/browser/src/node/index.ts
+++ b/packages/browser/src/node/index.ts
@@ -10,9 +10,10 @@ import { ParentBrowserProject } from './projectParent'
import { setupBrowserRpc } from './rpc'
export { createBrowserPool } from './pool'
-
export type { ProjectBrowser } from './project'
+export { parseKeyDef } from './utils'
+
export const createBrowserServer: BrowserServerFactory = async (options) => {
const project = options.project
const configFile = project.vite.config.configFile
diff --git a/packages/browser/src/node/project.ts b/packages/browser/src/node/project.ts
index d8a1f12bc3f4..4f11755ecd19 100644
--- a/packages/browser/src/node/project.ts
+++ b/packages/browser/src/node/project.ts
@@ -1,7 +1,9 @@
import type { StackTraceParserOptions } from '@vitest/utils/source-map'
import type { ViteDevServer } from 'vite'
import type { ParsedStack, SerializedConfig, TestError } from 'vitest'
+import type { BrowserCommands } from 'vitest/browser'
import type {
+ BrowserCommand,
BrowserProvider,
ProjectBrowser as IProjectBrowser,
ResolvedConfig,
@@ -57,6 +59,21 @@ export class ProjectBrowser implements IProjectBrowser {
return this.parent.vite
}
+ public registerCommand(
+ name: K,
+ cb: BrowserCommand<
+ Parameters,
+ ReturnType
+ >,
+ ): void {
+ if (!/^[a-z_$][\w$]*$/i.test(name)) {
+ throw new Error(
+ `Invalid command name "${name}". Only alphanumeric characters, $ and _ are allowed.`,
+ )
+ }
+ this.parent.commands[name] = cb
+ }
+
wrapSerializedConfig(): SerializedConfig {
const config = wrapConfig(this.project.serializedConfig)
config.env ??= {}
diff --git a/packages/browser/src/node/rpc.ts b/packages/browser/src/node/rpc.ts
index 16b84aea2e56..f008c9030d19 100644
--- a/packages/browser/src/node/rpc.ts
+++ b/packages/browser/src/node/rpc.ts
@@ -242,7 +242,7 @@ export function setupBrowserRpc(globalServer: ParentBrowserProject, defaultMocke
}
const commands = globalServer.commands
if (!commands || !commands[command]) {
- throw new Error(`Unknown command "${command}".`)
+ throw new Error(`Provider ${provider.name} does not support command "${command}".`)
}
const context = Object.assign(
{
@@ -251,6 +251,9 @@ export function setupBrowserRpc(globalServer: ParentBrowserProject, defaultMocke
provider,
contextId: sessionId,
sessionId,
+ triggerCommand: (name: string, ...args: any[]) => {
+ return commands[name](context, ...args)
+ },
},
provider.getCommandsContext(sessionId),
) as any as BrowserCommandContext
diff --git a/packages/browser/src/node/utils.ts b/packages/browser/src/node/utils.ts
index 71ed636a8a56..f61ed87fdc6c 100644
--- a/packages/browser/src/node/utils.ts
+++ b/packages/browser/src/node/utils.ts
@@ -1,4 +1,41 @@
-import type { BrowserProvider, BrowserProviderOption, ResolvedBrowserOptions, TestProject } from 'vitest/node'
+import type {
+ BrowserProvider,
+ BrowserProviderOption,
+ ResolvedBrowserOptions,
+ TestProject,
+} from 'vitest/node'
+
+import { defaultKeyMap } from '@testing-library/user-event/dist/esm/keyboard/keyMap.js'
+import { parseKeyDef as tlParse } from '@testing-library/user-event/dist/esm/keyboard/parseKeyDef.js'
+
+declare enum DOM_KEY_LOCATION {
+ STANDARD = 0,
+ LEFT = 1,
+ RIGHT = 2,
+ NUMPAD = 3,
+}
+
+interface keyboardKey {
+ /** Physical location on a keyboard */
+ code?: string
+ /** Character or functional key descriptor */
+ key?: string
+ /** Location on the keyboard for keys with multiple representation */
+ location?: DOM_KEY_LOCATION
+ /** Does the character in `key` require/imply AltRight to be pressed? */
+ altGr?: boolean
+ /** Does the character in `key` require/imply a shiftKey to be pressed? */
+ shift?: boolean
+}
+
+export function parseKeyDef(text: string): {
+ keyDef: keyboardKey
+ releasePrevious: boolean
+ releaseSelf: boolean
+ repeat: number
+}[] {
+ return tlParse(defaultKeyMap, text)
+}
export function replacer(code: string, values: Record): string {
return code.replace(/\{\s*(\w+)\s*\}/g, (_, key) => values[key] ?? _)
diff --git a/packages/vitest/src/node/types/browser.ts b/packages/vitest/src/node/types/browser.ts
index 441b7dcd0e38..ccb4de6df3ac 100644
--- a/packages/vitest/src/node/types/browser.ts
+++ b/packages/vitest/src/node/types/browser.ts
@@ -3,6 +3,7 @@ import type { CancelReason } from '@vitest/runner'
import type { Awaitable, ParsedStack, TestError } from '@vitest/utils'
import type { StackTraceParserOptions } from '@vitest/utils/source-map'
import type { Plugin, ViteDevServer } from 'vite'
+import type { BrowserCommands } from 'vitest/browser'
import type { BrowserTraceViewMode } from '../../runtime/config'
import type { BrowserTesterOptions } from '../../types/browser'
import type { TestProject } from '../project'
@@ -276,6 +277,10 @@ export interface BrowserCommandContext {
provider: BrowserProvider
project: TestProject
sessionId: string
+ triggerCommand: (
+ name: K,
+ ...args: Parameters
+ ) => ReturnType
}
export interface BrowserServerStateSession {
@@ -308,10 +313,17 @@ export interface ProjectBrowser {
initBrowserProvider: (project: TestProject) => Promise
parseStacktrace: (stack: string) => ParsedStack[]
parseErrorStacktrace: (error: TestError, options?: StackTraceParserOptions) => ParsedStack[]
+ registerCommand: (
+ name: K,
+ cb: BrowserCommand<
+ Parameters,
+ ReturnType
+ >
+ ) => void
}
-export interface BrowserCommand {
- (context: BrowserCommandContext, ...payload: Payload): Awaitable
+export interface BrowserCommand {
+ (context: BrowserCommandContext, ...payload: Payload): Awaitable
}
export interface BrowserScript {
diff --git a/tsconfig.base.json b/tsconfig.base.json
index b127f95f647c..06d63c2573c9 100644
--- a/tsconfig.base.json
+++ b/tsconfig.base.json
@@ -26,6 +26,7 @@
"vitest": ["./packages/vitest/src/public/index.ts"],
"vitest/internal/module-runner": ["./packages/vitest/src/public/module-runner.ts"],
"vitest/globals": ["./packages/vitest/globals.d.ts"],
+ "vitest/browser": ["./packages/vitest/browser/context.d.ts"],
"vitest/*": ["./packages/vitest/src/public/*"],
"vite-node": ["./packages/vite-node/src/index.ts"],
"vite-node/*": ["./packages/vite-node/src/*"]
From af88f996799198eb69020860f6039bff31e8b503 Mon Sep 17 00:00:00 2001
From: Vladimir Sheremet
Date: Mon, 29 Sep 2025 12:36:03 +0200
Subject: [PATCH 08/43] feat: add wdio commands
---
.../browser-playwright/src/commands/index.ts | 4 +-
.../src/commands/screenshot.ts | 58 +--
.../browser-webdriverio/src/commands/clear.ts | 10 +
.../browser-webdriverio/src/commands/click.ts | 44 ++
.../src/commands/dragAndDrop.ts | 27 ++
.../browser-webdriverio/src/commands/fill.ts | 12 +
.../browser-webdriverio/src/commands/hover.ts | 11 +
.../browser-webdriverio/src/commands/index.ts | 30 ++
.../src/commands/keyboard.ts | 126 ++++++
.../src/commands/screenshot.ts | 70 ++++
.../src/commands/select.ts | 25 ++
.../browser-webdriverio/src/commands/tab.ts | 11 +
.../browser-webdriverio/src/commands/type.ts | 38 ++
.../src/commands/upload.ts | 34 ++
.../browser-webdriverio/src/commands/utils.ts | 11 +
.../src/commands/viewport.ts | 9 +
.../browser-webdriverio/src/webdriverio.ts | 6 +
packages/browser/src/node/index.ts | 3 +-
packages/browser/src/node/pool.ts | 385 ------------------
packages/browser/src/node/project.ts | 1 +
packages/browser/src/node/utils.ts | 24 ++
21 files changed, 495 insertions(+), 444 deletions(-)
create mode 100644 packages/browser-webdriverio/src/commands/clear.ts
create mode 100644 packages/browser-webdriverio/src/commands/click.ts
create mode 100644 packages/browser-webdriverio/src/commands/dragAndDrop.ts
create mode 100644 packages/browser-webdriverio/src/commands/fill.ts
create mode 100644 packages/browser-webdriverio/src/commands/hover.ts
create mode 100644 packages/browser-webdriverio/src/commands/index.ts
create mode 100644 packages/browser-webdriverio/src/commands/keyboard.ts
create mode 100644 packages/browser-webdriverio/src/commands/screenshot.ts
create mode 100644 packages/browser-webdriverio/src/commands/select.ts
create mode 100644 packages/browser-webdriverio/src/commands/tab.ts
create mode 100644 packages/browser-webdriverio/src/commands/type.ts
create mode 100644 packages/browser-webdriverio/src/commands/upload.ts
create mode 100644 packages/browser-webdriverio/src/commands/utils.ts
create mode 100644 packages/browser-webdriverio/src/commands/viewport.ts
delete mode 100644 packages/browser/src/node/pool.ts
diff --git a/packages/browser-playwright/src/commands/index.ts b/packages/browser-playwright/src/commands/index.ts
index 2714f873a023..f143414771dd 100644
--- a/packages/browser-playwright/src/commands/index.ts
+++ b/packages/browser-playwright/src/commands/index.ts
@@ -4,7 +4,7 @@ import { dragAndDrop } from './dragAndDrop'
import { fill } from './fill'
import { hover } from './hover'
import { keyboard, keyboardCleanup } from './keyboard'
-import { screenshot } from './screenshot'
+import { takeScreenshot } from './screenshot'
import { selectOptions } from './select'
import { tab } from './tab'
import {
@@ -22,7 +22,7 @@ export default {
__vitest_click: click as typeof click,
__vitest_dblClick: dblClick as typeof dblClick,
__vitest_tripleClick: tripleClick as typeof tripleClick,
- __vitest_screenshot: screenshot as typeof screenshot,
+ __vitest_takeScreenshot: takeScreenshot as typeof takeScreenshot,
__vitest_type: type as typeof type,
__vitest_clear: clear as typeof clear,
__vitest_fill: fill as typeof fill,
diff --git a/packages/browser-playwright/src/commands/screenshot.ts b/packages/browser-playwright/src/commands/screenshot.ts
index 7c1058260047..9e6c18a76dde 100644
--- a/packages/browser-playwright/src/commands/screenshot.ts
+++ b/packages/browser-playwright/src/commands/screenshot.ts
@@ -1,29 +1,13 @@
import type { ScreenshotOptions } from 'vitest/browser'
-import type { BrowserCommand, BrowserCommandContext, ResolvedConfig } from 'vitest/node'
+import type { BrowserCommandContext } from 'vitest/node'
import { mkdir } from 'node:fs/promises'
-import { basename, dirname, normalize, relative, resolve } from 'pathe'
+import { resolveScreenshotPath } from '@vitest/browser'
+import { dirname, normalize } from 'pathe'
interface ScreenshotCommandOptions extends Omit {
element?: string
mask?: readonly string[]
}
-
-export const screenshot: BrowserCommand<[string, ScreenshotCommandOptions]> = async (
- context,
- name: string,
- options = {},
-) => {
- options.save ??= true
-
- if (!options.save) {
- options.base64 = true
- }
-
- const { buffer, path } = await takeScreenshot(context, name, options)
-
- return returnResult(options, path, buffer)
-}
-
/**
* Takes a screenshot using the provided browser context and returns a buffer and the expected screenshot path.
*
@@ -77,39 +61,3 @@ export async function takeScreenshot(
})
return { buffer, path }
}
-
-function resolveScreenshotPath(
- testPath: string,
- name: string,
- config: ResolvedConfig,
- customPath: string | undefined,
-): string {
- if (customPath) {
- return resolve(dirname(testPath), customPath)
- }
- const dir = dirname(testPath)
- const base = basename(testPath)
- if (config.browser.screenshotDirectory) {
- return resolve(
- config.browser.screenshotDirectory,
- relative(config.root, dir),
- base,
- name,
- )
- }
- return resolve(dir, '__screenshots__', base, name)
-}
-
-function returnResult(
- options: ScreenshotCommandOptions,
- path: string,
- buffer: Buffer,
-) {
- if (!options.save) {
- return buffer.toString('base64')
- }
- if (options.base64) {
- return { path, base64: buffer.toString('base64') }
- }
- return path
-}
diff --git a/packages/browser-webdriverio/src/commands/clear.ts b/packages/browser-webdriverio/src/commands/clear.ts
new file mode 100644
index 000000000000..214f72afa3c8
--- /dev/null
+++ b/packages/browser-webdriverio/src/commands/clear.ts
@@ -0,0 +1,10 @@
+import type { UserEvent } from 'vitest/browser'
+import type { UserEventCommand } from './utils'
+
+export const clear: UserEventCommand = async (
+ context,
+ selector,
+) => {
+ const browser = context.browser
+ await browser.$(selector).clearValue()
+}
diff --git a/packages/browser-webdriverio/src/commands/click.ts b/packages/browser-webdriverio/src/commands/click.ts
new file mode 100644
index 000000000000..292103eba281
--- /dev/null
+++ b/packages/browser-webdriverio/src/commands/click.ts
@@ -0,0 +1,44 @@
+import type { UserEvent } from 'vitest/browser'
+import type { UserEventCommand } from './utils'
+
+export const click: UserEventCommand = async (
+ context,
+ selector,
+ options = {},
+) => {
+ const browser = context.browser
+ await browser.$(selector).click(options as any)
+}
+
+export const dblClick: UserEventCommand = async (
+ context,
+ selector,
+ _options = {},
+) => {
+ const browser = context.browser
+ await browser.$(selector).doubleClick()
+}
+
+export const tripleClick: UserEventCommand = async (
+ context,
+ selector,
+ _options = {},
+) => {
+ const browser = context.browser
+ await browser
+ .action('pointer', { parameters: { pointerType: 'mouse' } })
+ // move the pointer over the button
+ .move({ origin: browser.$(selector) })
+ // simulate 3 clicks
+ .down()
+ .up()
+ .pause(50)
+ .down()
+ .up()
+ .pause(50)
+ .down()
+ .up()
+ .pause(50)
+ // run the sequence
+ .perform()
+}
diff --git a/packages/browser-webdriverio/src/commands/dragAndDrop.ts b/packages/browser-webdriverio/src/commands/dragAndDrop.ts
new file mode 100644
index 000000000000..549f4dbccabb
--- /dev/null
+++ b/packages/browser-webdriverio/src/commands/dragAndDrop.ts
@@ -0,0 +1,27 @@
+import type { UserEvent } from 'vitest/browser'
+import type { UserEventCommand } from './utils'
+
+export const dragAndDrop: UserEventCommand = async (
+ context,
+ source,
+ target,
+ options_,
+) => {
+ const $source = context.browser.$(source)
+ const $target = context.browser.$(target)
+ const options = (options_ || {}) as any
+ const duration = options.duration ?? 10
+
+ // https://github.com/webdriverio/webdriverio/issues/8022#issuecomment-1700919670
+ await context.browser
+ .action('pointer')
+ .move({ duration: 0, origin: $source, x: options.sourceX ?? 0, y: options.sourceY ?? 0 })
+ .down({ button: 0 })
+ .move({ duration: 0, origin: 'pointer', x: 0, y: 0 })
+ .pause(duration)
+ .move({ duration: 0, origin: $target, x: options.targetX ?? 0, y: options.targetY ?? 0 })
+ .move({ duration: 0, origin: 'pointer', x: 1, y: 0 })
+ .move({ duration: 0, origin: 'pointer', x: -1, y: 0 })
+ .up({ button: 0 })
+ .perform()
+}
diff --git a/packages/browser-webdriverio/src/commands/fill.ts b/packages/browser-webdriverio/src/commands/fill.ts
new file mode 100644
index 000000000000..f3f0b1ebb356
--- /dev/null
+++ b/packages/browser-webdriverio/src/commands/fill.ts
@@ -0,0 +1,12 @@
+import type { UserEvent } from 'vitest/browser'
+import type { UserEventCommand } from './utils'
+
+export const fill: UserEventCommand = async (
+ context,
+ selector,
+ text,
+ _options = {},
+) => {
+ const browser = context.browser
+ await browser.$(selector).setValue(text)
+}
diff --git a/packages/browser-webdriverio/src/commands/hover.ts b/packages/browser-webdriverio/src/commands/hover.ts
new file mode 100644
index 000000000000..3a0f5d248f06
--- /dev/null
+++ b/packages/browser-webdriverio/src/commands/hover.ts
@@ -0,0 +1,11 @@
+import type { UserEvent } from 'vitest/browser'
+import type { UserEventCommand } from './utils'
+
+export const hover: UserEventCommand = async (
+ context,
+ selector,
+ options = {},
+) => {
+ const browser = context.browser
+ await browser.$(selector).moveTo(options as any)
+}
diff --git a/packages/browser-webdriverio/src/commands/index.ts b/packages/browser-webdriverio/src/commands/index.ts
new file mode 100644
index 000000000000..a589c0265fb6
--- /dev/null
+++ b/packages/browser-webdriverio/src/commands/index.ts
@@ -0,0 +1,30 @@
+import { clear } from './clear'
+import { click, dblClick, tripleClick } from './click'
+import { dragAndDrop } from './dragAndDrop'
+import { fill } from './fill'
+import { hover } from './hover'
+import { keyboard, keyboardCleanup } from './keyboard'
+import { takeScreenshot } from './screenshot'
+import { selectOptions } from './select'
+import { tab } from './tab'
+import { type } from './type'
+import { upload } from './upload'
+import { viewport } from './viewport'
+
+export default {
+ __vitest_upload: upload as typeof upload,
+ __vitest_click: click as typeof click,
+ __vitest_dblClick: dblClick as typeof dblClick,
+ __vitest_tripleClick: tripleClick as typeof tripleClick,
+ __vitest_takeScreenshot: takeScreenshot as typeof takeScreenshot,
+ __vitest_type: type as typeof type,
+ __vitest_clear: clear as typeof clear,
+ __vitest_fill: fill as typeof fill,
+ __vitest_tab: tab as typeof tab,
+ __vitest_keyboard: keyboard as typeof keyboard,
+ __vitest_selectOptions: selectOptions as typeof selectOptions,
+ __vitest_dragAndDrop: dragAndDrop as typeof dragAndDrop,
+ __vitest_hover: hover as typeof hover,
+ __vitest_cleanup: keyboardCleanup as typeof keyboardCleanup,
+ __vitest_viewport: viewport as typeof viewport,
+}
diff --git a/packages/browser-webdriverio/src/commands/keyboard.ts b/packages/browser-webdriverio/src/commands/keyboard.ts
new file mode 100644
index 000000000000..1af975c925b5
--- /dev/null
+++ b/packages/browser-webdriverio/src/commands/keyboard.ts
@@ -0,0 +1,126 @@
+import type { BrowserProvider } from 'vitest/node'
+import type { WebdriverBrowserProvider } from '../webdriverio'
+import type { UserEventCommand } from './utils'
+
+import { parseKeyDef } from '@vitest/browser'
+import { Key } from 'webdriverio'
+
+export interface KeyboardState {
+ unreleased: string[]
+}
+
+export const keyboard: UserEventCommand<(
+ text: string,
+ state: KeyboardState
+) => Promise<{ unreleased: string[] }>> = async (
+ context,
+ text,
+ state,
+) => {
+ await context.browser.execute(focusIframe)
+
+ const pressed = new Set(state.unreleased)
+
+ await keyboardImplementation(
+ pressed,
+ context.provider,
+ context.sessionId,
+ text,
+ async () => {
+ await context.browser.execute(selectAll)
+ },
+ true,
+ )
+
+ return {
+ unreleased: Array.from(pressed),
+ }
+}
+
+export const keyboardCleanup: UserEventCommand<(state: KeyboardState) => Promise> = async (
+ context,
+ state,
+) => {
+ if (!state.unreleased) {
+ return
+ }
+ const keyboard = context.browser.action('key')
+ for (const key of state.unreleased) {
+ keyboard.up(key)
+ }
+ await keyboard.perform()
+}
+
+export async function keyboardImplementation(
+ pressed: Set,
+ provider: BrowserProvider,
+ _sessionId: string,
+ text: string,
+ selectAll: () => Promise,
+ skipRelease: boolean,
+): Promise<{ pressed: Set }> {
+ const browser = (provider as WebdriverBrowserProvider).browser!
+ const actions = parseKeyDef(text)
+
+ let keyboard = browser.action('key')
+
+ for (const { releasePrevious, releaseSelf, repeat, keyDef } of actions) {
+ let key = keyDef.key!
+ const special = Key[key as 'Shift']
+
+ if (special) {
+ key = special
+ }
+
+ if (pressed.has(key)) {
+ keyboard.up(key)
+ pressed.delete(key)
+ }
+
+ if (!releasePrevious) {
+ if (key === 'selectall') {
+ await keyboard.perform()
+ keyboard = browser.action('key')
+ await selectAll()
+ continue
+ }
+
+ for (let i = 1; i <= repeat; i++) {
+ keyboard.down(key)
+ }
+
+ if (releaseSelf) {
+ keyboard.up(key)
+ }
+ else {
+ pressed.add(key)
+ }
+ }
+ }
+
+ // seems like webdriverio doesn't release keys automatically if skipRelease is true and all events are keyUp
+ const allRelease = keyboard.toJSON().actions.every(action => action.type === 'keyUp')
+
+ await keyboard.perform(allRelease ? false : skipRelease)
+
+ return {
+ pressed,
+ }
+}
+
+function focusIframe() {
+ if (
+ !document.activeElement
+ || document.activeElement.ownerDocument !== document
+ || document.activeElement === document.body
+ ) {
+ window.focus()
+ }
+}
+
+function selectAll() {
+ const element = document.activeElement as HTMLInputElement
+ if (element && typeof element.select === 'function') {
+ element.select()
+ }
+}
diff --git a/packages/browser-webdriverio/src/commands/screenshot.ts b/packages/browser-webdriverio/src/commands/screenshot.ts
new file mode 100644
index 000000000000..1d090c6f3ce8
--- /dev/null
+++ b/packages/browser-webdriverio/src/commands/screenshot.ts
@@ -0,0 +1,70 @@
+import type { ScreenshotOptions } from 'vitest/browser'
+import type { BrowserCommandContext } from 'vitest/node'
+import { mkdir, rm } from 'node:fs/promises'
+import { normalize as platformNormalize } from 'node:path'
+import { resolveScreenshotPath } from '@vitest/browser'
+import { nanoid } from '@vitest/utils/helpers'
+import { dirname, normalize, resolve } from 'pathe'
+
+interface ScreenshotCommandOptions extends Omit {
+ element?: string
+ mask?: readonly string[]
+}
+
+/**
+ * Takes a screenshot using the provided browser context and returns a buffer and the expected screenshot path.
+ *
+ * **Note**: the returned `path` indicates where the screenshot *might* be found.
+ * It is not guaranteed to exist, especially if `options.save` is `false`.
+ *
+ * @throws {Error} If the function is not called within a test or if the browser provider does not support screenshots.
+ */
+export async function takeScreenshot(
+ context: BrowserCommandContext,
+ name: string,
+ options: Omit,
+): Promise<{ buffer: Buffer; path: string }> {
+ if (!context.testPath) {
+ throw new Error(`Cannot take a screenshot without a test path`)
+ }
+
+ const path = resolveScreenshotPath(
+ context.testPath,
+ name,
+ context.project.config,
+ options.path,
+ )
+
+ // playwright does not need a screenshot path if we don't intend to save it
+ let savePath: string | undefined
+
+ if (options.save) {
+ savePath = normalize(path)
+
+ await mkdir(dirname(savePath), { recursive: true })
+ }
+
+ // webdriverio needs a path, so if one is not already set we create a temporary one
+ if (savePath === undefined) {
+ savePath = resolve(context.project.tmpDir, nanoid())
+
+ await mkdir(context.project.tmpDir, { recursive: true })
+ }
+
+ const page = context.browser
+ const element = !options.element
+ ? await page.$('body')
+ : await page.$(`${options.element}`)
+
+ // webdriverio expects the path to contain the extension and only works with PNG files
+ const savePathWithExtension = savePath.endsWith('.png') ? savePath : `${savePath}.png`
+
+ // there seems to be a bug in webdriverio, `X:/` gets appended to cwd, so we convert to `X:\`
+ const buffer = await element.saveScreenshot(
+ platformNormalize(savePathWithExtension),
+ )
+ if (!options.save) {
+ await rm(savePathWithExtension, { force: true })
+ }
+ return { buffer, path }
+}
diff --git a/packages/browser-webdriverio/src/commands/select.ts b/packages/browser-webdriverio/src/commands/select.ts
new file mode 100644
index 000000000000..409633803707
--- /dev/null
+++ b/packages/browser-webdriverio/src/commands/select.ts
@@ -0,0 +1,25 @@
+import type { UserEvent } from 'vitest/browser'
+import type { UserEventCommand } from './utils'
+
+export const selectOptions: UserEventCommand = async (
+ context,
+ selector,
+ userValues,
+ _options = {},
+) => {
+ const values = userValues as any as [({ index: number })]
+
+ if (!values.length) {
+ return
+ }
+
+ const browser = context.browser
+
+ if (values.length === 1 && 'index' in values[0]) {
+ const selectElement = browser.$(selector)
+ await selectElement.selectByIndex(values[0].index)
+ }
+ else {
+ throw new Error('Provider "webdriverio" doesn\'t support selecting multiple values at once')
+ }
+}
diff --git a/packages/browser-webdriverio/src/commands/tab.ts b/packages/browser-webdriverio/src/commands/tab.ts
new file mode 100644
index 000000000000..3d9fc77b8eda
--- /dev/null
+++ b/packages/browser-webdriverio/src/commands/tab.ts
@@ -0,0 +1,11 @@
+import type { UserEvent } from 'vitest/browser'
+import type { UserEventCommand } from './utils'
+import { Key } from 'webdriverio'
+
+export const tab: UserEventCommand = async (
+ context,
+ options = {},
+) => {
+ const browser = context.browser
+ await browser.keys(options.shift === true ? [Key.Shift, Key.Tab] : [Key.Tab])
+}
diff --git a/packages/browser-webdriverio/src/commands/type.ts b/packages/browser-webdriverio/src/commands/type.ts
new file mode 100644
index 000000000000..24b71aa58775
--- /dev/null
+++ b/packages/browser-webdriverio/src/commands/type.ts
@@ -0,0 +1,38 @@
+import type { UserEvent } from 'vitest/browser'
+import type { UserEventCommand } from './utils'
+import { keyboardImplementation } from './keyboard'
+
+export const type: UserEventCommand = async (
+ context,
+ selector,
+ text,
+ options = {},
+) => {
+ const { skipClick = false, skipAutoClose = false } = options
+ const unreleased = new Set(Reflect.get(options, 'unreleased') as string[] ?? [])
+
+ const browser = context.browser
+ const element = browser.$(selector)
+
+ if (!skipClick && !await element.isFocused()) {
+ await element.click()
+ }
+
+ await keyboardImplementation(
+ unreleased,
+ context.provider,
+ context.sessionId,
+ text,
+ () => browser.execute(() => {
+ const element = document.activeElement as HTMLInputElement
+ if (element && typeof element.select === 'function') {
+ element.select()
+ }
+ }),
+ skipAutoClose,
+ )
+
+ return {
+ unreleased: Array.from(unreleased),
+ }
+}
diff --git a/packages/browser-webdriverio/src/commands/upload.ts b/packages/browser-webdriverio/src/commands/upload.ts
new file mode 100644
index 000000000000..48d8c9aaf1c2
--- /dev/null
+++ b/packages/browser-webdriverio/src/commands/upload.ts
@@ -0,0 +1,34 @@
+import type { UserEventUploadOptions } from '@vitest/browser/context'
+import type { UserEventCommand } from './utils'
+import { resolve } from 'pathe'
+
+export const upload: UserEventCommand<(element: string, files: Array, options: UserEventUploadOptions) => void> = async (
+ context,
+ selector,
+ files,
+ _options,
+) => {
+ const testPath = context.testPath
+ if (!testPath) {
+ throw new Error(`Cannot upload files outside of a test`)
+ }
+ const root = context.project.config.root
+
+ for (const file of files) {
+ if (typeof file !== 'string') {
+ throw new TypeError(`The "${context.provider.name}" provider doesn't support uploading files objects. Provide a file path instead.`)
+ }
+ }
+
+ const element = context.browser.$(selector)
+
+ for (const file of files) {
+ const filepath = resolve(root, file as string)
+ const remoteFilePath = await context.browser.uploadFile(filepath)
+ await element.addValue(remoteFilePath)
+ }
+}
diff --git a/packages/browser-webdriverio/src/commands/utils.ts b/packages/browser-webdriverio/src/commands/utils.ts
new file mode 100644
index 000000000000..89afac85af70
--- /dev/null
+++ b/packages/browser-webdriverio/src/commands/utils.ts
@@ -0,0 +1,11 @@
+import type { Locator } from 'vitest/browser'
+import type { BrowserCommand } from 'vitest/node'
+
+export type UserEventCommand any> = BrowserCommand<
+ ConvertUserEventParameters>
+>
+
+type ConvertElementToLocator = T extends Element | Locator ? string : T
+type ConvertUserEventParameters = {
+ [K in keyof T]: ConvertElementToLocator;
+}
diff --git a/packages/browser-webdriverio/src/commands/viewport.ts b/packages/browser-webdriverio/src/commands/viewport.ts
new file mode 100644
index 000000000000..98111fad6608
--- /dev/null
+++ b/packages/browser-webdriverio/src/commands/viewport.ts
@@ -0,0 +1,9 @@
+import type { WebdriverBrowserProvider } from '../webdriverio'
+import type { UserEventCommand } from './utils'
+
+export const viewport: UserEventCommand<(options: {
+ width: number
+ height: number
+}) => void> = async (context, options) => {
+ await (context.provider as WebdriverBrowserProvider).setViewport(options)
+}
diff --git a/packages/browser-webdriverio/src/webdriverio.ts b/packages/browser-webdriverio/src/webdriverio.ts
index bc01107d6a93..0cb17d9e2fae 100644
--- a/packages/browser-webdriverio/src/webdriverio.ts
+++ b/packages/browser-webdriverio/src/webdriverio.ts
@@ -4,6 +4,7 @@ import type {
} from '@vitest/browser/context'
import type { Capabilities } from '@wdio/types'
import type {
+ BrowserCommand,
BrowserProvider,
BrowserProviderOption,
CDPSession,
@@ -13,6 +14,7 @@ import type { ClickOptions, DragAndDropOptions, remote } from 'webdriverio'
import { createBrowserServer } from '@vitest/browser'
import { createDebugger } from 'vitest/node'
+import commands from './commands'
const debug = createDebugger('vitest:browser:wdio')
@@ -68,6 +70,10 @@ export class WebdriverBrowserProvider implements BrowserProvider {
this.project = project
this.browserName = project.config.browser.name as WebdriverBrowser
this.options = options
+
+ for (const [name, command] of Object.entries(commands)) {
+ project.browser!.registerCommand(name as any, command as BrowserCommand)
+ }
}
isIframeSwitched(): boolean {
diff --git a/packages/browser/src/node/index.ts b/packages/browser/src/node/index.ts
index 703bfefb2a28..d5ce35ab629c 100644
--- a/packages/browser/src/node/index.ts
+++ b/packages/browser/src/node/index.ts
@@ -9,10 +9,9 @@ import BrowserPlugin from './plugin'
import { ParentBrowserProject } from './projectParent'
import { setupBrowserRpc } from './rpc'
-export { createBrowserPool } from './pool'
export type { ProjectBrowser } from './project'
-export { parseKeyDef } from './utils'
+export { parseKeyDef, resolveScreenshotPath } from './utils'
export const createBrowserServer: BrowserServerFactory = async (options) => {
const project = options.project
diff --git a/packages/browser/src/node/pool.ts b/packages/browser/src/node/pool.ts
deleted file mode 100644
index 994b88b3ceb3..000000000000
--- a/packages/browser/src/node/pool.ts
+++ /dev/null
@@ -1,385 +0,0 @@
-import type { DeferPromise } from '@vitest/utils/helpers'
-import type {
- BrowserProvider,
- ProcessPool,
- TestProject,
- TestSpecification,
- Vitest,
-} from 'vitest/node'
-import crypto from 'node:crypto'
-import * as nodeos from 'node:os'
-import { performance } from 'node:perf_hooks'
-import { createDefer } from '@vitest/utils/helpers'
-import { stringify } from 'flatted'
-import { createDebugger } from 'vitest/node'
-
-const debug = createDebugger('vitest:browser:pool')
-
-export function createBrowserPool(vitest: Vitest): ProcessPool {
- const providers = new Set()
-
- const numCpus
- = typeof nodeos.availableParallelism === 'function'
- ? nodeos.availableParallelism()
- : nodeos.cpus().length
-
- const threadsCount = vitest.config.watch
- ? Math.max(Math.floor(numCpus / 2), 1)
- : Math.max(numCpus - 1, 1)
-
- const projectPools = new WeakMap()
-
- const ensurePool = (project: TestProject) => {
- if (projectPools.has(project)) {
- return projectPools.get(project)!
- }
-
- debug?.('creating pool for project %s', project.name)
-
- const resolvedUrls = project.browser!.vite.resolvedUrls
- const origin = resolvedUrls?.local[0] ?? resolvedUrls?.network[0]
-
- if (!origin) {
- throw new Error(
- `Can't find browser origin URL for project "${project.name}"`,
- )
- }
-
- const pool: BrowserPool = new BrowserPool(project, {
- maxWorkers: getThreadsCount(project),
- origin,
- })
- projectPools.set(project, pool)
- vitest.onCancel(() => {
- pool.cancel()
- })
-
- return pool
- }
-
- const runWorkspaceTests = async (method: 'run' | 'collect', specs: TestSpecification[]) => {
- const groupedFiles = new Map()
- for (const { project, moduleId } of specs) {
- const files = groupedFiles.get(project) || []
- files.push(moduleId)
- groupedFiles.set(project, files)
- }
-
- let isCancelled = false
- vitest.onCancel(() => {
- isCancelled = true
- })
-
- const initialisedPools = await Promise.all([...groupedFiles.entries()].map(async ([project, files]) => {
- await project._initBrowserProvider()
-
- if (!project.browser) {
- throw new TypeError(`The browser server was not initialized${project.name ? ` for the "${project.name}" project` : ''}. This is a bug in Vitest. Please, open a new issue with reproduction.`)
- }
-
- if (isCancelled) {
- return
- }
-
- debug?.('provider is ready for %s project', project.name)
-
- const pool = ensurePool(project)
- vitest.state.clearFiles(project, files)
- providers.add(project.browser!.provider)
-
- return {
- pool,
- provider: project.browser!.provider,
- runTests: () => pool.runTests(method, files),
- }
- }))
-
- if (isCancelled) {
- return
- }
-
- const parallelPools: (() => Promise)[] = []
- const nonParallelPools: (() => Promise)[] = []
-
- for (const pool of initialisedPools) {
- if (!pool) {
- // this means it was cancelled
- return
- }
-
- if (pool.provider.mocker && pool.provider.supportsParallelism) {
- parallelPools.push(pool.runTests)
- }
- else {
- nonParallelPools.push(pool.runTests)
- }
- }
-
- await Promise.all(parallelPools.map(runTests => runTests()))
-
- for (const runTests of nonParallelPools) {
- if (isCancelled) {
- return
- }
-
- await runTests()
- }
- }
-
- 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
- }
-
- return {
- name: 'browser',
- async close() {
- await Promise.all([...providers].map(provider => provider.close()))
- vitest._browserSessions.sessionIds.clear()
- providers.clear()
- vitest.projects.forEach((project) => {
- project.browser?.state.orchestrators.forEach((orchestrator) => {
- orchestrator.$close()
- })
- })
- debug?.('browser pool closed all providers')
- },
- runTests: files => runWorkspaceTests('run', files),
- collectTests: files => runWorkspaceTests('collect', files),
- }
-}
-
-function escapePathToRegexp(path: string): string {
- return path.replace(/[/\\.?*()^${}|[\]+]/g, '\\$&')
-}
-
-class BrowserPool {
- private _queue: string[] = []
- private _promise: DeferPromise | undefined
- private _providedContext: string | undefined
-
- private readySessions = new Set()
-
- constructor(
- private project: TestProject,
- private options: {
- maxWorkers: number
- origin: string
- },
- ) {}
-
- public cancel(): void {
- this._queue = []
- }
-
- public reject(error: Error): void {
- this._promise?.reject(error)
- this._promise = undefined
- this.cancel()
- }
-
- get orchestrators() {
- return this.project.browser!.state.orchestrators
- }
-
- async runTests(method: 'run' | 'collect', files: string[]): Promise {
- this._promise ??= createDefer()
-
- if (!files.length) {
- debug?.('no tests found, finishing test run immediately')
- this._promise.resolve()
- return this._promise
- }
-
- this._providedContext = stringify(this.project.getProvidedContext())
-
- this._queue.push(...files)
-
- this.readySessions.forEach((sessionId) => {
- if (this._queue.length) {
- this.readySessions.delete(sessionId)
- this.runNextTest(method, sessionId)
- }
- })
-
- if (this.orchestrators.size >= this.options.maxWorkers) {
- debug?.('all orchestrators are ready, not creating more')
- return this._promise
- }
-
- // 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,
- )
-
- const promises: Promise[] = []
- for (let i = 0; i < workerCount; i++) {
- const sessionId = crypto.randomUUID()
- this.project.vitest._browserSessions.sessionIds.add(sessionId)
- const project = this.project.name
- debug?.('[%s] creating session for %s', sessionId, project)
- const page = this.openPage(sessionId).then(() => {
- // start running tests on the page when it's ready
- this.runNextTest(method, sessionId)
- })
- promises.push(page)
- }
- await Promise.all(promises)
- debug?.('all sessions are created')
- return this._promise
- }
-
- private async openPage(sessionId: string) {
- const sessionPromise = this.project.vitest._browserSessions.createSession(
- sessionId,
- this.project,
- this,
- )
- const browser = this.project.browser!
- const url = new URL('/__vitest_test__/', this.options.origin)
- url.searchParams.set('sessionId', sessionId)
- const pagePromise = browser.provider.openPage(
- sessionId,
- url.toString(),
- )
- await Promise.all([sessionPromise, pagePromise])
- }
-
- private getOrchestrator(sessionId: string) {
- const orchestrator = this.orchestrators.get(sessionId)
- if (!orchestrator) {
- throw new Error(`Orchestrator not found for session ${sessionId}. This is a bug in Vitest. Please, open a new issue with reproduction.`)
- }
- return orchestrator
- }
-
- private finishSession(sessionId: string): void {
- this.readySessions.add(sessionId)
-
- // the last worker finished running tests
- if (this.readySessions.size === this.orchestrators.size) {
- this._promise?.resolve()
- this._promise = undefined
- debug?.('[%s] all tests finished running', sessionId)
- }
- else {
- debug?.(
- `did not finish sessions for ${sessionId}: |ready - %s| |overall - %s|`,
- [...this.readySessions].join(', '),
- [...this.orchestrators.keys()].join(', '),
- )
- }
- }
-
- private runNextTest(method: 'run' | 'collect', sessionId: string): void {
- const file = this._queue.shift()
-
- if (!file) {
- debug?.('[%s] no more tests to run', sessionId)
- const isolate = this.project.config.browser.isolate
- // we don't need to cleanup testers if isolation is enabled,
- // because cleanup is done at the end of every test
- if (isolate) {
- this.finishSession(sessionId)
- return
- }
-
- // we need to cleanup testers first because there is only
- // one iframe and it does the cleanup only after everything is completed
- const orchestrator = this.getOrchestrator(sessionId)
- orchestrator.cleanupTesters()
- .catch(error => this.reject(error))
- .finally(() => this.finishSession(sessionId))
- return
- }
-
- if (!this._promise) {
- throw new Error(`Unexpected empty queue`)
- }
- const startTime = performance.now()
-
- const orchestrator = this.getOrchestrator(sessionId)
- debug?.('[%s] run test %s', sessionId, file)
-
- this.setBreakpoint(sessionId, file).then(() => {
- // this starts running tests inside the orchestrator
- orchestrator.createTesters(
- {
- method,
- files: [file],
- // this will be parsed by the test iframe, not the orchestrator
- // so we need to stringify it first to avoid double serialization
- providedContext: this._providedContext || '[{}]',
- startTime,
- },
- )
- .then(() => {
- debug?.('[%s] test %s finished running', sessionId, file)
- this.runNextTest(method, sessionId)
- })
- .catch((error) => {
- // if user cancels the test run manually, ignore the error and exit gracefully
- if (
- this.project.vitest.isCancelling
- && error instanceof Error
- && error.message.startsWith('Browser connection was closed while running tests')
- ) {
- this.cancel()
- this._promise?.resolve()
- this._promise = undefined
- debug?.('[%s] browser connection was closed', sessionId)
- return
- }
- debug?.('[%s] error during %s test run: %s', sessionId, file, error)
- this.reject(error)
- })
- }).catch(err => this.reject(err))
- }
-
- async setBreakpoint(sessionId: string, file: string) {
- if (!this.project.config.inspector.waitForDebugger) {
- return
- }
-
- const provider = this.project.browser!.provider
- const browser = this.project.config.browser.name
-
- if (shouldIgnoreDebugger(provider.name, browser)) {
- debug?.('[$s] ignoring debugger in %s browser because it is not supported', sessionId, browser)
- return
- }
-
- if (!provider.getCDPSession) {
- throw new Error('Unable to set breakpoint, CDP not supported')
- }
-
- debug?.('[%s] set breakpoint for %s', sessionId, file)
- const session = await provider.getCDPSession(sessionId)
- await session.send('Debugger.enable', {})
- await session.send('Debugger.setBreakpointByUrl', {
- lineNumber: 0,
- urlRegex: escapePathToRegexp(file),
- })
- }
-}
-
-function shouldIgnoreDebugger(provider: string, browser: string) {
- if (provider === 'webdriverio') {
- return browser !== 'chrome' && browser !== 'edge'
- }
- return browser !== 'chromium'
-}
diff --git a/packages/browser/src/node/project.ts b/packages/browser/src/node/project.ts
index 4f11755ecd19..414ea765dbf0 100644
--- a/packages/browser/src/node/project.ts
+++ b/packages/browser/src/node/project.ts
@@ -66,6 +66,7 @@ export class ProjectBrowser implements IProjectBrowser {
ReturnType
>,
): void {
+ // TODO: register only for a specific project! don't override the global one because it's possible to have different providers in different projects
if (!/^[a-z_$][\w$]*$/i.test(name)) {
throw new Error(
`Invalid command name "${name}". Only alphanumeric characters, $ and _ are allowed.`,
diff --git a/packages/browser/src/node/utils.ts b/packages/browser/src/node/utils.ts
index f61ed87fdc6c..faba097364d9 100644
--- a/packages/browser/src/node/utils.ts
+++ b/packages/browser/src/node/utils.ts
@@ -2,11 +2,13 @@ import type {
BrowserProvider,
BrowserProviderOption,
ResolvedBrowserOptions,
+ ResolvedConfig,
TestProject,
} from 'vitest/node'
import { defaultKeyMap } from '@testing-library/user-event/dist/esm/keyboard/keyMap.js'
import { parseKeyDef as tlParse } from '@testing-library/user-event/dist/esm/keyboard/parseKeyDef.js'
+import { basename, dirname, relative, resolve } from 'pathe'
declare enum DOM_KEY_LOCATION {
STANDARD = 0,
@@ -41,6 +43,28 @@ export function replacer(code: string, values: Record): string {
return code.replace(/\{\s*(\w+)\s*\}/g, (_, key) => values[key] ?? _)
}
+export function resolveScreenshotPath(
+ testPath: string,
+ name: string,
+ config: ResolvedConfig,
+ customPath: string | undefined,
+): string {
+ if (customPath) {
+ return resolve(dirname(testPath), customPath)
+ }
+ const dir = dirname(testPath)
+ const base = basename(testPath)
+ if (config.browser.screenshotDirectory) {
+ return resolve(
+ config.browser.screenshotDirectory,
+ relative(config.root, dir),
+ base,
+ name,
+ )
+ }
+ return resolve(dir, '__screenshots__', base, name)
+}
+
export async function getBrowserProvider(
options: ResolvedBrowserOptions,
project: TestProject,
From a55dc59e7d3ef62b2af7da05763a0859896dc526 Mon Sep 17 00:00:00 2001
From: Vladimir Sheremet
Date: Mon, 29 Sep 2025 12:42:16 +0200
Subject: [PATCH 09/43] fix: remove old commands
---
packages/browser/src/node/commands/clear.ts | 23 --
packages/browser/src/node/commands/click.ts | 79 -------
.../browser/src/node/commands/dragAndDrop.ts | 42 ----
packages/browser/src/node/commands/fill.ts | 24 --
packages/browser/src/node/commands/fs.ts | 2 +-
packages/browser/src/node/commands/hover.ts | 21 --
packages/browser/src/node/commands/index.ts | 38 ----
.../browser/src/node/commands/keyboard.ts | 208 ------------------
.../browser/src/node/commands/screenshot.ts | 120 +---------
packages/browser/src/node/commands/select.ts | 51 -----
packages/browser/src/node/commands/tab.ts | 23 --
packages/browser/src/node/commands/trace.ts | 126 -----------
packages/browser/src/node/commands/type.ts | 62 ------
packages/browser/src/node/commands/upload.ts | 55 -----
.../browser/src/node/commands/viewport.ts | 14 --
15 files changed, 3 insertions(+), 885 deletions(-)
delete mode 100644 packages/browser/src/node/commands/clear.ts
delete mode 100644 packages/browser/src/node/commands/click.ts
delete mode 100644 packages/browser/src/node/commands/dragAndDrop.ts
delete mode 100644 packages/browser/src/node/commands/fill.ts
delete mode 100644 packages/browser/src/node/commands/hover.ts
delete mode 100644 packages/browser/src/node/commands/keyboard.ts
delete mode 100644 packages/browser/src/node/commands/select.ts
delete mode 100644 packages/browser/src/node/commands/tab.ts
delete mode 100644 packages/browser/src/node/commands/trace.ts
delete mode 100644 packages/browser/src/node/commands/type.ts
delete mode 100644 packages/browser/src/node/commands/upload.ts
delete mode 100644 packages/browser/src/node/commands/viewport.ts
diff --git a/packages/browser/src/node/commands/clear.ts b/packages/browser/src/node/commands/clear.ts
deleted file mode 100644
index 79f26644be07..000000000000
--- a/packages/browser/src/node/commands/clear.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-import type { UserEvent } from '../../../context'
-import type { UserEventCommand } from './utils'
-import { PlaywrightBrowserProvider } from '../providers/playwright'
-import { WebdriverBrowserProvider } from '../providers/webdriverio'
-
-export const clear: UserEventCommand = async (
- context,
- selector,
-) => {
- if (context.provider instanceof PlaywrightBrowserProvider) {
- const { iframe } = context
- const element = iframe.locator(selector)
- await element.clear()
- }
- else if (context.provider instanceof WebdriverBrowserProvider) {
- const browser = context.browser
- const element = await browser.$(selector)
- await element.clearValue()
- }
- else {
- throw new TypeError(`Provider "${context.provider.name}" does not support clearing elements`)
- }
-}
diff --git a/packages/browser/src/node/commands/click.ts b/packages/browser/src/node/commands/click.ts
deleted file mode 100644
index 31047a0138b6..000000000000
--- a/packages/browser/src/node/commands/click.ts
+++ /dev/null
@@ -1,79 +0,0 @@
-import type { UserEvent } from '../../../context'
-import type { UserEventCommand } from './utils'
-import { PlaywrightBrowserProvider } from '../providers/playwright'
-import { WebdriverBrowserProvider } from '../providers/webdriverio'
-
-export const click: UserEventCommand = async (
- context,
- selector,
- options = {},
-) => {
- const provider = context.provider
- if (provider instanceof PlaywrightBrowserProvider) {
- const tester = context.iframe
- await tester.locator(selector).click(options)
- }
- else if (provider instanceof WebdriverBrowserProvider) {
- const browser = context.browser
- await browser.$(selector).click(options as any)
- }
- else {
- throw new TypeError(`Provider "${provider.name}" doesn't support click command`)
- }
-}
-
-export const dblClick: UserEventCommand = async (
- context,
- selector,
- options = {},
-) => {
- const provider = context.provider
- if (provider instanceof PlaywrightBrowserProvider) {
- const tester = context.iframe
- await tester.locator(selector).dblclick(options)
- }
- else if (provider instanceof WebdriverBrowserProvider) {
- const browser = context.browser
- await browser.$(selector).doubleClick()
- }
- else {
- throw new TypeError(`Provider "${provider.name}" doesn't support dblClick command`)
- }
-}
-
-export const tripleClick: UserEventCommand = async (
- context,
- selector,
- options = {},
-) => {
- const provider = context.provider
- if (provider instanceof PlaywrightBrowserProvider) {
- const tester = context.iframe
- await tester.locator(selector).click({
- ...options,
- clickCount: 3,
- })
- }
- else if (provider instanceof WebdriverBrowserProvider) {
- const browser = context.browser
- await browser
- .action('pointer', { parameters: { pointerType: 'mouse' } })
- // move the pointer over the button
- .move({ origin: browser.$(selector) })
- // simulate 3 clicks
- .down()
- .up()
- .pause(50)
- .down()
- .up()
- .pause(50)
- .down()
- .up()
- .pause(50)
- // run the sequence
- .perform()
- }
- else {
- throw new TypeError(`Provider "${provider.name}" doesn't support tripleClick command`)
- }
-}
diff --git a/packages/browser/src/node/commands/dragAndDrop.ts b/packages/browser/src/node/commands/dragAndDrop.ts
deleted file mode 100644
index c665fd9ead10..000000000000
--- a/packages/browser/src/node/commands/dragAndDrop.ts
+++ /dev/null
@@ -1,42 +0,0 @@
-import type { UserEvent } from '../../../context'
-import type { UserEventCommand } from './utils'
-import { PlaywrightBrowserProvider } from '../providers/playwright'
-import { WebdriverBrowserProvider } from '../providers/webdriverio'
-
-export const dragAndDrop: UserEventCommand = async (
- context,
- source,
- target,
- options_,
-) => {
- if (context.provider instanceof PlaywrightBrowserProvider) {
- const frame = await context.frame()
- await frame.dragAndDrop(
- source,
- target,
- options_,
- )
- }
- else if (context.provider instanceof WebdriverBrowserProvider) {
- const $source = context.browser.$(source)
- const $target = context.browser.$(target)
- const options = (options_ || {}) as any
- const duration = options.duration ?? 10
-
- // https://github.com/webdriverio/webdriverio/issues/8022#issuecomment-1700919670
- await context.browser
- .action('pointer')
- .move({ duration: 0, origin: $source, x: options.sourceX ?? 0, y: options.sourceY ?? 0 })
- .down({ button: 0 })
- .move({ duration: 0, origin: 'pointer', x: 0, y: 0 })
- .pause(duration)
- .move({ duration: 0, origin: $target, x: options.targetX ?? 0, y: options.targetY ?? 0 })
- .move({ duration: 0, origin: 'pointer', x: 1, y: 0 })
- .move({ duration: 0, origin: 'pointer', x: -1, y: 0 })
- .up({ button: 0 })
- .perform()
- }
- else {
- throw new TypeError(`Provider "${context.provider.name}" does not support dragging elements`)
- }
-}
diff --git a/packages/browser/src/node/commands/fill.ts b/packages/browser/src/node/commands/fill.ts
deleted file mode 100644
index c3256cccb682..000000000000
--- a/packages/browser/src/node/commands/fill.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-import type { UserEvent } from '../../../context'
-import type { UserEventCommand } from './utils'
-import { PlaywrightBrowserProvider } from '../providers/playwright'
-import { WebdriverBrowserProvider } from '../providers/webdriverio'
-
-export const fill: UserEventCommand = async (
- context,
- selector,
- text,
- options = {},
-) => {
- if (context.provider instanceof PlaywrightBrowserProvider) {
- const { iframe } = context
- const element = iframe.locator(selector)
- await element.fill(text, options)
- }
- else if (context.provider instanceof WebdriverBrowserProvider) {
- const browser = context.browser
- await browser.$(selector).setValue(text)
- }
- else {
- throw new TypeError(`Provider "${context.provider.name}" does not support filling inputs`)
- }
-}
diff --git a/packages/browser/src/node/commands/fs.ts b/packages/browser/src/node/commands/fs.ts
index 0c9970d1cce4..d9558106023f 100644
--- a/packages/browser/src/node/commands/fs.ts
+++ b/packages/browser/src/node/commands/fs.ts
@@ -1,5 +1,5 @@
+import type { BrowserCommands } from 'vitest/browser'
import type { BrowserCommand, TestProject } from 'vitest/node'
-import type { BrowserCommands } from '../../../context'
import fs, { promises as fsp } from 'node:fs'
import { basename, dirname, resolve } from 'node:path'
import mime from 'mime/lite'
diff --git a/packages/browser/src/node/commands/hover.ts b/packages/browser/src/node/commands/hover.ts
deleted file mode 100644
index 7672eb839b75..000000000000
--- a/packages/browser/src/node/commands/hover.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-import type { UserEvent } from '../../../context'
-import type { UserEventCommand } from './utils'
-import { PlaywrightBrowserProvider } from '../providers/playwright'
-import { WebdriverBrowserProvider } from '../providers/webdriverio'
-
-export const hover: UserEventCommand = async (
- context,
- selector,
- options = {},
-) => {
- if (context.provider instanceof PlaywrightBrowserProvider) {
- await context.iframe.locator(selector).hover(options)
- }
- else if (context.provider instanceof WebdriverBrowserProvider) {
- const browser = context.browser
- await browser.$(selector).moveTo(options as any)
- }
- else {
- throw new TypeError(`Provider "${context.provider.name}" does not support hover`)
- }
-}
diff --git a/packages/browser/src/node/commands/index.ts b/packages/browser/src/node/commands/index.ts
index 4b4e02ba25ee..64e115b731a6 100644
--- a/packages/browser/src/node/commands/index.ts
+++ b/packages/browser/src/node/commands/index.ts
@@ -1,56 +1,18 @@
-import { clear } from './clear'
-import { click, dblClick, tripleClick } from './click'
-import { dragAndDrop } from './dragAndDrop'
-import { fill } from './fill'
import {
_fileInfo,
readFile,
removeFile,
writeFile,
} from './fs'
-import { hover } from './hover'
-import { keyboard, keyboardCleanup } from './keyboard'
import { screenshot } from './screenshot'
import { screenshotMatcher } from './screenshotMatcher'
-import { selectOptions } from './select'
-import { tab } from './tab'
-import {
- annotateTraces,
- deleteTracing,
- startChunkTrace,
- startTracing,
- stopChunkTrace,
-} from './trace'
-import { type } from './type'
-import { upload } from './upload'
-import { viewport } from './viewport'
export default {
readFile: readFile as typeof readFile,
removeFile: removeFile as typeof removeFile,
writeFile: writeFile as typeof writeFile,
-
// private commands
__vitest_fileInfo: _fileInfo as typeof _fileInfo,
- __vitest_upload: upload as typeof upload,
- __vitest_click: click as typeof click,
- __vitest_dblClick: dblClick as typeof dblClick,
- __vitest_tripleClick: tripleClick as typeof tripleClick,
__vitest_screenshot: screenshot as typeof screenshot,
- __vitest_type: type as typeof type,
- __vitest_clear: clear as typeof clear,
- __vitest_fill: fill as typeof fill,
- __vitest_tab: tab as typeof tab,
- __vitest_keyboard: keyboard as typeof keyboard,
- __vitest_selectOptions: selectOptions as typeof selectOptions,
- __vitest_dragAndDrop: dragAndDrop as typeof dragAndDrop,
- __vitest_hover: hover as typeof hover,
- __vitest_cleanup: keyboardCleanup as typeof keyboardCleanup,
- __vitest_viewport: viewport as typeof viewport,
__vitest_screenshotMatcher: screenshotMatcher as typeof screenshotMatcher,
- __vitest_deleteTracing: deleteTracing as typeof deleteTracing,
- __vitest_startChunkTrace: startChunkTrace as typeof startChunkTrace,
- __vitest_startTracing: startTracing as typeof startTracing,
- __vitest_stopChunkTrace: stopChunkTrace as typeof stopChunkTrace,
- __vitest_annotateTraces: annotateTraces as typeof annotateTraces,
}
diff --git a/packages/browser/src/node/commands/keyboard.ts b/packages/browser/src/node/commands/keyboard.ts
deleted file mode 100644
index 6024e55ab074..000000000000
--- a/packages/browser/src/node/commands/keyboard.ts
+++ /dev/null
@@ -1,208 +0,0 @@
-import type { BrowserProvider } from 'vitest/node'
-import type { UserEventCommand } from './utils'
-import { defaultKeyMap } from '@testing-library/user-event/dist/esm/keyboard/keyMap.js'
-import { parseKeyDef } from '@testing-library/user-event/dist/esm/keyboard/parseKeyDef.js'
-import { PlaywrightBrowserProvider } from '../providers/playwright'
-import { WebdriverBrowserProvider } from '../providers/webdriverio'
-
-export interface KeyboardState {
- unreleased: string[]
-}
-
-export const keyboard: UserEventCommand<(text: string, state: KeyboardState) => Promise<{ unreleased: string[] }>> = async (
- context,
- text,
- state,
-) => {
- if (context.provider instanceof PlaywrightBrowserProvider) {
- const frame = await context.frame()
- await frame.evaluate(focusIframe)
- }
- else if (context.provider instanceof WebdriverBrowserProvider) {
- await context.browser.execute(focusIframe)
- }
-
- const pressed = new Set(state.unreleased)
-
- await keyboardImplementation(
- pressed,
- context.provider,
- context.sessionId,
- text,
- async () => {
- if (context.provider instanceof PlaywrightBrowserProvider) {
- const frame = await context.frame()
- await frame.evaluate(selectAll)
- }
- else if (context.provider instanceof WebdriverBrowserProvider) {
- await context.browser.execute(selectAll)
- }
- else {
- throw new TypeError(`Provider "${context.provider.name}" does not support selecting all text`)
- }
- },
- true,
- )
-
- return {
- unreleased: Array.from(pressed),
- }
-}
-
-export const keyboardCleanup: UserEventCommand<(state: KeyboardState) => Promise> = async (
- context,
- state,
-) => {
- const { provider, sessionId } = context
- if (!state.unreleased) {
- return
- }
- if (provider instanceof PlaywrightBrowserProvider) {
- const page = provider.getPage(sessionId)
- for (const key of state.unreleased) {
- await page.keyboard.up(key)
- }
- }
- else if (provider instanceof WebdriverBrowserProvider) {
- const keyboard = provider.browser!.action('key')
- for (const key of state.unreleased) {
- keyboard.up(key)
- }
- await keyboard.perform()
- }
- else {
- throw new TypeError(`Provider "${context.provider.name}" does not support keyboard api`)
- }
-}
-
-// fallback to insertText for non US key
-// https://github.com/microsoft/playwright/blob/50775698ae13642742f2a1e8983d1d686d7f192d/packages/playwright-core/src/server/input.ts#L95
-const VALID_KEYS = new Set(['Escape', 'F1', 'F2', 'F3', 'F4', 'F5', 'F6', 'F7', 'F8', 'F9', 'F10', 'F11', 'F12', 'Backquote', '`', '~', 'Digit1', '1', '!', 'Digit2', '2', '@', 'Digit3', '3', '#', 'Digit4', '4', '$', 'Digit5', '5', '%', 'Digit6', '6', '^', 'Digit7', '7', '&', 'Digit8', '8', '*', 'Digit9', '9', '(', 'Digit0', '0', ')', 'Minus', '-', '_', 'Equal', '=', '+', 'Backslash', '\\', '|', 'Backspace', 'Tab', 'KeyQ', 'q', 'Q', 'KeyW', 'w', 'W', 'KeyE', 'e', 'E', 'KeyR', 'r', 'R', 'KeyT', 't', 'T', 'KeyY', 'y', 'Y', 'KeyU', 'u', 'U', 'KeyI', 'i', 'I', 'KeyO', 'o', 'O', 'KeyP', 'p', 'P', 'BracketLeft', '[', '{', 'BracketRight', ']', '}', 'CapsLock', 'KeyA', 'a', 'A', 'KeyS', 's', 'S', 'KeyD', 'd', 'D', 'KeyF', 'f', 'F', 'KeyG', 'g', 'G', 'KeyH', 'h', 'H', 'KeyJ', 'j', 'J', 'KeyK', 'k', 'K', 'KeyL', 'l', 'L', 'Semicolon', ';', ':', 'Quote', '\'', '"', 'Enter', '\n', '\r', 'ShiftLeft', 'Shift', 'KeyZ', 'z', 'Z', 'KeyX', 'x', 'X', 'KeyC', 'c', 'C', 'KeyV', 'v', 'V', 'KeyB', 'b', 'B', 'KeyN', 'n', 'N', 'KeyM', 'm', 'M', 'Comma', ',', '<', 'Period', '.', '>', 'Slash', '/', '?', 'ShiftRight', 'ControlLeft', 'Control', 'MetaLeft', 'Meta', 'AltLeft', 'Alt', 'Space', ' ', 'AltRight', 'AltGraph', 'MetaRight', 'ContextMenu', 'ControlRight', 'PrintScreen', 'ScrollLock', 'Pause', 'PageUp', 'PageDown', 'Insert', 'Delete', 'Home', 'End', 'ArrowLeft', 'ArrowUp', 'ArrowRight', 'ArrowDown', 'NumLock', 'NumpadDivide', 'NumpadMultiply', 'NumpadSubtract', 'Numpad7', 'Numpad8', 'Numpad9', 'Numpad4', 'Numpad5', 'Numpad6', 'NumpadAdd', 'Numpad1', 'Numpad2', 'Numpad3', 'Numpad0', 'NumpadDecimal', 'NumpadEnter', 'ControlOrMeta'])
-
-export async function keyboardImplementation(
- pressed: Set,
- provider: BrowserProvider,
- sessionId: string,
- text: string,
- selectAll: () => Promise,
- skipRelease: boolean,
-): Promise<{ pressed: Set }> {
- if (provider instanceof PlaywrightBrowserProvider) {
- const page = provider.getPage(sessionId)
- const actions = parseKeyDef(defaultKeyMap, text)
-
- for (const { releasePrevious, releaseSelf, repeat, keyDef } of actions) {
- const key = keyDef.key!
-
- // TODO: instead of calling down/up for each key, join non special
- // together, and call `type` once for all non special keys,
- // and then `press` for special keys
- if (pressed.has(key)) {
- if (VALID_KEYS.has(key)) {
- await page.keyboard.up(key)
- }
- pressed.delete(key)
- }
-
- if (!releasePrevious) {
- if (key === 'selectall') {
- await selectAll()
- continue
- }
-
- for (let i = 1; i <= repeat; i++) {
- if (VALID_KEYS.has(key)) {
- await page.keyboard.down(key)
- }
- else {
- await page.keyboard.insertText(key)
- }
- }
-
- if (releaseSelf) {
- if (VALID_KEYS.has(key)) {
- await page.keyboard.up(key)
- }
- }
- else {
- pressed.add(key)
- }
- }
- }
-
- if (!skipRelease && pressed.size) {
- for (const key of pressed) {
- if (VALID_KEYS.has(key)) {
- await page.keyboard.up(key)
- }
- }
- }
- }
- else if (provider instanceof WebdriverBrowserProvider) {
- const { Key } = await import('webdriverio')
- const browser = provider.browser!
- const actions = parseKeyDef(defaultKeyMap, text)
-
- let keyboard = browser.action('key')
-
- for (const { releasePrevious, releaseSelf, repeat, keyDef } of actions) {
- let key = keyDef.key!
- const special = Key[key as 'Shift']
-
- if (special) {
- key = special
- }
-
- if (pressed.has(key)) {
- keyboard.up(key)
- pressed.delete(key)
- }
-
- if (!releasePrevious) {
- if (key === 'selectall') {
- await keyboard.perform()
- keyboard = browser.action('key')
- await selectAll()
- continue
- }
-
- for (let i = 1; i <= repeat; i++) {
- keyboard.down(key)
- }
-
- if (releaseSelf) {
- keyboard.up(key)
- }
- else {
- pressed.add(key)
- }
- }
- }
-
- // seems like webdriverio doesn't release keys automatically if skipRelease is true and all events are keyUp
- const allRelease = keyboard.toJSON().actions.every(action => action.type === 'keyUp')
-
- await keyboard.perform(allRelease ? false : skipRelease)
- }
-
- return {
- pressed,
- }
-}
-
-function focusIframe() {
- if (
- !document.activeElement
- || document.activeElement.ownerDocument !== document
- || document.activeElement === document.body
- ) {
- window.focus()
- }
-}
-
-function selectAll() {
- const element = document.activeElement as HTMLInputElement
- if (element && typeof element.select === 'function') {
- element.select()
- }
-}
diff --git a/packages/browser/src/node/commands/screenshot.ts b/packages/browser/src/node/commands/screenshot.ts
index aaa279ab4859..76c349f105dc 100644
--- a/packages/browser/src/node/commands/screenshot.ts
+++ b/packages/browser/src/node/commands/screenshot.ts
@@ -1,11 +1,5 @@
-import type { BrowserCommand, BrowserCommandContext, ResolvedConfig } from 'vitest/node'
-import type { ScreenshotOptions } from '../../../context'
-import { mkdir, rm } from 'node:fs/promises'
-import { normalize as platformNormalize } from 'node:path'
-import { nanoid } from '@vitest/utils/helpers'
-import { basename, dirname, normalize, relative, resolve } from 'pathe'
-import { PlaywrightBrowserProvider } from '../providers/playwright'
-import { WebdriverBrowserProvider } from '../providers/webdriverio'
+import type { ScreenshotOptions } from 'vitest/browser'
+import type { BrowserCommand } from 'vitest/node'
interface ScreenshotCommandOptions extends Omit {
element?: string
@@ -40,116 +34,6 @@ export const screenshot: BrowserCommand<[string, ScreenshotCommandOptions]> = as
return returnResult(options, path, buffer)
}
-// TODO: remove this
-
-/**
- * Takes a screenshot using the provided browser context and returns a buffer and the expected screenshot path.
- *
- * **Note**: the returned `path` indicates where the screenshot *might* be found.
- * It is not guaranteed to exist, especially if `options.save` is `false`.
- *
- * @throws {Error} If the function is not called within a test or if the browser provider does not support screenshots.
- */
-export async function takeScreenshot(
- context: BrowserCommandContext,
- name: string,
- options: Omit,
-): Promise<{ buffer: Buffer; path: string }> {
- if (!context.testPath) {
- throw new Error(`Cannot take a screenshot without a test path`)
- }
-
- const path = resolveScreenshotPath(
- context.testPath,
- name,
- context.project.config,
- options.path,
- )
-
- // playwright does not need a screenshot path if we don't intend to save it
- let savePath: string | undefined
-
- if (options.save) {
- savePath = normalize(path)
-
- await mkdir(dirname(savePath), { recursive: true })
- }
-
- if (context.provider instanceof PlaywrightBrowserProvider) {
- const mask = options.mask?.map(selector => context.iframe.locator(selector))
-
- if (options.element) {
- const { element: selector, ...config } = options
- const element = context.iframe.locator(selector)
- const buffer = await element.screenshot({
- ...config,
- mask,
- path: savePath,
- })
- return { buffer, path }
- }
-
- const buffer = await context.iframe.locator('body').screenshot({
- ...options,
- mask,
- path: savePath,
- })
- return { buffer, path }
- }
-
- if (context.provider instanceof WebdriverBrowserProvider) {
- // webdriverio needs a path, so if one is not already set we create a temporary one
- if (savePath === undefined) {
- savePath = resolve(context.project.tmpDir, nanoid())
-
- await mkdir(context.project.tmpDir, { recursive: true })
- }
-
- const page = context.provider.browser!
- const element = !options.element
- ? await page.$('body')
- : await page.$(`${options.element}`)
-
- // webdriverio expects the path to contain the extension and only works with PNG files
- const savePathWithExtension = savePath.endsWith('.png') ? savePath : `${savePath}.png`
-
- // there seems to be a bug in webdriverio, `X:/` gets appended to cwd, so we convert to `X:\`
- const buffer = await element.saveScreenshot(
- platformNormalize(savePathWithExtension),
- )
- if (!options.save) {
- await rm(savePathWithExtension, { force: true })
- }
- return { buffer, path }
- }
-
- throw new Error(
- `Provider "${context.provider.name}" does not support screenshots`,
- )
-}
-
-function resolveScreenshotPath(
- testPath: string,
- name: string,
- config: ResolvedConfig,
- customPath: string | undefined,
-): string {
- if (customPath) {
- return resolve(dirname(testPath), customPath)
- }
- const dir = dirname(testPath)
- const base = basename(testPath)
- if (config.browser.screenshotDirectory) {
- return resolve(
- config.browser.screenshotDirectory,
- relative(config.root, dir),
- base,
- name,
- )
- }
- return resolve(dir, '__screenshots__', base, name)
-}
-
function returnResult(
options: ScreenshotCommandOptions,
path: string,
diff --git a/packages/browser/src/node/commands/select.ts b/packages/browser/src/node/commands/select.ts
deleted file mode 100644
index b2910a3176bb..000000000000
--- a/packages/browser/src/node/commands/select.ts
+++ /dev/null
@@ -1,51 +0,0 @@
-import type { ElementHandle } from 'playwright'
-import type { UserEvent } from '../../../context'
-import type { UserEventCommand } from './utils'
-import { PlaywrightBrowserProvider } from '../providers/playwright'
-import { WebdriverBrowserProvider } from '../providers/webdriverio'
-
-export const selectOptions: UserEventCommand = async (
- context,
- selector,
- userValues,
- options = {},
-) => {
- if (context.provider instanceof PlaywrightBrowserProvider) {
- const value = userValues as any as (string | { element: string })[]
- const { iframe } = context
- const selectElement = iframe.locator(selector)
-
- const values = await Promise.all(value.map(async (v) => {
- if (typeof v === 'string') {
- return v
- }
- const elementHandler = await iframe.locator(v.element).elementHandle()
- if (!elementHandler) {
- throw new Error(`Element not found: ${v.element}`)
- }
- return elementHandler
- })) as (readonly string[]) | (readonly ElementHandle[])
-
- await selectElement.selectOption(values, options)
- }
- else if (context.provider instanceof WebdriverBrowserProvider) {
- const values = userValues as any as [({ index: number })]
-
- if (!values.length) {
- return
- }
-
- const browser = context.browser
-
- if (values.length === 1 && 'index' in values[0]) {
- const selectElement = browser.$(selector)
- await selectElement.selectByIndex(values[0].index)
- }
- else {
- throw new Error('Provider "webdriverio" doesn\'t support selecting multiple values at once')
- }
- }
- else {
- throw new TypeError(`Provider "${context.provider.name}" doesn't support selectOptions command`)
- }
-}
diff --git a/packages/browser/src/node/commands/tab.ts b/packages/browser/src/node/commands/tab.ts
deleted file mode 100644
index 3739841b9aca..000000000000
--- a/packages/browser/src/node/commands/tab.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-import type { UserEvent } from '../../../context'
-import type { UserEventCommand } from './utils'
-import { PlaywrightBrowserProvider } from '../providers/playwright'
-import { WebdriverBrowserProvider } from '../providers/webdriverio'
-
-export const tab: UserEventCommand = async (
- context,
- options = {},
-) => {
- const provider = context.provider
- if (provider instanceof PlaywrightBrowserProvider) {
- const page = context.page
- await page.keyboard.press(options.shift === true ? 'Shift+Tab' : 'Tab')
- return
- }
- if (provider instanceof WebdriverBrowserProvider) {
- const { Key } = await import('webdriverio')
- const browser = context.browser
- await browser.keys(options.shift === true ? [Key.Shift, Key.Tab] : [Key.Tab])
- return
- }
- throw new Error(`Provider "${provider.name}" doesn't support tab command`)
-}
diff --git a/packages/browser/src/node/commands/trace.ts b/packages/browser/src/node/commands/trace.ts
deleted file mode 100644
index 0db3a32adcc6..000000000000
--- a/packages/browser/src/node/commands/trace.ts
+++ /dev/null
@@ -1,126 +0,0 @@
-import type { BrowserCommandContext } from 'vitest/node'
-import type { BrowserCommand } from '../plugin'
-import { unlink } from 'node:fs/promises'
-import { basename, dirname, relative, resolve } from 'pathe'
-import { PlaywrightBrowserProvider } from '../providers/playwright'
-
-export const startTracing: BrowserCommand<[]> = async ({ context, project, provider, sessionId }) => {
- if (provider instanceof PlaywrightBrowserProvider) {
- if (provider.tracingContexts.has(sessionId)) {
- return
- }
-
- provider.tracingContexts.add(sessionId)
- const options = project.config.browser!.trace
- await context.tracing.start({
- screenshots: options.screenshots ?? true,
- snapshots: options.snapshots ?? true,
- // currently, PW shows sources in private methods
- sources: false,
- }).catch(() => {
- provider.tracingContexts.delete(sessionId)
- })
- return
- }
- throw new TypeError(`The ${provider.name} provider does not support tracing.`)
-}
-
-export const startChunkTrace: BrowserCommand<[{ name: string; title: string }]> = async (
- command,
- { name, title },
-) => {
- const { provider, sessionId, testPath, context } = command
- if (!testPath) {
- throw new Error(`stopChunkTrace cannot be called outside of the test file.`)
- }
- if (provider instanceof PlaywrightBrowserProvider) {
- if (!provider.tracingContexts.has(sessionId)) {
- await startTracing(command)
- }
- const path = resolveTracesPath(command, name)
- provider.pendingTraces.set(path, sessionId)
- await context.tracing.startChunk({ name, title })
- return
- }
- throw new TypeError(`The ${provider.name} provider does not support tracing.`)
-}
-
-export const stopChunkTrace: BrowserCommand<[{ name: string }]> = async (
- context,
- { name },
-) => {
- if (context.provider instanceof PlaywrightBrowserProvider) {
- const path = resolveTracesPath(context, name)
- context.provider.pendingTraces.delete(path)
- await context.context.tracing.stopChunk({ path })
- return { tracePath: path }
- }
- throw new TypeError(`The ${context.provider.name} provider does not support tracing.`)
-}
-
-function resolveTracesPath({ testPath, project }: BrowserCommandContext, name: string) {
- if (!testPath) {
- throw new Error(`This command can only be called inside a test file.`)
- }
- const options = project.config.browser!.trace
- const sanitizedName = `${project.name.replace(/[^a-z0-9]/gi, '-')}-${name}.trace.zip`
- if (options.tracesDir) {
- return resolve(options.tracesDir, sanitizedName)
- }
- const dir = dirname(testPath)
- const base = basename(testPath)
- return resolve(
- dir,
- '__traces__',
- base,
- `${project.name.replace(/[^a-z0-9]/gi, '-')}-${name}.trace.zip`,
- )
-}
-
-export const deleteTracing: BrowserCommand<[{ traces: string[] }]> = async (
- context,
- { traces },
-) => {
- if (!context.testPath) {
- throw new Error(`stopChunkTrace cannot be called outside of the test file.`)
- }
- if (context.provider instanceof PlaywrightBrowserProvider) {
- return Promise.all(
- traces.map(trace => unlink(trace).catch((err) => {
- if (err.code === 'ENOENT') {
- // Ignore the error if the file doesn't exist
- return
- }
- // Re-throw other errors
- throw err
- })),
- )
- }
-
- throw new Error(`provider ${context.provider.name} is not supported`)
-}
-
-export const annotateTraces: BrowserCommand<[{ traces: string[]; testId: string }]> = async (
- { project },
- { testId, traces },
-) => {
- const vitest = project.vitest
- await Promise.all(traces.map((trace) => {
- const entity = vitest.state.getReportedEntityById(testId)
- return vitest._testRun.annotate(testId, {
- message: relative(project.config.root, trace),
- type: 'traces',
- attachment: {
- path: trace,
- contentType: 'application/octet-stream',
- },
- location: entity?.location
- ? {
- file: entity.module.moduleId,
- line: entity.location.line,
- column: entity.location.column,
- }
- : undefined,
- })
- }))
-}
diff --git a/packages/browser/src/node/commands/type.ts b/packages/browser/src/node/commands/type.ts
deleted file mode 100644
index 189e7343b280..000000000000
--- a/packages/browser/src/node/commands/type.ts
+++ /dev/null
@@ -1,62 +0,0 @@
-import type { UserEvent } from '../../../context'
-import type { UserEventCommand } from './utils'
-import { PlaywrightBrowserProvider } from '../providers/playwright'
-import { WebdriverBrowserProvider } from '../providers/webdriverio'
-import { keyboardImplementation } from './keyboard'
-
-export const type: UserEventCommand = async (
- context,
- selector,
- text,
- options = {},
-) => {
- const { skipClick = false, skipAutoClose = false } = options
- const unreleased = new Set(Reflect.get(options, 'unreleased') as string[] ?? [])
-
- if (context.provider instanceof PlaywrightBrowserProvider) {
- const { iframe } = context
- const element = iframe.locator(selector)
-
- if (!skipClick) {
- await element.focus()
- }
-
- await keyboardImplementation(
- unreleased,
- context.provider,
- context.sessionId,
- text,
- () => element.selectText(),
- skipAutoClose,
- )
- }
- else if (context.provider instanceof WebdriverBrowserProvider) {
- const browser = context.browser
- const element = browser.$(selector)
-
- if (!skipClick && !await element.isFocused()) {
- await element.click()
- }
-
- await keyboardImplementation(
- unreleased,
- context.provider,
- context.sessionId,
- text,
- () => browser.execute(() => {
- const element = document.activeElement as HTMLInputElement
- if (element && typeof element.select === 'function') {
- element.select()
- }
- }),
- skipAutoClose,
- )
- }
- else {
- throw new TypeError(`Provider "${context.provider.name}" does not support typing`)
- }
-
- return {
- unreleased: Array.from(unreleased),
- }
-}
diff --git a/packages/browser/src/node/commands/upload.ts b/packages/browser/src/node/commands/upload.ts
deleted file mode 100644
index 0893fa37a26e..000000000000
--- a/packages/browser/src/node/commands/upload.ts
+++ /dev/null
@@ -1,55 +0,0 @@
-import type { UserEventUploadOptions } from '@vitest/browser/context'
-import type { UserEventCommand } from './utils'
-import { resolve } from 'pathe'
-import { PlaywrightBrowserProvider } from '../providers/playwright'
-import { WebdriverBrowserProvider } from '../providers/webdriverio'
-
-export const upload: UserEventCommand<(element: string, files: Array, options: UserEventUploadOptions) => void> = async (
- context,
- selector,
- files,
- options,
-) => {
- const testPath = context.testPath
- if (!testPath) {
- throw new Error(`Cannot upload files outside of a test`)
- }
- const root = context.project.config.root
-
- if (context.provider instanceof PlaywrightBrowserProvider) {
- const { iframe } = context
- const playwrightFiles = files.map((file) => {
- if (typeof file === 'string') {
- return resolve(root, file)
- }
- return {
- name: file.name,
- mimeType: file.mimeType,
- buffer: Buffer.from(file.base64, 'base64'),
- }
- })
- await iframe.locator(selector).setInputFiles(playwrightFiles as string[], options)
- }
- else if (context.provider instanceof WebdriverBrowserProvider) {
- for (const file of files) {
- if (typeof file !== 'string') {
- throw new TypeError(`The "${context.provider.name}" provider doesn't support uploading files objects. Provide a file path instead.`)
- }
- }
-
- const element = context.browser.$(selector)
-
- for (const file of files) {
- const filepath = resolve(root, file as string)
- const remoteFilePath = await context.browser.uploadFile(filepath)
- await element.addValue(remoteFilePath)
- }
- }
- else {
- throw new TypeError(`Provider "${context.provider.name}" does not support uploading files via userEvent.upload`)
- }
-}
diff --git a/packages/browser/src/node/commands/viewport.ts b/packages/browser/src/node/commands/viewport.ts
deleted file mode 100644
index a117275d3fde..000000000000
--- a/packages/browser/src/node/commands/viewport.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import type { UserEventCommand } from './utils'
-import { WebdriverBrowserProvider } from '../providers/webdriverio'
-
-export const viewport: UserEventCommand<(options: {
- width: number
- height: number
-}) => void> = async (context, options) => {
- if (context.provider instanceof WebdriverBrowserProvider) {
- await context.provider.setViewport(options)
- }
- else {
- throw new TypeError(`Provider ${context.provider.name} doesn't support "viewport" command`)
- }
-}
From 4b02284949139421052a72e255377773ebf461d8 Mon Sep 17 00:00:00 2001
From: Vladimir Sheremet
Date: Mon, 29 Sep 2025 12:51:36 +0200
Subject: [PATCH 10/43] fix: make commands local
---
.../node/commands/screenshotMatcher/utils.ts | 7 +++----
packages/browser/src/node/project.ts | 20 +++++++++++++++++--
packages/browser/src/node/rpc.ts | 16 +++++++++------
packages/vitest/src/node/types/browser.ts | 5 +++++
4 files changed, 36 insertions(+), 12 deletions(-)
diff --git a/packages/browser/src/node/commands/screenshotMatcher/utils.ts b/packages/browser/src/node/commands/screenshotMatcher/utils.ts
index ae2bf7489ca8..37da8e08ff88 100644
--- a/packages/browser/src/node/commands/screenshotMatcher/utils.ts
+++ b/packages/browser/src/node/commands/screenshotMatcher/utils.ts
@@ -1,11 +1,10 @@
+import type { ScreenshotMatcherOptions } from 'vitest/browser'
import type { BrowserCommandContext, BrowserConfigOptions } from 'vitest/node'
-import type { ScreenshotMatcherOptions } from '../../../../context'
import type { ScreenshotMatcherArguments } from '../../../shared/screenshotMatcher/types'
import type { AnyCodec } from './codecs'
import { platform } from 'node:os'
import { deepMerge } from '@vitest/utils/helpers'
import { basename, dirname, extname, join, relative, resolve } from 'pathe'
-import { takeScreenshot } from '../screenshot'
import { getCodec } from './codecs'
import { getComparator } from './comparators'
@@ -238,8 +237,8 @@ export function takeDecodedScreenshot({
name: string
screenshotOptions: ScreenshotMatcherArguments[2]['screenshotOptions']
}): ReturnType {
- return takeScreenshot(
- context,
+ return context.triggerCommand(
+ '__vitest_takeScreenshot',
name,
{ ...screenshotOptions, save: false, element },
).then(
diff --git a/packages/browser/src/node/project.ts b/packages/browser/src/node/project.ts
index 414ea765dbf0..2362a1560b2e 100644
--- a/packages/browser/src/node/project.ts
+++ b/packages/browser/src/node/project.ts
@@ -4,6 +4,7 @@ import type { ParsedStack, SerializedConfig, TestError } from 'vitest'
import type { BrowserCommands } from 'vitest/browser'
import type {
BrowserCommand,
+ BrowserCommandContext,
BrowserProvider,
ProjectBrowser as IProjectBrowser,
ResolvedConfig,
@@ -59,6 +60,8 @@ export class ProjectBrowser implements IProjectBrowser {
return this.parent.vite
}
+ private commands = {} as Record>
+
public registerCommand(
name: K,
cb: BrowserCommand<
@@ -66,13 +69,26 @@ export class ProjectBrowser implements IProjectBrowser {
ReturnType
>,
): void {
- // TODO: register only for a specific project! don't override the global one because it's possible to have different providers in different projects
if (!/^[a-z_$][\w$]*$/i.test(name)) {
throw new Error(
`Invalid command name "${name}". Only alphanumeric characters, $ and _ are allowed.`,
)
}
- this.parent.commands[name] = cb
+ this.commands[name] = cb
+ }
+
+ public triggerCommand(
+ name: K,
+ context: BrowserCommandContext,
+ ...args: Parameters
+ ): ReturnType {
+ if (name in this.commands) {
+ return this.commands[name](context, ...args)
+ }
+ if (name in this.parent.commands) {
+ return this.parent.commands[name](context, ...args)
+ }
+ throw new Error(`Provider ${this.provider.name} does not support command "${name}".`)
}
wrapSerializedConfig(): SerializedConfig {
diff --git a/packages/browser/src/node/rpc.ts b/packages/browser/src/node/rpc.ts
index f008c9030d19..e8bea332d1ee 100644
--- a/packages/browser/src/node/rpc.ts
+++ b/packages/browser/src/node/rpc.ts
@@ -240,10 +240,6 @@ export function setupBrowserRpc(globalServer: ParentBrowserProject, defaultMocke
if (!provider) {
throw new Error('Commands are only available for browser tests.')
}
- const commands = globalServer.commands
- if (!commands || !commands[command]) {
- throw new Error(`Provider ${provider.name} does not support command "${command}".`)
- }
const context = Object.assign(
{
testPath,
@@ -252,12 +248,20 @@ export function setupBrowserRpc(globalServer: ParentBrowserProject, defaultMocke
contextId: sessionId,
sessionId,
triggerCommand: (name: string, ...args: any[]) => {
- return commands[name](context, ...args)
+ return project.browser!.triggerCommand(
+ name as any,
+ context,
+ ...args,
+ )
},
},
provider.getCommandsContext(sessionId),
) as any as BrowserCommandContext
- return await commands[command](context, ...payload)
+ return await project.browser!.triggerCommand(
+ command as any,
+ context,
+ ...payload,
+ )
},
resolveMock(rawId, importer, options) {
return mockResolver.resolveMock(rawId, importer, options)
diff --git a/packages/vitest/src/node/types/browser.ts b/packages/vitest/src/node/types/browser.ts
index ccb4de6df3ac..8e23f2fa5dd4 100644
--- a/packages/vitest/src/node/types/browser.ts
+++ b/packages/vitest/src/node/types/browser.ts
@@ -320,6 +320,11 @@ export interface ProjectBrowser {
ReturnType
>
) => void
+ triggerCommand: (
+ name: K,
+ context: BrowserCommandContext,
+ ...args: Parameters
+ ) => ReturnType
}
export interface BrowserCommand {
From 37475ae5f1f422c61fd13f7e849e9397eef07b32 Mon Sep 17 00:00:00 2001
From: Vladimir Sheremet
Date: Mon, 29 Sep 2025 15:35:58 +0200
Subject: [PATCH 11/43] feat: move locators
---
packages/browser-playwright/rollup.config.js | 8 +--
packages/browser-playwright/src/constants.ts | 5 ++
.../src/locators.ts} | 13 +++--
packages/browser-playwright/src/playwright.ts | 6 +++
packages/browser-preview/README.md | 49 +++++++++++++++++++
packages/browser-preview/rollup.config.js | 10 ++--
packages/browser-preview/src/constants.ts | 5 ++
.../src/locators.ts} | 12 +++--
packages/browser-preview/src/preview.ts | 6 +++
packages/browser-webdriverio/rollup.config.js | 10 ++--
.../src/commands/keyboard.ts | 1 -
packages/browser-webdriverio/src/constants.ts | 5 ++
.../src/locators.ts} | 22 +++++----
.../browser-webdriverio/src/webdriverio.ts | 6 +++
packages/browser/package.json | 30 +++---------
packages/browser/rollup.config.js | 20 +-------
packages/browser/src/client/tester/context.ts | 10 ++--
.../src/client/tester/expect-element.ts | 2 +-
.../client/tester/expect/toMatchScreenshot.ts | 2 +-
.../src/client/tester/locators/index.ts | 16 ++++--
packages/browser/src/client/tester/runner.ts | 2 +-
.../tester/{utils.ts => tester-utils.ts} | 10 +++-
packages/browser/src/client/tester/tester.ts | 2 +-
packages/browser/src/client/utils.ts | 2 +-
packages/browser/src/node/plugin.ts | 20 ++++----
.../browser/src/node/plugins/pluginContext.ts | 1 +
packages/browser/src/node/project.ts | 16 ++++--
packages/browser/src/node/projectParent.ts | 7 +--
packages/browser/src/node/rpc.ts | 7 ++-
packages/browser/src/node/utils.ts | 18 ++-----
.../src/node/projects/resolveProjects.ts | 26 +++-------
packages/vitest/src/node/types/browser.ts | 16 +++++-
pnpm-lock.yaml | 21 ++------
33 files changed, 215 insertions(+), 171 deletions(-)
create mode 100644 packages/browser-playwright/src/constants.ts
rename packages/{browser/src/client/tester/locators/playwright.ts => browser-playwright/src/locators.ts} (95%)
create mode 100644 packages/browser-preview/README.md
create mode 100644 packages/browser-preview/src/constants.ts
rename packages/{browser/src/client/tester/locators/preview.ts => browser-preview/src/locators.ts} (91%)
create mode 100644 packages/browser-webdriverio/src/constants.ts
rename packages/{browser/src/client/tester/locators/webdriverio.ts => browser-webdriverio/src/locators.ts} (92%)
rename packages/browser/src/client/tester/{utils.ts => tester-utils.ts} (96%)
diff --git a/packages/browser-playwright/rollup.config.js b/packages/browser-playwright/rollup.config.js
index 4a5ab941916b..aa8f975247c3 100644
--- a/packages/browser-playwright/rollup.config.js
+++ b/packages/browser-playwright/rollup.config.js
@@ -13,9 +13,6 @@ const external = [
...Object.keys(pkg.dependencies),
...Object.keys(pkg.peerDependencies || {}),
/^@?vitest(\/|$)/,
- '@vitest/browser/utils',
- 'worker_threads',
- 'node:worker_threads',
'vite',
'playwright-core/types/protocol',
]
@@ -36,7 +33,10 @@ const plugins = [
export default () =>
defineConfig([
{
- input: './src/index.ts',
+ input: {
+ index: './src/index.ts',
+ locators: './src/locators.ts',
+ },
output: {
dir: 'dist',
format: 'esm',
diff --git a/packages/browser-playwright/src/constants.ts b/packages/browser-playwright/src/constants.ts
new file mode 100644
index 000000000000..7229579f4e97
--- /dev/null
+++ b/packages/browser-playwright/src/constants.ts
@@ -0,0 +1,5 @@
+import { fileURLToPath } from 'node:url'
+import { resolve } from 'pathe'
+
+const pkgRoot = resolve(fileURLToPath(import.meta.url), '../..')
+export const distRoot: string = resolve(pkgRoot, 'dist')
diff --git a/packages/browser/src/client/tester/locators/playwright.ts b/packages/browser-playwright/src/locators.ts
similarity index 95%
rename from packages/browser/src/client/tester/locators/playwright.ts
rename to packages/browser-playwright/src/locators.ts
index 631bb0861f42..eee066ae985a 100644
--- a/packages/browser/src/client/tester/locators/playwright.ts
+++ b/packages/browser-playwright/src/locators.ts
@@ -6,8 +6,7 @@ import type {
UserEventHoverOptions,
UserEventSelectOptions,
UserEventUploadOptions,
-} from '@vitest/browser/context'
-import { page, server } from '@vitest/browser/context'
+} from 'vitest/browser'
import {
getByAltTextSelector,
getByLabelSelector,
@@ -16,9 +15,12 @@ import {
getByTestIdSelector,
getByTextSelector,
getByTitleSelector,
-} from 'ivya'
-import { getIframeScale, processTimeoutOptions } from '../utils'
-import { Locator, selectorEngine } from './index'
+ getIframeScale,
+ Locator,
+ processTimeoutOptions,
+ selectorEngine,
+} from '@vitest/browser/locators'
+import { page, server } from 'vitest/browser'
page.extend({
getByLabelText(text, options) {
@@ -43,6 +45,7 @@ page.extend({
return new PlaywrightLocator(getByTitleSelector(title, options))
},
+ // @ts-expect-error _createLocator is private
_createLocator(selector: string) {
return new PlaywrightLocator(selector)
},
diff --git a/packages/browser-playwright/src/playwright.ts b/packages/browser-playwright/src/playwright.ts
index 4529ac3ba2f9..d5100bd26344 100644
--- a/packages/browser-playwright/src/playwright.ts
+++ b/packages/browser-playwright/src/playwright.ts
@@ -28,9 +28,11 @@ import type {
} from 'vitest/node'
import { createBrowserServer } from '@vitest/browser'
import { createManualModuleSource } from '@vitest/mocker/node'
+import { resolve } from 'pathe'
import c from 'tinyrainbow'
import { createDebugger, isCSSRequest } from 'vitest/node'
import commands from './commands'
+import { distRoot } from './constants'
const debug = createDebugger('vitest:browser:playwright')
@@ -99,6 +101,10 @@ export class PlaywrightBrowserProvider implements BrowserProvider {
public tracingContexts: Set = new Set()
public pendingTraces: Map = new Map()
+ public initScripts: string[] = [
+ resolve(distRoot, 'locators.js'),
+ ]
+
constructor(
private project: TestProject,
private options: PlaywrightProviderOptions,
diff --git a/packages/browser-preview/README.md b/packages/browser-preview/README.md
new file mode 100644
index 000000000000..c1e0104fcf10
--- /dev/null
+++ b/packages/browser-preview/README.md
@@ -0,0 +1,49 @@
+# @vitest/browser-preview
+
+[](https://www.npmjs.com/package/@vitest/browser-preview)
+
+See how your tests look like in a real browser. For proper and stable browser testing, we recommend running tests in a headless browser in your CI instead. For this, you should use either:
+
+- [@vitest/browser-playwright](https://www.npmjs.com/package/@vitest/browser-playwright) - run tests using [playwright](https://playwright.dev/)
+- [@vitest/browser-webdriverio](https://www.npmjs.com/package/@vitest/browser-webdriverio) - run tests using [webdriverio](https://webdriver.io/)
+
+## Installation
+
+Install the package with your favorite package manager:
+
+```sh
+npm install -D @vitest/browser-preview
+# or
+yarn add -D @vitest/browser-preview
+# or
+pnpm add -D @vitest/browser-preview
+```
+
+Then specify it in the `browser.provider` field of your Vitest configuration:
+
+```ts
+// vitest.config.ts
+import { defineConfig } from 'vitest/config'
+import { preview } from '@vitest/browser-preview'
+
+export default defineConfig({
+ test: {
+ browser: {
+ provider: preview(),
+ instances: [
+ { name: 'chromium' },
+ ],
+ },
+ },
+})
+```
+
+Then run Vitest in the browser mode:
+
+```sh
+npx vitest --browser
+```
+
+If browser didn't open automatically, follow the link in the terminal to open the browser preview.
+
+[GitHub](https://github.com/vitest-dev/vitest/tree/main/packages/browser-preview) | [Documentation](https://vitest.dev/guide/browser/)
diff --git a/packages/browser-preview/rollup.config.js b/packages/browser-preview/rollup.config.js
index 4a5ab941916b..2e6d3bcf85a3 100644
--- a/packages/browser-preview/rollup.config.js
+++ b/packages/browser-preview/rollup.config.js
@@ -13,11 +13,6 @@ const external = [
...Object.keys(pkg.dependencies),
...Object.keys(pkg.peerDependencies || {}),
/^@?vitest(\/|$)/,
- '@vitest/browser/utils',
- 'worker_threads',
- 'node:worker_threads',
- 'vite',
- 'playwright-core/types/protocol',
]
const dtsUtils = createDtsUtils()
@@ -36,7 +31,10 @@ const plugins = [
export default () =>
defineConfig([
{
- input: './src/index.ts',
+ input: {
+ index: './src/index.ts',
+ locators: './src/locators.ts',
+ },
output: {
dir: 'dist',
format: 'esm',
diff --git a/packages/browser-preview/src/constants.ts b/packages/browser-preview/src/constants.ts
new file mode 100644
index 000000000000..7229579f4e97
--- /dev/null
+++ b/packages/browser-preview/src/constants.ts
@@ -0,0 +1,5 @@
+import { fileURLToPath } from 'node:url'
+import { resolve } from 'pathe'
+
+const pkgRoot = resolve(fileURLToPath(import.meta.url), '../..')
+export const distRoot: string = resolve(pkgRoot, 'dist')
diff --git a/packages/browser/src/client/tester/locators/preview.ts b/packages/browser-preview/src/locators.ts
similarity index 91%
rename from packages/browser/src/client/tester/locators/preview.ts
rename to packages/browser-preview/src/locators.ts
index 63a41179659f..12b5fd032f8d 100644
--- a/packages/browser/src/client/tester/locators/preview.ts
+++ b/packages/browser-preview/src/locators.ts
@@ -1,5 +1,5 @@
-import { page, server, userEvent } from '@vitest/browser/context'
import {
+ convertElementToCssSelector,
getByAltTextSelector,
getByLabelSelector,
getByPlaceholderSelector,
@@ -7,10 +7,11 @@ import {
getByTestIdSelector,
getByTextSelector,
getByTitleSelector,
-} from 'ivya'
-import { getElementError } from '../public-utils'
-import { convertElementToCssSelector } from '../utils'
-import { Locator, selectorEngine } from './index'
+ Locator,
+ selectorEngine,
+} from '@vitest/browser/locators'
+import { getElementError } from '@vitest/browser/utils'
+import { page, server, userEvent } from 'vitest/browser'
page.extend({
getByLabelText(text, options) {
@@ -35,6 +36,7 @@ page.extend({
return new PreviewLocator(getByTitleSelector(title, options))
},
+ // @ts-expect-error _createLocator is private
_createLocator(selector: string) {
return new PreviewLocator(selector)
},
diff --git a/packages/browser-preview/src/preview.ts b/packages/browser-preview/src/preview.ts
index 5956f4906240..c21989f99af0 100644
--- a/packages/browser-preview/src/preview.ts
+++ b/packages/browser-preview/src/preview.ts
@@ -1,5 +1,7 @@
import type { BrowserProvider, BrowserProviderOption, TestProject } from 'vitest/node'
import { createBrowserServer } from '@vitest/browser'
+import { resolve } from 'pathe'
+import { distRoot } from './constants'
export function preview(): BrowserProviderOption {
return {
@@ -18,6 +20,10 @@ export class PreviewBrowserProvider implements BrowserProvider {
private project!: TestProject
private open = false
+ public initScripts: string[] = [
+ resolve(distRoot, 'locators.js'),
+ ]
+
constructor(project: TestProject) {
this.project = project
this.open = false
diff --git a/packages/browser-webdriverio/rollup.config.js b/packages/browser-webdriverio/rollup.config.js
index 4a5ab941916b..2e6d3bcf85a3 100644
--- a/packages/browser-webdriverio/rollup.config.js
+++ b/packages/browser-webdriverio/rollup.config.js
@@ -13,11 +13,6 @@ const external = [
...Object.keys(pkg.dependencies),
...Object.keys(pkg.peerDependencies || {}),
/^@?vitest(\/|$)/,
- '@vitest/browser/utils',
- 'worker_threads',
- 'node:worker_threads',
- 'vite',
- 'playwright-core/types/protocol',
]
const dtsUtils = createDtsUtils()
@@ -36,7 +31,10 @@ const plugins = [
export default () =>
defineConfig([
{
- input: './src/index.ts',
+ input: {
+ index: './src/index.ts',
+ locators: './src/locators.ts',
+ },
output: {
dir: 'dist',
format: 'esm',
diff --git a/packages/browser-webdriverio/src/commands/keyboard.ts b/packages/browser-webdriverio/src/commands/keyboard.ts
index 1af975c925b5..d5b3b5a6a177 100644
--- a/packages/browser-webdriverio/src/commands/keyboard.ts
+++ b/packages/browser-webdriverio/src/commands/keyboard.ts
@@ -1,7 +1,6 @@
import type { BrowserProvider } from 'vitest/node'
import type { WebdriverBrowserProvider } from '../webdriverio'
import type { UserEventCommand } from './utils'
-
import { parseKeyDef } from '@vitest/browser'
import { Key } from 'webdriverio'
diff --git a/packages/browser-webdriverio/src/constants.ts b/packages/browser-webdriverio/src/constants.ts
new file mode 100644
index 000000000000..7229579f4e97
--- /dev/null
+++ b/packages/browser-webdriverio/src/constants.ts
@@ -0,0 +1,5 @@
+import { fileURLToPath } from 'node:url'
+import { resolve } from 'pathe'
+
+const pkgRoot = resolve(fileURLToPath(import.meta.url), '../..')
+export const distRoot: string = resolve(pkgRoot, 'dist')
diff --git a/packages/browser/src/client/tester/locators/webdriverio.ts b/packages/browser-webdriverio/src/locators.ts
similarity index 92%
rename from packages/browser/src/client/tester/locators/webdriverio.ts
rename to packages/browser-webdriverio/src/locators.ts
index f80fbb132dc0..e3972c509e8d 100644
--- a/packages/browser/src/client/tester/locators/webdriverio.ts
+++ b/packages/browser-webdriverio/src/locators.ts
@@ -3,9 +3,9 @@ import type {
UserEventDragAndDropOptions,
UserEventHoverOptions,
UserEventSelectOptions,
-} from '@vitest/browser/context'
-import { page, server } from '@vitest/browser/context'
+} from 'vitest/browser'
import {
+ convertElementToCssSelector,
getByAltTextSelector,
getByLabelSelector,
getByPlaceholderSelector,
@@ -13,11 +13,12 @@ import {
getByTestIdSelector,
getByTextSelector,
getByTitleSelector,
-} from 'ivya'
-import { getBrowserState } from '../../utils'
-import { getElementError } from '../public-utils'
-import { convertElementToCssSelector, getIframeScale } from '../utils'
-import { Locator, selectorEngine } from './index'
+ getIframeScale,
+ Locator,
+ selectorEngine,
+} from '@vitest/browser/locators'
+import { getElementError } from '@vitest/browser/utils'
+import { page, server } from 'vitest/browser'
page.extend({
getByLabelText(text, options) {
@@ -42,6 +43,7 @@ page.extend({
return new WebdriverIOLocator(getByTitleSelector(title, options))
},
+ // @ts-expect-error this is a private property
_createLocator(selector: string) {
return new WebdriverIOLocator(selector)
},
@@ -151,7 +153,7 @@ function getWebdriverioSelectOptions(element: Element, value: string | string[]
function processClickOptions(options_?: UserEventClickOptions) {
// only ui scales the iframe, so we need to adjust the position
- if (!options_ || !getBrowserState().config.browser.ui) {
+ if (!options_ || !server.config.browser.ui) {
return options_
}
const options = options_ as import('webdriverio').ClickOptions
@@ -169,7 +171,7 @@ function processClickOptions(options_?: UserEventClickOptions) {
function processHoverOptions(options_?: UserEventHoverOptions) {
// only ui scales the iframe, so we need to adjust the position
- if (!options_ || !getBrowserState().config.browser.ui) {
+ if (!options_ || !server.config.browser.ui) {
return options_
}
const options = options_ as import('webdriverio').MoveToOptions
@@ -185,7 +187,7 @@ function processHoverOptions(options_?: UserEventHoverOptions) {
function processDragAndDropOptions(options_?: UserEventDragAndDropOptions) {
// only ui scales the iframe, so we need to adjust the position
- if (!options_ || !getBrowserState().config.browser.ui) {
+ if (!options_ || !server.config.browser.ui) {
return options_
}
const cache = {}
diff --git a/packages/browser-webdriverio/src/webdriverio.ts b/packages/browser-webdriverio/src/webdriverio.ts
index 0cb17d9e2fae..72fca38d6e9e 100644
--- a/packages/browser-webdriverio/src/webdriverio.ts
+++ b/packages/browser-webdriverio/src/webdriverio.ts
@@ -13,8 +13,10 @@ import type {
import type { ClickOptions, DragAndDropOptions, remote } from 'webdriverio'
import { createBrowserServer } from '@vitest/browser'
+import { resolve } from 'pathe'
import { createDebugger } from 'vitest/node'
import commands from './commands'
+import { distRoot } from './constants'
const debug = createDebugger('vitest:browser:wdio')
@@ -52,6 +54,10 @@ export class WebdriverBrowserProvider implements BrowserProvider {
private iframeSwitched = false
private topLevelContext: string | undefined
+ public initScripts: string[] = [
+ resolve(distRoot, 'locators.js'),
+ ]
+
getSupportedBrowsers(): readonly string[] {
return webdriverBrowsers
}
diff --git a/packages/browser/package.json b/packages/browser/package.json
index ba9aa58c2f86..197843f31205 100644
--- a/packages/browser/package.json
+++ b/packages/browser/package.json
@@ -31,9 +31,9 @@
"types": "./matchers.d.ts",
"default": "./dummy.js"
},
- "./locator": {
- "types": "./dist/locators/index.d.ts",
- "default": "./dist/locators/index.js"
+ "./locators": {
+ "types": "./dist/locators.d.ts",
+ "default": "./dist/locators.js"
},
"./utils": {
"types": "./utils.d.ts",
@@ -62,24 +62,9 @@
"dev": "premove dist && pnpm run --stream '/^dev:/'"
},
"peerDependencies": {
- "playwright": "*",
- "vitest": "workspace:*",
- "webdriverio": "^7.0.0 || ^8.0.0 || ^9.0.0"
- },
- "peerDependenciesMeta": {
- "playwright": {
- "optional": true
- },
- "safaridriver": {
- "optional": true
- },
- "webdriverio": {
- "optional": true
- }
+ "vitest": "workspace:*"
},
"dependencies": {
- "@testing-library/dom": "^10.4.1",
- "@testing-library/user-event": "^14.6.1",
"@vitest/mocker": "workspace:*",
"@vitest/utils": "workspace:*",
"magic-string": "catalog:",
@@ -90,18 +75,15 @@
"ws": "catalog:"
},
"devDependencies": {
+ "@testing-library/user-event": "^14.6.1",
"@types/pngjs": "^6.0.5",
"@types/ws": "catalog:",
"@vitest/runner": "workspace:*",
- "@wdio/types": "^9.19.2",
"birpc": "catalog:",
"flatted": "catalog:",
"ivya": "^1.7.0",
"mime": "^4.1.0",
"pathe": "catalog:",
- "playwright": "^1.55.0",
- "playwright-core": "^1.55.0",
- "vitest": "workspace:*",
- "webdriverio": "^9.19.2"
+ "vitest": "workspace:*"
}
}
diff --git a/packages/browser/rollup.config.js b/packages/browser/rollup.config.js
index 780c28294aca..e4f1b248cd8c 100644
--- a/packages/browser/rollup.config.js
+++ b/packages/browser/rollup.config.js
@@ -71,10 +71,7 @@ export default () =>
},
{
input: {
- 'locators/playwright': './src/client/tester/locators/playwright.ts',
- 'locators/webdriverio': './src/client/tester/locators/webdriverio.ts',
- 'locators/preview': './src/client/tester/locators/preview.ts',
- 'locators/index': './src/client/tester/locators/index.ts',
+ 'locators': './src/client/tester/locators/index.ts',
'expect-element': './src/client/tester/expect-element.ts',
'utils': './src/client/tester/public-utils.ts',
},
@@ -146,7 +143,7 @@ export default () =>
},
{
input: dtsUtilsClient.dtsInput({
- 'locators/index': './src/client/tester/locators/index.ts',
+ locators: './src/client/tester/locators/index.ts',
}),
output: {
dir: 'dist',
@@ -157,17 +154,4 @@ export default () =>
external,
plugins: dtsUtilsClient.dts(),
},
- // {
- // input: './src/client/tester/jest-dom.ts',
- // output: {
- // file: './jest-dom.d.ts',
- // format: 'esm',
- // },
- // external: [],
- // plugins: [
- // dts({
- // respectExternal: true,
- // }),
- // ],
- // },
])
diff --git a/packages/browser/src/client/tester/context.ts b/packages/browser/src/client/tester/context.ts
index c640ca61acc6..2471574f6d09 100644
--- a/packages/browser/src/client/tester/context.ts
+++ b/packages/browser/src/client/tester/context.ts
@@ -2,19 +2,19 @@ import type {
Options as TestingLibraryOptions,
UserEvent as TestingLibraryUserEvent,
} from '@testing-library/user-event'
+import type { RunnerTask } from 'vitest'
import type {
BrowserLocators,
BrowserPage,
Locator,
UserEvent,
-} from '@vitest/browser/context'
-import type { RunnerTask } from 'vitest'
+} from 'vitest/browser'
import type { IframeViewportEvent } from '../client'
import type { BrowserRunnerState } from '../utils'
import type { Locator as LocatorAPI } from './locators/index'
import { getElementLocatorSelectors } from '@vitest/browser/utils'
import { ensureAwaited, getBrowserState, getWorkerState } from '../utils'
-import { convertToSelector, processTimeoutOptions } from './utils'
+import { convertToSelector, processTimeoutOptions } from './tester-utils'
// this file should not import anything directly, only types and utils
@@ -38,7 +38,7 @@ export function createUserEvent(__tl_user_event_base__?: TestingLibraryUserEvent
// https://playwright.dev/docs/api/class-keyboard
// https://webdriver.io/docs/api/browser/keys/
- const modifier = provider === `playwright`
+ const modifier = provider === 'playwright'
? 'ControlOrMeta'
: provider === 'webdriverio'
? 'Ctrl'
@@ -389,7 +389,7 @@ export const locators: BrowserLocators = {
_extendedMethods: new Set(),
}
-declare module '@vitest/browser/context' {
+declare module 'vitest/browser' {
interface BrowserPage {
/** @internal */
_createLocator: (selector: string) => Locator
diff --git a/packages/browser/src/client/tester/expect-element.ts b/packages/browser/src/client/tester/expect-element.ts
index 903e87273779..e86ae304cc66 100644
--- a/packages/browser/src/client/tester/expect-element.ts
+++ b/packages/browser/src/client/tester/expect-element.ts
@@ -3,7 +3,7 @@ import type { ExpectPollOptions, PromisifyDomAssertion } from 'vitest'
import { chai, expect } from 'vitest'
import { getType } from 'vitest/internal/browser'
import { matchers } from './expect'
-import { processTimeoutOptions } from './utils'
+import { processTimeoutOptions } from './tester-utils'
const kLocator = Symbol.for('$$vitest:locator')
diff --git a/packages/browser/src/client/tester/expect/toMatchScreenshot.ts b/packages/browser/src/client/tester/expect/toMatchScreenshot.ts
index 5e439499db0d..e30898f365a3 100644
--- a/packages/browser/src/client/tester/expect/toMatchScreenshot.ts
+++ b/packages/browser/src/client/tester/expect/toMatchScreenshot.ts
@@ -3,7 +3,7 @@ import type { ScreenshotMatcherOptions } from '../../../../context'
import type { ScreenshotMatcherArguments, ScreenshotMatcherOutput } from '../../../shared/screenshotMatcher/types'
import type { Locator } from '../locators'
import { getBrowserState, getWorkerState } from '../../utils'
-import { convertToSelector } from '../utils'
+import { convertToSelector } from '../tester-utils'
const counters = new Map([])
diff --git a/packages/browser/src/client/tester/locators/index.ts b/packages/browser/src/client/tester/locators/index.ts
index fd7ecf0a7a22..ecdba9ae3635 100644
--- a/packages/browser/src/client/tester/locators/index.ts
+++ b/packages/browser/src/client/tester/locators/index.ts
@@ -21,11 +21,21 @@ import {
getByTextSelector,
getByTitleSelector,
Ivya,
-
} from 'ivya'
import { ensureAwaited, getBrowserState } from '../../utils'
import { getElementError } from '../public-utils'
-import { escapeForTextSelector } from '../utils'
+import { escapeForTextSelector, isLocator } from '../tester-utils'
+
+export { convertElementToCssSelector, getIframeScale, processTimeoutOptions } from '../tester-utils'
+export {
+ getByAltTextSelector,
+ getByLabelSelector,
+ getByPlaceholderSelector,
+ getByRoleSelector,
+ getByTestIdSelector,
+ getByTextSelector,
+ getByTitleSelector,
+} from 'ivya'
// we prefer using playwright locators because they are more powerful and support Shadow DOM
export const selectorEngine: Ivya = Ivya.create({
@@ -125,7 +135,7 @@ export abstract class Locator {
): Promise {
const values = (Array.isArray(value) ? value : [value]).map((v) => {
if (typeof v !== 'string') {
- const selector = 'element' in v ? v.selector : selectorEngine.generateSelectorSimple(v)
+ const selector = isLocator(v) ? v.selector : selectorEngine.generateSelectorSimple(v)
return { element: selector }
}
return v
diff --git a/packages/browser/src/client/tester/runner.ts b/packages/browser/src/client/tester/runner.ts
index b3dfd2c61aed..9e36352b070d 100644
--- a/packages/browser/src/client/tester/runner.ts
+++ b/packages/browser/src/client/tester/runner.ts
@@ -11,7 +11,7 @@ import type {
} from '@vitest/runner'
import type { SerializedConfig, TestExecutionMethod, WorkerGlobalState } from 'vitest'
import type { VitestBrowserClientMocker } from './mocker'
-import type { CommandsManager } from './utils'
+import type { CommandsManager } from './tester-utils'
import { globalChannel, onCancel } from '@vitest/browser/client'
import { page, userEvent } from '@vitest/browser/context'
import { getTestName } from '@vitest/runner/utils'
diff --git a/packages/browser/src/client/tester/utils.ts b/packages/browser/src/client/tester/tester-utils.ts
similarity index 96%
rename from packages/browser/src/client/tester/utils.ts
rename to packages/browser/src/client/tester/tester-utils.ts
index e3702d4b7b30..6233c96efa7c 100644
--- a/packages/browser/src/client/tester/utils.ts
+++ b/packages/browser/src/client/tester/tester-utils.ts
@@ -206,8 +206,14 @@ export function convertToSelector(elementOrLocator: Element | Locator): string {
if (elementOrLocator instanceof Element) {
return convertElementToCssSelector(elementOrLocator)
}
- if ('selector' in elementOrLocator) {
- return (elementOrLocator as any).selector
+ if (isLocator(elementOrLocator)) {
+ return elementOrLocator.selector
}
throw new Error('Expected element or locator to be an instance of Element or Locator.')
}
+
+const kLocator = Symbol.for('$$vitest:locator')
+
+export function isLocator(element: unknown): element is Locator {
+ return (!!element && typeof element === 'object' && kLocator in element)
+}
diff --git a/packages/browser/src/client/tester/tester.ts b/packages/browser/src/client/tester/tester.ts
index e542e803328e..1fa6aa16a14a 100644
--- a/packages/browser/src/client/tester/tester.ts
+++ b/packages/browser/src/client/tester/tester.ts
@@ -17,7 +17,7 @@ import { VitestBrowserClientMocker } from './mocker'
import { createModuleMockerInterceptor } from './mocker-interceptor'
import { createSafeRpc } from './rpc'
import { browserHashMap, initiateRunner } from './runner'
-import { CommandsManager } from './utils'
+import { CommandsManager } from './tester-utils'
const debugVar = getConfig().env.VITEST_BROWSER_DEBUG
const debug = debugVar && debugVar !== 'false'
diff --git a/packages/browser/src/client/utils.ts b/packages/browser/src/client/utils.ts
index fc965e1c8b26..ee72e460680b 100644
--- a/packages/browser/src/client/utils.ts
+++ b/packages/browser/src/client/utils.ts
@@ -1,7 +1,7 @@
import type { VitestRunner } from '@vitest/runner'
import type { EvaluatedModules, SerializedConfig, WorkerGlobalState } from 'vitest'
import type { IframeOrchestrator } from './orchestrator'
-import type { CommandsManager } from './tester/utils'
+import type { CommandsManager } from './tester/tester-utils'
export async function importId(id: string): Promise {
const name = `/@id/${id}`.replace(/\\/g, '/')
diff --git a/packages/browser/src/node/plugin.ts b/packages/browser/src/node/plugin.ts
index 30091fa0899b..f65aa2ce2b46 100644
--- a/packages/browser/src/node/plugin.ts
+++ b/packages/browser/src/node/plugin.ts
@@ -8,7 +8,7 @@ import { createRequire } from 'node:module'
import { dynamicImportPlugin } from '@vitest/mocker/node'
import { toArray } from '@vitest/utils/helpers'
import MagicString from 'magic-string'
-import { basename, dirname, extname, resolve } from 'pathe'
+import { basename, dirname, extname, join, resolve } from 'pathe'
import sirv from 'sirv'
import { coverageConfigDefaults } from 'vitest/config'
import {
@@ -549,16 +549,14 @@ body {
},
injectTo: 'head' as const,
},
- parentServer.locatorsUrl
- ? {
- tag: 'script',
- attrs: {
- type: 'module',
- src: parentServer.locatorsUrl,
- },
- injectTo: 'head',
- } as const
- : null,
+ ...parentServer.initScripts.map(script => ({
+ tag: 'script',
+ attrs: {
+ type: 'module',
+ src: join('/@fs/', script),
+ },
+ injectTo: 'head',
+ } as const)),
...testerTags,
].filter(s => s != null)
},
diff --git a/packages/browser/src/node/plugins/pluginContext.ts b/packages/browser/src/node/plugins/pluginContext.ts
index cb266f701561..51a98eb80e6a 100644
--- a/packages/browser/src/node/plugins/pluginContext.ts
+++ b/packages/browser/src/node/plugins/pluginContext.ts
@@ -72,6 +72,7 @@ async function getUserEventImport(provider: string, resolve: (id: string, import
if (provider !== 'preview') {
return 'const _userEventSetup = undefined'
}
+ // TODO: resolve relative to @vitest/browser-preview
const resolved = await resolve('@testing-library/user-event', __dirname)
if (!resolved) {
throw new Error(`Failed to resolve user-event package from ${__dirname}`)
diff --git a/packages/browser/src/node/project.ts b/packages/browser/src/node/project.ts
index 2362a1560b2e..a882a0ea4236 100644
--- a/packages/browser/src/node/project.ts
+++ b/packages/browser/src/node/project.ts
@@ -77,11 +77,11 @@ export class ProjectBrowser implements IProjectBrowser {
this.commands[name] = cb
}
- public triggerCommand(
+ public triggerCommand = ((
name: K,
context: BrowserCommandContext,
...args: Parameters
- ): ReturnType {
+ ): ReturnType => {
if (name in this.commands) {
return this.commands[name](context, ...args)
}
@@ -89,7 +89,7 @@ export class ProjectBrowser implements IProjectBrowser {
return this.parent.commands[name](context, ...args)
}
throw new Error(`Provider ${this.provider.name} does not support command "${name}".`)
- }
+ }) as any
wrapSerializedConfig(): SerializedConfig {
const config = wrapConfig(this.project.serializedConfig)
@@ -103,6 +103,16 @@ export class ProjectBrowser implements IProjectBrowser {
return
}
this.provider = await getBrowserProvider(project.config.browser, project)
+ if (this.provider.initScripts) {
+ this.parent.initScripts = this.provider.initScripts
+ // make sure the script can be imported
+ this.provider.initScripts.forEach((script) => {
+ const allow = this.parent.vite.config.server.fs.allow
+ if (!allow.includes(script)) {
+ allow.push(script)
+ }
+ })
+ }
}
public parseErrorStacktrace(
diff --git a/packages/browser/src/node/projectParent.ts b/packages/browser/src/node/projectParent.ts
index 699bce4ec2f2..9c46f75a28d3 100644
--- a/packages/browser/src/node/projectParent.ts
+++ b/packages/browser/src/node/projectParent.ts
@@ -38,6 +38,8 @@ export class ParentBrowserProject {
public matchersUrl: string
public stateJs: Promise | string
+ public initScripts: string[] = []
+
public commands: Record> = {}
public children: Set = new Set()
public vitest: Vitest
@@ -129,11 +131,6 @@ export class ParentBrowserProject {
).then(js => (this.injectorJs = js))
this.errorCatcherUrl = join('/@fs/', resolve(distRoot, 'client/error-catcher.js'))
- const builtinProviders = ['playwright', 'webdriverio', 'preview']
- const providerName = project.config.browser.provider?.name || 'preview'
- if (builtinProviders.includes(providerName)) {
- this.locatorsUrl = join('/@fs/', distRoot, 'locators', `${providerName}.js`)
- }
this.matchersUrl = join('/@fs/', distRoot, 'expect-element.js')
this.stateJs = readFile(
resolve(distRoot, 'state.js'),
diff --git a/packages/browser/src/node/rpc.ts b/packages/browser/src/node/rpc.ts
index e8bea332d1ee..b6d24fb65de4 100644
--- a/packages/browser/src/node/rpc.ts
+++ b/packages/browser/src/node/rpc.ts
@@ -5,7 +5,6 @@ import type { BrowserCommandContext, ResolveSnapshotPathHandlerContext, TestProj
import type { WebSocket } from 'ws'
import type { WebSocketBrowserEvents, WebSocketBrowserHandlers } from '../types'
import type { ParentBrowserProject } from './projectParent'
-import type { WebdriverBrowserProvider } from './providers/webdriverio'
import type { BrowserServerState } from './state'
import { existsSync, promises as fs } from 'node:fs'
import { AutomockedModule, AutospiedModule, ManualMockedModule, RedirectedModule } from '@vitest/mocker'
@@ -220,7 +219,7 @@ export function setupBrowserRpc(globalServer: ParentBrowserProject, defaultMocke
return vitest.state.getCountOfFailedTests()
},
async wdioSwitchContext(direction) {
- const provider = project.browser!.provider as WebdriverBrowserProvider
+ const provider = project.browser!.provider
if (!provider) {
throw new Error('Commands are only available for browser tests.')
}
@@ -228,10 +227,10 @@ export function setupBrowserRpc(globalServer: ParentBrowserProject, defaultMocke
throw new Error('Switch context is only available for WebDriverIO provider.')
}
if (direction === 'iframe') {
- await provider.switchToTestFrame()
+ await (provider as any).switchToTestFrame()
}
else {
- await provider.switchToMainFrame()
+ await (provider as any).switchToMainFrame()
}
},
async triggerCommand(sessionId, command, testPath, payload) {
diff --git a/packages/browser/src/node/utils.ts b/packages/browser/src/node/utils.ts
index faba097364d9..131c640f14aa 100644
--- a/packages/browser/src/node/utils.ts
+++ b/packages/browser/src/node/utils.ts
@@ -1,6 +1,5 @@
import type {
BrowserProvider,
- BrowserProviderOption,
ResolvedBrowserOptions,
ResolvedConfig,
TestProject,
@@ -76,19 +75,8 @@ export async function getBrowserProvider(
`${name}Browser name is required. Please, set \`test.browser.instances[].browser\` option manually.`,
)
}
- if (
- // nothing is provided by default
- options.provider == null
- // the provider is provided via `--browser.provider=playwright`
- // or the config was serialized, but we can infer the factory by the name
- || ('_cli' in options.provider && typeof options.provider.providerFactory !== 'function')
- ) {
- const providers = await import('./providers/index')
- const name = (options.provider?.name || 'preview') as 'preview' | 'webdriverio' | 'playwright'
- if (!(name in providers)) {
- throw new Error(`Unknown browser provider "${name}". Available providers: ${Object.keys(providers).join(', ')}.`)
- }
- return (providers[name] as (options?: object) => BrowserProviderOption)(options.provider?.options).providerFactory(project)
+ if (options.provider == null) {
+ throw new Error(`Browser Mode requires the "provider" to always be specified.`)
}
const supportedBrowsers = options.provider.supportedBrowser || []
if (supportedBrowsers.length && !supportedBrowsers.includes(browser)) {
@@ -99,7 +87,7 @@ export async function getBrowserProvider(
)
}
if (typeof options.provider.providerFactory !== 'function') {
- throw new TypeError(`The "${name}" browser provider does not provide a "factory" function. Received ${typeof options.provider.providerFactory}.`)
+ throw new TypeError(`The "${name}" browser provider does not provide a "providerFactory" function. Received ${typeof options.provider.providerFactory}.`)
}
return options.provider.providerFactory(project)
}
diff --git a/packages/vitest/src/node/projects/resolveProjects.ts b/packages/vitest/src/node/projects/resolveProjects.ts
index 1f08dd168009..eac7449c64cb 100644
--- a/packages/vitest/src/node/projects/resolveProjects.ts
+++ b/packages/vitest/src/node/projects/resolveProjects.ts
@@ -18,7 +18,6 @@ import { configFiles as defaultConfigFiles } from '../../constants'
import { isTTY } from '../../utils/env'
import { VitestFilteredOutProjectError } from '../errors'
import { initializeProject, TestProject } from '../project'
-import { withLabel } from '../reporters/renderers/utils'
// vitest.config.*
// vite.config.*
@@ -190,24 +189,10 @@ export async function resolveBrowserProjects(
return
}
const instances = project.config.browser.instances || []
- const browser = project.config.browser.name
- if (instances.length === 0 && browser) {
- instances.push({
- browser,
- name: project.name ? `${project.name} (${browser})` : browser,
- })
- vitest.logger.warn(
- withLabel(
- 'yellow',
- 'Vitest',
- [
- `No browser "instances" were defined`,
- project.name ? ` for the "${project.name}" project. ` : '. ',
- `Running tests in "${project.config.browser.name}" browser. `,
- 'The "browser.name" field is deprecated since Vitest 3. ',
- 'Read more: https://vitest.dev/guide/browser/config#browser-instances',
- ].filter(Boolean).join(''),
- ),
+ if (instances.length === 0) {
+ throw new Error(
+ `No browser "instances" were defined${
+ project.name ? ` for the "${project.name}" project.` : ''}`,
)
}
const originalName = project.config.name
@@ -237,6 +222,9 @@ export async function resolveBrowserProjects(
if (name == null) {
throw new Error(`The browser configuration must have a "name" property. This is a bug in Vitest. Please, open a new issue with reproduction`)
}
+ if (config.provider?.name !== project.config.browser.provider?.name) {
+ throw new Error(`The instance cannot have a different provider from its parent. The "${name}" instance specifies "${config.provider?.name}" provider, but its parent has a "${project.config.browser.provider?.name}" provider.`)
+ }
if (names.has(name)) {
throw new Error(
diff --git a/packages/vitest/src/node/types/browser.ts b/packages/vitest/src/node/types/browser.ts
index 8e23f2fa5dd4..3a314e9a669b 100644
--- a/packages/vitest/src/node/types/browser.ts
+++ b/packages/vitest/src/node/types/browser.ts
@@ -44,6 +44,7 @@ export interface BrowserServerFactory {
export interface BrowserProvider {
name: string
mocker?: BrowserModuleMocker
+ readonly initScripts?: string[]
/**
* @experimental opt-in into file parallelisation
*/
@@ -85,7 +86,6 @@ export interface BrowserInstanceOption extends
| 'testerHtmlPath'
| 'screenshotDirectory'
| 'screenshotFailures'
- | 'provider'
> {
/**
* Name of the browser
@@ -93,6 +93,7 @@ export interface BrowserInstanceOption extends
browser: string
name?: string
+ provider?: BrowserProviderOption
}
export interface BrowserConfigOptions {
@@ -117,8 +118,19 @@ export interface BrowserConfigOptions {
/**
* Browser provider
+ * @example
+ * ```ts
+ * import { playwright } from '@vitest/browser-playwright'
+ * export default defineConfig({
+ * test: {
+ * browser: {
+ * provider: playwright(),
+ * },
+ * },
+ * })
+ * ```
*/
- provider?: BrowserProviderOption
+ provider: BrowserProviderOption
/**
* enable headless mode
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 8bb4e9081954..75ac748c92c0 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -441,12 +441,6 @@ importers:
packages/browser:
dependencies:
- '@testing-library/dom':
- specifier: ^10.4.1
- version: 10.4.1
- '@testing-library/user-event':
- specifier: ^14.6.1
- version: 14.6.1(@testing-library/dom@10.4.1)
'@vitest/mocker':
specifier: workspace:*
version: link:../mocker
@@ -472,6 +466,9 @@ importers:
specifier: 'catalog:'
version: 8.18.3
devDependencies:
+ '@testing-library/user-event':
+ specifier: ^14.6.1
+ version: 14.6.1(@testing-library/dom@10.4.1)
'@types/pngjs':
specifier: ^6.0.5
version: 6.0.5
@@ -481,9 +478,6 @@ importers:
'@vitest/runner':
specifier: workspace:*
version: link:../runner
- '@wdio/types':
- specifier: ^9.19.2
- version: 9.19.2
birpc:
specifier: 'catalog:'
version: 2.5.0
@@ -499,18 +493,9 @@ importers:
pathe:
specifier: 'catalog:'
version: 2.0.3
- playwright:
- specifier: ^1.55.0
- version: 1.55.0
- playwright-core:
- specifier: ^1.55.0
- version: 1.55.0
vitest:
specifier: workspace:*
version: link:../vitest
- webdriverio:
- specifier: ^9.19.2
- version: 9.19.2
packages/browser-playwright:
dependencies:
From f5c9a12cdf9be0fd6853616ed2b7f8b3aade97f0 Mon Sep 17 00:00:00 2001
From: Vladimir Sheremet
Date: Mon, 29 Sep 2025 15:37:04 +0200
Subject: [PATCH 12/43] chore: cleanup
---
packages/vitest/rollup.config.js | 1 +
1 file changed, 1 insertion(+)
diff --git a/packages/vitest/rollup.config.js b/packages/vitest/rollup.config.js
index f4d3a7921762..d5288bee78e3 100644
--- a/packages/vitest/rollup.config.js
+++ b/packages/vitest/rollup.config.js
@@ -70,6 +70,7 @@ const external = [
'node:console',
'inspector',
'vitest/optional-types.js',
+ 'vitest/browser',
'vite/module-runner',
'@vitest/mocker',
'@vitest/mocker/node',
From 37b7d3c5a35611fc45e5f4848c6a15cea838f229 Mon Sep 17 00:00:00 2001
From: Vladimir Sheremet
Date: Mon, 29 Sep 2025 15:48:55 +0200
Subject: [PATCH 13/43] chore: rename "@vitest/browser/context" to
"vitest/browser"
---
docs/guide/browser/assertion-api.md | 6 +-
docs/guide/browser/commands.md | 12 +-
docs/guide/browser/component-testing.md | 4 +-
docs/guide/browser/context.md | 2 +-
docs/guide/browser/index.md | 10 +-
docs/guide/browser/interactivity-api.md | 36 +-
docs/guide/browser/locators.md | 26 +-
docs/guide/browser/playwright.md | 2 +-
.../browser/visual-regression-testing.md | 2 +-
examples/lit/test/basic.test.ts | 2 +-
.../browser-playwright/src/commands/trace.ts | 16 +-
.../browser-playwright/src/commands/upload.ts | 2 +-
.../browser-playwright/src/commands/utils.ts | 2 +-
packages/browser-playwright/src/playwright.ts | 8 +-
.../src/commands/upload.ts | 2 +-
.../browser-webdriverio/src/webdriverio.ts | 4 +-
packages/browser/aria-role.d.ts | 96 ---
packages/browser/context.d.ts | 768 ------------------
packages/browser/context.js | 19 -
packages/browser/jest-dom.d.ts | 724 -----------------
packages/browser/matchers.d.ts | 29 -
.../src/client/tester/expect-element.ts | 2 +-
.../src/client/tester/expect/toBeVisible.ts | 2 +-
.../src/client/tester/expect/toHaveStyle.ts | 2 +-
.../src/client/tester/locators/index.ts | 6 +-
.../browser/src/client/tester/public-utils.ts | 4 +-
packages/browser/src/client/tester/runner.ts | 2 +-
.../browser/src/client/tester/tester-utils.ts | 2 +-
packages/browser/src/client/tester/tester.ts | 2 +-
packages/browser/src/client/vite.config.ts | 2 +-
packages/browser/src/node/commands/utils.ts | 2 +-
.../browser/src/node/plugins/pluginContext.ts | 4 +-
packages/browser/utils.d.ts | 2 +-
packages/coverage-v8/src/browser.ts | 2 +-
packages/vitest/browser/context.d.ts | 2 +-
.../vitest/src/node/config/resolveConfig.ts | 8 +-
packages/vitest/src/node/types/browser.ts | 4 +-
packages/vitest/src/utils/graph.ts | 2 +-
.../broken-iframe/submit-form.test.ts | 2 +-
.../browser-crash/browser-crash.test.ts | 4 +-
.../fixtures/expect-dom/toHaveLength.test.ts | 2 +-
.../expect-dom/toMatchScreenshot.test.ts | 2 +-
test/browser/fixtures/failing/failing.test.ts | 2 +-
.../fixtures/locators-custom/basic.test.tsx | 4 +-
test/browser/fixtures/locators/blog.test.tsx | 2 +-
test/browser/fixtures/locators/query.test.ts | 2 +-
.../multiple-different-configs/basic.test.js | 2 +-
.../timeout-hooks/hooks-timeout.test.ts | 2 +-
test/browser/fixtures/timeout/timeout.test.ts | 2 +-
.../fixtures/user-event/cleanup-retry.test.ts | 2 +-
.../fixtures/user-event/cleanup1.test.ts | 2 +-
.../fixtures/user-event/cleanup2.test.ts | 2 +-
.../fixtures/user-event/clipboard.test.ts | 2 +-
.../fixtures/user-event/keyboard.test.ts | 2 +-
test/browser/fixtures/viewport/basic.test.ts | 2 +-
.../browser/specs/to-match-screenshot.test.ts | 2 +-
test/browser/test/cdp.test.ts | 2 +-
test/browser/test/commands.test.ts | 4 +-
test/browser/test/dom.test.ts | 2 +-
test/browser/test/expect-element.test.ts | 2 +-
test/browser/test/iframe.test.ts | 2 +-
test/browser/test/userEvent.test.ts | 2 +-
test/browser/test/utils.test.ts | 2 +-
test/browser/test/viewport.test.ts | 2 +-
.../fails/node-browser-context.test.ts | 2 +-
.../cli/test/__snapshots__/fails.test.ts.snap | 2 +-
test/config/test/browser-configs.test.ts | 12 +
test/dts-playwright/src/basic.test.ts | 2 +-
68 files changed, 139 insertions(+), 1759 deletions(-)
delete mode 100644 packages/browser/aria-role.d.ts
delete mode 100644 packages/browser/context.d.ts
delete mode 100644 packages/browser/context.js
delete mode 100644 packages/browser/jest-dom.d.ts
delete mode 100644 packages/browser/matchers.d.ts
diff --git a/docs/guide/browser/assertion-api.md b/docs/guide/browser/assertion-api.md
index b10901a5b1c9..4c8e4b44a2d0 100644
--- a/docs/guide/browser/assertion-api.md
+++ b/docs/guide/browser/assertion-api.md
@@ -7,10 +7,10 @@ title: Assertion API | Browser Mode
Vitest provides a wide range of DOM assertions out of the box forked from [`@testing-library/jest-dom`](https://github.com/testing-library/jest-dom) library with the added support for locators and built-in retry-ability.
::: tip TypeScript Support
-If you are using [TypeScript](/guide/browser/#typescript) or want to have correct type hints in `expect`, make sure you have `@vitest/browser/context` referenced somewhere. If you never imported from there, you can add a `reference` comment in any file that's covered by your `tsconfig.json`:
+If you are using [TypeScript](/guide/browser/#typescript) or want to have correct type hints in `expect`, make sure you have `vitest/browser` referenced somewhere. If you never imported from there, you can add a `reference` comment in any file that's covered by your `tsconfig.json`:
```ts
-///
+///
```
:::
@@ -18,7 +18,7 @@ Tests in the browser might fail inconsistently due to their asynchronous nature.
```ts
import { expect, test } from 'vitest'
-import { page } from '@vitest/browser/context'
+import { page } from 'vitest/browser'
test('error banner is rendered', async () => {
triggerError()
diff --git a/docs/guide/browser/commands.md b/docs/guide/browser/commands.md
index 0b7bc1254905..8e37044306b9 100644
--- a/docs/guide/browser/commands.md
+++ b/docs/guide/browser/commands.md
@@ -20,7 +20,7 @@ This API follows [`server.fs`](https://vitejs.dev/config/server-options.html#ser
:::
```ts
-import { server } from '@vitest/browser/context'
+import { server } from 'vitest/browser'
const { readFile, writeFile, removeFile } = server.commands
@@ -38,10 +38,10 @@ it('handles files', async () => {
## CDP Session
-Vitest exposes access to raw Chrome Devtools Protocol via the `cdp` method exported from `@vitest/browser/context`. It is mostly useful to library authors to build tools on top of it.
+Vitest exposes access to raw Chrome Devtools Protocol via the `cdp` method exported from `vitest/browser`. It is mostly useful to library authors to build tools on top of it.
```ts
-import { cdp } from '@vitest/browser/context'
+import { cdp } from 'vitest/browser'
const input = document.createElement('input')
document.body.appendChild(input)
@@ -97,10 +97,10 @@ export default function BrowserCommands(): Plugin {
}
```
-Then you can call it inside your test by importing it from `@vitest/browser/context`:
+Then you can call it inside your test by importing it from `vitest/browser`:
```ts
-import { commands } from '@vitest/browser/context'
+import { commands } from 'vitest/browser'
import { expect, test } from 'vitest'
test('custom command works correctly', async () => {
@@ -109,7 +109,7 @@ test('custom command works correctly', async () => {
})
// if you are using TypeScript, you can augment the module
-declare module '@vitest/browser/context' {
+declare module 'vitest/browser' {
interface BrowserCommands {
myCustomCommand: (arg1: string, arg2: string) => Promise<{
someValue: true
diff --git a/docs/guide/browser/component-testing.md b/docs/guide/browser/component-testing.md
index 1f7d7a389c79..fe6e80f4d4e6 100644
--- a/docs/guide/browser/component-testing.md
+++ b/docs/guide/browser/component-testing.md
@@ -138,7 +138,7 @@ The key is using `page.elementLocator()` to bridge Testing Library's DOM output
```jsx
// For Solid.js components
import { render } from '@testing-library/solid'
-import { page } from '@vitest/browser/context'
+import { page } from 'vitest/browser'
test('Solid component handles user interaction', async () => {
// Use Testing Library to render the component
@@ -564,7 +564,7 @@ import { render } from 'vitest-browser-react' // [!code ++]
### Key Differences
- Use `await expect.element()` instead of `expect()` for DOM assertions
-- Use `@vitest/browser/context` for user interactions instead of `@testing-library/user-event`
+- Use `vitest/browser` for user interactions instead of `@testing-library/user-event`
- Browser Mode provides real browser environment for accurate testing
## Learn More
diff --git a/docs/guide/browser/context.md b/docs/guide/browser/context.md
index d8710a0cdfc1..8bedda3b1178 100644
--- a/docs/guide/browser/context.md
+++ b/docs/guide/browser/context.md
@@ -4,7 +4,7 @@ title: Context API | Browser Mode
# Context API
-Vitest exposes a context module via `@vitest/browser/context` entry point. As of 2.0, it exposes a small set of utilities that might be useful to you in tests.
+Vitest exposes a context module via `vitest/browser` entry point. As of 2.0, it exposes a small set of utilities that might be useful to you in tests.
## `userEvent`
diff --git a/docs/guide/browser/index.md b/docs/guide/browser/index.md
index adb543221465..aceebb09cca7 100644
--- a/docs/guide/browser/index.md
+++ b/docs/guide/browser/index.md
@@ -369,7 +369,7 @@ By default, you don't need any external packages to work with the Browser Mode:
```js [example.test.js]
import { expect, test } from 'vitest'
-import { page } from '@vitest/browser/context'
+import { page } from 'vitest/browser'
import { render } from './my-render-function.js'
test('properly handles form inputs', async () => {
@@ -407,15 +407,15 @@ Besides rendering components and locating elements, you will also need to make a
```ts
import { expect } from 'vitest'
-import { page } from '@vitest/browser/context'
+import { page } from 'vitest/browser'
// element is rendered correctly
await expect.element(page.getByText('Hello World')).toBeInTheDocument()
```
-Vitest exposes a [Context API](/guide/browser/context) with a small set of utilities that might be useful to you in tests. For example, if you need to make an interaction, like clicking an element or typing text into an input, you can use `userEvent` from `@vitest/browser/context`. Read more at the [Interactivity API](/guide/browser/interactivity-api).
+Vitest exposes a [Context API](/guide/browser/context) with a small set of utilities that might be useful to you in tests. For example, if you need to make an interaction, like clicking an element or typing text into an input, you can use `userEvent` from `vitest/browser`. Read more at the [Interactivity API](/guide/browser/interactivity-api).
```ts
-import { page, userEvent } from '@vitest/browser/context'
+import { page, userEvent } from 'vitest/browser'
await userEvent.fill(page.getByLabelText(/username/i), 'Alice')
// or just locator.fill
await page.getByLabelText(/username/i).fill('Alice')
@@ -532,7 +532,7 @@ For unsupported frameworks, we recommend using `testing-library` packages:
You can also see more examples in [`browser-examples`](https://github.com/vitest-tests/browser-examples) repository.
::: warning
-`testing-library` provides a package `@testing-library/user-event`. We do not recommend using it directly because it simulates events instead of actually triggering them - instead, use [`userEvent`](/guide/browser/interactivity-api) imported from `@vitest/browser/context` that uses Chrome DevTools Protocol or Webdriver (depending on the provider) under the hood.
+`testing-library` provides a package `@testing-library/user-event`. We do not recommend using it directly because it simulates events instead of actually triggering them - instead, use [`userEvent`](/guide/browser/interactivity-api) imported from `vitest/browser` that uses Chrome DevTools Protocol or Webdriver (depending on the provider) under the hood.
:::
::: code-group
diff --git a/docs/guide/browser/interactivity-api.md b/docs/guide/browser/interactivity-api.md
index b9a6c2de7033..301ade07c19f 100644
--- a/docs/guide/browser/interactivity-api.md
+++ b/docs/guide/browser/interactivity-api.md
@@ -7,7 +7,7 @@ title: Interactivity API | Browser Mode
Vitest implements a subset of [`@testing-library/user-event`](https://testing-library.com/docs/user-event/intro) APIs using [Chrome DevTools Protocol](https://chromedevtools.github.io/devtools-protocol/) or [webdriver](https://www.w3.org/TR/webdriver/) instead of faking events which makes the browser behaviour more reliable and consistent with how users interact with a page.
```ts
-import { userEvent } from '@vitest/browser/context'
+import { userEvent } from 'vitest/browser'
await userEvent.click(document.querySelector('.button'))
```
@@ -23,10 +23,10 @@ function setup(): UserEvent
Creates a new user event instance. This is useful if you need to keep the state of keyboard to press and release buttons correctly.
::: warning
-Unlike `@testing-library/user-event`, the default `userEvent` instance from `@vitest/browser/context` is created once, not every time its methods are called! You can see the difference in how it works in this snippet:
+Unlike `@testing-library/user-event`, the default `userEvent` instance from `vitest/browser` is created once, not every time its methods are called! You can see the difference in how it works in this snippet:
```ts
-import { userEvent as vitestUserEvent } from '@vitest/browser/context'
+import { userEvent as vitestUserEvent } from 'vitest/browser'
import { userEvent as originalUserEvent } from '@testing-library/user-event'
await vitestUserEvent.keyboard('{Shift}') // press shift without releasing
@@ -51,7 +51,7 @@ function click(
Click on an element. Inherits provider's options. Please refer to your provider's documentation for detailed explanation about how this method works.
```ts
-import { page, userEvent } from '@vitest/browser/context'
+import { page, userEvent } from 'vitest/browser'
test('clicks on an element', async () => {
const logo = page.getByRole('img', { name: /logo/ })
@@ -82,7 +82,7 @@ Triggers a double click event on an element.
Please refer to your provider's documentation for detailed explanation about how this method works.
```ts
-import { page, userEvent } from '@vitest/browser/context'
+import { page, userEvent } from 'vitest/browser'
test('triggers a double click on an element', async () => {
const logo = page.getByRole('img', { name: /logo/ })
@@ -113,7 +113,7 @@ Triggers a triple click event on an element. Since there is no `tripleclick` in
Please refer to your provider's documentation for detailed explanation about how this method works.
```ts
-import { page, userEvent } from '@vitest/browser/context'
+import { page, userEvent } from 'vitest/browser'
test('triggers a triple click on an element', async () => {
const logo = page.getByRole('img', { name: /logo/ })
@@ -150,7 +150,7 @@ function fill(
Set a value to the `input`/`textarea`/`contenteditable` field. This will remove any existing text in the input before setting the new value.
```ts
-import { page, userEvent } from '@vitest/browser/context'
+import { page, userEvent } from 'vitest/browser'
test('update input', async () => {
const input = page.getByRole('input')
@@ -189,7 +189,7 @@ The `userEvent.keyboard` allows you to trigger keyboard strokes. If any input ha
This API supports [user-event `keyboard` syntax](https://testing-library.com/docs/user-event/keyboard).
```ts
-import { userEvent } from '@vitest/browser/context'
+import { userEvent } from 'vitest/browser'
test('trigger keystrokes', async () => {
await userEvent.keyboard('foo') // translates to: f, o, o
@@ -215,7 +215,7 @@ function tab(options?: UserEventTabOptions): Promise
Sends a `Tab` key event. This is a shorthand for `userEvent.keyboard('{tab}')`.
```ts
-import { page, userEvent } from '@vitest/browser/context'
+import { page, userEvent } from 'vitest/browser'
test('tab works', async () => {
const [input1, input2] = page.getByRole('input').elements()
@@ -259,7 +259,7 @@ This function allows you to type characters into an `input`/`textarea`/`contente
If you just need to press characters without an input, use [`userEvent.keyboard`](#userevent-keyboard) API.
```ts
-import { page, userEvent } from '@vitest/browser/context'
+import { page, userEvent } from 'vitest/browser'
test('update input', async () => {
const input = page.getByRole('input')
@@ -289,7 +289,7 @@ function clear(element: Element | Locator, options?: UserEventClearOptions): Pro
This method clears the input element content.
```ts
-import { page, userEvent } from '@vitest/browser/context'
+import { page, userEvent } from 'vitest/browser'
test('clears input', async () => {
const input = page.getByRole('input')
@@ -336,7 +336,7 @@ Unlike `@testing-library`, Vitest doesn't support [listbox](https://developer.mo
:::
```ts
-import { page, userEvent } from '@vitest/browser/context'
+import { page, userEvent } from 'vitest/browser'
test('clears input', async () => {
const select = page.getByRole('select')
@@ -386,7 +386,7 @@ If you are using `playwright` provider, the cursor moves to "some" visible point
:::
```ts
-import { page, userEvent } from '@vitest/browser/context'
+import { page, userEvent } from 'vitest/browser'
test('hovers logo element', async () => {
const logo = page.getByRole('img', { name: /logo/ })
@@ -419,7 +419,7 @@ By default, the cursor position is in "some" visible place (in `playwright` prov
:::
```ts
-import { page, userEvent } from '@vitest/browser/context'
+import { page, userEvent } from 'vitest/browser'
test('unhover logo element', async () => {
const logo = page.getByRole('img', { name: /logo/ })
@@ -449,7 +449,7 @@ function upload(
Change a file input element to have the specified files.
```ts
-import { page, userEvent } from '@vitest/browser/context'
+import { page, userEvent } from 'vitest/browser'
test('can upload a file', async () => {
const input = page.getByRole('button', { name: /Upload files/ })
@@ -488,7 +488,7 @@ function dragAndDrop(
Drags the source element on top of the target element. Don't forget that the `source` element has to have the `draggable` attribute set to `true`.
```ts
-import { page, userEvent } from '@vitest/browser/context'
+import { page, userEvent } from 'vitest/browser'
test('drag and drop works', async () => {
const source = page.getByRole('img', { name: /logo/ })
@@ -520,7 +520,7 @@ function copy(): Promise
Copy the selected text to the clipboard.
```js
-import { page, userEvent } from '@vitest/browser/context'
+import { page, userEvent } from 'vitest/browser'
test('copy and paste', async () => {
// write to 'source'
@@ -553,7 +553,7 @@ function cut(): Promise
Cut the selected text to the clipboard.
```js
-import { page, userEvent } from '@vitest/browser/context'
+import { page, userEvent } from 'vitest/browser'
test('copy and paste', async () => {
// write to 'source'
diff --git a/docs/guide/browser/locators.md b/docs/guide/browser/locators.md
index e442e9dc2259..f3d93f3437d1 100644
--- a/docs/guide/browser/locators.md
+++ b/docs/guide/browser/locators.md
@@ -609,7 +609,7 @@ function click(options?: UserEventClickOptions): Promise
Click on an element. You can use the options to set the cursor position.
```ts
-import { page } from '@vitest/browser/context'
+import { page } from 'vitest/browser'
await page.getByRole('img', { name: 'Rose' }).click()
```
@@ -625,7 +625,7 @@ function dblClick(options?: UserEventDoubleClickOptions): Promise
Triggers a double click event on an element. You can use the options to set the cursor position.
```ts
-import { page } from '@vitest/browser/context'
+import { page } from 'vitest/browser'
await page.getByRole('img', { name: 'Rose' }).dblClick()
```
@@ -641,7 +641,7 @@ function tripleClick(options?: UserEventTripleClickOptions): Promise
Triggers a triple click event on an element. Since there is no `tripleclick` in browser api, this method will fire three click events in a row.
```ts
-import { page } from '@vitest/browser/context'
+import { page } from 'vitest/browser'
await page.getByRole('img', { name: 'Rose' }).tripleClick()
```
@@ -657,7 +657,7 @@ function clear(options?: UserEventClearOptions): Promise
Clears the input element content.
```ts
-import { page } from '@vitest/browser/context'
+import { page } from 'vitest/browser'
await page.getByRole('textbox', { name: 'Full Name' }).clear()
```
@@ -673,7 +673,7 @@ function hover(options?: UserEventHoverOptions): Promise
Moves the cursor position to the selected element.
```ts
-import { page } from '@vitest/browser/context'
+import { page } from 'vitest/browser'
await page.getByRole('img', { name: 'Rose' }).hover()
```
@@ -689,7 +689,7 @@ function unhover(options?: UserEventHoverOptions): Promise
This works the same as [`locator.hover`](#hover), but moves the cursor to the `document.body` element instead.
```ts
-import { page } from '@vitest/browser/context'
+import { page } from 'vitest/browser'
await page.getByRole('img', { name: 'Rose' }).unhover()
```
@@ -705,7 +705,7 @@ function fill(text: string, options?: UserEventFillOptions): Promise
Sets the value of the current `input`, `textarea` or `contenteditable` element.
```ts
-import { page } from '@vitest/browser/context'
+import { page } from 'vitest/browser'
await page.getByRole('input', { name: 'Full Name' }).fill('Mr. Bean')
```
@@ -724,7 +724,7 @@ function dropTo(
Drags the current element to the target location.
```ts
-import { page } from '@vitest/browser/context'
+import { page } from 'vitest/browser'
const paris = page.getByText('Paris')
const france = page.getByText('France')
@@ -752,7 +752,7 @@ function selectOptions(
Choose one or more values from a `