From f973c278289c305838796ba4cd84f5a8d51bcc30 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Mon, 20 Mar 2023 22:40:18 +0100 Subject: [PATCH 01/10] feat: show browser console in the terminal --- packages/browser/src/client/main.ts | 37 +++++++++++++++++++++++++++++ packages/vitest/src/api/setup.ts | 3 +++ packages/vitest/src/api/types.ts | 3 ++- test/browser/test/dom.test.ts | 4 ++++ 4 files changed, 46 insertions(+), 1 deletion(-) diff --git a/packages/browser/src/client/main.ts b/packages/browser/src/client/main.ts index ebf2ad4e67b0..f64189fa6b6f 100644 --- a/packages/browser/src/client/main.ts +++ b/packages/browser/src/client/main.ts @@ -1,3 +1,4 @@ +/* eslint-disable no-console */ import type { VitestClient } from '@vitest/ws-client' import { createClient } from '@vitest/ws-client' // eslint-disable-next-line no-restricted-imports @@ -48,6 +49,41 @@ async function loadConfig() { throw new Error('cannot load configuration after 5 retries') } +const { Date } = globalThis + +const interceptLog = (client: VitestClient) => { + // TODO: add support for more console methods + const { log, info, error } = console + const processLog = (args: unknown[]) => { + // TODO: make better log, use concordance(?) + return args.map(String).join(' ') + } + const sendLog = (type: 'stdout' | 'stderr', args: unknown[]) => { + const content = processLog(args) + const unknownTestId = '__vitest__unknown_test__' + // @ts-expect-error untyped global + const taskId = globalThis.__vitest_worker__?.current?.id ?? unknownTestId + client.rpc.sendLog({ + content, + time: Date.now(), + taskId, + type, + size: content.length, + }) + } + const stdout = (base: (...args: unknown[]) => void) => (...args: unknown[]) => { + sendLog('stdout', args) + return base(...args) + } + console.log = stdout(log) + console.info = stdout(info) + + console.error = (...args) => { + sendLog('stderr', args) + return error(...args) + } +} + ws.addEventListener('open', async () => { await loadConfig() @@ -66,6 +102,7 @@ ws.addEventListener('open', async () => { const iFrame = document.getElementById('vitest-ui') as HTMLIFrameElement iFrame.setAttribute('src', '/__vitest__/') + interceptLog(client) await runTests(paths, config, client) }) diff --git a/packages/vitest/src/api/setup.ts b/packages/vitest/src/api/setup.ts index 342be15b6508..735b9ee0826a 100644 --- a/packages/vitest/src/api/setup.ts +++ b/packages/vitest/src/api/setup.ts @@ -52,6 +52,9 @@ export function setup(ctx: Vitest, server?: ViteDevServer) { getPaths() { return ctx.state.getPaths() }, + sendLog(log) { + return ctx.report('onUserConsoleLog', log) + }, resolveSnapshotPath(testPath) { return ctx.snapshot.resolvePath(testPath) }, diff --git a/packages/vitest/src/api/types.ts b/packages/vitest/src/api/types.ts index d56d4cf8fd8f..aa7e1f09cb45 100644 --- a/packages/vitest/src/api/types.ts +++ b/packages/vitest/src/api/types.ts @@ -1,5 +1,5 @@ import type { TransformResult } from 'vite' -import type { File, ModuleGraphData, Reporter, ResolvedConfig, SnapshotResult, TaskResultPack } from '../types' +import type { File, ModuleGraphData, Reporter, ResolvedConfig, SnapshotResult, TaskResultPack, UserConsoleLog } from '../types' export interface TransformResultWithSource extends TransformResult { source?: string @@ -9,6 +9,7 @@ export interface WebSocketHandlers { onCollected(files?: File[]): Promise onTaskUpdate(packs: TaskResultPack[]): void onDone(name: string): void + sendLog(log: UserConsoleLog): void getFiles(): File[] getPaths(): string[] getConfig(): ResolvedConfig diff --git a/test/browser/test/dom.test.ts b/test/browser/test/dom.test.ts index 12fd4e0fbc33..de164fe51375 100644 --- a/test/browser/test/dom.test.ts +++ b/test/browser/test/dom.test.ts @@ -4,5 +4,9 @@ test('render div', async () => { const div = document.createElement('div') div.textContent = 'Hello World' document.body.appendChild(div) + // TODO: test console + // eslint-disable-next-line no-console + console.log('hello world') + console.error('error world') expect(div.textContent).toBe('Hello World') }) From b15dd6aaaa6617b621e7ce1f6af0f45fd4e2e4c9 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Tue, 21 Mar 2023 10:41:43 +0100 Subject: [PATCH 02/10] chore: exclude node builtins form optimizing, so they can be polyfilled --- packages/browser/src/node/index.ts | 25 +++++++++---------------- 1 file changed, 9 insertions(+), 16 deletions(-) diff --git a/packages/browser/src/node/index.ts b/packages/browser/src/node/index.ts index 39bf632b082a..7b089537f8c4 100644 --- a/packages/browser/src/node/index.ts +++ b/packages/browser/src/node/index.ts @@ -18,21 +18,6 @@ export default (base = '/'): Plugin[] => { { enforce: 'pre', name: 'vitest:browser', - async resolveId(id) { - if (id === '/__vitest_index__') - return this.resolve('vitest/browser') - - if (id === '/__vitest_runners__') - return this.resolve('vitest/runners') - - if (id.startsWith('node:')) - id = id.slice(5) - - if (polyfills.includes(id)) - return polyfillPath(normalizeId(id)) - - return null - }, async configureServer(server) { server.middlewares.use( base, @@ -45,8 +30,16 @@ export default (base = '/'): Plugin[] => { }, { name: 'modern-node-polyfills', + enforce: 'pre', + config() { + return { + optimizeDeps: { + exclude: [...polyfills, ...builtinModules], + }, + } + }, async resolveId(id) { - if (!builtinModules.includes(id)) + if (!builtinModules.includes(id) && !polyfills.includes(id) && !id.startsWith('node:')) return id = normalizeId(id) From 34826202b961b70e6c5762b9a174fe702814e6ec Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Tue, 21 Mar 2023 11:18:28 +0100 Subject: [PATCH 03/10] feat: process log --- packages/browser/src/client/main.ts | 27 +++++++++++++++------------ packages/vitest/package.json | 4 ++++ packages/vitest/rollup.config.js | 22 ++++++++++++---------- packages/vitest/src/public/utils.ts | 1 + packages/vitest/utils.d.ts | 1 + test/browser/test/dom.test.ts | 2 +- 6 files changed, 34 insertions(+), 23 deletions(-) create mode 100644 packages/vitest/src/public/utils.ts create mode 100644 packages/vitest/utils.d.ts diff --git a/packages/browser/src/client/main.ts b/packages/browser/src/client/main.ts index f64189fa6b6f..c111c41fd562 100644 --- a/packages/browser/src/client/main.ts +++ b/packages/browser/src/client/main.ts @@ -24,6 +24,11 @@ const browserHashMap = new Map() const url = new URL(location.href) const testId = url.searchParams.get('id') || 'unknown' +const importId = (id: string) => { + const name = `/@id/${id}` + return import(name) +} + const getQueryPaths = () => { return url.searchParams.getAll('path') } @@ -51,13 +56,15 @@ async function loadConfig() { const { Date } = globalThis -const interceptLog = (client: VitestClient) => { +const interceptLog = async (client: VitestClient) => { + const { stringify, format } = await importId('vitest/utils') as typeof import('vitest/utils') // TODO: add support for more console methods const { log, info, error } = console - const processLog = (args: unknown[]) => { - // TODO: make better log, use concordance(?) - return args.map(String).join(' ') - } + const processLog = (args: unknown[]) => args.map((a) => { + if (a instanceof Element) + return stringify(a) + return format(a) + }).join(' ') const sendLog = (type: 'stdout' | 'stderr', args: unknown[]) => { const content = processLog(args) const unknownTestId = '__vitest__unknown_test__' @@ -102,7 +109,7 @@ ws.addEventListener('open', async () => { const iFrame = document.getElementById('vitest-ui') as HTMLIFrameElement iFrame.setAttribute('src', '/__vitest__/') - interceptLog(client) + await interceptLog(client) await runTests(paths, config, client) }) @@ -112,14 +119,10 @@ async function runTests(paths: string[], config: any, client: VitestClient) { const viteClientPath = '/@vite/client' await import(viteClientPath) - // we use dynamic import here, because this file is bundled with UI, - // but we need to resolve correct path at runtime - const path = '/__vitest_index__' - const { startTests, setupCommonEnv, setupSnapshotEnvironment } = await import(path) as typeof import('vitest/browser') + const { startTests, setupCommonEnv, setupSnapshotEnvironment } = await importId('vitest/browser') as typeof import('vitest/browser') if (!runner) { - const runnerPath = '/__vitest_runners__' - const { VitestTestRunner } = await import(runnerPath) as typeof import('vitest/runners') + const { VitestTestRunner } = await importId('vitest/runners') as typeof import('vitest/runners') const BrowserRunner = createBrowserRunner(VitestTestRunner) runner = new BrowserRunner({ config, client, browserHashMap }) } diff --git a/packages/vitest/package.json b/packages/vitest/package.json index 23b48dd6481f..2804e2abe9c4 100644 --- a/packages/vitest/package.json +++ b/packages/vitest/package.json @@ -61,6 +61,10 @@ "types": "./dist/environments.d.ts", "import": "./dist/environments.js" }, + "./utils": { + "types": "./dist/utils.d.ts", + "import": "./dist/utils.js" + }, "./config": { "types": "./config.d.ts", "require": "./dist/config.cjs", diff --git a/packages/vitest/rollup.config.js b/packages/vitest/rollup.config.js index fe021e67f5ff..fb4ef9e6eaeb 100644 --- a/packages/vitest/rollup.config.js +++ b/packages/vitest/rollup.config.js @@ -28,18 +28,20 @@ const entries = [ 'src/runtime/entry.ts', 'src/integrations/spy.ts', 'src/coverage.ts', + 'src/public/utils.ts', ] -const dtsEntries = [ - 'src/index.ts', - 'src/node.ts', - 'src/environments.ts', - 'src/browser.ts', - 'src/runners.ts', - 'src/suite.ts', - 'src/config.ts', - 'src/coverage.ts', -] +const dtsEntries = { + index: 'src/index.ts', + node: 'src/node.ts', + environments: 'src/environments.ts', + browser: 'src/browser.ts', + runners: 'src/runners.ts', + suite: 'src/suite.ts', + config: 'src/config.ts', + coverage: 'src/coverage.ts', + utils: 'src/public/utils.ts', +} const external = [ ...builtinModules, diff --git a/packages/vitest/src/public/utils.ts b/packages/vitest/src/public/utils.ts new file mode 100644 index 000000000000..4bb19378e173 --- /dev/null +++ b/packages/vitest/src/public/utils.ts @@ -0,0 +1 @@ +export * from '@vitest/utils' \ No newline at end of file diff --git a/packages/vitest/utils.d.ts b/packages/vitest/utils.d.ts new file mode 100644 index 000000000000..916f85c977e7 --- /dev/null +++ b/packages/vitest/utils.d.ts @@ -0,0 +1 @@ +export * from './dist/utils.js' \ No newline at end of file diff --git a/test/browser/test/dom.test.ts b/test/browser/test/dom.test.ts index de164fe51375..e5320764b5e3 100644 --- a/test/browser/test/dom.test.ts +++ b/test/browser/test/dom.test.ts @@ -6,7 +6,7 @@ test('render div', async () => { document.body.appendChild(div) // TODO: test console // eslint-disable-next-line no-console - console.log('hello world') + console.log('hello world', div) console.error('error world') expect(div.textContent).toBe('Hello World') }) From 422b5600e9f2e556761d0c3d10b71cacf48fb6ae Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Tue, 21 Mar 2023 11:42:58 +0100 Subject: [PATCH 04/10] refactor: move files around --- packages/browser/src/client/logger.ts | 41 ++++++++++++++++++++++++ packages/browser/src/client/main.ts | 45 ++------------------------- packages/browser/src/client/utils.ts | 4 +++ 3 files changed, 47 insertions(+), 43 deletions(-) create mode 100644 packages/browser/src/client/logger.ts create mode 100644 packages/browser/src/client/utils.ts diff --git a/packages/browser/src/client/logger.ts b/packages/browser/src/client/logger.ts new file mode 100644 index 000000000000..29f6de540690 --- /dev/null +++ b/packages/browser/src/client/logger.ts @@ -0,0 +1,41 @@ +/* eslint-disable no-console */ + +import type { VitestClient } from '@vitest/ws-client' +import { importId } from './utils' + +const { Date } = globalThis + +export const interceptLog = async (client: VitestClient) => { + const { stringify, format } = await importId('vitest/utils') as typeof import('vitest/utils') + // TODO: add support for more console methods + const { log, info, error } = console + const processLog = (args: unknown[]) => args.map((a) => { + if (a instanceof Element) + return stringify(a) + return format(a) + }).join(' ') + const sendLog = (type: 'stdout' | 'stderr', args: unknown[]) => { + const content = processLog(args) + const unknownTestId = '__vitest__unknown_test__' + // @ts-expect-error untyped global + const taskId = globalThis.__vitest_worker__?.current?.id ?? unknownTestId + client.rpc.sendLog({ + content, + time: Date.now(), + taskId, + type, + size: content.length, + }) + } + const stdout = (base: (...args: unknown[]) => void) => (...args: unknown[]) => { + sendLog('stdout', args) + return base(...args) + } + console.log = stdout(log) + console.info = stdout(info) + + console.error = (...args) => { + sendLog('stderr', args) + return error(...args) + } +} diff --git a/packages/browser/src/client/main.ts b/packages/browser/src/client/main.ts index c111c41fd562..d66fc833569e 100644 --- a/packages/browser/src/client/main.ts +++ b/packages/browser/src/client/main.ts @@ -1,4 +1,3 @@ -/* eslint-disable no-console */ import type { VitestClient } from '@vitest/ws-client' import { createClient } from '@vitest/ws-client' // eslint-disable-next-line no-restricted-imports @@ -6,6 +5,8 @@ import type { ResolvedConfig } from 'vitest' import type { VitestRunner } from '@vitest/runner' import { createBrowserRunner } from './runner' import { BrowserSnapshotEnvironment } from './snapshot' +import { importId } from './utils' +import { interceptLog } from './logger' // @ts-expect-error mocking some node apis globalThis.process = { env: {}, argv: [], cwd: () => '/', stdout: { write: () => {} }, nextTick: cb => cb() } @@ -24,11 +25,6 @@ const browserHashMap = new Map() const url = new URL(location.href) const testId = url.searchParams.get('id') || 'unknown' -const importId = (id: string) => { - const name = `/@id/${id}` - return import(name) -} - const getQueryPaths = () => { return url.searchParams.getAll('path') } @@ -54,43 +50,6 @@ async function loadConfig() { throw new Error('cannot load configuration after 5 retries') } -const { Date } = globalThis - -const interceptLog = async (client: VitestClient) => { - const { stringify, format } = await importId('vitest/utils') as typeof import('vitest/utils') - // TODO: add support for more console methods - const { log, info, error } = console - const processLog = (args: unknown[]) => args.map((a) => { - if (a instanceof Element) - return stringify(a) - return format(a) - }).join(' ') - const sendLog = (type: 'stdout' | 'stderr', args: unknown[]) => { - const content = processLog(args) - const unknownTestId = '__vitest__unknown_test__' - // @ts-expect-error untyped global - const taskId = globalThis.__vitest_worker__?.current?.id ?? unknownTestId - client.rpc.sendLog({ - content, - time: Date.now(), - taskId, - type, - size: content.length, - }) - } - const stdout = (base: (...args: unknown[]) => void) => (...args: unknown[]) => { - sendLog('stdout', args) - return base(...args) - } - console.log = stdout(log) - console.info = stdout(info) - - console.error = (...args) => { - sendLog('stderr', args) - return error(...args) - } -} - ws.addEventListener('open', async () => { await loadConfig() diff --git a/packages/browser/src/client/utils.ts b/packages/browser/src/client/utils.ts new file mode 100644 index 000000000000..dc6f8229d5d0 --- /dev/null +++ b/packages/browser/src/client/utils.ts @@ -0,0 +1,4 @@ +export const importId = (id: string) => { + const name = `/@id/${id}` + return import(name) +} From 34b500fb258c53aec576473a2b16c2a14984fdd3 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Tue, 21 Mar 2023 13:17:02 +0100 Subject: [PATCH 05/10] feat: support more console methods --- packages/browser/src/client/logger.ts | 95 ++++++++++++++++++++++----- packages/browser/src/client/main.ts | 4 +- packages/utils/src/display.ts | 8 ++- test/browser/test/dom.test.ts | 4 -- 4 files changed, 88 insertions(+), 23 deletions(-) diff --git a/packages/browser/src/client/logger.ts b/packages/browser/src/client/logger.ts index 29f6de540690..9d04d7c64043 100644 --- a/packages/browser/src/client/logger.ts +++ b/packages/browser/src/client/logger.ts @@ -5,17 +5,18 @@ import { importId } from './utils' const { Date } = globalThis -export const interceptLog = async (client: VitestClient) => { - const { stringify, format } = await importId('vitest/utils') as typeof import('vitest/utils') - // TODO: add support for more console methods - const { log, info, error } = console - const processLog = (args: unknown[]) => args.map((a) => { - if (a instanceof Element) - return stringify(a) - return format(a) - }).join(' ') - const sendLog = (type: 'stdout' | 'stderr', args: unknown[]) => { - const content = processLog(args) +export const setupConsoleLogSpy = async (client: VitestClient) => { + const { stringify, format, utilInspect } = await importId('vitest/utils') as typeof import('vitest/utils') + const { log, info, error, dir, dirxml, trace, time, timeEnd, timeLog, warn, debug, count, countReset } = console + const formatInput = (input: unknown) => { + if (input instanceof Node) + return stringify(input) + return format(input) + } + const processLog = (args: unknown[]) => args.map(formatInput).join(' ') + const sendLog = (type: 'stdout' | 'stderr', content: string) => { + if (content.startsWith('[vite]')) + return const unknownTestId = '__vitest__unknown_test__' // @ts-expect-error untyped global const taskId = globalThis.__vitest_worker__?.current?.id ?? unknownTestId @@ -28,14 +29,78 @@ export const interceptLog = async (client: VitestClient) => { }) } const stdout = (base: (...args: unknown[]) => void) => (...args: unknown[]) => { - sendLog('stdout', args) + sendLog('stdout', processLog(args)) + return base(...args) + } + const stderr = (base: (...args: unknown[]) => void) => (...args: unknown[]) => { + sendLog('stderr', processLog(args)) return base(...args) } console.log = stdout(log) + console.debug = stdout(debug) console.info = stdout(info) - console.error = (...args) => { - sendLog('stderr', args) - return error(...args) + console.error = stderr(error) + console.warn = stderr(warn) + + console.dir = (item, options) => { + sendLog('stdout', utilInspect(item, options)) + return dir(item, options) + } + + console.dirxml = (...args) => { + sendLog('stdout', processLog(args)) + return dirxml(...args) + } + + console.trace = (...args: unknown[]) => { + const content = processLog(args) + const error = new Error('Trace') + const stack = (error.stack || '').split('\n').slice(2).join('\n') + sendLog('stdout', `${content}\n${stack}`) + return trace(...args) + } + + const timeLabels: Record = {} + + console.time = (label = 'default') => { + const now = performance.now() + time(label) + timeLabels[label] = now + } + + console.timeLog = (label = 'default') => { + timeLog(label) + if (!(label in timeLabels)) + sendLog('stderr', `Timer "${label}" does not exist`) + else + sendLog('stdout', `${label}: ${timeLabels[label]} ms`) + } + + console.timeEnd = (label = 'default') => { + const end = performance.now() + timeEnd(label) + const start = timeLabels[label] + if (!(label in timeLabels)) { + sendLog('stderr', `Timer "${label}" does not exist`) + } + else if (start) { + const duration = end - start + sendLog('stdout', `${label}: ${duration} ms`) + } + } + + const countLabels: Record = {} + + console.count = (label = 'default') => { + const counter = (countLabels[label] ?? 0) + 1 + countLabels[label] = counter + sendLog('stdout', `${label}: ${counter}`) + return count(label) + } + + console.countReset = (label = 'default') => { + countLabels[label] = 0 + return countReset(label) } } diff --git a/packages/browser/src/client/main.ts b/packages/browser/src/client/main.ts index d66fc833569e..456ade0aa633 100644 --- a/packages/browser/src/client/main.ts +++ b/packages/browser/src/client/main.ts @@ -6,7 +6,7 @@ import type { VitestRunner } from '@vitest/runner' import { createBrowserRunner } from './runner' import { BrowserSnapshotEnvironment } from './snapshot' import { importId } from './utils' -import { interceptLog } from './logger' +import { setupConsoleLogSpy } from './logger' // @ts-expect-error mocking some node apis globalThis.process = { env: {}, argv: [], cwd: () => '/', stdout: { write: () => {} }, nextTick: cb => cb() } @@ -68,7 +68,7 @@ ws.addEventListener('open', async () => { const iFrame = document.getElementById('vitest-ui') as HTMLIFrameElement iFrame.setAttribute('src', '/__vitest__/') - await interceptLog(client) + await setupConsoleLogSpy(client) await runTests(paths, config, client) }) diff --git a/packages/utils/src/display.ts b/packages/utils/src/display.ts index 59403888944a..3b51d0a5891b 100644 --- a/packages/utils/src/display.ts +++ b/packages/utils/src/display.ts @@ -8,8 +8,12 @@ export function format(...args: any[]) { return util.format(...args) } +export function utilInspect(item: unknown, options?: util.InspectOptions) { + return util.inspect(item, options) +} + // chai utils -export function inspect(obj: unknown): string { +export function loupeInspect(obj: unknown): string { return loupe(obj, { depth: 2, truncate: 40, @@ -18,7 +22,7 @@ export function inspect(obj: unknown): string { export function objDisplay(obj: unknown) { const truncateThreshold = 40 - const str = inspect(obj) + const str = loupeInspect(obj) const type = Object.prototype.toString.call(obj) if (str.length >= truncateThreshold) { diff --git a/test/browser/test/dom.test.ts b/test/browser/test/dom.test.ts index e5320764b5e3..12fd4e0fbc33 100644 --- a/test/browser/test/dom.test.ts +++ b/test/browser/test/dom.test.ts @@ -4,9 +4,5 @@ test('render div', async () => { const div = document.createElement('div') div.textContent = 'Hello World' document.body.appendChild(div) - // TODO: test console - // eslint-disable-next-line no-console - console.log('hello world', div) - console.error('error world') expect(div.textContent).toBe('Hello World') }) From c92848654253947cc2e16359f6a7ce9266b72e02 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Tue, 21 Mar 2023 13:17:22 +0100 Subject: [PATCH 06/10] chore: cleanup --- packages/browser/src/client/logger.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/browser/src/client/logger.ts b/packages/browser/src/client/logger.ts index 9d04d7c64043..a784e10b962f 100644 --- a/packages/browser/src/client/logger.ts +++ b/packages/browser/src/client/logger.ts @@ -1,9 +1,7 @@ -/* eslint-disable no-console */ - import type { VitestClient } from '@vitest/ws-client' import { importId } from './utils' -const { Date } = globalThis +const { Date, console } = globalThis export const setupConsoleLogSpy = async (client: VitestClient) => { const { stringify, format, utilInspect } = await importId('vitest/utils') as typeof import('vitest/utils') From 3e0c6e9150d35aef7966a4517ab6720ac1cc3fd4 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Wed, 22 Mar 2023 15:18:39 +0100 Subject: [PATCH 07/10] chore: test, use rpc with a proxy --- packages/browser/src/client/logger.ts | 6 +-- packages/browser/src/client/main.ts | 6 ++- packages/browser/src/client/rpc.ts | 71 +++++++++++++++++++++++++ packages/browser/src/client/runner.ts | 5 +- packages/browser/src/client/snapshot.ts | 11 ++-- packages/runner/src/utils/collect.ts | 3 +- packages/vitest/src/runtime/rpc.ts | 2 +- test/browser/package.json | 3 +- test/browser/specs/runner.test.mjs | 51 ++++++++++++++++++ test/browser/test.mjs | 32 ----------- test/browser/test/logs.test.ts | 54 +++++++++++++++++++ test/browser/vitest.config.ts | 1 + 12 files changed, 198 insertions(+), 47 deletions(-) create mode 100644 packages/browser/src/client/rpc.ts create mode 100644 test/browser/specs/runner.test.mjs delete mode 100644 test/browser/test.mjs create mode 100644 test/browser/test/logs.test.ts diff --git a/packages/browser/src/client/logger.ts b/packages/browser/src/client/logger.ts index a784e10b962f..55f64953808f 100644 --- a/packages/browser/src/client/logger.ts +++ b/packages/browser/src/client/logger.ts @@ -1,9 +1,9 @@ -import type { VitestClient } from '@vitest/ws-client' +import { rpc } from './rpc' import { importId } from './utils' const { Date, console } = globalThis -export const setupConsoleLogSpy = async (client: VitestClient) => { +export const setupConsoleLogSpy = async () => { const { stringify, format, utilInspect } = await importId('vitest/utils') as typeof import('vitest/utils') const { log, info, error, dir, dirxml, trace, time, timeEnd, timeLog, warn, debug, count, countReset } = console const formatInput = (input: unknown) => { @@ -18,7 +18,7 @@ export const setupConsoleLogSpy = async (client: VitestClient) => { const unknownTestId = '__vitest__unknown_test__' // @ts-expect-error untyped global const taskId = globalThis.__vitest_worker__?.current?.id ?? unknownTestId - client.rpc.sendLog({ + rpc().sendLog({ content, time: Date.now(), taskId, diff --git a/packages/browser/src/client/main.ts b/packages/browser/src/client/main.ts index 456ade0aa633..96bc4f1c1f21 100644 --- a/packages/browser/src/client/main.ts +++ b/packages/browser/src/client/main.ts @@ -7,6 +7,7 @@ import { createBrowserRunner } from './runner' import { BrowserSnapshotEnvironment } from './snapshot' import { importId } from './utils' import { setupConsoleLogSpy } from './logger' +import { rpc, rpcDone } from './rpc' // @ts-expect-error mocking some node apis globalThis.process = { env: {}, argv: [], cwd: () => '/', stdout: { write: () => {} }, nextTick: cb => cb() } @@ -68,7 +69,7 @@ ws.addEventListener('open', async () => { const iFrame = document.getElementById('vitest-ui') as HTMLIFrameElement iFrame.setAttribute('src', '/__vitest__/') - await setupConsoleLogSpy(client) + await setupConsoleLogSpy() await runTests(paths, config, client) }) @@ -103,6 +104,7 @@ async function runTests(paths: string[], config: any, client: VitestClient) { await startTests(files, runner) } finally { - await client.rpc.onDone(testId) + await rpcDone() + await rpc().onDone(testId) } } diff --git a/packages/browser/src/client/rpc.ts b/packages/browser/src/client/rpc.ts new file mode 100644 index 000000000000..2607bf9dc542 --- /dev/null +++ b/packages/browser/src/client/rpc.ts @@ -0,0 +1,71 @@ +import { + getSafeTimers, +} from '@vitest/utils' +import type { VitestClient } from '@vitest/ws-client' + +const { get } = Reflect +const safeRandom = Math.random + +function withSafeTimers(fn: () => void) { + const { setTimeout, clearTimeout, nextTick, setImmediate, clearImmediate } = getSafeTimers() + + const currentSetTimeout = globalThis.setTimeout + const currentClearTimeout = globalThis.clearTimeout + const currentRandom = globalThis.Math.random + const currentNextTick = globalThis.process.nextTick + const currentSetImmediate = globalThis.setImmediate + const currentClearImmediate = globalThis.clearImmediate + + try { + globalThis.setTimeout = setTimeout + globalThis.clearTimeout = clearTimeout + globalThis.Math.random = safeRandom + globalThis.process.nextTick = nextTick + globalThis.setImmediate = setImmediate + globalThis.clearImmediate = clearImmediate + + const result = fn() + return result + } + finally { + globalThis.setTimeout = currentSetTimeout + globalThis.clearTimeout = currentClearTimeout + globalThis.Math.random = currentRandom + globalThis.setImmediate = currentSetImmediate + globalThis.clearImmediate = currentClearImmediate + nextTick(() => { + globalThis.process.nextTick = currentNextTick + }) + } +} + +const promises = new Set>() + +export const rpcDone = async () => { + if (!promises.size) + return + const awaitable = Array.from(promises) + return Promise.all(awaitable) +} + +export const rpc = (): VitestClient['rpc'] => { + // @ts-expect-error not typed global + const { rpc } = globalThis.__vitest_worker__ + return new Proxy(rpc, { + get(target, p, handler) { + const sendCall = get(target, p, handler) + const safeSendCall = (...args: any[]) => withSafeTimers(async () => { + const result = sendCall(...args) + promises.add(result) + try { + return await result + } + finally { + promises.delete(result) + } + }) + safeSendCall.asEvent = sendCall.asEvent + return safeSendCall + }, + }) +} diff --git a/packages/browser/src/client/runner.ts b/packages/browser/src/client/runner.ts index db6a72af9bc4..1e84b081c1b9 100644 --- a/packages/browser/src/client/runner.ts +++ b/packages/browser/src/client/runner.ts @@ -1,5 +1,6 @@ import type { File, TaskResult, Test } from '@vitest/runner' import type { VitestClient } from '@vitest/ws-client' +import { rpc } from './rpc' import type { ResolvedConfig } from '#types' interface BrowserRunnerOptions { @@ -29,11 +30,11 @@ export function createBrowserRunner(original: any) { } onCollected(files: File[]): unknown { - return this.client.rpc.onCollected(files) + return rpc().onCollected(files) } onTaskUpdate(task: [string, TaskResult | undefined][]): Promise { - return this.client.rpc.onTaskUpdate(task) + return rpc().onTaskUpdate(task) } async importFile(filepath: string) { diff --git a/packages/browser/src/client/snapshot.ts b/packages/browser/src/client/snapshot.ts index d7b26a666364..391ffde1c56a 100644 --- a/packages/browser/src/client/snapshot.ts +++ b/packages/browser/src/client/snapshot.ts @@ -1,26 +1,27 @@ import type { VitestClient } from '@vitest/ws-client' +import { rpc } from './rpc' import type { SnapshotEnvironment } from '#types' export class BrowserSnapshotEnvironment implements SnapshotEnvironment { constructor(private client: VitestClient) {} readSnapshotFile(filepath: string): Promise { - return this.client.rpc.readFile(filepath) + return rpc().readFile(filepath) } saveSnapshotFile(filepath: string, snapshot: string): Promise { - return this.client.rpc.writeFile(filepath, snapshot) + return rpc().writeFile(filepath, snapshot) } resolvePath(filepath: string): Promise { - return this.client.rpc.resolveSnapshotPath(filepath) + return rpc().resolveSnapshotPath(filepath) } removeSnapshotFile(filepath: string): Promise { - return this.client.rpc.removeFile(filepath) + return rpc().removeFile(filepath) } async prepareDirectory(filepath: string): Promise { - await this.client.rpc.createDirectory(filepath) + await rpc().createDirectory(filepath) } } diff --git a/packages/runner/src/utils/collect.ts b/packages/runner/src/utils/collect.ts index 753797b0a0b2..013faa8db3ad 100644 --- a/packages/runner/src/utils/collect.ts +++ b/packages/runner/src/utils/collect.ts @@ -1,4 +1,5 @@ import type { Suite, TaskBase } from '../types' +import { processError } from './error' /** * If any tasks been marked as `only`, mark all other tasks as `skip`. @@ -65,7 +66,7 @@ function skipAllTasks(suite: Suite) { function checkAllowOnly(task: TaskBase, allowOnly?: boolean) { if (allowOnly) return - const error = new Error('[Vitest] Unexpected .only modifier. Remove it or pass --allowOnly argument to bypass this error') + const error = processError(new Error('[Vitest] Unexpected .only modifier. Remove it or pass --allowOnly argument to bypass this error')) task.result = { state: 'fail', error, diff --git a/packages/vitest/src/runtime/rpc.ts b/packages/vitest/src/runtime/rpc.ts index 7c4ea63ff253..f0dccd917cae 100644 --- a/packages/vitest/src/runtime/rpc.ts +++ b/packages/vitest/src/runtime/rpc.ts @@ -1,7 +1,7 @@ import { getSafeTimers, } from '@vitest/utils' -import { getWorkerState } from '../utils' +import { getWorkerState } from '../utils/global' const { get } = Reflect const safeRandom = Math.random diff --git a/test/browser/package.json b/test/browser/package.json index d9bf628eb1d3..40e29bfb08d3 100644 --- a/test/browser/package.json +++ b/test/browser/package.json @@ -1,8 +1,9 @@ { "name": "@vitest/test-browser", "private": true, + "module": "true", "scripts": { - "test": "node test.mjs", + "test": "node --test specs/", "coverage": "vitest run --coverage" }, "devDependencies": { diff --git a/test/browser/specs/runner.test.mjs b/test/browser/specs/runner.test.mjs new file mode 100644 index 000000000000..b320eeb64340 --- /dev/null +++ b/test/browser/specs/runner.test.mjs @@ -0,0 +1,51 @@ +import assert from 'node:assert' +import { readFile } from 'node:fs/promises' +import test from 'node:test' +import { execa } from 'execa' + +const browser = process.env.BROWSER || 'chrome' + +const { stderr, stdout } = await execa('npx', ['vitest', `--browser=${browser}`], { + env: { + ...process.env, + CI: 'true', + NO_COLOR: 'true', + }, +}) + +test('tests are actually running', async () => { + const browserResult = await readFile('./browser.json', 'utf-8') + const browserResultJson = JSON.parse(browserResult) + + assert.ok(browserResultJson.testResults.length === 5, 'Not all the tests have been run') + + for (const result of browserResultJson.testResults) + assert.ok(result.status === 'passed', `${result.name} has failed`) +}) + +test('logs are redirected to stdout', async () => { + assert.match(stdout, /stdout | test\/logs.test.ts > logging to stdout/) + assert.match(stdout, /hello from console.log/, 'prints console.log') + assert.match(stdout, /hello from console.info/, 'prints console.info') + assert.match(stdout, /hello from console.debug/, 'prints console.debug') + assert.match(stdout, /{ hello: 'from dir' }/, 'prints console.dir') + assert.match(stdout, /{ hello: 'from dirxml' }/, 'prints console.dixml') + assert.match(stdout, /hello from console.trace\s+at/, 'prints console.trace') + assert.match(stdout, /dom
/, 'prints dom') + assert.match(stdout, /default: 1/, 'prints first default count') + assert.match(stdout, /default: 2/, 'prints second default count') + assert.match(stdout, /default: 3/, 'prints third default count') + assert.match(stdout, /count: 1/, 'prints first custom count') + assert.match(stdout, /count: 2/, 'prints second custom count') + assert.match(stdout, /count: 3/, 'prints third custom count') + assert.match(stdout, /default: [\d.]+ ms/, 'prints default time') + assert.match(stdout, /time: [\d.]+ ms/, 'prints custom time') +}) + +test('logs are redirected to stderr', async () => { + assert.match(stderr, /stderr | test\/logs.test.ts > logging to stderr/) + assert.match(stderr, /hello from console.error/, 'prints console.log') + assert.match(stderr, /hello from console.warn/, 'prints console.info') + assert.match(stderr, /Timer "invalid timeLog" does not exist/, 'prints errored timeLog') + assert.match(stderr, /Timer "invalid timeEnd" does not exist/, 'prints errored timeEnd') +}) diff --git a/test/browser/test.mjs b/test/browser/test.mjs deleted file mode 100644 index ee9e7d82786d..000000000000 --- a/test/browser/test.mjs +++ /dev/null @@ -1,32 +0,0 @@ -import assert from 'node:assert' -import { readFile } from 'node:fs/promises' -import { execa } from 'execa' - -const browser = process.env.BROWSER || 'chrome' - -let error -await execa('npx', ['vitest', `--browser=${browser}`], { - env: { - ...process.env, - CI: 'true', - NO_COLOR: 'true', - }, - stdout: 'inherit', - stderr: 'inherit', -}) - .catch((e) => { - error = e - }) - -if (error) { - console.error(error) - process.exit(1) -} - -const browserResult = await readFile('./browser.json', 'utf-8') -const browserResultJson = JSON.parse(browserResult) - -assert.ok(browserResultJson.testResults.length === 4, 'Not all the tests have been run') - -for (const result of browserResultJson.testResults) - assert.ok(result.status === 'passed') diff --git a/test/browser/test/logs.test.ts b/test/browser/test/logs.test.ts new file mode 100644 index 000000000000..c1377092b872 --- /dev/null +++ b/test/browser/test/logs.test.ts @@ -0,0 +1,54 @@ +/* eslint-disable no-console */ +import { test } from 'vitest' + +test('logging to stdout', () => { + console.log('hello from console.log') + console.info('hello from console.info') + console.debug('hello from console.debug') + console.dir({ hello: 'from dir' }) + console.dirxml({ hello: 'from dirxml' }) + console.trace('hello from console.trace') +}) + +test('logging to stderr', () => { + console.error('hello from console.error') + console.warn('hello from console.warn') +}) + +test('logging DOM element', () => { + const element = document.createElement('div') + console.log('dom', element) +}) + +test('logging default counter', () => { + console.count() + console.count() + console.count() + console.countReset() + console.count() +}) + +test('logging custom counter', () => { + console.count('count') + console.count('count') + console.count('count') + console.countReset('count') + console.count('count') +}) + +test('logging default time', () => { + console.time() + console.timeLog() + console.timeEnd() +}) + +test('logging custom time', () => { + console.time('time') + console.timeLog('time') + console.timeEnd('time') +}) + +test('logging invalid time', () => { + console.timeLog('invalid timeLog') + console.timeEnd('invalid timeEnd') +}) diff --git a/test/browser/vitest.config.ts b/test/browser/vitest.config.ts index 938b1a1a7885..a1378d68dfb2 100644 --- a/test/browser/vitest.config.ts +++ b/test/browser/vitest.config.ts @@ -4,6 +4,7 @@ const noop = () => {} export default defineConfig({ test: { + include: ['test/**.test.{ts,js}'], browser: { enabled: true, name: 'chrome', From f60366d539378a7be12c069e4cbb9ed47d1211cf Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Wed, 22 Mar 2023 15:29:18 +0100 Subject: [PATCH 08/10] refactor: create a single safe rpc --- packages/browser/src/client/main.ts | 15 +++++++++------ packages/browser/src/client/rpc.ts | 19 +++++++++++-------- packages/browser/src/client/runner.ts | 4 ---- packages/browser/src/client/snapshot.ts | 3 --- 4 files changed, 20 insertions(+), 21 deletions(-) diff --git a/packages/browser/src/client/main.ts b/packages/browser/src/client/main.ts index 96bc4f1c1f21..398f4bcde674 100644 --- a/packages/browser/src/client/main.ts +++ b/packages/browser/src/client/main.ts @@ -1,4 +1,3 @@ -import type { VitestClient } from '@vitest/ws-client' import { createClient } from '@vitest/ws-client' // eslint-disable-next-line no-restricted-imports import type { ResolvedConfig } from 'vitest' @@ -7,7 +6,7 @@ import { createBrowserRunner } from './runner' import { BrowserSnapshotEnvironment } from './snapshot' import { importId } from './utils' import { setupConsoleLogSpy } from './logger' -import { rpc, rpcDone } from './rpc' +import { createSafeRpc, rpc, rpcDone } from './rpc' // @ts-expect-error mocking some node apis globalThis.process = { env: {}, argv: [], cwd: () => '/', stdout: { write: () => {} }, nextTick: cb => cb() } @@ -54,12 +53,16 @@ async function loadConfig() { ws.addEventListener('open', async () => { await loadConfig() + const { getSafeTimers } = await importId('vitest/utils') as typeof import('vitest/utils') + const safeRpc = createSafeRpc(client, getSafeTimers) + // @ts-expect-error mocking vitest apis globalThis.__vitest_worker__ = { config, browserHashMap, moduleCache: new Map(), rpc: client.rpc, + safeRpc, } // @ts-expect-error mocking vitest apis @@ -70,11 +73,11 @@ ws.addEventListener('open', async () => { iFrame.setAttribute('src', '/__vitest__/') await setupConsoleLogSpy() - await runTests(paths, config, client) + await runTests(paths, config) }) let hasSnapshot = false -async function runTests(paths: string[], config: any, client: VitestClient) { +async function runTests(paths: string[], config: any) { // need to import it before any other import, otherwise Vite optimizer will hang const viteClientPath = '/@vite/client' await import(viteClientPath) @@ -84,11 +87,11 @@ async function runTests(paths: string[], config: any, client: VitestClient) { if (!runner) { const { VitestTestRunner } = await importId('vitest/runners') as typeof import('vitest/runners') const BrowserRunner = createBrowserRunner(VitestTestRunner) - runner = new BrowserRunner({ config, client, browserHashMap }) + runner = new BrowserRunner({ config, browserHashMap }) } if (!hasSnapshot) { - setupSnapshotEnvironment(new BrowserSnapshotEnvironment(client)) + setupSnapshotEnvironment(new BrowserSnapshotEnvironment()) hasSnapshot = true } diff --git a/packages/browser/src/client/rpc.ts b/packages/browser/src/client/rpc.ts index 2607bf9dc542..af1b6e7790ec 100644 --- a/packages/browser/src/client/rpc.ts +++ b/packages/browser/src/client/rpc.ts @@ -1,4 +1,4 @@ -import { +import type { getSafeTimers, } from '@vitest/utils' import type { VitestClient } from '@vitest/ws-client' @@ -6,8 +6,8 @@ import type { VitestClient } from '@vitest/ws-client' const { get } = Reflect const safeRandom = Math.random -function withSafeTimers(fn: () => void) { - const { setTimeout, clearTimeout, nextTick, setImmediate, clearImmediate } = getSafeTimers() +function withSafeTimers(getTimers: typeof getSafeTimers, fn: () => void) { + const { setTimeout, clearTimeout, nextTick, setImmediate, clearImmediate } = getTimers() const currentSetTimeout = globalThis.setTimeout const currentClearTimeout = globalThis.clearTimeout @@ -48,13 +48,11 @@ export const rpcDone = async () => { return Promise.all(awaitable) } -export const rpc = (): VitestClient['rpc'] => { - // @ts-expect-error not typed global - const { rpc } = globalThis.__vitest_worker__ - return new Proxy(rpc, { +export const createSafeRpc = (client: VitestClient, getTimers: () => any): VitestClient['rpc'] => { + return new Proxy(client.rpc, { get(target, p, handler) { const sendCall = get(target, p, handler) - const safeSendCall = (...args: any[]) => withSafeTimers(async () => { + const safeSendCall = (...args: any[]) => withSafeTimers(getTimers, async () => { const result = sendCall(...args) promises.add(result) try { @@ -69,3 +67,8 @@ export const rpc = (): VitestClient['rpc'] => { }, }) } + +export const rpc = (): VitestClient['rpc'] => { + // @ts-expect-error not typed global + return globalThis.__vitest_worker__.safeRpc +} diff --git a/packages/browser/src/client/runner.ts b/packages/browser/src/client/runner.ts index 1e84b081c1b9..3e3a3a536e76 100644 --- a/packages/browser/src/client/runner.ts +++ b/packages/browser/src/client/runner.ts @@ -1,11 +1,9 @@ import type { File, TaskResult, Test } from '@vitest/runner' -import type { VitestClient } from '@vitest/ws-client' import { rpc } from './rpc' import type { ResolvedConfig } from '#types' interface BrowserRunnerOptions { config: ResolvedConfig - client: VitestClient browserHashMap: Map } @@ -13,13 +11,11 @@ export function createBrowserRunner(original: any) { return class BrowserTestRunner extends original { public config: ResolvedConfig hashMap = new Map() - client: VitestClient constructor(options: BrowserRunnerOptions) { super(options.config) this.config = options.config this.hashMap = options.browserHashMap - this.client = options.client } async onAfterRunTest(task: Test) { diff --git a/packages/browser/src/client/snapshot.ts b/packages/browser/src/client/snapshot.ts index 391ffde1c56a..ec4b810b1092 100644 --- a/packages/browser/src/client/snapshot.ts +++ b/packages/browser/src/client/snapshot.ts @@ -1,10 +1,7 @@ -import type { VitestClient } from '@vitest/ws-client' import { rpc } from './rpc' import type { SnapshotEnvironment } from '#types' export class BrowserSnapshotEnvironment implements SnapshotEnvironment { - constructor(private client: VitestClient) {} - readSnapshotFile(filepath: string): Promise { return rpc().readFile(filepath) } From 11e927db32420567f19bb6228c102354b8a6bc83 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Wed, 22 Mar 2023 16:09:15 +0100 Subject: [PATCH 09/10] chore: trace --- test/browser/specs/runner.test.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/browser/specs/runner.test.mjs b/test/browser/specs/runner.test.mjs index b320eeb64340..8c87292227b2 100644 --- a/test/browser/specs/runner.test.mjs +++ b/test/browser/specs/runner.test.mjs @@ -30,7 +30,7 @@ test('logs are redirected to stdout', async () => { assert.match(stdout, /hello from console.debug/, 'prints console.debug') assert.match(stdout, /{ hello: 'from dir' }/, 'prints console.dir') assert.match(stdout, /{ hello: 'from dirxml' }/, 'prints console.dixml') - assert.match(stdout, /hello from console.trace\s+at/, 'prints console.trace') + assert.match(stdout, /hello from console.trace\s+\w+/, 'prints console.trace') assert.match(stdout, /dom
/, 'prints dom') assert.match(stdout, /default: 1/, 'prints first default count') assert.match(stdout, /default: 2/, 'prints second default count') From 3247a1943df56d1dcd3420669ac6bb41d5adca47 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Wed, 22 Mar 2023 16:36:15 +0100 Subject: [PATCH 10/10] chore: add new lines --- packages/vitest/src/public/utils.ts | 2 +- packages/vitest/utils.d.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/vitest/src/public/utils.ts b/packages/vitest/src/public/utils.ts index 4bb19378e173..7030fbb320bf 100644 --- a/packages/vitest/src/public/utils.ts +++ b/packages/vitest/src/public/utils.ts @@ -1 +1 @@ -export * from '@vitest/utils' \ No newline at end of file +export * from '@vitest/utils' diff --git a/packages/vitest/utils.d.ts b/packages/vitest/utils.d.ts index 916f85c977e7..e3f344e48a8d 100644 --- a/packages/vitest/utils.d.ts +++ b/packages/vitest/utils.d.ts @@ -1 +1 @@ -export * from './dist/utils.js' \ No newline at end of file +export * from './dist/utils.js'