Skip to content

Commit 7ab0f4a

Browse files
authored
feat(browser): support --inspect-brk (#6434)
1 parent 8d883cf commit 7ab0f4a

File tree

11 files changed

+187
-12
lines changed

11 files changed

+187
-12
lines changed

docs/guide/debugging.md

+10-6
Original file line numberDiff line numberDiff line change
@@ -40,18 +40,19 @@ Then in the debug tab, ensure 'Debug Current Test File' is selected. You can the
4040

4141
### Browser mode
4242

43-
To debug [Vitest Browser Mode](/guide/browser/index.md), pass `--inspect` in CLI or define it in your Vitest configuration:
43+
To debug [Vitest Browser Mode](/guide/browser/index.md), pass `--inspect` or `--inspect-brk` in CLI or define it in your Vitest configuration:
4444

4545
::: code-group
4646
```bash [CLI]
47-
vitest --inspect --browser
47+
vitest --inspect-brk --browser --no-file-parallelism
4848
```
4949
```ts [vitest.config.js]
5050
import { defineConfig } from 'vitest/config'
5151

5252
export default defineConfig({
5353
test: {
54-
inspect: true,
54+
inspectBrk: true,
55+
fileParallelism: false,
5556
browser: {
5657
name: 'chromium',
5758
provider: 'playwright',
@@ -61,10 +62,10 @@ export default defineConfig({
6162
```
6263
:::
6364

64-
By default Vitest will use port `9229` as debugging port. You can overwrite it with by passing value in `inspect`:
65+
By default Vitest will use port `9229` as debugging port. You can overwrite it with by passing value in `--inspect-brk`:
6566

6667
```bash
67-
vitest --inspect=127.0.0.1:3000 --browser
68+
vitest --inspect-brk=127.0.0.1:3000 --browser --no-file-parallelism
6869
```
6970

7071
Use following [VSCode Compound configuration](https://code.visualstudio.com/docs/editor/debugging#_compound-launch-configurations) for launching Vitest and attaching debugger in the browser:
@@ -79,7 +80,7 @@ Use following [VSCode Compound configuration](https://code.visualstudio.com/docs
7980
"name": "Run Vitest Browser",
8081
"program": "${workspaceRoot}/node_modules/vitest/vitest.mjs",
8182
"console": "integratedTerminal",
82-
"args": ["--inspect", "--browser"]
83+
"args": ["--inspect-brk", "--browser", "--no-file-parallelism"]
8384
},
8485
{
8586
"type": "chrome",
@@ -120,6 +121,9 @@ vitest --inspect-brk --pool threads --poolOptions.threads.singleThread
120121

121122
# To run in a single child process
122123
vitest --inspect-brk --pool forks --poolOptions.forks.singleFork
124+
125+
# To run in browser mode
126+
vitest --inspect-brk --browser --no-file-parallelism
123127
```
124128

125129
If you are using Vitest 1.1 or higher, you can also just provide `--no-file-parallelism` flag:

packages/browser/src/node/pool.ts

+22-1
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,23 @@ export function createBrowserPool(ctx: Vitest): ProcessPool {
3737
)
3838
}
3939

40+
async function setBreakpoint(contextId: string, file: string) {
41+
if (!project.config.inspector.waitForDebugger) {
42+
return
43+
}
44+
45+
if (!provider.getCDPSession) {
46+
throw new Error('Unable to set breakpoint, CDP not supported')
47+
}
48+
49+
const session = await provider.getCDPSession(contextId)
50+
await session.send('Debugger.enable', {})
51+
await session.send('Debugger.setBreakpointByUrl', {
52+
lineNumber: 0,
53+
urlRegex: escapePathToRegexp(file),
54+
})
55+
}
56+
4057
const filesPerThread = Math.ceil(files.length / threadsCount)
4158

4259
// TODO: make it smarter,
@@ -83,7 +100,7 @@ export function createBrowserPool(ctx: Vitest): ProcessPool {
83100
const url = new URL('/', origin)
84101
url.searchParams.set('contextId', contextId)
85102
const page = provider
86-
.openPage(contextId, url.toString())
103+
.openPage(contextId, url.toString(), () => setBreakpoint(contextId, files[0]))
87104
.then(() => waitPromise)
88105
promises.push(page)
89106
}
@@ -145,3 +162,7 @@ export function createBrowserPool(ctx: Vitest): ProcessPool {
145162
collectTests: files => runWorkspaceTests('collect', files),
146163
}
147164
}
165+
166+
function escapePathToRegexp(path: string): string {
167+
return path.replace(/[/\\.?*()^${}|[\]+]/g, '\\$&')
168+
}

packages/browser/src/node/providers/playwright.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -184,8 +184,9 @@ export class PlaywrightBrowserProvider implements BrowserProvider {
184184
return page
185185
}
186186

187-
async openPage(contextId: string, url: string) {
187+
async openPage(contextId: string, url: string, beforeNavigate?: () => Promise<void>) {
188188
const browserPage = await this.openBrowserPage(contextId)
189+
await beforeNavigate?.()
189190
await browserPage.goto(url)
190191
}
191192

packages/vitest/src/node/config/resolveConfig.ts

+1-2
Original file line numberDiff line numberDiff line change
@@ -213,9 +213,8 @@ export function resolveConfig(
213213
&& resolved.poolOptions?.threads?.singleThread
214214
const isSingleFork
215215
= resolved.pool === 'forks' && resolved.poolOptions?.forks?.singleFork
216-
const isBrowser = resolved.browser.enabled
217216

218-
if (resolved.fileParallelism && !isSingleThread && !isSingleFork && !isBrowser) {
217+
if (resolved.fileParallelism && !isSingleThread && !isSingleFork) {
219218
const inspectOption = `--inspect${resolved.inspectBrk ? '-brk' : ''}`
220219
throw new Error(
221220
`You cannot use ${inspectOption} without "--no-file-parallelism", "poolOptions.threads.singleThread" or "poolOptions.forks.singleFork"`,

packages/vitest/src/node/types/browser.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ export interface BrowserProvider {
2727
beforeCommand?: (command: string, args: unknown[]) => Awaitable<void>
2828
afterCommand?: (command: string, args: unknown[]) => Awaitable<void>
2929
getCommandsContext: (contextId: string) => Record<string, unknown>
30-
openPage: (contextId: string, url: string) => Promise<void>
30+
openPage: (contextId: string, url: string, beforeNavigate?: () => Promise<void>) => Promise<void>
3131
getCDPSession?: (contextId: string) => Promise<CDPSession>
3232
close: () => Awaitable<void>
3333
// eslint-disable-next-line ts/method-signature-style -- we want to allow extended options

pnpm-lock.yaml

+3
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { expect, test } from "vitest";
2+
3+
test("sum", () => {
4+
expect(1 + 1).toBe(2)
5+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { defineConfig } from 'vitest/config';
2+
3+
export default defineConfig({
4+
server: { port: 5199 },
5+
test: {
6+
watch: false,
7+
browser: {
8+
provider: "playwright",
9+
name: "chromium",
10+
headless: true,
11+
},
12+
},
13+
});

test/browser/package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
"url": "^0.11.3",
3333
"vitest": "workspace:*",
3434
"vitest-browser-react": "^0.0.1",
35-
"webdriverio": "^8.32.2"
35+
"webdriverio": "^8.32.2",
36+
"ws": "^8.18.0"
3637
}
3738
}

test/browser/specs/inspect.test.ts

+115
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import type { InspectorNotification } from 'node:inspector'
2+
import { expect, test, vi } from 'vitest'
3+
import WebSocket from 'ws'
4+
5+
import { runVitestCli } from '../../test-utils'
6+
7+
type Message = Partial<InspectorNotification<any>>
8+
9+
const IS_PLAYWRIGHT_CHROMIUM = process.env.BROWSER === 'chromium' && process.env.PROVIDER === 'playwright'
10+
const REMOTE_DEBUG_URL = '127.0.0.1:9123'
11+
12+
test.runIf(IS_PLAYWRIGHT_CHROMIUM || !process.env.CI)('--inspect-brk stops at test file', async () => {
13+
const { vitest, waitForClose } = await runVitestCli(
14+
'--root',
15+
'fixtures/inspect',
16+
'--browser',
17+
'--no-file-parallelism',
18+
'--inspect-brk',
19+
REMOTE_DEBUG_URL,
20+
)
21+
22+
await vitest.waitForStdout(`Debugger listening on ws://${REMOTE_DEBUG_URL}`)
23+
24+
const url = await vi.waitFor(() =>
25+
fetch(`http://${REMOTE_DEBUG_URL}/json/list`)
26+
.then(response => response.json())
27+
.then(json => json[0].webSocketDebuggerUrl))
28+
29+
const { receive, send } = await createChannel(url)
30+
31+
const paused = receive('Debugger.paused')
32+
send({ method: 'Debugger.enable' })
33+
send({ method: 'Runtime.enable' })
34+
35+
await receive('Runtime.executionContextCreated')
36+
send({ method: 'Runtime.runIfWaitingForDebugger' })
37+
38+
const { params } = await paused
39+
const scriptId = params.callFrames[0].functionLocation.scriptId
40+
41+
// Verify that debugger paused on test file
42+
const { result } = await send({ method: 'Debugger.getScriptSource', params: { scriptId } })
43+
44+
expect(result.scriptSource).toContain('test("sum", () => {')
45+
expect(result.scriptSource).toContain('expect(1 + 1).toBe(2)')
46+
47+
send({ method: 'Debugger.resume' })
48+
49+
await vitest.waitForStdout('Test Files 1 passed (1)')
50+
await waitForClose()
51+
})
52+
53+
async function createChannel(url: string) {
54+
const ws = new WebSocket(url)
55+
56+
let id = 1
57+
let listeners = []
58+
59+
ws.onmessage = (message) => {
60+
const response = JSON.parse(message.data.toString())
61+
listeners.forEach(listener => listener(response))
62+
}
63+
64+
async function receive(methodOrId?: string | { id: number }): Promise<Message> {
65+
const { promise, resolve, reject } = withResolvers()
66+
listeners.push(listener)
67+
ws.onerror = reject
68+
69+
function listener(message) {
70+
const filter = typeof methodOrId === 'string' ? { method: methodOrId } : { id: methodOrId.id }
71+
72+
const methodMatch = message.method && message.method === filter.method
73+
const idMatch = message.id && message.id === filter.id
74+
75+
if (methodMatch || idMatch) {
76+
resolve(message)
77+
listeners = listeners.filter(l => l !== listener)
78+
ws.onerror = undefined
79+
}
80+
else if (!filter.id && !filter.method) {
81+
resolve(message)
82+
}
83+
}
84+
85+
return promise
86+
}
87+
88+
async function send(message: Message): Promise<any> {
89+
const currentId = id++
90+
const json = JSON.stringify({ ...message, id: currentId })
91+
92+
const receiver = receive({ id: currentId })
93+
ws.send(json)
94+
95+
return receiver
96+
}
97+
98+
await new Promise((resolve, reject) => {
99+
ws.onerror = reject
100+
ws.on('open', resolve)
101+
})
102+
103+
return { receive, send }
104+
}
105+
106+
function withResolvers() {
107+
let reject: (error: unknown) => void
108+
let resolve: (response: Message) => void
109+
110+
const promise: Promise<Message> = new Promise((...args) => {
111+
[resolve, reject] = args
112+
})
113+
114+
return { promise, resolve, reject }
115+
}

test/config/test/failures.test.ts

+13
Original file line numberDiff line numberDiff line change
@@ -61,12 +61,24 @@ test('inspect cannot be used with multi-threading', async () => {
6161
expect(stderr).toMatch('Error: You cannot use --inspect without "--no-file-parallelism", "poolOptions.threads.singleThread" or "poolOptions.forks.singleFork"')
6262
})
6363

64+
test('inspect in browser mode requires no-file-parallelism', async () => {
65+
const { stderr } = await runVitest({ inspect: true, browser: { enabled: true, name: 'chromium', provider: 'playwright' } })
66+
67+
expect(stderr).toMatch('Error: You cannot use --inspect without "--no-file-parallelism", "poolOptions.threads.singleThread" or "poolOptions.forks.singleFork"')
68+
})
69+
6470
test('inspect-brk cannot be used with multi processing', async () => {
6571
const { stderr } = await runVitest({ inspect: true, pool: 'forks', poolOptions: { forks: { singleFork: false } } })
6672

6773
expect(stderr).toMatch('Error: You cannot use --inspect without "--no-file-parallelism", "poolOptions.threads.singleThread" or "poolOptions.forks.singleFork"')
6874
})
6975

76+
test('inspect-brk in browser mode requires no-file-parallelism', async () => {
77+
const { stderr } = await runVitest({ inspectBrk: true, browser: { enabled: true, name: 'chromium', provider: 'playwright' } })
78+
79+
expect(stderr).toMatch('Error: You cannot use --inspect-brk without "--no-file-parallelism", "poolOptions.threads.singleThread" or "poolOptions.forks.singleFork"')
80+
})
81+
7082
test('inspect and --inspect-brk cannot be used when not playwright + chromium', async () => {
7183
for (const option of ['inspect', 'inspectBrk']) {
7284
const cli = `--inspect${option === 'inspectBrk' ? '-brk' : ''}`
@@ -78,6 +90,7 @@ test('inspect and --inspect-brk cannot be used when not playwright + chromium',
7890

7991
const { stderr } = await runVitest({
8092
[option]: true,
93+
fileParallelism: false,
8194
browser: {
8295
enabled: true,
8396
provider,

0 commit comments

Comments
 (0)