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

fix(browser): inline pretty-format and replace picocolors with tinyrainbow #6077

Merged
merged 13 commits into from
Jul 10, 2024
Merged
Prev Previous commit
Next Next commit
feat: improve error handling in the browser mode
  • Loading branch information
sheremet-va committed Jul 10, 2024
commit cdf122c4e532cf6d7bdbe5b8a3bd9e44bcad6e2b
3 changes: 3 additions & 0 deletions packages/browser/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@
"types": "./context.d.ts",
"default": "./context.js"
},
"./client": {
"default": "./dist/client.js"
},
"./matchers": {
"types": "./matchers.d.ts"
},
Expand Down
15 changes: 15 additions & 0 deletions packages/browser/rollup.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,21 @@ export default () =>
}),
],
},
{
input: './src/client/client.ts',
output: {
file: 'dist/client.js',
format: 'esm',
},
plugins: [
resolve({
preferBuiltins: true,
}),
esbuild({
target: 'node18',
}),
],
},
{
input: './src/client/tester/state.ts',
output: {
Expand Down
4 changes: 2 additions & 2 deletions packages/browser/src/client/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { getBrowserState } from './utils'

const PAGE_TYPE = getBrowserState().type

export const PORT = import.meta.hot ? '51204' : location.port
export const PORT = location.port
export const HOST = [location.hostname, PORT].filter(Boolean).join(':')
export const SESSION_ID
= PAGE_TYPE === 'orchestrator'
Expand Down Expand Up @@ -136,4 +136,4 @@ function createClient() {

export const client = createClient()

export { channel, waitForChannel } from './channel'
export * from './channel'
3 changes: 2 additions & 1 deletion packages/browser/src/client/orchestrator.html
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@
height: 100%;
}
</style>
<script>{__VITEST_INJECTOR__}</script>
{__VITEST_INJECTOR__}
{__VITEST_ERROR_CATCHER__}
{__VITEST_SCRIPTS__}
</head>
<body>
Expand Down
4 changes: 2 additions & 2 deletions packages/browser/src/client/orchestrator.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import type { ResolvedConfig } from 'vitest'
import { channel, client } from '@vitest/browser/client'
import { generateHash } from '@vitest/runner/utils'
import { type GlobalChannelIncomingEvent, type IframeChannelEvent, type IframeChannelIncomingEvent, globalChannel } from '@vitest/browser/client'
import { relative } from 'pathe'
import { channel, client } from './client'
import { getBrowserState, getConfig } from './utils'
import { getUiAPI } from './ui'
import { type GlobalChannelIncomingEvent, type IframeChannelEvent, type IframeChannelIncomingEvent, globalChannel } from './channel'
import { createModuleMocker } from './tester/msw'

const url = new URL(location.href)
Expand Down
81 changes: 81 additions & 0 deletions packages/browser/src/client/public/error-catcher.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { channel, client } from '/@id/@vitest/browser/client'

function on(event, listener) {
window.addEventListener(event, listener)
return () => window.removeEventListener(event, listener)
}

function serializeError(unhandledError) {
if (typeof unhandledError !== 'object' || !unhandledError) {
return {
message: String(unhandledError),
}
}

return {
name: unhandledError.name,
message: unhandledError.message,
stack: String(unhandledError.stack),
}
}

function catchWindowErrors(cb) {
let userErrorListenerCount = 0
function throwUnhandlerError(e) {
if (userErrorListenerCount === 0 && e.error != null) {
cb(e)
}
else {
console.error(e.error)
}
}
const addEventListener = window.addEventListener.bind(window)
const removeEventListener = window.removeEventListener.bind(window)
window.addEventListener('error', throwUnhandlerError)
window.addEventListener = function (...args) {
if (args[0] === 'error') {
userErrorListenerCount++
}
return addEventListener.apply(this, args)
}
window.removeEventListener = function (...args) {
if (args[0] === 'error' && userErrorListenerCount) {
userErrorListenerCount--
}
return removeEventListener.apply(this, args)
}
return function clearErrorHandlers() {
window.removeEventListener('error', throwUnhandlerError)
}
}

function registerUnexpectedErrors() {
catchWindowErrors(event =>
reportUnexpectedError('Error', event.error),
)
on('unhandledrejection', event =>
reportUnexpectedError('Unhandled Rejection', event.reason))
}

async function reportUnexpectedError(
type,
error,
) {
const processedError = serializeError(error)
await client.rpc.onUnhandledError(processedError, type)
const state = __vitest_browser_runner__

if (state.type === 'orchestrator') {
return
}

if (!state.runTests || !__vitest_worker__.current) {
channel.postMessage({
type: 'done',
filenames: state.files,
id: state.iframeId,
})
}
}

registerUnexpectedErrors()
2 changes: 1 addition & 1 deletion packages/browser/src/client/tester/context.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { Task, WorkerGlobalState } from 'vitest'
import type { BrowserRPC } from '@vitest/browser/client'
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

Expand Down
4 changes: 2 additions & 2 deletions packages/browser/src/client/tester/mocker.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { getType } from '@vitest/utils'
import { extname, join } from 'pathe'
import type { IframeChannelOutgoingEvent } from '@vitest/browser/client'
import { channel, waitForChannel } from '@vitest/browser/client'
import { getBrowserState, importId } from '../utils'
import type { IframeChannelOutgoingEvent } from '../channel'
import { channel, waitForChannel } from '../client'
import { rpc } from './rpc'

const now = Date.now
Expand Down
4 changes: 2 additions & 2 deletions packages/browser/src/client/tester/msw.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { channel } from '@vitest/browser/client'
import type {
IframeChannelEvent,
IframeMockEvent,
IframeMockingDoneEvent,
IframeUnmockEvent,
} from '../channel'
import { channel } from '../channel'
} from '@vitest/browser/client'

export function createModuleMocker() {
const mocks: Map<string, string | null | undefined> = new Map()
Expand Down
2 changes: 1 addition & 1 deletion packages/browser/src/client/tester/rpc.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { getSafeTimers } from 'vitest/utils'
import type { VitestBrowserClient } from '../client'
import type { VitestBrowserClient } from '@vitest/browser/client'

const { get } = Reflect

Expand Down
2 changes: 1 addition & 1 deletion packages/browser/src/client/tester/runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import { NodeBenchmarkRunner, VitestTestRunner } from 'vitest/runners'
import { loadDiffConfig, loadSnapshotSerializers, takeCoverageInsideWorker } from 'vitest/browser'
import { TraceMap, originalPositionFor } from 'vitest/utils'
import { page } from '@vitest/browser/context'
import { globalChannel } from '@vitest/browser/client'
import { importFs, importId } from '../utils'
import { globalChannel } from '../channel'
import { VitestBrowserSnapshotEnvironment } from './snapshot'
import { rpc } from './rpc'
import type { VitestBrowserClientMocker } from './mocker'
Expand Down
2 changes: 1 addition & 1 deletion packages/browser/src/client/tester/snapshot.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { SnapshotEnvironment } from 'vitest/snapshot'
import { type ParsedStack, TraceMap, originalPositionFor } from 'vitest/utils'
import type { VitestBrowserClient } from '../client'
import type { VitestBrowserClient } from '@vitest/browser/client'

export class VitestBrowserSnapshotEnvironment implements SnapshotEnvironment {
private sourceMaps = new Map<string, any>()
Expand Down
2 changes: 1 addition & 1 deletion packages/browser/src/client/tester/state.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { WorkerGlobalState } from 'vitest'
import { parse } from 'flatted'
import type { BrowserRPC } from '@vitest/browser/client'
import { getBrowserState } from '../utils'
import type { BrowserRPC } from '../client'

const config = getBrowserState().config
const contextId = getBrowserState().contextId
Expand Down
3 changes: 2 additions & 1 deletion packages/browser/src/client/tester/tester.html
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@
min-height: 100vh;
}
</style>
<script>{__VITEST_INJECTOR__}</script>
{__VITEST_INJECTOR__}
<script>{__VITEST_STATE__}</script>
{__VITEST_ERROR_CATCHER__}
{__VITEST_SCRIPTS__}
</head>
<body
Expand Down
14 changes: 6 additions & 8 deletions packages/browser/src/client/tester/tester.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
import { SpyModule, collectTests, setupCommonEnv, startTests } from 'vitest/browser'
import { channel, client, onCancel } from '@vitest/browser/client'
import { getBrowserState, getConfig, getWorkerState } from '../utils'
import { channel, client, onCancel } from '../client'
import { setupDialogsSpy } from './dialog'
import {
registerUnexpectedErrors,
serializeError,
} from './unhandled'
import { setupConsoleLogSpy } from './logger'
import { createSafeRpc } from './rpc'
import { browserHashMap, initiateRunner } from './runner'
Expand Down Expand Up @@ -59,8 +55,6 @@ async function prepareTestEnvironment(files: string[]) {
runner.onCancel?.(reason)
})

registerUnexpectedErrors(rpc)

return {
runner,
config,
Expand Down Expand Up @@ -92,7 +86,11 @@ async function executeTests(method: 'run' | 'collect', files: string[]) {
}
catch (error: any) {
debug('runner cannot be loaded because it threw an error', error.stack || error.message)
await client.rpc.onUnhandledError(serializeError(error), 'Preload Error')
await client.rpc.onUnhandledError({
name: error.name,
message: error.message,
stack: String(error.stack),
}, 'Preload Error')
done(files)
return
}
Expand Down
25 changes: 15 additions & 10 deletions packages/browser/src/client/tester/unhandled.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
import { processError } from 'vitest/browser'
import type { client } from '../client'
import { client } from '@vitest/browser/client'

function on(event: string, listener: (...args: any[]) => void) {
window.addEventListener(event, listener)
return () => window.removeEventListener(event, listener)
}

export function serializeError(unhandledError: any) {
function serializeError(unhandledError: any) {
if (typeof unhandledError !== 'object' || !unhandledError) {
return {
message: String(unhandledError),
}
}

return {
...unhandledError,
name: unhandledError.name,
message: unhandledError.message,
stack: String(unhandledError.stack),
Expand Down Expand Up @@ -49,19 +53,20 @@ function catchWindowErrors(cb: (e: ErrorEvent) => void) {
}
}

export function registerUnexpectedErrors(rpc: typeof client.rpc) {
function registerUnexpectedErrors() {
catchWindowErrors(event =>
reportUnexpectedError(rpc, 'Error', event.error),
reportUnexpectedError('Error', event.error),
)
on('unhandledrejection', event =>
reportUnexpectedError(rpc, 'Unhandled Rejection', event.reason))
reportUnexpectedError('Unhandled Rejection', event.reason))
}

async function reportUnexpectedError(
rpc: typeof client.rpc,
type: string,
error: any,
) {
const processedError = processError(error)
await rpc.onUnhandledError(processedError, type)
const processedError = serializeError(error)
await client.rpc.onUnhandledError(processedError, type)
}

registerUnexpectedErrors()
8 changes: 7 additions & 1 deletion packages/browser/src/client/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,13 @@ export default defineConfig({
orchestrator: resolve(__dirname, './orchestrator.html'),
tester: resolve(__dirname, './tester/tester.html'),
},
external: [/^vitest\//, 'vitest', /^msw/, '@vitest/browser/context'],
external: [
/^vitest\//,
'vitest',
/^msw/,
'@vitest/browser/context',
'@vitest/browser/client',
],
},
},
plugins: [
Expand Down
5 changes: 5 additions & 0 deletions packages/browser/src/node/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export class BrowserServer implements IBrowserServer {
public testerHtml: Promise<string> | string
public orchestratorHtml: Promise<string> | string
public injectorJs: Promise<string> | string
public errorCatcherJs: Promise<string> | string
public stateJs: Promise<string> | string

public state: BrowserServerState
Expand Down Expand Up @@ -86,6 +87,10 @@ export class BrowserServer implements IBrowserServer {
resolve(distRoot, 'client/esm-client-injector.js'),
'utf8',
).then(js => (this.injectorJs = js))
this.errorCatcherJs = readFile(
resolve(distRoot, 'client/error-catcher.js'),
'utf8',
).then(js => (this.errorCatcherJs = js))
this.stateJs = readFile(
resolve(distRoot, 'state.js'),
'utf-8',
Expand Down
6 changes: 4 additions & 2 deletions packages/browser/src/node/serverOrchestrator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,8 @@ export async function resolveOrchestrator(
.replace(
'<!-- !LOAD_METADATA! -->',
[
'<script>{__VITEST_INJECTOR__}</script>',
'{__VITEST_INJECTOR__}',
'{__VITEST_ERROR_CATCHER__}',
'{__VITEST_SCRIPTS__}',
`<script type="module" crossorigin src="${base}${jsEntry}"></script>`,
].join('\n'),
Expand All @@ -70,7 +71,8 @@ export async function resolveOrchestrator(
__VITEST_FAVICON__: server.faviconUrl,
__VITEST_TITLE__: 'Vitest Browser Runner',
__VITEST_SCRIPTS__: server.orchestratorScripts,
__VITEST_INJECTOR__: injector,
__VITEST_INJECTOR__: `<script type="module">${injector}</script>`,
__VITEST_ERROR_CATCHER__: `<script type="module">${server.errorCatcherJs}</script>`,
__VITEST_CONTEXT_ID__: JSON.stringify(contextId),
})
}
3 changes: 2 additions & 1 deletion packages/browser/src/node/serverTester.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,8 @@ export async function resolveTester(
__VITEST_FAVICON__: server.faviconUrl,
__VITEST_TITLE__: 'Vitest Browser Tester',
__VITEST_SCRIPTS__: server.testerScripts,
__VITEST_INJECTOR__: injector,
__VITEST_INJECTOR__: `<script type="module">${injector}</script>`,
__VITEST_ERROR_CATCHER__: `<script type="module">${server.errorCatcherJs}</script>`,
__VITEST_APPEND__:
`<script type="module">
__vitest_browser_runner__.runningFiles = ${tests}
Expand Down
4 changes: 3 additions & 1 deletion packages/ui/client/constants.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
export const PORT = import.meta.hot ? '51204' : location.port
// @ts-expect-error not typed global
const browserState = window.__vitest_browser_runner__
export const PORT = import.meta.hot && !browserState ? '51204' : location.port
export const HOST = [location.hostname, PORT].filter(Boolean).join(':')
export const ENTRY_URL = `${
location.protocol === 'https:' ? 'wss:' : 'ws:'
Expand Down
Loading