diff --git a/packages/browser/package.json b/packages/browser/package.json index 7056ad29da41..a1c036c246ab 100644 --- a/packages/browser/package.json +++ b/packages/browser/package.json @@ -41,6 +41,7 @@ "types": "./dist/index.d.ts", "files": [ "*.d.ts", + "context.js", "dist", "providers" ], @@ -74,7 +75,8 @@ "@vitest/utils": "workspace:*", "magic-string": "^0.30.10", "msw": "^2.3.1", - "sirv": "^2.0.4" + "sirv": "^2.0.4", + "ws": "^8.17.1" }, "devDependencies": { "@types/ws": "^8.5.10", diff --git a/packages/browser/rollup.config.js b/packages/browser/rollup.config.js index c760fe9c2317..3022e2ebfb64 100644 --- a/packages/browser/rollup.config.js +++ b/packages/browser/rollup.config.js @@ -15,6 +15,7 @@ const external = [ /^@?vitest(\/|$)/, 'worker_threads', 'node:worker_threads', + 'vite', ] const plugins = [ @@ -45,7 +46,7 @@ export default () => plugins, }, { - input: './src/client/context.ts', + input: './src/client/tester/context.ts', output: { file: 'dist/context.js', format: 'esm', @@ -56,6 +57,20 @@ export default () => }), ], }, + { + input: './src/client/tester/state.ts', + output: { + file: 'dist/state.js', + format: 'esm', + }, + plugins: [ + esbuild({ + target: 'node18', + minifyWhitespace: true, + }), + resolve(), + ], + }, { input: input.index, output: { @@ -63,6 +78,8 @@ export default () => format: 'esm', }, external, - plugins: [dts()], + plugins: [dts({ + respectExternal: true, + })], }, ]) diff --git a/packages/browser/src/client/client.ts b/packages/browser/src/client/client.ts index 3a18135c886f..8f54dc788987 100644 --- a/packages/browser/src/client/client.ts +++ b/packages/browser/src/client/client.ts @@ -1,8 +1,7 @@ import type { CancelReason } from '@vitest/runner' import { type BirpcReturn, createBirpc } from 'birpc' -import type { WebSocketBrowserEvents, WebSocketBrowserHandlers } from 'vitest' import { parse, stringify } from 'flatted' -import type { VitestBrowserClientMocker } from './mocker' +import type { WebSocketBrowserEvents, WebSocketBrowserHandlers } from '../node/types' import { getBrowserState } from './utils' const PAGE_TYPE = getBrowserState().type @@ -51,18 +50,6 @@ function createClient() { ctx.rpc = createBirpc( { onCancel: setCancel, - async startMocking(id: string) { - // @ts-expect-error not typed global - if (typeof __vitest_mocker__ === 'undefined') { - throw new TypeError( - `Cannot mock modules in the orchestrator process`, - ) - } - // @ts-expect-error not typed global - const mocker = __vitest_mocker__ as VitestBrowserClientMocker - const exports = await mocker.resolve(id) - return Object.keys(exports) - }, async createTesters(files: string[]) { if (PAGE_TYPE !== 'orchestrator') { return diff --git a/packages/browser/src/client/orchestrator.ts b/packages/browser/src/client/orchestrator.ts index c6c33c4be9ca..c36696d21b3e 100644 --- a/packages/browser/src/client/orchestrator.ts +++ b/packages/browser/src/client/orchestrator.ts @@ -2,11 +2,11 @@ import type { ResolvedConfig } from 'vitest' import { generateHash } from '@vitest/runner/utils' import { relative } from 'pathe' import { channel, client } from './client' -import { rpcDone } from './rpc' +import { rpcDone } from './tester/rpc' import { getBrowserState, getConfig } from './utils' import { getUiAPI } from './ui' import type { IframeChannelEvent, IframeChannelIncomingEvent } from './channel' -import { createModuleMocker } from './msw' +import { createModuleMocker } from './tester/msw' const url = new URL(location.href) diff --git a/packages/browser/src/client/public/esm-client-injector.js b/packages/browser/src/client/public/esm-client-injector.js index f45cd06eb61a..34667ced9e60 100644 --- a/packages/browser/src/client/public/esm-client-injector.js +++ b/packages/browser/src/client/public/esm-client-injector.js @@ -24,6 +24,7 @@ window.__vitest_browser_runner__ = { type: { __VITEST_TYPE__ }, contextId: { __VITEST_CONTEXT_ID__ }, provider: { __VITEST_PROVIDER__ }, + providedContext: { __VITEST_PROVIDED_CONTEXT__ }, }; const config = __vitest_browser_runner__.config; diff --git a/packages/browser/src/client/context.ts b/packages/browser/src/client/tester/context.ts similarity index 95% rename from packages/browser/src/client/context.ts rename to packages/browser/src/client/tester/context.ts index 88bc2d8aa4df..062b372aaf9e 100644 --- a/packages/browser/src/client/context.ts +++ b/packages/browser/src/client/tester/context.ts @@ -1,7 +1,7 @@ import type { Task, WorkerGlobalState } from 'vitest' -import type { BrowserPage, UserEvent, UserEventClickOptions, UserEventTabOptions, UserEventTypeOptions } from '../../context' -import type { BrowserRPC } from './client' -import type { BrowserRunnerState } from './utils' +import type { BrowserPage, UserEvent, UserEventClickOptions, UserEventTabOptions, UserEventTypeOptions } from '../../../context' +import type { BrowserRunnerState } from '../utils' +import type { BrowserRPC } from '../client' // this file should not import anything directly, only types @@ -143,7 +143,9 @@ function getWebdriverioSelectOptions(element: Element, value: string | string[] return [{ index: valueIndex }] } - const labelIndex = options.findIndex(option => option.textContent?.trim() === optionValue || option.ariaLabel === optionValue) + const labelIndex = options.findIndex(option => + option.textContent?.trim() === optionValue || option.ariaLabel === optionValue, + ) if (labelIndex === -1) { throw new Error(`The option "${optionValue}" was not found in the "select" options.`) diff --git a/packages/browser/src/client/dialog.ts b/packages/browser/src/client/tester/dialog.ts similarity index 100% rename from packages/browser/src/client/dialog.ts rename to packages/browser/src/client/tester/dialog.ts diff --git a/packages/browser/src/client/logger.ts b/packages/browser/src/client/tester/logger.ts similarity index 94% rename from packages/browser/src/client/logger.ts rename to packages/browser/src/client/tester/logger.ts index dae709776928..c7dcee45ae37 100644 --- a/packages/browser/src/client/logger.ts +++ b/packages/browser/src/client/tester/logger.ts @@ -1,12 +1,10 @@ +import { format, inspect, stringify } from 'vitest/utils' +import { getConfig } from '../utils' import { rpc } from './rpc' -import { getConfig, importId } from './utils' const { Date, console } = globalThis -export async function setupConsoleLogSpy() { - const { stringify, format, inspect } = (await importId( - 'vitest/utils', - )) as typeof import('vitest/utils') +export function setupConsoleLogSpy() { const { log, info, diff --git a/packages/browser/src/client/mocker.ts b/packages/browser/src/client/tester/mocker.ts similarity index 98% rename from packages/browser/src/client/mocker.ts rename to packages/browser/src/client/tester/mocker.ts index c04b9c7b63bc..20efe5eb54e9 100644 --- a/packages/browser/src/client/mocker.ts +++ b/packages/browser/src/client/tester/mocker.ts @@ -1,9 +1,9 @@ import { getType } from '@vitest/utils' import { extname, join } from 'pathe' +import { getBrowserState, importId } from '../utils' +import type { IframeChannelOutgoingEvent } from '../channel' +import { channel, waitForChannel } from '../client' import { rpc } from './rpc' -import { getBrowserState, importId } from './utils' -import { channel, waitForChannel } from './client' -import type { IframeChannelOutgoingEvent } from './channel' const now = Date.now diff --git a/packages/browser/src/client/msw.ts b/packages/browser/src/client/tester/msw.ts similarity index 80% rename from packages/browser/src/client/msw.ts rename to packages/browser/src/client/tester/msw.ts index e5dac3579c5f..da09efd982b9 100644 --- a/packages/browser/src/client/msw.ts +++ b/packages/browser/src/client/tester/msw.ts @@ -5,9 +5,8 @@ import type { IframeMockEvent, IframeMockingDoneEvent, IframeUnmockEvent, -} from './channel' -import { channel } from './channel' -import { client } from './client' +} from '../channel' +import { channel } from '../channel' export function createModuleMocker() { const mocks: Map = new Map() @@ -23,7 +22,6 @@ export function createModuleMocker() { // using a factory if (mock === undefined) { - // TODO: check how the error looks const exports = await getFactoryExports(path) const module = `const module = __vitest_mocker__.get('${path}');` const keys = exports @@ -46,12 +44,7 @@ export function createModuleMocker() { return Response.redirect(mock) } - const content = await client.rpc.automock(path) - return new Response(content, { - headers: { - 'Content-Type': 'application/javascript', - }, - }) + return Response.redirect(injectQuery(path, 'mock=auto')) }), ) @@ -68,7 +61,7 @@ export function createModuleMocker() { startPromise = worker .start({ serviceWorker: { - url: '/__virtual_vitest__:mocker-worker.js', + url: '/__vitest_msw__', }, quiet: true, }) @@ -133,3 +126,23 @@ function passthrough() { }, }) } + +const postfixRE = /[?#].*$/ +function cleanUrl(url: string): string { + return url.replace(postfixRE, '') +} + +const replacePercentageRE = /%/g +function injectQuery(url: string, queryToInject: string): string { + // encode percents for consistent behavior with pathToFileURL + // see #2614 for details + const resolvedUrl = new URL( + url.replace(replacePercentageRE, '%25'), + location.href, + ) + const { search, hash } = resolvedUrl + const pathname = cleanUrl(url) + return `${pathname}?${queryToInject}${search ? `&${search.slice(1)}` : ''}${ + hash ?? '' + }` +} diff --git a/packages/browser/src/client/rpc.ts b/packages/browser/src/client/tester/rpc.ts similarity index 77% rename from packages/browser/src/client/rpc.ts rename to packages/browser/src/client/tester/rpc.ts index f93b8e8295ba..07ccbc11e5eb 100644 --- a/packages/browser/src/client/rpc.ts +++ b/packages/browser/src/client/tester/rpc.ts @@ -1,6 +1,5 @@ -import type { getSafeTimers } from '@vitest/utils' -import { importId } from './utils' -import type { VitestBrowserClient } from './client' +import { getSafeTimers } from 'vitest/utils' +import type { VitestBrowserClient } from '../client' const { get } = Reflect @@ -42,7 +41,6 @@ export async function rpcDone() { export function createSafeRpc( client: VitestBrowserClient, - getTimers: () => any, ): VitestBrowserClient['rpc'] { return new Proxy(client.rpc, { get(target, p, handler) { @@ -51,7 +49,7 @@ export function createSafeRpc( } const sendCall = get(target, p, handler) const safeSendCall = (...args: any[]) => - withSafeTimers(getTimers, async () => { + withSafeTimers(getSafeTimers, async () => { const result = sendCall(...args) promises.add(result) try { @@ -67,14 +65,6 @@ export function createSafeRpc( }) } -export async function loadSafeRpc(client: VitestBrowserClient) { - // if importing /@id/ failed, we reload the page waiting until Vite prebundles it - const { getSafeTimers } = (await importId( - 'vitest/utils', - )) as typeof import('vitest/utils') - return createSafeRpc(client, getSafeTimers) -} - export function rpc(): VitestBrowserClient['rpc'] { // @ts-expect-error not typed global return globalThis.__vitest_worker__.rpc diff --git a/packages/browser/src/client/runner.ts b/packages/browser/src/client/tester/runner.ts similarity index 97% rename from packages/browser/src/client/runner.ts rename to packages/browser/src/client/tester/runner.ts index df34c9343f19..1ccbe183bc3f 100644 --- a/packages/browser/src/client/runner.ts +++ b/packages/browser/src/client/tester/runner.ts @@ -1,9 +1,9 @@ import type { File, Task, TaskResultPack, VitestRunner } from '@vitest/runner' import type { ResolvedConfig, WorkerGlobalState } from 'vitest' import type { VitestExecutor } from 'vitest/execute' -import { rpc } from './rpc' -import { importId } from './utils' +import { importId } from '../utils' import { VitestBrowserSnapshotEnvironment } from './snapshot' +import { rpc } from './rpc' import type { VitestBrowserClientMocker } from './mocker' interface BrowserRunnerOptions { @@ -126,8 +126,7 @@ export async function initiateRunner( takeCoverageInsideWorker(config.coverage, { executeId: importId }), }) if (!config.snapshotOptions.snapshotEnvironment) { - config.snapshotOptions.snapshotEnvironment - = new VitestBrowserSnapshotEnvironment() + config.snapshotOptions.snapshotEnvironment = new VitestBrowserSnapshotEnvironment() } const runner = new BrowserRunner({ config, diff --git a/packages/browser/src/client/snapshot.ts b/packages/browser/src/client/tester/snapshot.ts similarity index 95% rename from packages/browser/src/client/snapshot.ts rename to packages/browser/src/client/tester/snapshot.ts index 3b6cd50f3ddc..65218325943d 100644 --- a/packages/browser/src/client/snapshot.ts +++ b/packages/browser/src/client/tester/snapshot.ts @@ -1,5 +1,5 @@ import type { SnapshotEnvironment } from 'vitest/snapshot' -import type { VitestBrowserClient } from './client' +import type { VitestBrowserClient } from '../client' export class VitestBrowserSnapshotEnvironment implements SnapshotEnvironment { getVersion(): string { diff --git a/packages/browser/src/client/tester/state.ts b/packages/browser/src/client/tester/state.ts new file mode 100644 index 000000000000..411a0ffe7914 --- /dev/null +++ b/packages/browser/src/client/tester/state.ts @@ -0,0 +1,46 @@ +import type { WorkerGlobalState } from 'vitest' +import { parse } from 'flatted' +import { getBrowserState } from '../utils' + +const config = getBrowserState().config + +const providedContext = parse(getBrowserState().providedContext) + +const state: WorkerGlobalState = { + ctx: { + pool: 'browser', + worker: './browser.js', + workerId: 1, + config, + projectName: config.name || '', + files: [], + environment: { + name: 'browser', + options: null, + }, + providedContext, + invalidates: [], + }, + onCancel: null as any, + mockMap: new Map(), + config, + environment: { + name: 'browser', + transformMode: 'web', + setup() { + throw new Error('Not called in the browser') + }, + }, + moduleCache: getBrowserState().moduleCache, + rpc: null as any, + durations: { + environment: 0, + prepare: performance.now(), + }, + providedContext, +} + +// @ts-expect-error not typed global +globalThis.__vitest_browser__ = true +// @ts-expect-error not typed global +globalThis.__vitest_worker__ = state diff --git a/packages/browser/src/client/tester.html b/packages/browser/src/client/tester/tester.html similarity index 88% rename from packages/browser/src/client/tester.html rename to packages/browser/src/client/tester/tester.html index 8dfad61fb9b0..60bfdd5089f1 100644 --- a/packages/browser/src/client/tester.html +++ b/packages/browser/src/client/tester/tester.html @@ -17,6 +17,7 @@ } + {__VITEST_SCRIPTS__} - + {__VITEST_APPEND__} diff --git a/packages/browser/src/client/tester.ts b/packages/browser/src/client/tester/tester.ts similarity index 72% rename from packages/browser/src/client/tester.ts rename to packages/browser/src/client/tester/tester.ts index 9f33f2a27840..50d40ffab756 100644 --- a/packages/browser/src/client/tester.ts +++ b/packages/browser/src/client/tester/tester.ts @@ -1,18 +1,15 @@ -import type { WorkerGlobalState } from 'vitest' -import { channel, client, onCancel } from './client' +import { SpyModule, setupCommonEnv, startTests } from 'vitest/browser' +import { getBrowserState, getConfig, getWorkerState } from '../utils' +import { channel, client, onCancel } from '../client' import { setupDialogsSpy } from './dialog' -import { setupConsoleLogSpy } from './logger' -import { browserHashMap, initiateRunner } from './runner' -import { getBrowserState, getConfig, importId } from './utils' -import { loadSafeRpc } from './rpc' -import { VitestBrowserClientMocker } from './mocker' import { registerUnexpectedErrors, - registerUnhandledErrors, serializeError, } from './unhandled' - -const stopErrorHandler = registerUnhandledErrors() +import { setupConsoleLogSpy } from './logger' +import { createSafeRpc } from './rpc' +import { browserHashMap, initiateRunner } from './runner' +import { VitestBrowserClientMocker } from './mocker' const url = new URL(location.href) const reloadStart = url.searchParams.get('__reloadStart') @@ -76,63 +73,27 @@ async function tryCall( } } -const startTime = performance.now() - async function prepareTestEnvironment(files: string[]) { debug('trying to resolve runner', `${reloadStart}`) const config = getConfig() - const viteClientPath = `/@vite/client` - await import(viteClientPath) - - const rpc: any = await loadSafeRpc(client) - - const providedContext = await client.rpc.getProvidedContext() - - const state: WorkerGlobalState = { - ctx: { - pool: 'browser', - worker: './browser.js', - workerId: 1, - config, - projectName: config.name || '', - files, - environment: { - name: 'browser', - options: null, - }, - providedContext, - invalidates: [], - }, - onCancel, - mockMap: new Map(), - config, - environment: { - name: 'browser', - transformMode: 'web', - setup() { - throw new Error('Not called in the browser') - }, - }, - moduleCache: getBrowserState().moduleCache, - rpc, - durations: { - environment: 0, - prepare: startTime, - }, - providedContext, - } - // @ts-expect-error untyped global for internal use - globalThis.__vitest_browser__ = true - // @ts-expect-error mocking vitest apis - globalThis.__vitest_worker__ = state + const rpc = createSafeRpc(client) + + const state = getWorkerState() + + state.ctx.files = files + state.onCancel = onCancel + state.rpc = rpc as any + const mocker = new VitestBrowserClientMocker() // @ts-expect-error mocking vitest apis globalThis.__vitest_mocker__ = mocker - await setupConsoleLogSpy() + setupConsoleLogSpy() setupDialogsSpy() + const runner = await initiateRunner(state, mocker, config) + const version = url.searchParams.get('browserv') || '' files.forEach((filename) => { const currentVersion = browserHashMap.get(filename) @@ -141,13 +102,6 @@ async function prepareTestEnvironment(files: string[]) { } }) - const [runner, { startTests, setupCommonEnv, SpyModule }] = await Promise.all( - [ - initiateRunner(state, mocker, config), - importId('vitest/browser') as Promise, - ], - ) - mocker.setSpyModule(SpyModule) mocker.setupWorker() @@ -155,7 +109,6 @@ async function prepareTestEnvironment(files: string[]) { runner.onCancel?.(reason) }) - stopErrorHandler() registerUnexpectedErrors(rpc) return { diff --git a/packages/browser/src/client/unhandled.ts b/packages/browser/src/client/tester/unhandled.ts similarity index 66% rename from packages/browser/src/client/unhandled.ts rename to packages/browser/src/client/tester/unhandled.ts index d9f049573fd8..1974b761bc1d 100644 --- a/packages/browser/src/client/unhandled.ts +++ b/packages/browser/src/client/tester/unhandled.ts @@ -1,6 +1,5 @@ -import type { client } from './client' -import { channel } from './client' -import { getBrowserState, importId } from './utils' +import { processError } from 'vitest/browser' +import type { client } from '../client' function on(event: string, listener: (...args: any[]) => void) { window.addEventListener(event, listener) @@ -16,18 +15,6 @@ export function serializeError(unhandledError: any) { } } -// we can't import "processError" yet because error might've been thrown before the module was loaded -async function defaultErrorReport(type: string, unhandledError: any) { - const error = serializeError(unhandledError) - channel.postMessage({ - type: 'error', - files: getBrowserState().runningFiles, - error, - errorType: type, - id: getBrowserState().iframeId!, - }) -} - function catchWindowErrors(cb: (e: ErrorEvent) => void) { let userErrorListenerCount = 0 function throwUnhandlerError(e: ErrorEvent) { @@ -62,18 +49,6 @@ function catchWindowErrors(cb: (e: ErrorEvent) => void) { } } -export function registerUnhandledErrors() { - const stopErrorHandler = catchWindowErrors(e => - defaultErrorReport('Error', e.error), - ) - const stopRejectionHandler = on('unhandledrejection', e => - defaultErrorReport('Unhandled Rejection', e.reason)) - return () => { - stopErrorHandler() - stopRejectionHandler() - } -} - export function registerUnexpectedErrors(rpc: typeof client.rpc) { catchWindowErrors(event => reportUnexpectedError(rpc, 'Error', event.error), @@ -87,9 +62,6 @@ async function reportUnexpectedError( type: string, error: any, ) { - const { processError } = (await importId( - 'vitest/browser', - )) as typeof import('vitest/browser') const processedError = processError(error) await rpc.onUnhandledError(processedError, type) } diff --git a/packages/browser/src/client/utils.ts b/packages/browser/src/client/utils.ts index 611fba2ce36c..9c3321594ab3 100644 --- a/packages/browser/src/client/utils.ts +++ b/packages/browser/src/client/utils.ts @@ -18,6 +18,7 @@ export interface BrowserRunnerState { viteConfig: { root: string } + providedContext: string type: 'tester' | 'orchestrator' wrapModule: (module: () => T) => T iframeId?: string @@ -26,7 +27,18 @@ export interface BrowserRunnerState { createTesters?: (files: string[]) => Promise } +/* @__NO_SIDE_EFFECTS__ */ export function getBrowserState(): BrowserRunnerState { // @ts-expect-error not typed global return window.__vitest_browser_runner__ } + +/* @__NO_SIDE_EFFECTS__ */ +export function getWorkerState(): WorkerGlobalState { + // @ts-expect-error not typed global + const state = window.__vitest_worker__ + if (!state) { + throw new Error('Worker state is not found. This is an issue with Vitest. Please, open an issue.') + } + return state +} diff --git a/packages/browser/src/client/vite.config.ts b/packages/browser/src/client/vite.config.ts index 9292b4b05a4d..1840470bb8a1 100644 --- a/packages/browser/src/client/vite.config.ts +++ b/packages/browser/src/client/vite.config.ts @@ -17,7 +17,7 @@ export default defineConfig({ rollupOptions: { input: { orchestrator: resolve(__dirname, './orchestrator.html'), - tester: resolve(__dirname, './tester.html'), + tester: resolve(__dirname, './tester/tester.html'), }, external: [/__virtual_vitest__/], }, @@ -27,8 +27,8 @@ export default defineConfig({ name: 'virtual:msw', enforce: 'pre', resolveId(id) { - if (id.startsWith('msw')) { - return `/__virtual_vitest__:${id}` + if (id.startsWith('msw') || id.startsWith('vitest')) { + return `/__virtual_vitest__?id=${encodeURIComponent(id)}` } }, }, diff --git a/packages/browser/src/node/index.ts b/packages/browser/src/node/index.ts index 4a450362d97b..9187848a2fc2 100644 --- a/packages/browser/src/node/index.ts +++ b/packages/browser/src/node/index.ts @@ -1,434 +1,53 @@ -import { fileURLToPath } from 'node:url' -import { readFile } from 'node:fs/promises' -import { createRequire } from 'node:module' -import { basename, join, resolve } from 'pathe' -import sirv from 'sirv' -import type { ViteDevServer } from 'vite' -import type { ResolvedConfig } from 'vitest' -import type { BrowserScript, WorkspaceProject } from 'vitest/node' -import { getFilePoolName, distDir as vitestDist } from 'vitest/node' -import { type Plugin, coverageConfigDefaults } from 'vitest/config' -import { slash, toArray } from '@vitest/utils' -import BrowserContext from './plugins/pluginContext' -import DynamicImport from './plugins/pluginDynamicImport' - -export type { BrowserCommand } from 'vitest/node' -export { defineBrowserCommand } from './commands/utils' - -export default (project: WorkspaceProject, base = '/'): Plugin[] => { - const pkgRoot = resolve(fileURLToPath(import.meta.url), '../..') - const distRoot = resolve(pkgRoot, 'dist') - - return [ - { - enforce: 'pre', - name: 'vitest:browser', - async config(viteConfig) { - // Enables using ignore hint for coverage providers with @preserve keyword - if (viteConfig.esbuild !== false) { - viteConfig.esbuild ||= {} - viteConfig.esbuild.legalComments = 'inline' - } - }, - async configureServer(server) { - const testerHtml = readFile( - resolve(distRoot, 'client/tester.html'), - 'utf8', - ) - const orchestratorHtml = project.config.browser.ui - ? readFile(resolve(distRoot, 'client/__vitest__/index.html'), 'utf8') - : readFile(resolve(distRoot, 'client/orchestrator.html'), 'utf8') - const injectorJs = readFile( - resolve(distRoot, 'client/esm-client-injector.js'), - 'utf8', - ) - const manifest = (async () => { - return JSON.parse( - await readFile(`${distRoot}/client/.vite/manifest.json`, 'utf8'), - ) - })() - const favicon = `${base}favicon.svg` - const testerPrefix = `${base}__vitest_test__/__test__/` - server.middlewares.use((_req, res, next) => { - const headers = server.config.server.headers - if (headers) { - for (const name in headers) { - res.setHeader(name, headers[name]!) - } - } - next() - }) - let orchestratorScripts: string | undefined - let testerScripts: string | undefined - server.middlewares.use(async (req, res, next) => { - if (!req.url) { - return next() - } - const url = new URL(req.url, 'http://localhost') - if (!url.pathname.startsWith(testerPrefix) && url.pathname !== base) { - return next() - } - - res.setHeader( - 'Cache-Control', - 'no-cache, max-age=0, must-revalidate', - ) - res.setHeader('Content-Type', 'text/html; charset=utf-8') - - const config = wrapConfig(project.getSerializableConfig()) - config.env ??= {} - config.env.VITEST_BROWSER_DEBUG - = process.env.VITEST_BROWSER_DEBUG || '' - - // remove custom iframe related headers to allow the iframe to load - res.removeHeader('X-Frame-Options') - - if (url.pathname === base) { - let contextId = url.searchParams.get('contextId') - // it's possible to open the page without a context, - // for now, let's assume it should be the first one - if (!contextId) { - contextId = project.browserState.keys().next().value ?? 'none' - } - - const files = project.browserState.get(contextId!)?.files ?? [] - - const injector = replacer(await injectorJs, { - __VITEST_PROVIDER__: JSON.stringify(project.browserProvider!.name), - __VITEST_CONFIG__: JSON.stringify(config), - __VITEST_VITE_CONFIG__: JSON.stringify({ - root: project.browser!.config.root, - }), - __VITEST_FILES__: JSON.stringify(files), - __VITEST_TYPE__: - url.pathname === base ? '"orchestrator"' : '"tester"', - __VITEST_CONTEXT_ID__: JSON.stringify(contextId), - }) - - // disable CSP for the orchestrator as we are the ones controlling it - res.removeHeader('Content-Security-Policy') - - if (!orchestratorScripts) { - orchestratorScripts = await formatScripts( - project.config.browser.orchestratorScripts, - server, - ) - } - - let baseHtml = await orchestratorHtml - - // if UI is enabled, use UI HTML and inject the orchestrator script - if (project.config.browser.ui) { - const manifestContent = await manifest - const jsEntry = manifestContent['orchestrator.html'].file - baseHtml = baseHtml - .replaceAll('./assets/', `${base}__vitest__/assets/`) - .replace( - '', - [ - '', - '{__VITEST_SCRIPTS__}', - ``, - ].join('\n'), - ) - } - - const html = replacer(baseHtml, { - __VITEST_FAVICON__: favicon, - __VITEST_TITLE__: 'Vitest Browser Runner', - __VITEST_SCRIPTS__: orchestratorScripts, - __VITEST_INJECTOR__: injector, - __VITEST_CONTEXT_ID__: JSON.stringify(contextId), - }) - res.write(html, 'utf-8') - res.end() - return - } - - const csp = res.getHeader('Content-Security-Policy') - if (typeof csp === 'string') { - // add frame-ancestors to allow the iframe to be loaded by Vitest, - // but keep the rest of the CSP - res.setHeader( - 'Content-Security-Policy', - csp.replace(/frame-ancestors [^;]+/, 'frame-ancestors *'), - ) - } - - const [contextId, testFile] = url.pathname - .slice(testerPrefix.length) - .split('/') - const decodedTestFile = decodeURIComponent(testFile) - 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 - = decodedTestFile === '__vitest_all__' - || !testFiles.includes(decodedTestFile) - ? '__vitest_browser_runner__.files' - : JSON.stringify([decodedTestFile]) - const iframeId = JSON.stringify(decodedTestFile) - const files = project.browserState.get(contextId)?.files ?? [] - - const injector = replacer(await injectorJs, { - __VITEST_PROVIDER__: JSON.stringify(project.browserProvider!.name), - __VITEST_CONFIG__: JSON.stringify(config), - __VITEST_FILES__: JSON.stringify(files), - __VITEST_VITE_CONFIG__: JSON.stringify({ - root: project.browser!.config.root, - }), - __VITEST_TYPE__: - url.pathname === base ? '"orchestrator"' : '"tester"', - __VITEST_CONTEXT_ID__: JSON.stringify(contextId), - }) - - if (!testerScripts) { - testerScripts = await formatScripts( - project.config.browser.testerScripts, - server, - ) - } - - const html = replacer(await testerHtml, { - __VITEST_FAVICON__: favicon, - __VITEST_TITLE__: 'Vitest Browser Tester', - __VITEST_SCRIPTS__: testerScripts, - __VITEST_INJECTOR__: injector, - __VITEST_APPEND__: - // TODO: have only a single global variable to not pollute the global scope - ``, - }) - res.write(html, 'utf-8') - res.end() - }) - server.middlewares.use( - base, - sirv(resolve(distRoot, 'client'), { - single: false, - dev: true, - }), - ) - - const coverageFolder = resolveCoverageFolder(project) - const coveragePath = coverageFolder ? coverageFolder[1] : undefined - if (coveragePath && base === coveragePath) { - throw new Error( - `The ui base path and the coverage path cannot be the same: ${base}, change coverage.reportsDirectory`, - ) - } - - coverageFolder - && server.middlewares.use( - coveragePath!, - sirv(coverageFolder[0], { - single: true, - dev: true, - setHeaders: (res) => { - res.setHeader( - 'Cache-Control', - 'public,max-age=0,must-revalidate', - ) - }, - }), - ) - }, - }, - { - name: 'vitest:browser:tests', - enforce: 'pre', - async config() { - const allTestFiles = await project.globTestFiles() - const browserTestFiles = allTestFiles.filter( - file => getFilePoolName(project, file) === 'browser', - ) - const setupFiles = toArray(project.config.setupFiles) - const vitestPaths = [ - resolve(vitestDist, 'index.js'), - resolve(vitestDist, 'browser.js'), - resolve(vitestDist, 'runners.js'), - resolve(vitestDist, 'utils.js'), - ] - return { - optimizeDeps: { - entries: [...browserTestFiles, ...setupFiles, ...vitestPaths], - exclude: [ - 'vitest', - 'vitest/utils', - 'vitest/browser', - 'vitest/runners', - '@vitest/utils', - '@vitest/runner', - '@vitest/spy', - '@vitest/utils/error', - '@vitest/snapshot', - '@vitest/expect', - 'std-env', - 'tinybench', - 'tinyspy', - 'pathe', - 'msw', - 'msw/browser', - ], - include: [ - 'vitest > @vitest/utils > pretty-format', - 'vitest > @vitest/snapshot > pretty-format', - 'vitest > @vitest/snapshot > magic-string', - 'vitest > pretty-format', - 'vitest > pretty-format > ansi-styles', - 'vitest > pretty-format > ansi-regex', - 'vitest > chai', - 'vitest > chai > loupe', - 'vitest > @vitest/runner > p-limit', - 'vitest > @vitest/utils > diff-sequences', - '@vitest/browser > @testing-library/user-event', - '@vitest/browser > @testing-library/dom', - ], - }, - } - }, - async resolveId(id) { - if (!/\?browserv=\w+$/.test(id)) { - return - } - - let useId = id.slice(0, id.lastIndexOf('?')) - if (useId.startsWith('/@fs/')) { - useId = useId.slice(5) - } - - if (/^\w:/.test(useId)) { - useId = useId.replace(/\\/g, '/') - } - - return useId - }, - }, - { - name: 'vitest:browser:resolve-virtual', - async resolveId(rawId) { - if (rawId.startsWith('/__virtual_vitest__:')) { - let id = rawId.slice('/__virtual_vitest__:'.length) - // TODO: don't hardcode - if (id === 'mocker-worker.js') { - id = 'msw/mockServiceWorker.js' - } - - const resolved = await this.resolve(id, distRoot, { - skipSelf: true, - }) - return resolved - } - }, - }, - BrowserContext(project), - DynamicImport(), - // TODO: remove this when @testing-library/vue supports ESM - { - name: 'vitest:browser:support-vue-testing-library', - config() { - return { - optimizeDeps: { - esbuildOptions: { - plugins: [ - { - name: 'test-utils-rewrite', - setup(build) { - const _require = createRequire(import.meta.url) - build.onResolve({ filter: /@vue\/test-utils/ }, (args) => { - // resolve to CJS instead of the browser because the browser version expects a global Vue object - const resolved = _require.resolve(args.path, { - paths: [args.importer], - }) - return { path: resolved } - }) - }, - }, - ], - }, - }, - } - }, - }, - ] -} - -function resolveCoverageFolder(project: WorkspaceProject) { - const options = project.ctx.config - const htmlReporter = options.coverage?.enabled - ? toArray(options.coverage.reporter).find((reporter) => { - if (typeof reporter === 'string') { - return reporter === 'html' - } - - return reporter[0] === 'html' - }) - : undefined - - if (!htmlReporter) { - return undefined - } - - // reportsDirectory not resolved yet - const root = resolve( - options.root || process.cwd(), - options.coverage.reportsDirectory || coverageConfigDefaults.reportsDirectory, - ) - - const subdir - = Array.isArray(htmlReporter) - && htmlReporter.length > 1 - && 'subdir' in htmlReporter[1] - ? htmlReporter[1].subdir - : undefined - - if (!subdir || typeof subdir !== 'string') { - return [root, `/${basename(root)}/`] - } +import type { WorkspaceProject } from 'vitest/node' +import type { Plugin } from 'vitest/config' +import { createServer } from 'vitest/node' +import { setupBrowserRpc } from './rpc' +import { BrowserServer } from './server' +import BrowserPlugin from './plugin' + +export type { BrowserServer } from './server' +export { createBrowserPool } from './pool' + +export async function createBrowserServer( + project: WorkspaceProject, + configFile: string | undefined, + prePlugins: Plugin[] = [], + postPlugins: Plugin[] = [], +) { + const server = new BrowserServer(project, '/') - return [resolve(root, subdir), `/${basename(root)}/${subdir}/`] -} + const root = project.config.root -function wrapConfig(config: ResolvedConfig): ResolvedConfig { - return { - ...config, - // workaround RegExp serialization - testNamePattern: config.testNamePattern - ? (config.testNamePattern.toString() as any as RegExp) - : undefined, - } -} + await project.ctx.packageInstaller.ensureInstalled('@vitest/browser', root) -function replacer(code: string, values: Record) { - return code.replace(/\{\s*(\w+)\s*\}/g, (_, key) => values[key] ?? '') -} + const configPath = typeof configFile === 'string' ? configFile : false -async function formatScripts( - scripts: BrowserScript[] | undefined, - server: ViteDevServer, -) { - if (!scripts?.length) { - return '' - } - const promises = scripts.map( - async ({ content, src, async, id, type = 'module' }, index) => { - 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 `` + const vite = await createServer({ + ...project.options, // spread project config inlined in root workspace config + base: '/', + logLevel: 'error', + mode: project.config.mode, + configFile: configPath, + // watch is handled by Vitest + server: { + hmr: false, + watch: null, + preTransformRequests: false, }, - ) - return (await Promise.all(promises)).join('\n') + plugins: [ + ...prePlugins, + ...(project.options?.plugins || []), + BrowserPlugin(server), + ...postPlugins, + ], + }) + + await vite.listen() + + setupBrowserRpc(server) + // if (project.config.browser.ui) { + // setupUiRpc(project.ctx, server) + // } + + return server } diff --git a/packages/browser/src/node/plugin.ts b/packages/browser/src/node/plugin.ts new file mode 100644 index 000000000000..0893d1fb9b82 --- /dev/null +++ b/packages/browser/src/node/plugin.ts @@ -0,0 +1,314 @@ +import { fileURLToPath } from 'node:url' +import { createRequire } from 'node:module' +import { readFileSync } from 'node:fs' +import { basename, resolve } from 'pathe' +import sirv from 'sirv' +import type { WorkspaceProject } from 'vitest/node' +import { getFilePoolName, resolveApiServerConfig, resolveFsAllow, distDir as vitestDist } from 'vitest/node' +import { type Plugin, coverageConfigDefaults } from 'vitest/config' +import { toArray } from '@vitest/utils' +import { defaultBrowserPort } from 'vitest/config' +import BrowserContext from './plugins/pluginContext' +import DynamicImport from './plugins/pluginDynamicImport' +import type { BrowserServer } from './server' +import { resolveOrchestrator } from './serverOrchestrator' +import { resolveTester } from './serverTester' + +export type { BrowserCommand } from 'vitest/node' +export { defineBrowserCommand } from './commands/utils' + +export default (browserServer: BrowserServer, base = '/'): Plugin[] => { + const pkgRoot = resolve(fileURLToPath(import.meta.url), '../..') + const distRoot = resolve(pkgRoot, 'dist') + const project = browserServer.project + + return [ + { + enforce: 'pre', + name: 'vitest:browser', + async configureServer(server) { + browserServer.setServer(server) + + // eslint-disable-next-line prefer-arrow-callback + server.middlewares.use(function vitestHeaders(_req, res, next) { + const headers = server.config.server.headers + if (headers) { + for (const name in headers) { + res.setHeader(name, headers[name]!) + } + } + next() + }) + // eslint-disable-next-line prefer-arrow-callback + server.middlewares.use(async function vitestBrowserMode(req, res, next) { + if (!req.url) { + return next() + } + const url = new URL(req.url, 'http://localhost') + if (!url.pathname.startsWith(browserServer.prefixTesterUrl) && url.pathname !== base) { + return next() + } + + res.setHeader( + 'Cache-Control', + 'no-cache, max-age=0, must-revalidate', + ) + res.setHeader('Content-Type', 'text/html; charset=utf-8') + + // remove custom iframe related headers to allow the iframe to load + res.removeHeader('X-Frame-Options') + + if (url.pathname === base) { + const html = await resolveOrchestrator(browserServer, url, res) + res.write(html, 'utf-8') + res.end() + return + } + + const html = await resolveTester(browserServer, url, res) + res.write(html, 'utf-8') + res.end() + }) + + server.middlewares.use( + `${base}favicon.svg`, + (_, res) => { + const content = readFileSync(resolve(distRoot, 'client/favicon.svg')) + res.write(content, 'utf-8') + res.end() + }, + ) + + const coverageFolder = resolveCoverageFolder(project) + const coveragePath = coverageFolder ? coverageFolder[1] : undefined + if (coveragePath && base === coveragePath) { + throw new Error( + `The ui base path and the coverage path cannot be the same: ${base}, change coverage.reportsDirectory`, + ) + } + + coverageFolder && server.middlewares.use( + coveragePath!, + sirv(coverageFolder[0], { + single: true, + dev: true, + setHeaders: (res) => { + res.setHeader( + 'Cache-Control', + 'public,max-age=0,must-revalidate', + ) + }, + }), + ) + }, + }, + { + name: 'vitest:browser:tests', + enforce: 'pre', + async config() { + const allTestFiles = await project.globTestFiles() + const browserTestFiles = allTestFiles.filter( + file => getFilePoolName(project, file) === 'browser', + ) + const setupFiles = toArray(project.config.setupFiles) + return { + optimizeDeps: { + entries: [ + ...browserTestFiles, + ...setupFiles, + resolve(vitestDist, 'index.js'), + resolve(vitestDist, 'browser.js'), + resolve(vitestDist, 'runners.js'), + resolve(vitestDist, 'utils.js'), + ], + exclude: [ + 'vitest', + 'vitest/utils', + 'vitest/browser', + 'vitest/runners', + '@vitest/utils', + '@vitest/runner', + '@vitest/spy', + '@vitest/utils/error', + '@vitest/snapshot', + '@vitest/expect', + 'std-env', + 'tinybench', + 'tinyspy', + 'pathe', + 'msw', + 'msw/browser', + ], + include: [ + 'vitest > @vitest/utils > pretty-format', + 'vitest > @vitest/snapshot > pretty-format', + 'vitest > @vitest/snapshot > magic-string', + 'vitest > pretty-format', + 'vitest > pretty-format > ansi-styles', + 'vitest > pretty-format > ansi-regex', + 'vitest > chai', + 'vitest > chai > loupe', + 'vitest > @vitest/runner > p-limit', + 'vitest > @vitest/utils > diff-sequences', + '@vitest/browser > @testing-library/user-event', + '@vitest/browser > @testing-library/dom', + ], + }, + } + }, + async resolveId(id) { + if (!/\?browserv=\w+$/.test(id)) { + return + } + + let useId = id.slice(0, id.lastIndexOf('?')) + if (useId.startsWith('/@fs/')) { + useId = useId.slice(5) + } + + if (/^\w:/.test(useId)) { + useId = useId.replace(/\\/g, '/') + } + + return useId + }, + }, + { + name: 'vitest:browser:resolve-virtual', + async resolveId(rawId) { + if (rawId.startsWith('/__virtual_vitest__')) { + const url = new URL(rawId, 'http://localhost') + if (!url.searchParams.has('id')) { + throw new TypeError(`Invalid virtual module id: ${rawId}, requires "id" query.`) + } + + const id = decodeURIComponent(url.searchParams.get('id')!) + + const resolved = await this.resolve(id, distRoot, { + skipSelf: true, + }) + return resolved + } + + if (rawId === '/__vitest_msw__') { + return this.resolve('msw/mockServiceWorker.js', distRoot, { + skipSelf: true, + }) + } + }, + }, + { + name: 'vitest:browser:assets', + resolveId(id) { + if (id.startsWith('/__vitest_browser__/') || id.startsWith('/__vitest__/')) { + return resolve(distRoot, 'client', id.slice(1)) + } + }, + }, + BrowserContext(browserServer), + DynamicImport(), + { + name: 'vitest:browser:config', + enforce: 'post', + async config(viteConfig) { + // Enables using ignore hint for coverage providers with @preserve keyword + if (viteConfig.esbuild !== false) { + viteConfig.esbuild ||= {} + viteConfig.esbuild.legalComments = 'inline' + } + const server = resolveApiServerConfig( + viteConfig.test?.browser || {}, + defaultBrowserPort, + ) || { + port: defaultBrowserPort, + } + + // browser never runs in middleware mode + server.middlewareMode = false + + viteConfig.server = { + ...viteConfig.server, + ...server, + open: false, + } + viteConfig.server.fs ??= {} + viteConfig.server.fs.allow = viteConfig.server.fs.allow || [] + viteConfig.server.fs.allow.push( + ...resolveFsAllow( + project.ctx.config.root, + project.ctx.server.config.configFile, + ), + ) + + return { + resolve: { + alias: viteConfig.test?.alias, + }, + } + }, + }, + // TODO: remove this when @testing-library/vue supports ESM + { + name: 'vitest:browser:support-vue-testing-library', + config() { + return { + optimizeDeps: { + esbuildOptions: { + plugins: [ + { + name: 'test-utils-rewrite', + setup(build) { + const _require = createRequire(import.meta.url) + build.onResolve({ filter: /@vue\/test-utils/ }, (args) => { + // resolve to CJS instead of the browser because the browser version expects a global Vue object + const resolved = _require.resolve(args.path, { + paths: [args.importer], + }) + return { path: resolved } + }) + }, + }, + ], + }, + }, + } + }, + }, + ] +} + +function resolveCoverageFolder(project: WorkspaceProject) { + const options = project.ctx.config + const htmlReporter = options.coverage?.enabled + ? toArray(options.coverage.reporter).find((reporter) => { + if (typeof reporter === 'string') { + return reporter === 'html' + } + + return reporter[0] === 'html' + }) + : undefined + + if (!htmlReporter) { + return undefined + } + + // reportsDirectory not resolved yet + const root = resolve( + options.root || process.cwd(), + options.coverage.reportsDirectory || coverageConfigDefaults.reportsDirectory, + ) + + const subdir + = Array.isArray(htmlReporter) + && htmlReporter.length > 1 + && 'subdir' in htmlReporter[1] + ? htmlReporter[1].subdir + : undefined + + if (!subdir || typeof subdir !== 'string') { + return [root, `/${basename(root)}/`] + } + + return [resolve(root, subdir), `/${basename(root)}/${subdir}/`] +} diff --git a/packages/browser/src/node/plugins/pluginContext.ts b/packages/browser/src/node/plugins/pluginContext.ts index f9dadb32a943..664e78db79b1 100644 --- a/packages/browser/src/node/plugins/pluginContext.ts +++ b/packages/browser/src/node/plugins/pluginContext.ts @@ -1,17 +1,19 @@ import { fileURLToPath } from 'node:url' import type { Plugin } from 'vitest/config' -import type { BrowserProvider, WorkspaceProject } from 'vitest/node' +import type { BrowserProvider } from 'vitest/node' import { dirname, resolve } from 'pathe' import type { PluginContext } from 'rollup' import { slash } from '@vitest/utils' import builtinCommands from '../commands/index' +import type { BrowserServer } from '../server' const VIRTUAL_ID_CONTEXT = '\0@vitest/browser/context' const ID_CONTEXT = '@vitest/browser/context' const __dirname = dirname(fileURLToPath(import.meta.url)) -export default function BrowserContext(project: WorkspaceProject): Plugin { +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 @@ -36,7 +38,7 @@ export default function BrowserContext(project: WorkspaceProject): Plugin { }, load(id) { if (id === VIRTUAL_ID_CONTEXT) { - return generateContextFile.call(this, project) + return generateContextFile.call(this, server) } }, } @@ -44,12 +46,12 @@ export default function BrowserContext(project: WorkspaceProject): Plugin { async function generateContextFile( this: PluginContext, - project: WorkspaceProject, + server: BrowserServer, ) { - const commands = Object.keys(project.config.browser.commands ?? {}) + const commands = Object.keys(server.project.config.browser.commands ?? {}) const filepathCode = '__vitest_worker__.filepath || __vitest_worker__.current?.file?.filepath || undefined' - const provider = project.browserProvider! + const provider = server.provider const commandsCode = commands .filter(command => !command.startsWith('__vitest')) @@ -75,7 +77,7 @@ export const server = { platform: ${JSON.stringify(process.platform)}, version: ${JSON.stringify(process.version)}, provider: ${JSON.stringify(provider.name)}, - browser: ${JSON.stringify(project.config.browser.name)}, + browser: ${JSON.stringify(server.project.config.browser.name)}, commands: { ${commandsCode} } diff --git a/packages/vitest/src/node/pools/browser.ts b/packages/browser/src/node/pool.ts similarity index 82% rename from packages/vitest/src/node/pools/browser.ts rename to packages/browser/src/node/pool.ts index a4503d04505d..8a83cff82453 100644 --- a/packages/vitest/src/node/pools/browser.ts +++ b/packages/browser/src/node/pool.ts @@ -1,12 +1,8 @@ import * as nodeos from 'node:os' import crypto from 'node:crypto' -import { createDefer } from '@vitest/utils' import { relative } from 'pathe' -import type { Vitest } from '../core' -import type { ProcessPool } from '../pool' -import type { WorkspaceProject } from '../workspace' -import type { BrowserProvider } from '../../types/browser' -import { createDebugger } from '../../utils/debugger' +import type { BrowserProvider, ProcessPool, Vitest, WorkspaceProject } from 'vitest/node' +import { createDebugger } from 'vitest/node' const debug = createDebugger('vitest:browser:pool') @@ -18,20 +14,13 @@ export function createBrowserPool(ctx: Vitest): ProcessPool { project: WorkspaceProject, files: string[], ) => { - const defer = createDefer() - project.browserState.set(contextId, { - files, - resolve: () => { - defer.resolve() - project.browserState.delete(contextId) - }, - reject: defer.reject, - }) - return await defer + const context = project.browser!.state.createAsyncContext(contextId, files) + return await context } const runTests = async (project: WorkspaceProject, files: string[]) => { ctx.state.clearFiles(project, files) + const browser = project.browser! // const mocker = project.browserMocker // mocker.mocks.forEach((_, id) => { // mocker.invalidateModuleById(id) @@ -45,10 +34,10 @@ export function createBrowserPool(ctx: Vitest): ProcessPool { // isCancelled = true // }) - const provider = project.browserProvider! + const provider = browser.provider providers.add(provider) - const resolvedUrls = project.browser?.resolvedUrls + const resolvedUrls = browser.vite.resolvedUrls const origin = resolvedUrls?.local[0] ?? resolvedUrls?.network[0] if (!origin) { @@ -76,7 +65,7 @@ export function createBrowserPool(ctx: Vitest): ProcessPool { threadsCount, ) - const orchestrators = [...project.browserRpc.orchestrators.entries()] + const orchestrators = [...browser.state.orchestrators.entries()] const promises: Promise[] = [] @@ -133,7 +122,7 @@ export function createBrowserPool(ctx: Vitest): ProcessPool { function getThreadsCount(project: WorkspaceProject) { const config = project.config.browser - if (!config.headless || !project.browserProvider!.supportsParallelism) { + if (!config.headless || !project.browser!.provider.supportsParallelism) { return 1 } diff --git a/packages/browser/src/node/providers/preview.ts b/packages/browser/src/node/providers/preview.ts index df2ebe522905..9b412ca961a5 100644 --- a/packages/browser/src/node/providers/preview.ts +++ b/packages/browser/src/node/providers/preview.ts @@ -34,10 +34,10 @@ export class PreviewBrowserProvider implements BrowserProvider { if (!this.ctx.browser) { throw new Error('Browser is not initialized') } - const options = this.ctx.browser.config.server + const options = this.ctx.browser.vite.config.server const _open = options.open options.open = url - this.ctx.browser.openBrowser() + this.ctx.browser.vite.openBrowser() options.open = _open } diff --git a/packages/browser/src/node/resolveMock.ts b/packages/browser/src/node/resolveMock.ts new file mode 100644 index 000000000000..ff8b148a4f30 --- /dev/null +++ b/packages/browser/src/node/resolveMock.ts @@ -0,0 +1,128 @@ +import { existsSync, readdirSync } from 'node:fs' +import { builtinModules } from 'node:module' +import { basename, dirname, extname, isAbsolute, join, resolve } from 'pathe' +import type { PartialResolvedId } from 'rollup' +import type { ResolvedConfig } from 'vitest' +import type { WorkspaceProject } from 'vitest/node' + +export async function resolveMock( + project: WorkspaceProject, + rawId: string, + importer: string, + hasFactory: boolean, +) { + const { id, fsPath, external } = await resolveId(project, rawId, importer) + + if (hasFactory) { + return { type: 'factory' as const, resolvedId: id } + } + + const mockPath = resolveMockPath(project.config.root, fsPath, external) + + return { + type: mockPath === null ? ('automock' as const) : ('redirect' as const), + mockPath, + resolvedId: id, + } +} + +async function resolveId(project: WorkspaceProject, rawId: string, importer: string) { + const resolved = await project.browser!.vite.pluginContainer.resolveId( + rawId, + importer, + { + ssr: false, + }, + ) + return resolveModule(project, rawId, resolved) +} + +async function resolveModule(project: WorkspaceProject, rawId: string, resolved: PartialResolvedId | null) { + const id = resolved?.id || rawId + const external + = !isAbsolute(id) || isModuleDirectory(project.config, id) ? rawId : null + return { + id, + fsPath: cleanUrl(id), + external, + } +} + +function isModuleDirectory(config: ResolvedConfig, path: string) { + const moduleDirectories = config.server.deps?.moduleDirectories || [ + '/node_modules/', + ] + return moduleDirectories.some((dir: string) => path.includes(dir)) +} + +function resolveMockPath(root: string, mockPath: string, external: string | null) { + const path = external || mockPath + + // it's a node_module alias + // all mocks should be inside /__mocks__ + if (external || isNodeBuiltin(mockPath) || !existsSync(mockPath)) { + const mockDirname = dirname(path) // for nested mocks: @vueuse/integration/useJwt + const mockFolder = join( + root, + '__mocks__', + mockDirname, + ) + + if (!existsSync(mockFolder)) { + return null + } + + const files = readdirSync(mockFolder) + const baseOriginal = basename(path) + + for (const file of files) { + const baseFile = basename(file, extname(file)) + if (baseFile === baseOriginal) { + return resolve(mockFolder, file) + } + } + + return null + } + + const dir = dirname(path) + const baseId = basename(path) + const fullPath = resolve(dir, '__mocks__', baseId) + return existsSync(fullPath) ? fullPath : null +} + +const prefixedBuiltins = new Set(['node:test']) + +const builtins = new Set([ + ...builtinModules, + 'assert/strict', + 'diagnostics_channel', + 'dns/promises', + 'fs/promises', + 'path/posix', + 'path/win32', + 'readline/promises', + 'stream/consumers', + 'stream/promises', + 'stream/web', + 'timers/promises', + 'util/types', + 'wasi', +]) + +const NODE_BUILTIN_NAMESPACE = 'node:' +export function isNodeBuiltin(id: string): boolean { + if (prefixedBuiltins.has(id)) { + return true + } + return builtins.has( + id.startsWith(NODE_BUILTIN_NAMESPACE) + ? id.slice(NODE_BUILTIN_NAMESPACE.length) + : id, + ) +} + +const postfixRE = /[?#].*$/ +export function cleanUrl(url: string): string { + return url.replace(postfixRE, '') +} diff --git a/packages/vitest/src/api/browser.ts b/packages/browser/src/node/rpc.ts similarity index 71% rename from packages/vitest/src/api/browser.ts rename to packages/browser/src/node/rpc.ts index 75026733b844..0a69eb389eec 100644 --- a/packages/vitest/src/api/browser.ts +++ b/packages/browser/src/node/rpc.ts @@ -1,33 +1,30 @@ import { existsSync, promises as fs } from 'node:fs' - import { dirname } from 'pathe' import { createBirpc } from 'birpc' import { parse, stringify } from 'flatted' import type { WebSocket } from 'ws' import { WebSocketServer } from 'ws' -import { isFileServingAllowed, parseAst } from 'vite' -import type { ViteDevServer } from 'vite' -import type { EncodedSourceMap } from '@ampproject/remapping' -import remapping from '@ampproject/remapping' -import { BROWSER_API_PATH } from '../constants' -import { stringifyReplace } from '../utils' -import type { WorkspaceProject } from '../node/workspace' -import { createDebugger } from '../utils/debugger' -import { automockModule } from '../node/automockBrowser' -import type { BrowserCommandContext } from '../types/browser' +import { isFileServingAllowed } from 'vite' +import type { BrowserCommandContext } from 'vitest/node' +import { createDebugger } from 'vitest/node' import type { WebSocketBrowserEvents, WebSocketBrowserHandlers } from './types' +import type { BrowserServer } from './server' +import { resolveMock } from './resolveMock' const debug = createDebugger('vitest:browser:api') +const BROWSER_API_PATH = '/__vitest_browser_api__' + export function setupBrowserRpc( - project: WorkspaceProject, - server: ViteDevServer, + server: BrowserServer, ) { + const project = server.project + const vite = server.vite const ctx = project.ctx const wss = new WebSocketServer({ noServer: true }) - server.httpServer?.on('upgrade', (request, socket, head) => { + vite.httpServer?.on('upgrade', (request, socket, head) => { if (!request.url) { return } @@ -43,9 +40,9 @@ export function setupBrowserRpc( wss.handleUpgrade(request, socket, head, (ws) => { wss.emit('connection', ws, request) - const rpc = setupClient(sessionId, ws) - const rpcs = project.browserRpc - const clients = type === 'tester' ? rpcs.testers : rpcs.orchestrators + const rpc = setupClient(ws) + const state = server.state + const clients = type === 'tester' ? state.testers : state.orchestrators clients.set(sessionId, rpc) debug?.('[%s] Browser API connected to %s', sessionId, type) @@ -58,14 +55,14 @@ export function setupBrowserRpc( }) function checkFileAccess(path: string) { - if (!isFileServingAllowed(path, server)) { + if (!isFileServingAllowed(path, vite)) { throw new Error( `Access denied to "${path}". See Vite config documentation for "server.fs": https://vitejs.dev/config/server-options.html#server-fs-strict.`, ) } } - function setupClient(sessionId: string, ws: WebSocket) { + function setupClient(ws: WebSocket) { const rpc = createBirpc( { async onUnhandledError(error, type) { @@ -113,8 +110,8 @@ export function setupBrowserRpc( } return fs.unlink(id) }, - async getBrowserFileSourceMap(id) { - const mod = project.browser?.moduleGraph.getModuleById(id) + getBrowserFileSourceMap(id) { + const mod = server.vite.moduleGraph.getModuleById(id) return mod?.transformResult?.map }, onCancel(reason) { @@ -138,7 +135,7 @@ export function setupBrowserRpc( }, async triggerCommand(contextId, command, testPath, payload) { debug?.('[%s] Triggering command "%s"', contextId, command) - const provider = project.browserProvider + const provider = server.provider if (!provider) { throw new Error('Commands are only available for browser tests.') } @@ -171,38 +168,14 @@ export function setupBrowserRpc( }, finishBrowserTests(contextId: string) { debug?.('[%s] Finishing browser tests for context', contextId) - return project.browserState.get(contextId)?.resolve() - }, - getProvidedContext() { - return 'ctx' in project ? project.getProvidedContext() : ({} as any) - }, - // TODO: cache this automock result - async automock(id) { - const result = await project.browser!.transformRequest(id) - if (!result) { - throw new Error(`Module "${id}" not found.`) - } - const ms = automockModule(result.code, parseAst) - const code = ms.toString() - const sourcemap = ms.generateMap({ hires: 'boundary', source: id }) - const combinedMap - = result.map && result.map.mappings - ? remapping( - [ - { ...sourcemap, version: 3 }, - result.map as EncodedSourceMap, - ], - () => null, - ) - : sourcemap - return `${code}\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,${Buffer.from(JSON.stringify(combinedMap)).toString('base64')}` + return server.state.getContext(contextId)?.resolve() }, resolveMock(rawId, importer, hasFactory) { - return project.browserMocker.resolveMock(rawId, importer, hasFactory) + return resolveMock(project, rawId, importer, hasFactory) }, invalidate(ids) { ids.forEach((id) => { - const moduleGraph = project.browser!.moduleGraph + const moduleGraph = server.vite.moduleGraph const module = moduleGraph.getModuleById(id) if (module) { moduleGraph.invalidateModule(module, new Set(), Date.now(), true) @@ -227,3 +200,35 @@ export function setupBrowserRpc( return rpc } } +// 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. + return Object.getOwnPropertyNames(value).reduce( + (clone, prop) => ({ + ...clone, + [prop]: value[prop], + }), + {}, + ) +} + +/** + * Replacer function for serialization methods such as JS.stringify() or + * flatted.stringify(). + */ +export function stringifyReplace(key: string, value: any) { + if (value instanceof Error) { + const cloned = cloneByOwnProperties(value) + return { + name: value.name, + message: value.message, + stack: value.stack, + ...cloned, + } + } + else { + return value + } +} diff --git a/packages/browser/src/node/server.ts b/packages/browser/src/node/server.ts new file mode 100644 index 000000000000..f76f124d28c4 --- /dev/null +++ b/packages/browser/src/node/server.ts @@ -0,0 +1,153 @@ +import { readFile } from 'node:fs/promises' +import { fileURLToPath } from 'node:url' +import type { + BrowserProvider, + BrowserScript, + BrowserServer as IBrowserServer, + Vite, + WorkspaceProject, +} from 'vitest/node' +import { join, resolve } from 'pathe' +import { slash } from '@vitest/utils' +import type { ResolvedConfig } from 'vitest' +import { BrowserServerState } from './state' +import { getBrowserProvider } from './utils' + +export class BrowserServer implements IBrowserServer { + public faviconUrl: string + public prefixTesterUrl: string + + public orchestratorScripts: string | undefined + public testerScripts: string | undefined + + public manifest: Promise | Vite.Manifest + public testerHtml: Promise | string + public orchestratorHtml: Promise | string + public injectorJs: Promise | string + public stateJs: Promise | string + + public state: BrowserServerState + public provider!: BrowserProvider + + public vite!: Vite.ViteDevServer + + constructor( + public project: WorkspaceProject, + public base: string, + ) { + 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` + + this.manifest = (async () => { + return JSON.parse( + await readFile(`${distRoot}/client/.vite/manifest.json`, 'utf8'), + ) + })().then(manifest => (this.manifest = manifest)) + + this.testerHtml = readFile( + resolve(distRoot, 'client/tester/tester.html'), + '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')) + .then(html => (this.orchestratorHtml = html)) + this.injectorJs = readFile( + resolve(distRoot, 'client/esm-client-injector.js'), + 'utf8', + ).then(js => (this.injectorJs = js)) + this.stateJs = readFile( + resolve(distRoot, 'state.js'), + 'utf-8', + ).then(js => (this.stateJs = js)) + } + + setServer(server: Vite.ViteDevServer) { + this.vite = server + } + + getSerializableConfig() { + const config = wrapConfig(this.project.getSerializableConfig()) + config.env ??= {} + config.env.VITEST_BROWSER_DEBUG = process.env.VITEST_BROWSER_DEBUG || '' + return config + } + + resolveTesterUrl(pathname: string) { + const [contextId, testFile] = pathname + .slice(this.prefixTesterUrl.length) + .split('/') + const decodedTestFile = decodeURIComponent(testFile) + return { contextId, testFile: decodedTestFile } + } + + 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) => { + 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 `` + }, + ) + return (await Promise.all(promises)).join('\n') + } + + async initBrowserProvider() { + if (this.provider) { + return + } + const Provider = await getBrowserProvider(this.project.config.browser, this.project) + this.provider = new Provider() + const browser = this.project.config.browser.name + if (!browser) { + throw new Error( + `[${this.project.getName()}] 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.getName()}] 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, { + browser, + options: providerOptions, + }) + } + + async close() { + await this.vite.close() + } +} + +function wrapConfig(config: ResolvedConfig): ResolvedConfig { + return { + ...config, + // workaround RegExp serialization + testNamePattern: config.testNamePattern + ? (config.testNamePattern.toString() as any as RegExp) + : undefined, + } +} diff --git a/packages/browser/src/node/serverOrchestrator.ts b/packages/browser/src/node/serverOrchestrator.ts new file mode 100644 index 000000000000..bfa20dc15011 --- /dev/null +++ b/packages/browser/src/node/serverOrchestrator.ts @@ -0,0 +1,76 @@ +import type { IncomingMessage, ServerResponse } from 'node:http' +import type { BrowserServer } from './server' +import { replacer } from './utils' + +export async function resolveOrchestrator( + server: BrowserServer, + 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) { + const contexts = [...server.state.orchestrators.keys()] + contextId = contexts[contexts.length - 1] ?? 'none' + } + + const files = server.state.getContext(contextId!)?.files ?? [] + + const config = server.getSerializableConfig() + const injectorJs = typeof server.injectorJs === 'string' + ? server.injectorJs + : await server.injectorJs + + const injector = replacer(injectorJs, { + __VITEST_PROVIDER__: JSON.stringify(server.provider.name), + __VITEST_CONFIG__: JSON.stringify(config), + __VITEST_VITE_CONFIG__: JSON.stringify({ + root: server.vite.config.root, + }), + __VITEST_FILES__: JSON.stringify(files), + __VITEST_TYPE__: '"orchestrator"', + __VITEST_CONTEXT_ID__: JSON.stringify(contextId), + __VITEST_PROVIDED_CONTEXT__: '{}', + }) + + // 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( + project.config.browser.orchestratorScripts, + ) + } + + let baseHtml = typeof server.orchestratorHtml === 'string' + ? server.orchestratorHtml + : await server.orchestratorHtml + + // if UI is enabled, use UI HTML and inject the orchestrator script + if (project.config.browser.ui) { + const manifestContent = server.manifest instanceof Promise + ? await server.manifest + : server.manifest + const jsEntry = manifestContent['orchestrator.html'].file + const base = server.vite.config.base || '/' + baseHtml = baseHtml + .replaceAll('./assets/', `${base}__vitest__/assets/`) + .replace( + '', + [ + '', + '{__VITEST_SCRIPTS__}', + ``, + ].join('\n'), + ) + } + + return replacer(baseHtml, { + __VITEST_FAVICON__: server.faviconUrl, + __VITEST_TITLE__: 'Vitest Browser Runner', + __VITEST_SCRIPTS__: server.orchestratorScripts, + __VITEST_INJECTOR__: injector, + __VITEST_CONTEXT_ID__: JSON.stringify(contextId), + }) +} diff --git a/packages/browser/src/node/serverTester.ts b/packages/browser/src/node/serverTester.ts new file mode 100644 index 000000000000..9b1ba337a32f --- /dev/null +++ b/packages/browser/src/node/serverTester.ts @@ -0,0 +1,80 @@ +import type { IncomingMessage, ServerResponse } from 'node:http' +import { stringify } from 'flatted' +import { replacer } from './utils' +import type { BrowserServer } from './server' + +export async function resolveTester( + server: BrowserServer, + url: URL, + res: ServerResponse, +): Promise { + const csp = res.getHeader('Content-Security-Policy') + if (typeof csp === 'string') { + // add frame-ancestors to allow the iframe to be loaded by Vitest, + // but keep the rest of the CSP + res.setHeader( + 'Content-Security-Policy', + csp.replace(/frame-ancestors [^;]+/, 'frame-ancestors *'), + ) + } + + const { contextId, testFile } = server.resolveTesterUrl(url.pathname) + const project = server.project + const state = server.state + 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 + = testFile === '__vitest_all__' + || !testFiles.includes(testFile) + ? '__vitest_browser_runner__.files' + : JSON.stringify([testFile]) + const iframeId = JSON.stringify(testFile) + const files = state.getContext(contextId)?.files ?? [] + + const injectorJs = typeof server.injectorJs === 'string' + ? server.injectorJs + : await server.injectorJs + + const config = server.getSerializableConfig() + + const injector = replacer(injectorJs, { + __VITEST_PROVIDER__: JSON.stringify(server.provider.name), + __VITEST_CONFIG__: JSON.stringify(config), + __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_PROVIDED_CONTEXT__: JSON.stringify(stringify(project.getProvidedContext())), + }) + + if (!server.testerScripts) { + const testerScripts = await server.formatScripts( + project.config.browser.testerScripts, + ) + const clientScript = `` + const stateJs = typeof server.stateJs === 'string' + ? server.stateJs + : await server.stateJs + const stateScript = `` + server.testerScripts = `${stateScript}${clientScript}${testerScripts}` + } + + const testerHtml = typeof server.testerHtml === 'string' + ? server.testerHtml + : await server.testerHtml + + return replacer(testerHtml, { + __VITEST_FAVICON__: server.faviconUrl, + __VITEST_TITLE__: 'Vitest Browser Tester', + __VITEST_SCRIPTS__: server.testerScripts, + __VITEST_INJECTOR__: injector, + __VITEST_APPEND__: + ``, + }) +} diff --git a/packages/browser/src/node/state.ts b/packages/browser/src/node/state.ts new file mode 100644 index 000000000000..cd127623f165 --- /dev/null +++ b/packages/browser/src/node/state.ts @@ -0,0 +1,27 @@ +import { createDefer } from '@vitest/utils' +import type { BrowserServerStateContext, BrowserServerState as IBrowserServerState } from 'vitest/node' +import type { WebSocketBrowserRPC } from './types' + +export class BrowserServerState implements IBrowserServerState { + public orchestrators = new Map() + public testers = new Map() + + private contexts = new Map() + + getContext(contextId: string) { + return this.contexts.get(contextId) + } + + createAsyncContext(contextId: string, files: string[]): Promise { + const defer = createDefer() + this.contexts.set(contextId, { + files, + resolve: () => { + defer.resolve() + this.contexts.delete(contextId) + }, + reject: defer.reject, + }) + return defer + } +} diff --git a/packages/browser/src/node/types.ts b/packages/browser/src/node/types.ts new file mode 100644 index 000000000000..575e00cf3b4e --- /dev/null +++ b/packages/browser/src/node/types.ts @@ -0,0 +1,77 @@ +import type { BirpcReturn } from 'birpc' +import type { AfterSuiteRunMeta, CancelReason, File, Reporter, SnapshotResult, TaskResultPack, UserConsoleLog } from 'vitest' + +export interface WebSocketBrowserHandlers { + resolveSnapshotPath: (testPath: string) => string + resolveSnapshotRawPath: (testPath: string, rawPath: string) => string + onUnhandledError: (error: unknown, type: string) => Promise + onCollected: (files?: File[]) => Promise + onTaskUpdate: (packs: TaskResultPack[]) => void + onAfterSuiteRun: (meta: AfterSuiteRunMeta) => void + onCancel: (reason: CancelReason) => void + getCountOfFailedTests: () => number + readSnapshotFile: (id: string) => Promise + saveSnapshotFile: (id: string, content: string) => Promise + removeSnapshotFile: (id: string) => Promise + sendLog: (log: UserConsoleLog) => void + finishBrowserTests: (contextId: string) => void + snapshotSaved: (snapshot: SnapshotResult) => void + debug: (...args: string[]) => void + resolveId: ( + id: string, + importer?: string + ) => Promise<{ id: string } | null> + triggerCommand: ( + contextId: string, + command: string, + testPath: string | undefined, + payload: unknown[] + ) => Promise + resolveMock: ( + id: string, + importer: string, + hasFactory: boolean + ) => Promise<{ + type: 'factory' | 'redirect' | 'automock' + mockPath?: string | null + resolvedId: string + }> + invalidate: (ids: string[]) => void + getBrowserFileSourceMap: ( + id: string + ) => SourceMap | null | { mappings: '' } | undefined +} + +export interface WebSocketEvents + extends Pick< + Reporter, + | 'onCollected' + | 'onFinished' + | 'onTaskUpdate' + | 'onUserConsoleLog' + | 'onPathsCollected' + | 'onSpecsCollected' + > { + onFinishedReportCoverage: () => void +} + +export interface WebSocketBrowserEvents { + onCancel: (reason: CancelReason) => void + createTesters: (files: string[]) => Promise +} + +export type WebSocketBrowserRPC = BirpcReturn< + WebSocketBrowserEvents, + WebSocketBrowserHandlers +> + +interface SourceMap { + file: string + mappings: string + names: string[] + sources: string[] + sourcesContent?: string[] + version: number + toString: () => string + toUrl: () => string +} diff --git a/packages/vitest/src/integrations/browser.ts b/packages/browser/src/node/utils.ts similarity index 70% rename from packages/vitest/src/integrations/browser.ts rename to packages/browser/src/node/utils.ts index 86713305844c..ce99c80e448f 100644 --- a/packages/vitest/src/integrations/browser.ts +++ b/packages/browser/src/node/utils.ts @@ -1,8 +1,8 @@ -import type { WorkspaceProject } from '../node/workspace' -import type { - BrowserProviderModule, - ResolvedBrowserOptions, -} from '../types/browser' +import type { BrowserProviderModule, ResolvedBrowserOptions, WorkspaceProject } from 'vitest/node' + +export function replacer(code: string, values: Record) { + return code.replace(/\{\s*(\w+)\s*\}/g, (_, key) => values[key] ?? '') +} const builtinProviders = ['webdriverio', 'playwright', 'preview'] @@ -11,13 +11,7 @@ export async function getBrowserProvider( project: WorkspaceProject, ): Promise { if (options.provider == null || builtinProviders.includes(options.provider)) { - await project.ctx.packageInstaller.ensureInstalled( - '@vitest/browser', - project.config.root, - ) - const providers = (await project.runner.executeId( - '@vitest/browser/providers', - )) as typeof import('@vitest/browser/providers') + const providers = await import('./providers') const provider = (options.provider || 'preview') as | 'webdriverio' | 'playwright' diff --git a/packages/vite-node/src/server.ts b/packages/vite-node/src/server.ts index aea222661763..09256d2b1175 100644 --- a/packages/vite-node/src/server.ts +++ b/packages/vite-node/src/server.ts @@ -1,7 +1,7 @@ import { performance } from 'node:perf_hooks' import { existsSync } from 'node:fs' import assert from 'node:assert' -import { isAbsolute, join, normalize, relative, resolve } from 'pathe' +import { join, normalize, relative, resolve } from 'pathe' import type { TransformResult, ViteDevServer } from 'vite' import createDebug from 'debug' import type { @@ -13,7 +13,6 @@ import type { } from './types' import { shouldExternalize } from './externalize' import { - cleanUrl, normalizeModuleId, toArray, toFilePath, @@ -179,24 +178,6 @@ export class ViteNodeServer { }) } - async resolveModule(rawId: string, resolved: ViteNodeResolveId | null) { - const id = resolved?.id || rawId - const external - = !isAbsolute(id) || this.isModuleDirectory(id) ? rawId : null - return { - id, - fsPath: cleanUrl(id), - external, - } - } - - private isModuleDirectory(path: string) { - const moduleDirectories = this.options.deps?.moduleDirectories || [ - '/node_modules/', - ] - return moduleDirectories.some((dir: string) => path.includes(dir)) - } - getSourceMap(source: string) { const fetchResult = this.fetchCache.get(source)?.result if (fetchResult?.map) { diff --git a/packages/vitest/src/api/setup.ts b/packages/vitest/src/api/setup.ts index 026c4edaad76..cf9e77851c9f 100644 --- a/packages/vitest/src/api/setup.ts +++ b/packages/vitest/src/api/setup.ts @@ -89,7 +89,7 @@ export function setup(ctx: Vitest, _server?: ViteDevServer) { async getTransformResult(projectName: string, id, browser = false) { const project = ctx.getProjectByName(projectName) const result: TransformResultWithSource | null | undefined = browser - ? await project.browser!.transformRequest(id) + ? await project.browser!.vite.transformRequest(id) : await project.vitenode.transformRequest(id) if (result) { try { diff --git a/packages/vitest/src/api/types.ts b/packages/vitest/src/api/types.ts index 0a8d28e9811b..749225e99103 100644 --- a/packages/vitest/src/api/types.ts +++ b/packages/vitest/src/api/types.ts @@ -1,17 +1,11 @@ import type { TransformResult } from 'vite' -import type { CancelReason } from '@vitest/runner' import type { BirpcReturn } from 'birpc' -import type { ViteNodeResolveId } from 'vite-node' import type { - AfterSuiteRunMeta, File, ModuleGraphData, - ProvidedContext, Reporter, ResolvedConfig, - SnapshotResult, TaskResultPack, - UserConsoleLog, } from '../types' export interface TransformResultWithSource extends TransformResult { @@ -42,49 +36,6 @@ export interface WebSocketHandlers { getUnhandledErrors: () => unknown[] } -export interface WebSocketBrowserHandlers { - resolveSnapshotPath: (testPath: string) => string - resolveSnapshotRawPath: (testPath: string, rawPath: string) => string - onUnhandledError: (error: unknown, type: string) => Promise - onCollected: (files?: File[]) => Promise - onTaskUpdate: (packs: TaskResultPack[]) => void - onAfterSuiteRun: (meta: AfterSuiteRunMeta) => void - onCancel: (reason: CancelReason) => void - getCountOfFailedTests: () => number - readSnapshotFile: (id: string) => Promise - saveSnapshotFile: (id: string, content: string) => Promise - removeSnapshotFile: (id: string) => Promise - sendLog: (log: UserConsoleLog) => void - finishBrowserTests: (contextId: string) => void - snapshotSaved: (snapshot: SnapshotResult) => void - debug: (...args: string[]) => void - resolveId: ( - id: string, - importer?: string - ) => Promise - triggerCommand: ( - contextId: string, - command: string, - testPath: string | undefined, - payload: unknown[] - ) => Promise - resolveMock: ( - id: string, - importer: string, - hasFactory: boolean - ) => Promise<{ - type: 'factory' | 'redirect' | 'automock' - mockPath?: string | null - resolvedId: string - }> - automock: (id: string) => Promise - invalidate: (ids: string[]) => void - getBrowserFileSourceMap: ( - id: string - ) => Promise - getProvidedContext: () => ProvidedContext -} - export interface WebSocketEvents extends Pick< Reporter, @@ -98,14 +49,4 @@ export interface WebSocketEvents onFinishedReportCoverage: () => void } -export interface WebSocketBrowserEvents { - onCancel: (reason: CancelReason) => void - startMocking: (id: string) => Promise - createTesters: (files: string[]) => Promise -} - export type WebSocketRPC = BirpcReturn -export type WebSocketBrowserRPC = BirpcReturn< - WebSocketBrowserEvents, - WebSocketBrowserHandlers -> diff --git a/packages/vitest/src/config.ts b/packages/vitest/src/config.ts index 5e382f7622c4..6b80fd38df45 100644 --- a/packages/vitest/src/config.ts +++ b/packages/vitest/src/config.ts @@ -7,6 +7,7 @@ export interface UserWorkspaceConfig extends ViteUserConfig { // will import vitest declare test in module 'vite' export { + defaultBrowserPort, configDefaults, defaultInclude, defaultExclude, diff --git a/packages/vitest/src/constants.ts b/packages/vitest/src/constants.ts index a1943a457300..066c7b8c143a 100644 --- a/packages/vitest/src/constants.ts +++ b/packages/vitest/src/constants.ts @@ -6,7 +6,6 @@ export const defaultInspectPort = 9229 export const EXIT_CODE_RESTART = 43 export const API_PATH = '/__vitest_api__' -export const BROWSER_API_PATH = '/__vitest_browser_api__' export const extraInlineDeps = [ /^(?!.*node_modules).*\.mjs$/, diff --git a/packages/vitest/src/defaults.ts b/packages/vitest/src/defaults.ts index ca5e0da982e7..4f35202d56a0 100644 --- a/packages/vitest/src/defaults.ts +++ b/packages/vitest/src/defaults.ts @@ -7,6 +7,8 @@ import type { } from './types' import { isCI } from './utils/env' +export { defaultBrowserPort } from './constants' + export const defaultInclude = ['**/*.{test,spec}.?(c|m)[jt]s?(x)'] export const defaultExclude = [ '**/node_modules/**', diff --git a/packages/vitest/src/integrations/browser/mocker.ts b/packages/vitest/src/integrations/browser/mocker.ts deleted file mode 100644 index c904820e93ac..000000000000 --- a/packages/vitest/src/integrations/browser/mocker.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { existsSync, readdirSync } from 'node:fs' -import { basename, dirname, extname, join, resolve } from 'pathe' -import { isNodeBuiltin } from 'vite-node/utils' -import type { WorkspaceProject } from '../../node/workspace' - -export class VitestBrowserServerMocker { - // string means it will read from __mocks__ folder - // undefined means there is a factory mock that will be called on the server - // null means it should be auto mocked - public mocks = new Map< - string, - { sessionId: string; mock: string | null | undefined } - >() - - // private because the typecheck fails on build if it's exposed - // due to a self reference - #project: WorkspaceProject - - constructor(project: WorkspaceProject) { - this.#project = project - } - - public async resolveMock( - rawId: string, - importer: string, - hasFactory: boolean, - ) { - const { id, fsPath, external } = await this.resolveId(rawId, importer) - - if (hasFactory) { - return { type: 'factory' as const, resolvedId: id } - } - - const mockPath = this.resolveMockPath(fsPath, external) - - return { - type: mockPath === null ? ('automock' as const) : ('redirect' as const), - mockPath, - resolvedId: id, - } - } - - private async resolveId(rawId: string, importer: string) { - const resolved = await this.#project.browser!.pluginContainer.resolveId( - rawId, - importer, - { - ssr: false, - }, - ) - return this.#project.vitenode.resolveModule(rawId, resolved) - } - - public resolveMockPath(mockPath: string, external: string | null) { - const path = external || mockPath - - // it's a node_module alias - // all mocks should be inside /__mocks__ - if (external || isNodeBuiltin(mockPath) || !existsSync(mockPath)) { - const mockDirname = dirname(path) // for nested mocks: @vueuse/integration/useJwt - const mockFolder = join( - this.#project.config.root, - '__mocks__', - mockDirname, - ) - - if (!existsSync(mockFolder)) { - return null - } - - const files = readdirSync(mockFolder) - const baseOriginal = basename(path) - - for (const file of files) { - const baseFile = basename(file, extname(file)) - if (baseFile === baseOriginal) { - return resolve(mockFolder, file) - } - } - - return null - } - - const dir = dirname(path) - const baseId = basename(path) - const fullPath = resolve(dir, '__mocks__', baseId) - return existsSync(fullPath) ? fullPath : null - } -} diff --git a/packages/vitest/src/integrations/browser/server.ts b/packages/vitest/src/integrations/browser/server.ts deleted file mode 100644 index e8cdc9e29e8c..000000000000 --- a/packages/vitest/src/integrations/browser/server.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { createServer } from 'vite' -import { defaultBrowserPort } from '../../constants' -import { resolveApiServerConfig } from '../../node/config' -import { CoverageTransform } from '../../node/plugins/coverageTransform' -import type { WorkspaceProject } from '../../node/workspace' -import { MocksPlugin } from '../../node/plugins/mocks' -import { resolveFsAllow } from '../../node/plugins/utils' -import { setupBrowserRpc } from '../../api/browser' -import { setup as setupUiRpc } from '../../api/setup' - -export async function createBrowserServer( - project: WorkspaceProject, - configFile: string | undefined, -) { - const root = project.config.root - - await project.ctx.packageInstaller.ensureInstalled('@vitest/browser', root) - - const configPath = typeof configFile === 'string' ? configFile : false - - const server = await createServer({ - ...project.options, // spread project config inlined in root workspace config - base: '/', - logLevel: 'error', - mode: project.config.mode, - configFile: configPath, - // watch is handled by Vitest - server: { - hmr: false, - watch: null, - preTransformRequests: false, - }, - plugins: [ - ...(project.options?.plugins || []), - MocksPlugin(), - (await import('@vitest/browser')).default(project, '/'), - CoverageTransform(project.ctx), - { - enforce: 'post', - name: 'vitest:browser:config', - async config(config) { - const server = resolveApiServerConfig( - config.test?.browser || {}, - defaultBrowserPort, - ) || { - port: defaultBrowserPort, - } - - // browser never runs in middleware mode - server.middlewareMode = false - - config.server = { - ...config.server, - ...server, - open: false, - } - config.server.fs ??= {} - config.server.fs.allow = config.server.fs.allow || [] - config.server.fs.allow.push( - ...resolveFsAllow( - project.ctx.config.root, - project.ctx.server.config.configFile, - ), - ) - - return { - resolve: { - alias: config.test?.alias, - }, - } - }, - }, - ], - }) - - await server.listen() - - setupBrowserRpc(project, server) - if (project.config.browser.ui) { - setupUiRpc(project.ctx, server) - } - - return server -} diff --git a/packages/vitest/src/integrations/vi.ts b/packages/vitest/src/integrations/vi.ts index 4fe2eafb320d..c1ce2952dac4 100644 --- a/packages/vitest/src/integrations/vi.ts +++ b/packages/vitest/src/integrations/vi.ts @@ -372,10 +372,13 @@ export interface VitestUtils { } function createVitest(): VitestUtils { - const _mocker: VitestMocker + let _mockedDate: Date | null = null + let _config: null | ResolvedConfig = null + + function _mocker(): VitestMocker { + // @ts-expect-error injected by vite-nide + return typeof __vitest_mocker__ !== 'undefined' // @ts-expect-error injected by vite-nide - = typeof __vitest_mocker__ !== 'undefined' - // @ts-expect-error injected by vite-nide ? __vitest_mocker__ : new Proxy( {}, @@ -388,8 +391,7 @@ function createVitest(): VitestUtils { }, }, ) - let _mockedDate: Date | null = null - let _config: null | ResolvedConfig = null + } const workerState = getWorkerState() @@ -541,16 +543,16 @@ function createVitest(): VitestUtils { ) } const importer = getImporter('mock') - _mocker.queueMock( + _mocker().queueMock( path, importer, factory ? () => factory(() => - _mocker.importActual( + _mocker().importActual( path, importer, - _mocker.getMockContext().callstack, + _mocker().getMockContext().callstack, ), ) : undefined, @@ -564,7 +566,7 @@ function createVitest(): VitestUtils { `vi.unmock() expects a string path, but received a ${typeof path}`, ) } - _mocker.queueUnmock(path, getImporter('unmock')) + _mocker().queueUnmock(path, getImporter('unmock')) }, doMock(path: string | Promise, factory?: MockFactoryWithHelper) { @@ -574,16 +576,16 @@ function createVitest(): VitestUtils { ) } const importer = getImporter('doMock') - _mocker.queueMock( + _mocker().queueMock( path, importer, factory ? () => factory(() => - _mocker.importActual( + _mocker().importActual( path, importer, - _mocker.getMockContext().callstack, + _mocker().getMockContext().callstack, ), ) : undefined, @@ -597,19 +599,19 @@ function createVitest(): VitestUtils { `vi.doUnmock() expects a string path, but received a ${typeof path}`, ) } - _mocker.queueUnmock(path, getImporter('doUnmock')) + _mocker().queueUnmock(path, getImporter('doUnmock')) }, async importActual(path: string): Promise { - return _mocker.importActual( + return _mocker().importActual( path, getImporter('importActual'), - _mocker.getMockContext().callstack, + _mocker().getMockContext().callstack, ) }, async importMock(path: string): Promise> { - return _mocker.importMock(path, getImporter('importMock')) + return _mocker().importMock(path, getImporter('importMock')) }, // this is typed in the interface so it's not necessary to type it here diff --git a/packages/browser/src/node/automocker.ts b/packages/vitest/src/node/automock.ts similarity index 100% rename from packages/browser/src/node/automocker.ts rename to packages/vitest/src/node/automock.ts diff --git a/packages/vitest/src/node/automockBrowser.ts b/packages/vitest/src/node/automockBrowser.ts deleted file mode 100644 index 551f67e96883..000000000000 --- a/packages/vitest/src/node/automockBrowser.ts +++ /dev/null @@ -1,168 +0,0 @@ -import type { - Declaration, - ExportDefaultDeclaration, - ExportNamedDeclaration, - Expression, - Identifier, - Literal, - Pattern, - Positioned, - Program, -} from '@vitest/utils/ast' -import MagicString from 'magic-string' - -// TODO: better source map replacement -export function automockModule(code: string, parse: (code: string) => Program) { - const ast = parse(code) - - const m = new MagicString(code) - - const allSpecifiers: { name: string; alias?: string }[] = [] - let importIndex = 0 - for (const _node of ast.body) { - if (_node.type === 'ExportAllDeclaration') { - throw new Error( - `automocking files with \`export *\` is not supported in browser mode because it cannot be statically analysed`, - ) - } - - if (_node.type === 'ExportNamedDeclaration') { - const node = _node as Positioned - const declaration = node.declaration // export const name - - function traversePattern(expression: Pattern) { - // export const test = '1' - if (expression.type === 'Identifier') { - allSpecifiers.push({ name: expression.name }) - } - // export const [test, ...rest] = [1, 2, 3] - else if (expression.type === 'ArrayPattern') { - expression.elements.forEach((element) => { - if (!element) { - return - } - traversePattern(element) - }) - } - else if (expression.type === 'ObjectPattern') { - expression.properties.forEach((property) => { - // export const { ...rest } = {} - if (property.type === 'RestElement') { - traversePattern(property) - } - // export const { test, test2: alias } = {} - else if (property.type === 'Property') { - traversePattern(property.value) - } - else { - property satisfies never - } - }) - } - else if (expression.type === 'RestElement') { - traversePattern(expression.argument) - } - // const [name[1], name[2]] = [] - // cannot be used in export - else if (expression.type === 'AssignmentPattern') { - throw new Error( - `AssignmentPattern is not supported. Please open a new bug report.`, - ) - } - // const test = thing.func() - // cannot be used in export - else if (expression.type === 'MemberExpression') { - throw new Error( - `MemberExpression is not supported. Please open a new bug report.`, - ) - } - else { - expression satisfies never - } - } - - if (declaration) { - if (declaration.type === 'FunctionDeclaration') { - allSpecifiers.push({ name: declaration.id.name }) - } - else if (declaration.type === 'VariableDeclaration') { - declaration.declarations.forEach((declaration) => { - traversePattern(declaration.id) - }) - } - else if (declaration.type === 'ClassDeclaration') { - allSpecifiers.push({ name: declaration.id.name }) - } - else { - declaration satisfies never - } - m.remove(node.start, (declaration as Positioned).start) - } - - const specifiers = node.specifiers || [] - const source = node.source - - if (!source && specifiers.length) { - specifiers.forEach((specifier) => { - const exported = specifier.exported as Literal | Identifier - - allSpecifiers.push({ - alias: exported.type === 'Literal' ? exported.raw! : exported.name, - name: specifier.local.name, - }) - }) - m.remove(node.start, node.end) - } - else if (source && specifiers.length) { - const importNames: [string, string][] = [] - - specifiers.forEach((specifier) => { - const importedName = `__vitest_imported_${importIndex++}__` - const exported = specifier.exported as Literal | Identifier - importNames.push([specifier.local.name, importedName]) - allSpecifiers.push({ - name: importedName, - alias: exported.type === 'Literal' ? exported.raw! : exported.name, - }) - }) - - const importString = `import { ${importNames - .map(([name, alias]) => `${name} as ${alias}`) - .join(', ')} } from '${source.value}'` - - m.overwrite(node.start, node.end, importString) - } - } - if (_node.type === 'ExportDefaultDeclaration') { - const node = _node as Positioned - const declaration = node.declaration as Positioned - allSpecifiers.push({ name: '__vitest_default', alias: 'default' }) - m.overwrite(node.start, declaration.start, `const __vitest_default = `) - } - } - const moduleObject = ` -const __vitest_es_current_module__ = { - __esModule: true, - ${allSpecifiers.map(({ name }) => `["${name}"]: ${name},`).join('\n ')} -} -const __vitest_mocked_module__ = __vitest_mocker__.mockObject(__vitest_es_current_module__) -` - const assigning = allSpecifiers - .map(({ name }, index) => { - return `const __vitest_mocked_${index}__ = __vitest_mocked_module__["${name}"]` - }) - .join('\n') - - const redeclarations = allSpecifiers - .map(({ name, alias }, index) => { - return ` __vitest_mocked_${index}__ as ${alias || name},` - }) - .join('\n') - const specifiersExports = ` -export { -${redeclarations} -} -` - m.append(moduleObject + assigning + specifiersExports) - return m -} diff --git a/packages/vitest/src/node/core.ts b/packages/vitest/src/node/core.ts index b179a9d2cd90..dfad23e65356 100644 --- a/packages/vitest/src/node/core.ts +++ b/packages/vitest/src/node/core.ts @@ -870,8 +870,8 @@ export class Vitest { serverMods?.forEach(mod => server.moduleGraph.invalidateModule(mod)) if (browser) { - const browserMods = browser.moduleGraph.getModulesByFile(filepath) - browserMods?.forEach(mod => browser.moduleGraph.invalidateModule(mod)) + const browserMods = browser.vite.moduleGraph.getModulesByFile(filepath) + browserMods?.forEach(mod => browser.vite.moduleGraph.invalidateModule(mod)) } }) } @@ -1050,8 +1050,10 @@ export class Vitest { closePromises.push(...this._onClose.map(fn => fn())) return Promise.allSettled(closePromises).then((results) => { - results.filter(r => r.status === 'rejected').forEach((err) => { - this.logger.error('error during close', (err as PromiseRejectedResult).reason) + results.forEach((r) => { + if (r.status === 'rejected') { + this.logger.error('error during close', r.reason) + } }) this.logger.logUpdate.done() // restore terminal cursor }) diff --git a/packages/vitest/src/node/index.ts b/packages/vitest/src/node/index.ts index 731e1934a6a5..ad2640aab95d 100644 --- a/packages/vitest/src/node/index.ts +++ b/packages/vitest/src/node/index.ts @@ -10,6 +10,9 @@ export type { WorkspaceSpec, ProcessPool } from './pool' export { createMethodsRPC } from './pools/rpc' export { getFilePoolName } from './pool' export { VitestPackageInstaller } from './packageInstaller' +export { createDebugger } from '../utils/debugger' +export { resolveFsAllow } from './plugins/utils' +export { resolveApiServerConfig, resolveConfig } from './config' export { distDir, rootDir } from '../paths' @@ -22,13 +25,20 @@ export { BaseSequencer } from './sequencers/BaseSequencer' export type { BrowserProviderInitializationOptions, BrowserProvider, + BrowserProviderModule, + ResolvedBrowserOptions, BrowserProviderOptions, BrowserScript, BrowserCommand, BrowserCommandContext, + BrowserServer, + BrowserServerState, + BrowserServerStateContext, + BrowserOrchestrator, } from '../types/browser' export type { JsonOptions } from './reporters/json' export type { JUnitOptions } from './reporters/junit' export type { HTMLOptions } from './reporters/html' -export { isFileServingAllowed } from 'vite' +export { isFileServingAllowed, createServer } from 'vite' +export type * as Vite from 'vite' diff --git a/packages/vitest/src/node/logger.ts b/packages/vitest/src/node/logger.ts index a3c496b0056c..5c01e76d79d8 100644 --- a/packages/vitest/src/node/logger.ts +++ b/packages/vitest/src/node/logger.ts @@ -205,7 +205,7 @@ export class Logger { const name = project.getName() const output = project.isCore() ? '' : ` [${name}]` - const resolvedUrls = project.browser.resolvedUrls + const resolvedUrls = project.browser.vite.resolvedUrls const origin = resolvedUrls?.local[0] ?? resolvedUrls?.network[0] this.log( c.dim( diff --git a/packages/vitest/src/node/plugins/coverageTransform.ts b/packages/vitest/src/node/plugins/coverageTransform.ts index ac6a8f77ebd8..f537d9b069a7 100644 --- a/packages/vitest/src/node/plugins/coverageTransform.ts +++ b/packages/vitest/src/node/plugins/coverageTransform.ts @@ -3,7 +3,7 @@ import { normalizeRequestId } from 'vite-node/utils' import type { Vitest } from '../core' -export function CoverageTransform(ctx: Vitest): VitePlugin | null { +export function CoverageTransform(ctx: Vitest): VitePlugin { return { name: 'vitest:coverage-transform', transform(srcCode, id) { diff --git a/packages/vitest/src/node/plugins/index.ts b/packages/vitest/src/node/plugins/index.ts index 11aa752a0ede..48133fb4db53 100644 --- a/packages/vitest/src/node/plugins/index.ts +++ b/packages/vitest/src/node/plugins/index.ts @@ -15,7 +15,7 @@ import { defaultPort } from '../../constants' import { SsrReplacerPlugin } from './ssrReplacer' import { CSSEnablerPlugin } from './cssEnabler' import { CoverageTransform } from './coverageTransform' -import { MocksPlugin } from './mocks' +import { MocksPlugins } from './mocks' import { deleteDefineConfig, hijackVitePluginInject, @@ -247,7 +247,7 @@ export async function VitestPlugin( ...CSSEnablerPlugin(ctx), CoverageTransform(ctx), options.ui ? await UIPlugin() : null, - MocksPlugin(), + ...MocksPlugins(), VitestResolver(ctx), VitestOptimizer(), NormalizeURLPlugin(), diff --git a/packages/vitest/src/node/plugins/mocks.ts b/packages/vitest/src/node/plugins/mocks.ts index 4ac708b71236..267871d9915d 100644 --- a/packages/vitest/src/node/plugins/mocks.ts +++ b/packages/vitest/src/node/plugins/mocks.ts @@ -1,16 +1,33 @@ import type { Plugin } from 'vite' +import { cleanUrl } from 'vite-node/utils' import { hoistMocks } from '../hoistMocks' import { distDir } from '../../paths' +import { automockModule } from '../automock' -export function MocksPlugin(): Plugin { - return { - name: 'vitest:mocks', - enforce: 'post', - transform(code, id) { - if (id.includes(distDir)) { - return - } - return hoistMocks(code, id, this.parse) +export function MocksPlugins(): Plugin[] { + return [ + { + name: 'vitest:mocks', + enforce: 'post', + transform(code, id) { + if (id.includes(distDir)) { + return + } + return hoistMocks(code, id, this.parse) + }, }, - } + { + name: 'vitest:automock', + enforce: 'post', + transform(code, id) { + if (id.includes('mock=auto')) { + const ms = automockModule(code, this.parse) + return { + code: ms.toString(), + map: ms.generateMap({ hires: true, source: cleanUrl(id) }), + } + } + }, + }, + ] } diff --git a/packages/vitest/src/node/plugins/workspace.ts b/packages/vitest/src/node/plugins/workspace.ts index 172d371138cc..c6e1af2d6ac7 100644 --- a/packages/vitest/src/node/plugins/workspace.ts +++ b/packages/vitest/src/node/plugins/workspace.ts @@ -9,7 +9,7 @@ import type { ResolvedConfig, UserWorkspaceConfig } from '../../types' import { CoverageTransform } from './coverageTransform' import { CSSEnablerPlugin } from './cssEnabler' import { SsrReplacerPlugin } from './ssrReplacer' -import { MocksPlugin } from './mocks' +import { MocksPlugins } from './mocks' import { deleteDefineConfig, hijackVitePluginInject, @@ -136,7 +136,7 @@ export function WorkspaceVitestPlugin( SsrReplacerPlugin(), ...CSSEnablerPlugin(project), CoverageTransform(project.ctx), - MocksPlugin(), + ...MocksPlugins(), VitestResolver(project.ctx), VitestOptimizer(), NormalizeURLPlugin(), diff --git a/packages/vitest/src/node/pool.ts b/packages/vitest/src/node/pool.ts index 604a6127fd37..ef183ef1081c 100644 --- a/packages/vitest/src/node/pool.ts +++ b/packages/vitest/src/node/pool.ts @@ -5,7 +5,6 @@ import { isWindows } from '../utils/env' import type { Vitest } from './core' import { createForksPool } from './pools/forks' import { createThreadsPool } from './pools/threads' -import { createBrowserPool } from './pools/browser' import { createVmThreadsPool } from './pools/vmThreads' import type { WorkspaceProject } from './workspace' import { createTypecheckPool } from './pools/typecheck' @@ -17,6 +16,8 @@ export type RunWithFiles = ( invalidates?: string[] ) => Awaitable +type LocalPool = Exclude + export interface ProcessPool { name: string runTests: RunWithFiles @@ -153,17 +154,17 @@ export function createPool(ctx: Vitest): ProcessPool { return poolInstance as ProcessPool } - const filesByPool: Record = { + const filesByPool: Record = { forks: [], threads: [], - browser: [], + // browser: [], vmThreads: [], vmForks: [], typescript: [], } - const factories: Record ProcessPool> = { - browser: () => createBrowserPool(ctx), + const factories: Record ProcessPool> = { + // browser: () => createBrowserPool(ctx), vmThreads: () => createVmThreadsPool(ctx, options), threads: () => createThreadsPool(ctx, options), forks: () => createForksPool(ctx, options), @@ -203,6 +204,14 @@ export function createPool(ctx: Vitest): ProcessPool { return pools[pool]!.runTests(specs, invalidate) } + if (pool === 'browser') { + pools[pool] ??= await (async () => { + const { createBrowserPool } = await import('@vitest/browser') + return createBrowserPool(ctx) + })() + return pools[pool]!.runTests(specs, invalidate) + } + const poolHandler = await resolveCustomPool(pool) pools[poolHandler.name] ??= poolHandler return poolHandler.runTests(specs, invalidate) diff --git a/packages/vitest/src/node/workspace.ts b/packages/vitest/src/node/workspace.ts index 044183549bcd..85a0386790f6 100644 --- a/packages/vitest/src/node/workspace.ts +++ b/packages/vitest/src/node/workspace.ts @@ -18,7 +18,6 @@ import type { } from 'vite' import { ViteNodeRunner } from 'vite-node/client' import { ViteNodeServer } from 'vite-node/server' -import { createBrowserServer } from '../integrations/browser/server' import type { ProvidedContext, ResolvedConfig, @@ -27,16 +26,16 @@ import type { Vitest, } from '../types' import type { Typechecker } from '../typecheck/typechecker' -import type { BrowserProvider } from '../types/browser' -import { getBrowserProvider } from '../integrations/browser' import { deepMerge, nanoid } from '../utils/base' -import { VitestBrowserServerMocker } from '../integrations/browser/mocker' -import type { WebSocketBrowserRPC } from '../api/types' +import { setup } from '../api/setup' +import type { BrowserServer } from '../types/browser' import { isBrowserEnabled, resolveConfig } from './config' import { WorkspaceVitestPlugin } from './plugins/workspace' import { createViteServer } from './vite' import type { GlobalSetupFile } from './globalSetup' import { loadGlobalSetupFiles } from './globalSetup' +import { MocksPlugins } from './plugins/mocks' +import { CoverageTransform } from './plugins/coverageTransform' interface InitializeProjectOptions extends UserWorkspaceConfig { workspaceConfigPath: string @@ -89,29 +88,11 @@ export class WorkspaceProject { server!: ViteDevServer vitenode!: ViteNodeServer runner!: ViteNodeRunner - browser?: ViteDevServer + browser?: BrowserServer typechecker?: Typechecker closingPromise: Promise | undefined - // TODO: abstract browser related things and move to @vitest/browser - browserProvider: BrowserProvider | undefined - browserMocker = new VitestBrowserServerMocker(this) - // TODO: I mean, we really need to abstract it - browserRpc = { - orchestrators: new Map(), - testers: new Map(), - } - - browserState = new Map< - string, - { - files: string[] - resolve: () => void - reject: (v: unknown) => void - } - >() - testFilesList: string[] | null = null public readonly id = nanoid() @@ -208,14 +189,14 @@ export class WorkspaceProject { getModulesByFilepath(file: string) { const set = this.server.moduleGraph.getModulesByFile(file) - || this.browser?.moduleGraph.getModulesByFile(file) + || this.browser?.vite.moduleGraph.getModulesByFile(file) return set || new Set() } getModuleById(id: string) { return ( this.server.moduleGraph.getModuleById(id) - || this.browser?.moduleGraph.getModuleById(id) + || this.browser?.vite.moduleGraph.getModuleById(id) ) } @@ -227,7 +208,7 @@ export class WorkspaceProject { getBrowserSourceMapModuleById( id: string, ): TransformResult['map'] | undefined { - return this.browser?.moduleGraph.getModuleById(id)?.transformResult?.map + return this.browser?.vite.moduleGraph.getModuleById(id)?.transformResult?.map } get reporters() { @@ -360,8 +341,19 @@ export class WorkspaceProject { if (!this.isBrowserEnabled()) { return } + await this.ctx.packageInstaller.ensureInstalled('@vitest/browser', this.config.root) + const { createBrowserServer } = await import('@vitest/browser') await this.browser?.close() - this.browser = await createBrowserServer(this, configFile) + const browser = await createBrowserServer( + this, + configFile, + [...MocksPlugins()], + [CoverageTransform(this.ctx)], + ) + this.browser = browser + if (this.config.browser.ui) { + setup(this.ctx, browser.vite) + } } static createBasicProject(ctx: Vitest) { @@ -535,29 +527,6 @@ export class WorkspaceProject { if (!this.isBrowserEnabled()) { return } - if (this.browserProvider) { - return - } - const Provider = await getBrowserProvider(this.config.browser, this) - this.browserProvider = new Provider() - const browser = this.config.browser.name - if (!browser) { - throw new Error( - `[${this.getName()}] Browser name is required. Please, set \`test.browser.name\` option manually.`, - ) - } - const supportedBrowsers = this.browserProvider.getSupportedBrowsers() - if (supportedBrowsers.length && !supportedBrowsers.includes(browser)) { - throw new Error( - `[${this.getName()}] Browser "${browser}" is not supported by the browser provider "${ - this.browserProvider.name - }". Supported browsers: ${supportedBrowsers.join(', ')}.`, - ) - } - const providerOptions = this.config.browser.providerOptions - await this.browserProvider.initialize(this, { - browser, - options: providerOptions, - }) + await this.browser?.initBrowserProvider() } } diff --git a/packages/vitest/src/types/browser.ts b/packages/vitest/src/types/browser.ts index 85dd2e72a26e..305550a4a4c5 100644 --- a/packages/vitest/src/types/browser.ts +++ b/packages/vitest/src/types/browser.ts @@ -1,4 +1,6 @@ import type { Awaitable } from '@vitest/utils' +import type { ViteDevServer } from 'vite' +import type { CancelReason } from '@vitest/runner' import type { WorkspaceProject } from '../node/workspace' import type { ApiConfig } from './config' @@ -148,6 +150,31 @@ export interface BrowserCommandContext { contextId: string } +export interface BrowserServerStateContext { + files: string[] + resolve: () => void + reject: (v: unknown) => void +} + +export interface BrowserOrchestrator { + createTesters: (files: string[]) => Promise + onCancel: (reason: CancelReason) => Promise +} + +export interface BrowserServerState { + orchestrators: Map + getContext: (contextId: string) => BrowserServerStateContext | undefined + createAsyncContext: (contextId: string, files: string[]) => Promise +} + +export interface BrowserServer { + vite: ViteDevServer + state: BrowserServerState + provider: BrowserProvider + close: () => Promise + initBrowserProvider: () => Promise +} + export interface BrowserCommand { (context: BrowserCommandContext, ...payload: Payload): Awaitable } diff --git a/packages/vitest/src/types/index.ts b/packages/vitest/src/types/index.ts index 1cb10341959c..31196af13b9b 100644 --- a/packages/vitest/src/types/index.ts +++ b/packages/vitest/src/types/index.ts @@ -13,6 +13,7 @@ export type * from './worker' export type * from './general' export type * from './coverage' export type * from './benchmark' +export type { CancelReason } from '@vitest/runner' export type { DiffOptions } from '@vitest/utils/diff' export type { MockedFunction, diff --git a/packages/vitest/src/utils/graph.ts b/packages/vitest/src/utils/graph.ts index 577427223e47..ee53d83b5baa 100644 --- a/packages/vitest/src/utils/graph.ts +++ b/packages/vitest/src/utils/graph.ts @@ -29,7 +29,7 @@ export async function getModuleGraph( let id = clearId(mod.id) seen.set(mod, id) const rewrote = browser - ? mod.file?.includes(project.browser!.config.cacheDir) + ? mod.file?.includes(project.browser!.vite.config.cacheDir) ? mod.id : false : await project.vitenode.shouldExternalize(id) @@ -50,7 +50,7 @@ export async function getModuleGraph( return id } if (browser && project.browser) { - await get(project.browser.moduleGraph.getModuleById(id)) + await get(project.browser.vite.moduleGraph.getModuleById(id)) } else { await get(project.server.moduleGraph.getModuleById(id)) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 10430812e52e..7bea584f80a8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -36,7 +36,7 @@ importers: devDependencies: '@antfu/eslint-config': specifier: ^2.21.1 - version: 2.21.1(@vue/compiler-sfc@3.4.27)(eslint@9.5.0)(typescript@5.4.5)(vitest@packages+vitest) + version: 2.21.1(@vue/compiler-sfc@3.4.29)(eslint@9.5.0)(typescript@5.4.5)(vitest@packages+vitest) '@antfu/ni': specifier: ^0.21.12 version: 0.21.12 @@ -447,6 +447,9 @@ importers: sirv: specifier: ^2.0.4 version: 2.0.4 + ws: + specifier: ^8.17.1 + version: 8.17.1 devDependencies: '@types/ws': specifier: ^8.5.10 @@ -719,7 +722,7 @@ importers: version: 1.1.43 '@testing-library/vue': specifier: ^8.1.0 - version: 8.1.0(@vue/compiler-sfc@3.4.27)(vue@3.4.29) + version: 8.1.0(@vue/compiler-sfc@3.4.29)(vue@3.4.29) '@types/codemirror': specifier: ^5.60.15 version: 5.60.15 @@ -782,7 +785,7 @@ importers: version: 5.2.6(@types/node@20.14.2) vite-plugin-pages: specifier: ^0.32.2 - version: 0.32.2(@vue/compiler-sfc@3.4.27)(vite@5.2.6)(vue-router@4.3.3) + version: 0.32.2(@vue/compiler-sfc@3.4.29)(vite@5.2.6)(vue-router@4.3.3) vue: specifier: ^3.4.29 version: 3.4.29(typescript@5.4.5) @@ -1552,7 +1555,7 @@ packages: '@jridgewell/gen-mapping': 0.3.5 '@jridgewell/trace-mapping': 0.3.25 - /@antfu/eslint-config@2.21.1(@vue/compiler-sfc@3.4.27)(eslint@9.5.0)(typescript@5.4.5)(vitest@packages+vitest): + /@antfu/eslint-config@2.21.1(@vue/compiler-sfc@3.4.29)(eslint@9.5.0)(typescript@5.4.5)(vitest@packages+vitest): resolution: {integrity: sha512-CG7U7nihU73zufrxe5Rr4pxsHrW60GXl9yzRpRY+ImGQ2CVhd0eb3fqJYdNwDJBgKgqHGWX4p1ovYvno/jfWHA==} hasBin: true peerDependencies: @@ -1624,7 +1627,7 @@ packages: eslint-plugin-vitest: 0.5.4(@typescript-eslint/eslint-plugin@7.13.0)(eslint@9.5.0)(typescript@5.4.5)(vitest@packages+vitest) eslint-plugin-vue: 9.26.0(eslint@9.5.0) eslint-plugin-yml: 1.14.0(eslint@9.5.0) - eslint-processor-vue-blocks: 0.1.2(@vue/compiler-sfc@3.4.27)(eslint@9.5.0) + eslint-processor-vue-blocks: 0.1.2(@vue/compiler-sfc@3.4.29)(eslint@9.5.0) globals: 15.4.0 jsonc-eslint-parser: 2.4.0 local-pkg: 0.5.0 @@ -1760,10 +1763,10 @@ packages: '@babel/helper-compilation-targets': 7.23.6 '@babel/helper-module-transforms': 7.24.5(@babel/core@7.24.5) '@babel/helpers': 7.24.5 - '@babel/parser': 7.24.5 + '@babel/parser': 7.24.7 '@babel/template': 7.24.0 '@babel/traverse': 7.24.5 - '@babel/types': 7.24.5 + '@babel/types': 7.24.7 convert-source-map: 2.0.0 debug: 4.3.5 gensync: 1.0.0-beta.2 @@ -1818,7 +1821,7 @@ packages: resolution: {integrity: sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.24.0 + '@babel/types': 7.24.7 dev: true /@babel/helper-annotate-as-pure@7.24.7: @@ -1978,7 +1981,7 @@ packages: resolution: {integrity: sha512-viKb0F9f2s0BCS22QSF308z/+1YWKV/76mwt61NBzS5izMzDPwdq1pTrzf+Li3npBWX9KdQbkeCt1jSAM7lZqg==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.24.0 + '@babel/types': 7.24.7 dev: true /@babel/helper-module-imports@7.24.7: @@ -5289,7 +5292,7 @@ packages: dependencies: '@testing-library/dom': 10.1.0 - /@testing-library/vue@8.1.0(@vue/compiler-sfc@3.4.27)(vue@3.4.29): + /@testing-library/vue@8.1.0(@vue/compiler-sfc@3.4.29)(vue@3.4.29): resolution: {integrity: sha512-ls4RiHO1ta4mxqqajWRh8158uFObVrrtAPoxk7cIp4HrnQUj/ScKzqz53HxYpG3X6Zb7H2v+0eTGLSoy8HQ2nA==} engines: {node: '>=14'} peerDependencies: @@ -5301,7 +5304,7 @@ packages: dependencies: '@babel/runtime': 7.24.4 '@testing-library/dom': 9.3.4 - '@vue/compiler-sfc': 3.4.27 + '@vue/compiler-sfc': 3.4.29 '@vue/test-utils': 2.4.6 vue: 3.4.29(typescript@5.4.5) dev: true @@ -5344,14 +5347,14 @@ packages: /@types/babel__generator@7.6.6: resolution: {integrity: sha512-66BXMKb/sUWbMdBNdMvajU7i/44RkrA3z/Yt1c7R5xejt8qh84iU54yUWCtm0QwGJlDcf/gg4zd/x4mpLAlb/w==} dependencies: - '@babel/types': 7.24.0 + '@babel/types': 7.24.7 dev: true /@types/babel__template@7.4.3: resolution: {integrity: sha512-ciwyCLeuRfxboZ4isgdNZi/tkt06m8Tw6uGbBSBgWrnnZGNXiEyM27xc/PjXGQLqlZ6ylbgHMnm7ccF9tCkOeQ==} dependencies: - '@babel/parser': 7.24.4 - '@babel/types': 7.24.0 + '@babel/parser': 7.24.7 + '@babel/types': 7.24.7 dev: true /@types/babel__traverse@7.20.3: @@ -6206,7 +6209,7 @@ packages: /@vue/compiler-core@3.4.21: resolution: {integrity: sha512-MjXawxZf2SbZszLPYxaFCjxfibYrzr3eYbKxwpLR9EQN+oaziSu3qKVbwBERj1IFIB8OLUewxB5m/BFzi613og==} dependencies: - '@babel/parser': 7.24.4 + '@babel/parser': 7.24.7 '@vue/shared': 3.4.21 entities: 4.5.0 estree-walker: 2.0.2 @@ -6216,23 +6219,13 @@ packages: /@vue/compiler-core@3.4.26: resolution: {integrity: sha512-N9Vil6Hvw7NaiyFUFBPXrAyETIGlQ8KcFMkyk6hW1Cl6NvoqvP+Y8p1Eqvx+UdqsnrnI9+HMUEJegzia3mhXmQ==} dependencies: - '@babel/parser': 7.24.4 + '@babel/parser': 7.24.7 '@vue/shared': 3.4.26 entities: 4.5.0 estree-walker: 2.0.2 source-map-js: 1.2.0 dev: true - /@vue/compiler-core@3.4.27: - resolution: {integrity: sha512-E+RyqY24KnyDXsCuQrI+mlcdW3ALND6U7Gqa/+bVwbcpcR3BRRIckFoz7Qyd4TTlnugtwuI7YgjbvsLmxb+yvg==} - dependencies: - '@babel/parser': 7.24.5 - '@vue/shared': 3.4.27 - entities: 4.5.0 - estree-walker: 2.0.2 - source-map-js: 1.2.0 - dev: true - /@vue/compiler-core@3.4.29: resolution: {integrity: sha512-TFKiRkKKsRCKvg/jTSSKK7mYLJEQdUiUfykbG49rubC9SfDyvT2JrzTReopWlz2MxqeLyxh9UZhvxEIBgAhtrg==} dependencies: @@ -6256,13 +6249,6 @@ packages: '@vue/shared': 3.4.26 dev: true - /@vue/compiler-dom@3.4.27: - resolution: {integrity: sha512-kUTvochG/oVgE1w5ViSr3KUBh9X7CWirebA3bezTbB5ZKBQZwR2Mwj9uoSKRMFcz4gSMzzLXBPD6KpCLb9nvWw==} - dependencies: - '@vue/compiler-core': 3.4.27 - '@vue/shared': 3.4.27 - dev: true - /@vue/compiler-dom@3.4.29: resolution: {integrity: sha512-A6+iZ2fKIEGnfPJejdB7b1FlJzgiD+Y/sxxKwJWg1EbJu6ZPgzaPQQ51ESGNv0CP6jm6Z7/pO6Ia8Ze6IKrX7w==} dependencies: @@ -6305,20 +6291,6 @@ packages: source-map-js: 1.2.0 dev: true - /@vue/compiler-sfc@3.4.27: - resolution: {integrity: sha512-nDwntUEADssW8e0rrmE0+OrONwmRlegDA1pD6QhVeXxjIytV03yDqTey9SBDiALsvAd5U4ZrEKbMyVXhX6mCGA==} - dependencies: - '@babel/parser': 7.24.7 - '@vue/compiler-core': 3.4.27 - '@vue/compiler-dom': 3.4.27 - '@vue/compiler-ssr': 3.4.27 - '@vue/shared': 3.4.27 - estree-walker: 2.0.2 - magic-string: 0.30.10 - postcss: 8.4.38 - source-map-js: 1.2.0 - dev: true - /@vue/compiler-sfc@3.4.29: resolution: {integrity: sha512-zygDcEtn8ZimDlrEQyLUovoWgKQic6aEQqRXce2WXBvSeHbEbcAsXyCk9oG33ZkyWH4sl9D3tkYc1idoOkdqZQ==} dependencies: @@ -6346,13 +6318,6 @@ packages: '@vue/shared': 3.4.26 dev: true - /@vue/compiler-ssr@3.4.27: - resolution: {integrity: sha512-CVRzSJIltzMG5FcidsW0jKNQnNRYC8bT21VegyMMtHmhW3UOI7knmUehzswXLrExDLE6lQCZdrhD4ogI7c+vuw==} - dependencies: - '@vue/compiler-dom': 3.4.27 - '@vue/shared': 3.4.27 - dev: true - /@vue/compiler-ssr@3.4.29: resolution: {integrity: sha512-rFbwCmxJ16tDp3N8XCx5xSQzjhidYjXllvEcqX/lopkoznlNPz3jyy0WGJCyhAaVQK677WWFt3YO/WUEkMMUFQ==} dependencies: @@ -6400,7 +6365,7 @@ packages: dependencies: '@volar/language-core': 1.10.4 '@volar/source-map': 1.10.4 - '@vue/compiler-dom': 3.4.27 + '@vue/compiler-dom': 3.4.29 '@vue/shared': 3.4.27 computeds: 0.0.1 minimatch: 9.0.4 @@ -6418,8 +6383,8 @@ packages: optional: true dependencies: '@volar/language-core': 2.3.0 - '@vue/compiler-dom': 3.4.27 - '@vue/shared': 3.4.27 + '@vue/compiler-dom': 3.4.29 + '@vue/shared': 3.4.29 computeds: 0.0.1 minimatch: 9.0.4 path-browserify: 1.0.1 @@ -6508,13 +6473,13 @@ packages: vue: 3.4.26(typescript@5.4.5) dev: true - /@vue/server-renderer@3.4.27(vue@3.4.21): - resolution: {integrity: sha512-dlAMEuvmeA3rJsOMJ2J1kXU7o7pOxgsNHVr9K8hB3ImIkSuBrIdy0vF66h8gf8Tuinf1TK3mPAz2+2sqyf3KzA==} + /@vue/server-renderer@3.4.29(vue@3.4.21): + resolution: {integrity: sha512-HMLCmPI2j/k8PVkSBysrA2RxcxC5DgBiCdj7n7H2QtR8bQQPqKAe8qoaxLcInzouBmzwJ+J0x20ygN/B5mYBng==} peerDependencies: - vue: 3.4.27 + vue: 3.4.29 dependencies: - '@vue/compiler-ssr': 3.4.27 - '@vue/shared': 3.4.27 + '@vue/compiler-ssr': 3.4.29 + '@vue/shared': 3.4.29 vue: 3.4.21(typescript@5.4.5) dev: true optional: true @@ -6551,8 +6516,8 @@ packages: js-beautify: 1.14.6 vue: 3.4.21(typescript@5.4.5) optionalDependencies: - '@vue/compiler-dom': 3.4.27 - '@vue/server-renderer': 3.4.27(vue@3.4.21) + '@vue/compiler-dom': 3.4.29 + '@vue/server-renderer': 3.4.29(vue@3.4.21) dev: true /@vue/test-utils@2.4.6: @@ -9356,13 +9321,13 @@ packages: - supports-color dev: true - /eslint-processor-vue-blocks@0.1.2(@vue/compiler-sfc@3.4.27)(eslint@9.5.0): + /eslint-processor-vue-blocks@0.1.2(@vue/compiler-sfc@3.4.29)(eslint@9.5.0): resolution: {integrity: sha512-PfpJ4uKHnqeL/fXUnzYkOax3aIenlwewXRX8jFinA1a2yCFnLgMuiH3xvCgvHHUlV2xJWQHbCTdiJWGwb3NqpQ==} peerDependencies: '@vue/compiler-sfc': ^3.3.0 eslint: ^8.50.0 || ^9.0.0 dependencies: - '@vue/compiler-sfc': 3.4.27 + '@vue/compiler-sfc': 3.4.29 eslint: 9.5.0 dev: true @@ -11332,7 +11297,7 @@ packages: whatwg-encoding: 3.1.1 whatwg-mimetype: 4.0.0 whatwg-url: 14.0.0 - ws: 8.16.0 + ws: 8.17.1 xml-name-validator: 5.0.0 transitivePeerDependencies: - bufferutil @@ -15641,7 +15606,7 @@ packages: vfile-message: 4.0.2 dev: true - /vite-plugin-pages@0.32.2(@vue/compiler-sfc@3.4.27)(vite@5.2.6)(vue-router@4.3.3): + /vite-plugin-pages@0.32.2(@vue/compiler-sfc@3.4.29)(vite@5.2.6)(vue-router@4.3.3): resolution: {integrity: sha512-wX4lRlylcTggwHUH7bV1SVJpQMz4ZUkbzzAbbBBH5M7c/9Yx5rAqrjYELIca6vLCoCpSd+r6/WI7KnvRK/Drcg==} peerDependencies: '@solidjs/router': '*' @@ -15660,7 +15625,7 @@ packages: optional: true dependencies: '@types/debug': 4.1.12 - '@vue/compiler-sfc': 3.4.27 + '@vue/compiler-sfc': 3.4.29 debug: 4.3.5 deep-equal: 2.2.3 extract-comments: 1.1.0 diff --git a/test/browser/specs/mocking.test.ts b/test/browser/specs/mocking.test.ts index 650a3c23ab96..fc5eb8c58d75 100644 --- a/test/browser/specs/mocking.test.ts +++ b/test/browser/specs/mocking.test.ts @@ -1,4 +1,4 @@ -import { expect, test } from 'vitest' +import { expect, onTestFailed, test } from 'vitest' import { runVitest } from '../../test-utils' test.each([true, false])('mocking works correctly - isolated %s', async (isolate) => { @@ -6,6 +6,12 @@ test.each([true, false])('mocking works correctly - isolated %s', async (isolate root: 'fixtures/mocking', isolate, }) + + onTestFailed(() => { + console.error(result.stdout) + console.error(result.stderr) + }) + expect(result.stderr).toBe('') expect(result.stdout).toContain('automocked.test.ts') expect(result.stdout).toContain('mocked-__mocks__.test.ts') diff --git a/test/browser/vitest.config.mts b/test/browser/vitest.config.mts index 813dcfcb24f0..8caab8b37a29 100644 --- a/test/browser/vitest.config.mts +++ b/test/browser/vitest.config.mts @@ -27,6 +27,7 @@ export default defineConfig({ include: ['@vitest/cjs-lib'], }, test: { + testTimeout: process.env.CI ? 120_000 : 10_000, include: ['test/**.test.{ts,js}'], // having a snapshot environment doesn't affect browser tests snapshotEnvironment: './custom-snapshot-env.ts', diff --git a/test/core/test/browserAutomocker.test.ts b/test/core/test/browserAutomocker.test.ts index b48728f39fd2..1848678627bc 100644 --- a/test/core/test/browserAutomocker.test.ts +++ b/test/core/test/browserAutomocker.test.ts @@ -1,6 +1,6 @@ -import { automockModule } from '@vitest/browser/src/node/automocker.js' import { parseAst } from 'vite' import { expect, it } from 'vitest' +import { automockModule } from 'vitest/src/node/automock.js' function automock(code: string) { return automockModule(code, parseAst).toString()