Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: show browser console in the terminal #3048

Merged
merged 10 commits into from
Mar 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
104 changes: 104 additions & 0 deletions packages/browser/src/client/logger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { rpc } from './rpc'
import { importId } from './utils'

const { Date, console } = globalThis

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) => {
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
rpc().sendLog({
content,
time: Date.now(),
taskId,
type,
size: content.length,
})
}
const stdout = (base: (...args: unknown[]) => void) => (...args: unknown[]) => {
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 = 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<string, number> = {}

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<string, number> = {}

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)
}
}
28 changes: 16 additions & 12 deletions packages/browser/src/client/main.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
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'
import type { VitestRunner } from '@vitest/runner'
import { createBrowserRunner } from './runner'
import { BrowserSnapshotEnvironment } from './snapshot'
import { importId } from './utils'
import { setupConsoleLogSpy } from './logger'
import { createSafeRpc, rpc, rpcDone } from './rpc'

// @ts-expect-error mocking some node apis
globalThis.process = { env: {}, argv: [], cwd: () => '/', stdout: { write: () => {} }, nextTick: cb => cb() }
Expand Down Expand Up @@ -51,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
Expand All @@ -66,29 +72,26 @@ ws.addEventListener('open', async () => {
const iFrame = document.getElementById('vitest-ui') as HTMLIFrameElement
iFrame.setAttribute('src', '/__vitest__/')

await runTests(paths, config, client)
await setupConsoleLogSpy()
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)

// 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 })
runner = new BrowserRunner({ config, browserHashMap })
}

if (!hasSnapshot) {
setupSnapshotEnvironment(new BrowserSnapshotEnvironment(client))
setupSnapshotEnvironment(new BrowserSnapshotEnvironment())
hasSnapshot = true
}

Expand All @@ -104,6 +107,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)
}
}
74 changes: 74 additions & 0 deletions packages/browser/src/client/rpc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import type {
getSafeTimers,
} from '@vitest/utils'
import type { VitestClient } from '@vitest/ws-client'

const { get } = Reflect
const safeRandom = Math.random

function withSafeTimers(getTimers: typeof getSafeTimers, fn: () => void) {
const { setTimeout, clearTimeout, nextTick, setImmediate, clearImmediate } = getTimers()

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<Promise<unknown>>()

export const rpcDone = async () => {
if (!promises.size)
return
const awaitable = Array.from(promises)
return Promise.all(awaitable)
}

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(getTimers, async () => {
const result = sendCall(...args)
promises.add(result)
try {
return await result
}
finally {
promises.delete(result)
}
})
safeSendCall.asEvent = sendCall.asEvent
return safeSendCall
},
})
}

export const rpc = (): VitestClient['rpc'] => {
// @ts-expect-error not typed global
return globalThis.__vitest_worker__.safeRpc
}
9 changes: 3 additions & 6 deletions packages/browser/src/client/runner.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,21 @@
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<string, string>
}

export function createBrowserRunner(original: any) {
return class BrowserTestRunner extends original {
public config: ResolvedConfig
hashMap = new Map<string, string>()
client: VitestClient

constructor(options: BrowserRunnerOptions) {
super(options.config)
this.config = options.config
this.hashMap = options.browserHashMap
this.client = options.client
}

async onAfterRunTest(task: Test) {
Expand All @@ -29,11 +26,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<void> {
return this.client.rpc.onTaskUpdate(task)
return rpc().onTaskUpdate(task)
}

async importFile(filepath: string) {
Expand Down
14 changes: 6 additions & 8 deletions packages/browser/src/client/snapshot.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,24 @@
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<string | null> {
return this.client.rpc.readFile(filepath)
return rpc().readFile(filepath)
}

saveSnapshotFile(filepath: string, snapshot: string): Promise<void> {
return this.client.rpc.writeFile(filepath, snapshot)
return rpc().writeFile(filepath, snapshot)
}

resolvePath(filepath: string): Promise<string> {
return this.client.rpc.resolveSnapshotPath(filepath)
return rpc().resolveSnapshotPath(filepath)
}

removeSnapshotFile(filepath: string): Promise<void> {
return this.client.rpc.removeFile(filepath)
return rpc().removeFile(filepath)
}

async prepareDirectory(filepath: string): Promise<void> {
await this.client.rpc.createDirectory(filepath)
await rpc().createDirectory(filepath)
}
}
4 changes: 4 additions & 0 deletions packages/browser/src/client/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export const importId = (id: string) => {
const name = `/@id/${id}`
return import(name)
}
25 changes: 9 additions & 16 deletions packages/browser/src/node/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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)
Expand Down
3 changes: 2 additions & 1 deletion packages/runner/src/utils/collect.ts
Original file line number Diff line number Diff line change
@@ -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`.
Expand Down Expand Up @@ -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,
Expand Down