diff --git a/packages/browser/src/node/rpc.ts b/packages/browser/src/node/rpc.ts index 24712ef21..03fe96109 100644 --- a/packages/browser/src/node/rpc.ts +++ b/packages/browser/src/node/rpc.ts @@ -15,7 +15,7 @@ import { extractSourcemapFromFile } from '@vitest/utils/source-map/node' import { createBirpc } from 'birpc' import { parse, stringify } from 'flatted' import { dirname, join, resolve } from 'pathe' -import { createDebugger, isFileLoadingAllowed, isValidApiRequest } from 'vitest/node' +import { BrowserConnectionError, createDebugger, isFileLoadingAllowed, isValidApiRequest } from 'vitest/node' import { WebSocketServer } from 'ws' const debug = createDebugger('vitest:browser:api') @@ -152,7 +152,7 @@ export function setupBrowserRpc(globalServer: ParentBrowserProject, defaultMocke } // this will reject any hanging methods if there are any rpc.$close( - new Error(`[vitest] Browser connection was closed while running tests. Was the page closed unexpectedly?`), + new BrowserConnectionError(`[vitest] Browser connection was closed while running tests. Was the page closed unexpectedly?`), ) }) }) diff --git a/packages/vitest/src/node/errors.ts b/packages/vitest/src/node/errors.ts index d4ffa49e2..c758b2295 100644 --- a/packages/vitest/src/node/errors.ts +++ b/packages/vitest/src/node/errors.ts @@ -14,6 +14,10 @@ export class GitNotFoundError extends Error { } } +export class BrowserConnectionError extends Error { + code = 'VITEST_BROWSER_CONNECTION_CLOSED' +} + export class LocationFilterFileNotFoundError extends Error { code = 'VITEST_LOCATION_FILTER_FILE_NOT_FOUND' diff --git a/packages/vitest/src/node/pools/browser.ts b/packages/vitest/src/node/pools/browser.ts index 72fac8011..ef6f5cdda 100644 --- a/packages/vitest/src/node/pools/browser.ts +++ b/packages/vitest/src/node/pools/browser.ts @@ -15,6 +15,7 @@ import { createDefer } from '@vitest/utils/helpers' import { stringify } from 'flatted' import { createDebugger } from '../../utils/debugger' import { detectCodeBlock } from '../../utils/test-helpers' +import { BrowserConnectionError } from '../errors' const debug = createDebugger('vitest:browser:pool') @@ -435,8 +436,7 @@ class BrowserPool { // if user cancels the test run manually, ignore the error and exit gracefully if ( this.project.vitest.isCancelling - && error instanceof Error - && error.message.startsWith('Browser connection was closed while running tests') + && error instanceof BrowserConnectionError ) { this.cancel() this._promise?.resolve() diff --git a/packages/vitest/src/public/node.ts b/packages/vitest/src/public/node.ts index 1799d1c41..e6f9e43eb 100644 --- a/packages/vitest/src/public/node.ts +++ b/packages/vitest/src/public/node.ts @@ -21,7 +21,7 @@ export type { } from '../node/core' export { BaseCoverageProvider } from '../node/coverage' export { createVitest } from '../node/create' -export { GitNotFoundError, FilesNotFoundError as TestsNotFoundError } from '../node/errors' +export { BrowserConnectionError, GitNotFoundError, FilesNotFoundError as TestsNotFoundError } from '../node/errors' export { Logger } from '../node/logger' export { VitestPackageInstaller } from '../node/packageInstaller' export { resolveFsAllow } from '../node/plugins/utils' diff --git a/test/browser/specs/bail-out.test.ts b/test/browser/specs/bail-out.test.ts index 291dc6b4c..e6ac816cf 100644 --- a/test/browser/specs/bail-out.test.ts +++ b/test/browser/specs/bail-out.test.ts @@ -1,5 +1,5 @@ import { expect, test } from 'vitest' -import { runBrowserTests } from './utils' +import { instances, provider, runBrowserTests, runInlineBrowserTests } from './utils' test('fails gracefully when browser crashes', async () => { const { stderr } = await runBrowserTests({ @@ -14,6 +14,42 @@ test('fails gracefully when browser crashes', async () => { ) }) +// crashing the browser after `cancelCurrentRun` rejects the pending +// `createTesters` call while `isCancelling` is set, so the run must +// exit gracefully instead of reporting an unhandled error +test.runIf(provider.name === 'playwright' && instances[0].browser !== 'webkit')( + 'exits gracefully when the browser connection is closed while cancelling', + async () => { + const crashUrl = instances[0].browser === 'firefox' ? 'about:crashcontent' : 'chrome://crash' + const { ctx, stderr } = await runInlineBrowserTests( + { + 'cancel.test.ts': ` + import { commands } from 'vitest/browser' + import { test } from 'vitest' + + test('cancels the run and crashes the browser', async () => { + await commands.cancelAndCrash() + }) + `, + }, + { + browser: { + instances: [instances[0]], + commands: { + async cancelAndCrash(context) { + context.project.vitest.cancelCurrentRun('keyboard-input') + await context.page.goto(crashUrl, { timeout: 1000 }).catch(() => {}) + }, + }, + }, + }, + ) + + expect(ctx!.state.getUnhandledErrors()).toEqual([]) + expect(stderr).not.toContain('Failed to run the test') + }, +) + test('vitest bails out when the iframe is no longer accessible', async () => { const { stderr } = await runBrowserTests({ root: './fixtures/broken-iframe', diff --git a/test/unit/test/exports.test.ts b/test/unit/test/exports.test.ts index 9fa6c6019..aad856da5 100644 --- a/test/unit/test/exports.test.ts +++ b/test/unit/test/exports.test.ts @@ -83,6 +83,7 @@ it('exports snapshot', async ({ skip, task }) => { "AgentReporter": "function", "BaseCoverageProvider": "function", "BaseSequencer": "function", + "BrowserConnectionError": "function", "DefaultReporter": "function", "DotReporter": "function", "ForksPoolWorker": "function",