From cb8f9c8ce8fea95d1f551c9d1d754bd72d3fcd60 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Wed, 27 Nov 2024 18:18:13 +0100 Subject: [PATCH 01/51] feat: allow multi-browser configuration --- packages/browser/src/client/client.ts | 2 +- packages/browser/src/node/index.ts | 23 ++++++++-- packages/browser/src/node/plugin.ts | 46 +++++++++---------- .../browser/src/node/plugins/pluginContext.ts | 20 +------- packages/browser/src/node/pool.ts | 2 +- .../browser/src/node/providers/playwright.ts | 2 +- packages/browser/src/node/rpc.ts | 42 +++++++++-------- packages/browser/src/node/server.ts | 42 +++++++++++++---- .../browser/src/node/serverOrchestrator.ts | 11 +++-- packages/browser/src/node/serverTester.ts | 7 +-- packages/browser/src/node/state.ts | 3 +- packages/vitest/src/node/core.ts | 4 +- packages/vitest/src/node/project.ts | 46 +++++++++++++++++-- packages/vitest/src/node/types/browser.ts | 11 ++++- .../src/node/workspace/resolveWorkspace.ts | 42 +++++++++++++++-- test/browser/vitest.config.mts | 4 ++ 16 files changed, 209 insertions(+), 98 deletions(-) diff --git a/packages/browser/src/client/client.ts b/packages/browser/src/client/client.ts index fd4e8c48a50b..9f341e8b0792 100644 --- a/packages/browser/src/client/client.ts +++ b/packages/browser/src/client/client.ts @@ -14,7 +14,7 @@ export const SESSION_ID : getBrowserState().testerId export const ENTRY_URL = `${ location.protocol === 'https:' ? 'wss:' : 'ws:' -}//${HOST}/__vitest_browser_api__?type=${PAGE_TYPE}&sessionId=${SESSION_ID}` +}//${HOST}/__vitest_browser_api__?type=${PAGE_TYPE}&sessionId=${SESSION_ID}&contextId=${getBrowserState().contextId}&projectName=${getBrowserState().config.name || ''}` let setCancel = (_: CancelReason) => {} export const onCancel = new Promise((resolve) => { diff --git a/packages/browser/src/node/index.ts b/packages/browser/src/node/index.ts index c7af212b3c98..1a4a94e254e3 100644 --- a/packages/browser/src/node/index.ts +++ b/packages/browser/src/node/index.ts @@ -1,5 +1,5 @@ import type { Plugin } from 'vitest/config' -import type { TestProject } from 'vitest/node' +import type { BrowserServer as IBrowserServer, TestProject } from 'vitest/node' import c from 'tinyrainbow' import { createViteLogger, createViteServer } from 'vitest/node' import { version } from '../../package.json' @@ -18,10 +18,10 @@ export async function createBrowserServer( prePlugins: Plugin[] = [], postPlugins: Plugin[] = [], ) { - if (project.ctx.version !== version) { - project.ctx.logger.warn( + if (project.vitest.version !== version) { + project.vitest.logger.warn( c.yellow( - `Loaded ${c.inverse(c.yellow(` vitest@${project.ctx.version} `))} and ${c.inverse(c.yellow(` @vitest/browser@${version} `))}.` + `Loaded ${c.inverse(c.yellow(` vitest@${project.vitest.version} `))} and ${c.inverse(c.yellow(` @vitest/browser@${version} `))}.` + '\nRunning mixed versions is not supported and may lead into bugs' + '\nUpdate your dependencies and make sure the versions match.', ), @@ -34,7 +34,7 @@ export async function createBrowserServer( const logLevel = (process.env.VITEST_BROWSER_DEBUG as 'info') ?? 'info' - const logger = createViteLogger(project.logger, logLevel, { + const logger = createViteLogger(project.vitest.logger, logLevel, { allowClearScreen: false, }) @@ -77,3 +77,16 @@ export async function createBrowserServer( return server } + +export function cloneBrowserServer( + project: TestProject, + browserServer: IBrowserServer, +) { + const clone = new BrowserServer( + project, + '/', + ) + clone.state = browserServer.state as any + clone.setServer(browserServer.vite) + return clone +} diff --git a/packages/browser/src/node/plugin.ts b/packages/browser/src/node/plugin.ts index f52a5b0b0015..6c80dafd7d44 100644 --- a/packages/browser/src/node/plugin.ts +++ b/packages/browser/src/node/plugin.ts @@ -1,6 +1,6 @@ import type { Stats } from 'node:fs' import type { HtmlTagDescriptor } from 'vite' -import type { TestProject } from 'vitest/node' +import type { Vitest } from 'vitest/node' import type { BrowserServer } from './server' import { lstatSync, readFileSync } from 'node:fs' import { createRequire } from 'node:module' @@ -22,10 +22,8 @@ export type { BrowserCommand } from 'vitest/node' const versionRegexp = /(?:\?|&)v=\w{8}/ export default (browserServer: BrowserServer, base = '/'): Plugin[] => { - const project = browserServer.project - function isPackageExists(pkg: string, root: string) { - return browserServer.project.ctx.packageInstaller.isPackageExists?.(pkg, { + return browserServer.vitest.packageInstaller.isPackageExists?.(pkg, { paths: [root], }) } @@ -89,7 +87,7 @@ export default (browserServer: BrowserServer, base = '/'): Plugin[] => { }, ) - const coverageFolder = resolveCoverageFolder(project) + const coverageFolder = resolveCoverageFolder(browserServer.vitest) const coveragePath = coverageFolder ? coverageFolder[1] : undefined if (coveragePath && base === coveragePath) { throw new Error( @@ -113,7 +111,7 @@ export default (browserServer: BrowserServer, base = '/'): Plugin[] => { ) } - const screenshotFailures = project.config.browser.ui && project.config.browser.screenshotFailures + const screenshotFailures = browserServer.config.browser.ui && browserServer.config.browser.screenshotFailures if (screenshotFailures) { // eslint-disable-next-line prefer-arrow-callback @@ -184,16 +182,17 @@ export default (browserServer: BrowserServer, base = '/'): Plugin[] => { name: 'vitest:browser:tests', enforce: 'pre', async config() { + const project = browserServer.vitest.getProjectByName(browserServer.config.name) const { testFiles: allTestFiles } = await project.globTestFiles() const browserTestFiles = allTestFiles.filter( file => getFilePoolName(project, file) === 'browser', ) - const setupFiles = toArray(project.config.setupFiles) + const setupFiles = toArray(browserServer.config.setupFiles) // replace env values - cannot be reassign at runtime const define: Record = {} - for (const env in (project.config.env || {})) { - const stringValue = JSON.stringify(project.config.env[env]) + for (const env in (browserServer.config.env || {})) { + const stringValue = JSON.stringify(browserServer.config.env[env]) define[`import.meta.env.${env}`] = stringValue } @@ -204,7 +203,7 @@ export default (browserServer: BrowserServer, base = '/'): Plugin[] => { resolve(vitestDist, 'browser.js'), resolve(vitestDist, 'runners.js'), resolve(vitestDist, 'utils.js'), - ...(project.config.snapshotSerializers || []), + ...(browserServer.config.snapshotSerializers || []), ] const exclude = [ @@ -230,22 +229,22 @@ export default (browserServer: BrowserServer, base = '/'): Plugin[] => { 'msw/browser', ] - if (typeof project.config.diff === 'string') { - entries.push(project.config.diff) + if (typeof browserServer.config.diff === 'string') { + entries.push(browserServer.config.diff) } - if (project.ctx.coverageProvider) { - const coverage = project.ctx.config.coverage + if (browserServer.vitest.coverageProvider) { + const coverage = browserServer.vitest.config.coverage const provider = coverage.provider if (provider === 'v8') { - const path = tryResolve('@vitest/coverage-v8', [project.config.root]) + const path = tryResolve('@vitest/coverage-v8', [browserServer.config.root]) if (path) { entries.push(path) exclude.push('@vitest/coverage-v8/browser') } } else if (provider === 'istanbul') { - const path = tryResolve('@vitest/coverage-istanbul', [project.config.root]) + const path = tryResolve('@vitest/coverage-istanbul', [browserServer.config.root]) if (path) { entries.push(path) exclude.push('@vitest/coverage-istanbul') @@ -266,7 +265,7 @@ export default (browserServer: BrowserServer, base = '/'): Plugin[] => { '@vitest/browser > @testing-library/dom', ] - const fileRoot = browserTestFiles[0] ? dirname(browserTestFiles[0]) : project.config.root + const fileRoot = browserTestFiles[0] ? dirname(browserTestFiles[0]) : browserServer.config.root const svelte = isPackageExists('vitest-browser-svelte', fileRoot) if (svelte) { @@ -360,7 +359,7 @@ export default (browserServer: BrowserServer, base = '/'): Plugin[] => { viteConfig.esbuild.legalComments = 'inline' } - const defaultPort = project.ctx._browserLastPort++ + const defaultPort = browserServer.vitest._browserLastPort++ const api = resolveApiServerConfig( viteConfig.test?.browser || {}, @@ -378,8 +377,8 @@ export default (browserServer: BrowserServer, base = '/'): Plugin[] => { viteConfig.server.fs.allow = viteConfig.server.fs.allow || [] viteConfig.server.fs.allow.push( ...resolveFsAllow( - project.ctx.config.root, - project.ctx.server.config.configFile, + browserServer.vitest.config.root, + browserServer.vitest.server.config.configFile, ), distRoot, ) @@ -394,6 +393,7 @@ export default (browserServer: BrowserServer, base = '/'): Plugin[] => { { name: 'vitest:browser:in-source-tests', transform(code, id) { + const project = browserServer.vitest.getProjectByName(browserServer.config.name) if (!project.isTestFile(id) || !code.includes('import.meta.vitest')) { return } @@ -431,7 +431,7 @@ export default (browserServer: BrowserServer, base = '/'): Plugin[] => { if (!browserServer.testerScripts) { const testerScripts = await browserServer.formatScripts( - project.config.browser.testerScripts, + browserServer.config.browser.testerScripts, ) browserServer.testerScripts = testerScripts } @@ -583,8 +583,8 @@ function getRequire() { return _require } -function resolveCoverageFolder(project: TestProject) { - const options = project.ctx.config +function resolveCoverageFolder(vitest: Vitest) { + const options = vitest.config const htmlReporter = options.coverage?.enabled ? toArray(options.coverage.reporter).find((reporter) => { if (typeof reporter === 'string') { diff --git a/packages/browser/src/node/plugins/pluginContext.ts b/packages/browser/src/node/plugins/pluginContext.ts index 54c193d74b67..43be1508fe90 100644 --- a/packages/browser/src/node/plugins/pluginContext.ts +++ b/packages/browser/src/node/plugins/pluginContext.ts @@ -5,7 +5,6 @@ import type { BrowserServer } from '../server' import { fileURLToPath } from 'node:url' import { slash } from '@vitest/utils' import { dirname, resolve } from 'pathe' -import builtinCommands from '../commands/index' const VIRTUAL_ID_CONTEXT = '\0@vitest/browser/context' const ID_CONTEXT = '@vitest/browser/context' @@ -13,21 +12,6 @@ const ID_CONTEXT = '@vitest/browser/context' const __dirname = dirname(fileURLToPath(import.meta.url)) export default function BrowserContext(server: BrowserServer): Plugin { - const project = server.project - project.config.browser.commands ??= {} - for (const [name, command] of Object.entries(builtinCommands)) { - project.config.browser.commands[name] ??= command - } - - // validate names because they can't be used as identifiers - for (const command in project.config.browser.commands) { - if (!/^[a-z_$][\w$]*$/i.test(command)) { - throw new Error( - `Invalid command name "${command}". Only alphanumeric characters, $ and _ are allowed.`, - ) - } - } - return { name: 'vitest:browser:virtual-module:context', enforce: 'pre', @@ -48,7 +32,7 @@ async function generateContextFile( this: PluginContext, server: BrowserServer, ) { - const commands = Object.keys(server.project.config.browser.commands ?? {}) + const commands = Object.keys(server.config.browser.commands ?? {}) const filepathCode = '__vitest_worker__.filepath || __vitest_worker__.current?.file?.filepath || undefined' const provider = server.provider @@ -77,7 +61,7 @@ export const server = { platform: ${JSON.stringify(process.platform)}, version: ${JSON.stringify(process.version)}, provider: ${JSON.stringify(provider.name)}, - browser: ${JSON.stringify(server.project.config.browser.name)}, + browser: __vitest_browser_runner__.config.browser.name, commands: { ${commandsCode} }, diff --git a/packages/browser/src/node/pool.ts b/packages/browser/src/node/pool.ts index aef0f4ef9d01..e8f207d4db87 100644 --- a/packages/browser/src/node/pool.ts +++ b/packages/browser/src/node/pool.ts @@ -12,7 +12,7 @@ async function waitForTests( project: TestProject, files: string[], ) { - const context = project.browser!.state.createAsyncContext(method, contextId, files) + const context = project.browser!.state.createAsyncContext(method, contextId, files, project.name) return await context } diff --git a/packages/browser/src/node/providers/playwright.ts b/packages/browser/src/node/providers/playwright.ts index f01e3cf37f3e..aafc4da80e79 100644 --- a/packages/browser/src/node/providers/playwright.ts +++ b/packages/browser/src/node/providers/playwright.ts @@ -80,7 +80,7 @@ export class PlaywrightBrowserProvider implements BrowserProvider { launchOptions.args.push(`--remote-debugging-port=${port}`) launchOptions.args.push(`--remote-debugging-address=${host}`) - this.project.logger.log(`Debugger listening on ws://${host}:${port}`) + this.project.vitest.logger.log(`Debugger listening on ws://${host}:${port}`) } // start Vitest UI maximized only on supported browsers diff --git a/packages/browser/src/node/rpc.ts b/packages/browser/src/node/rpc.ts index 0147438cddb9..46e4422cacd2 100644 --- a/packages/browser/src/node/rpc.ts +++ b/packages/browser/src/node/rpc.ts @@ -1,5 +1,5 @@ import type { ErrorWithDiff } from 'vitest' -import type { BrowserCommandContext, ResolveSnapshotPathHandlerContext } from 'vitest/node' +import type { BrowserCommandContext, ResolveSnapshotPathHandlerContext, TestProject } from 'vitest/node' import type { WebSocket } from 'ws' import type { BrowserServer } from './server' import type { WebSocketBrowserEvents, WebSocketBrowserHandlers } from './types' @@ -16,9 +16,8 @@ const debug = createDebugger('vitest:browser:api') const BROWSER_API_PATH = '/__vitest_browser_api__' export function setupBrowserRpc(server: BrowserServer) { - const project = server.project const vite = server.vite - const ctx = project.ctx + const vitest = server.vitest const wss = new WebSocketServer({ noServer: true }) @@ -32,13 +31,16 @@ export function setupBrowserRpc(server: BrowserServer) { return } + // TODO: validate that params exist const type = searchParams.get('type') ?? 'tester' const sessionId = searchParams.get('sessionId') ?? '0' + const projectName = searchParams.get('projectName') ?? '' + const project = vitest.getProjectByName(projectName) wss.handleUpgrade(request, socket, head, (ws) => { wss.emit('connection', ws, request) - const rpc = setupClient(sessionId, ws) + const rpc = setupClient(project, sessionId, ws) const state = server.state const clients = type === 'tester' ? state.testers : state.orchestrators clients.set(sessionId, rpc) @@ -61,7 +63,7 @@ export function setupBrowserRpc(server: BrowserServer) { } } - function setupClient(sessionId: string, ws: WebSocket) { + function setupClient(project: TestProject, sessionId: string, ws: WebSocket) { const mockResolver = new ServerMockResolver(server.vite, { moduleDirectories: project.config.server?.deps?.moduleDirectories, }) @@ -73,32 +75,32 @@ export function setupBrowserRpc(server: BrowserServer) { const _error = error as ErrorWithDiff _error.stacks = server.parseErrorStacktrace(_error) } - ctx.state.catchError(error, type) + vitest.state.catchError(error, type) }, async onCollected(files) { - ctx.state.collectFiles(project, files) - await ctx.report('onCollected', files) + vitest.state.collectFiles(project, files) + await vitest.report('onCollected', files) }, async onTaskUpdate(packs) { - ctx.state.updateTasks(packs) - await ctx.report('onTaskUpdate', packs) + vitest.state.updateTasks(packs) + await vitest.report('onTaskUpdate', packs) }, onAfterSuiteRun(meta) { - ctx.coverageProvider?.onAfterSuiteRun(meta) + vitest.coverageProvider?.onAfterSuiteRun(meta) }, sendLog(log) { - return ctx.report('onUserConsoleLog', log) + return vitest.report('onUserConsoleLog', log) }, resolveSnapshotPath(testPath) { - return ctx.snapshot.resolvePath(testPath, { - config: project.getSerializableConfig(), + return vitest.snapshot.resolvePath(testPath, { + config: project.serializedConfig, }) }, resolveSnapshotRawPath(testPath, rawPath) { - return ctx.snapshot.resolveRawPath(testPath, rawPath) + return vitest.snapshot.resolveRawPath(testPath, rawPath) }, snapshotSaved(snapshot) { - ctx.snapshot.add(snapshot) + vitest.snapshot.add(snapshot) }, async readSnapshotFile(snapshotPath) { checkFileAccess(snapshotPath) @@ -124,16 +126,16 @@ export function setupBrowserRpc(server: BrowserServer) { return mod?.transformResult?.map }, onCancel(reason) { - ctx.cancelCurrentRun(reason) + vitest.cancelCurrentRun(reason) }, async resolveId(id, importer) { return mockResolver.resolveId(id, importer) }, debug(...args) { - ctx.logger.console.debug(...args) + vitest.logger.console.debug(...args) }, getCountOfFailedTests() { - return ctx.state.getCountOfFailedTests() + return vitest.state.getCountOfFailedTests() }, async triggerCommand(contextId, command, testPath, payload) { debug?.('[%s] Triggering command "%s"', contextId, command) @@ -201,7 +203,7 @@ export function setupBrowserRpc(server: BrowserServer) { }, ) - ctx.onCancel(reason => rpc.onCancel(reason)) + vitest.onCancel(reason => rpc.onCancel(reason)) return rpc } diff --git a/packages/browser/src/node/server.ts b/packages/browser/src/node/server.ts index 56dfbde9f7d6..8c02745fe3af 100644 --- a/packages/browser/src/node/server.ts +++ b/packages/browser/src/node/server.ts @@ -5,8 +5,10 @@ import type { BrowserScript, CDPSession, BrowserServer as IBrowserServer, + ResolvedConfig, TestProject, Vite, + Vitest, } from 'vitest/node' import { existsSync } from 'node:fs' import { readFile } from 'node:fs/promises' @@ -15,6 +17,7 @@ import { slash } from '@vitest/utils' import { parseErrorStacktrace, parseStacktrace, type StackTraceParserOptions } from '@vitest/utils/source-map' import { join, resolve } from 'pathe' import { BrowserServerCDPHandler } from './cdp' +import builtinCommands from './commands/index' import { BrowserServerState } from './state' import { getBrowserProvider } from './utils' @@ -40,11 +43,15 @@ export class BrowserServer implements IBrowserServer { public vite!: Vite.ViteDevServer private stackTraceOptions: StackTraceParserOptions + public vitest: Vitest + public config: ResolvedConfig constructor( - public project: TestProject, + project: TestProject, public base: string, ) { + this.vitest = project.vitest + this.config = project.config this.stackTraceOptions = { frameFilter: project.config.onStackTrace, getSourceMap: (id) => { @@ -64,6 +71,20 @@ export class BrowserServer implements IBrowserServer { }, } + project.config.browser.commands ??= {} + for (const [name, command] of Object.entries(builtinCommands)) { + project.config.browser.commands[name] ??= command + } + + // validate names because they can't be used as identifiers + for (const command in project.config.browser.commands) { + if (!/^[a-z_$][\w$]*$/i.test(command)) { + throw new Error( + `Invalid command name "${command}". Only alphanumeric characters, $ and _ are allowed.`, + ) + } + } + this.state = new BrowserServerState() const pkgRoot = resolve(fileURLToPath(import.meta.url), '../..') @@ -115,8 +136,9 @@ export class BrowserServer implements IBrowserServer { this.vite = server } - getSerializableConfig() { - const config = wrapConfig(this.project.getSerializableConfig()) + wrapSerializedConfig(projectName: string) { + const project = this.vitest.getProjectByName(projectName) + const config = wrapConfig(project.serializedConfig) config.env ??= {} config.env.VITEST_BROWSER_DEBUG = process.env.VITEST_BROWSER_DEBUG || '' return config @@ -165,28 +187,28 @@ export class BrowserServer implements IBrowserServer { return (await Promise.all(promises)) } - async initBrowserProvider() { + async initBrowserProvider(project: TestProject) { if (this.provider) { return } - const Provider = await getBrowserProvider(this.project.config.browser, this.project) + const Provider = await getBrowserProvider(project.config.browser, project) this.provider = new Provider() - const browser = this.project.config.browser.name + const browser = project.config.browser.name if (!browser) { throw new Error( - `[${this.project.name}] Browser name is required. Please, set \`test.browser.name\` option manually.`, + `[${project.name}] Browser name is required. Please, set \`test.browser.name\` option manually.`, ) } const supportedBrowsers = this.provider.getSupportedBrowsers() if (supportedBrowsers.length && !supportedBrowsers.includes(browser)) { throw new Error( - `[${this.project.name}] Browser "${browser}" is not supported by the browser provider "${ + `[${project.name}] Browser "${browser}" is not supported by the browser provider "${ this.provider.name }". Supported browsers: ${supportedBrowsers.join(', ')}.`, ) } - const providerOptions = this.project.config.browser.providerOptions - await this.provider.initialize(this.project, { + const providerOptions = project.config.browser.providerOptions + await this.provider.initialize(project, { browser, options: providerOptions, }) diff --git a/packages/browser/src/node/serverOrchestrator.ts b/packages/browser/src/node/serverOrchestrator.ts index ece1471e0f8e..516837c6558b 100644 --- a/packages/browser/src/node/serverOrchestrator.ts +++ b/packages/browser/src/node/serverOrchestrator.ts @@ -7,7 +7,6 @@ export async function resolveOrchestrator( url: URL, res: ServerResponse, ) { - const project = server.project let contextId = url.searchParams.get('contextId') // it's possible to open the page without a context if (!contextId) { @@ -15,7 +14,8 @@ export async function resolveOrchestrator( contextId = contexts[contexts.length - 1] ?? 'none' } - const files = server.state.getContext(contextId!)?.files ?? [] + const contextState = server.state.getContext(contextId!) + const files = contextState?.files ?? [] const injectorJs = typeof server.injectorJs === 'string' ? server.injectorJs @@ -23,7 +23,8 @@ export async function resolveOrchestrator( const injector = replacer(injectorJs, { __VITEST_PROVIDER__: JSON.stringify(server.provider.name), - __VITEST_CONFIG__: JSON.stringify(server.getSerializableConfig()), + // TODO: check when context is not found + __VITEST_CONFIG__: JSON.stringify(server.wrapSerializedConfig(contextState?.projectName || '')), __VITEST_VITE_CONFIG__: JSON.stringify({ root: server.vite.config.root, }), @@ -39,7 +40,7 @@ export async function resolveOrchestrator( if (!server.orchestratorScripts) { server.orchestratorScripts = (await server.formatScripts( - project.config.browser.orchestratorScripts, + server.config.browser.orchestratorScripts, )).map((script) => { let html = '`, __VITEST_ERROR_CATCHER__: ``, - __VITEST_CONTEXT_ID__: JSON.stringify(contextId), + __VITEST_SESSION_ID__: JSON.stringify(sessionId), }) } diff --git a/packages/browser/src/node/serverTester.ts b/packages/browser/src/node/serverTester.ts index 8f1d40725d53..225d0ea098a7 100644 --- a/packages/browser/src/node/serverTester.ts +++ b/packages/browser/src/node/serverTester.ts @@ -22,11 +22,10 @@ export async function resolveTester( ) } - const { contextId, testFile } = server.resolveTesterUrl(url.pathname) - const state = server.state - const context = state.getContext(contextId) - // TODO: what happens if no context? (how is it possible?) - const project = server.vitest.getProjectByName(context?.projectName ?? '') + const { sessionId, testFile } = server.resolveTesterUrl(url.pathname) + const session = server.vitest._browserSessions.getSession(sessionId) + // TODO: if no session, 400 + const project = server.vitest.getProjectByName(session?.project.name ?? '') const { testFiles } = await project.globTestFiles() // if decoded test file is "__vitest_all__" or not in the list of known files, run all tests const tests @@ -35,22 +34,22 @@ export async function resolveTester( ? '__vitest_browser_runner__.files' : JSON.stringify([testFile]) const iframeId = JSON.stringify(testFile) - const files = context?.files ?? [] - const method = context?.method ?? 'run' + const files = session?.files ?? [] + const method = session?.method ?? 'run' const injectorJs = typeof server.injectorJs === 'string' ? server.injectorJs : await server.injectorJs const injector = replacer(injectorJs, { - __VITEST_PROVIDER__: JSON.stringify(server.provider.name), + __VITEST_PROVIDER__: JSON.stringify(project.browser!.provider.name), __VITEST_CONFIG__: JSON.stringify(server.wrapSerializedConfig(project.name)), __VITEST_FILES__: JSON.stringify(files), __VITEST_VITE_CONFIG__: JSON.stringify({ root: server.vite.config.root, }), __VITEST_TYPE__: '"tester"', - __VITEST_CONTEXT_ID__: JSON.stringify(contextId), + __VITEST_SESSION_ID__: JSON.stringify(sessionId), __VITEST_TESTER_ID__: JSON.stringify(crypto.randomUUID()), __VITEST_PROVIDED_CONTEXT__: JSON.stringify(stringify(project.getProvidedContext())), }) @@ -74,7 +73,7 @@ export async function resolveTester( }) } catch (err) { - context?.reject(err) + session?.reject(err) next(err) } } diff --git a/packages/browser/src/node/state.ts b/packages/browser/src/node/state.ts index a8e0590f03c0..eb4eb49689c7 100644 --- a/packages/browser/src/node/state.ts +++ b/packages/browser/src/node/state.ts @@ -1,35 +1,7 @@ -import type { BrowserServerStateContext, BrowserServerState as IBrowserServerState } from 'vitest/node' -import type { BrowserServerCDPHandler } from './cdp' +import type { BrowserServerState as IBrowserServerState } from 'vitest/node' import type { WebSocketBrowserRPC } from './types' -import { createDefer } from '@vitest/utils' export class BrowserServerState implements IBrowserServerState { public readonly orchestrators = new Map() public readonly testers = new Map() - public readonly cdps = new Map() - - private contexts = new Map() - - getContext(contextId: string) { - return this.contexts.get(contextId) - } - - createAsyncContext(method: 'run' | 'collect', contextId: string, files: string[], projectName: string): Promise { - const defer = createDefer() - this.contexts.set(contextId, { - files, - method, - projectName, - resolve: () => { - defer.resolve() - this.contexts.delete(contextId) - }, - reject: defer.reject, - }) - return defer - } - - async removeCDPHandler(sessionId: string) { - this.cdps.delete(sessionId) - } } diff --git a/packages/browser/src/node/types.ts b/packages/browser/src/node/types.ts index 91ed6c8c605d..adce00b7ba70 100644 --- a/packages/browser/src/node/types.ts +++ b/packages/browser/src/node/types.ts @@ -15,7 +15,7 @@ export interface WebSocketBrowserHandlers { saveSnapshotFile: (id: string, content: string) => Promise removeSnapshotFile: (id: string) => Promise sendLog: (log: UserConsoleLog) => void - finishBrowserTests: (contextId: string) => void + finishBrowserTests: (sessionId: string) => void snapshotSaved: (snapshot: SnapshotResult) => void debug: (...args: string[]) => void resolveId: ( @@ -23,7 +23,7 @@ export interface WebSocketBrowserHandlers { importer?: string ) => Promise triggerCommand: ( - contextId: string, + sessionId: string, command: string, testPath: string | undefined, payload: unknown[] @@ -39,8 +39,8 @@ export interface WebSocketBrowserHandlers { ) => SourceMap | null | { mappings: '' } | undefined // cdp - sendCdpEvent: (contextId: string, event: string, payload?: Record) => unknown - trackCdpEvent: (contextId: string, type: 'on' | 'once' | 'off', event: string, listenerId: string) => void + sendCdpEvent: (sessionId: string, event: string, payload?: Record) => unknown + trackCdpEvent: (sessionId: string, type: 'on' | 'once' | 'off', event: string, listenerId: string) => void } export interface WebSocketEvents diff --git a/packages/vitest/src/node/browser/sessions.ts b/packages/vitest/src/node/browser/sessions.ts new file mode 100644 index 000000000000..9233a9f6d9a7 --- /dev/null +++ b/packages/vitest/src/node/browser/sessions.ts @@ -0,0 +1,26 @@ +import type { TestProject } from 'vitest/node' +import type { BrowserServerStateSession } from '../types/browser' +import { createDefer } from '@vitest/utils' + +export class BrowserSessions { + private sessions = new Map() + + getSession(sessionId: string) { + return this.sessions.get(sessionId) + } + + createAsyncSession(method: 'run' | 'collect', sessionId: string, files: string[], project: TestProject): Promise { + const defer = createDefer() + this.sessions.set(sessionId, { + files, + method, + project, + resolve: () => { + defer.resolve() + this.sessions.delete(sessionId) + }, + reject: defer.reject, + }) + return defer + } +} diff --git a/packages/vitest/src/node/core.ts b/packages/vitest/src/node/core.ts index 17218ebf1b9f..99769cf2dba2 100644 --- a/packages/vitest/src/node/core.ts +++ b/packages/vitest/src/node/core.ts @@ -25,6 +25,7 @@ import { defaultBrowserPort, workspacesFiles as workspaceFiles } from '../consta import { getCoverageProvider } from '../integrations/coverage' import { distDir } from '../paths' import { wildcardPatternToRegExp } from '../utils/base' +import { BrowserSessions } from './browser/sessions' import { VitestCache } from './cache' import { groupFilters, parseFilter } from './cli/filter' import { resolveConfig } from './config/resolveConfig' @@ -79,7 +80,11 @@ export class Vitest { public packageInstaller: VitestPackageInstaller - /** TODO: rename to `_coreRootProject` */ + // it's possible to share the same provider between different project, + // so we need a single place where to store them + // the `state` is shared with the UI, so we can't reuse it + /** @internal */ _browserSessions = new BrowserSessions() + /** @internal */ public coreWorkspaceProject!: TestProject diff --git a/packages/vitest/src/node/project.ts b/packages/vitest/src/node/project.ts index b775718245dd..521087a87680 100644 --- a/packages/vitest/src/node/project.ts +++ b/packages/vitest/src/node/project.ts @@ -472,11 +472,11 @@ export class TestProject { } /** @internal */ - async _initBrowserServer() { + _initBrowserServer = deduped(async () => { if (!this.isBrowserEnabled() || this.browser) { return } - await this.vitest.packageInstaller.ensureInstalled('@vitest/browser', this.config.root, this.ctx.version) + await this.vitest.packageInstaller.ensureInstalled('@vitest/browser', this.config.root, this.vitest.version) const { createBrowserServer, distRoot } = await import('@vitest/browser') const browser = await createBrowserServer( this, @@ -497,7 +497,7 @@ export class TestProject { if (this.config.browser.ui) { setup(this.vitest, browser.vite) } - } + }) /** * Closes the project and all associated resources. This can only be called once; the closing promise is cached until the server restarts. @@ -597,7 +597,7 @@ export class TestProject { } /** @internal */ - async _initBrowserProvider(): Promise { + _initBrowserProvider = deduped(async (): Promise => { if (!this.isBrowserEnabled() || this.browser?.provider) { return } @@ -605,7 +605,7 @@ export class TestProject { await this._initBrowserServer() } await this.browser?.initBrowserProvider(this) - } + }) /** @internal */ static _createBasicProject(vitest: Vitest): TestProject { @@ -646,30 +646,42 @@ export class TestProject { project.config.provide[providedKey], ) } - clone._initBrowserServer = async function _initBrowserServer() { + clone._initBrowserServer = deduped(async () => { if (clone.browser) { return } await project._initBrowserServer() const { cloneBrowserServer } = await import('@vitest/browser') - this.browser = cloneBrowserServer(clone, project.browser!) - } - clone._initBrowserProvider = async function _initBrowserProvider() { - if (!this.isBrowserEnabled() || this.browser?.provider) { + clone.browser = cloneBrowserServer(clone, project.browser!) + }) + clone._initBrowserProvider = deduped(async () => { + if (!clone.isBrowserEnabled() || clone.browser?.provider) { return } - if (!this.browser) { - await this._initBrowserServer() + if (!clone.browser) { + await clone._initBrowserServer() } if (!project.browser?.provider) { await project.browser?.initBrowserProvider(project) } - await this.browser?.initBrowserProvider(clone) - } + await clone.browser?.initBrowserProvider(clone) + }) return clone } } +function deduped(cb: () => Promise) { + let _promise: Promise | undefined + return () => { + if (!_promise) { + _promise = cb().finally(() => { + _promise = undefined + }) + } + return _promise + } +} + export { /** @deprecated use `TestProject` instead */ TestProject as WorkspaceProject, diff --git a/packages/vitest/src/node/types/browser.ts b/packages/vitest/src/node/types/browser.ts index f162dfd2b35b..438b76119300 100644 --- a/packages/vitest/src/node/types/browser.ts +++ b/packages/vitest/src/node/types/browser.ts @@ -26,9 +26,9 @@ export interface BrowserProvider { getSupportedBrowsers: () => readonly string[] beforeCommand?: (command: string, args: unknown[]) => Awaitable afterCommand?: (command: string, args: unknown[]) => Awaitable - getCommandsContext: (contextId: string) => Record - openPage: (contextId: string, url: string, beforeNavigate?: () => Promise) => Promise - getCDPSession?: (contextId: string) => Promise + getCommandsContext: (sessionId: string) => Record + openPage: (sessionId: string, url: string, beforeNavigate?: () => Promise) => Promise + getCDPSession?: (sessionId: string) => Promise close: () => Awaitable // eslint-disable-next-line ts/method-signature-style -- we want to allow extended options initialize( @@ -187,13 +187,15 @@ export interface BrowserCommandContext { testPath: string | undefined provider: BrowserProvider project: TestProject + /** @deprecated use `sessionId` instead */ contextId: string + sessionId: string } -export interface BrowserServerStateContext { +export interface BrowserServerStateSession { files: string[] method: 'run' | 'collect' - projectName: string + project: TestProject resolve: () => void reject: (v: unknown) => void } @@ -206,8 +208,6 @@ export interface BrowserOrchestrator { export interface BrowserServerState { orchestrators: Map - getContext: (contextId: string) => BrowserServerStateContext | undefined - createAsyncContext: (method: 'collect' | 'run', contextId: string, files: string[], projectName: string) => Promise } export interface BrowserServer { diff --git a/packages/vitest/src/public/node.ts b/packages/vitest/src/public/node.ts index edb086a81e57..01e24ecbcffe 100644 --- a/packages/vitest/src/public/node.ts +++ b/packages/vitest/src/public/node.ts @@ -61,7 +61,7 @@ export type { BrowserScript, BrowserServer, BrowserServerState, - BrowserServerStateContext, + BrowserServerStateSession as BrowserServerStateContext, CDPSession, ResolvedBrowserOptions, } from '../node/types/browser' diff --git a/test/browser/vitest.config.mts b/test/browser/vitest.config.mts index ffbb30907c82..496960963616 100644 --- a/test/browser/vitest.config.mts +++ b/test/browser/vitest.config.mts @@ -1,4 +1,4 @@ -import type { BrowserCommand } from 'vitest/node' +import type { BrowserCommand, TestSpecification, WorkspaceSpec } from 'vitest/node' import { dirname, resolve } from 'node:path' import { fileURLToPath } from 'node:url' import * as util from 'node:util' @@ -103,6 +103,19 @@ export default defineConfig({ env: { BROWSER: browser, }, + sequence: { + sequencer: class Sequencer { + shard() { + return [] + } + + sort(specifications: TestSpecification[]) { + const webkit = specifications.find(p => p.project.name === 'webkit') + const firefox = specifications.find(p => p.project.name === 'firefox') + return [firefox, webkit] as WorkspaceSpec[] + } + }, + }, }, plugins: [ { From 417cf8fff13ada9c15d4ed291df78387f693d8cc Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Thu, 28 Nov 2024 12:37:43 +0100 Subject: [PATCH 03/51] chore: cleanup --- test/browser/vitest.config.mts | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/test/browser/vitest.config.mts b/test/browser/vitest.config.mts index 496960963616..d252d3c91e51 100644 --- a/test/browser/vitest.config.mts +++ b/test/browser/vitest.config.mts @@ -1,4 +1,4 @@ -import type { BrowserCommand, TestSpecification, WorkspaceSpec } from 'vitest/node' +import type { BrowserCommand } from 'vitest/node' import { dirname, resolve } from 'node:path' import { fileURLToPath } from 'node:url' import * as util from 'node:util' @@ -39,10 +39,6 @@ export default defineConfig({ browser: { enabled: true, name: browser, - capabilities: [ - { browser: 'webkit' }, - { browser: 'firefox' }, - ], headless: false, provider, isolate: false, @@ -103,19 +99,6 @@ export default defineConfig({ env: { BROWSER: browser, }, - sequence: { - sequencer: class Sequencer { - shard() { - return [] - } - - sort(specifications: TestSpecification[]) { - const webkit = specifications.find(p => p.project.name === 'webkit') - const firefox = specifications.find(p => p.project.name === 'firefox') - return [firefox, webkit] as WorkspaceSpec[] - } - }, - }, }, plugins: [ { From ac159887a4fdc1d3d5b3286af3acc115b09135a4 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Thu, 28 Nov 2024 12:38:02 +0100 Subject: [PATCH 04/51] chore: lint --- packages/vitest/src/node/cli/cli-config.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/vitest/src/node/cli/cli-config.ts b/packages/vitest/src/node/cli/cli-config.ts index 8aa7118b6206..39b8ede7f198 100644 --- a/packages/vitest/src/node/cli/cli-config.ts +++ b/packages/vitest/src/node/cli/cli-config.ts @@ -416,6 +416,7 @@ export const cliOptionsConfig: VitestCLIOptions = { screenshotFailures: null, locators: null, testerHtmlPath: null, + capabilities: null, }, }, pool: { From d414864ba57c0ecd825591bc78a5532498cc281b Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Thu, 28 Nov 2024 12:41:28 +0100 Subject: [PATCH 05/51] fix: inherit the name to avoid name conflict --- .../vitest/src/node/workspace/resolveWorkspace.ts | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/packages/vitest/src/node/workspace/resolveWorkspace.ts b/packages/vitest/src/node/workspace/resolveWorkspace.ts index 1c96ff41e656..f07bf9529081 100644 --- a/packages/vitest/src/node/workspace/resolveWorkspace.ts +++ b/packages/vitest/src/node/workspace/resolveWorkspace.ts @@ -132,7 +132,6 @@ export async function resolveWorkspace( export function resolveBrowserWorkspace( resolvedProjects: TestProject[], ) { - const names = new Set(resolvedProjects.map(p => p.name)) resolvedProjects.forEach((project) => { const capabilities = project.config.browser.capabilities if (!project.config.browser.enabled || !capabilities || capabilities.length === 0) { @@ -140,19 +139,16 @@ export function resolveBrowserWorkspace( } const [firstCapability, ...restCapabilities] = capabilities - project.config.name ||= firstCapability.browser + project.config.name ||= project.config.name + ? `${project.config.name} (${firstCapability.browser})` + : firstCapability.browser project.config.browser.name = firstCapability.browser project.config.browser.providerOptions = firstCapability restCapabilities.forEach(({ browser, ...capability }) => { - if (names.has(browser)) { - // TODO: better error message - add how to fix - throw new Error(`Project name "${browser}" already exists in the workspace.`) - } - const clone = TestProject._cloneBrowserProject(project, { ...project.config, - name: browser, + name: project.config.name ? `${project.config} (${browser})` : browser, browser: { ...project.config.browser, name: browser, From 2f0c0842e166e2700f59e853e9df4eb18a24ef6e Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Thu, 28 Nov 2024 17:53:09 +0100 Subject: [PATCH 06/51] docs: add configuring playwright/webdriverio section --- docs/.vitepress/config.ts | 15 ++++++ docs/guide/browser/playwright.md | 80 +++++++++++++++++++++++++++++++ docs/guide/browser/webdriverio.md | 69 ++++++++++++++++++++++++++ 3 files changed, 164 insertions(+) create mode 100644 docs/guide/browser/playwright.md create mode 100644 docs/guide/browser/webdriverio.md diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index 388181400ce4..9b3235a083f9 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -66,6 +66,7 @@ export default ({ mode }: { mode: string }) => { groupIconVitePlugin({ customIcon: { 'CLI': 'vscode-icons:file-type-shell', + 'vitest.shims': 'vscode-icons:file-type-vitest', 'vitest.workspace': 'vscode-icons:file-type-vitest', 'vitest.config': 'vscode-icons:file-type-vitest', '.spec.ts': 'vscode-icons:file-type-testts', @@ -212,6 +213,20 @@ export default ({ mode }: { mode: string }) => { }, ], }, + { + text: 'Configuration', + collapsed: false, + items: [ + { + text: 'Configuring Playwright', + link: '/guide/browser/playwright', + }, + { + text: 'Configuring WebdriverIO', + link: '/guide/browser/webdriverio', + }, + ], + }, { text: 'API', collapsed: false, diff --git a/docs/guide/browser/playwright.md b/docs/guide/browser/playwright.md new file mode 100644 index 000000000000..f7186236d39b --- /dev/null +++ b/docs/guide/browser/playwright.md @@ -0,0 +1,80 @@ +# Configuring Playwright + +By default, TypeScript doesn't recognize providers options and extra `expect` properties. Make sure to reference `@vitest/browser/providers/playwright` so TypeScript can pick up definitions for custom options: + +```ts [vitest.shims.d.ts] +/// +``` + +Alternatively, you can also add it to `compilerOptions.types` field in your `tsconfig.json` file. Note that specifying anything in this field will disable [auto loading](https://www.typescriptlang.org/tsconfig/#types) of `@types/*` packages. + +```json [tsconfig.json] +{ + "compilerOptions": { + "types": ["@vitest/browser/providers/playwright"] + } +} +``` + +Vitest opens a single page to run all tests in the same file. You can configure the `launch` and `context` properties in `capabilities`: + +```ts{9-10} [vitest.config.ts] +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + browser: { + capabilities: [ + { + browser: 'firefox', + launch: {}, + context: {}, + }, + ], + }, + }, +}) +``` + +::: warning +Before Vitest 2.2, these options were located on `test.browser.providerOptions` property: + +```ts [vitest.config.ts] +export default defineConfig({ + test: { + browser: { + providerOptions: { + launch: {}, + context: {}, + }, + }, + }, +}) +``` + +`providerOptions` is deprecated in favour of `capabilities`. +::: + +## launch + +These options are directly passed down to `playwright[browser].launch` command. You can read more about the command and available arguments in the [Playwright documentation](https://playwright.dev/docs/api/class-browsertype#browser-type-launch). + +::: warning +Vitest will ignore `launch.headless` option. Instead, use [`test.browser.headless`](/config/#browser-headless). + +Note that Vitest will push debugging flags to `launch.args` if [`--inspect`](/guide/cli#inspect) is enabled. +::: + +## context + +Vitest creates a new context for every test file by calling [`browser.newContext()`](https://playwright.dev/docs/api/class-browsercontext). You can configure this behaviour by specifying [custom arguments](https://playwright.dev/docs/api/class-apirequest#api-request-new-context). + +::: tip +Note that the context is created for every _test file_, not every _test_ like in playwright test runner. +::: + +::: warning +Vitest awlays sets `ignoreHTTPSErrors` to `true` in case your server is served via HTTPS and `serviceWorkers` to `'allow'` to support module mocking via [MSW](https://mswjs.io). + +It is also recommended to use [`test.browser.viewport`](/config/#browser-headless) instead of specifying it here as it will be lost when tests are running in headless mode. +::: diff --git a/docs/guide/browser/webdriverio.md b/docs/guide/browser/webdriverio.md new file mode 100644 index 000000000000..69aac76b25c9 --- /dev/null +++ b/docs/guide/browser/webdriverio.md @@ -0,0 +1,69 @@ +# Configuring WebdriverIO + +::: info Playwright vs WebdriverIO +If you do not already use WebdriverIO in your project, we recommend starting with [Playwright](/guide/browser/playwright) as it is easier to configure and has more flexible API. +::: + +By default, TypeScript doesn't recognize providers options and extra `expect` properties. Make sure to reference `@vitest/browser/providers/webdriverio` so TypeScript can pick up definitions for custom options: + +```ts [vitest.shims.d.ts] +/// +``` + +Alternatively, you can also add it to `compilerOptions.types` field in your `tsconfig.json` file. Note that specifying anything in this field will disable [auto loading](https://www.typescriptlang.org/tsconfig/#types) of `@types/*` packages. + +```json [tsconfig.json] +{ + "compilerOptions": { + "types": ["@vitest/browser/providers/webdriverio"] + } +} +``` + +Vitest opens a single page to run all tests in the same file. You can configure any property specified in `RemoteOptions` in `capabilities`: + +```ts{9-12} [vitest.config.ts] +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + browser: { + capabilities: [ + { + browser: 'chrome', + capabilities: { + browserVersion: 86, + platformName: 'Windows 10', + }, + }, + ], + }, + }, +}) +``` + +::: warning +Before Vitest 2.2, these options were located on `test.browser.providerOptions` property: + +```ts [vitest.config.ts] +export default defineConfig({ + test: { + browser: { + providerOptions: { + capabilities: {}, + }, + }, + }, +}) +``` + +`providerOptions` is deprecated in favour of `capabilities`. +::: + +You can find most available options in the [WebdriverIO documentation](https://webdriver.io/docs/configuration/). Note that Vitest will ignore all test runner options because we only use `webdriverio`'s browser capabilities. + +::: tip +Most useful options are located on `capabilities` object. WebdriverIO allows nested capabilities, but Vitest will ignore those options because we rely on a different mechanism to spawn several browsers. + +Note that Vitest will ignore `capabilities.browserName`. Use [`test.browser.capabilities.name`](/config/#browser-capabilities-name) instead. +::: From 7eb30017eb4c63b928900f538757017bafcdf437 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Thu, 28 Nov 2024 17:53:53 +0100 Subject: [PATCH 07/51] feat: merge custom config --- packages/vitest/src/create/browser/creator.ts | 29 ++++++----- .../vitest/src/node/config/resolveConfig.ts | 25 +++++++++- packages/vitest/src/node/core.ts | 2 +- packages/vitest/src/node/project.ts | 2 +- packages/vitest/src/node/types/browser.ts | 39 +++++++++++++-- packages/vitest/src/node/types/config.ts | 4 +- .../src/node/workspace/resolveWorkspace.ts | 50 +++++++++++++++++-- 7 files changed, 123 insertions(+), 28 deletions(-) diff --git a/packages/vitest/src/create/browser/creator.ts b/packages/vitest/src/create/browser/creator.ts index 31875b2932b2..55f6057ce958 100644 --- a/packages/vitest/src/create/browser/creator.ts +++ b/packages/vitest/src/create/browser/creator.ts @@ -228,9 +228,9 @@ function getPossibleProvider(dependencies: Record) { function getProviderDocsLink(provider: string) { switch (provider) { case 'playwright': - return 'https://playwright.dev' + return 'https://vitest.dev/guide/browser/playwright' case 'webdriverio': - return 'https://webdriver.io' + return 'https://vitest.dev/guide/browser/webdriverio' } } @@ -251,7 +251,7 @@ async function generateWorkspaceFile(options: { configPath: string rootConfig: string provider: string - browser: string + browsers: string[] }) { const relativeRoot = relative(dirname(options.configPath), options.rootConfig) const workspaceContent = [ @@ -265,10 +265,11 @@ async function generateWorkspaceFile(options: { ` test: {`, ` browser: {`, ` enabled: true,`, - ` name: '${options.browser}',`, ` provider: '${options.provider}',`, options.provider !== 'preview' && ` // ${getProviderDocsLink(options.provider)}`, - options.provider !== 'preview' && ` providerOptions: {},`, + ` capabilities: [`, + ...options.browsers.map(browser => ` { browser: '${browser}' },`), + ` ],`, ` },`, ` },`, ` },`, @@ -283,7 +284,7 @@ async function generateFrameworkConfigFile(options: { framework: string frameworkPlugin: string | null provider: string - browser: string + browsers: string[] }) { const frameworkImport = options.framework === 'svelte' ? `import { svelte } from '${options.frameworkPlugin}'` @@ -297,10 +298,11 @@ async function generateFrameworkConfigFile(options: { ` test: {`, ` browser: {`, ` enabled: true,`, - ` name: '${options.browser}',`, ` provider: '${options.provider}',`, options.provider !== 'preview' && ` // ${getProviderDocsLink(options.provider)}`, - options.provider !== 'preview' && ` providerOptions: {},`, + ` capabilities: [`, + ...options.browsers.map(browser => ` { browser: '${browser}' },`), + ` ],`, ` },`, ` },`, `})`, @@ -391,9 +393,10 @@ export async function create() { return fail() } - const { browser } = await prompt({ - type: 'select', - name: 'browser', + // TODO: allow multiselect + const { browsers } = await prompt({ + type: 'multiselect', + name: 'browsers', message: 'Choose a browser', choices: getBrowserNames(provider).map(browser => ({ title: browser, @@ -471,7 +474,7 @@ export async function create() { configPath: browserWorkspaceFile, rootConfig, provider, - browser, + browsers, }) log(c.green('✔'), 'Created a workspace file for browser tests:', c.bold(relative(process.cwd(), browserWorkspaceFile))) } @@ -482,7 +485,7 @@ export async function create() { framework, frameworkPlugin, provider, - browser, + browsers, }) log(c.green('✔'), 'Created a config file for browser tests', c.bold(relative(process.cwd(), configPath))) } diff --git a/packages/vitest/src/node/config/resolveConfig.ts b/packages/vitest/src/node/config/resolveConfig.ts index 78f19e22094a..e132db20718a 100644 --- a/packages/vitest/src/node/config/resolveConfig.ts +++ b/packages/vitest/src/node/config/resolveConfig.ts @@ -230,9 +230,30 @@ export function resolveConfig( } } + const browser = resolved.browser + + if (browser.enabled) { + if (!browser.name && !browser.capabilities) { + throw new Error(`Vitest Browser Mode requires "browser.name" (deprecated) or "browser.capabilities" options, none were set.`) + } + + if (browser.name && browser.capabilities) { + throw new Error(`Cannot use both "browser.name" and "browser.capabilities" options at the same time. Use only "browser.capabilities" instead.`) + } + + if (browser.capabilities && !browser.capabilities.length) { + throw new Error(`"browser.capabilities" was set in the config, but the array is empty. Define at least one browser capability.`) + } + + // TODO: don't throw if --project=chromium is passed filtering capabilities to a single one + // if (browser.provider === 'preview' && (browser.capabilities?.length || 0) > 1) { + // throw new Error(`Browser provider "preview" does not support multiple capabilities. Use "playwright" or "webdriverio" instead.`) + // } + } + // Browser-mode "Playwright + Chromium" only features: - if (resolved.browser.enabled && !(resolved.browser.provider === 'playwright' && resolved.browser.name === 'chromium')) { - const browserConfig = { browser: { provider: resolved.browser.provider, name: resolved.browser.name } } + if (browser.enabled && !(browser.provider === 'playwright' && browser.name === 'chromium')) { + const browserConfig = { browser: { provider: browser.provider, name: browser.name } } if (resolved.coverage.enabled && resolved.coverage.provider === 'v8') { throw new Error( diff --git a/packages/vitest/src/node/core.ts b/packages/vitest/src/node/core.ts index 99769cf2dba2..0002e844b6cf 100644 --- a/packages/vitest/src/node/core.ts +++ b/packages/vitest/src/node/core.ts @@ -291,7 +291,7 @@ export class Vitest { this._workspaceConfigPath = workspaceConfigPath if (!workspaceConfigPath) { - return resolveBrowserWorkspace([this._createRootProject()]) + return resolveBrowserWorkspace(this, [this._createRootProject()]) } const workspaceModule = await this.runner.executeFile(workspaceConfigPath) as { diff --git a/packages/vitest/src/node/project.ts b/packages/vitest/src/node/project.ts index 521087a87680..d13761ec8ebf 100644 --- a/packages/vitest/src/node/project.ts +++ b/packages/vitest/src/node/project.ts @@ -643,7 +643,7 @@ export class TestProject { // type is very strict here, so we cast it to any (clone.provide as (key: string, value: unknown) => void)( providedKey, - project.config.provide[providedKey], + config.provide[providedKey], ) } clone._initBrowserServer = deduped(async () => { diff --git a/packages/vitest/src/node/types/browser.ts b/packages/vitest/src/node/types/browser.ts index 438b76119300..b3b70695b23a 100644 --- a/packages/vitest/src/node/types/browser.ts +++ b/packages/vitest/src/node/types/browser.ts @@ -3,7 +3,7 @@ import type { Awaitable, ErrorWithDiff, ParsedStack } from '@vitest/utils' import type { StackTraceParserOptions } from '@vitest/utils/source-map' import type { ViteDevServer } from 'vite' import type { TestProject } from '../project' -import type { ApiConfig } from './config' +import type { ApiConfig, ProjectConfig } from './config' export interface BrowserProviderInitializationOptions { browser: string @@ -45,6 +45,32 @@ export interface BrowserProviderOptions {} export type BrowserBuiltinProvider = 'webdriverio' | 'playwright' | 'preview' +type UnsupportedProperties = + | 'browser' + | 'typecheck' + | 'alias' + | 'sequence' + | 'root' + | 'pool' + | 'poolOptions' + // browser mode doesn't support a custom runner + | 'runner' + // non-browser options + | 'api' + | 'deps' + | 'testTransformMode' + | 'poolMatchGlobs' + | 'environmentMatchGlobs' + | 'environment' + | 'environmentOptions' + | 'server' + | 'benchmark' + +// TODO: document all options +export interface BrowserCapabilities extends BrowserProviderOptions, Omit, Pick { + browser: string +} + export interface BrowserConfigOptions { /** * if running tests in the browser should be the default @@ -57,11 +83,12 @@ export interface BrowserConfigOptions { * Name of the browser * @deprecated use `capabilities` instead */ - name: string + name?: string - capabilities?: ({ - browser: string - } & BrowserProviderOptions)[] + /** + * Configurations for different browsers + */ + capabilities?: BrowserCapabilities[] /** * Browser provider @@ -254,6 +281,8 @@ export interface BrowserScript { } export interface ResolvedBrowserOptions extends BrowserConfigOptions { + name: string + providerOptions?: BrowserProviderOptions enabled: boolean headless: boolean isolate: boolean diff --git a/packages/vitest/src/node/types/config.ts b/packages/vitest/src/node/types/config.ts index 0ae7b73db302..eaa222dc429d 100644 --- a/packages/vitest/src/node/types/config.ts +++ b/packages/vitest/src/node/types/config.ts @@ -1081,14 +1081,16 @@ type NonProjectOptions = | 'maxWorkers' | 'minWorkers' | 'fileParallelism' + | 'workspace' export type ProjectConfig = Omit< - UserConfig, + InlineConfig, NonProjectOptions | 'sequencer' | 'deps' | 'poolOptions' > & { + mode?: string sequencer?: Omit deps?: Omit poolOptions?: { diff --git a/packages/vitest/src/node/workspace/resolveWorkspace.ts b/packages/vitest/src/node/workspace/resolveWorkspace.ts index f07bf9529081..49d468826d51 100644 --- a/packages/vitest/src/node/workspace/resolveWorkspace.ts +++ b/packages/vitest/src/node/workspace/resolveWorkspace.ts @@ -1,5 +1,5 @@ import type { Vitest } from '../core' -import type { TestProjectConfiguration, UserConfig, UserWorkspaceConfig } from '../types/config' +import type { ResolvedConfig, TestProjectConfiguration, UserConfig, UserWorkspaceConfig } from '../types/config' import { existsSync, promises as fs } from 'node:fs' import os from 'node:os' import { limitConcurrency } from '@vitest/runner/utils' @@ -8,6 +8,7 @@ import { dirname, relative, resolve } from 'pathe' import { mergeConfig } from 'vite' import { configFiles as defaultConfigFiles } from '../../constants' import { initializeProject, TestProject } from '../project' +import { withLabel } from '../reporters/renderers/utils' import { isDynamicPattern } from './fast-glob-pattern' export async function resolveWorkspace( @@ -96,7 +97,7 @@ export async function resolveWorkspace( // pretty rare case - the glob didn't match anything and there are no inline configs if (!projectPromises.length) { - return resolveBrowserWorkspace([vitest._createRootProject()]) + return resolveBrowserWorkspace(vitest, [vitest._createRootProject()]) } const resolvedProjects = await Promise.all(projectPromises) @@ -126,10 +127,11 @@ export async function resolveWorkspace( names.add(name) } - return resolveBrowserWorkspace(resolvedProjects) + return resolveBrowserWorkspace(vitest, resolvedProjects) } export function resolveBrowserWorkspace( + vitest: Vitest, resolvedProjects: TestProject[], ) { resolvedProjects.forEach((project) => { @@ -142,19 +144,57 @@ export function resolveBrowserWorkspace( project.config.name ||= project.config.name ? `${project.config.name} (${firstCapability.browser})` : firstCapability.browser + + if (project.config.browser.name) { + vitest.logger.warn( + withLabel('yellow', 'Vitest', `Browser name "${project.config.browser.name}" is ignored because it's overriden by the capabilities. To hide this warning, remove the "name" property from the browser configuration.`), + ) + } + + if (project.config.browser.providerOptions) { + vitest.logger.warn( + withLabel('yellow', 'Vitest', `"providerOptions"${project.config.name ? ` in "${project.config.name}" project` : ''} is ignored because it's overriden by the capabilities. To hide this warning, remove the "providerOptions" property from the browser configuration.`), + ) + } + project.config.browser.name = firstCapability.browser project.config.browser.providerOptions = firstCapability restCapabilities.forEach(({ browser, ...capability }) => { - const clone = TestProject._cloneBrowserProject(project, { + // TODO: cover with tests + // browser-only options + const { + locators, + viewport, + testerHtmlPath, + screenshotDirectory, + screenshotFailures, + // @ts-expect-error remove just in case + browser: _browser, + // TODO: need a lot of tests + ...overrideConfig + } = capability + const currentConfig = project.config.browser + const clonedConfig = mergeConfig({ ...project.config, name: project.config.name ? `${project.config} (${browser})` : browser, browser: { ...project.config.browser, + locators: locators + ? { + testIdAttribute: locators.testIdAttribute ?? currentConfig.locators.testIdAttribute, + } + : project.config.browser.locators, + viewport: viewport ?? currentConfig.viewport, + testerHtmlPath: testerHtmlPath ?? currentConfig.testerHtmlPath, + screenshotDirectory: screenshotDirectory ?? currentConfig.screenshotDirectory, + screenshotFailures: screenshotFailures ?? currentConfig.screenshotFailures, name: browser, providerOptions: capability, }, - }) + // TODO: should resolve, not merge/override + } satisfies ResolvedConfig, overrideConfig) as ResolvedConfig + const clone = TestProject._cloneBrowserProject(project, clonedConfig) resolvedProjects.push(clone) }) From 04048a269de401e506347764fd6c97918256a7e0 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Sun, 1 Dec 2024 23:50:46 +0100 Subject: [PATCH 08/51] docs: add `actionTimeout` to docs --- docs/guide/browser/playwright.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/docs/guide/browser/playwright.md b/docs/guide/browser/playwright.md index f7186236d39b..8c2830442d8a 100644 --- a/docs/guide/browser/playwright.md +++ b/docs/guide/browser/playwright.md @@ -78,3 +78,19 @@ Vitest awlays sets `ignoreHTTPSErrors` to `true` in case your server is served v It is also recommended to use [`test.browser.viewport`](/config/#browser-headless) instead of specifying it here as it will be lost when tests are running in headless mode. ::: + +## `actionTimeout` 2.2.0 + +- **Default:** no timeout, 1 second before 2.2.0 + +This value configures the default timeout it takes for Playwright to wait until all accessibility checks pass and [the action](/guide/browser/interactivity-api) is actually done. + +You can also configure the action timeout per-action: + +```ts +import { page, userEvent } from '@vitest/browser/context' + +await userEvent.click(page.getByRole('button'), { + timeout: 1_000, +}) +``` From b1087f8ca0da0f8601e58f007d7a506cb9ab3080 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Wed, 4 Dec 2024 15:26:35 +0100 Subject: [PATCH 09/51] refactor: rename capabilities to configs --- docs/guide/browser/index.md | 47 ------------------- docs/guide/browser/playwright.md | 6 +-- docs/guide/browser/webdriverio.md | 6 +-- packages/vitest/src/create/browser/creator.ts | 4 +- packages/vitest/src/node/cli/cli-config.ts | 2 +- .../vitest/src/node/config/resolveConfig.ts | 18 +++---- packages/vitest/src/node/types/browser.ts | 12 ++--- .../src/node/workspace/resolveWorkspace.ts | 25 ++++------ 8 files changed, 32 insertions(+), 88 deletions(-) diff --git a/docs/guide/browser/index.md b/docs/guide/browser/index.md index 4712eac69ec8..32272ba6305a 100644 --- a/docs/guide/browser/index.md +++ b/docs/guide/browser/index.md @@ -234,53 +234,6 @@ export default defineWorkspace([ ]) ``` -### Provider Configuration - -:::tabs key:provider -== Playwright -You can configure how Vitest [launches the browser](https://playwright.dev/docs/api/class-browsertype#browser-type-launch) and creates the [page context](https://playwright.dev/docs/api/class-browsercontext) via [`providerOptions`](/config/#browser-provideroptions) field: - -```ts [vitest.config.ts] -export default defineConfig({ - test: { - browser: { - providerOptions: { - launch: { - devtools: true, - }, - context: { - geolocation: { - latitude: 45, - longitude: -30, - }, - reducedMotion: 'reduce', - }, - }, - }, - }, -}) -``` -== WebdriverIO -You can configure what [options](https://webdriver.io/docs/configuration#webdriverio) Vitest should use when starting a browser via [`providerOptions`](/config/#browser-provideroptions) field: - -```ts -export default defineConfig({ - test: { - browser: { - browser: 'chrome', - providerOptions: { - region: 'eu', - capabilities: { - browserVersion: '27.0', - platformName: 'Windows 10', - }, - }, - }, - }, -}) -``` -::: - ## Browser Option Types The browser option in Vitest depends on the provider. Vitest will fail, if you pass `--browser` and don't specify its name in the config file. Available options: diff --git a/docs/guide/browser/playwright.md b/docs/guide/browser/playwright.md index 8c2830442d8a..988a240aa612 100644 --- a/docs/guide/browser/playwright.md +++ b/docs/guide/browser/playwright.md @@ -16,7 +16,7 @@ Alternatively, you can also add it to `compilerOptions.types` field in your `tsc } ``` -Vitest opens a single page to run all tests in the same file. You can configure the `launch` and `context` properties in `capabilities`: +Vitest opens a single page to run all tests in the same file. You can configure the `launch` and `context` properties in `configs`: ```ts{9-10} [vitest.config.ts] import { defineConfig } from 'vitest/config' @@ -24,7 +24,7 @@ import { defineConfig } from 'vitest/config' export default defineConfig({ test: { browser: { - capabilities: [ + configs: [ { browser: 'firefox', launch: {}, @@ -52,7 +52,7 @@ export default defineConfig({ }) ``` -`providerOptions` is deprecated in favour of `capabilities`. +`providerOptions` is deprecated in favour of `configs`. ::: ## launch diff --git a/docs/guide/browser/webdriverio.md b/docs/guide/browser/webdriverio.md index 69aac76b25c9..15ec520e3988 100644 --- a/docs/guide/browser/webdriverio.md +++ b/docs/guide/browser/webdriverio.md @@ -20,7 +20,7 @@ Alternatively, you can also add it to `compilerOptions.types` field in your `tsc } ``` -Vitest opens a single page to run all tests in the same file. You can configure any property specified in `RemoteOptions` in `capabilities`: +Vitest opens a single page to run all tests in the same file. You can configure any property specified in `RemoteOptions` in `configs`: ```ts{9-12} [vitest.config.ts] import { defineConfig } from 'vitest/config' @@ -28,7 +28,7 @@ import { defineConfig } from 'vitest/config' export default defineConfig({ test: { browser: { - capabilities: [ + configs: [ { browser: 'chrome', capabilities: { @@ -57,7 +57,7 @@ export default defineConfig({ }) ``` -`providerOptions` is deprecated in favour of `capabilities`. +`providerOptions` is deprecated in favour of `configs`. ::: You can find most available options in the [WebdriverIO documentation](https://webdriver.io/docs/configuration/). Note that Vitest will ignore all test runner options because we only use `webdriverio`'s browser capabilities. diff --git a/packages/vitest/src/create/browser/creator.ts b/packages/vitest/src/create/browser/creator.ts index 55f6057ce958..239bfe2fdd42 100644 --- a/packages/vitest/src/create/browser/creator.ts +++ b/packages/vitest/src/create/browser/creator.ts @@ -267,7 +267,7 @@ async function generateWorkspaceFile(options: { ` enabled: true,`, ` provider: '${options.provider}',`, options.provider !== 'preview' && ` // ${getProviderDocsLink(options.provider)}`, - ` capabilities: [`, + ` configs: [`, ...options.browsers.map(browser => ` { browser: '${browser}' },`), ` ],`, ` },`, @@ -300,7 +300,7 @@ async function generateFrameworkConfigFile(options: { ` enabled: true,`, ` provider: '${options.provider}',`, options.provider !== 'preview' && ` // ${getProviderDocsLink(options.provider)}`, - ` capabilities: [`, + ` configs: [`, ...options.browsers.map(browser => ` { browser: '${browser}' },`), ` ],`, ` },`, diff --git a/packages/vitest/src/node/cli/cli-config.ts b/packages/vitest/src/node/cli/cli-config.ts index 39b8ede7f198..62416e12b611 100644 --- a/packages/vitest/src/node/cli/cli-config.ts +++ b/packages/vitest/src/node/cli/cli-config.ts @@ -416,7 +416,7 @@ export const cliOptionsConfig: VitestCLIOptions = { screenshotFailures: null, locators: null, testerHtmlPath: null, - capabilities: null, + configs: null, }, }, pool: { diff --git a/packages/vitest/src/node/config/resolveConfig.ts b/packages/vitest/src/node/config/resolveConfig.ts index e132db20718a..56f7fe0e1690 100644 --- a/packages/vitest/src/node/config/resolveConfig.ts +++ b/packages/vitest/src/node/config/resolveConfig.ts @@ -233,22 +233,18 @@ export function resolveConfig( const browser = resolved.browser if (browser.enabled) { - if (!browser.name && !browser.capabilities) { - throw new Error(`Vitest Browser Mode requires "browser.name" (deprecated) or "browser.capabilities" options, none were set.`) + if (!browser.name && !browser.configs) { + throw new Error(`Vitest Browser Mode requires "browser.name" (deprecated) or "browser.configs" options, none were set.`) } - if (browser.name && browser.capabilities) { - throw new Error(`Cannot use both "browser.name" and "browser.capabilities" options at the same time. Use only "browser.capabilities" instead.`) + if (browser.name && browser.configs) { + // --browser=chromium filters configs to a single one + browser.configs = browser.configs.filter(capability => capability.browser === browser.name) } - if (browser.capabilities && !browser.capabilities.length) { - throw new Error(`"browser.capabilities" was set in the config, but the array is empty. Define at least one browser capability.`) + if (browser.configs && !browser.configs.length) { + throw new Error(`"browser.configs" was set in the config, but the array is empty. Define at least one browser capability.`) } - - // TODO: don't throw if --project=chromium is passed filtering capabilities to a single one - // if (browser.provider === 'preview' && (browser.capabilities?.length || 0) > 1) { - // throw new Error(`Browser provider "preview" does not support multiple capabilities. Use "playwright" or "webdriverio" instead.`) - // } } // Browser-mode "Playwright + Chromium" only features: diff --git a/packages/vitest/src/node/types/browser.ts b/packages/vitest/src/node/types/browser.ts index b3b70695b23a..d5e5b54a267b 100644 --- a/packages/vitest/src/node/types/browser.ts +++ b/packages/vitest/src/node/types/browser.ts @@ -7,7 +7,7 @@ import type { ApiConfig, ProjectConfig } from './config' export interface BrowserProviderInitializationOptions { browser: string - options?: BrowserProviderOptions + options?: BrowserConfig } export interface CDPSession { @@ -67,7 +67,7 @@ type UnsupportedProperties = | 'benchmark' // TODO: document all options -export interface BrowserCapabilities extends BrowserProviderOptions, Omit, Pick { +export interface BrowserConfig extends BrowserProviderOptions, Omit, Pick { browser: string } @@ -81,14 +81,14 @@ export interface BrowserConfigOptions { /** * Name of the browser - * @deprecated use `capabilities` instead + * @deprecated use `configs` instead. if both are defined, this will filter `configs` by name. */ name?: string /** - * Configurations for different browsers + * Configurations for different browser setups */ - capabilities?: BrowserCapabilities[] + configs?: BrowserConfig[] /** * Browser provider @@ -106,7 +106,7 @@ export interface BrowserConfigOptions { * * @example * { playwright: { launch: { devtools: true } } - * @deprecated use `capabilities` instead + * @deprecated use `configs` instead */ providerOptions?: BrowserProviderOptions diff --git a/packages/vitest/src/node/workspace/resolveWorkspace.ts b/packages/vitest/src/node/workspace/resolveWorkspace.ts index 49d468826d51..b835ecdd8065 100644 --- a/packages/vitest/src/node/workspace/resolveWorkspace.ts +++ b/packages/vitest/src/node/workspace/resolveWorkspace.ts @@ -135,32 +135,26 @@ export function resolveBrowserWorkspace( resolvedProjects: TestProject[], ) { resolvedProjects.forEach((project) => { - const capabilities = project.config.browser.capabilities - if (!project.config.browser.enabled || !capabilities || capabilities.length === 0) { + const configs = project.config.browser.configs + if (!project.config.browser.enabled || !configs || configs.length === 0) { return } - const [firstCapability, ...restCapabilities] = capabilities + const [firstConfig, ...restConfigs] = configs project.config.name ||= project.config.name - ? `${project.config.name} (${firstCapability.browser})` - : firstCapability.browser - - if (project.config.browser.name) { - vitest.logger.warn( - withLabel('yellow', 'Vitest', `Browser name "${project.config.browser.name}" is ignored because it's overriden by the capabilities. To hide this warning, remove the "name" property from the browser configuration.`), - ) - } + ? `${project.config.name} (${firstConfig.browser})` + : firstConfig.browser if (project.config.browser.providerOptions) { vitest.logger.warn( - withLabel('yellow', 'Vitest', `"providerOptions"${project.config.name ? ` in "${project.config.name}" project` : ''} is ignored because it's overriden by the capabilities. To hide this warning, remove the "providerOptions" property from the browser configuration.`), + withLabel('yellow', 'Vitest', `"providerOptions"${project.config.name ? ` in "${project.config.name}" project` : ''} is ignored because it's overriden by the configs. To hide this warning, remove the "providerOptions" property from the browser configuration.`), ) } - project.config.browser.name = firstCapability.browser - project.config.browser.providerOptions = firstCapability + project.config.browser.name = firstConfig.browser + project.config.browser.providerOptions = firstConfig - restCapabilities.forEach(({ browser, ...capability }) => { + restConfigs.forEach(({ browser, ...capability }) => { // TODO: cover with tests // browser-only options const { @@ -191,6 +185,7 @@ export function resolveBrowserWorkspace( screenshotFailures: screenshotFailures ?? currentConfig.screenshotFailures, name: browser, providerOptions: capability, + configs: undefined, // projects cannot spawn more configs }, // TODO: should resolve, not merge/override } satisfies ResolvedConfig, overrideConfig) as ResolvedConfig From c737b3d4f4c1edb5b9b007bdc420c88a313616ab Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Wed, 4 Dec 2024 16:40:18 +0100 Subject: [PATCH 10/51] test: cover incorrect browser configs --- .../vitest/src/node/config/resolveConfig.ts | 8 +++-- test/config/test/failures.test.ts | 31 +++++++++++++++++++ 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/packages/vitest/src/node/config/resolveConfig.ts b/packages/vitest/src/node/config/resolveConfig.ts index 56f7fe0e1690..6861372785fa 100644 --- a/packages/vitest/src/node/config/resolveConfig.ts +++ b/packages/vitest/src/node/config/resolveConfig.ts @@ -237,13 +237,17 @@ export function resolveConfig( throw new Error(`Vitest Browser Mode requires "browser.name" (deprecated) or "browser.configs" options, none were set.`) } + const configs = browser.configs if (browser.name && browser.configs) { // --browser=chromium filters configs to a single one - browser.configs = browser.configs.filter(capability => capability.browser === browser.name) + browser.configs = browser.configs.filter(config_ => config_.browser === browser.name) } if (browser.configs && !browser.configs.length) { - throw new Error(`"browser.configs" was set in the config, but the array is empty. Define at least one browser capability.`) + throw new Error([ + `"browser.configs" was set in the config, but the array is empty. Define at least one browser config.`, + browser.name && configs?.length ? ` The "browser.name" was set to "${browser.name}" which filtered all configs (${configs.map(c => c.browser).join(', ')}). Did you mean to use another name?` : '', + ].join('')) } } diff --git a/test/config/test/failures.test.ts b/test/config/test/failures.test.ts index 1d98a7a1a673..cc22d44d95af 100644 --- a/test/config/test/failures.test.ts +++ b/test/config/test/failures.test.ts @@ -288,3 +288,34 @@ test('maxConcurrency 0 prints a warning', async () => { expect(ctx?.config.maxConcurrency).toBe(5) expect(stderr).toMatch('The option "maxConcurrency" cannot be set to 0. Using default value 5 instead.') }) + +test('browser.name or browser.configs are required', async () => { + const { stderr, exitCode } = await runVitestCli('--browser.enabled') + expect(exitCode).toBe(1) + expect(stderr).toMatch('Vitest Browser Mode requires "browser.name" (deprecated) or "browser.configs" options, none were set.') +}) + +test('browser.configs is empty', async () => { + const { stderr } = await runVitest({ + browser: { + enabled: true, + provider: 'playwright', + configs: [], + }, + }) + expect(stderr).toMatch('"browser.configs" was set in the config, but the array is empty. Define at least one browser config.') +}) + +test('browser.name filteres all browser.configs are required', async () => { + const { stderr } = await runVitest({ + browser: { + enabled: true, + name: 'chromium', + provider: 'playwright', + configs: [ + { browser: 'firefox' }, + ], + }, + }) + expect(stderr).toMatch('"browser.configs" was set in the config, but the array is empty. Define at least one browser config. The "browser.name" was set to "chromium" which filtered all configs (firefox). Did you mean to use another name?') +}) From 98c6d092e6ae3c2fe59399e223d8d92d455d6a6f Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Wed, 4 Dec 2024 16:40:42 +0100 Subject: [PATCH 11/51] fix: export BrowserConfig, providers still only get BrowserProviderConfig --- packages/vitest/src/node/types/browser.ts | 2 +- packages/vitest/src/node/types/config.ts | 2 +- packages/vitest/src/public/node.ts | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/vitest/src/node/types/browser.ts b/packages/vitest/src/node/types/browser.ts index d5e5b54a267b..c1b95e1b627c 100644 --- a/packages/vitest/src/node/types/browser.ts +++ b/packages/vitest/src/node/types/browser.ts @@ -7,7 +7,7 @@ import type { ApiConfig, ProjectConfig } from './config' export interface BrowserProviderInitializationOptions { browser: string - options?: BrowserConfig + options?: BrowserProviderOptions } export interface CDPSession { diff --git a/packages/vitest/src/node/types/config.ts b/packages/vitest/src/node/types/config.ts index eaa222dc429d..e9674a18052a 100644 --- a/packages/vitest/src/node/types/config.ts +++ b/packages/vitest/src/node/types/config.ts @@ -25,7 +25,7 @@ import type { Reporter } from './reporter' export type { CoverageOptions, ResolvedCoverageOptions } export type { BenchmarkUserOptions } export type { RuntimeConfig, SerializedConfig } from '../../runtime/config' -export type { BrowserConfigOptions, BrowserScript } from './browser' +export type { BrowserConfig, BrowserConfigOptions, BrowserScript } from './browser' export type { CoverageIstanbulOptions, CoverageV8Options } from './coverage' export type { SequenceHooks, SequenceSetupFiles } from '@vitest/runner' diff --git a/packages/vitest/src/public/node.ts b/packages/vitest/src/public/node.ts index 01e24ecbcffe..974f27a22446 100644 --- a/packages/vitest/src/public/node.ts +++ b/packages/vitest/src/public/node.ts @@ -52,6 +52,7 @@ export type { BrowserBuiltinProvider, BrowserCommand, BrowserCommandContext, + BrowserConfig, BrowserConfigOptions, BrowserOrchestrator, BrowserProvider, From 6d04b97df52b8f8f74c96356c8770ac692da1e75 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Wed, 4 Dec 2024 18:03:54 +0100 Subject: [PATCH 12/51] fix: make sure configs are correctly inherited --- packages/vitest/src/node/core.ts | 2 +- packages/vitest/src/node/project.ts | 12 ++ packages/vitest/src/node/types/browser.ts | 6 +- .../src/node/workspace/resolveWorkspace.ts | 119 +++++++++------ packages/vitest/src/public/node.ts | 2 +- test/config/test/browser-configs.test.ts | 141 ++++++++++++++++++ test/config/test/failures.test.ts | 30 ++++ 7 files changed, 261 insertions(+), 51 deletions(-) create mode 100644 test/config/test/browser-configs.test.ts diff --git a/packages/vitest/src/node/core.ts b/packages/vitest/src/node/core.ts index 0002e844b6cf..3ef33c9dd3ec 100644 --- a/packages/vitest/src/node/core.ts +++ b/packages/vitest/src/node/core.ts @@ -291,7 +291,7 @@ export class Vitest { this._workspaceConfigPath = workspaceConfigPath if (!workspaceConfigPath) { - return resolveBrowserWorkspace(this, [this._createRootProject()]) + return resolveBrowserWorkspace(this, new Set(), [this._createRootProject()]) } const workspaceModule = await this.runner.executeFile(workspaceConfigPath) as { diff --git a/packages/vitest/src/node/project.ts b/packages/vitest/src/node/project.ts index d13761ec8ebf..4b5b3d8d25b1 100644 --- a/packages/vitest/src/node/project.ts +++ b/packages/vitest/src/node/project.ts @@ -158,6 +158,12 @@ export class TestProject { if (!this._vite) { throw new Error('The server was not set. It means that `project.vite` was called before the Vite server was established.') } + // checking it once should be enough + Object.defineProperty(this, 'vite', { + configurable: true, + writable: true, + value: this._vite, + }) return this._vite } @@ -168,6 +174,12 @@ export class TestProject { if (!this._config) { throw new Error('The config was not set. It means that `project.config` was called before the Vite server was established.') } + // checking it once should be enough + Object.defineProperty(this, 'config', { + configurable: true, + writable: true, + value: this._config, + }) return this._config } diff --git a/packages/vitest/src/node/types/browser.ts b/packages/vitest/src/node/types/browser.ts index c1b95e1b627c..dbda7b7c5445 100644 --- a/packages/vitest/src/node/types/browser.ts +++ b/packages/vitest/src/node/types/browser.ts @@ -66,8 +66,10 @@ type UnsupportedProperties = | 'server' | 'benchmark' -// TODO: document all options -export interface BrowserConfig extends BrowserProviderOptions, Omit, Pick { +export interface BrowserConfig + extends BrowserProviderOptions, + Omit, + Pick { browser: string } diff --git a/packages/vitest/src/node/workspace/resolveWorkspace.ts b/packages/vitest/src/node/workspace/resolveWorkspace.ts index b835ecdd8065..7e5ebd05b711 100644 --- a/packages/vitest/src/node/workspace/resolveWorkspace.ts +++ b/packages/vitest/src/node/workspace/resolveWorkspace.ts @@ -1,5 +1,5 @@ import type { Vitest } from '../core' -import type { ResolvedConfig, TestProjectConfiguration, UserConfig, UserWorkspaceConfig } from '../types/config' +import type { BrowserConfig, ResolvedConfig, TestProjectConfiguration, UserConfig, UserWorkspaceConfig } from '../types/config' import { existsSync, promises as fs } from 'node:fs' import os from 'node:os' import { limitConcurrency } from '@vitest/runner/utils' @@ -97,7 +97,7 @@ export async function resolveWorkspace( // pretty rare case - the glob didn't match anything and there are no inline configs if (!projectPromises.length) { - return resolveBrowserWorkspace(vitest, [vitest._createRootProject()]) + return resolveBrowserWorkspace(vitest, new Set(), [vitest._createRootProject()]) } const resolvedProjects = await Promise.all(projectPromises) @@ -127,76 +127,101 @@ export async function resolveWorkspace( names.add(name) } - return resolveBrowserWorkspace(vitest, resolvedProjects) + return resolveBrowserWorkspace(vitest, names, resolvedProjects) } export function resolveBrowserWorkspace( vitest: Vitest, + names: Set, resolvedProjects: TestProject[], ) { + const newConfigs: [project: TestProject, config: ResolvedConfig][] = [] + resolvedProjects.forEach((project) => { const configs = project.config.browser.configs if (!project.config.browser.enabled || !configs || configs.length === 0) { return } const [firstConfig, ...restConfigs] = configs + const originalName = project.config.name - project.config.name ||= project.config.name - ? `${project.config.name} (${firstConfig.browser})` + const newName = originalName + ? `${originalName} (${firstConfig.browser})` : firstConfig.browser + if (names.has(newName)) { + throw new Error(`Cannot redefine the project name for a nameless project. The project name "${firstConfig.browser}" was already defined. All projects in a workspace should have unique names. Make sure your configuration is correct.`) + } + names.add(newName) if (project.config.browser.providerOptions) { vitest.logger.warn( - withLabel('yellow', 'Vitest', `"providerOptions"${project.config.name ? ` in "${project.config.name}" project` : ''} is ignored because it's overriden by the configs. To hide this warning, remove the "providerOptions" property from the browser configuration.`), + withLabel('yellow', 'Vitest', `"providerOptions"${originalName ? ` in "${originalName}" project` : ''} is ignored because it's overriden by the configs. To hide this warning, remove the "providerOptions" property from the browser configuration.`), ) } - project.config.browser.name = firstConfig.browser - project.config.browser.providerOptions = firstConfig - - restConfigs.forEach(({ browser, ...capability }) => { - // TODO: cover with tests - // browser-only options - const { - locators, - viewport, - testerHtmlPath, - screenshotDirectory, - screenshotFailures, - // @ts-expect-error remove just in case - browser: _browser, - // TODO: need a lot of tests - ...overrideConfig - } = capability - const currentConfig = project.config.browser - const clonedConfig = mergeConfig({ - ...project.config, - name: project.config.name ? `${project.config} (${browser})` : browser, - browser: { - ...project.config.browser, - locators: locators - ? { - testIdAttribute: locators.testIdAttribute ?? currentConfig.locators.testIdAttribute, - } - : project.config.browser.locators, - viewport: viewport ?? currentConfig.viewport, - testerHtmlPath: testerHtmlPath ?? currentConfig.testerHtmlPath, - screenshotDirectory: screenshotDirectory ?? currentConfig.screenshotDirectory, - screenshotFailures: screenshotFailures ?? currentConfig.screenshotFailures, - name: browser, - providerOptions: capability, - configs: undefined, // projects cannot spawn more configs - }, - // TODO: should resolve, not merge/override - } satisfies ResolvedConfig, overrideConfig) as ResolvedConfig - const clone = TestProject._cloneBrowserProject(project, clonedConfig) - - resolvedProjects.push(clone) + restConfigs.forEach((config) => { + const browser = config.browser + const name = config.name + const newName = name || (originalName ? `${originalName} (${browser})` : browser) + if (names.has(newName)) { + throw new Error( + [ + `Cannot define a nested project for a ${browser} browser. The project name "${newName}" was already defined. `, + 'If you have multiple configs for the same browser, make sure to define a custom "name". ', + 'All projects in a workspace should have unique names. Make sure your configuration is correct.', + ].join(''), + ) + } + names.add(newName) + const clonedConfig = cloneConfig(project, config) + clonedConfig.name = newName + newConfigs.push([project, clonedConfig]) }) + + Object.assign(project.config, cloneConfig(project, firstConfig)) + project.config.name = newName + }) + newConfigs.forEach(([project, clonedConfig]) => { + const clone = TestProject._cloneBrowserProject(project, clonedConfig) + resolvedProjects.push(clone) }) return resolvedProjects } +function cloneConfig(project: TestProject, { browser, ...config }: BrowserConfig) { + const { + locators, + viewport, + testerHtmlPath, + screenshotDirectory, + screenshotFailures, + // @ts-expect-error remove just in case + browser: _browser, + name, + ...overrideConfig + } = config + const currentConfig = project.config.browser + return mergeConfig({ + ...project.config, + browser: { + ...project.config.browser, + locators: locators + ? { + testIdAttribute: locators.testIdAttribute ?? currentConfig.locators.testIdAttribute, + } + : project.config.browser.locators, + viewport: viewport ?? currentConfig.viewport, + testerHtmlPath: testerHtmlPath ?? currentConfig.testerHtmlPath, + screenshotDirectory: screenshotDirectory ?? currentConfig.screenshotDirectory, + screenshotFailures: screenshotFailures ?? currentConfig.screenshotFailures, + name: browser, + providerOptions: config, + configs: undefined, // projects cannot spawn more configs + }, + // TODO: should resolve, not merge/override + } satisfies ResolvedConfig, overrideConfig) as ResolvedConfig +} + async function resolveTestProjectConfigs( vitest: Vitest, workspaceConfigPath: string | undefined, diff --git a/packages/vitest/src/public/node.ts b/packages/vitest/src/public/node.ts index 974f27a22446..5e2d220dc424 100644 --- a/packages/vitest/src/public/node.ts +++ b/packages/vitest/src/public/node.ts @@ -8,7 +8,7 @@ export const version = Vitest.version export { parseCLI } from '../node/cli/cac' export { startVitest } from '../node/cli/cli-api' export { resolveApiServerConfig, resolveConfig } from '../node/config/resolveConfig' -export type { Vitest } from '../node/core' +export type { Vitest, VitestOptions } from '../node/core' export { createVitest } from '../node/create' export { GitNotFoundError, FilesNotFoundError as TestsNotFoundError } from '../node/errors' export type { GlobalSetupContext } from '../node/globalSetup' diff --git a/test/config/test/browser-configs.test.ts b/test/config/test/browser-configs.test.ts new file mode 100644 index 000000000000..d0ac90eecc3a --- /dev/null +++ b/test/config/test/browser-configs.test.ts @@ -0,0 +1,141 @@ +import type { ViteUserConfig } from 'vitest/config' +import type { UserConfig, VitestOptions } from 'vitest/node' +import { expect, test } from 'vitest' +import { createVitest } from 'vitest/node' + +async function vitest(cliOptions: UserConfig, configValue: UserConfig = {}, viteConfig: ViteUserConfig = {}, vitestOptions: VitestOptions = {}) { + return await createVitest('test', { ...cliOptions, watch: false }, { ...viteConfig, test: configValue as any }, vitestOptions) +} + +test('assignes names as browsers', async () => { + const { projects } = await vitest({ + browser: { + enabled: true, + configs: [ + { browser: 'chromium' }, + { browser: 'firefox' }, + { browser: 'webkit' }, + ], + }, + }) + expect(projects.map(p => p.name)).toEqual([ + 'chromium', + 'firefox', + 'webkit', + ]) +}) + +test('assignes names as browsers in a custom project', async () => { + const { projects } = await vitest({ + workspace: [ + { + test: { + name: 'custom', + browser: { + enabled: true, + configs: [ + { browser: 'chromium' }, + { browser: 'firefox' }, + { browser: 'webkit' }, + { browser: 'webkit', name: 'hello-world' }, + ], + }, + }, + }, + ], + }) + expect(projects.map(p => p.name)).toEqual([ + 'custom (chromium)', + 'custom (firefox)', + 'custom (webkit)', + 'hello-world', + ]) +}) + +test.only('inherits browser options', async () => { + const { projects } = await vitest({ + setupFiles: ['/test/setup.ts'], + provide: { + browser: true, + }, + browser: { + enabled: true, + headless: true, + screenshotFailures: false, + testerHtmlPath: '/custom-path.html', + screenshotDirectory: '/custom-directory', + fileParallelism: false, + viewport: { + width: 300, + height: 900, + }, + locators: { + testIdAttribute: 'data-tid', + }, + configs: [ + { + browser: 'chromium', + screenshotFailures: true, + }, + { + browser: 'firefox', + screenshotFailures: true, + locators: { + testIdAttribute: 'data-custom', + }, + viewport: { + width: 900, + height: 300, + }, + testerHtmlPath: '/custom-overriden-path.html', + screenshotDirectory: '/custom-overriden-directory', + }, + ], + }, + }) + expect(projects.map(p => p.config)).toMatchObject([ + { + name: 'chromium', + setupFiles: ['/test/setup.ts'], + provide: { + browser: true, + }, + browser: { + enabled: true, + headless: true, + screenshotFailures: true, + screenshotDirectory: '/custom-directory', + viewport: { + width: 300, + height: 900, + }, + locators: { + testIdAttribute: 'data-tid', + }, + fileParallelism: false, + testerHtmlPath: '/custom-path.html', + }, + }, + { + name: 'firefox', + setupFiles: ['/test/setup.ts'], + provide: { + browser: true, + }, + browser: { + enabled: true, + headless: true, + screenshotFailures: true, + viewport: { + width: 900, + height: 300, + }, + screenshotDirectory: '/custom-overriden-directory', + locators: { + testIdAttribute: 'data-custom', + }, + testerHtmlPath: '/custom-overriden-path.html', + }, + }, + ]) +}) diff --git a/test/config/test/failures.test.ts b/test/config/test/failures.test.ts index cc22d44d95af..c855ce34cfcb 100644 --- a/test/config/test/failures.test.ts +++ b/test/config/test/failures.test.ts @@ -319,3 +319,33 @@ test('browser.name filteres all browser.configs are required', async () => { }) expect(stderr).toMatch('"browser.configs" was set in the config, but the array is empty. Define at least one browser config. The "browser.name" was set to "chromium" which filtered all configs (firefox). Did you mean to use another name?') }) + +test('browser.configs throws an error if no custom name is provided', async () => { + const { stderr } = await runVitest({ + browser: { + enabled: true, + provider: 'playwright', + configs: [ + { browser: 'firefox' }, + { browser: 'firefox' }, + ], + }, + }) + expect(stderr).toMatch('Cannot define a nested project for a firefox browser. The project name "firefox" was already defined. If you have multiple configs for the same browser, make sure to define a custom "name". All projects in a workspace should have unique names. Make sure your configuration is correct.') +}) + +test('browser.configs throws an error if no custom name is provided, but the config name is inherited', async () => { + const { stderr } = await runVitest({ + name: 'custom', + browser: { + enabled: true, + provider: 'playwright', + configs: [ + { browser: 'firefox' }, + { browser: 'firefox' }, + ], + }, + }) + expect(stderr).toMatch('Cannot define a nested project for a firefox browser. The project name "custom (firefox)" was already defined. If you have multiple configs for the same browser, make sure to define a custom "name". All projects in a workspace should have unique names. Make sure your configuration is correct.') +}) + From 8102f1a287515a2e1a01094dd36979dc9d48c606 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Wed, 4 Dec 2024 18:12:26 +0100 Subject: [PATCH 13/51] test: add a fail for a duplcate name --- test/config/test/failures.test.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/test/config/test/failures.test.ts b/test/config/test/failures.test.ts index c855ce34cfcb..e645d8207a9e 100644 --- a/test/config/test/failures.test.ts +++ b/test/config/test/failures.test.ts @@ -349,3 +349,22 @@ test('browser.configs throws an error if no custom name is provided, but the con expect(stderr).toMatch('Cannot define a nested project for a firefox browser. The project name "custom (firefox)" was already defined. If you have multiple configs for the same browser, make sure to define a custom "name". All projects in a workspace should have unique names. Make sure your configuration is correct.') }) +test('throws an error if name conflicts with a workspace name', async () => { + const { stderr } = await runVitest({ + workspace: [ + { test: { name: '1 (firefox)' } }, + { + test: { + browser: { + enabled: true, + provider: 'playwright', + configs: [ + { browser: 'firefox' }, + ], + }, + }, + }, + ], + }) + expect(stderr).toMatch('Cannot redefine the project name for a nameless project. The project name "firefox" was already defined. All projects in a workspace should have unique names. Make sure your configuration is correct.') +}) From 5da2a78406f5f3fc84379afe8eeed2ed7a4e28e7 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Wed, 4 Dec 2024 18:16:34 +0100 Subject: [PATCH 14/51] test: remove .only --- test/config/test/browser-configs.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/config/test/browser-configs.test.ts b/test/config/test/browser-configs.test.ts index d0ac90eecc3a..4440b094355a 100644 --- a/test/config/test/browser-configs.test.ts +++ b/test/config/test/browser-configs.test.ts @@ -52,7 +52,7 @@ test('assignes names as browsers in a custom project', async () => { ]) }) -test.only('inherits browser options', async () => { +test('inherits browser options', async () => { const { projects } = await vitest({ setupFiles: ['/test/setup.ts'], provide: { From 06e690d47ac7d4d4ac5063ce24fb955a17386543 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Wed, 4 Dec 2024 18:36:20 +0100 Subject: [PATCH 15/51] chore: fix type issue --- packages/browser/providers/webdriverio.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/browser/providers/webdriverio.d.ts b/packages/browser/providers/webdriverio.d.ts index 3675cd995b47..1c4a2b75bfdb 100644 --- a/packages/browser/providers/webdriverio.d.ts +++ b/packages/browser/providers/webdriverio.d.ts @@ -2,7 +2,7 @@ import type { RemoteOptions, ClickOptions, DragAndDropOptions } from 'webdriveri import '../matchers.js' declare module 'vitest/node' { - interface BrowserProviderOptions extends RemoteOptions {} + interface BrowserProviderOptions extends Partial {} export interface UserEventClickOptions extends ClickOptions {} From e935ece454fea958c72ecd842f0b6b05ac03f941 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Mon, 9 Dec 2024 14:08:33 +0100 Subject: [PATCH 16/51] chore: fix typecheck --- test/config/test/browser-configs.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/config/test/browser-configs.test.ts b/test/config/test/browser-configs.test.ts index 4440b094355a..aa63e0398375 100644 --- a/test/config/test/browser-configs.test.ts +++ b/test/config/test/browser-configs.test.ts @@ -57,7 +57,7 @@ test('inherits browser options', async () => { setupFiles: ['/test/setup.ts'], provide: { browser: true, - }, + } as any, browser: { enabled: true, headless: true, @@ -99,7 +99,7 @@ test('inherits browser options', async () => { setupFiles: ['/test/setup.ts'], provide: { browser: true, - }, + } as any, browser: { enabled: true, headless: true, @@ -121,7 +121,7 @@ test('inherits browser options', async () => { setupFiles: ['/test/setup.ts'], provide: { browser: true, - }, + } as any, browser: { enabled: true, headless: true, From ae3586758407f217b9a954f8d6f0fbc56ea9b1ae Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Mon, 9 Dec 2024 14:15:27 +0100 Subject: [PATCH 17/51] chore: test --- test/config/test/browser-configs.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/test/config/test/browser-configs.test.ts b/test/config/test/browser-configs.test.ts index aa63e0398375..f171019241eb 100644 --- a/test/config/test/browser-configs.test.ts +++ b/test/config/test/browser-configs.test.ts @@ -134,6 +134,7 @@ test('inherits browser options', async () => { locators: { testIdAttribute: 'data-custom', }, + testerHtmlPath: '/custom-overriden-path.html', }, }, From 4d980695f1c7a9eef6f5572055685f951632c884 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Mon, 9 Dec 2024 14:23:05 +0100 Subject: [PATCH 18/51] chore: test --- test/config/test/browser-configs.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/test/config/test/browser-configs.test.ts b/test/config/test/browser-configs.test.ts index f171019241eb..aa63e0398375 100644 --- a/test/config/test/browser-configs.test.ts +++ b/test/config/test/browser-configs.test.ts @@ -134,7 +134,6 @@ test('inherits browser options', async () => { locators: { testIdAttribute: 'data-custom', }, - testerHtmlPath: '/custom-overriden-path.html', }, }, From 3cc6dc5a6b862f1701c728363fbc47b2106a1756 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Mon, 9 Dec 2024 14:41:50 +0100 Subject: [PATCH 19/51] chore: remove ts directive --- test/coverage-test/test/isolation.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/test/coverage-test/test/isolation.test.ts b/test/coverage-test/test/isolation.test.ts index 7a84b419b461..6100dbfee4e8 100644 --- a/test/coverage-test/test/isolation.test.ts +++ b/test/coverage-test/test/isolation.test.ts @@ -31,7 +31,6 @@ for (const isolate of [true, false]) { reporter: ['json', 'html'], }, - // @ts-expect-error -- merged in runVitest browser: { isolate, }, From 5006f1b676887bb3a327cf23137c8cb314d18163 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Tue, 10 Dec 2024 11:31:32 +0100 Subject: [PATCH 20/51] fix: expose browser sessions --- packages/vitest/src/node/core.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/vitest/src/node/core.ts b/packages/vitest/src/node/core.ts index d14202f999c1..ed67851af28a 100644 --- a/packages/vitest/src/node/core.ts +++ b/packages/vitest/src/node/core.ts @@ -24,6 +24,7 @@ import { defaultBrowserPort, workspacesFiles as workspaceFiles } from '../consta import { getCoverageProvider } from '../integrations/coverage' import { distDir } from '../paths' import { wildcardPatternToRegExp } from '../utils/base' +import { BrowserSessions } from './browser/sessions' import { VitestCache } from './cache' import { resolveConfig } from './config/resolveConfig' import { FilesNotFoundError } from './errors' @@ -88,6 +89,7 @@ export class Vitest { /** @internal */ coreWorkspaceProject: TestProject | undefined /** @internal */ resolvedProjects: TestProject[] = [] /** @internal */ _browserLastPort = defaultBrowserPort + /** @internal */ _browserSessions = new BrowserSessions() /** @internal */ _options: UserConfig = {} /** @internal */ reporters: Reporter[] = undefined! /** @internal */ vitenode: ViteNodeServer = undefined! From 4844839b0ca5cebf252b4a232e68a88acd2ec68b Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Tue, 10 Dec 2024 11:39:23 +0100 Subject: [PATCH 21/51] docs: fix version --- docs/guide/browser/playwright.md | 6 +++--- docs/guide/browser/webdriverio.md | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/guide/browser/playwright.md b/docs/guide/browser/playwright.md index 988a240aa612..14a70abf4cec 100644 --- a/docs/guide/browser/playwright.md +++ b/docs/guide/browser/playwright.md @@ -37,7 +37,7 @@ export default defineConfig({ ``` ::: warning -Before Vitest 2.2, these options were located on `test.browser.providerOptions` property: +Before Vitest 3, these options were located on `test.browser.providerOptions` property: ```ts [vitest.config.ts] export default defineConfig({ @@ -79,9 +79,9 @@ Vitest awlays sets `ignoreHTTPSErrors` to `true` in case your server is served v It is also recommended to use [`test.browser.viewport`](/config/#browser-headless) instead of specifying it here as it will be lost when tests are running in headless mode. ::: -## `actionTimeout` 2.2.0 +## `actionTimeout` 3.0.0 -- **Default:** no timeout, 1 second before 2.2.0 +- **Default:** no timeout, 1 second before 3.0.0 This value configures the default timeout it takes for Playwright to wait until all accessibility checks pass and [the action](/guide/browser/interactivity-api) is actually done. diff --git a/docs/guide/browser/webdriverio.md b/docs/guide/browser/webdriverio.md index 15ec520e3988..e8b446838f14 100644 --- a/docs/guide/browser/webdriverio.md +++ b/docs/guide/browser/webdriverio.md @@ -43,7 +43,7 @@ export default defineConfig({ ``` ::: warning -Before Vitest 2.2, these options were located on `test.browser.providerOptions` property: +Before Vitest 3, these options were located on `test.browser.providerOptions` property: ```ts [vitest.config.ts] export default defineConfig({ From 4fdaa4561dcf7f1fcdedaf3d40c843d99daa07b5 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Tue, 10 Dec 2024 12:04:35 +0100 Subject: [PATCH 22/51] feat: add a confirmation flag if running in headed mode --- packages/vitest/src/node/packageInstaller.ts | 2 +- packages/vitest/src/node/reporters/base.ts | 4 +-- .../src/node/workspace/resolveWorkspace.ts | 31 ++++++++++++++++++- packages/vitest/src/utils/env.ts | 3 ++ test/browser/vitest.config.mts | 4 ++- 5 files changed, 39 insertions(+), 5 deletions(-) diff --git a/packages/vitest/src/node/packageInstaller.ts b/packages/vitest/src/node/packageInstaller.ts index 4189e88347d9..f1a06f8d80c9 100644 --- a/packages/vitest/src/node/packageInstaller.ts +++ b/packages/vitest/src/node/packageInstaller.ts @@ -46,7 +46,7 @@ export class VitestPackageInstaller { } const prompts = await import('prompts') - const { install } = await prompts.prompt({ + const { install } = await prompts.default({ type: 'confirm', name: 'install', message: c.reset(`Do you want to install ${c.green(dependency)}?`), diff --git a/packages/vitest/src/node/reporters/base.ts b/packages/vitest/src/node/reporters/base.ts index e4c9d42abb2d..4e45efedba3e 100644 --- a/packages/vitest/src/node/reporters/base.ts +++ b/packages/vitest/src/node/reporters/base.ts @@ -8,7 +8,7 @@ import { toArray } from '@vitest/utils' import { parseStacktrace } from '@vitest/utils/source-map' import { relative } from 'pathe' import c from 'tinyrainbow' -import { isCI, isDeno, isNode } from '../../utils/env' +import { isTTY } from '../../utils/env' import { hasFailedSnapshot } from '../../utils/tasks' import { F_CHECK, F_POINTER, F_RIGHT } from './renderers/figures' import { countTestErrors, divider, formatProjectName, formatTime, formatTimeString, getStateString, getStateSymbol, padSummaryTitle, renderSnapshotSummary, taskFail, withLabel } from './renderers/utils' @@ -34,7 +34,7 @@ export abstract class BaseReporter implements Reporter { private _timeStart = formatTimeString(new Date()) constructor(options: BaseOptions = {}) { - this.isTTY = options.isTTY ?? ((isNode || isDeno) && process.stdout?.isTTY && !isCI) + this.isTTY = options.isTTY ?? isTTY } onInit(ctx: Vitest) { diff --git a/packages/vitest/src/node/workspace/resolveWorkspace.ts b/packages/vitest/src/node/workspace/resolveWorkspace.ts index 3dc0ae970312..56324a5153b0 100644 --- a/packages/vitest/src/node/workspace/resolveWorkspace.ts +++ b/packages/vitest/src/node/workspace/resolveWorkspace.ts @@ -7,6 +7,7 @@ import fg from 'fast-glob' import { dirname, relative, resolve } from 'pathe' import { mergeConfig } from 'vite' import { configFiles as defaultConfigFiles } from '../../constants' +import { isTTY } from '../../utils/env' import { initializeProject, TestProject } from '../project' import { withLabel } from '../reporters/renderers/utils' import { isDynamicPattern } from './fast-glob-pattern' @@ -130,7 +131,7 @@ export async function resolveWorkspace( return resolveBrowserWorkspace(vitest, names, resolvedProjects) } -export function resolveBrowserWorkspace( +export async function resolveBrowserWorkspace( vitest: Vitest, names: Set, resolvedProjects: TestProject[], @@ -185,6 +186,34 @@ export function resolveBrowserWorkspace( const clone = TestProject._cloneBrowserProject(project, clonedConfig) resolvedProjects.push(clone) }) + + const headedBrowserProjects = resolvedProjects.filter((project) => { + return project.config.browser.enabled && !project.config.browser.headless + }) + if (headedBrowserProjects.length > 1) { + const message = [ + `Found multiple projects that run browser tests in headed mode: "${headedBrowserProjects.map(p => p.name).join('", "')}".`, + ` Vitest cannot run multiple headed browsers at the same time.`, + ].join('') + if (!isTTY) { + throw new Error(`${message} Please, filter projects with --browser=name or --project=name flag or run tests with "headless: true" option.`) + } + const prompts = await import('prompts') + const { projectName } = await prompts.default({ + type: 'select', + name: 'projectName', + choices: headedBrowserProjects.map(project => ({ + title: project.name, + value: project.name, + })), + message: `${message} Select a single project to run or cancel and run tests with "headless: true" option. Note that you can also start tests with --browser=name or --project=name flag.`, + }) + if (!projectName) { + throw new Error('The test run was aborted.') + } + return resolvedProjects.filter(project => project.name === projectName) + } + return resolvedProjects } diff --git a/packages/vitest/src/utils/env.ts b/packages/vitest/src/utils/env.ts index 4ecfcef7e29f..29b93231f66a 100644 --- a/packages/vitest/src/utils/env.ts +++ b/packages/vitest/src/utils/env.ts @@ -1,3 +1,5 @@ +import { isCI } from 'std-env' + export const isNode: boolean = typeof process < 'u' && typeof process.stdout < 'u' @@ -9,4 +11,5 @@ export const isDeno: boolean && process.versions?.deno !== undefined export const isWindows = (isNode || isDeno) && process.platform === 'win32' export const isBrowser: boolean = typeof window !== 'undefined' +export const isTTY: boolean = ((isNode || isDeno) && process.stdout?.isTTY && !isCI) export { isCI, provider as stdProvider } from 'std-env' diff --git a/test/browser/vitest.config.mts b/test/browser/vitest.config.mts index d252d3c91e51..5baef26f7059 100644 --- a/test/browser/vitest.config.mts +++ b/test/browser/vitest.config.mts @@ -38,8 +38,10 @@ export default defineConfig({ snapshotEnvironment: './custom-snapshot-env.ts', browser: { enabled: true, - name: browser, headless: false, + configs: [ + { browser }, + ], provider, isolate: false, testerScripts: [ From 72cd39c0154c050f788119e9121bd30e2f9e8e69 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Tue, 10 Dec 2024 14:26:28 +0100 Subject: [PATCH 23/51] fix: pass project id so the preview provider can work without queries --- packages/browser/src/client/client.ts | 2 +- packages/browser/src/node/rpc.ts | 6 +++--- packages/vitest/src/node/packageInstaller.ts | 6 ++---- test/config/test/failures.test.ts | 16 ++++++++++++++++ test/test-utils/index.ts | 12 ++++++++---- 5 files changed, 30 insertions(+), 12 deletions(-) diff --git a/packages/browser/src/client/client.ts b/packages/browser/src/client/client.ts index 35b933fec6a6..b6fa768adcb1 100644 --- a/packages/browser/src/client/client.ts +++ b/packages/browser/src/client/client.ts @@ -14,7 +14,7 @@ export const RPC_ID : getBrowserState().testerId export const ENTRY_URL = `${ location.protocol === 'https:' ? 'wss:' : 'ws:' -}//${HOST}/__vitest_browser_api__?type=${PAGE_TYPE}&rpcId=${RPC_ID}&sessionId=${getBrowserState().sessionId}` +}//${HOST}/__vitest_browser_api__?type=${PAGE_TYPE}&rpcId=${RPC_ID}&sessionId=${getBrowserState().sessionId}&projectName=${getBrowserState().config.name || ''}` let setCancel = (_: CancelReason) => {} export const onCancel = new Promise((resolve) => { diff --git a/packages/browser/src/node/rpc.ts b/packages/browser/src/node/rpc.ts index ae5e0aaf4b94..df04bddf63db 100644 --- a/packages/browser/src/node/rpc.ts +++ b/packages/browser/src/node/rpc.ts @@ -36,16 +36,16 @@ export function setupBrowserRpc(server: BrowserServer) { const type = searchParams.get('type') const sessionId = searchParams.get('sessionId') ?? '' const rpcId = searchParams.get('rpcId') - const session = vitest._browserSessions.getSession(sessionId) + const projectName = searchParams.get('projectName') if (type !== 'tester' && type !== 'orchestrator') { throw new Error(`[vitest] Type query in ${request.url} is invalid. Type should be either "tester" or "orchestrator".`) } - if (!session || !sessionId || !rpcId) { + if (!sessionId || !rpcId || !projectName) { throw new Error(`[vitest] Invalid URL ${request.url}. "sessionId" and "rpcId" are required.`) } - const project = session.project + const project = vitest.getProjectByName(searchParams.get('projectName') || '') wss.handleUpgrade(request, socket, head, (ws) => { wss.emit('connection', ws, request) diff --git a/packages/vitest/src/node/packageInstaller.ts b/packages/vitest/src/node/packageInstaller.ts index f1a06f8d80c9..7a443681e092 100644 --- a/packages/vitest/src/node/packageInstaller.ts +++ b/packages/vitest/src/node/packageInstaller.ts @@ -2,7 +2,7 @@ import { createRequire } from 'node:module' import url from 'node:url' import { isPackageExists } from 'local-pkg' import c from 'tinyrainbow' -import { isCI } from '../utils/env' +import { isTTY } from '../utils/env' const __dirname = url.fileURLToPath(new URL('.', import.meta.url)) @@ -31,8 +31,6 @@ export class VitestPackageInstaller { return true } - const promptInstall = !isCI && process.stdout.isTTY - process.stderr.write( c.red( `${c.inverse( @@ -41,7 +39,7 @@ export class VitestPackageInstaller { ), ) - if (!promptInstall) { + if (!isTTY) { return false } diff --git a/test/config/test/failures.test.ts b/test/config/test/failures.test.ts index 415f49bb079f..6a552a9ba093 100644 --- a/test/config/test/failures.test.ts +++ b/test/config/test/failures.test.ts @@ -369,6 +369,22 @@ test('throws an error if name conflicts with a workspace name', async () => { expect(stderr).toMatch('Cannot redefine the project name for a nameless project. The project name "firefox" was already defined. All projects in a workspace should have unique names. Make sure your configuration is correct.') }) +test('throws an error if several browsers are headed in nonTTY mode', async () => { + const { stderr } = await runVitest({ + browser: { + enabled: true, + provider: 'playwright', + headless: false, + configs: [ + { browser: 'chromium' }, + { browser: 'firefox' }, + ], + }, + }) + expect(stderr).toContain('Found multiple projects that run browser tests in headed mode: "chromium", "firefox"') + expect(stderr).toContain('Please, filter projects with --browser=name or --project=name flag or run tests with "headless: true" option') +}) + test('non existing project name will throw', async () => { const { stderr } = await runVitest({ project: 'non-existing-project' }) expect(stderr).toMatch('No projects matched the filter "non-existing-project".') diff --git a/test/test-utils/index.ts b/test/test-utils/index.ts index cf4eb1e793e9..85b4cbf4c0f2 100644 --- a/test/test-utils/index.ts +++ b/test/test-utils/index.ts @@ -134,7 +134,11 @@ export async function runVitest( } } -export async function runCli(command: string, _options?: Partial | string, ...args: string[]) { +interface CliOptions extends Partial { + earlyReturn?: boolean +} + +export async function runCli(command: string, _options?: CliOptions | string, ...args: string[]) { let options = _options if (typeof _options === 'string') { @@ -172,7 +176,7 @@ export async function runCli(command: string, _options?: Partial | stri await isDone }) - if (args.includes('--inspect') || args.includes('--inspect-brk')) { + if ((options as CliOptions)?.earlyReturn || args.includes('--inspect') || args.includes('--inspect-brk')) { return output() } @@ -192,12 +196,12 @@ export async function runCli(command: string, _options?: Partial | stri return output() } -export async function runVitestCli(_options?: Partial | string, ...args: string[]) { +export async function runVitestCli(_options?: CliOptions | string, ...args: string[]) { process.env.VITE_TEST_WATCHER_DEBUG = 'true' return runCli('vitest', _options, ...args) } -export async function runViteNodeCli(_options?: Partial | string, ...args: string[]) { +export async function runViteNodeCli(_options?: CliOptions | string, ...args: string[]) { process.env.VITE_TEST_WATCHER_DEBUG = 'true' const { vitest, ...rest } = await runCli('vite-node', _options, ...args) From eae4fd4aa82304576d9ed8e3dd5b8d03823b7a4b Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Tue, 10 Dec 2024 14:33:02 +0100 Subject: [PATCH 24/51] fix: projectName is not null --- packages/browser/src/node/rpc.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/browser/src/node/rpc.ts b/packages/browser/src/node/rpc.ts index df04bddf63db..930da3a093f1 100644 --- a/packages/browser/src/node/rpc.ts +++ b/packages/browser/src/node/rpc.ts @@ -41,11 +41,15 @@ export function setupBrowserRpc(server: BrowserServer) { if (type !== 'tester' && type !== 'orchestrator') { throw new Error(`[vitest] Type query in ${request.url} is invalid. Type should be either "tester" or "orchestrator".`) } - if (!sessionId || !rpcId || !projectName) { - throw new Error(`[vitest] Invalid URL ${request.url}. "sessionId" and "rpcId" are required.`) + if (!sessionId || !rpcId || projectName == null) { + throw new Error(`[vitest] Invalid URL ${request.url}. "projectName", "sessionId" and "rpcId" are required.`) } - const project = vitest.getProjectByName(searchParams.get('projectName') || '') + const project = vitest.getProjectByName(projectName) + + if (!project) { + throw new Error(`[vitest] Project "${projectName}" not found.`) + } wss.handleUpgrade(request, socket, head, (ws) => { wss.emit('connection', ws, request) From 03bdf64a95a2943d7a3e3366a0f30e0f51caf256 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Tue, 10 Dec 2024 15:25:36 +0100 Subject: [PATCH 25/51] docs: add `browse.configs` docs --- docs/.vitepress/config.ts | 4 + docs/config/index.md | 212 +----------- docs/guide/browser/config.md | 311 ++++++++++++++++++ docs/guide/browser/index.md | 31 +- docs/guide/migration.md | 25 ++ .../src/node/workspace/resolveWorkspace.ts | 11 +- 6 files changed, 377 insertions(+), 217 deletions(-) create mode 100644 docs/guide/browser/config.md diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index 53ccb318fc91..a13e16e476b5 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -219,6 +219,10 @@ export default ({ mode }: { mode: string }) => { text: 'Configuration', collapsed: false, items: [ + { + text: 'Browser Config Reference', + link: '/guide/browser/config', + }, { text: 'Configuring Playwright', link: '/guide/browser/playwright', diff --git a/docs/config/index.md b/docs/config/index.md index c4d6ed1ca9b5..ad5d0cd12dfd 100644 --- a/docs/config/index.md +++ b/docs/config/index.md @@ -1739,221 +1739,17 @@ Open Vitest UI (WIP) Listen to port and serve API. When set to true, the default port is 51204 -### browser {#browser} +### browser experimental {#browser} -- **Type:** `{ enabled?, name?, provider?, headless?, api? }` -- **Default:** `{ enabled: false, headless: process.env.CI, api: 63315 }` -- **CLI:** `--browser`, `--browser=`, `--browser.name=chrome --browser.headless` +- **Default:** `{ enabled: false }` +- **CLI:** `--browser=`, `--browser.name=chrome --browser.headless` -Run Vitest tests in a browser. We use [WebdriverIO](https://webdriver.io/) for running tests by default, but it can be configured with [browser.provider](#browser-provider) option. - -::: tip NOTE -Read more about testing in a real browser in the [guide page](/guide/browser/). -::: +Configuration for running browser tests. Please, refer to the ["Browser Config Reference"](/guide/browser/config) article. ::: warning This is an experimental feature. Breaking changes might not follow SemVer, please pin Vitest's version when using it. ::: -#### browser.enabled - -- **Type:** `boolean` -- **Default:** `false` -- **CLI:** `--browser`, `--browser.enabled=false` - -Run all tests inside a browser by default. - -#### browser.name - -- **Type:** `string` -- **CLI:** `--browser=safari` - -Run all tests in a specific browser. Possible options in different providers: - -- `webdriverio`: `firefox`, `chrome`, `edge`, `safari` -- `playwright`: `firefox`, `webkit`, `chromium` -- custom: any string that will be passed to the provider - -#### browser.headless - -- **Type:** `boolean` -- **Default:** `process.env.CI` -- **CLI:** `--browser.headless`, `--browser.headless=false` - -Run the browser in a `headless` mode. If you are running Vitest in CI, it will be enabled by default. - -#### browser.isolate - -- **Type:** `boolean` -- **Default:** `true` -- **CLI:** `--browser.isolate`, `--browser.isolate=false` - -Run every test in a separate iframe. - -#### browser.testerHtmlPath 2.1.4 {#browser-testerhtmlpath} - -- **Type:** `string` -- **Default:** `@vitest/browser/tester.html` - -A path to the HTML entry point. Can be relative to the root of the project. This file will be processed with [`transformIndexHtml`](https://vite.dev/guide/api-plugin#transformindexhtml) hook. - -#### browser.api - -- **Type:** `number | { port?, strictPort?, host? }` -- **Default:** `63315` -- **CLI:** `--browser.api=63315`, `--browser.api.port=1234, --browser.api.host=example.com` - -Configure options for Vite server that serves code in the browser. Does not affect [`test.api`](#api) option. By default, Vitest assigns port `63315` to avoid conflicts with the development server, allowing you to run both in parallel. - -#### browser.provider - -- **Type:** `'webdriverio' | 'playwright' | 'preview' | string` -- **Default:** `'preview'` -- **CLI:** `--browser.provider=playwright` - -Path to a provider that will be used when running browser tests. Vitest provides three providers which are `preview` (default), `webdriverio` and `playwright`. Custom providers should be exported using `default` export and have this shape: - -```ts -export interface BrowserProvider { - name: string - getSupportedBrowsers: () => readonly string[] - initialize: (ctx: Vitest, options: { browser: string; options?: BrowserProviderOptions }) => Awaitable - openPage: (url: string) => Awaitable - close: () => Awaitable -} -``` - -::: warning -This is an advanced API for library authors. If you just need to run tests in a browser, use the [browser](#browser) option. -::: - -#### browser.providerOptions {#browser-provideroptions} - -- **Type:** `BrowserProviderOptions` - -Options that will be passed down to provider when calling `provider.initialize`. - -```ts -import { defineConfig } from 'vitest/config' - -export default defineConfig({ - test: { - browser: { - providerOptions: { - launch: { - devtools: true, - }, - }, - }, - }, -}) -``` - -::: tip -To have a better type safety when using built-in providers, you should reference one of these types (for provider that you are using) in your [config file](/config/): - -```ts -/// -/// -``` -::: - -#### browser.ui {#browser-ui} - -- **Type:** `boolean` -- **Default:** `!isCI` -- **CLI:** `--browser.ui=false` - -Should Vitest UI be injected into the page. By default, injects UI iframe during development. - -#### browser.viewport {#browser-viewport} - -- **Type:** `{ width, height }` -- **Default:** `414x896` - -Default iframe's viewport. - -#### browser.locators {#browser-locators} - -Options for built-in [browser locators](/guide/browser/locators). - -##### browser.locators.testIdAttribute - -- **Type:** `string` -- **Default:** `data-testid` - -Attribute used to find elements with `getByTestId` locator. - -#### browser.screenshotDirectory {#browser-screenshotdirectory} - -- **Type:** `string` -- **Default:** `__snapshots__` in the test file directory - -Path to the snapshots directory relative to the `root`. - -#### browser.screenshotFailures {#browser-screenshotfailures} - -- **Type:** `boolean` -- **Default:** `!browser.ui` - -Should Vitest take screenshots if the test fails. - -#### browser.orchestratorScripts {#browser-orchestratorscripts} - -- **Type:** `BrowserScript[]` -- **Default:** `[]` - -Custom scripts that should be injected into the orchestrator HTML before test iframes are initiated. This HTML document only sets up iframes and doesn't actually import your code. - -The script `src` and `content` will be processed by Vite plugins. Script should be provided in the following shape: - -```ts -export interface BrowserScript { - /** - * If "content" is provided and type is "module", this will be its identifier. - * - * If you are using TypeScript, you can add `.ts` extension here for example. - * @default `injected-${index}.js` - */ - id?: string - /** - * JavaScript content to be injected. This string is processed by Vite plugins if type is "module". - * - * You can use `id` to give Vite a hint about the file extension. - */ - content?: string - /** - * Path to the script. This value is resolved by Vite so it can be a node module or a file path. - */ - src?: string - /** - * If the script should be loaded asynchronously. - */ - async?: boolean - /** - * Script type. - * @default 'module' - */ - type?: string -} -``` - -#### browser.testerScripts {#browser-testerscripts} - -- **Type:** `BrowserScript[]` -- **Default:** `[]` - -Custom scripts that should be injected into the tester HTML before the tests environment is initiated. This is useful to inject polyfills required for Vitest browser implementation. It is recommended to use [`setupFiles`](#setupfiles) in almost all cases instead of this. - -The script `src` and `content` will be processed by Vite plugins. - -#### browser.commands {#browser-commands} - -- **Type:** `Record` -- **Default:** `{ readFile, writeFile, ... }` - -Custom [commands](/guide/browser/commands) that can be imported during browser tests from `@vitest/browser/commands`. - ### clearMocks - **Type:** `boolean` diff --git a/docs/guide/browser/config.md b/docs/guide/browser/config.md new file mode 100644 index 000000000000..de8efeb0b05f --- /dev/null +++ b/docs/guide/browser/config.md @@ -0,0 +1,311 @@ +# Browser Config Reference + +You can change the browser configuration by updating the `test.browser` field in your [config file](/config/). An example of a simple config file: + +```ts [vitest.config.ts] +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + browser: { + enabled: true, + provider: 'playwright', + configs: [ + { + browser: 'chromium', + setupFile: './chromium-setup.js', + }, + ], + }, + }, +}) +``` + +Please, refer to the ["Config Reference"](/config/) article for different config examples. + +::: warning +_All listed options_ on this page are located within a `test` property inside the configuration: + +```ts [vitest.config.js] +export default defineConfig({ + test: { + browser: {}, + }, +}) +``` +::: + +## browser.enabled + +- **Type:** `boolean` +- **Default:** `false` +- **CLI:** `--browser`, `--browser.enabled=false` + +Run all tests inside a browser by default. Note that `--browser` only works if you have at least one [`browser.configs`](#browser-configs). + +## browser.configs + +- **Type:** `BrowserConfig` +- **Default:** `[{ browser: name }]` + +Defines multiple browser setups. Every config has to have at least a `browser` field. The config supports your providers configurations: + +- [Configuring Playwright](/guide/browser/playwright) +- [Configuring WebdriverIO](/guide/browser/webdriverio) + +::: tip +To have a better type safety when using built-in providers, you should reference one of these types (for provider that you are using) in your [config file](/config/): + +```ts +/// +/// +``` +::: + +In addition to that, you can also specify most of the [project options](/config/) (not marked with a icon) and some of the `browser` options like `browser.testerHtmlPath`. + +::: warning +Every browser config inherits options from the root config: + +```ts{3,9} [vitest.config.ts] +export default defineConfig({ + test: { + setupFile: ['./root-setup-file.js'], + browser: { + enabled: true, + testerHtmlPath: './custom-path.html', + configs: [ + { + // will have both setup files: "root" and "browser" + setupFile: ['./browser-setup-file.js'], + // implicitly has "testerHtmlPath" from the root config // [!code warning] + // testerHtmlPath: './custom-path.html', // [!code warning] + }, + ], + }, + }, +}) +``` +::: + +List of available `browser` options: + +- [`browser.locators`](#browser-locators) +- [`browser.viewport`](#browser-viewport) +- [`browser.testerHtmlPath`](#browser-testerhtmlpath) +- [`browser.screenshotDirectory`](#browser-screenshotdirectory) +- [`browser.screenshotFailures`](#browser-screenshotfailures) + +By default, Vitest creates an array with a single element which uses the [`browser.name`](#browser-name) field as a `browser`. Note that this behaviour will be removed with Vitets 4. + +Under the hood, Vitest transforms these configs into separate [test projects](/advanced/api/test-project) sharing a single Vite server for better caching performance. + +## browser.name deprecated {#browser-name} + +- **Type:** `string` +- **CLI:** `--browser=safari` + +::: danger +This API is deprecated an will be removed in Vitest 4. Please, use [`browser.configs`](#browser-configs) field instead. +::: + +Run all tests in a specific browser. Possible options in different providers: + +- `webdriverio`: `firefox`, `chrome`, `edge`, `safari` +- `playwright`: `firefox`, `webkit`, `chromium` +- custom: any string that will be passed to the provider + +## browser.headless {#browser-headless} + +- **Type:** `boolean` +- **Default:** `process.env.CI` +- **CLI:** `--browser.headless`, `--browser.headless=false` + +Run the browser in a `headless` mode. If you are running Vitest in CI, it will be enabled by default. + +## browser.isolate {#browser-isolate} + +- **Type:** `boolean` +- **Default:** `true` +- **CLI:** `--browser.isolate`, `--browser.isolate=false` + +Run every test in a separate iframe. + +## browser.testerHtmlPath {#browser-testerhtmlpath} + +- **Type:** `string` + +A path to the HTML entry point. Can be relative to the root of the project. This file will be processed with [`transformIndexHtml`](https://vite.dev/guide/api-plugin#transformindexhtml) hook. + +## browser.api {#browser-api} + +- **Type:** `number | { port?, strictPort?, host? }` +- **Default:** `63315` +- **CLI:** `--browser.api=63315`, `--browser.api.port=1234, --browser.api.host=example.com` + +Configure options for Vite server that serves code in the browser. Does not affect [`test.api`](#api) option. By default, Vitest assigns port `63315` to avoid conflicts with the development server, allowing you to run both in parallel. + +## browser.provider experimental {#browser-provider} + +- **Type:** `'webdriverio' | 'playwright' | 'preview' | string` +- **Default:** `'preview'` +- **CLI:** `--browser.provider=playwright` + +::: danger ADVANCED API +The provider API is highly experimental and can change between patches. If you just need to run tests in a browser, use the [`browser.configs`](#browser-configs) option. +::: + +Path to a provider that will be used when running browser tests. Vitest provides three providers which are `preview` (default), `webdriverio` and `playwright`. Custom providers should be exported using `default` export and have this shape: + +```ts +export interface BrowserProvider { + name: string + supportsParallelism: boolean + getSupportedBrowsers: () => readonly string[] + beforeCommand?: (command: string, args: unknown[]) => Awaitable + afterCommand?: (command: string, args: unknown[]) => Awaitable + getCommandsContext: (sessionId: string) => Record + openPage: (sessionId: string, url: string, beforeNavigate?: () => Promise) => Promise + getCDPSession?: (sessionId: string) => Promise + close: () => Awaitable + initialize( + ctx: TestProject, + options: BrowserProviderInitializationOptions + ): Awaitable +} +``` + +## browser.providerOptions deprecated {#browser-provideroptions} + +- **Type:** `BrowserProviderOptions` + +::: danger +This API is deprecated an will be removed in Vitest 4. Please, use [`browser.configs`](#browser-configs) field instead. +::: + +Options that will be passed down to provider when calling `provider.initialize`. + +```ts +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + browser: { + providerOptions: { + launch: { + devtools: true, + }, + }, + }, + }, +}) +``` + +::: tip +To have a better type safety when using built-in providers, you should reference one of these types (for provider that you are using) in your [config file](/config/): + +```ts +/// +/// +``` +::: + +## browser.ui {#browser-ui} + +- **Type:** `boolean` +- **Default:** `!isCI` +- **CLI:** `--browser.ui=false` + +Should Vitest UI be injected into the page. By default, injects UI iframe during development. + +## browser.viewport {#browser-viewport} + +- **Type:** `{ width, height }` +- **Default:** `414x896` + +Default iframe's viewport. + +## browser.locators {#browser-locators} + +Options for built-in [browser locators](/guide/browser/locators). + +### browser.locators.testIdAttribute + +- **Type:** `string` +- **Default:** `data-testid` + +Attribute used to find elements with `getByTestId` locator. + +## browser.screenshotDirectory {#browser-screenshotdirectory} + +- **Type:** `string` +- **Default:** `__snapshots__` in the test file directory + +Path to the screenshots directory relative to the `root`. + +## browser.screenshotFailures {#browser-screenshotfailures} + +- **Type:** `boolean` +- **Default:** `!browser.ui` + +Should Vitest take screenshots if the test fails. + +## browser.orchestratorScripts {#browser-orchestratorscripts} + +- **Type:** `BrowserScript[]` +- **Default:** `[]` + +Custom scripts that should be injected into the orchestrator HTML before test iframes are initiated. This HTML document only sets up iframes and doesn't actually import your code. + +The script `src` and `content` will be processed by Vite plugins. Script should be provided in the following shape: + +```ts +export interface BrowserScript { + /** + * If "content" is provided and type is "module", this will be its identifier. + * + * If you are using TypeScript, you can add `.ts` extension here for example. + * @default `injected-${index}.js` + */ + id?: string + /** + * JavaScript content to be injected. This string is processed by Vite plugins if type is "module". + * + * You can use `id` to give Vite a hint about the file extension. + */ + content?: string + /** + * Path to the script. This value is resolved by Vite so it can be a node module or a file path. + */ + src?: string + /** + * If the script should be loaded asynchronously. + */ + async?: boolean + /** + * Script type. + * @default 'module' + */ + type?: string +} +``` + +## browser.testerScripts {#browser-testerscripts} + +- **Type:** `BrowserScript[]` +- **Default:** `[]` + +::: danger +This API is deprecated an will be removed in Vitest 4. Please, use [`browser.testerHtmlPath`](#browser-testerHtmlPath) field instead. +::: + +Custom scripts that should be injected into the tester HTML before the tests environment is initiated. This is useful to inject polyfills required for Vitest browser implementation. It is recommended to use [`setupFiles`](#setupfiles) in almost all cases instead of this. + +The script `src` and `content` will be processed by Vite plugins. + +## browser.commands {#browser-commands} + +- **Type:** `Record` +- **Default:** `{ readFile, writeFile, ... }` + +Custom [commands](/guide/browser/commands) that can be imported during browser tests from `@vitest/browser/commands`. diff --git a/docs/guide/browser/index.md b/docs/guide/browser/index.md index 32272ba6305a..cf18966f7d84 100644 --- a/docs/guide/browser/index.md +++ b/docs/guide/browser/index.md @@ -95,7 +95,7 @@ bun add -D vitest @vitest/browser webdriverio ## Configuration -To activate browser mode in your Vitest configuration, you can use the `--browser` flag or set the `browser.enabled` field to `true` in your Vitest configuration file. Here is an example configuration using the browser field: +To activate browser mode in your Vitest configuration, you can use the `--browser=name` flag or set the `browser.enabled` field to `true` in your Vitest configuration file. Here is an example configuration using the browser field: ```ts [vitest.config.ts] import { defineConfig } from 'vitest/config' @@ -104,7 +104,10 @@ export default defineConfig({ browser: { provider: 'playwright', // or 'webdriverio' enabled: true, - name: 'chromium', // browser name is required + // at least one config is required + configs: [ + { browser: 'chromium' }, + ], }, } }) @@ -129,7 +132,9 @@ export default defineConfig({ browser: { enabled: true, provider: 'playwright', - name: 'chromium', + configs: [ + { browser: 'chromium' }, + ], } } }) @@ -144,7 +149,9 @@ export default defineConfig({ browser: { enabled: true, provider: 'playwright', - name: 'chromium', + configs: [ + { browser: 'chromium' }, + ], } } }) @@ -159,7 +166,9 @@ export default defineConfig({ browser: { enabled: true, provider: 'playwright', - name: 'chromium', + configs: [ + { browser: 'chromium' }, + ], } } }) @@ -174,7 +183,9 @@ export default defineConfig({ browser: { enabled: true, provider: 'playwright', - name: 'chromium', + configs: [ + { browser: 'chromium' }, + ], } } }) @@ -189,7 +200,9 @@ export default defineConfig({ browser: { enabled: true, provider: 'playwright', - name: 'chromium', + configs: [ + { browser: 'chromium' }, + ], } } }) @@ -227,7 +240,9 @@ export default defineWorkspace([ name: 'browser', browser: { enabled: true, - name: 'chrome', + configs: [ + { browser: 'chromium' }, + ], }, }, }, diff --git a/docs/guide/migration.md b/docs/guide/migration.md index cfed94061c6e..97e02666c4f2 100644 --- a/docs/guide/migration.md +++ b/docs/guide/migration.md @@ -29,6 +29,31 @@ test('validation works', () => { }, 1000) // Ok ✅ ``` +### `browser.name` and `browser.providerOptions` are Deprecated + +Both [`browser.name`](/guide/browser/config/#browser-name) and [`browser.providerOptions`](/guide/browser/config/#browser-provideroptions) will be removed in Vitest 4. Instead of them, use the new [`browser.configs`](/guide/browser/config/#browser-configs) option: + +```ts +export default defineConfig({ + test: { + browser: { + name: 'chromium', // [!code --] + providerOptions: { // [!code --] + launch: { devtools: true }, // [!code --] + }, // [!code --] + configs: [ // [!code ++] + { // [!code ++] + browser: 'chromium', // [!code ++] + launch: { devtools: true }, // [!code ++] + }, // [!code ++] + ], // [!code ++] + }, + }, +}) +``` + +With the new `browser.configs` field you can also specify multiple browser configurations. + ### `Custom` Type is Deprecated API {#custom-type-is-deprecated} The `Custom` type is now an alias for the `Test` type. Note that Vitest updated the public types in 2.1 and changed exported names to `RunnerCustomCase` and `RunnerTestCase`: diff --git a/packages/vitest/src/node/workspace/resolveWorkspace.ts b/packages/vitest/src/node/workspace/resolveWorkspace.ts index 56324a5153b0..4a77b556624b 100644 --- a/packages/vitest/src/node/workspace/resolveWorkspace.ts +++ b/packages/vitest/src/node/workspace/resolveWorkspace.ts @@ -146,6 +146,10 @@ export async function resolveBrowserWorkspace( const [firstConfig, ...restConfigs] = configs const originalName = project.config.name + if (!firstConfig.browser) { + throw new Error(`The browser configuration must have a "browser" property. The first item in "browser.configs" doesn't have it. Make sure your${originalName ? ` "${originalName}"` : ''} configuration is correct.`) + } + const newName = originalName ? `${originalName} (${firstConfig.browser})` : firstConfig.browser @@ -160,8 +164,13 @@ export async function resolveBrowserWorkspace( ) } - restConfigs.forEach((config) => { + restConfigs.forEach((config, index) => { const browser = config.browser + if (!browser) { + const nth = index + 1 + const ending = nth === 2 ? 'nd' : nth === 3 ? 'rd' : 'th' + throw new Error(`The browser configuration must have a "browser" property. The ${nth}${ending} item in "browser.configs" doesn't have it. Make sure your${originalName ? ` "${originalName}"` : ''} configuration is correct.`) + } const name = config.name const newName = name || (originalName ? `${originalName} (${browser})` : browser) if (names.has(newName)) { From b7b07afc30a85db023cadf1dee16d437b31add1c Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Tue, 10 Dec 2024 16:13:16 +0100 Subject: [PATCH 26/51] feat: allow per-cofig headless mode --- packages/browser/src/node/server.ts | 2 +- packages/vitest/src/node/types/browser.ts | 2 +- packages/vitest/src/node/workspace/resolveWorkspace.ts | 3 +++ 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/browser/src/node/server.ts b/packages/browser/src/node/server.ts index 2e435e409c19..48e6d67993f2 100644 --- a/packages/browser/src/node/server.ts +++ b/packages/browser/src/node/server.ts @@ -198,7 +198,7 @@ export class BrowserServer implements IBrowserServer { const browser = project.config.browser.name if (!browser) { throw new Error( - `[${project.name}] Browser name is required. Please, set \`test.browser.name\` option manually.`, + `[${project.name}] Browser name is required. Please, set \`test.browser.configs.browser\` option manually.`, ) } const supportedBrowsers = this.provider.getSupportedBrowsers() diff --git a/packages/vitest/src/node/types/browser.ts b/packages/vitest/src/node/types/browser.ts index dbda7b7c5445..b395c5495430 100644 --- a/packages/vitest/src/node/types/browser.ts +++ b/packages/vitest/src/node/types/browser.ts @@ -69,7 +69,7 @@ type UnsupportedProperties = export interface BrowserConfig extends BrowserProviderOptions, Omit, - Pick { + Pick { browser: string } diff --git a/packages/vitest/src/node/workspace/resolveWorkspace.ts b/packages/vitest/src/node/workspace/resolveWorkspace.ts index 4a77b556624b..ba3680dc59e2 100644 --- a/packages/vitest/src/node/workspace/resolveWorkspace.ts +++ b/packages/vitest/src/node/workspace/resolveWorkspace.ts @@ -231,6 +231,7 @@ function cloneConfig(project: TestProject, { browser, ...config }: BrowserConfig locators, viewport, testerHtmlPath, + headless, screenshotDirectory, screenshotFailures, // @ts-expect-error remove just in case @@ -252,6 +253,8 @@ function cloneConfig(project: TestProject, { browser, ...config }: BrowserConfig testerHtmlPath: testerHtmlPath ?? currentConfig.testerHtmlPath, screenshotDirectory: screenshotDirectory ?? currentConfig.screenshotDirectory, screenshotFailures: screenshotFailures ?? currentConfig.screenshotFailures, + // TODO: test that CLI arg is preferred over the local config + headless: project.vitest._options?.browser?.headless ?? headless ?? currentConfig.headless, name: browser, providerOptions: config, configs: undefined, // projects cannot spawn more configs From 9acafa5eea0886b7b64ee451f22a397f6b863b38 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Tue, 10 Dec 2024 16:13:24 +0100 Subject: [PATCH 27/51] docs: add multi project guide --- docs/.vitepress/config.ts | 14 +++ docs/guide/browser/config.md | 5 + docs/guide/browser/index.md | 4 +- docs/guide/browser/multiple-setups.md | 134 +++++++++++++++++++++ packages/vitest/src/node/cli/cli-config.ts | 2 +- 5 files changed, 156 insertions(+), 3 deletions(-) create mode 100644 docs/guide/browser/multiple-setups.md diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index a13e16e476b5..991389fe0374 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -222,14 +222,17 @@ export default ({ mode }: { mode: string }) => { { text: 'Browser Config Reference', link: '/guide/browser/config', + docFooterText: 'Browser Config Reference | Browser Mode', }, { text: 'Configuring Playwright', link: '/guide/browser/playwright', + docFooterText: 'Configuring Playwright | Browser Mode', }, { text: 'Configuring WebdriverIO', link: '/guide/browser/webdriverio', + docFooterText: 'Configuring WebdriverIO | Browser Mode', }, ], }, @@ -264,6 +267,17 @@ export default ({ mode }: { mode: string }) => { }, ], }, + { + text: 'Guides', + collapsed: false, + items: [ + { + text: 'Multiple Setups', + link: '/guide/browser/multiple-setups', + docFooterText: 'Multiple Setups | Browser Mode', + }, + ], + }, { items: [ ...footer(), diff --git a/docs/guide/browser/config.md b/docs/guide/browser/config.md index de8efeb0b05f..32432c156a44 100644 --- a/docs/guide/browser/config.md +++ b/docs/guide/browser/config.md @@ -86,10 +86,15 @@ export default defineConfig({ }, }) ``` + +During development, Vitest supports only one [non-headless](#browser-headless) configuration. You can limit the headed project yourself by specifying `headless: false` in the config, or by providing the `--browser.headless=false` flag, or by filtering projects with `--project=chromium` flag. + +For more examples, refer to the ["Multiple Setups" guide](/guide/browser/multiple-setups). ::: List of available `browser` options: +- [`browser.headless`](#browser-headless) - [`browser.locators`](#browser-locators) - [`browser.viewport`](#browser-viewport) - [`browser.testerHtmlPath`](#browser-testerhtmlpath) diff --git a/docs/guide/browser/index.md b/docs/guide/browser/index.md index cf18966f7d84..ee6dbe64ac22 100644 --- a/docs/guide/browser/index.md +++ b/docs/guide/browser/index.md @@ -329,7 +329,7 @@ npx vitest --browser=chrome Or you can provide browser options to CLI with dot notation: ```sh -npx vitest --browser.name=chrome --browser.headless +npx vitest --browser.headless ``` By default, Vitest will automatically open the browser UI for development. Your tests will run inside an iframe in the center. You can configure the viewport by selecting the preferred dimensions, calling `page.viewport` inside the test, or setting default values in [the config](/config/#browser-viewport). @@ -358,7 +358,7 @@ export default defineConfig({ You can also set headless mode using the `--browser.headless` flag in the CLI, like this: ```sh -npx vitest --browser.name=chrome --browser.headless +npx vitest --browser.headless ``` In this case, Vitest will run in headless mode using the Chrome browser. diff --git a/docs/guide/browser/multiple-setups.md b/docs/guide/browser/multiple-setups.md new file mode 100644 index 000000000000..a84894530384 --- /dev/null +++ b/docs/guide/browser/multiple-setups.md @@ -0,0 +1,134 @@ +# Multiple Setups + +Since Vitest 3, you can specify several different browser setups using the new [`browser.configs`](/guide/browser/config/#browser-configs) option. + +The main advatage of using the `browser.configs` over the [workspace](/guide/workspace) is improved caching. Every project will use the same Vite server meaning the file transform and [dependency pre-bundling](https://vite.dev/guide/dep-pre-bundling.html) has to happen only once. + +## Several Browsers + +You can use the `browser.configs` field to specify options for different browsers. For example, if you want to run the same tests in different browsers, the minimal configuration will look like this: + +```ts [vitest.config.ts] +import { defineConfig } from 'vitest/config' +export default defineConfig({ + test: { + browser: { + enabled: true, + provider: 'playwright', + headless: true, + configs: [ + { browser: 'chromium' }, + { browser: 'firefox' }, + { browser: 'webkit' }, + ], + }, + }, +}) +``` + +## Different Setups + +You can also specify different config options independently from the browser (although, the configs _can_ also have `browser` fields): + +::: code-group +```ts [vitest.config.ts] +import { defineConfig } from 'vitest/config' +export default defineConfig({ + test: { + browser: { + enabled: true, + provider: 'playwright', + headless: true, + configs: [ + { + browser: 'chromium', + name: 'chromium-1', + setupFiles: ['./ratio-setup.ts'], + provide: { + ratio: 1, + } + }, + { + browser: 'chromium', + name: 'chromium-2', + provide: { + ratio: 2, + } + }, + ], + }, + }, +}) +``` +```ts [example.test.ts] +import { expect, inject, test } from 'vitest' +import { globalSetupModifier } from './example.js' + +test('ratio works', () => { + expect(inject('ratio') * globalSetupModifier).toBe(14) +}) +``` +::: + +In this example Vitest will run all tests in `chromium` browser, but execute a `'./ratio-setup.ts'` file only in the first configuration and inject a different `ratio` value depending on the [`provide` field](/config/#provide). + +::: warning +Note that you need to define the custom `name` value if you are using the same browser name because Vitest will assign the `browser` as the project name otherwise. +::: + +## Filtering + +You can filter what projects to run with the [`--project` flag](/guide/cli#project). Vitest will automatically assign the browser name as a project name if it is not assigned manually. If the root config already has a name, Vitest will merge them: `custom` -> `custom (browser)`. + +```shell +$ vitest --project=chromium +``` + +::: code-group +```ts{6,8} [default] +export default defineConfig({ + test: { + browser: { + configs: [ + // name: chromium + { browser: 'chromium' }, + // name: custom + { browser: 'firefox', name: 'custom' }, + ] + } + } +}) +``` +```ts{3,7,9} [custom] +export default defineConfig({ + test: { + name: 'custom', + browser: { + configs: [ + // name: custom (chromium) + { browser: 'chromium' }, + // name: manual + { browser: 'firefox', name: 'manual' }, + ] + } + } +}) +``` +::: + +::: warning +Vitest cannot run multiple configs that have `headless` mode set to `false` (the default behaviour). During development, you can select what project to run in your terminal: + +```shell +? Found multiple projects that run browser tests in headed mode: "chromium", "firefox". +Vitest cannot run multiple headed browsers at the same time. Select a single project +to run or cancel and run tests with "headless: true" option. Note that you can also +start tests with --browser=name or --project=name flag. › - Use arrow-keys. Return to submit. +❯ chromium + firefox +``` + +If you have several non-headless projects in CI (i.e. the `headless: false` is set manually in the config and not overriden in CI env), Vitest will fail the run and won't start any tests. + +The ability to run tests in headless mode is not affected by this. You can still run all configs in parallel as long as they don't have `headless: false`. +::: diff --git a/packages/vitest/src/node/cli/cli-config.ts b/packages/vitest/src/node/cli/cli-config.ts index ea8f1f75da70..7e991e5d882f 100644 --- a/packages/vitest/src/node/cli/cli-config.ts +++ b/packages/vitest/src/node/cli/cli-config.ts @@ -371,7 +371,7 @@ export const cliOptionsConfig: VitestCLIOptions = { }, name: { description: - 'Run all tests in a specific browser. Some browsers are only available for specific providers (see `--browser.provider`). Visit [`browser.name`](https://vitest.dev/config/#browser-name) for more information', + 'Run all tests in a specific browser. Some browsers are only available for specific providers (see `--browser.provider`). Visit [`browser.name`](https://vitest.dev/guide/browser/config/#browser-name) for more information', argument: '', }, headless: { From fe8b84e2143062e2970b9ddec1fbc3eab6521a3e Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Tue, 10 Dec 2024 16:19:23 +0100 Subject: [PATCH 28/51] docs: fix broken links --- docs/guide/browser/multiple-setups.md | 2 +- docs/guide/migration.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/guide/browser/multiple-setups.md b/docs/guide/browser/multiple-setups.md index a84894530384..072526ee1a60 100644 --- a/docs/guide/browser/multiple-setups.md +++ b/docs/guide/browser/multiple-setups.md @@ -1,6 +1,6 @@ # Multiple Setups -Since Vitest 3, you can specify several different browser setups using the new [`browser.configs`](/guide/browser/config/#browser-configs) option. +Since Vitest 3, you can specify several different browser setups using the new [`browser.configs`](/guide/browser/config#browser-configs) option. The main advatage of using the `browser.configs` over the [workspace](/guide/workspace) is improved caching. Every project will use the same Vite server meaning the file transform and [dependency pre-bundling](https://vite.dev/guide/dep-pre-bundling.html) has to happen only once. diff --git a/docs/guide/migration.md b/docs/guide/migration.md index 97e02666c4f2..aeb58d56ec9a 100644 --- a/docs/guide/migration.md +++ b/docs/guide/migration.md @@ -31,7 +31,7 @@ test('validation works', () => { ### `browser.name` and `browser.providerOptions` are Deprecated -Both [`browser.name`](/guide/browser/config/#browser-name) and [`browser.providerOptions`](/guide/browser/config/#browser-provideroptions) will be removed in Vitest 4. Instead of them, use the new [`browser.configs`](/guide/browser/config/#browser-configs) option: +Both [`browser.name`](/guide/browser/config#browser-name) and [`browser.providerOptions`](/guide/browser/config#browser-provideroptions) will be removed in Vitest 4. Instead of them, use the new [`browser.configs`](/guide/browser/config#browser-configs) option: ```ts export default defineConfig({ From 163097e3a43d1c903e70a11fd0d6f4a94c2d8041 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Tue, 10 Dec 2024 16:19:28 +0100 Subject: [PATCH 29/51] test: fix project name --- test/browser/specs/filter.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/browser/specs/filter.test.ts b/test/browser/specs/filter.test.ts index c07099cfcaf7..bd207c379deb 100644 --- a/test/browser/specs/filter.test.ts +++ b/test/browser/specs/filter.test.ts @@ -8,7 +8,7 @@ test('filter', async () => { }, ['test/basic.test.ts']) expect(stderr).toBe('') - expect(stdout).toContain('✓ test/basic.test.ts > basic 2') + expect(stdout).toContain('✓ |chromium| test/basic.test.ts > basic 2') expect(stdout).toContain('Test Files 1 passed') expect(stdout).toContain('Tests 1 passed | 3 skipped') }) From 59b4a4002ce22548bb8a1d164b4874ab1eb881ff Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Tue, 10 Dec 2024 16:27:11 +0100 Subject: [PATCH 30/51] test: fix project name --- test/browser/specs/filter.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/browser/specs/filter.test.ts b/test/browser/specs/filter.test.ts index bd207c379deb..35a85f3dd2eb 100644 --- a/test/browser/specs/filter.test.ts +++ b/test/browser/specs/filter.test.ts @@ -1,5 +1,5 @@ import { expect, test } from 'vitest' -import { runBrowserTests } from './utils' +import { browser, runBrowserTests } from './utils' test('filter', async () => { const { stderr, stdout } = await runBrowserTests({ @@ -8,7 +8,7 @@ test('filter', async () => { }, ['test/basic.test.ts']) expect(stderr).toBe('') - expect(stdout).toContain('✓ |chromium| test/basic.test.ts > basic 2') + expect(stdout).toContain(`✓ |${browser}| test/basic.test.ts > basic 2`) expect(stdout).toContain('Test Files 1 passed') expect(stdout).toContain('Tests 1 passed | 3 skipped') }) From 53e509c5076e6d50bc8a28f6fb6d035034c5ff4b Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Tue, 10 Dec 2024 16:46:21 +0100 Subject: [PATCH 31/51] docs: update browser links --- docs/.vitepress/scripts/cli-generator.ts | 2 +- docs/guide/browser/commands.md | 2 +- docs/guide/browser/locators.md | 4 ++-- docs/guide/browser/playwright.md | 4 ++-- docs/guide/browser/webdriverio.md | 2 +- docs/guide/cli-generated.md | 24 ++++++++++++------------ 6 files changed, 19 insertions(+), 19 deletions(-) diff --git a/docs/.vitepress/scripts/cli-generator.ts b/docs/.vitepress/scripts/cli-generator.ts index b71dfa5c3ad4..2aabeab8c95a 100644 --- a/docs/.vitepress/scripts/cli-generator.ts +++ b/docs/.vitepress/scripts/cli-generator.ts @@ -75,7 +75,7 @@ const options = resolveOptions(cliOptionsConfig) const template = options.map((option) => { const title = option.title const cli = option.cli - const config = skipConfig.has(title) ? '' : `[${title}](/config/#${title.toLowerCase().replace(/\./g, '-')})` + const config = skipConfig.has(title) ? '' : `[${title}](${title.includes('browser.') ? '/guide/browser/' : '/'}config/#${title.toLowerCase().replace(/\./g, '-')})` return `### ${title}\n\n- **CLI:** ${cli}\n${config ? `- **Config:** ${config}\n` : ''}\n${option.description}\n` }).join('\n') diff --git a/docs/guide/browser/commands.md b/docs/guide/browser/commands.md index 419cd0b04e4a..d00232caa106 100644 --- a/docs/guide/browser/commands.md +++ b/docs/guide/browser/commands.md @@ -61,7 +61,7 @@ CDP session works only with `playwright` provider and only when using `chromium` ## Custom Commands -You can also add your own commands via [`browser.commands`](/config/#browser-commands) config option. If you develop a library, you can provide them via a `config` hook inside a plugin: +You can also add your own commands via [`browser.commands`](/guide/browser/config#browser-commands) config option. If you develop a library, you can provide them via a `config` hook inside a plugin: ```ts import type { Plugin } from 'vitest/config' diff --git a/docs/guide/browser/locators.md b/docs/guide/browser/locators.md index be8ed5e5cf7e..f30b05ccdd80 100644 --- a/docs/guide/browser/locators.md +++ b/docs/guide/browser/locators.md @@ -364,7 +364,7 @@ page.getByTitle('Create') // ❌ function getByTestId(text: string | RegExp): Locator ``` -Creates a locator capable of finding an element that matches the specified test id attribute. You can configure the attribute name with [`browser.locators.testIdAttribute`](/config/#browser-locators-testidattribute). +Creates a locator capable of finding an element that matches the specified test id attribute. You can configure the attribute name with [`browser.locators.testIdAttribute`](/guide/browser/config#browser-locators-testidattribute). ```tsx
@@ -569,7 +569,7 @@ function screenshot(options?: LocatorScreenshotOptions & { base64?: false }): Pr Creates a screenshot of the element matching the locator's selector. -You can specify the save location for the screenshot using the `path` option, which is relative to the current test file. If the `path` option is not set, Vitest will default to using [`browser.screenshotDirectory`](/config/#browser-screenshotdirectory) (`__screenshot__` by default), along with the names of the file and the test to determine the screenshot's filepath. +You can specify the save location for the screenshot using the `path` option, which is relative to the current test file. If the `path` option is not set, Vitest will default to using [`browser.screenshotDirectory`](/guide/browser/config#browser-screenshotdirectory) (`__screenshot__` by default), along with the names of the file and the test to determine the screenshot's filepath. If you also need the content of the screenshot, you can specify `base64: true` to return it alongside the filepath where the screenshot is saved. diff --git a/docs/guide/browser/playwright.md b/docs/guide/browser/playwright.md index 14a70abf4cec..c1a3742126eb 100644 --- a/docs/guide/browser/playwright.md +++ b/docs/guide/browser/playwright.md @@ -60,7 +60,7 @@ export default defineConfig({ These options are directly passed down to `playwright[browser].launch` command. You can read more about the command and available arguments in the [Playwright documentation](https://playwright.dev/docs/api/class-browsertype#browser-type-launch). ::: warning -Vitest will ignore `launch.headless` option. Instead, use [`test.browser.headless`](/config/#browser-headless). +Vitest will ignore `launch.headless` option. Instead, use [`test.browser.headless`](/guide/browser/config#browser-headless). Note that Vitest will push debugging flags to `launch.args` if [`--inspect`](/guide/cli#inspect) is enabled. ::: @@ -76,7 +76,7 @@ Note that the context is created for every _test file_, not every _test_ like in ::: warning Vitest awlays sets `ignoreHTTPSErrors` to `true` in case your server is served via HTTPS and `serviceWorkers` to `'allow'` to support module mocking via [MSW](https://mswjs.io). -It is also recommended to use [`test.browser.viewport`](/config/#browser-headless) instead of specifying it here as it will be lost when tests are running in headless mode. +It is also recommended to use [`test.browser.viewport`](/guide/browser/config#browser-headless) instead of specifying it here as it will be lost when tests are running in headless mode. ::: ## `actionTimeout` 3.0.0 diff --git a/docs/guide/browser/webdriverio.md b/docs/guide/browser/webdriverio.md index e8b446838f14..2bd7737ae523 100644 --- a/docs/guide/browser/webdriverio.md +++ b/docs/guide/browser/webdriverio.md @@ -65,5 +65,5 @@ You can find most available options in the [WebdriverIO documentation](https://w ::: tip Most useful options are located on `capabilities` object. WebdriverIO allows nested capabilities, but Vitest will ignore those options because we rely on a different mechanism to spawn several browsers. -Note that Vitest will ignore `capabilities.browserName`. Use [`test.browser.capabilities.name`](/config/#browser-capabilities-name) instead. +Note that Vitest will ignore `capabilities.browserName`. Use [`test.browser.configs.name`](/guide/browser/config#browser-capabilities-name) instead. ::: diff --git a/docs/guide/cli-generated.md b/docs/guide/cli-generated.md index 9acbdfb0421d..b419d25735d9 100644 --- a/docs/guide/cli-generated.md +++ b/docs/guide/cli-generated.md @@ -316,77 +316,77 @@ Mock browser API with happy-dom ### browser.enabled - **CLI:** `--browser.enabled` -- **Config:** [browser.enabled](/config/#browser-enabled) +- **Config:** [browser.enabled](/guide/browser/config/#browser-enabled) Run tests in the browser. Equivalent to `--browser.enabled` (default: `false`) ### browser.name - **CLI:** `--browser.name ` -- **Config:** [browser.name](/config/#browser-name) +- **Config:** [browser.name](/guide/browser/config/#browser-name) -Run all tests in a specific browser. Some browsers are only available for specific providers (see `--browser.provider`). Visit [`browser.name`](https://vitest.dev/config/#browser-name) for more information +Run all tests in a specific browser. Some browsers are only available for specific providers (see `--browser.provider`). Visit [`browser.name`](https://vitest.dev/guide/browser/config/#browser-name) for more information ### browser.headless - **CLI:** `--browser.headless` -- **Config:** [browser.headless](/config/#browser-headless) +- **Config:** [browser.headless](/guide/browser/config/#browser-headless) Run the browser in headless mode (i.e. without opening the GUI (Graphical User Interface)). If you are running Vitest in CI, it will be enabled by default (default: `process.env.CI`) ### browser.api.port - **CLI:** `--browser.api.port [port]` -- **Config:** [browser.api.port](/config/#browser-api-port) +- **Config:** [browser.api.port](/guide/browser/config/#browser-api-port) Specify server port. Note if the port is already being used, Vite will automatically try the next available port so this may not be the actual port the server ends up listening on. If true will be set to `63315` ### browser.api.host - **CLI:** `--browser.api.host [host]` -- **Config:** [browser.api.host](/config/#browser-api-host) +- **Config:** [browser.api.host](/guide/browser/config/#browser-api-host) Specify which IP addresses the server should listen on. Set this to `0.0.0.0` or `true` to listen on all addresses, including LAN and public addresses ### browser.api.strictPort - **CLI:** `--browser.api.strictPort` -- **Config:** [browser.api.strictPort](/config/#browser-api-strictport) +- **Config:** [browser.api.strictPort](/guide/browser/config/#browser-api-strictport) Set to true to exit if port is already in use, instead of automatically trying the next available port ### browser.provider - **CLI:** `--browser.provider ` -- **Config:** [browser.provider](/config/#browser-provider) +- **Config:** [browser.provider](/guide/browser/config/#browser-provider) Provider used to run browser tests. Some browsers are only available for specific providers. Can be "webdriverio", "playwright", "preview", or the path to a custom provider. Visit [`browser.provider`](https://vitest.dev/config/#browser-provider) for more information (default: `"preview"`) ### browser.providerOptions - **CLI:** `--browser.providerOptions ` -- **Config:** [browser.providerOptions](/config/#browser-provideroptions) +- **Config:** [browser.providerOptions](/guide/browser/config/#browser-provideroptions) Options that are passed down to a browser provider. Visit [`browser.providerOptions`](https://vitest.dev/config/#browser-provideroptions) for more information ### browser.isolate - **CLI:** `--browser.isolate` -- **Config:** [browser.isolate](/config/#browser-isolate) +- **Config:** [browser.isolate](/guide/browser/config/#browser-isolate) Run every browser test file in isolation. To disable isolation, use `--browser.isolate=false` (default: `true`) ### browser.ui - **CLI:** `--browser.ui` -- **Config:** [browser.ui](/config/#browser-ui) +- **Config:** [browser.ui](/guide/browser/config/#browser-ui) Show Vitest UI when running tests (default: `!process.env.CI`) ### browser.fileParallelism - **CLI:** `--browser.fileParallelism` -- **Config:** [browser.fileParallelism](/config/#browser-fileparallelism) +- **Config:** [browser.fileParallelism](/guide/browser/config/#browser-fileparallelism) Should browser test files run in parallel. Use `--browser.fileParallelism=false` to disable (default: `true`) From 3f16f8df46ebaf54f690b61ec3332362e6568e54 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Wed, 11 Dec 2024 10:55:34 +0100 Subject: [PATCH 32/51] refactor: rename browser.configs to browser.instances --- docs/guide/browser/config.md | 16 +++++----- docs/guide/browser/index.md | 16 +++++----- docs/guide/browser/multiple-setups.md | 20 ++++++------ docs/guide/browser/playwright.md | 6 ++-- docs/guide/browser/webdriverio.md | 8 ++--- docs/guide/migration.md | 6 ++-- packages/browser/src/node/server.ts | 2 +- packages/vitest/src/node/cli/cli-config.ts | 2 +- .../vitest/src/node/config/resolveConfig.ts | 14 ++++---- packages/vitest/src/node/types/browser.ts | 18 ++++++++--- packages/vitest/src/node/types/config.ts | 2 +- .../src/node/workspace/resolveWorkspace.ts | 14 ++++---- packages/vitest/src/public/node.ts | 4 +-- test/config/test/browser-configs.test.ts | 6 ++-- test/config/test/failures.test.ts | 32 +++++++++---------- 15 files changed, 88 insertions(+), 78 deletions(-) diff --git a/docs/guide/browser/config.md b/docs/guide/browser/config.md index 32432c156a44..170af5d2b0ea 100644 --- a/docs/guide/browser/config.md +++ b/docs/guide/browser/config.md @@ -10,7 +10,7 @@ export default defineConfig({ browser: { enabled: true, provider: 'playwright', - configs: [ + instances: [ { browser: 'chromium', setupFile: './chromium-setup.js', @@ -41,9 +41,9 @@ export default defineConfig({ - **Default:** `false` - **CLI:** `--browser`, `--browser.enabled=false` -Run all tests inside a browser by default. Note that `--browser` only works if you have at least one [`browser.configs`](#browser-configs). +Run all tests inside a browser by default. Note that `--browser` only works if you have at least one [`browser.instances`](#browser-instances) item. -## browser.configs +## browser.instances - **Type:** `BrowserConfig` - **Default:** `[{ browser: name }]` @@ -74,7 +74,7 @@ export default defineConfig({ browser: { enabled: true, testerHtmlPath: './custom-path.html', - configs: [ + instances: [ { // will have both setup files: "root" and "browser" setupFile: ['./browser-setup-file.js'], @@ -103,7 +103,7 @@ List of available `browser` options: By default, Vitest creates an array with a single element which uses the [`browser.name`](#browser-name) field as a `browser`. Note that this behaviour will be removed with Vitets 4. -Under the hood, Vitest transforms these configs into separate [test projects](/advanced/api/test-project) sharing a single Vite server for better caching performance. +Under the hood, Vitest transforms these instances into separate [test projects](/advanced/api/test-project) sharing a single Vite server for better caching performance. ## browser.name deprecated {#browser-name} @@ -111,7 +111,7 @@ Under the hood, Vitest transforms these configs into separate [test projects](/a - **CLI:** `--browser=safari` ::: danger -This API is deprecated an will be removed in Vitest 4. Please, use [`browser.configs`](#browser-configs) field instead. +This API is deprecated an will be removed in Vitest 4. Please, use [`browser.instances`](#browser-instances) option instead. ::: Run all tests in a specific browser. Possible options in different providers: @@ -157,7 +157,7 @@ Configure options for Vite server that serves code in the browser. Does not affe - **CLI:** `--browser.provider=playwright` ::: danger ADVANCED API -The provider API is highly experimental and can change between patches. If you just need to run tests in a browser, use the [`browser.configs`](#browser-configs) option. +The provider API is highly experimental and can change between patches. If you just need to run tests in a browser, use the [`browser.instances`](#browser-instances) option instead. ::: Path to a provider that will be used when running browser tests. Vitest provides three providers which are `preview` (default), `webdriverio` and `playwright`. Custom providers should be exported using `default` export and have this shape: @@ -185,7 +185,7 @@ export interface BrowserProvider { - **Type:** `BrowserProviderOptions` ::: danger -This API is deprecated an will be removed in Vitest 4. Please, use [`browser.configs`](#browser-configs) field instead. +This API is deprecated an will be removed in Vitest 4. Please, use [`browser.instances`](#browser-instances) option instead. ::: Options that will be passed down to provider when calling `provider.initialize`. diff --git a/docs/guide/browser/index.md b/docs/guide/browser/index.md index ee6dbe64ac22..a9fa766d8e5e 100644 --- a/docs/guide/browser/index.md +++ b/docs/guide/browser/index.md @@ -104,8 +104,8 @@ export default defineConfig({ browser: { provider: 'playwright', // or 'webdriverio' enabled: true, - // at least one config is required - configs: [ + // at least one instance is required + instances: [ { browser: 'chromium' }, ], }, @@ -132,7 +132,7 @@ export default defineConfig({ browser: { enabled: true, provider: 'playwright', - configs: [ + instances: [ { browser: 'chromium' }, ], } @@ -149,7 +149,7 @@ export default defineConfig({ browser: { enabled: true, provider: 'playwright', - configs: [ + instances: [ { browser: 'chromium' }, ], } @@ -166,7 +166,7 @@ export default defineConfig({ browser: { enabled: true, provider: 'playwright', - configs: [ + instances: [ { browser: 'chromium' }, ], } @@ -183,7 +183,7 @@ export default defineConfig({ browser: { enabled: true, provider: 'playwright', - configs: [ + instances: [ { browser: 'chromium' }, ], } @@ -200,7 +200,7 @@ export default defineConfig({ browser: { enabled: true, provider: 'playwright', - configs: [ + instances: [ { browser: 'chromium' }, ], } @@ -240,7 +240,7 @@ export default defineWorkspace([ name: 'browser', browser: { enabled: true, - configs: [ + instances: [ { browser: 'chromium' }, ], }, diff --git a/docs/guide/browser/multiple-setups.md b/docs/guide/browser/multiple-setups.md index 072526ee1a60..78bb6259c3e3 100644 --- a/docs/guide/browser/multiple-setups.md +++ b/docs/guide/browser/multiple-setups.md @@ -1,12 +1,12 @@ # Multiple Setups -Since Vitest 3, you can specify several different browser setups using the new [`browser.configs`](/guide/browser/config#browser-configs) option. +Since Vitest 3, you can specify several different browser setups using the new [`browser.instances`](/guide/browser/config#browser-instances) option. -The main advatage of using the `browser.configs` over the [workspace](/guide/workspace) is improved caching. Every project will use the same Vite server meaning the file transform and [dependency pre-bundling](https://vite.dev/guide/dep-pre-bundling.html) has to happen only once. +The main advatage of using the `browser.instances` over the [workspace](/guide/workspace) is improved caching. Every project will use the same Vite server meaning the file transform and [dependency pre-bundling](https://vite.dev/guide/dep-pre-bundling.html) has to happen only once. ## Several Browsers -You can use the `browser.configs` field to specify options for different browsers. For example, if you want to run the same tests in different browsers, the minimal configuration will look like this: +You can use the `browser.instances` field to specify options for different browsers. For example, if you want to run the same tests in different browsers, the minimal configuration will look like this: ```ts [vitest.config.ts] import { defineConfig } from 'vitest/config' @@ -16,7 +16,7 @@ export default defineConfig({ enabled: true, provider: 'playwright', headless: true, - configs: [ + instances: [ { browser: 'chromium' }, { browser: 'firefox' }, { browser: 'webkit' }, @@ -28,7 +28,7 @@ export default defineConfig({ ## Different Setups -You can also specify different config options independently from the browser (although, the configs _can_ also have `browser` fields): +You can also specify different config options independently from the browser (although, the instances _can_ also have `browser` fields): ::: code-group ```ts [vitest.config.ts] @@ -39,7 +39,7 @@ export default defineConfig({ enabled: true, provider: 'playwright', headless: true, - configs: [ + instances: [ { browser: 'chromium', name: 'chromium-1', @@ -89,7 +89,7 @@ $ vitest --project=chromium export default defineConfig({ test: { browser: { - configs: [ + instances: [ // name: chromium { browser: 'chromium' }, // name: custom @@ -104,7 +104,7 @@ export default defineConfig({ test: { name: 'custom', browser: { - configs: [ + instances: [ // name: custom (chromium) { browser: 'chromium' }, // name: manual @@ -117,7 +117,7 @@ export default defineConfig({ ::: ::: warning -Vitest cannot run multiple configs that have `headless` mode set to `false` (the default behaviour). During development, you can select what project to run in your terminal: +Vitest cannot run multiple instances that have `headless` mode set to `false` (the default behaviour). During development, you can select what project to run in your terminal: ```shell ? Found multiple projects that run browser tests in headed mode: "chromium", "firefox". @@ -130,5 +130,5 @@ start tests with --browser=name or --project=name flag. › - Use arrow-keys. Re If you have several non-headless projects in CI (i.e. the `headless: false` is set manually in the config and not overriden in CI env), Vitest will fail the run and won't start any tests. -The ability to run tests in headless mode is not affected by this. You can still run all configs in parallel as long as they don't have `headless: false`. +The ability to run tests in headless mode is not affected by this. You can still run all instances in parallel as long as they don't have `headless: false`. ::: diff --git a/docs/guide/browser/playwright.md b/docs/guide/browser/playwright.md index c1a3742126eb..d143a2b4c759 100644 --- a/docs/guide/browser/playwright.md +++ b/docs/guide/browser/playwright.md @@ -16,7 +16,7 @@ Alternatively, you can also add it to `compilerOptions.types` field in your `tsc } ``` -Vitest opens a single page to run all tests in the same file. You can configure the `launch` and `context` properties in `configs`: +Vitest opens a single page to run all tests in the same file. You can configure the `launch` and `context` properties in `instances`: ```ts{9-10} [vitest.config.ts] import { defineConfig } from 'vitest/config' @@ -24,7 +24,7 @@ import { defineConfig } from 'vitest/config' export default defineConfig({ test: { browser: { - configs: [ + instances: [ { browser: 'firefox', launch: {}, @@ -52,7 +52,7 @@ export default defineConfig({ }) ``` -`providerOptions` is deprecated in favour of `configs`. +`providerOptions` is deprecated in favour of `instances`. ::: ## launch diff --git a/docs/guide/browser/webdriverio.md b/docs/guide/browser/webdriverio.md index 2bd7737ae523..b0afdf789713 100644 --- a/docs/guide/browser/webdriverio.md +++ b/docs/guide/browser/webdriverio.md @@ -20,7 +20,7 @@ Alternatively, you can also add it to `compilerOptions.types` field in your `tsc } ``` -Vitest opens a single page to run all tests in the same file. You can configure any property specified in `RemoteOptions` in `configs`: +Vitest opens a single page to run all tests in the same file. You can configure any property specified in `RemoteOptions` in `instances`: ```ts{9-12} [vitest.config.ts] import { defineConfig } from 'vitest/config' @@ -28,7 +28,7 @@ import { defineConfig } from 'vitest/config' export default defineConfig({ test: { browser: { - configs: [ + instances: [ { browser: 'chrome', capabilities: { @@ -57,7 +57,7 @@ export default defineConfig({ }) ``` -`providerOptions` is deprecated in favour of `configs`. +`providerOptions` is deprecated in favour of `instances`. ::: You can find most available options in the [WebdriverIO documentation](https://webdriver.io/docs/configuration/). Note that Vitest will ignore all test runner options because we only use `webdriverio`'s browser capabilities. @@ -65,5 +65,5 @@ You can find most available options in the [WebdriverIO documentation](https://w ::: tip Most useful options are located on `capabilities` object. WebdriverIO allows nested capabilities, but Vitest will ignore those options because we rely on a different mechanism to spawn several browsers. -Note that Vitest will ignore `capabilities.browserName`. Use [`test.browser.configs.name`](/guide/browser/config#browser-capabilities-name) instead. +Note that Vitest will ignore `capabilities.browserName`. Use [`test.browser.instances.name`](/guide/browser/config#browser-capabilities-name) instead. ::: diff --git a/docs/guide/migration.md b/docs/guide/migration.md index aeb58d56ec9a..d57569fd7c98 100644 --- a/docs/guide/migration.md +++ b/docs/guide/migration.md @@ -31,7 +31,7 @@ test('validation works', () => { ### `browser.name` and `browser.providerOptions` are Deprecated -Both [`browser.name`](/guide/browser/config#browser-name) and [`browser.providerOptions`](/guide/browser/config#browser-provideroptions) will be removed in Vitest 4. Instead of them, use the new [`browser.configs`](/guide/browser/config#browser-configs) option: +Both [`browser.name`](/guide/browser/config#browser-name) and [`browser.providerOptions`](/guide/browser/config#browser-provideroptions) will be removed in Vitest 4. Instead of them, use the new [`browser.instances`](/guide/browser/config#browser-instances) option: ```ts export default defineConfig({ @@ -41,7 +41,7 @@ export default defineConfig({ providerOptions: { // [!code --] launch: { devtools: true }, // [!code --] }, // [!code --] - configs: [ // [!code ++] + instances: [ // [!code ++] { // [!code ++] browser: 'chromium', // [!code ++] launch: { devtools: true }, // [!code ++] @@ -52,7 +52,7 @@ export default defineConfig({ }) ``` -With the new `browser.configs` field you can also specify multiple browser configurations. +With the new `browser.instances` field you can also specify multiple browser configurations. ### `Custom` Type is Deprecated API {#custom-type-is-deprecated} diff --git a/packages/browser/src/node/server.ts b/packages/browser/src/node/server.ts index 48e6d67993f2..1e6723bfedd8 100644 --- a/packages/browser/src/node/server.ts +++ b/packages/browser/src/node/server.ts @@ -198,7 +198,7 @@ export class BrowserServer implements IBrowserServer { const browser = project.config.browser.name if (!browser) { throw new Error( - `[${project.name}] Browser name is required. Please, set \`test.browser.configs.browser\` option manually.`, + `[${project.name}] Browser name is required. Please, set \`test.browser.instances[].browser\` option manually.`, ) } const supportedBrowsers = this.provider.getSupportedBrowsers() diff --git a/packages/vitest/src/node/cli/cli-config.ts b/packages/vitest/src/node/cli/cli-config.ts index 7e991e5d882f..73612fd7df6e 100644 --- a/packages/vitest/src/node/cli/cli-config.ts +++ b/packages/vitest/src/node/cli/cli-config.ts @@ -416,7 +416,7 @@ export const cliOptionsConfig: VitestCLIOptions = { screenshotFailures: null, locators: null, testerHtmlPath: null, - configs: null, + instances: null, }, }, pool: { diff --git a/packages/vitest/src/node/config/resolveConfig.ts b/packages/vitest/src/node/config/resolveConfig.ts index 6861372785fa..45d303fc4401 100644 --- a/packages/vitest/src/node/config/resolveConfig.ts +++ b/packages/vitest/src/node/config/resolveConfig.ts @@ -233,19 +233,19 @@ export function resolveConfig( const browser = resolved.browser if (browser.enabled) { - if (!browser.name && !browser.configs) { - throw new Error(`Vitest Browser Mode requires "browser.name" (deprecated) or "browser.configs" options, none were set.`) + if (!browser.name && !browser.instances) { + throw new Error(`Vitest Browser Mode requires "browser.name" (deprecated) or "browser.instances" options, none were set.`) } - const configs = browser.configs - if (browser.name && browser.configs) { + const configs = browser.instances + if (browser.name && browser.instances) { // --browser=chromium filters configs to a single one - browser.configs = browser.configs.filter(config_ => config_.browser === browser.name) + browser.instances = browser.instances.filter(config_ => config_.browser === browser.name) } - if (browser.configs && !browser.configs.length) { + if (browser.instances && !browser.instances.length) { throw new Error([ - `"browser.configs" was set in the config, but the array is empty. Define at least one browser config.`, + `"browser.instances" was set in the config, but the array is empty. Define at least one browser config.`, browser.name && configs?.length ? ` The "browser.name" was set to "${browser.name}" which filtered all configs (${configs.map(c => c.browser).join(', ')}). Did you mean to use another name?` : '', ].join('')) } diff --git a/packages/vitest/src/node/types/browser.ts b/packages/vitest/src/node/types/browser.ts index b395c5495430..87acdf69b214 100644 --- a/packages/vitest/src/node/types/browser.ts +++ b/packages/vitest/src/node/types/browser.ts @@ -66,10 +66,20 @@ type UnsupportedProperties = | 'server' | 'benchmark' -export interface BrowserConfig - extends BrowserProviderOptions, +export interface BrowserInstanceOption extends BrowserProviderOptions, Omit, - Pick { + Pick< + BrowserConfigOptions, + | 'headless' + | 'locators' + | 'viewport' + | 'testerHtmlPath' + | 'screenshotDirectory' + | 'screenshotFailures' + > { + /** + * Name of the browser + */ browser: string } @@ -90,7 +100,7 @@ export interface BrowserConfigOptions { /** * Configurations for different browser setups */ - configs?: BrowserConfig[] + instances?: BrowserInstanceOption[] /** * Browser provider diff --git a/packages/vitest/src/node/types/config.ts b/packages/vitest/src/node/types/config.ts index e9674a18052a..6e3ddbd85302 100644 --- a/packages/vitest/src/node/types/config.ts +++ b/packages/vitest/src/node/types/config.ts @@ -25,7 +25,7 @@ import type { Reporter } from './reporter' export type { CoverageOptions, ResolvedCoverageOptions } export type { BenchmarkUserOptions } export type { RuntimeConfig, SerializedConfig } from '../../runtime/config' -export type { BrowserConfig, BrowserConfigOptions, BrowserScript } from './browser' +export type { BrowserConfigOptions, BrowserInstanceOption, BrowserScript } from './browser' export type { CoverageIstanbulOptions, CoverageV8Options } from './coverage' export type { SequenceHooks, SequenceSetupFiles } from '@vitest/runner' diff --git a/packages/vitest/src/node/workspace/resolveWorkspace.ts b/packages/vitest/src/node/workspace/resolveWorkspace.ts index ba3680dc59e2..64eb29c7c5cc 100644 --- a/packages/vitest/src/node/workspace/resolveWorkspace.ts +++ b/packages/vitest/src/node/workspace/resolveWorkspace.ts @@ -1,5 +1,5 @@ import type { Vitest } from '../core' -import type { BrowserConfig, ResolvedConfig, TestProjectConfiguration, UserConfig, UserWorkspaceConfig } from '../types/config' +import type { BrowserInstanceOption, ResolvedConfig, TestProjectConfiguration, UserConfig, UserWorkspaceConfig } from '../types/config' import { existsSync, promises as fs } from 'node:fs' import os from 'node:os' import { limitConcurrency } from '@vitest/runner/utils' @@ -139,7 +139,7 @@ export async function resolveBrowserWorkspace( const newConfigs: [project: TestProject, config: ResolvedConfig][] = [] resolvedProjects.forEach((project) => { - const configs = project.config.browser.configs + const configs = project.config.browser.instances if (!project.config.browser.enabled || !configs || configs.length === 0) { return } @@ -147,7 +147,7 @@ export async function resolveBrowserWorkspace( const originalName = project.config.name if (!firstConfig.browser) { - throw new Error(`The browser configuration must have a "browser" property. The first item in "browser.configs" doesn't have it. Make sure your${originalName ? ` "${originalName}"` : ''} configuration is correct.`) + throw new Error(`The browser configuration must have a "browser" property. The first item in "browser.instances" doesn't have it. Make sure your${originalName ? ` "${originalName}"` : ''} configuration is correct.`) } const newName = originalName @@ -169,7 +169,7 @@ export async function resolveBrowserWorkspace( if (!browser) { const nth = index + 1 const ending = nth === 2 ? 'nd' : nth === 3 ? 'rd' : 'th' - throw new Error(`The browser configuration must have a "browser" property. The ${nth}${ending} item in "browser.configs" doesn't have it. Make sure your${originalName ? ` "${originalName}"` : ''} configuration is correct.`) + throw new Error(`The browser configuration must have a "browser" property. The ${nth}${ending} item in "browser.instances" doesn't have it. Make sure your${originalName ? ` "${originalName}"` : ''} configuration is correct.`) } const name = config.name const newName = name || (originalName ? `${originalName} (${browser})` : browser) @@ -177,7 +177,7 @@ export async function resolveBrowserWorkspace( throw new Error( [ `Cannot define a nested project for a ${browser} browser. The project name "${newName}" was already defined. `, - 'If you have multiple configs for the same browser, make sure to define a custom "name". ', + 'If you have multiple instances for the same browser, make sure to define a custom "name". ', 'All projects in a workspace should have unique names. Make sure your configuration is correct.', ].join(''), ) @@ -226,7 +226,7 @@ export async function resolveBrowserWorkspace( return resolvedProjects } -function cloneConfig(project: TestProject, { browser, ...config }: BrowserConfig) { +function cloneConfig(project: TestProject, { browser, ...config }: BrowserInstanceOption) { const { locators, viewport, @@ -257,7 +257,7 @@ function cloneConfig(project: TestProject, { browser, ...config }: BrowserConfig headless: project.vitest._options?.browser?.headless ?? headless ?? currentConfig.headless, name: browser, providerOptions: config, - configs: undefined, // projects cannot spawn more configs + instances: undefined, // projects cannot spawn more configs }, // TODO: should resolve, not merge/override } satisfies ResolvedConfig, overrideConfig) as ResolvedConfig diff --git a/packages/vitest/src/public/node.ts b/packages/vitest/src/public/node.ts index 1933494bc7e6..18a2e0c46b64 100644 --- a/packages/vitest/src/public/node.ts +++ b/packages/vitest/src/public/node.ts @@ -60,8 +60,8 @@ export type { BrowserBuiltinProvider, BrowserCommand, BrowserCommandContext, - BrowserConfig, BrowserConfigOptions, + BrowserInstanceOption, BrowserOrchestrator, BrowserProvider, BrowserProviderInitializationOptions, @@ -70,7 +70,7 @@ export type { BrowserScript, BrowserServer, BrowserServerState, - BrowserServerStateSession as BrowserServerStateContext, + BrowserServerStateSession, CDPSession, ResolvedBrowserOptions, } from '../node/types/browser' diff --git a/test/config/test/browser-configs.test.ts b/test/config/test/browser-configs.test.ts index aa63e0398375..1e5d1c27b783 100644 --- a/test/config/test/browser-configs.test.ts +++ b/test/config/test/browser-configs.test.ts @@ -11,7 +11,7 @@ test('assignes names as browsers', async () => { const { projects } = await vitest({ browser: { enabled: true, - configs: [ + instances: [ { browser: 'chromium' }, { browser: 'firefox' }, { browser: 'webkit' }, @@ -33,7 +33,7 @@ test('assignes names as browsers in a custom project', async () => { name: 'custom', browser: { enabled: true, - configs: [ + instances: [ { browser: 'chromium' }, { browser: 'firefox' }, { browser: 'webkit' }, @@ -72,7 +72,7 @@ test('inherits browser options', async () => { locators: { testIdAttribute: 'data-tid', }, - configs: [ + instances: [ { browser: 'chromium', screenshotFailures: true, diff --git a/test/config/test/failures.test.ts b/test/config/test/failures.test.ts index 6a552a9ba093..d14aab5a46ea 100644 --- a/test/config/test/failures.test.ts +++ b/test/config/test/failures.test.ts @@ -289,64 +289,64 @@ test('maxConcurrency 0 prints a warning', async () => { expect(stderr).toMatch('The option "maxConcurrency" cannot be set to 0. Using default value 5 instead.') }) -test('browser.name or browser.configs are required', async () => { +test('browser.name or browser.instances are required', async () => { const { stderr, exitCode } = await runVitestCli('--browser.enabled') expect(exitCode).toBe(1) - expect(stderr).toMatch('Vitest Browser Mode requires "browser.name" (deprecated) or "browser.configs" options, none were set.') + expect(stderr).toMatch('Vitest Browser Mode requires "browser.name" (deprecated) or "browser.instances" options, none were set.') }) -test('browser.configs is empty', async () => { +test('browser.instances is empty', async () => { const { stderr } = await runVitest({ browser: { enabled: true, provider: 'playwright', - configs: [], + instances: [], }, }) - expect(stderr).toMatch('"browser.configs" was set in the config, but the array is empty. Define at least one browser config.') + expect(stderr).toMatch('"browser.instances" was set in the config, but the array is empty. Define at least one browser config.') }) -test('browser.name filteres all browser.configs are required', async () => { +test('browser.name filteres all browser.instances are required', async () => { const { stderr } = await runVitest({ browser: { enabled: true, name: 'chromium', provider: 'playwright', - configs: [ + instances: [ { browser: 'firefox' }, ], }, }) - expect(stderr).toMatch('"browser.configs" was set in the config, but the array is empty. Define at least one browser config. The "browser.name" was set to "chromium" which filtered all configs (firefox). Did you mean to use another name?') + expect(stderr).toMatch('"browser.instances" was set in the config, but the array is empty. Define at least one browser config. The "browser.name" was set to "chromium" which filtered all configs (firefox). Did you mean to use another name?') }) -test('browser.configs throws an error if no custom name is provided', async () => { +test('browser.instances throws an error if no custom name is provided', async () => { const { stderr } = await runVitest({ browser: { enabled: true, provider: 'playwright', - configs: [ + instances: [ { browser: 'firefox' }, { browser: 'firefox' }, ], }, }) - expect(stderr).toMatch('Cannot define a nested project for a firefox browser. The project name "firefox" was already defined. If you have multiple configs for the same browser, make sure to define a custom "name". All projects in a workspace should have unique names. Make sure your configuration is correct.') + expect(stderr).toMatch('Cannot define a nested project for a firefox browser. The project name "firefox" was already defined. If you have multiple instances for the same browser, make sure to define a custom "name". All projects in a workspace should have unique names. Make sure your configuration is correct.') }) -test('browser.configs throws an error if no custom name is provided, but the config name is inherited', async () => { +test('browser.instances throws an error if no custom name is provided, but the config name is inherited', async () => { const { stderr } = await runVitest({ name: 'custom', browser: { enabled: true, provider: 'playwright', - configs: [ + instances: [ { browser: 'firefox' }, { browser: 'firefox' }, ], }, }) - expect(stderr).toMatch('Cannot define a nested project for a firefox browser. The project name "custom (firefox)" was already defined. If you have multiple configs for the same browser, make sure to define a custom "name". All projects in a workspace should have unique names. Make sure your configuration is correct.') + expect(stderr).toMatch('Cannot define a nested project for a firefox browser. The project name "custom (firefox)" was already defined. If you have multiple instances for the same browser, make sure to define a custom "name". All projects in a workspace should have unique names. Make sure your configuration is correct.') }) test('throws an error if name conflicts with a workspace name', async () => { @@ -358,7 +358,7 @@ test('throws an error if name conflicts with a workspace name', async () => { browser: { enabled: true, provider: 'playwright', - configs: [ + instances: [ { browser: 'firefox' }, ], }, @@ -375,7 +375,7 @@ test('throws an error if several browsers are headed in nonTTY mode', async () = enabled: true, provider: 'playwright', headless: false, - configs: [ + instances: [ { browser: 'chromium' }, { browser: 'firefox' }, ], From 978e3696f8be5f6bb44bfee83f21d9cef0d2f51d Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Wed, 11 Dec 2024 11:02:49 +0100 Subject: [PATCH 33/51] chore: cleanup --- docs/.vitepress/scripts/cli-generator.ts | 2 +- docs/guide/cli-generated.md | 22 +++++++++++----------- test/browser/vitest.config.mts | 2 +- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/docs/.vitepress/scripts/cli-generator.ts b/docs/.vitepress/scripts/cli-generator.ts index 2aabeab8c95a..4f3ba4fe06cb 100644 --- a/docs/.vitepress/scripts/cli-generator.ts +++ b/docs/.vitepress/scripts/cli-generator.ts @@ -75,7 +75,7 @@ const options = resolveOptions(cliOptionsConfig) const template = options.map((option) => { const title = option.title const cli = option.cli - const config = skipConfig.has(title) ? '' : `[${title}](${title.includes('browser.') ? '/guide/browser/' : '/'}config/#${title.toLowerCase().replace(/\./g, '-')})` + const config = skipConfig.has(title) ? '' : `[${title}](${title.includes('browser.') ? '/guide/browser/config' : '/config/'}#${title.toLowerCase().replace(/\./g, '-')})` return `### ${title}\n\n- **CLI:** ${cli}\n${config ? `- **Config:** ${config}\n` : ''}\n${option.description}\n` }).join('\n') diff --git a/docs/guide/cli-generated.md b/docs/guide/cli-generated.md index b419d25735d9..3a0dc829b266 100644 --- a/docs/guide/cli-generated.md +++ b/docs/guide/cli-generated.md @@ -316,77 +316,77 @@ Mock browser API with happy-dom ### browser.enabled - **CLI:** `--browser.enabled` -- **Config:** [browser.enabled](/guide/browser/config/#browser-enabled) +- **Config:** [browser.enabled](/guide/browser/config#browser-enabled) Run tests in the browser. Equivalent to `--browser.enabled` (default: `false`) ### browser.name - **CLI:** `--browser.name ` -- **Config:** [browser.name](/guide/browser/config/#browser-name) +- **Config:** [browser.name](/guide/browser/config#browser-name) Run all tests in a specific browser. Some browsers are only available for specific providers (see `--browser.provider`). Visit [`browser.name`](https://vitest.dev/guide/browser/config/#browser-name) for more information ### browser.headless - **CLI:** `--browser.headless` -- **Config:** [browser.headless](/guide/browser/config/#browser-headless) +- **Config:** [browser.headless](/guide/browser/config#browser-headless) Run the browser in headless mode (i.e. without opening the GUI (Graphical User Interface)). If you are running Vitest in CI, it will be enabled by default (default: `process.env.CI`) ### browser.api.port - **CLI:** `--browser.api.port [port]` -- **Config:** [browser.api.port](/guide/browser/config/#browser-api-port) +- **Config:** [browser.api.port](/guide/browser/config#browser-api-port) Specify server port. Note if the port is already being used, Vite will automatically try the next available port so this may not be the actual port the server ends up listening on. If true will be set to `63315` ### browser.api.host - **CLI:** `--browser.api.host [host]` -- **Config:** [browser.api.host](/guide/browser/config/#browser-api-host) +- **Config:** [browser.api.host](/guide/browser/config#browser-api-host) Specify which IP addresses the server should listen on. Set this to `0.0.0.0` or `true` to listen on all addresses, including LAN and public addresses ### browser.api.strictPort - **CLI:** `--browser.api.strictPort` -- **Config:** [browser.api.strictPort](/guide/browser/config/#browser-api-strictport) +- **Config:** [browser.api.strictPort](/guide/browser/config#browser-api-strictport) Set to true to exit if port is already in use, instead of automatically trying the next available port ### browser.provider - **CLI:** `--browser.provider ` -- **Config:** [browser.provider](/guide/browser/config/#browser-provider) +- **Config:** [browser.provider](/guide/browser/config#browser-provider) Provider used to run browser tests. Some browsers are only available for specific providers. Can be "webdriverio", "playwright", "preview", or the path to a custom provider. Visit [`browser.provider`](https://vitest.dev/config/#browser-provider) for more information (default: `"preview"`) ### browser.providerOptions - **CLI:** `--browser.providerOptions ` -- **Config:** [browser.providerOptions](/guide/browser/config/#browser-provideroptions) +- **Config:** [browser.providerOptions](/guide/browser/config#browser-provideroptions) Options that are passed down to a browser provider. Visit [`browser.providerOptions`](https://vitest.dev/config/#browser-provideroptions) for more information ### browser.isolate - **CLI:** `--browser.isolate` -- **Config:** [browser.isolate](/guide/browser/config/#browser-isolate) +- **Config:** [browser.isolate](/guide/browser/config#browser-isolate) Run every browser test file in isolation. To disable isolation, use `--browser.isolate=false` (default: `true`) ### browser.ui - **CLI:** `--browser.ui` -- **Config:** [browser.ui](/guide/browser/config/#browser-ui) +- **Config:** [browser.ui](/guide/browser/config#browser-ui) Show Vitest UI when running tests (default: `!process.env.CI`) ### browser.fileParallelism - **CLI:** `--browser.fileParallelism` -- **Config:** [browser.fileParallelism](/guide/browser/config/#browser-fileparallelism) +- **Config:** [browser.fileParallelism](/guide/browser/config#browser-fileparallelism) Should browser test files run in parallel. Use `--browser.fileParallelism=false` to disable (default: `true`) diff --git a/test/browser/vitest.config.mts b/test/browser/vitest.config.mts index 5baef26f7059..979d1eeaf885 100644 --- a/test/browser/vitest.config.mts +++ b/test/browser/vitest.config.mts @@ -39,7 +39,7 @@ export default defineConfig({ browser: { enabled: true, headless: false, - configs: [ + instances: [ { browser }, ], provider, From da3c0c36efca08d8d90dca9dc27341caff25abe2 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Wed, 11 Dec 2024 18:46:55 +0100 Subject: [PATCH 34/51] test: rewrite browser tests to use several browsers --- .github/workflows/ci.yml | 15 +---- docs/advanced/api/vitest.md | 9 ++- .../mocker/src/browser/interceptor-msw.ts | 7 ++- packages/vitest/src/node/core.ts | 14 +++-- .../fixtures/benchmark/vitest.config.ts | 8 +-- .../fixtures/locators/vitest.config.ts | 7 +-- .../fixtures/mocking-watch/vitest.config.ts | 7 +-- .../browser/fixtures/mocking/vitest.config.ts | 7 +-- .../fixtures/server-url/vitest.config.ts | 10 ++-- .../fixtures/setup-file/vitest.config.ts | 7 +-- .../browser/fixtures/timeout/vitest.config.ts | 21 +++---- .../fixtures/unhandled/vitest.config.ts | 7 +-- .../fixtures/update-snapshot/vitest.config.ts | 7 +-- .../fixtures/user-event/vitest.config.ts | 7 +-- test/browser/settings.ts | 25 +++++++++ test/browser/setup.unit.ts | 55 +++++++++++++++++++ test/browser/specs/benchmark.test.ts | 3 +- test/browser/specs/filter.test.ts | 10 ++-- test/browser/specs/fix-4686.test.ts | 17 ++---- test/browser/specs/inspect.test.ts | 4 +- test/browser/specs/locators.test.ts | 15 +++-- test/browser/specs/mocking.test.ts | 50 ++++++++++------- test/browser/specs/runner.test.ts | 38 ++++++------- test/browser/specs/server-url.test.ts | 4 +- test/browser/specs/setup-file.test.ts | 19 +++---- test/browser/specs/unhandled.test.ts | 9 ++- test/browser/specs/update-snapshot.test.ts | 3 +- test/browser/specs/utils.ts | 4 +- test/browser/tsconfig.json | 9 ++- test/browser/vitest.config.mts | 25 +++++++-- test/browser/vitest.config.unit.mts | 6 +- 31 files changed, 258 insertions(+), 171 deletions(-) create mode 100644 test/browser/settings.ts create mode 100644 test/browser/setup.unit.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bae9ce02fa3e..b77c69717315 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -124,7 +124,7 @@ jobs: test-browser: needs: changed - name: 'Browser: ${{ matrix.browser[0] }}, ${{ matrix.os }}' + name: 'Browsers: ${{ matrix.os }}' if: needs.changed.outputs.should_skip != 'true' runs-on: ${{ matrix.os }} @@ -133,10 +133,6 @@ jobs: os: - macos-latest - windows-latest - browser: - - [chromium, chrome] - - [firefox, firefox] - - [webkit] fail-fast: false timeout-minutes: 30 @@ -149,26 +145,19 @@ jobs: node-version: 20 - uses: browser-actions/setup-chrome@v1 - if: ${{ matrix.browser[0] == 'chromium' }} - uses: browser-actions/setup-firefox@v1 - if: ${{ matrix.browser[0] == 'firefox' }} - name: Install run: pnpm i - name: Install Playwright Dependencies - run: pnpm exec playwright install ${{ matrix.browser[0] }} --with-deps --only-shell + run: pnpm exec playwright install --with-deps --only-shell - name: Build run: pnpm run build - name: Test Browser (playwright) run: pnpm run test:browser:playwright - env: - BROWSER: ${{ matrix.browser[0] }} - name: Test Browser (webdriverio) run: pnpm run test:browser:webdriverio - if: ${{ matrix.browser[1] }} - env: - BROWSER: ${{ matrix.browser[1] }} diff --git a/docs/advanced/api/vitest.md b/docs/advanced/api/vitest.md index cb8582ba970f..ad6861e9b98f 100644 --- a/docs/advanced/api/vitest.md +++ b/docs/advanced/api/vitest.md @@ -34,7 +34,6 @@ Vitest 3 is one step closer to stabilising the public API. To achieve that, we d - `changeNamePattern` - `changeFilenamePattern` - `rerunFailed` -- `updateSnapshot` - `_createRootProject` (renamed to `_ensureRootProject`, but still private) - `filterTestsBySource` (this was moved to the new internal `vitest.specifications` instance) - `runFiles` (use [`runTestSpecifications`](#runtestspecifications) instead) @@ -326,6 +325,14 @@ function runTestSpecifications( This method emits `reporter.onWatcherRerun` and `onTestsRerun` events, then it runs tests with [`runTestSpecifications`](#runtestspecifications). If there were no errors in the main process, it will emit `reporter.onWatcherStart` event. +## updateSnapshot + +```ts +function updateSnapshot(files?: string[]): Promise +``` + +Update snapshots in specified files. If no files are provided, it will update files with failed tests and obsolete snapshots. + ## collectTests ```ts diff --git a/packages/mocker/src/browser/interceptor-msw.ts b/packages/mocker/src/browser/interceptor-msw.ts index 53f50899ddad..fe480d913046 100644 --- a/packages/mocker/src/browser/interceptor-msw.ts +++ b/packages/mocker/src/browser/interceptor-msw.ts @@ -128,10 +128,11 @@ export class ModuleMockerMSWInterceptor implements ModuleMockerInterceptor { } } -const timestampRegexp = /(\?|&)t=\d{13}/ -const versionRegexp = /(\?|&)v=\w{8}/ +const trailingSeparatorRE = /[?&]$/ +const timestampRE = /\bt=\d{13}&?\b/ +const versionRE = /\bv=\w{8}&?\b/ function cleanQuery(url: string) { - return url.replace(timestampRegexp, '').replace(versionRegexp, '') + return url.replace(timestampRE, '').replace(versionRE, '').replace(trailingSeparatorRE, '') } function passthrough() { diff --git a/packages/vitest/src/node/core.ts b/packages/vitest/src/node/core.ts index ed67851af28a..6be0b052ab39 100644 --- a/packages/vitest/src/node/core.ts +++ b/packages/vitest/src/node/core.ts @@ -819,7 +819,7 @@ export class Vitest { } /** @internal */ - async rerunFiles(files: string[] = this.state.getFilepaths(), trigger?: string, allTestsRun = true, resetTestNamePattern = false): Promise { + async rerunFiles(files: string[] = this.state.getFilepaths(), trigger?: string, allTestsRun = true, resetTestNamePattern = false): Promise { if (resetTestNamePattern) { this.configOverride.testNamePattern = undefined } @@ -834,9 +834,10 @@ export class Vitest { this.report('onWatcherRerun', files, trigger), ...this._onUserTestsRerun.map(fn => fn(specifications)), ]) - await this.runFiles(specifications, allTestsRun) + const testResult = await this.runFiles(specifications, allTestsRun) await this.report('onWatcherStart', this.state.getFiles(files)) + return testResult } /** @internal */ @@ -902,8 +903,11 @@ export class Vitest { await this.rerunFiles(this.state.getFailedFilepaths(), 'rerun failed', false) } - /** @internal */ - async updateSnapshot(files?: string[]): Promise { + /** + * Update snapshots in specified files. If no files are provided, it will update files with failed tests and obsolete snapshots. + * @param files The list of files on the file system + */ + async updateSnapshot(files?: string[]): Promise { // default to failed files files = files || [ ...this.state.getFailedFilepaths(), @@ -913,7 +917,7 @@ export class Vitest { this.enableSnapshotUpdate() try { - await this.rerunFiles(files, 'update snapshot', false) + return await this.rerunFiles(files, 'update snapshot', false) } finally { this.resetSnapshotUpdate() diff --git a/test/browser/fixtures/benchmark/vitest.config.ts b/test/browser/fixtures/benchmark/vitest.config.ts index 2214155ca2c6..c57a43ee6168 100644 --- a/test/browser/fixtures/benchmark/vitest.config.ts +++ b/test/browser/fixtures/benchmark/vitest.config.ts @@ -1,16 +1,14 @@ import { fileURLToPath } from 'node:url' import { defineConfig } from 'vitest/config' - -const provider = process.env.PROVIDER || 'playwright' -const name = - process.env.BROWSER || (provider === 'playwright' ? 'chromium' : 'chrome') +import { instances, provider } from '../../settings' export default defineConfig({ test: { browser: { enabled: true, + headless: true, provider, - name, + instances, }, }, cacheDir: fileURLToPath(new URL("./node_modules/.vite", import.meta.url)), diff --git a/test/browser/fixtures/locators/vitest.config.ts b/test/browser/fixtures/locators/vitest.config.ts index e32545f4ab22..d8b79ec319ef 100644 --- a/test/browser/fixtures/locators/vitest.config.ts +++ b/test/browser/fixtures/locators/vitest.config.ts @@ -1,9 +1,6 @@ import { fileURLToPath } from 'node:url' import { defineConfig } from 'vitest/config' - -const provider = process.env.PROVIDER || 'playwright' -const name = - process.env.BROWSER || (provider === 'playwright' ? 'chromium' : 'chrome') +import { instances, provider } from '../../settings' export default defineConfig({ optimizeDeps: { @@ -15,8 +12,8 @@ export default defineConfig({ browser: { enabled: true, provider, - name, headless: true, + instances, }, onConsoleLog(log) { if (log.includes('ReactDOMTestUtils.act')) { diff --git a/test/browser/fixtures/mocking-watch/vitest.config.ts b/test/browser/fixtures/mocking-watch/vitest.config.ts index ebe6e47aaef6..74daa1beee99 100644 --- a/test/browser/fixtures/mocking-watch/vitest.config.ts +++ b/test/browser/fixtures/mocking-watch/vitest.config.ts @@ -1,9 +1,6 @@ import { fileURLToPath } from 'node:url' import { defineConfig } from 'vitest/config' - -const provider = process.env.PROVIDER || 'playwright' -const name = - process.env.BROWSER || (provider === 'playwright' ? 'chromium' : 'chrome') +import { instances, provider } from '../../settings' export default defineConfig({ optimizeDeps: { @@ -15,7 +12,7 @@ export default defineConfig({ fileParallelism: false, enabled: true, provider, - name, + instances, headless: true, }, }, diff --git a/test/browser/fixtures/mocking/vitest.config.ts b/test/browser/fixtures/mocking/vitest.config.ts index 3620bdc48ef4..f72ca8935f64 100644 --- a/test/browser/fixtures/mocking/vitest.config.ts +++ b/test/browser/fixtures/mocking/vitest.config.ts @@ -1,9 +1,6 @@ import { fileURLToPath } from 'node:url' import { defineConfig } from 'vitest/config' - -const provider = process.env.PROVIDER || 'playwright' -const name = - process.env.BROWSER || (provider === 'playwright' ? 'chromium' : 'chrome') +import { instances, provider } from '../../settings' export default defineConfig({ optimizeDeps: { @@ -15,7 +12,7 @@ export default defineConfig({ browser: { enabled: true, provider, - name, + instances, headless: true, }, }, diff --git a/test/browser/fixtures/server-url/vitest.config.ts b/test/browser/fixtures/server-url/vitest.config.ts index 25eb22e5965b..ad483f64e89d 100644 --- a/test/browser/fixtures/server-url/vitest.config.ts +++ b/test/browser/fixtures/server-url/vitest.config.ts @@ -2,13 +2,11 @@ import path from 'node:path' import { fileURLToPath } from 'node:url' import { defineConfig } from 'vitest/config' import basicSsl from '@vitejs/plugin-basic-ssl' +import { instances, provider } from '../../settings' // test https by // TEST_HTTPS=1 pnpm test-fixtures --root fixtures/server-url -const provider = process.env.PROVIDER || 'playwright'; -const browser = process.env.BROWSER || (provider === 'playwright' ? 'chromium' : 'chrome'); - // ignore https errors due to self-signed certificate from plugin-basic-ssl // https://playwright.dev/docs/api/class-browser#browser-new-context-option-ignore-https-errors // https://webdriver.io/docs/configuration/#strictssl and acceptInsecureCerts in https://webdriver.io/docs/api/browser/#properties @@ -28,8 +26,10 @@ export default defineConfig({ api: process.env.TEST_HTTPS ? 51122 : 51133, enabled: true, provider, - name: browser, - providerOptions, + instances: instances.map(instance => ({ + ...instance, + ...providerOptions, + })), }, }, // separate cacheDir from test/browser/vite.config.ts diff --git a/test/browser/fixtures/setup-file/vitest.config.ts b/test/browser/fixtures/setup-file/vitest.config.ts index 8172e44773a7..e14b3cbf47f9 100644 --- a/test/browser/fixtures/setup-file/vitest.config.ts +++ b/test/browser/fixtures/setup-file/vitest.config.ts @@ -1,9 +1,6 @@ import { fileURLToPath } from 'node:url' import { defineConfig } from 'vitest/config' - -const provider = process.env.PROVIDER || 'playwright' -const name = - process.env.BROWSER || (provider === 'playwright' ? 'chromium' : 'chrome') +import { instances, provider } from '../../settings' export default defineConfig({ cacheDir: fileURLToPath(new URL("./node_modules/.vite", import.meta.url)), @@ -12,7 +9,7 @@ export default defineConfig({ browser: { enabled: true, provider, - name, + instances, headless: false, }, }, diff --git a/test/browser/fixtures/timeout/vitest.config.ts b/test/browser/fixtures/timeout/vitest.config.ts index 997fa84abcc4..b482d8bb44e6 100644 --- a/test/browser/fixtures/timeout/vitest.config.ts +++ b/test/browser/fixtures/timeout/vitest.config.ts @@ -1,9 +1,6 @@ import { fileURLToPath } from 'node:url' import { defineConfig } from 'vitest/config' - -const provider = process.env.PROVIDER || 'playwright' -const name = - process.env.BROWSER || (provider === 'playwright' ? 'chromium' : 'chrome') +import { instances, provider } from '../../settings' export default defineConfig({ cacheDir: fileURLToPath(new URL("./node_modules/.vite", import.meta.url)), @@ -11,17 +8,17 @@ export default defineConfig({ browser: { enabled: true, provider, - name, - providerOptions: { + instances: instances.map(instance => ({ + ...instance, context: { - actionTimeout: 500 - } - } + actionTimeout: 500, + }, + })), }, expect: { poll: { - timeout: 500 - } - } + timeout: 500, + }, + }, }, }) diff --git a/test/browser/fixtures/unhandled/vitest.config.ts b/test/browser/fixtures/unhandled/vitest.config.ts index 5f5d430812b3..c98221e84305 100644 --- a/test/browser/fixtures/unhandled/vitest.config.ts +++ b/test/browser/fixtures/unhandled/vitest.config.ts @@ -1,9 +1,6 @@ import { fileURLToPath } from 'node:url' import { defineConfig } from 'vitest/config' - -const provider = process.env.PROVIDER || 'playwright' -const name = - process.env.BROWSER || (provider === 'playwright' ? 'chromium' : 'chrome') +import { instances, provider } from '../../settings' export default defineConfig({ cacheDir: fileURLToPath(new URL("./node_modules/.vite", import.meta.url)), @@ -11,7 +8,7 @@ export default defineConfig({ browser: { enabled: true, provider, - name, + instances, headless: true, }, }, diff --git a/test/browser/fixtures/update-snapshot/vitest.config.ts b/test/browser/fixtures/update-snapshot/vitest.config.ts index aec07d121a3f..23e4c31f07ff 100644 --- a/test/browser/fixtures/update-snapshot/vitest.config.ts +++ b/test/browser/fixtures/update-snapshot/vitest.config.ts @@ -1,22 +1,19 @@ import path from 'node:path' import { fileURLToPath } from 'node:url' import { defineConfig } from 'vitest/config' +import { instances, provider } from '../../settings' /* manually test snapshot by pnpm -C test/browser test-fixtures --root fixtures/update-snapshot */ -const provider = process.env.PROVIDER || 'playwright' -const browser = - process.env.BROWSER || (provider === 'playwright' ? 'chromium' : 'chrome') - export default defineConfig({ test: { browser: { enabled: true, provider, - name: browser, + instances, }, }, cacheDir: path.join( diff --git a/test/browser/fixtures/user-event/vitest.config.ts b/test/browser/fixtures/user-event/vitest.config.ts index c3fe79b6ac9c..52962b06a2cc 100644 --- a/test/browser/fixtures/user-event/vitest.config.ts +++ b/test/browser/fixtures/user-event/vitest.config.ts @@ -1,9 +1,6 @@ import { fileURLToPath } from 'node:url' import { defineConfig } from 'vitest/config' - -const provider = process.env.PROVIDER || 'playwright' -const name = - process.env.BROWSER || (provider === 'playwright' ? 'chromium' : 'chrome') +import { provider, instances } from '../../settings' export default defineConfig({ cacheDir: fileURLToPath(new URL("./node_modules/.vite", import.meta.url)), @@ -11,7 +8,7 @@ export default defineConfig({ browser: { enabled: true, provider, - name, + instances, }, }, }) diff --git a/test/browser/settings.ts b/test/browser/settings.ts new file mode 100644 index 000000000000..19022e7baaef --- /dev/null +++ b/test/browser/settings.ts @@ -0,0 +1,25 @@ +import type { BrowserInstanceOption } from 'vitest/node' + +export const provider = process.env.PROVIDER || 'playwright' +export const browser = process.env.BROWSER || (provider !== 'playwright' ? 'chromium' : 'chrome') + +const devInstances: BrowserInstanceOption[] = [ + { browser }, +] + +const playwrightInstances: BrowserInstanceOption[] = [ + { browser: 'chromium' }, + { browser: 'firefox' }, + { browser: 'webkit' }, +] + +const webdriverioInstances: BrowserInstanceOption[] = [ + { browser: 'chrome' }, + { browser: 'firefox' }, +] + +export const instances = process.env.BROWSER + ? devInstances + : provider === 'playwright' + ? playwrightInstances + : webdriverioInstances diff --git a/test/browser/setup.unit.ts b/test/browser/setup.unit.ts new file mode 100644 index 000000000000..dd69c73378c1 --- /dev/null +++ b/test/browser/setup.unit.ts @@ -0,0 +1,55 @@ +import { expect } from 'vitest' + +interface SummaryOptions { + passed?: number +} + +expect.extend({ + toReportPassedTest(stdout: string, testName: string, testProject?: string) { + const includePattern = `✓ ${testProject ? `|${testProject}| ` : ''}${testName}` + const pass = stdout.includes(`✓ ${testProject ? `|${testProject}| ` : ''}${testName}`) + return { + pass, + message: () => `expected ${pass ? 'not ' : ''}to have "${includePattern}" in the report.\n\nstdout:\n${stdout}`, + } + }, + toReportSummaryTestFiles(stdout: string, { passed }: SummaryOptions) { + const includePattern = `Test Files ${passed} passed` + const pass = !passed || stdout.includes(includePattern) + return { + pass, + message: () => `expected ${pass ? 'not ' : ''}to have "${includePattern}" in the report.\n\nstdout:\n${stdout}`, + } + }, + toReportSummaryTests(stdout: string, { passed }: SummaryOptions) { + const includePattern = `Tests ${passed} passed` + const pass = !passed || stdout.includes(includePattern) + return { + pass, + message: () => `expected ${pass ? 'not ' : ''}to have "${includePattern}" in the report.\n\nstdout:\n${stdout}`, + } + }, + toReportNoErrors(stderr: string) { + const pass = !stderr + return { + pass, + message: () => `expected ${pass ? 'not ' : ''}to have no errors.\n\nstderr:\n${stderr}`, + } + }, +}) + +declare module 'vitest' { + // eslint-disable-next-line unused-imports/no-unused-vars + interface Assertion { + // eslint-disable-next-line ts/method-signature-style + toReportPassedTest(testName: string, testProject?: string): void + // eslint-disable-next-line ts/method-signature-style + toReportSummaryTestFiles(options: SummaryOptions): void + // eslint-disable-next-line ts/method-signature-style + toReportSummaryTests(options: SummaryOptions): void + // eslint-disable-next-line ts/method-signature-style + toReportNoErrors(): void + } +} + +export {} diff --git a/test/browser/specs/benchmark.test.ts b/test/browser/specs/benchmark.test.ts index ac5cec0f7959..1b3eb94b5d7c 100644 --- a/test/browser/specs/benchmark.test.ts +++ b/test/browser/specs/benchmark.test.ts @@ -3,7 +3,8 @@ import { runVitest } from '../../test-utils' test('benchmark', async () => { const result = await runVitest({ root: 'fixtures/benchmark' }, [], 'benchmark') - expect(result.stderr).toBe('') + expect(result.stderr).toReportNoErrors() + // TODO 2024-12-11 check |name| when it's supported expect(result.stdout).toContain('✓ basic.bench.ts > suite-a') expect(result.exitCode).toBe(0) }) diff --git a/test/browser/specs/filter.test.ts b/test/browser/specs/filter.test.ts index 35a85f3dd2eb..864bfbc6fd83 100644 --- a/test/browser/specs/filter.test.ts +++ b/test/browser/specs/filter.test.ts @@ -1,5 +1,5 @@ import { expect, test } from 'vitest' -import { browser, runBrowserTests } from './utils' +import { instances, runBrowserTests } from './utils' test('filter', async () => { const { stderr, stdout } = await runBrowserTests({ @@ -8,7 +8,9 @@ test('filter', async () => { }, ['test/basic.test.ts']) expect(stderr).toBe('') - expect(stdout).toContain(`✓ |${browser}| test/basic.test.ts > basic 2`) - expect(stdout).toContain('Test Files 1 passed') - expect(stdout).toContain('Tests 1 passed | 3 skipped') + instances.forEach(({ browser }) => { + expect(stdout).toReportPassedTest('test/basic.test.ts > basic 2', browser) + }) + expect(stdout).toContain(`Test Files ${instances.length} passed`) + expect(stdout).toContain(`Tests ${instances.length} passed | ${instances.length * 3} skipped`) }) diff --git a/test/browser/specs/fix-4686.test.ts b/test/browser/specs/fix-4686.test.ts index 85e9e0af2e3b..bbc1846d96e0 100644 --- a/test/browser/specs/fix-4686.test.ts +++ b/test/browser/specs/fix-4686.test.ts @@ -1,10 +1,10 @@ // fix #4686 import { expect, test } from 'vitest' -import { runBrowserTests } from './utils' +import { instances, runBrowserTests } from './utils' test('tests run in presence of config.base', async () => { - const { stderr, ctx } = await runBrowserTests( + const { stderr, stdout } = await runBrowserTests( { config: './vitest.config-basepath.mts', }, @@ -12,13 +12,8 @@ test('tests run in presence of config.base', async () => { ) expect(stderr).toBe('') - expect( - Object.fromEntries( - ctx.state.getFiles().map(f => [f.name, f.result.state]), - ), - ).toMatchInlineSnapshot(` - { - "test/basic.test.ts": "pass", - } - `) + + instances.forEach(({ browser }) => { + expect(stdout).toContain(`✓ |${browser}| test/basic.test.ts`) + }) }) diff --git a/test/browser/specs/inspect.test.ts b/test/browser/specs/inspect.test.ts index b186d10889e2..efc063984fbc 100644 --- a/test/browser/specs/inspect.test.ts +++ b/test/browser/specs/inspect.test.ts @@ -6,10 +6,10 @@ import { runVitestCli } from '../../test-utils' type Message = Partial> -const IS_PLAYWRIGHT_CHROMIUM = process.env.BROWSER === 'chromium' && process.env.PROVIDER === 'playwright' +const IS_PLAYWRIGHT = process.env.PROVIDER === 'playwright' const REMOTE_DEBUG_URL = '127.0.0.1:9123' -test.runIf(IS_PLAYWRIGHT_CHROMIUM || !process.env.CI)('--inspect-brk stops at test file', async () => { +test.runIf(IS_PLAYWRIGHT || !process.env.CI)('--inspect-brk stops at test file', async () => { const { vitest, waitForClose } = await runVitestCli( '--root', 'fixtures/inspect', diff --git a/test/browser/specs/locators.test.ts b/test/browser/specs/locators.test.ts index 753926422536..55003c55ab7b 100644 --- a/test/browser/specs/locators.test.ts +++ b/test/browser/specs/locators.test.ts @@ -1,5 +1,5 @@ import { expect, test } from 'vitest' -import { runBrowserTests } from './utils' +import { instances, runBrowserTests } from './utils' test('locators work correctly', async () => { const { stderr, stdout } = await runBrowserTests({ @@ -7,8 +7,13 @@ test('locators work correctly', async () => { reporters: [['verbose', { isTTY: false }]], }) - expect(stderr).toBe('') - expect(stdout).toContain('✓ blog.test.tsx') - expect(stdout).toContain('✓ query.test.ts') - expect(stdout).toContain('Test Files 2 passed (2)') + expect(stderr).toReportNoErrors() + + instances.forEach(({ browser }) => { + expect(stdout).toReportPassedTest('blog.test.tsx', browser) + expect(stdout).toReportPassedTest('query.test.ts', browser) + }) + + expect(stdout).toReportSummaryTestFiles({ passed: instances.length * 2 }) + expect(stdout).toReportSummaryTests({ passed: instances.length * 3 }) }) diff --git a/test/browser/specs/mocking.test.ts b/test/browser/specs/mocking.test.ts index a42fc91957fd..1f2a8855fcc3 100644 --- a/test/browser/specs/mocking.test.ts +++ b/test/browser/specs/mocking.test.ts @@ -1,5 +1,6 @@ import { expect, onTestFailed, onTestFinished, test } from 'vitest' import { editFile, runVitest } from '../../test-utils' +import { instances } from '../settings' test.each([true, false])('mocking works correctly - isolated %s', async (isolate) => { const result = await runVitest({ @@ -12,19 +13,23 @@ test.each([true, false])('mocking works correctly - isolated %s', async (isolate console.error(result.stderr) }) - expect(result.stderr).toBe('') - expect(result.stdout).toContain('automocked.test.ts') - expect(result.stdout).toContain('mocked-__mocks__.test.ts') - expect(result.stdout).toContain('mocked-factory.test.ts') - expect(result.stdout).toContain('mocked-factory-hoisted.test.ts') - expect(result.stdout).toContain('not-mocked.test.ts') - expect(result.stdout).toContain('mocked-nested.test.ts') - expect(result.stdout).toContain('not-mocked-nested.test.ts') - expect(result.stdout).toContain('import-actual-in-mock.test.ts') - expect(result.stdout).toContain('import-actual-query.test.ts') - expect(result.stdout).toContain('import-mock.test.ts') - expect(result.stdout).toContain('mocked-do-mock-factory.test.ts') - expect(result.stdout).toContain('import-actual-dep.test.ts') + expect(result.stderr).toReportNoErrors() + + instances.forEach(({ browser }) => { + expect(result.stdout).toReportPassedTest('automocked.test.ts', browser) + expect(result.stdout).toReportPassedTest('mocked-__mocks__.test.ts', browser) + expect(result.stdout).toReportPassedTest('mocked-factory.test.ts', browser) + expect(result.stdout).toReportPassedTest('mocked-factory-hoisted.test.ts', browser) + expect(result.stdout).toReportPassedTest('not-mocked.test.ts', browser) + expect(result.stdout).toReportPassedTest('mocked-nested.test.ts', browser) + expect(result.stdout).toReportPassedTest('not-mocked-nested.test.ts', browser) + expect(result.stdout).toReportPassedTest('import-actual-in-mock.test.ts', browser) + expect(result.stdout).toReportPassedTest('import-actual-query.test.ts', browser) + expect(result.stdout).toReportPassedTest('import-mock.test.ts', browser) + expect(result.stdout).toReportPassedTest('mocked-do-mock-factory.test.ts', browser) + expect(result.stdout).toReportPassedTest('import-actual-dep.test.ts', browser) + }) + expect(result.exitCode).toBe(0) }) @@ -35,21 +40,26 @@ test('mocking dependency correctly invalidates it on rerun', async () => { }) onTestFinished(async () => { await ctx.close() - await ctx.closingPromise }) await vitest.waitForStdout('Waiting for file changes...') - expect(vitest.stderr).toBe('') - expect(vitest.stdout).toContain('1_mocked-on-watch-change.test.ts') - expect(vitest.stdout).toContain('2_not-mocked-import.test.ts') + expect(vitest.stderr).toReportNoErrors() + + instances.forEach(({ browser }) => { + expect(vitest.stdout).toReportPassedTest('1_mocked-on-watch-change.test.ts', browser) + expect(vitest.stdout).toReportPassedTest('2_not-mocked-import.test.ts', browser) + }) vitest.resetOutput() editFile('./fixtures/mocking-watch/1_mocked-on-watch-change.test.ts', content => `${content}\n`) await vitest.waitForStdout('Waiting for file changes...') - expect(vitest.stderr).toBe('') - expect(vitest.stdout).toContain('1_mocked-on-watch-change.test.ts') - expect(vitest.stdout).not.toContain('2_not-mocked-import.test.ts') + expect(vitest.stderr).toReportNoErrors() + + instances.forEach(({ browser }) => { + expect(vitest.stdout).toReportPassedTest('1_mocked-on-watch-change.test.ts', browser) + expect(vitest.stdout).not.toReportPassedTest('2_not-mocked-import.test.ts', browser) + }) }) diff --git a/test/browser/specs/runner.test.ts b/test/browser/specs/runner.test.ts index 4ccc02129dbe..64dfaafdb0e2 100644 --- a/test/browser/specs/runner.test.ts +++ b/test/browser/specs/runner.test.ts @@ -1,6 +1,6 @@ import { readFile } from 'node:fs/promises' import { beforeAll, describe, expect, onTestFailed, test } from 'vitest' -import { browser, provider, runBrowserTests } from './utils' +import { instances, provider, runBrowserTests } from './utils' describe('running browser tests', async () => { let stderr: string @@ -28,9 +28,9 @@ describe('running browser tests', async () => { console.error(stderr) }) - expect(browserResultJson.testResults).toHaveLength(19) - expect(passedTests).toHaveLength(17) - expect(failedTests).toHaveLength(2) + expect(browserResultJson.testResults).toHaveLength(19 * instances.length) + expect(passedTests).toHaveLength(17 * instances.length) + expect(failedTests).toHaveLength(2 * instances.length) expect(stderr).not.toContain('optimized dependencies changed') expect(stderr).not.toContain('has been externalized for browser compatibility') @@ -89,7 +89,7 @@ describe('running browser tests', async () => { expect(stderr).toMatch(/hello from console.trace\s+(\w+|@)/) }) - test.runIf(browser !== 'webkit')(`logs have stack traces in non-safari`, () => { + test(`logs have stack traces`, () => { expect(stdout).toMatch(` log with a stack ❯ test/logs.test.ts:58:10 @@ -100,20 +100,20 @@ error with a stack `.trim()) // console.trace processes the stack trace correctly expect(stderr).toMatch('test/logs.test.ts:60:10') - }) - test.runIf(browser === 'webkit')(`logs have stack traces in safari`, () => { + if (instances.some(({ browser }) => browser === 'webkit')) { // safari print stack trace in a different place - expect(stdout).toMatch(` + expect(stdout).toMatch(` log with a stack ❯ test/logs.test.ts:58:14 `.trim()) - expect(stderr).toMatch(` + expect(stderr).toMatch(` error with a stack ❯ test/logs.test.ts:59:16 `.trim()) - // console.trace processes the stack trace correctly - expect(stderr).toMatch('test/logs.test.ts:60:16') + // console.trace processes the stack trace correctly + expect(stderr).toMatch('test/logs.test.ts:60:16') + } }) test(`stack trace points to correct file in every browser`, () => { @@ -141,17 +141,15 @@ error with a stack }) test('user-event', async () => { - const { ctx } = await runBrowserTests({ + const { stdout } = await runBrowserTests({ root: './fixtures/user-event', }) - expect(Object.fromEntries(ctx.state.getFiles().map(f => [f.name, f.result.state]))).toMatchInlineSnapshot(` - { - "cleanup-retry.test.ts": "pass", - "cleanup1.test.ts": "pass", - "cleanup2.test.ts": "pass", - "keyboard.test.ts": "pass", - } - `) + instances.forEach(({ browser }) => { + expect(stdout).toReportPassedTest('cleanup-retry.test.ts', browser) + expect(stdout).toReportPassedTest('cleanup1.test.ts', browser) + expect(stdout).toReportPassedTest('cleanup2.test.ts', browser) + expect(stdout).toReportPassedTest('keyboard.test.ts', browser) + }) }) test('timeout', async () => { diff --git a/test/browser/specs/server-url.test.ts b/test/browser/specs/server-url.test.ts index 153cbd1cba55..0a1c5a5d5fc3 100644 --- a/test/browser/specs/server-url.test.ts +++ b/test/browser/specs/server-url.test.ts @@ -1,5 +1,5 @@ import { afterEach, expect, test } from 'vitest' -import { runBrowserTests } from './utils' +import { instances, runBrowserTests } from './utils' afterEach(() => { delete process.env.TEST_HTTPS @@ -24,5 +24,5 @@ test('server-url https', async () => { expect(stderr).toBe('') const url = ctx?.projects[0].browser?.vite.resolvedUrls?.local[0] expect(url).toBe('https://localhost:51122/') - expect(stdout).toContain('Test Files 1 passed') + expect(stdout).toReportSummaryTestFiles({ passed: instances.length }) }) diff --git a/test/browser/specs/setup-file.test.ts b/test/browser/specs/setup-file.test.ts index bdf97071bbe5..73699beb1b6e 100644 --- a/test/browser/specs/setup-file.test.ts +++ b/test/browser/specs/setup-file.test.ts @@ -1,23 +1,18 @@ // fix https://github.com/vitest-dev/vitest/issues/6690 import { expect, test } from 'vitest' -import { runBrowserTests } from './utils' +import { instances, runBrowserTests } from './utils' test('setup file imports the same modules', async () => { - const { stderr, ctx } = await runBrowserTests( + const { stderr, stdout } = await runBrowserTests( { root: './fixtures/setup-file', }, ) - expect(stderr).toBe('') - expect( - Object.fromEntries( - ctx.state.getFiles().map(f => [f.name, f.result.state]), - ), - ).toMatchInlineSnapshot(` - { - "module-equality.test.ts": "pass", - } - `) + expect(stderr).toReportNoErrors() + + instances.forEach(({ browser }) => { + expect(stdout).toReportPassedTest('module-equality.test.ts', browser) + }) }) diff --git a/test/browser/specs/unhandled.test.ts b/test/browser/specs/unhandled.test.ts index a45ed8da2997..c05ebc95a2a1 100644 --- a/test/browser/specs/unhandled.test.ts +++ b/test/browser/specs/unhandled.test.ts @@ -1,15 +1,14 @@ import { expect, test } from 'vitest' -import { browser, runBrowserTests } from './utils' +import { instances, runBrowserTests } from './utils' test('prints correct unhandled error stack', async () => { const { stderr } = await runBrowserTests({ root: './fixtures/unhandled', }) - if (browser === 'webkit') { + expect(stderr).toContain('throw-unhandled-error.test.ts:9:10') + + if (instances.some(({ browser }) => browser === 'webkit')) { expect(stderr).toContain('throw-unhandled-error.test.ts:9:20') } - else { - expect(stderr).toContain('throw-unhandled-error.test.ts:9:10') - } }) diff --git a/test/browser/specs/update-snapshot.test.ts b/test/browser/specs/update-snapshot.test.ts index 688d776874b4..d61ad03fb72d 100644 --- a/test/browser/specs/update-snapshot.test.ts +++ b/test/browser/specs/update-snapshot.test.ts @@ -1,7 +1,7 @@ import { readFileSync } from 'node:fs' import { expect, onTestFailed, onTestFinished, test } from 'vitest' import { createFile, editFile } from '../../test-utils' -import { runBrowserTests } from './utils' +import { instances, runBrowserTests } from './utils' test('update snapshot', async () => { // setup wrong snapshot value @@ -15,6 +15,7 @@ test('update snapshot', async () => { const ctx = await runBrowserTests({ watch: true, root: './fixtures/update-snapshot', + project: [instances[0].browser], // TODO 2024-12-11 Sheremet V.A. test with multiple browsers reporters: ['default'], // use simple reporter to not pollute stdout browser: { headless: true }, }, [], { diff --git a/test/browser/specs/utils.ts b/test/browser/specs/utils.ts index 2b29e55d7a69..f46242609e6e 100644 --- a/test/browser/specs/utils.ts +++ b/test/browser/specs/utils.ts @@ -1,9 +1,9 @@ import type { UserConfig as ViteUserConfig } from 'vite' import type { UserConfig } from 'vitest/node' import { runVitest } from '../../test-utils' +import { browser } from '../settings' -export const provider = process.env.PROVIDER || 'playwright' -export const browser = process.env.BROWSER || (provider !== 'playwright' ? 'chromium' : 'chrome') +export { browser, instances, provider } from '../settings' export async function runBrowserTests( config?: Omit & { browser?: Partial }, diff --git a/test/browser/tsconfig.json b/test/browser/tsconfig.json index 083a747ea373..fe4b26cb6b5f 100644 --- a/test/browser/tsconfig.json +++ b/test/browser/tsconfig.json @@ -13,5 +13,12 @@ "vitest/import-meta" ], "esModuleInterop": true - } + }, + "include": [ + "fixtures/**/*.ts", + "test/**/*.ts", + "specs/**/*.ts", + "./vitest.config.*", + "./setup.unit.ts" + ] } diff --git a/test/browser/vitest.config.mts b/test/browser/vitest.config.mts index 979d1eeaf885..0436736e63ec 100644 --- a/test/browser/vitest.config.mts +++ b/test/browser/vitest.config.mts @@ -1,4 +1,4 @@ -import type { BrowserCommand } from 'vitest/node' +import type { BrowserCommand, BrowserInstanceOption } from 'vitest/node' import { dirname, resolve } from 'node:path' import { fileURLToPath } from 'node:url' import * as util from 'node:util' @@ -19,6 +19,21 @@ const stripVTControlCharacters: BrowserCommand<[text: string]> = (_, text) => { return util.stripVTControlCharacters(text) } +const devInstances: BrowserInstanceOption[] = [ + { browser }, +] + +const playwrightInstances: BrowserInstanceOption[] = [ + { browser: 'chromium' }, + { browser: 'firefox' }, + { browser: 'webkit' }, +] + +const webdriverioInstances: BrowserInstanceOption[] = [ + { browser: 'chrome' }, + { browser: 'firefox' }, +] + export default defineConfig({ server: { headers: { @@ -39,9 +54,11 @@ export default defineConfig({ browser: { enabled: true, headless: false, - instances: [ - { browser }, - ], + instances: process.env.BROWSER + ? devInstances + : provider === 'playwright' + ? playwrightInstances + : webdriverioInstances, provider, isolate: false, testerScripts: [ diff --git a/test/browser/vitest.config.unit.mts b/test/browser/vitest.config.unit.mts index 0618ce0ca59a..8bcb174b0ae8 100644 --- a/test/browser/vitest.config.unit.mts +++ b/test/browser/vitest.config.unit.mts @@ -8,7 +8,9 @@ export default defineConfig({ singleFork: true, }, }, - hookTimeout: process.env.CI ? 120_000 : 20_000, - testTimeout: process.env.CI ? 120_000 : 20_000, + setupFiles: ['./setup.unit.ts'], + // 3 is the maximum of browser instances - in a perfect world they will run in parallel + hookTimeout: process.env.CI ? 120_000 * 3 : 20_000, + testTimeout: process.env.CI ? 120_000 * 3 : 20_000, }, }) From 9ba935d5db2769e7bc608255e0be95d89362f9ec Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Wed, 11 Dec 2024 18:56:09 +0100 Subject: [PATCH 35/51] fix: don't return ctx.updateSnapshot --- packages/vitest/src/api/setup.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/vitest/src/api/setup.ts b/packages/vitest/src/api/setup.ts index 879d662b60ad..d362d2ba5bc3 100644 --- a/packages/vitest/src/api/setup.ts +++ b/packages/vitest/src/api/setup.ts @@ -97,11 +97,13 @@ export function setup(ctx: Vitest, _server?: ViteDevServer) { async getModuleGraph(project, id, browser): Promise { return getModuleGraph(ctx, project, id, browser) }, - updateSnapshot(file?: File) { + async updateSnapshot(file?: File) { if (!file) { - return ctx.updateSnapshot() + await ctx.updateSnapshot() + } + else { + await ctx.updateSnapshot([file.filepath]) } - return ctx.updateSnapshot([file.filepath]) }, getUnhandledErrors() { return ctx.state.getUnhandledErrors() From 99e3ae2e0db560f5c35c647be9fcada45d180a5e Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Thu, 12 Dec 2024 10:04:06 +0100 Subject: [PATCH 36/51] chore: add node-20 to ci title --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b77c69717315..b043692271de 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -124,7 +124,7 @@ jobs: test-browser: needs: changed - name: 'Browsers: ${{ matrix.os }}' + name: 'Browsers: node-20, ${{ matrix.os }}' if: needs.changed.outputs.should_skip != 'true' runs-on: ${{ matrix.os }} From 71f91d67b60ca24e841ffa07a5b9fcd6a8c63355 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Mon, 16 Dec 2024 14:53:57 +0100 Subject: [PATCH 37/51] fix: use local browser server and allow --project filter --- .../browser/src/node/serverOrchestrator.ts | 47 ++++++++++--------- packages/browser/src/node/serverTester.ts | 32 +++++++------ .../src/node/workspace/resolveWorkspace.ts | 8 ++++ test/config/test/browser-configs.test.ts | 19 ++++++++ 4 files changed, 68 insertions(+), 38 deletions(-) diff --git a/packages/browser/src/node/serverOrchestrator.ts b/packages/browser/src/node/serverOrchestrator.ts index 10c3c260e9fe..43378886b43c 100644 --- a/packages/browser/src/node/serverOrchestrator.ts +++ b/packages/browser/src/node/serverOrchestrator.ts @@ -3,30 +3,31 @@ import type { BrowserServer } from './server' import { replacer } from './utils' export async function resolveOrchestrator( - server: BrowserServer, + globalServer: BrowserServer, url: URL, res: ServerResponse, ) { let sessionId = url.searchParams.get('sessionId') // it's possible to open the page without a context if (!sessionId) { - const contexts = [...server.state.orchestrators.keys()] + const contexts = [...globalServer.state.orchestrators.keys()] sessionId = contexts[contexts.length - 1] ?? 'none' } - const contextState = server.vitest._browserSessions.getSession(sessionId!) + const contextState = globalServer.vitest._browserSessions.getSession(sessionId!) const files = contextState?.files ?? [] + const browserServer = contextState?.project.browser as BrowserServer || globalServer - const injectorJs = typeof server.injectorJs === 'string' - ? server.injectorJs - : await server.injectorJs + const injectorJs = typeof browserServer.injectorJs === 'string' + ? browserServer.injectorJs + : await browserServer.injectorJs const injector = replacer(injectorJs, { - __VITEST_PROVIDER__: JSON.stringify(server.config.browser.provider || 'preview'), + __VITEST_PROVIDER__: JSON.stringify(browserServer.config.browser.provider || 'preview'), // TODO: check when context is not found - __VITEST_CONFIG__: JSON.stringify(server.wrapSerializedConfig(contextState?.project.name || '')), + __VITEST_CONFIG__: JSON.stringify(browserServer.wrapSerializedConfig(contextState?.project.name || '')), __VITEST_VITE_CONFIG__: JSON.stringify({ - root: server.vite.config.root, + root: browserServer.vite.config.root, }), __VITEST_FILES__: JSON.stringify(files), __VITEST_TYPE__: '"orchestrator"', @@ -38,9 +39,9 @@ export async function resolveOrchestrator( // disable CSP for the orchestrator as we are the ones controlling it res.removeHeader('Content-Security-Policy') - if (!server.orchestratorScripts) { - server.orchestratorScripts = (await server.formatScripts( - server.config.browser.orchestratorScripts, + if (!globalServer.orchestratorScripts) { + globalServer.orchestratorScripts = (await globalServer.formatScripts( + globalServer.config.browser.orchestratorScripts, )).map((script) => { let html = '`, - __VITEST_ERROR_CATCHER__: ``, + __VITEST_ERROR_CATCHER__: ``, __VITEST_SESSION_ID__: JSON.stringify(sessionId), }) } diff --git a/packages/browser/src/node/serverTester.ts b/packages/browser/src/node/serverTester.ts index 225d0ea098a7..61e8c7b3abcf 100644 --- a/packages/browser/src/node/serverTester.ts +++ b/packages/browser/src/node/serverTester.ts @@ -7,7 +7,7 @@ import { join } from 'pathe' import { replacer } from './utils' export async function resolveTester( - server: BrowserServer, + globalServer: BrowserServer, url: URL, res: ServerResponse, next: Connect.NextFunction, @@ -22,10 +22,10 @@ export async function resolveTester( ) } - const { sessionId, testFile } = server.resolveTesterUrl(url.pathname) - const session = server.vitest._browserSessions.getSession(sessionId) + const { sessionId, testFile } = globalServer.resolveTesterUrl(url.pathname) + const session = globalServer.vitest._browserSessions.getSession(sessionId) // TODO: if no session, 400 - const project = server.vitest.getProjectByName(session?.project.name ?? '') + const project = globalServer.vitest.getProjectByName(session?.project.name ?? '') const { testFiles } = await project.globTestFiles() // if decoded test file is "__vitest_all__" or not in the list of known files, run all tests const tests @@ -37,16 +37,18 @@ export async function resolveTester( const files = session?.files ?? [] const method = session?.method ?? 'run' - const injectorJs = typeof server.injectorJs === 'string' - ? server.injectorJs - : await server.injectorJs + // TODO: test for different configurations in multi-browser instances + const browserServer = project.browser as BrowserServer || globalServer + const injectorJs: string = typeof browserServer.injectorJs === 'string' + ? browserServer.injectorJs + : await browserServer.injectorJs const injector = replacer(injectorJs, { __VITEST_PROVIDER__: JSON.stringify(project.browser!.provider.name), - __VITEST_CONFIG__: JSON.stringify(server.wrapSerializedConfig(project.name)), + __VITEST_CONFIG__: JSON.stringify(browserServer.wrapSerializedConfig(project.name)), __VITEST_FILES__: JSON.stringify(files), __VITEST_VITE_CONFIG__: JSON.stringify({ - root: server.vite.config.root, + root: browserServer.vite.config.root, }), __VITEST_TYPE__: '"tester"', __VITEST_SESSION_ID__: JSON.stringify(sessionId), @@ -54,15 +56,15 @@ export async function resolveTester( __VITEST_PROVIDED_CONTEXT__: JSON.stringify(stringify(project.getProvidedContext())), }) - const testerHtml = typeof server.testerHtml === 'string' - ? server.testerHtml - : await server.testerHtml + const testerHtml = typeof browserServer.testerHtml === 'string' + ? browserServer.testerHtml + : await browserServer.testerHtml try { - const url = join('/@fs/', server.testerFilepath) - const indexhtml = await server.vite.transformIndexHtml(url, testerHtml) + const url = join('/@fs/', browserServer.testerFilepath) + const indexhtml = await browserServer.vite.transformIndexHtml(url, testerHtml) return replacer(indexhtml, { - __VITEST_FAVICON__: server.faviconUrl, + __VITEST_FAVICON__: globalServer.faviconUrl, __VITEST_INJECTOR__: injector, __VITEST_APPEND__: ` __vitest_browser_runner__.runningFiles = ${tests} diff --git a/packages/vitest/src/node/workspace/resolveWorkspace.ts b/packages/vitest/src/node/workspace/resolveWorkspace.ts index 64eb29c7c5cc..7c703797bf2c 100644 --- a/packages/vitest/src/node/workspace/resolveWorkspace.ts +++ b/packages/vitest/src/node/workspace/resolveWorkspace.ts @@ -3,6 +3,7 @@ import type { BrowserInstanceOption, ResolvedConfig, TestProjectConfiguration, U import { existsSync, promises as fs } from 'node:fs' import os from 'node:os' import { limitConcurrency } from '@vitest/runner/utils' +import { toArray } from '@vitest/utils' import fg from 'fast-glob' import { dirname, relative, resolve } from 'pathe' import { mergeConfig } from 'vite' @@ -137,6 +138,8 @@ export async function resolveBrowserWorkspace( resolvedProjects: TestProject[], ) { const newConfigs: [project: TestProject, config: ResolvedConfig][] = [] + const filter = new Set() + toArray(vitest.config.project).forEach(name => filter.add(name)) resolvedProjects.forEach((project) => { const configs = project.config.browser.instances @@ -173,6 +176,11 @@ export async function resolveBrowserWorkspace( } const name = config.name const newName = name || (originalName ? `${originalName} (${browser})` : browser) + // skip the project if it's filtered out + if (filter.size && !filter.has(newName)) { + return + } + if (names.has(newName)) { throw new Error( [ diff --git a/test/config/test/browser-configs.test.ts b/test/config/test/browser-configs.test.ts index 1e5d1c27b783..254bb64c856f 100644 --- a/test/config/test/browser-configs.test.ts +++ b/test/config/test/browser-configs.test.ts @@ -11,6 +11,7 @@ test('assignes names as browsers', async () => { const { projects } = await vitest({ browser: { enabled: true, + headless: true, instances: [ { browser: 'chromium' }, { browser: 'firefox' }, @@ -25,6 +26,23 @@ test('assignes names as browsers', async () => { ]) }) +test('filters projects', async () => { + const { projects } = await vitest({ + project: 'chromium', + browser: { + enabled: true, + instances: [ + { browser: 'chromium' }, + { browser: 'firefox' }, + { browser: 'webkit' }, + ], + }, + }) + expect(projects.map(p => p.name)).toEqual([ + 'chromium', + ]) +}) + test('assignes names as browsers in a custom project', async () => { const { projects } = await vitest({ workspace: [ @@ -33,6 +51,7 @@ test('assignes names as browsers in a custom project', async () => { name: 'custom', browser: { enabled: true, + headless: true, instances: [ { browser: 'chromium' }, { browser: 'firefox' }, From b28dbeb2853884360864e965a2d05614d768a741 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Mon, 16 Dec 2024 16:33:55 +0100 Subject: [PATCH 38/51] fix: filter with a regexp --- .../src/node/workspace/resolveWorkspace.ts | 6 +++--- test/config/test/browser-configs.test.ts | 17 +++++++++++++++++ 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/packages/vitest/src/node/workspace/resolveWorkspace.ts b/packages/vitest/src/node/workspace/resolveWorkspace.ts index 7c703797bf2c..00823a357e3b 100644 --- a/packages/vitest/src/node/workspace/resolveWorkspace.ts +++ b/packages/vitest/src/node/workspace/resolveWorkspace.ts @@ -8,6 +8,7 @@ import fg from 'fast-glob' import { dirname, relative, resolve } from 'pathe' import { mergeConfig } from 'vite' import { configFiles as defaultConfigFiles } from '../../constants' +import { wildcardPatternToRegExp } from '../../utils/base' import { isTTY } from '../../utils/env' import { initializeProject, TestProject } from '../project' import { withLabel } from '../reporters/renderers/utils' @@ -138,8 +139,7 @@ export async function resolveBrowserWorkspace( resolvedProjects: TestProject[], ) { const newConfigs: [project: TestProject, config: ResolvedConfig][] = [] - const filter = new Set() - toArray(vitest.config.project).forEach(name => filter.add(name)) + const filters = toArray(vitest.config.project).map(s => wildcardPatternToRegExp(s)) resolvedProjects.forEach((project) => { const configs = project.config.browser.instances @@ -177,7 +177,7 @@ export async function resolveBrowserWorkspace( const name = config.name const newName = name || (originalName ? `${originalName} (${browser})` : browser) // skip the project if it's filtered out - if (filter.size && !filter.has(newName)) { + if (filters.length && !filters.some(pattern => pattern.test(newName))) { return } diff --git a/test/config/test/browser-configs.test.ts b/test/config/test/browser-configs.test.ts index 254bb64c856f..efdd91b47396 100644 --- a/test/config/test/browser-configs.test.ts +++ b/test/config/test/browser-configs.test.ts @@ -43,6 +43,23 @@ test('filters projects', async () => { ]) }) +test('filters projects with a wildecard', async () => { + const { projects } = await vitest({ + project: 'chrom*', + browser: { + enabled: true, + instances: [ + { browser: 'chromium' }, + { browser: 'firefox' }, + { browser: 'webkit' }, + ], + }, + }) + expect(projects.map(p => p.name)).toEqual([ + 'chromium', + ]) +}) + test('assignes names as browsers in a custom project', async () => { const { projects } = await vitest({ workspace: [ From 0ca97b8b8c16f80c2fb5ddb892372da10b87caea Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Tue, 17 Dec 2024 11:20:18 +0100 Subject: [PATCH 39/51] feat: add connect timeout for the websocket connection --- docs/guide/browser/config.md | 35 +++++++++++++------- packages/browser/src/node/rpc.ts | 28 +++++++++++++--- packages/browser/src/node/server.ts | 2 +- packages/vitest/src/node/browser/sessions.ts | 8 +++++ packages/vitest/src/node/types/browser.ts | 7 ++++ 5 files changed, 62 insertions(+), 18 deletions(-) diff --git a/docs/guide/browser/config.md b/docs/guide/browser/config.md index 170af5d2b0ea..e71099de3667 100644 --- a/docs/guide/browser/config.md +++ b/docs/guide/browser/config.md @@ -120,7 +120,7 @@ Run all tests in a specific browser. Possible options in different providers: - `playwright`: `firefox`, `webkit`, `chromium` - custom: any string that will be passed to the provider -## browser.headless {#browser-headless} +## browser.headless - **Type:** `boolean` - **Default:** `process.env.CI` @@ -128,7 +128,7 @@ Run all tests in a specific browser. Possible options in different providers: Run the browser in a `headless` mode. If you are running Vitest in CI, it will be enabled by default. -## browser.isolate {#browser-isolate} +## browser.isolate - **Type:** `boolean` - **Default:** `true` @@ -136,13 +136,13 @@ Run the browser in a `headless` mode. If you are running Vitest in CI, it will b Run every test in a separate iframe. -## browser.testerHtmlPath {#browser-testerhtmlpath} +## browser.testerHtmlPath - **Type:** `string` A path to the HTML entry point. Can be relative to the root of the project. This file will be processed with [`transformIndexHtml`](https://vite.dev/guide/api-plugin#transformindexhtml) hook. -## browser.api {#browser-api} +## browser.api - **Type:** `number | { port?, strictPort?, host? }` - **Default:** `63315` @@ -215,7 +215,7 @@ To have a better type safety when using built-in providers, you should reference ``` ::: -## browser.ui {#browser-ui} +## browser.ui - **Type:** `boolean` - **Default:** `!isCI` @@ -223,14 +223,14 @@ To have a better type safety when using built-in providers, you should reference Should Vitest UI be injected into the page. By default, injects UI iframe during development. -## browser.viewport {#browser-viewport} +## browser.viewport - **Type:** `{ width, height }` - **Default:** `414x896` Default iframe's viewport. -## browser.locators {#browser-locators} +## browser.locators Options for built-in [browser locators](/guide/browser/locators). @@ -241,21 +241,21 @@ Options for built-in [browser locators](/guide/browser/locators). Attribute used to find elements with `getByTestId` locator. -## browser.screenshotDirectory {#browser-screenshotdirectory} +## browser.screenshotDirectory - **Type:** `string` - **Default:** `__snapshots__` in the test file directory Path to the screenshots directory relative to the `root`. -## browser.screenshotFailures {#browser-screenshotfailures} +## browser.screenshotFailures - **Type:** `boolean` - **Default:** `!browser.ui` Should Vitest take screenshots if the test fails. -## browser.orchestratorScripts {#browser-orchestratorscripts} +## browser.orchestratorScripts - **Type:** `BrowserScript[]` - **Default:** `[]` @@ -295,7 +295,7 @@ export interface BrowserScript { } ``` -## browser.testerScripts {#browser-testerscripts} +## browser.testerScripts - **Type:** `BrowserScript[]` - **Default:** `[]` @@ -308,9 +308,20 @@ Custom scripts that should be injected into the tester HTML before the tests env The script `src` and `content` will be processed by Vite plugins. -## browser.commands {#browser-commands} +## browser.commands - **Type:** `Record` - **Default:** `{ readFile, writeFile, ... }` Custom [commands](/guide/browser/commands) that can be imported during browser tests from `@vitest/browser/commands`. + +## browser.connectTimeout + +- **Type:** `number` +- **Default:** `60_000` + +The timeout in milliseconds. If connection to the browser takes longer, the test suite will fail. + +::: info +This is the time it should take for the browser to establish the WebSocket connection with the Vitest server. In normal circumstances, this timeout should never be reached. +::: diff --git a/packages/browser/src/node/rpc.ts b/packages/browser/src/node/rpc.ts index 930da3a093f1..6076466d5f4a 100644 --- a/packages/browser/src/node/rpc.ts +++ b/packages/browser/src/node/rpc.ts @@ -34,21 +34,33 @@ export function setupBrowserRpc(server: BrowserServer) { } const type = searchParams.get('type') - const sessionId = searchParams.get('sessionId') ?? '' const rpcId = searchParams.get('rpcId') const projectName = searchParams.get('projectName') if (type !== 'tester' && type !== 'orchestrator') { - throw new Error(`[vitest] Type query in ${request.url} is invalid. Type should be either "tester" or "orchestrator".`) + return error( + new Error(`[vitest] Type query in ${request.url} is invalid. Type should be either "tester" or "orchestrator".`), + ) + } + + if (!rpcId || projectName == null) { + return error( + new Error(`[vitest] Invalid URL ${request.url}. "projectName", "sessionId" and "rpcId" queries are required.`), + ) } - if (!sessionId || !rpcId || projectName == null) { - throw new Error(`[vitest] Invalid URL ${request.url}. "projectName", "sessionId" and "rpcId" are required.`) + + if (type === 'orchestrator') { + const session = vitest._browserSessions.getSession(rpcId) + // it's possible the session was already resolved by the preview provider + session?.connected() } const project = vitest.getProjectByName(projectName) if (!project) { - throw new Error(`[vitest] Project "${projectName}" not found.`) + return error( + new Error(`[vitest] Project "${projectName}" not found.`), + ) } wss.handleUpgrade(request, socket, head, (ws) => { @@ -69,6 +81,12 @@ export function setupBrowserRpc(server: BrowserServer) { }) }) + // we don't throw an error inside a stream because this can segfault the process + function error(err: Error) { + console.error(err) + vitest.state.catchError(err, 'RPC Error') + } + function checkFileAccess(path: string) { if (!isFileServingAllowed(path, vite)) { throw new Error( diff --git a/packages/browser/src/node/server.ts b/packages/browser/src/node/server.ts index 1e6723bfedd8..4ecd50d527ed 100644 --- a/packages/browser/src/node/server.ts +++ b/packages/browser/src/node/server.ts @@ -276,7 +276,7 @@ export class BrowserServer implements IBrowserServer { return handler } - async removeCDPHandler(sessionId: string) { + removeCDPHandler(sessionId: string) { this.cdps.delete(sessionId) } diff --git a/packages/vitest/src/node/browser/sessions.ts b/packages/vitest/src/node/browser/sessions.ts index 9233a9f6d9a7..06c702025260 100644 --- a/packages/vitest/src/node/browser/sessions.ts +++ b/packages/vitest/src/node/browser/sessions.ts @@ -11,10 +11,18 @@ export class BrowserSessions { createAsyncSession(method: 'run' | 'collect', sessionId: string, files: string[], project: TestProject): Promise { const defer = createDefer() + + const timeout = setTimeout(() => { + defer.reject(new Error(`Failed to connect to the browser session "${sessionId}" within the timeout.`)) + }, project.vitest.config.browser.connectTimeout ?? 60_000).unref() + this.sessions.set(sessionId, { files, method, project, + connected: () => { + clearTimeout(timeout) + }, resolve: () => { defer.resolve() this.sessions.delete(sessionId) diff --git a/packages/vitest/src/node/types/browser.ts b/packages/vitest/src/node/types/browser.ts index 87acdf69b214..7e915d6068c5 100644 --- a/packages/vitest/src/node/types/browser.ts +++ b/packages/vitest/src/node/types/browser.ts @@ -220,6 +220,12 @@ export interface BrowserConfigOptions { * @see {@link https://vitest.dev/guide/browser/commands} */ commands?: Record> + + /** + * Timeout for connecting to the browser + * @default 30000 + */ + connectTimeout?: number } export interface BrowserCommandContext { @@ -235,6 +241,7 @@ export interface BrowserServerStateSession { files: string[] method: 'run' | 'collect' project: TestProject + connected: () => void resolve: () => void reject: (v: unknown) => void } From 57a0af14f607885509edc9fd9ad6e5fc74334df0 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Tue, 17 Dec 2024 11:20:34 +0100 Subject: [PATCH 40/51] fix: fail tester if session is invalid --- packages/browser/src/node/serverOrchestrator.ts | 12 +++++++----- packages/browser/src/node/serverTester.ts | 16 +++++++++++----- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/packages/browser/src/node/serverOrchestrator.ts b/packages/browser/src/node/serverOrchestrator.ts index 43378886b43c..b9a3975c92b2 100644 --- a/packages/browser/src/node/serverOrchestrator.ts +++ b/packages/browser/src/node/serverOrchestrator.ts @@ -14,9 +14,12 @@ export async function resolveOrchestrator( sessionId = contexts[contexts.length - 1] ?? 'none' } - const contextState = globalServer.vitest._browserSessions.getSession(sessionId!) - const files = contextState?.files ?? [] - const browserServer = contextState?.project.browser as BrowserServer || globalServer + // it's ok to not have a session here, especially in the preview provider + // because the user could refresh the page which would remove the session id from the url + + const session = globalServer.vitest._browserSessions.getSession(sessionId!) + const files = session?.files ?? [] + const browserServer = session?.project.browser as BrowserServer || globalServer const injectorJs = typeof browserServer.injectorJs === 'string' ? browserServer.injectorJs @@ -24,8 +27,7 @@ export async function resolveOrchestrator( const injector = replacer(injectorJs, { __VITEST_PROVIDER__: JSON.stringify(browserServer.config.browser.provider || 'preview'), - // TODO: check when context is not found - __VITEST_CONFIG__: JSON.stringify(browserServer.wrapSerializedConfig(contextState?.project.name || '')), + __VITEST_CONFIG__: JSON.stringify(browserServer.wrapSerializedConfig(session?.project.name || '')), __VITEST_VITE_CONFIG__: JSON.stringify({ root: browserServer.vite.config.root, }), diff --git a/packages/browser/src/node/serverTester.ts b/packages/browser/src/node/serverTester.ts index 61e8c7b3abcf..825fdadc17db 100644 --- a/packages/browser/src/node/serverTester.ts +++ b/packages/browser/src/node/serverTester.ts @@ -24,8 +24,14 @@ export async function resolveTester( const { sessionId, testFile } = globalServer.resolveTesterUrl(url.pathname) const session = globalServer.vitest._browserSessions.getSession(sessionId) - // TODO: if no session, 400 - const project = globalServer.vitest.getProjectByName(session?.project.name ?? '') + + if (!session) { + res.statusCode = 400 + res.end('Invalid session ID') + return + } + + const project = globalServer.vitest.getProjectByName(session.project.name || '') const { testFiles } = await project.globTestFiles() // if decoded test file is "__vitest_all__" or not in the list of known files, run all tests const tests @@ -34,8 +40,8 @@ export async function resolveTester( ? '__vitest_browser_runner__.files' : JSON.stringify([testFile]) const iframeId = JSON.stringify(testFile) - const files = session?.files ?? [] - const method = session?.method ?? 'run' + const files = session.files ?? [] + const method = session.method ?? 'run' // TODO: test for different configurations in multi-browser instances const browserServer = project.browser as BrowserServer || globalServer @@ -75,7 +81,7 @@ export async function resolveTester( }) } catch (err) { - session?.reject(err) + session.reject(err) next(err) } } From 0d62e6dadaf464860d5e9b3b991c947173213024 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Tue, 17 Dec 2024 14:12:58 +0100 Subject: [PATCH 41/51] fix: check for body height before saving a screenshot --- packages/browser/src/client/tester/runner.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/browser/src/client/tester/runner.ts b/packages/browser/src/client/tester/runner.ts index 40ff4501b2c5..ae455c131dc9 100644 --- a/packages/browser/src/client/tester/runner.ts +++ b/packages/browser/src/client/tester/runner.ts @@ -61,8 +61,13 @@ export function createBrowserRunner( } onTaskFinished = async (task: Task) => { - if (this.config.browser.screenshotFailures && task.result?.state === 'fail') { - task.meta.failScreenshotPath = await page.screenshot() + if (this.config.browser.screenshotFailures && document.body.clientHeight > 0 && task.result?.state === 'fail') { + const screenshot = await page.screenshot().catch((err) => { + console.error('[vitest] Failed to take a screenshot', err) + }) + if (screenshot) { + task.meta.failScreenshotPath = screenshot + } } } From 1e41e34b9f2da9fb44733731baa58f23c38a5a9e Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Tue, 17 Dec 2024 14:13:15 +0100 Subject: [PATCH 42/51] chore: add more debug statements to the orchestrator --- packages/browser/src/client/orchestrator.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/browser/src/client/orchestrator.ts b/packages/browser/src/client/orchestrator.ts index 31227616363c..4450ed22213c 100644 --- a/packages/browser/src/client/orchestrator.ts +++ b/packages/browser/src/client/orchestrator.ts @@ -14,9 +14,7 @@ class IframeOrchestrator { private runningFiles = new Set() private iframes = new Map() - public async init() { - const testFiles = getBrowserState().files - + public async init(testFiles: string[]) { debug('test files', testFiles.join(', ')) this.runningFiles.clear() @@ -38,6 +36,7 @@ class IframeOrchestrator { testFiles.forEach(file => this.runningFiles.add(file)) const config = getConfig() + debug('create testers', testFiles.join(', ')) const container = await getContainer(config) if (config.browser.ui) { @@ -51,6 +50,7 @@ class IframeOrchestrator { this.iframes.clear() if (config.isolate === false) { + debug('create iframe', ID_ALL) const iframe = this.createIframe(container, ID_ALL) await setIframeViewport(iframe, width, height) @@ -63,6 +63,7 @@ class IframeOrchestrator { return } + debug('create iframe', file) const iframe = this.createIframe(container, file) await setIframeViewport(iframe, width, height) @@ -233,7 +234,7 @@ async function getContainer(config: SerializedConfig): Promise { client.waitForConnection().then(async () => { const testFiles = getBrowserState().files - await orchestrator.init() + await orchestrator.init(testFiles) // if page was refreshed, there will be no test files // createTesters will be called again when tests are running in the UI From 603e323be7f5170cd875e7c5e681712bec0a8432 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Tue, 17 Dec 2024 16:20:21 +0100 Subject: [PATCH 43/51] fix: make sure the cloned config doesn't reference the root config --- eslint.config.js | 2 +- packages/browser/src/node/index.ts | 20 +- .../middlewares/orchestratorMiddleware.ts | 18 +- .../src/node/middlewares/testerMiddleware.ts | 9 +- packages/browser/src/node/plugin.ts | 96 +++++---- .../browser/src/node/plugins/pluginContext.ts | 22 +- packages/browser/src/node/project.ts | 122 +++++++++++ .../src/node/{server.ts => projectParent.ts} | 202 +++++++----------- .../browser/src/node/providers/playwright.ts | 1 + packages/browser/src/node/rpc.ts | 29 +-- .../browser/src/node/serverOrchestrator.ts | 27 ++- packages/browser/src/node/serverTester.ts | 40 ++-- packages/runner/src/fixture.ts | 4 - packages/runner/src/types/tasks.ts | 4 + packages/vitest/src/node/browser/sessions.ts | 2 +- packages/vitest/src/node/project.ts | 112 +++++----- packages/vitest/src/node/types/browser.ts | 6 +- .../src/node/workspace/resolveWorkspace.ts | 48 ++--- packages/vitest/src/public/node.ts | 3 +- .../multiple-different-configs/basic.test.js | 23 ++ .../customTester.html | 13 ++ .../vitest.config.js | 30 +++ test/browser/package.json | 1 + .../specs/multiple-different-configs.test.ts | 14 ++ 24 files changed, 498 insertions(+), 350 deletions(-) create mode 100644 packages/browser/src/node/project.ts rename packages/browser/src/node/{server.ts => projectParent.ts} (66%) create mode 100644 test/browser/fixtures/multiple-different-configs/basic.test.js create mode 100644 test/browser/fixtures/multiple-different-configs/customTester.html create mode 100644 test/browser/fixtures/multiple-different-configs/vitest.config.js create mode 100644 test/browser/specs/multiple-different-configs.test.ts diff --git a/eslint.config.js b/eslint.config.js index 2b17354694d6..1bccb592d1ec 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -79,7 +79,7 @@ export default antfu( 'no-restricted-imports': [ 'error', { - paths: ['vitest', 'path'], + paths: ['vitest', 'path', 'vitest/node'], }, ], }, diff --git a/packages/browser/src/node/index.ts b/packages/browser/src/node/index.ts index dca4ba971633..fd1143f65254 100644 --- a/packages/browser/src/node/index.ts +++ b/packages/browser/src/node/index.ts @@ -1,16 +1,16 @@ import type { Plugin } from 'vitest/config' -import type { BrowserServer as IBrowserServer, TestProject } from 'vitest/node' +import type { TestProject } from 'vitest/node' import c from 'tinyrainbow' import { createViteLogger, createViteServer } from 'vitest/node' import { version } from '../../package.json' import BrowserPlugin from './plugin' +import { ParentBrowserProject } from './projectParent' import { setupBrowserRpc } from './rpc' -import { BrowserServer } from './server' export { distRoot } from './constants' export { createBrowserPool } from './pool' -export type { BrowserServer } from './server' +export type { ProjectBrowser } from './project' export async function createBrowserServer( project: TestProject, @@ -28,7 +28,7 @@ export async function createBrowserServer( ) } - const server = new BrowserServer(project, '/') + const server = new ParentBrowserProject(project, '/') const configPath = typeof configFile === 'string' ? configFile : false @@ -77,15 +77,3 @@ export async function createBrowserServer( return server } - -export function cloneBrowserServer( - project: TestProject, - browserServer: IBrowserServer, -) { - const clone = new BrowserServer( - project, - '/', - ) - clone.setServer(browserServer.vite) - return clone -} diff --git a/packages/browser/src/node/middlewares/orchestratorMiddleware.ts b/packages/browser/src/node/middlewares/orchestratorMiddleware.ts index c4d9039ba2fb..c6d5d8848a8d 100644 --- a/packages/browser/src/node/middlewares/orchestratorMiddleware.ts +++ b/packages/browser/src/node/middlewares/orchestratorMiddleware.ts @@ -1,23 +1,25 @@ import type { Connect } from 'vite' -import type { BrowserServer } from '../server' +import type { ParentBrowserProject } from '../projectParent' import { resolveOrchestrator } from '../serverOrchestrator' import { allowIframes, disableCache } from './utils' -export function createOrchestratorMiddleware(browserServer: BrowserServer): Connect.NextHandleFunction { +export function createOrchestratorMiddleware(parentServer: ParentBrowserProject): Connect.NextHandleFunction { return async function vitestOrchestratorMiddleware(req, res, next) { if (!req.url) { return next() } const url = new URL(req.url, 'http://localhost') - if (url.pathname !== browserServer.base) { + if (url.pathname !== parentServer.base) { return next() } - disableCache(res) - allowIframes(res) + const html = await resolveOrchestrator(parentServer, url, res) + if (html) { + disableCache(res) + allowIframes(res) - const html = await resolveOrchestrator(browserServer, url, res) - res.write(html, 'utf-8') - res.end() + res.write(html, 'utf-8') + res.end() + } } } diff --git a/packages/browser/src/node/middlewares/testerMiddleware.ts b/packages/browser/src/node/middlewares/testerMiddleware.ts index cb10e068857a..435ff7149ed1 100644 --- a/packages/browser/src/node/middlewares/testerMiddleware.ts +++ b/packages/browser/src/node/middlewares/testerMiddleware.ts @@ -1,9 +1,9 @@ import type { Connect } from 'vite' -import type { BrowserServer } from '../server' +import type { ParentBrowserProject } from '../projectParent' import { resolveTester } from '../serverTester' import { allowIframes, disableCache } from './utils' -export function createTesterMiddleware(browserServer: BrowserServer): Connect.NextHandleFunction { +export function createTesterMiddleware(browserServer: ParentBrowserProject): Connect.NextHandleFunction { return async function vitestTesterMiddleware(req, res, next) { if (!req.url) { return next() @@ -13,11 +13,10 @@ export function createTesterMiddleware(browserServer: BrowserServer): Connect.Ne return next() } - disableCache(res) - allowIframes(res) - const html = await resolveTester(browserServer, url, res, next) if (html) { + disableCache(res) + allowIframes(res) res.write(html, 'utf-8') res.end() } diff --git a/packages/browser/src/node/plugin.ts b/packages/browser/src/node/plugin.ts index a6a27050a272..73f43899d760 100644 --- a/packages/browser/src/node/plugin.ts +++ b/packages/browser/src/node/plugin.ts @@ -1,7 +1,7 @@ import type { Stats } from 'node:fs' import type { HtmlTagDescriptor } from 'vite' import type { Vitest } from 'vitest/node' -import type { BrowserServer } from './server' +import type { ParentBrowserProject } from './projectParent' import { lstatSync, readFileSync } from 'node:fs' import { createRequire } from 'node:module' import { dynamicImportPlugin } from '@vitest/mocker/node' @@ -21,9 +21,9 @@ export type { BrowserCommand } from 'vitest/node' const versionRegexp = /(?:\?|&)v=\w{8}/ -export default (browserServer: BrowserServer, base = '/'): Plugin[] => { +export default (parentServer: ParentBrowserProject, base = '/'): Plugin[] => { function isPackageExists(pkg: string, root: string) { - return browserServer.vitest.packageInstaller.isPackageExists?.(pkg, { + return parentServer.vitest.packageInstaller.isPackageExists?.(pkg, { paths: [root], }) } @@ -33,7 +33,7 @@ export default (browserServer: BrowserServer, base = '/'): Plugin[] => { enforce: 'pre', name: 'vitest:browser', async configureServer(server) { - browserServer.setServer(server) + parentServer.setServer(server) // eslint-disable-next-line prefer-arrow-callback server.middlewares.use(function vitestHeaders(_req, res, next) { @@ -45,8 +45,8 @@ export default (browserServer: BrowserServer, base = '/'): Plugin[] => { } next() }) - server.middlewares.use(createOrchestratorMiddleware(browserServer)) - server.middlewares.use(createTesterMiddleware(browserServer)) + server.middlewares.use(createOrchestratorMiddleware(parentServer)) + server.middlewares.use(createTesterMiddleware(parentServer)) server.middlewares.use( `${base}favicon.svg`, @@ -57,7 +57,7 @@ export default (browserServer: BrowserServer, base = '/'): Plugin[] => { }, ) - const coverageFolder = resolveCoverageFolder(browserServer.vitest) + const coverageFolder = resolveCoverageFolder(parentServer.vitest) const coveragePath = coverageFolder ? coverageFolder[1] : undefined if (coveragePath && base === coveragePath) { throw new Error( @@ -81,12 +81,12 @@ export default (browserServer: BrowserServer, base = '/'): Plugin[] => { ) } - const screenshotFailures = browserServer.config.browser.ui && browserServer.config.browser.screenshotFailures + const uiEnabled = parentServer.config.browser.ui - if (screenshotFailures) { + if (uiEnabled) { // eslint-disable-next-line prefer-arrow-callback server.middlewares.use(`${base}__screenshot-error`, function vitestBrowserScreenshotError(req, res) { - if (!req.url || !browserServer.provider) { + if (!req.url) { res.statusCode = 404 res.end() return @@ -152,17 +152,17 @@ export default (browserServer: BrowserServer, base = '/'): Plugin[] => { name: 'vitest:browser:tests', enforce: 'pre', async config() { - const project = browserServer.vitest.getProjectByName(browserServer.config.name) + const project = parentServer.vitest.getProjectByName(parentServer.config.name) const { testFiles: allTestFiles } = await project.globTestFiles() const browserTestFiles = allTestFiles.filter( file => getFilePoolName(project, file) === 'browser', ) - const setupFiles = toArray(browserServer.config.setupFiles) + const setupFiles = toArray(project.config.setupFiles) // replace env values - cannot be reassign at runtime const define: Record = {} - for (const env in (browserServer.config.env || {})) { - const stringValue = JSON.stringify(browserServer.config.env[env]) + for (const env in (project.config.env || {})) { + const stringValue = JSON.stringify(project.config.env[env]) define[`import.meta.env.${env}`] = stringValue } @@ -173,7 +173,7 @@ export default (browserServer: BrowserServer, base = '/'): Plugin[] => { resolve(vitestDist, 'browser.js'), resolve(vitestDist, 'runners.js'), resolve(vitestDist, 'utils.js'), - ...(browserServer.config.snapshotSerializers || []), + ...(project.config.snapshotSerializers || []), ] const exclude = [ @@ -199,22 +199,22 @@ export default (browserServer: BrowserServer, base = '/'): Plugin[] => { 'msw/browser', ] - if (typeof browserServer.config.diff === 'string') { - entries.push(browserServer.config.diff) + if (typeof project.config.diff === 'string') { + entries.push(project.config.diff) } - if (browserServer.vitest.coverageProvider) { - const coverage = browserServer.vitest.config.coverage + if (parentServer.vitest.coverageProvider) { + const coverage = parentServer.vitest.config.coverage const provider = coverage.provider if (provider === 'v8') { - const path = tryResolve('@vitest/coverage-v8', [browserServer.config.root]) + const path = tryResolve('@vitest/coverage-v8', [parentServer.config.root]) if (path) { entries.push(path) exclude.push('@vitest/coverage-v8/browser') } } else if (provider === 'istanbul') { - const path = tryResolve('@vitest/coverage-istanbul', [browserServer.config.root]) + const path = tryResolve('@vitest/coverage-istanbul', [parentServer.config.root]) if (path) { entries.push(path) exclude.push('@vitest/coverage-istanbul') @@ -235,7 +235,7 @@ export default (browserServer: BrowserServer, base = '/'): Plugin[] => { '@vitest/browser > @testing-library/dom', ] - const fileRoot = browserTestFiles[0] ? dirname(browserTestFiles[0]) : browserServer.config.root + const fileRoot = browserTestFiles[0] ? dirname(browserTestFiles[0]) : project.config.root const svelte = isPackageExists('vitest-browser-svelte', fileRoot) if (svelte) { @@ -302,14 +302,14 @@ export default (browserServer: BrowserServer, base = '/'): Plugin[] => { } }, transform(code, id) { - if (id.includes(browserServer.vite.config.cacheDir) && id.includes('loupe.js')) { + if (id.includes(parentServer.vite.config.cacheDir) && id.includes('loupe.js')) { // loupe bundle has a nastry require('util') call that leaves a warning in the console const utilRequire = 'nodeUtil = require_util();' return code.replace(utilRequire, ' '.repeat(utilRequire.length)) } }, }, - BrowserContext(browserServer), + BrowserContext(parentServer), dynamicImportPlugin({ globalThisAccessor: '"__vitest_browser_runner__"', filter(id) { @@ -329,7 +329,7 @@ export default (browserServer: BrowserServer, base = '/'): Plugin[] => { viteConfig.esbuild.legalComments = 'inline' } - const defaultPort = browserServer.vitest._browserLastPort++ + const defaultPort = parentServer.vitest._browserLastPort++ const api = resolveApiServerConfig( viteConfig.test?.browser || {}, @@ -347,8 +347,8 @@ export default (browserServer: BrowserServer, base = '/'): Plugin[] => { viteConfig.server.fs.allow = viteConfig.server.fs.allow || [] viteConfig.server.fs.allow.push( ...resolveFsAllow( - browserServer.vitest.config.root, - browserServer.vitest.server.config.configFile, + parentServer.vitest.config.root, + parentServer.vitest.vite.config.configFile, ), distRoot, ) @@ -363,7 +363,7 @@ export default (browserServer: BrowserServer, base = '/'): Plugin[] => { { name: 'vitest:browser:in-source-tests', transform(code, id) { - const project = browserServer.vitest.getProjectByName(browserServer.config.name) + const project = parentServer.vitest.getProjectByName(parentServer.config.name) if (!project.isCachedTestFile(id) || !code.includes('import.meta.vitest')) { return } @@ -395,26 +395,30 @@ export default (browserServer: BrowserServer, base = '/'): Plugin[] => { name: 'vitest:browser:transform-tester-html', enforce: 'pre', async transformIndexHtml(html, ctx) { - if (ctx.filename !== browserServer.testerFilepath) { + const projectBrowser = [...parentServer.children].find((server) => { + return ctx.filename === server.testerFilepath + }) + if (!projectBrowser) { return } - if (!browserServer.testerScripts) { - const testerScripts = await browserServer.formatScripts( - browserServer.config.browser.testerScripts, + if (!parentServer.testerScripts) { + const testerScripts = await parentServer.formatScripts( + parentServer.config.browser.testerScripts, ) - browserServer.testerScripts = testerScripts + parentServer.testerScripts = testerScripts } - const stateJs = typeof browserServer.stateJs === 'string' - ? browserServer.stateJs - : await browserServer.stateJs + const stateJs = typeof parentServer.stateJs === 'string' + ? parentServer.stateJs + : await parentServer.stateJs const testerTags: HtmlTagDescriptor[] = [] - const isDefaultTemplate = resolve(distRoot, 'client/tester/tester.html') === browserServer.testerFilepath + + const isDefaultTemplate = resolve(distRoot, 'client/tester/tester.html') === projectBrowser.testerFilepath if (!isDefaultTemplate) { - const manifestContent = browserServer.manifest instanceof Promise - ? await browserServer.manifest - : browserServer.manifest + const manifestContent = parentServer.manifest instanceof Promise + ? await parentServer.manifest + : parentServer.manifest const testerEntry = manifestContent['tester/tester.html'] testerTags.push({ @@ -422,7 +426,7 @@ export default (browserServer: BrowserServer, base = '/'): Plugin[] => { attrs: { type: 'module', crossorigin: '', - src: `${browserServer.base}${testerEntry.file}`, + src: `${parentServer.base}${testerEntry.file}`, }, injectTo: 'head', }) @@ -434,7 +438,7 @@ export default (browserServer: BrowserServer, base = '/'): Plugin[] => { { tag: 'link', attrs: { - href: `${browserServer.base}${entryManifest.file}`, + href: `${parentServer.base}${entryManifest.file}`, rel: 'modulepreload', crossorigin: '', }, @@ -478,21 +482,21 @@ body { tag: 'script', attrs: { type: 'module', - src: browserServer.errorCatcherUrl, + src: parentServer.errorCatcherUrl, }, injectTo: 'head' as const, }, - browserServer.locatorsUrl + parentServer.locatorsUrl ? { tag: 'script', attrs: { type: 'module', - src: browserServer.locatorsUrl, + src: parentServer.locatorsUrl, }, injectTo: 'head', } as const : null, - ...browserServer.testerScripts, + ...parentServer.testerScripts, ...testerTags, { tag: 'script', diff --git a/packages/browser/src/node/plugins/pluginContext.ts b/packages/browser/src/node/plugins/pluginContext.ts index 400cd5c5027a..cf8080cf2035 100644 --- a/packages/browser/src/node/plugins/pluginContext.ts +++ b/packages/browser/src/node/plugins/pluginContext.ts @@ -1,7 +1,6 @@ import type { PluginContext } from 'rollup' import type { Plugin } from 'vitest/config' -import type { BrowserProvider } from 'vitest/node' -import type { BrowserServer } from '../server' +import type { ParentBrowserProject } from '../projectParent' import { fileURLToPath } from 'node:url' import { slash } from '@vitest/utils' import { dirname, resolve } from 'pathe' @@ -11,7 +10,7 @@ const ID_CONTEXT = '@vitest/browser/context' const __dirname = dirname(fileURLToPath(import.meta.url)) -export default function BrowserContext(server: BrowserServer): Plugin { +export default function BrowserContext(globalServer: ParentBrowserProject): Plugin { return { name: 'vitest:browser:virtual-module:context', enforce: 'pre', @@ -22,7 +21,7 @@ export default function BrowserContext(server: BrowserServer): Plugin { }, load(id) { if (id === VIRTUAL_ID_CONTEXT) { - return generateContextFile.call(this, server) + return generateContextFile.call(this, globalServer) } }, } @@ -30,12 +29,13 @@ export default function BrowserContext(server: BrowserServer): Plugin { async function generateContextFile( this: PluginContext, - server: BrowserServer, + globalServer: ParentBrowserProject, ) { - const commands = Object.keys(server.config.browser.commands ?? {}) + const commands = Object.keys(globalServer.commands) const filepathCode = '__vitest_worker__.filepath || __vitest_worker__.current?.file?.filepath || undefined' - const provider = server.provider + const provider = [...globalServer.children][0].provider || { name: 'preview' } + const providerName = provider.name const commandsCode = commands .filter(command => !command.startsWith('__vitest')) @@ -45,7 +45,7 @@ async function generateContextFile( .join('\n') const userEventNonProviderImport = await getUserEventImport( - provider, + providerName, this.resolve.bind(this), ) const distContextPath = slash(`/@fs/${resolve(__dirname, 'context.js')}`) @@ -60,7 +60,7 @@ const sessionId = __vitest_browser_runner__.sessionId export const server = { platform: ${JSON.stringify(process.platform)}, version: ${JSON.stringify(process.version)}, - provider: ${JSON.stringify(provider.name)}, + provider: ${JSON.stringify(provider)}, browser: __vitest_browser_runner__.config.browser.name, commands: { ${commandsCode} @@ -73,8 +73,8 @@ export { page, cdp } ` } -async function getUserEventImport(provider: BrowserProvider, resolve: (id: string, importer: string) => Promise) { - if (provider.name !== 'preview') { +async function getUserEventImport(provider: string, resolve: (id: string, importer: string) => Promise) { + if (provider !== 'preview') { return 'const _userEventSetup = undefined' } const resolved = await resolve('@testing-library/user-event', __dirname) diff --git a/packages/browser/src/node/project.ts b/packages/browser/src/node/project.ts new file mode 100644 index 000000000000..f528c6ee25e9 --- /dev/null +++ b/packages/browser/src/node/project.ts @@ -0,0 +1,122 @@ +import type { StackTraceParserOptions } from '@vitest/utils/source-map' +import type { ErrorWithDiff, SerializedConfig } from 'vitest' +import type { + BrowserProvider, + ProjectBrowser as IProjectBrowser, + ResolvedConfig, + TestProject, + Vitest, +} from 'vitest/node' +import type { ParentBrowserProject } from './projectParent' +import { existsSync } from 'node:fs' +import { readFile } from 'node:fs/promises' +import { fileURLToPath } from 'node:url' +import { resolve } from 'pathe' +import { BrowserServerState } from './state' +import { getBrowserProvider } from './utils' + +export class ProjectBrowser implements IProjectBrowser { + public testerHtml: Promise | string + public testerFilepath: string + public locatorsUrl: string | undefined + + public provider!: BrowserProvider + public vitest: Vitest + public config: ResolvedConfig + public children = new Set() + + public parent!: ParentBrowserProject + + public state = new BrowserServerState() + + constructor( + public project: TestProject, + public base: string, + ) { + this.vitest = project.vitest + this.config = project.config + + const pkgRoot = resolve(fileURLToPath(import.meta.url), '../..') + const distRoot = resolve(pkgRoot, 'dist') + + const testerHtmlPath = project.config.browser.testerHtmlPath + ? resolve(project.config.root, project.config.browser.testerHtmlPath) + : resolve(distRoot, 'client/tester/tester.html') + if (!existsSync(testerHtmlPath)) { + throw new Error(`Tester HTML file "${testerHtmlPath}" doesn't exist.`) + } + this.testerFilepath = testerHtmlPath + + this.testerHtml = readFile( + testerHtmlPath, + 'utf8', + ).then(html => (this.testerHtml = html)) + } + + get vite() { + return this.parent.vite + } + + wrapSerializedConfig() { + const config = wrapConfig(this.project.serializedConfig) + config.env ??= {} + config.env.VITEST_BROWSER_DEBUG = process.env.VITEST_BROWSER_DEBUG || '' + return config + } + + async initBrowserProvider(project: TestProject) { + if (this.provider) { + return + } + const Provider = await getBrowserProvider(project.config.browser, project) + this.provider = new Provider() + const browser = project.config.browser.name + const name = project.name ? `[${project.name}] ` : '' + if (!browser) { + throw new Error( + `${name}Browser name is required. Please, set \`test.browser.instances[].browser\` option manually.`, + ) + } + const supportedBrowsers = this.provider.getSupportedBrowsers() + if (supportedBrowsers.length && !supportedBrowsers.includes(browser)) { + throw new Error( + `${name}Browser "${browser}" is not supported by the browser provider "${ + this.provider.name + }". Supported browsers: ${supportedBrowsers.join(', ')}.`, + ) + } + const providerOptions = project.config.browser.providerOptions + await this.provider.initialize(project, { + browser, + options: providerOptions, + }) + } + + public parseErrorStacktrace( + e: ErrorWithDiff, + options: StackTraceParserOptions = {}, + ) { + return this.parent.parseErrorStacktrace(e, options) + } + + public parseStacktrace( + trace: string, + options: StackTraceParserOptions = {}, + ) { + return this.parent.parseStacktrace(trace, options) + } + + async close() { + await this.parent.vite.close() + } +} + +function wrapConfig(config: SerializedConfig): SerializedConfig { + return { + ...config, + // workaround RegExp serialization + testNamePattern: config.testNamePattern + ? (config.testNamePattern.toString() as any as RegExp) + : undefined, + } +} diff --git a/packages/browser/src/node/server.ts b/packages/browser/src/node/projectParent.ts similarity index 66% rename from packages/browser/src/node/server.ts rename to packages/browser/src/node/projectParent.ts index 4ecd50d527ed..9e90a954f78b 100644 --- a/packages/browser/src/node/server.ts +++ b/packages/browser/src/node/projectParent.ts @@ -1,55 +1,48 @@ import type { HtmlTagDescriptor } from 'vite' -import type { ErrorWithDiff, SerializedConfig } from 'vitest' +import type { ErrorWithDiff, ParsedStack } from 'vitest' import type { - BrowserProvider, + BrowserCommand, BrowserScript, CDPSession, - BrowserServer as IBrowserServer, ResolvedConfig, TestProject, Vite, Vitest, } from 'vitest/node' -import { existsSync } from 'node:fs' +import type { BrowserServerState } from './state' import { readFile } from 'node:fs/promises' -import { fileURLToPath } from 'node:url' -import { slash } from '@vitest/utils' import { parseErrorStacktrace, parseStacktrace, type StackTraceParserOptions } from '@vitest/utils/source-map' import { join, resolve } from 'pathe' import { BrowserServerCDPHandler } from './cdp' import builtinCommands from './commands/index' -import { BrowserServerState } from './state' -import { getBrowserProvider } from './utils' - -export class BrowserServer implements IBrowserServer { - public faviconUrl: string - public prefixTesterUrl: string +import { distRoot } from './constants' +import { ProjectBrowser } from './project' +import { slash } from './utils' +export class ParentBrowserProject { public orchestratorScripts: string | undefined public testerScripts: HtmlTagDescriptor[] | undefined + public faviconUrl: string + public prefixTesterUrl: string public manifest: Promise | Vite.Manifest - public testerHtml: Promise | string - public testerFilepath: string + + public vite!: Vite.ViteDevServer + private stackTraceOptions: StackTraceParserOptions public orchestratorHtml: Promise | string public injectorJs: Promise | string public errorCatcherUrl: string public locatorsUrl: string | undefined public stateJs: Promise | string - public state: BrowserServerState - public provider!: BrowserProvider - - public vite!: Vite.ViteDevServer - - private stackTraceOptions: StackTraceParserOptions + public commands: Record> = {} + public children = new Set() public vitest: Vitest - public config: ResolvedConfig - public readonly cdps = new Map() + public config: ResolvedConfig constructor( - project: TestProject, + public project: TestProject, public base: string, ) { this.vitest = project.vitest @@ -73,9 +66,8 @@ export class BrowserServer implements IBrowserServer { }, } - project.config.browser.commands ??= {} for (const [name, command] of Object.entries(builtinCommands)) { - project.config.browser.commands[name] ??= command + this.commands[name] ??= command } // validate names because they can't be used as identifiers @@ -85,13 +77,9 @@ export class BrowserServer implements IBrowserServer { `Invalid command name "${command}". Only alphanumeric characters, $ and _ are allowed.`, ) } + this.commands[command] = project.config.browser.commands[command] } - this.state = new BrowserServerState() - - const pkgRoot = resolve(fileURLToPath(import.meta.url), '../..') - const distRoot = resolve(pkgRoot, 'dist') - this.prefixTesterUrl = `${base}__vitest_test__/__test__/` this.faviconUrl = `${base}__vitest__/favicon.svg` @@ -101,18 +89,6 @@ export class BrowserServer implements IBrowserServer { ) })().then(manifest => (this.manifest = manifest)) - const testerHtmlPath = project.config.browser.testerHtmlPath - ? resolve(project.config.root, project.config.browser.testerHtmlPath) - : resolve(distRoot, 'client/tester/tester.html') - if (!existsSync(testerHtmlPath)) { - throw new Error(`Tester HTML file "${testerHtmlPath}" doesn't exist.`) - } - this.testerFilepath = testerHtmlPath - - this.testerHtml = readFile( - testerHtmlPath, - 'utf8', - ).then(html => (this.testerHtml = html)) this.orchestratorHtml = (project.config.browser.ui ? readFile(resolve(distRoot, 'client/__vitest__/index.html'), 'utf8') : readFile(resolve(distRoot, 'client/orchestrator.html'), 'utf8')) @@ -134,92 +110,27 @@ export class BrowserServer implements IBrowserServer { ).then(js => (this.stateJs = js)) } - setServer(server: Vite.ViteDevServer) { - this.vite = server + public setServer(vite: Vite.ViteDevServer) { + this.vite = vite } - wrapSerializedConfig(projectName: string) { - const project = this.vitest.getProjectByName(projectName) - const config = wrapConfig(project.serializedConfig) - config.env ??= {} - config.env.VITEST_BROWSER_DEBUG = process.env.VITEST_BROWSER_DEBUG || '' - return config - } - - resolveTesterUrl(pathname: string) { - const [sessionId, testFile] = pathname - .slice(this.prefixTesterUrl.length) - .split('/') - const decodedTestFile = decodeURIComponent(testFile) - return { sessionId, testFile: decodedTestFile } - } - - async formatScripts( - scripts: BrowserScript[] | undefined, - ) { - if (!scripts?.length) { - return [] + public spawn(project: TestProject): ProjectBrowser { + if (!this.vite) { + throw new Error(`Cannot spawn child server without a parent dev server.`) } - const server = this.vite - const promises = scripts.map( - async ({ content, src, async, id, type = 'module' }, index): Promise => { - const srcLink = (src ? (await server.pluginContainer.resolveId(src))?.id : undefined) || src - const transformId = srcLink || join(server.config.root, `virtual__${id || `injected-${index}.js`}`) - await server.moduleGraph.ensureEntryFromUrl(transformId) - const contentProcessed - = content && type === 'module' - ? (await server.pluginContainer.transform(content, transformId)).code - : content - return { - tag: 'script', - attrs: { - type, - ...(async ? { async: '' } : {}), - ...(srcLink - ? { - src: srcLink.startsWith('http') ? srcLink : slash(`/@fs/${srcLink}`), - } - : {}), - }, - injectTo: 'head', - children: contentProcessed || '', - } - }, + const clone = new ProjectBrowser( + project, + '/', ) - return (await Promise.all(promises)) - } - - async initBrowserProvider(project: TestProject) { - if (this.provider) { - return - } - const Provider = await getBrowserProvider(project.config.browser, project) - this.provider = new Provider() - const browser = project.config.browser.name - if (!browser) { - throw new Error( - `[${project.name}] Browser name is required. Please, set \`test.browser.instances[].browser\` option manually.`, - ) - } - const supportedBrowsers = this.provider.getSupportedBrowsers() - if (supportedBrowsers.length && !supportedBrowsers.includes(browser)) { - throw new Error( - `[${project.name}] Browser "${browser}" is not supported by the browser provider "${ - this.provider.name - }". Supported browsers: ${supportedBrowsers.join(', ')}.`, - ) - } - const providerOptions = project.config.browser.providerOptions - await this.provider.initialize(project, { - browser, - options: providerOptions, - }) + clone.parent = this + this.children.add(clone) + return clone } public parseErrorStacktrace( e: ErrorWithDiff, options: StackTraceParserOptions = {}, - ) { + ): ParsedStack[] { return parseErrorStacktrace(e, { ...this.stackTraceOptions, ...options, @@ -229,13 +140,14 @@ export class BrowserServer implements IBrowserServer { public parseStacktrace( trace: string, options: StackTraceParserOptions = {}, - ) { + ): ParsedStack[] { return parseStacktrace(trace, { ...this.stackTraceOptions, ...options, }) } + public readonly cdps = new Map() private cdpSessionsPromises = new Map>() async ensureCDPHandler(sessionId: string, rpcId: string) { @@ -250,6 +162,9 @@ export class BrowserServer implements IBrowserServer { const browser = browserSession.project.browser! const provider = browser.provider + if (!provider) { + throw new Error(`Browser provider is not defined for the project "${browserSession.project.name}".`) + } if (!provider.getCDPSession) { throw new Error(`CDP is not supported by the provider "${provider.name}".`) } @@ -280,17 +195,44 @@ export class BrowserServer implements IBrowserServer { this.cdps.delete(sessionId) } - async close() { - await this.vite.close() + async formatScripts(scripts: BrowserScript[] | undefined) { + if (!scripts?.length) { + return [] + } + const server = this.vite + const promises = scripts.map( + async ({ content, src, async, id, type = 'module' }, index): Promise => { + const srcLink = (src ? (await server.pluginContainer.resolveId(src))?.id : undefined) || src + const transformId = srcLink || join(server.config.root, `virtual__${id || `injected-${index}.js`}`) + await server.moduleGraph.ensureEntryFromUrl(transformId) + const contentProcessed + = content && type === 'module' + ? (await server.pluginContainer.transform(content, transformId)).code + : content + return { + tag: 'script', + attrs: { + type, + ...(async ? { async: '' } : {}), + ...(srcLink + ? { + src: srcLink.startsWith('http') ? srcLink : slash(`/@fs/${srcLink}`), + } + : {}), + }, + injectTo: 'head', + children: contentProcessed || '', + } + }, + ) + return (await Promise.all(promises)) } -} -function wrapConfig(config: SerializedConfig): SerializedConfig { - return { - ...config, - // workaround RegExp serialization - testNamePattern: config.testNamePattern - ? (config.testNamePattern.toString() as any as RegExp) - : undefined, + resolveTesterUrl(pathname: string) { + const [sessionId, testFile] = pathname + .slice(this.prefixTesterUrl.length) + .split('/') + const decodedTestFile = decodeURIComponent(testFile) + return { sessionId, testFile: decodedTestFile } } } diff --git a/packages/browser/src/node/providers/playwright.ts b/packages/browser/src/node/providers/playwright.ts index 2a08cef23695..6fb088cf3541 100644 --- a/packages/browser/src/node/providers/playwright.ts +++ b/packages/browser/src/node/providers/playwright.ts @@ -128,6 +128,7 @@ export class PlaywrightBrowserProvider implements BrowserProvider { public getPage(sessionId: string) { const page = this.pages.get(sessionId) if (!page) { + console.log({ pages: [...this.pages.keys()] }) throw new Error(`Page "${sessionId}" not found in ${this.browserName} browser.`) } return page diff --git a/packages/browser/src/node/rpc.ts b/packages/browser/src/node/rpc.ts index 6076466d5f4a..e153f59f0647 100644 --- a/packages/browser/src/node/rpc.ts +++ b/packages/browser/src/node/rpc.ts @@ -2,7 +2,7 @@ import type { Duplex } from 'node:stream' import type { ErrorWithDiff } from 'vitest' import type { BrowserCommandContext, ResolveSnapshotPathHandlerContext, TestProject } from 'vitest/node' import type { WebSocket } from 'ws' -import type { BrowserServer } from './server' +import type { ParentBrowserProject } from './projectParent' import type { BrowserServerState } from './state' import type { WebSocketBrowserEvents, WebSocketBrowserHandlers } from './types' import { existsSync, promises as fs } from 'node:fs' @@ -17,9 +17,9 @@ const debug = createDebugger('vitest:browser:api') const BROWSER_API_PATH = '/__vitest_browser_api__' -export function setupBrowserRpc(server: BrowserServer) { - const vite = server.vite - const vitest = server.vitest +export function setupBrowserRpc(globalServer: ParentBrowserProject) { + const vite = globalServer.vite + const vitest = globalServer.vitest const wss = new WebSocketServer({ noServer: true }) @@ -35,6 +35,7 @@ export function setupBrowserRpc(server: BrowserServer) { const type = searchParams.get('type') const rpcId = searchParams.get('rpcId') + const sessionId = searchParams.get('sessionId') const projectName = searchParams.get('projectName') if (type !== 'tester' && type !== 'orchestrator') { @@ -43,14 +44,14 @@ export function setupBrowserRpc(server: BrowserServer) { ) } - if (!rpcId || projectName == null) { + if (!sessionId || !rpcId || projectName == null) { return error( new Error(`[vitest] Invalid URL ${request.url}. "projectName", "sessionId" and "rpcId" queries are required.`), ) } if (type === 'orchestrator') { - const session = vitest._browserSessions.getSession(rpcId) + const session = vitest._browserSessions.getSession(sessionId) // it's possible the session was already resolved by the preview provider session?.connected() } @@ -76,7 +77,7 @@ export function setupBrowserRpc(server: BrowserServer) { ws.on('close', () => { debug?.('[%s] Browser API disconnected from %s', rpcId, type) clients.delete(rpcId) - server.removeCDPHandler(rpcId) + globalServer.removeCDPHandler(rpcId) }) }) }) @@ -96,7 +97,7 @@ export function setupBrowserRpc(server: BrowserServer) { } function setupClient(project: TestProject, rpcId: string, ws: WebSocket) { - const mockResolver = new ServerMockResolver(server.vite, { + const mockResolver = new ServerMockResolver(globalServer.vite, { moduleDirectories: project.config.server?.deps?.moduleDirectories, }) @@ -105,7 +106,7 @@ export function setupBrowserRpc(server: BrowserServer) { async onUnhandledError(error, type) { if (error && typeof error === 'object') { const _error = error as ErrorWithDiff - _error.stacks = server.parseErrorStacktrace(_error) + _error.stacks = globalServer.parseErrorStacktrace(_error) } vitest.state.catchError(error, type) }, @@ -154,7 +155,7 @@ export function setupBrowserRpc(server: BrowserServer) { return fs.unlink(id) }, getBrowserFileSourceMap(id) { - const mod = server.vite.moduleGraph.getModuleById(id) + const mod = globalServer.vite.moduleGraph.getModuleById(id) return mod?.transformResult?.map }, onCancel(reason) { @@ -175,7 +176,7 @@ export function setupBrowserRpc(server: BrowserServer) { if (!provider) { throw new Error('Commands are only available for browser tests.') } - const commands = project.config.browser?.commands + const commands = globalServer.commands if (!commands || !commands[command]) { throw new Error(`Unknown command "${command}".`) } @@ -212,11 +213,11 @@ export function setupBrowserRpc(server: BrowserServer) { // CDP async sendCdpEvent(sessionId: string, event: string, payload?: Record) { - const cdp = await server.ensureCDPHandler(sessionId, rpcId) + const cdp = await globalServer.ensureCDPHandler(sessionId, rpcId) return cdp.send(event, payload) }, async trackCdpEvent(sessionId: string, type: 'on' | 'once' | 'off', event: string, listenerId: string) { - const cdp = await server.ensureCDPHandler(sessionId, rpcId) + const cdp = await globalServer.ensureCDPHandler(sessionId, rpcId) cdp[type](event, listenerId) }, }, @@ -237,8 +238,8 @@ export function setupBrowserRpc(server: BrowserServer) { return rpc } } -// Serialization support utils. +// Serialization support utils. function cloneByOwnProperties(value: any) { // Clones the value's properties into a new Object. The simpler approach of // Object.assign() won't work in the case that properties are not enumerable. diff --git a/packages/browser/src/node/serverOrchestrator.ts b/packages/browser/src/node/serverOrchestrator.ts index b9a3975c92b2..f1d61fd713ef 100644 --- a/packages/browser/src/node/serverOrchestrator.ts +++ b/packages/browser/src/node/serverOrchestrator.ts @@ -1,16 +1,17 @@ import type { IncomingMessage, ServerResponse } from 'node:http' -import type { BrowserServer } from './server' +import type { ProjectBrowser } from './project' +import type { ParentBrowserProject } from './projectParent' import { replacer } from './utils' export async function resolveOrchestrator( - globalServer: BrowserServer, + globalServer: ParentBrowserProject, url: URL, res: ServerResponse, ) { let sessionId = url.searchParams.get('sessionId') // it's possible to open the page without a context if (!sessionId) { - const contexts = [...globalServer.state.orchestrators.keys()] + const contexts = [...globalServer.children].flatMap(p => [...p.state.orchestrators.keys()]) sessionId = contexts[contexts.length - 1] ?? 'none' } @@ -19,17 +20,21 @@ export async function resolveOrchestrator( const session = globalServer.vitest._browserSessions.getSession(sessionId!) const files = session?.files ?? [] - const browserServer = session?.project.browser as BrowserServer || globalServer + const browserProject = (session?.project.browser as ProjectBrowser | undefined) || [...globalServer.children][0] - const injectorJs = typeof browserServer.injectorJs === 'string' - ? browserServer.injectorJs - : await browserServer.injectorJs + if (!browserProject) { + return + } + + const injectorJs = typeof globalServer.injectorJs === 'string' + ? globalServer.injectorJs + : await globalServer.injectorJs const injector = replacer(injectorJs, { - __VITEST_PROVIDER__: JSON.stringify(browserServer.config.browser.provider || 'preview'), - __VITEST_CONFIG__: JSON.stringify(browserServer.wrapSerializedConfig(session?.project.name || '')), + __VITEST_PROVIDER__: JSON.stringify(browserProject.config.browser.provider || 'preview'), + __VITEST_CONFIG__: JSON.stringify(browserProject.wrapSerializedConfig()), __VITEST_VITE_CONFIG__: JSON.stringify({ - root: browserServer.vite.config.root, + root: browserProject.vite.config.root, }), __VITEST_FILES__: JSON.stringify(files), __VITEST_TYPE__: '"orchestrator"', @@ -64,7 +69,7 @@ export async function resolveOrchestrator( ? await globalServer.manifest : globalServer.manifest const jsEntry = manifestContent['orchestrator.html'].file - const base = browserServer.vite.config.base || '/' + const base = browserProject.parent.vite.config.base || '/' baseHtml = baseHtml .replaceAll('./assets/', `${base}__vitest__/assets/`) .replace( diff --git a/packages/browser/src/node/serverTester.ts b/packages/browser/src/node/serverTester.ts index 825fdadc17db..182997506e2d 100644 --- a/packages/browser/src/node/serverTester.ts +++ b/packages/browser/src/node/serverTester.ts @@ -1,13 +1,14 @@ import type { IncomingMessage, ServerResponse } from 'node:http' import type { Connect } from 'vite' -import type { BrowserServer } from './server' +import type { ProjectBrowser } from './project' +import type { ParentBrowserProject } from './projectParent' import crypto from 'node:crypto' import { stringify } from 'flatted' import { join } from 'pathe' import { replacer } from './utils' export async function resolveTester( - globalServer: BrowserServer, + globalServer: ParentBrowserProject, url: URL, res: ServerResponse, next: Connect.NextFunction, @@ -43,18 +44,24 @@ export async function resolveTester( const files = session.files ?? [] const method = session.method ?? 'run' - // TODO: test for different configurations in multi-browser instances - const browserServer = project.browser as BrowserServer || globalServer - const injectorJs: string = typeof browserServer.injectorJs === 'string' - ? browserServer.injectorJs - : await browserServer.injectorJs + const browserProject = (project.browser as ProjectBrowser | undefined) || [...globalServer.children][0] + + if (!browserProject) { + res.statusCode = 400 + res.end('Invalid session ID') + return + } + + const injectorJs: string = typeof globalServer.injectorJs === 'string' + ? globalServer.injectorJs + : await globalServer.injectorJs const injector = replacer(injectorJs, { - __VITEST_PROVIDER__: JSON.stringify(project.browser!.provider.name), - __VITEST_CONFIG__: JSON.stringify(browserServer.wrapSerializedConfig(project.name)), + __VITEST_PROVIDER__: JSON.stringify(project.browser!.provider!.name), + __VITEST_CONFIG__: JSON.stringify(browserProject.wrapSerializedConfig()), __VITEST_FILES__: JSON.stringify(files), __VITEST_VITE_CONFIG__: JSON.stringify({ - root: browserServer.vite.config.root, + root: browserProject.vite.config.root, }), __VITEST_TYPE__: '"tester"', __VITEST_SESSION_ID__: JSON.stringify(sessionId), @@ -62,14 +69,14 @@ export async function resolveTester( __VITEST_PROVIDED_CONTEXT__: JSON.stringify(stringify(project.getProvidedContext())), }) - const testerHtml = typeof browserServer.testerHtml === 'string' - ? browserServer.testerHtml - : await browserServer.testerHtml + const testerHtml = typeof browserProject.testerHtml === 'string' + ? browserProject.testerHtml + : await browserProject.testerHtml try { - const url = join('/@fs/', browserServer.testerFilepath) - const indexhtml = await browserServer.vite.transformIndexHtml(url, testerHtml) - return replacer(indexhtml, { + const url = join('/@fs/', browserProject.testerFilepath) + const indexhtml = await browserProject.vite.transformIndexHtml(url, testerHtml) + const html = replacer(indexhtml, { __VITEST_FAVICON__: globalServer.faviconUrl, __VITEST_INJECTOR__: injector, __VITEST_APPEND__: ` @@ -79,6 +86,7 @@ export async function resolveTester( document.querySelector('script[data-vitest-append]').remove() `, }) + return html } catch (err) { session.reject(err) diff --git a/packages/runner/src/fixture.ts b/packages/runner/src/fixture.ts index cd073ec911e3..34a12f4e27d6 100644 --- a/packages/runner/src/fixture.ts +++ b/packages/runner/src/fixture.ts @@ -5,10 +5,6 @@ import { getFixture } from './map' export interface FixtureItem extends FixtureOptions { prop: string value: any - /** - * Indicated if the injected value should be preferred over the fixture value - */ - injected?: boolean /** * Indicates whether the fixture is a function */ diff --git a/packages/runner/src/types/tasks.ts b/packages/runner/src/types/tasks.ts index 1c593cb744ae..e5b2adfb09c9 100644 --- a/packages/runner/src/types/tasks.ts +++ b/packages/runner/src/types/tasks.ts @@ -443,6 +443,10 @@ export interface FixtureOptions { * Whether to automatically set up current fixture, even though it's not being used in tests. */ auto?: boolean + /** + * Indicated if the injected value from the config should be preferred over the fixture value + */ + injected?: boolean } export type Use = (value: T) => Promise diff --git a/packages/vitest/src/node/browser/sessions.ts b/packages/vitest/src/node/browser/sessions.ts index 06c702025260..b6847e000b05 100644 --- a/packages/vitest/src/node/browser/sessions.ts +++ b/packages/vitest/src/node/browser/sessions.ts @@ -1,4 +1,4 @@ -import type { TestProject } from 'vitest/node' +import type { TestProject } from '../project' import type { BrowserServerStateSession } from '../types/browser' import { createDefer } from '@vitest/utils' diff --git a/packages/vitest/src/node/project.ts b/packages/vitest/src/node/project.ts index 9cb532cb1e26..44c090e13db8 100644 --- a/packages/vitest/src/node/project.ts +++ b/packages/vitest/src/node/project.ts @@ -9,7 +9,7 @@ import type { ProvidedContext } from '../types/general' import type { OnTestsRerunHandler, Vitest } from './core' import type { GlobalSetupFile } from './globalSetup' import type { Logger } from './logger' -import type { BrowserServer } from './types/browser' +import type { ParentProjectBrowser, ProjectBrowser } from './types/browser' import type { ResolvedConfig, SerializedConfig, @@ -52,7 +52,7 @@ export class TestProject { /** * Browser instance if the browser is enabled. This is initialized when the tests run for the first time. */ - public browser?: BrowserServer + public browser?: ProjectBrowser /** @deprecated use `vitest` instead */ public ctx: Vitest @@ -64,6 +64,7 @@ export class TestProject { /** @internal */ vitenode!: ViteNodeServer /** @internal */ typechecker?: Typechecker + /** @internal */ _config?: ResolvedConfig private runner!: ViteNodeRunner @@ -74,7 +75,6 @@ export class TestProject { private _globalSetups?: GlobalSetupFile[] private _provided: ProvidedContext = {} as any - private _config?: ResolvedConfig private _vite?: ViteDevServer constructor( @@ -177,11 +177,11 @@ export class TestProject { throw new Error('The config was not set. It means that `project.config` was called before the Vite server was established.') } // checking it once should be enough - Object.defineProperty(this, 'config', { - configurable: true, - writable: true, - value: this._config, - }) + // Object.defineProperty(this, 'config', { + // configurable: true, + // writable: true, + // value: this._config, + // }) return this._config } @@ -492,11 +492,19 @@ export class TestProject { } /** @internal */ - _initBrowserServer = deduped(async () => { - if (!this.isBrowserEnabled() || this.browser) { + _parentBrowser?: ParentProjectBrowser + /** @internal */ + _parent?: TestProject + /** @internal */ + _initParentBrowser = deduped(async () => { + if (!this.isBrowserEnabled() || this._parentBrowser) { return } - await this.vitest.packageInstaller.ensureInstalled('@vitest/browser', this.config.root, this.vitest.version) + await this.vitest.packageInstaller.ensureInstalled( + '@vitest/browser', + this.config.root, + this.vitest.version, + ) const { createBrowserServer, distRoot } = await import('@vitest/browser') const browser = await createBrowserServer( this, @@ -513,12 +521,21 @@ export class TestProject { ], [CoverageTransform(this.vitest)], ) - this.browser = browser + this._parentBrowser = browser if (this.config.browser.ui) { setup(this.vitest, browser.vite) } }) + /** @internal */ + _initBrowserServer = deduped(async () => { + await this._parent?._initParentBrowser() + + if (!this.browser && this._parent?._parentBrowser) { + this.browser = this._parent._parentBrowser.spawn(this) + } + }) + /** * Closes the project and all associated resources. This can only be called once; the closing promise is cached until the server restarts. * If the resources are needed again, create a new project. @@ -635,6 +652,18 @@ export class TestProject { await this.browser?.initBrowserProvider(this) }) + /** @internal */ + public _provideObject(context: ProvidedContext): void { + for (const _providedKey in context) { + const providedKey = _providedKey as keyof ProvidedContext + // type is very strict here, so we cast it to any + (this.provide as (key: string, value: unknown) => void)( + providedKey, + context[providedKey], + ) + } + } + /** @internal */ static _createBasicProject(vitest: Vitest): TestProject { const project = new TestProject( @@ -645,69 +674,36 @@ export class TestProject { project.runner = vitest.runner project._vite = vitest.server project._config = vitest.config - for (const _providedKey in vitest.config.provide) { - const providedKey = _providedKey as keyof ProvidedContext - // type is very strict here, so we cast it to any - (project.provide as (key: string, value: unknown) => void)( - providedKey, - vitest.config.provide[providedKey], - ) - } + project._provideObject(vitest.config.provide) return project } /** @internal */ - static _cloneBrowserProject(project: TestProject, config: ResolvedConfig): TestProject { + static _cloneBrowserProject(parent: TestProject, config: ResolvedConfig): TestProject { const clone = new TestProject( - project.path, - project.vitest, + parent.path, + parent.vitest, ) - clone.vitenode = project.vitenode - clone.runner = project.runner - clone._vite = project._vite + clone.vitenode = parent.vitenode + clone.runner = parent.runner + clone._vite = parent._vite clone._config = config - for (const _providedKey in config.provide) { - const providedKey = _providedKey as keyof ProvidedContext - // type is very strict here, so we cast it to any - (clone.provide as (key: string, value: unknown) => void)( - providedKey, - config.provide[providedKey], - ) - } - clone._initBrowserServer = deduped(async () => { - if (clone.browser) { - return - } - await project._initBrowserServer() - const { cloneBrowserServer } = await import('@vitest/browser') - clone.browser = cloneBrowserServer(clone, project.browser!) - }) - clone._initBrowserProvider = deduped(async () => { - if (!clone.isBrowserEnabled() || clone.browser?.provider) { - return - } - if (!clone.browser) { - await clone._initBrowserServer() - } - if (!project.browser?.provider) { - await project.browser?.initBrowserProvider(project) - } - await clone.browser?.initBrowserProvider(clone) - }) + clone._parent = parent + clone._provideObject(config.provide) return clone } } -function deduped(cb: () => Promise) { +function deduped Promise>(cb: T): T { let _promise: Promise | undefined - return () => { + return ((...args: any[]) => { if (!_promise) { - _promise = cb().finally(() => { + _promise = cb(...args).finally(() => { _promise = undefined }) } return _promise - } + }) as T } export { diff --git a/packages/vitest/src/node/types/browser.ts b/packages/vitest/src/node/types/browser.ts index 7e915d6068c5..8ff550fbca6d 100644 --- a/packages/vitest/src/node/types/browser.ts +++ b/packages/vitest/src/node/types/browser.ts @@ -256,7 +256,11 @@ export interface BrowserServerState { orchestrators: Map } -export interface BrowserServer { +export interface ParentProjectBrowser { + spawn: (project: TestProject) => ProjectBrowser +} + +export interface ProjectBrowser { vite: ViteDevServer state: BrowserServerState provider: BrowserProvider diff --git a/packages/vitest/src/node/workspace/resolveWorkspace.ts b/packages/vitest/src/node/workspace/resolveWorkspace.ts index 00823a357e3b..be5eca8bc895 100644 --- a/packages/vitest/src/node/workspace/resolveWorkspace.ts +++ b/packages/vitest/src/node/workspace/resolveWorkspace.ts @@ -3,7 +3,7 @@ import type { BrowserInstanceOption, ResolvedConfig, TestProjectConfiguration, U import { existsSync, promises as fs } from 'node:fs' import os from 'node:os' import { limitConcurrency } from '@vitest/runner/utils' -import { toArray } from '@vitest/utils' +import { deepClone, toArray } from '@vitest/utils' import fg from 'fast-glob' import { dirname, relative, resolve } from 'pathe' import { mergeConfig } from 'vite' @@ -138,28 +138,28 @@ export async function resolveBrowserWorkspace( names: Set, resolvedProjects: TestProject[], ) { - const newConfigs: [project: TestProject, config: ResolvedConfig][] = [] + // const newConfigs: [project: TestProject, config: ResolvedConfig][] = [] const filters = toArray(vitest.config.project).map(s => wildcardPatternToRegExp(s)) + const removeProjects = new Set() resolvedProjects.forEach((project) => { const configs = project.config.browser.instances if (!project.config.browser.enabled || !configs || configs.length === 0) { return } - const [firstConfig, ...restConfigs] = configs const originalName = project.config.name - - if (!firstConfig.browser) { - throw new Error(`The browser configuration must have a "browser" property. The first item in "browser.instances" doesn't have it. Make sure your${originalName ? ` "${originalName}"` : ''} configuration is correct.`) - } - - const newName = originalName - ? `${originalName} (${firstConfig.browser})` - : firstConfig.browser - if (names.has(newName)) { - throw new Error(`Cannot redefine the project name for a nameless project. The project name "${firstConfig.browser}" was already defined. All projects in a workspace should have unique names. Make sure your configuration is correct.`) + const filteredConfigs = !filters.length + ? configs + : configs.filter((config) => { + const browser = config.browser + const newName = config.name || (originalName ? `${originalName} (${browser})` : browser) + return filters.some(pattern => pattern.test(newName)) + }) + + // every project was filtered out + if (!filteredConfigs.length) { + return } - names.add(newName) if (project.config.browser.providerOptions) { vitest.logger.warn( @@ -167,7 +167,7 @@ export async function resolveBrowserWorkspace( ) } - restConfigs.forEach((config, index) => { + filteredConfigs.forEach((config, index) => { const browser = config.browser if (!browser) { const nth = index + 1 @@ -176,10 +176,6 @@ export async function resolveBrowserWorkspace( } const name = config.name const newName = name || (originalName ? `${originalName} (${browser})` : browser) - // skip the project if it's filtered out - if (filters.length && !filters.some(pattern => pattern.test(newName))) { - return - } if (names.has(newName)) { throw new Error( @@ -193,17 +189,15 @@ export async function resolveBrowserWorkspace( names.add(newName) const clonedConfig = cloneConfig(project, config) clonedConfig.name = newName - newConfigs.push([project, clonedConfig]) + const clone = TestProject._cloneBrowserProject(project, clonedConfig) + resolvedProjects.push(clone) }) - Object.assign(project.config, cloneConfig(project, firstConfig)) - project.config.name = newName - }) - newConfigs.forEach(([project, clonedConfig]) => { - const clone = TestProject._cloneBrowserProject(project, clonedConfig) - resolvedProjects.push(clone) + removeProjects.add(project) }) + resolvedProjects = resolvedProjects.filter(project => !removeProjects.has(project)) + const headedBrowserProjects = resolvedProjects.filter((project) => { return project.config.browser.enabled && !project.config.browser.headless }) @@ -249,7 +243,7 @@ function cloneConfig(project: TestProject, { browser, ...config }: BrowserInstan } = config const currentConfig = project.config.browser return mergeConfig({ - ...project.config, + ...deepClone(project.config), browser: { ...project.config.browser, locators: locators diff --git a/packages/vitest/src/public/node.ts b/packages/vitest/src/public/node.ts index 18a2e0c46b64..bc7fbcb502a4 100644 --- a/packages/vitest/src/public/node.ts +++ b/packages/vitest/src/public/node.ts @@ -68,10 +68,11 @@ export type { BrowserProviderModule, BrowserProviderOptions, BrowserScript, - BrowserServer, BrowserServerState, BrowserServerStateSession, CDPSession, + ParentProjectBrowser, + ProjectBrowser, ResolvedBrowserOptions, } from '../node/types/browser' /** @deprecated use `createViteServer` instead */ diff --git a/test/browser/fixtures/multiple-different-configs/basic.test.js b/test/browser/fixtures/multiple-different-configs/basic.test.js new file mode 100644 index 000000000000..f5480f9199a5 --- /dev/null +++ b/test/browser/fixtures/multiple-different-configs/basic.test.js @@ -0,0 +1,23 @@ +import { test as baseTest, expect, inject } from 'vitest'; +import { server } from '@vitest/browser/context' + +const test = baseTest.extend({ + // chromium should inject the value as "true" + // firefox doesn't provide this value in the config, it will stay undefined + providedVar: [undefined, { injected: true }] +}) + +test('html injected', ({ providedVar }) => { + // window.HTML_INJECTED_VAR is injected only for chromium via a script in customTester.html + console.log(`[${server.config.name}] HTML_INJECTED_VAR is ${window.HTML_INJECTED_VAR}`) + expect(providedVar).toBe(window.HTML_INJECTED_VAR) +}) + +test.runIf(server.config.name === 'firefox')('[firefox] firefoxValue injected', ({ providedVar }) => { + expect(providedVar).toBeUndefined() + expect(inject('firefoxValue')).toBe(true) +}) + +test.runIf(server.config.name === 'chromium')('[chromium] firefoxValue is not injected', () => { + expect(inject('firefoxValue')).toBeUndefined() +}) \ No newline at end of file diff --git a/test/browser/fixtures/multiple-different-configs/customTester.html b/test/browser/fixtures/multiple-different-configs/customTester.html new file mode 100644 index 000000000000..97cc881fe0a2 --- /dev/null +++ b/test/browser/fixtures/multiple-different-configs/customTester.html @@ -0,0 +1,13 @@ + + + + + + Document + + + + + \ No newline at end of file diff --git a/test/browser/fixtures/multiple-different-configs/vitest.config.js b/test/browser/fixtures/multiple-different-configs/vitest.config.js new file mode 100644 index 000000000000..bf4a95d5378c --- /dev/null +++ b/test/browser/fixtures/multiple-different-configs/vitest.config.js @@ -0,0 +1,30 @@ +import { defineConfig } from 'vitest/config'; +import { fileURLToPath } from 'node:url' + +export default defineConfig({ + clearScreen: false, + cacheDir: fileURLToPath(new URL("./node_modules/.vite", import.meta.url)), + test: { + browser: { + provider: 'playwright', + enabled: true, + headless: true, + screenshotFailures: false, + instances: [ + { + browser: 'chromium', + testerHtmlPath: './customTester.html', + provide: { + providedVar: true, + }, + }, + { + browser: 'firefox', + provide: { + firefoxValue: true, + }, + }, + ], + }, + }, +}) diff --git a/test/browser/package.json b/test/browser/package.json index aa7c3602ba69..0d9c6742185d 100644 --- a/test/browser/package.json +++ b/test/browser/package.json @@ -12,6 +12,7 @@ "test-mocking": "vitest --root ./fixtures/mocking", "test-mocking-watch": "vitest --root ./fixtures/mocking-watch", "test-locators": "vitest --root ./fixtures/locators", + "test-different-configs": "vitest --root ./fixtures/multiple-different-configs", "test-setup-file": "vitest --root ./fixtures/setup-file", "test-snapshots": "vitest --root ./fixtures/update-snapshot", "coverage": "vitest --coverage.enabled --coverage.provider=istanbul --browser.headless=yes", diff --git a/test/browser/specs/multiple-different-configs.test.ts b/test/browser/specs/multiple-different-configs.test.ts new file mode 100644 index 000000000000..2e7551ed0e8e --- /dev/null +++ b/test/browser/specs/multiple-different-configs.test.ts @@ -0,0 +1,14 @@ +import { expect, test } from 'vitest' +import { provider } from '../settings' +import { runBrowserTests } from './utils' + +test.runIf(provider === 'playwright')('[playwright] runs multiple different configurations correctly', async () => { + const { stdout, exitCode, stderr } = await runBrowserTests({ + root: './fixtures/multiple-different-configs', + }) + + expect(stderr).toBe('') + expect(exitCode).toBe(0) + expect(stdout).toContain('[chromium] HTML_INJECTED_VAR is true') + expect(stdout).toContain('[firefox] HTML_INJECTED_VAR is undefined') +}) From 35b7f3dc5a37b432a37ff0d44abc1d9a3ac87545 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Tue, 17 Dec 2024 16:25:16 +0100 Subject: [PATCH 44/51] chore: ctx -> vitest --- packages/browser/src/node/rpc.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/browser/src/node/rpc.ts b/packages/browser/src/node/rpc.ts index d02c9a32ab0f..45c4a5b22c3a 100644 --- a/packages/browser/src/node/rpc.ts +++ b/packages/browser/src/node/rpc.ts @@ -111,9 +111,9 @@ export function setupBrowserRpc(globalServer: ParentBrowserProject) { vitest.state.catchError(error, type) }, async onQueued(file) { - ctx.state.collectFiles(project, [file]) - const testModule = ctx.state.getReportedEntity(file) as TestModule - await ctx.report('onTestModuleQueued', testModule) + vitest.state.collectFiles(project, [file]) + const testModule = vitest.state.getReportedEntity(file) as TestModule + await vitest.report('onTestModuleQueued', testModule) }, async onCollected(files) { vitest.state.collectFiles(project, files) From 00f76df3241a36f61dea412193e9fe061f12258e Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Tue, 17 Dec 2024 16:28:18 +0100 Subject: [PATCH 45/51] chore: cleanup --- packages/browser/src/node/providers/playwright.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/browser/src/node/providers/playwright.ts b/packages/browser/src/node/providers/playwright.ts index 6fb088cf3541..2a08cef23695 100644 --- a/packages/browser/src/node/providers/playwright.ts +++ b/packages/browser/src/node/providers/playwright.ts @@ -128,7 +128,6 @@ export class PlaywrightBrowserProvider implements BrowserProvider { public getPage(sessionId: string) { const page = this.pages.get(sessionId) if (!page) { - console.log({ pages: [...this.pages.keys()] }) throw new Error(`Page "${sessionId}" not found in ${this.browserName} browser.`) } return page From 8de42d265418970c22247dfc82430588a250ffc5 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Tue, 17 Dec 2024 16:31:50 +0100 Subject: [PATCH 46/51] test: add webdriverio test --- .../multiple-different-configs/vitest.config.js | 7 +++++-- test/browser/specs/multiple-different-configs.test.ts | 11 +++++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/test/browser/fixtures/multiple-different-configs/vitest.config.js b/test/browser/fixtures/multiple-different-configs/vitest.config.js index bf4a95d5378c..8c0598179894 100644 --- a/test/browser/fixtures/multiple-different-configs/vitest.config.js +++ b/test/browser/fixtures/multiple-different-configs/vitest.config.js @@ -1,22 +1,25 @@ import { defineConfig } from 'vitest/config'; import { fileURLToPath } from 'node:url' +const provider = process.env.PROVIDER || 'playwright' + export default defineConfig({ clearScreen: false, cacheDir: fileURLToPath(new URL("./node_modules/.vite", import.meta.url)), test: { browser: { - provider: 'playwright', + provider: provider, enabled: true, headless: true, screenshotFailures: false, instances: [ { - browser: 'chromium', + browser: provider === 'playwright' ? 'chromium' : 'chrome', testerHtmlPath: './customTester.html', provide: { providedVar: true, }, + name: 'chromium', }, { browser: 'firefox', diff --git a/test/browser/specs/multiple-different-configs.test.ts b/test/browser/specs/multiple-different-configs.test.ts index 2e7551ed0e8e..48cd36e4a1a2 100644 --- a/test/browser/specs/multiple-different-configs.test.ts +++ b/test/browser/specs/multiple-different-configs.test.ts @@ -12,3 +12,14 @@ test.runIf(provider === 'playwright')('[playwright] runs multiple different conf expect(stdout).toContain('[chromium] HTML_INJECTED_VAR is true') expect(stdout).toContain('[firefox] HTML_INJECTED_VAR is undefined') }) + +test.runIf(provider === 'webdriverio')('[webdriverio] runs multiple different configurations correctly', async () => { + const { stdout, exitCode, stderr } = await runBrowserTests({ + root: './fixtures/multiple-different-configs', + }) + + expect(stderr).toBe('') + expect(exitCode).toBe(0) + expect(stdout).toContain('[chromium] HTML_INJECTED_VAR is true') + expect(stdout).toContain('[firefox] HTML_INJECTED_VAR is undefined') +}) From 0882a8153c646cc97eedb83ad81639ccb3a0ce6d Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Tue, 17 Dec 2024 16:48:16 +0100 Subject: [PATCH 47/51] fix: populate configs if "browser" is used, but "instances" is not --- packages/vitest/src/node/cli/cli-config.ts | 4 ++++ packages/vitest/src/node/project.ts | 2 +- .../src/node/workspace/resolveWorkspace.ts | 21 +++++++++++++++++-- 3 files changed, 24 insertions(+), 3 deletions(-) diff --git a/packages/vitest/src/node/cli/cli-config.ts b/packages/vitest/src/node/cli/cli-config.ts index 73612fd7df6e..6e9db83aa22f 100644 --- a/packages/vitest/src/node/cli/cli-config.ts +++ b/packages/vitest/src/node/cli/cli-config.ts @@ -408,6 +408,10 @@ export const cliOptionsConfig: VitestCLIOptions = { description: 'Should browser test files run in parallel. Use `--browser.fileParallelism=false` to disable (default: `true`)', }, + connectTimeout: { + description: 'If connection to the browser takes longer, the test suite will fail (default: `60_000`)', + argument: '', + }, orchestratorScripts: null, testerScripts: null, commands: null, diff --git a/packages/vitest/src/node/project.ts b/packages/vitest/src/node/project.ts index 44c090e13db8..1e156da24c5c 100644 --- a/packages/vitest/src/node/project.ts +++ b/packages/vitest/src/node/project.ts @@ -653,7 +653,7 @@ export class TestProject { }) /** @internal */ - public _provideObject(context: ProvidedContext): void { + public _provideObject(context: Partial): void { for (const _providedKey in context) { const providedKey = _providedKey as keyof ProvidedContext // type is very strict here, so we cast it to any diff --git a/packages/vitest/src/node/workspace/resolveWorkspace.ts b/packages/vitest/src/node/workspace/resolveWorkspace.ts index be5eca8bc895..fe803087f42d 100644 --- a/packages/vitest/src/node/workspace/resolveWorkspace.ts +++ b/packages/vitest/src/node/workspace/resolveWorkspace.ts @@ -143,10 +143,27 @@ export async function resolveBrowserWorkspace( const removeProjects = new Set() resolvedProjects.forEach((project) => { - const configs = project.config.browser.instances - if (!project.config.browser.enabled || !configs || configs.length === 0) { + if (!project.config.browser.enabled) { return } + const configs = project.config.browser.instances || [] + if (configs.length === 0) { + // browser.name should be defined, otherwise the config fails in "resolveConfig" + configs.push({ browser: project.config.browser.name }) + console.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(''), + ), + ) + } const originalName = project.config.name const filteredConfigs = !filters.length ? configs From fd781df7084b6936644f8a1713dc81d8b73a4fd8 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Tue, 17 Dec 2024 16:54:03 +0100 Subject: [PATCH 48/51] chore: fix browser snapshot --- test/cli/test/__snapshots__/list.test.ts.snap | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test/cli/test/__snapshots__/list.test.ts.snap b/test/cli/test/__snapshots__/list.test.ts.snap index efc6f0b467ad..2dc10a05972d 100644 --- a/test/cli/test/__snapshots__/list.test.ts.snap +++ b/test/cli/test/__snapshots__/list.test.ts.snap @@ -20,12 +20,12 @@ Error: describe error `; exports[`correctly outputs all tests with args: "--browser.enabled" 1`] = ` -"basic.test.ts > basic suite > inner suite > some test -basic.test.ts > basic suite > inner suite > another test -basic.test.ts > basic suite > basic test -basic.test.ts > outside test -math.test.ts > 1 plus 1 -math.test.ts > failing test +"[chromium] basic.test.ts > basic suite > inner suite > some test +[chromium] basic.test.ts > basic suite > inner suite > another test +[chromium] basic.test.ts > basic suite > basic test +[chromium] basic.test.ts > outside test +[chromium] math.test.ts > 1 plus 1 +[chromium] math.test.ts > failing test " `; From a76eaa0708b8f09724320adddb2a51566c943bf0 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Thu, 19 Dec 2024 09:27:08 +0100 Subject: [PATCH 49/51] test: fix tests --- .../browser/src/node/plugins/pluginContext.ts | 2 +- .../vitest/src/node/config/resolveConfig.ts | 2 +- test/config/fixtures/bail/vitest.config.ts | 4 ++- test/config/test/bail.test.ts | 26 +++++++++++++++---- test/config/test/browser-html.test.ts | 12 ++++----- test/config/test/failures.test.ts | 2 +- 6 files changed, 33 insertions(+), 15 deletions(-) diff --git a/packages/browser/src/node/plugins/pluginContext.ts b/packages/browser/src/node/plugins/pluginContext.ts index cf8080cf2035..5929822ae557 100644 --- a/packages/browser/src/node/plugins/pluginContext.ts +++ b/packages/browser/src/node/plugins/pluginContext.ts @@ -60,7 +60,7 @@ const sessionId = __vitest_browser_runner__.sessionId export const server = { platform: ${JSON.stringify(process.platform)}, version: ${JSON.stringify(process.version)}, - provider: ${JSON.stringify(provider)}, + provider: ${JSON.stringify(providerName)}, browser: __vitest_browser_runner__.config.browser.name, commands: { ${commandsCode} diff --git a/packages/vitest/src/node/config/resolveConfig.ts b/packages/vitest/src/node/config/resolveConfig.ts index bc65895dd69b..24f1a59d8687 100644 --- a/packages/vitest/src/node/config/resolveConfig.ts +++ b/packages/vitest/src/node/config/resolveConfig.ts @@ -240,7 +240,7 @@ export function resolveConfig( const configs = browser.instances if (browser.name && browser.instances) { // --browser=chromium filters configs to a single one - browser.instances = browser.instances.filter(config_ => config_.browser === browser.name) + browser.instances = browser.instances.filter(instance => instance.browser === browser.name) } if (browser.instances && !browser.instances.length) { diff --git a/test/config/fixtures/bail/vitest.config.ts b/test/config/fixtures/bail/vitest.config.ts index f117ebe338a0..0a7dfa7957b9 100644 --- a/test/config/fixtures/bail/vitest.config.ts +++ b/test/config/fixtures/bail/vitest.config.ts @@ -29,8 +29,10 @@ export default defineConfig({ }, browser: { headless: true, - name: 'chrome', provider: 'webdriverio', + instances: [ + { browser: 'chrome' }, + ], }, }, }) diff --git a/test/config/test/bail.test.ts b/test/config/test/bail.test.ts index d03c1b0cedc9..87d7105e4dd4 100644 --- a/test/config/test/bail.test.ts +++ b/test/config/test/bail.test.ts @@ -4,24 +4,32 @@ import { expect, test } from 'vitest' import { runVitest } from '../../test-utils' const configs: UserConfig[] = [] -const pools: UserConfig[] = [{ pool: 'threads' }, { pool: 'forks' }, { pool: 'threads', poolOptions: { threads: { singleThread: true } } }] +const pools: UserConfig[] = [ + { pool: 'threads' }, + { pool: 'forks' }, + { pool: 'threads', poolOptions: { threads: { singleThread: true } } }, +] if (process.platform !== 'win32') { pools.push( { browser: { enabled: true, - name: 'chromium', provider: 'playwright', fileParallelism: false, + instances: [ + { browser: 'chromium' }, + ], }, }, { browser: { enabled: true, - name: 'chromium', provider: 'playwright', fileParallelism: true, + instances: [ + { browser: 'chromium' }, + ], }, }, ) @@ -70,9 +78,17 @@ for (const config of configs) { }, }) + const browser = !!config.browser + expect(exitCode).toBe(1) - expect(stdout).toMatch('✓ test/first.test.ts > 1 - first.test.ts - this should pass') - expect(stdout).toMatch('× test/first.test.ts > 2 - first.test.ts - this should fail') + if (browser) { + expect(stdout).toMatch('✓ |chromium| test/first.test.ts > 1 - first.test.ts - this should pass') + expect(stdout).toMatch('× |chromium| test/first.test.ts > 2 - first.test.ts - this should fail') + } + else { + expect(stdout).toMatch('✓ test/first.test.ts > 1 - first.test.ts - this should pass') + expect(stdout).toMatch('× test/first.test.ts > 2 - first.test.ts - this should fail') + } // Cancelled tests should not be run expect(stdout).not.toMatch('test/first.test.ts > 3 - first.test.ts - this should be skipped') diff --git a/test/config/test/browser-html.test.ts b/test/config/test/browser-html.test.ts index 19abfc738cdc..93f31b8563de 100644 --- a/test/config/test/browser-html.test.ts +++ b/test/config/test/browser-html.test.ts @@ -26,10 +26,10 @@ test('allows correct custom html', async () => { const { stderr, stdout, exitCode } = await runVitest({ root, config: './vitest.config.correct.ts', - reporters: ['basic'], + reporters: [['default', { summary: false }]], }) expect(stderr).toBe('') - expect(stdout).toContain('✓ browser-basic.test.ts') + expect(stdout).toContain('✓ |chromium| browser-basic.test.ts') expect(exitCode).toBe(0) }) @@ -37,10 +37,10 @@ test('allows custom transformIndexHtml with custom html file', async () => { const { stderr, stdout, exitCode } = await runVitest({ root, config: './vitest.config.custom-transformIndexHtml.ts', - reporters: ['basic'], + reporters: [['default', { summary: false }]], }) expect(stderr).toBe('') - expect(stdout).toContain('✓ browser-custom.test.ts') + expect(stdout).toContain('✓ |chromium| browser-custom.test.ts') expect(exitCode).toBe(0) }) @@ -48,9 +48,9 @@ test('allows custom transformIndexHtml without custom html file', async () => { const { stderr, stdout, exitCode } = await runVitest({ root, config: './vitest.config.default-transformIndexHtml.ts', - reporters: ['basic'], + reporters: [['default', { summary: false }]], }) expect(stderr).toBe('') - expect(stdout).toContain('✓ browser-custom.test.ts') + expect(stdout).toContain('✓ |chromium| browser-custom.test.ts') expect(exitCode).toBe(0) }) diff --git a/test/config/test/failures.test.ts b/test/config/test/failures.test.ts index d14aab5a46ea..74a42e121db2 100644 --- a/test/config/test/failures.test.ts +++ b/test/config/test/failures.test.ts @@ -366,7 +366,7 @@ test('throws an error if name conflicts with a workspace name', async () => { }, ], }) - expect(stderr).toMatch('Cannot redefine the project name for a nameless project. The project name "firefox" was already defined. All projects in a workspace should have unique names. Make sure your configuration is correct.') + expect(stderr).toMatch('Cannot define a nested project for a firefox browser. The project name "1 (firefox)" was already defined. If you have multiple instances for the same browser, make sure to define a custom "name". All projects in a workspace should have unique names. Make sure your configuration is correct.') }) test('throws an error if several browsers are headed in nonTTY mode', async () => { From 4ea5ff6b7e866478e2b0daffcc02479aba9eda09 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Thu, 19 Dec 2024 09:35:19 +0100 Subject: [PATCH 50/51] test: fix bail test --- test/config/test/bail.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/config/test/bail.test.ts b/test/config/test/bail.test.ts index 87d7105e4dd4..261953ef3a88 100644 --- a/test/config/test/bail.test.ts +++ b/test/config/test/bail.test.ts @@ -78,12 +78,12 @@ for (const config of configs) { }, }) - const browser = !!config.browser + const browser = config.browser?.instances?.[0].browser expect(exitCode).toBe(1) if (browser) { - expect(stdout).toMatch('✓ |chromium| test/first.test.ts > 1 - first.test.ts - this should pass') - expect(stdout).toMatch('× |chromium| test/first.test.ts > 2 - first.test.ts - this should fail') + expect(stdout).toMatch(`✓ |${browser}| test/first.test.ts > 1 - first.test.ts - this should pass`) + expect(stdout).toMatch(`× |${browser}| test/first.test.ts > 2 - first.test.ts - this should fail`) } else { expect(stdout).toMatch('✓ test/first.test.ts > 1 - first.test.ts - this should pass') From ee42ccbe80f5cbdbf2937de93a6a84f7e8ea3864 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Thu, 19 Dec 2024 13:39:42 +0100 Subject: [PATCH 51/51] fix: don't enable browser if the config is set --- packages/vitest/src/node/cli/cac.ts | 8 ++++++++ packages/vitest/src/node/cli/cli-api.ts | 9 --------- .../vitest/src/node/workspace/resolveWorkspace.ts | 1 - test/config/test/bail.test.ts | 11 +++++++---- 4 files changed, 15 insertions(+), 14 deletions(-) diff --git a/packages/vitest/src/node/cli/cac.ts b/packages/vitest/src/node/cli/cac.ts index b0a8de330fb4..0fb1d604a9ad 100644 --- a/packages/vitest/src/node/cli/cac.ts +++ b/packages/vitest/src/node/cli/cac.ts @@ -256,6 +256,14 @@ function normalizeCliOptions(cliFilters: string[], argv: CliOptions): CliOptions argv.includeTaskLocation ??= true } + // running "vitest --browser.headless" + if (typeof argv.browser === 'object' && !('enabled' in argv.browser)) { + argv.browser.enabled = true + } + if (typeof argv.typecheck?.only === 'boolean') { + argv.typecheck.enabled ??= true + } + return argv } diff --git a/packages/vitest/src/node/cli/cli-api.ts b/packages/vitest/src/node/cli/cli-api.ts index 4272c30064ad..5edaac0e073a 100644 --- a/packages/vitest/src/node/cli/cli-api.ts +++ b/packages/vitest/src/node/cli/cli-api.ts @@ -143,15 +143,6 @@ export async function prepareVitest( // this shouldn't affect _application root_ that can be changed inside config const root = resolve(options.root || process.cwd()) - // running "vitest --browser.headless" - if (typeof options.browser === 'object' && !('enabled' in options.browser)) { - options.browser.enabled = true - } - - if (typeof options.typecheck?.only === 'boolean') { - options.typecheck.enabled ??= true - } - const ctx = await createVitest(mode, options, viteOverrides, vitestOptions) const environmentPackage = getEnvPackageName(ctx.config.environment) diff --git a/packages/vitest/src/node/workspace/resolveWorkspace.ts b/packages/vitest/src/node/workspace/resolveWorkspace.ts index fe803087f42d..84451100456f 100644 --- a/packages/vitest/src/node/workspace/resolveWorkspace.ts +++ b/packages/vitest/src/node/workspace/resolveWorkspace.ts @@ -138,7 +138,6 @@ export async function resolveBrowserWorkspace( names: Set, resolvedProjects: TestProject[], ) { - // const newConfigs: [project: TestProject, config: ResolvedConfig][] = [] const filters = toArray(vitest.config.project).map(s => wildcardPatternToRegExp(s)) const removeProjects = new Set() diff --git a/test/config/test/bail.test.ts b/test/config/test/bail.test.ts index 261953ef3a88..5fc1e72db67d 100644 --- a/test/config/test/bail.test.ts +++ b/test/config/test/bail.test.ts @@ -69,7 +69,7 @@ for (const config of configs) { // THREADS here means that multiple tests are run parallel process.env.THREADS = isParallel ? 'true' : 'false' - const { exitCode, stdout } = await runVitest({ + const { exitCode, stdout, ctx } = await runVitest({ root: './fixtures/bail', bail: 1, ...config, @@ -78,12 +78,15 @@ for (const config of configs) { }, }) - const browser = config.browser?.instances?.[0].browser + expect(ctx?.config.pool).toBe(config.pool || 'forks') + expect(ctx?.config.browser.enabled).toBe(config.browser?.enabled ?? false) + + const browser = config.browser?.enabled expect(exitCode).toBe(1) if (browser) { - expect(stdout).toMatch(`✓ |${browser}| test/first.test.ts > 1 - first.test.ts - this should pass`) - expect(stdout).toMatch(`× |${browser}| test/first.test.ts > 2 - first.test.ts - this should fail`) + expect(stdout).toMatch(`✓ |chromium| test/first.test.ts > 1 - first.test.ts - this should pass`) + expect(stdout).toMatch(`× |chromium| test/first.test.ts > 2 - first.test.ts - this should fail`) } else { expect(stdout).toMatch('✓ test/first.test.ts > 1 - first.test.ts - this should pass')