Skip to content

Commit

Permalink
feat(browser): support --inspect-brk (#6434)
Browse files Browse the repository at this point in the history
  • Loading branch information
AriPerkkio authored Sep 8, 2024
1 parent 8d883cf commit 7ab0f4a
Show file tree
Hide file tree
Showing 11 changed files with 187 additions and 12 deletions.
16 changes: 10 additions & 6 deletions docs/guide/debugging.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,18 +40,19 @@ Then in the debug tab, ensure 'Debug Current Test File' is selected. You can the

### Browser mode

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

::: code-group
```bash [CLI]
vitest --inspect --browser
vitest --inspect-brk --browser --no-file-parallelism
```
```ts [vitest.config.js]
import { defineConfig } from 'vitest/config'

export default defineConfig({
test: {
inspect: true,
inspectBrk: true,
fileParallelism: false,
browser: {
name: 'chromium',
provider: 'playwright',
Expand All @@ -61,10 +62,10 @@ export default defineConfig({
```
:::

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

```bash
vitest --inspect=127.0.0.1:3000 --browser
vitest --inspect-brk=127.0.0.1:3000 --browser --no-file-parallelism
```

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

# To run in a single child process
vitest --inspect-brk --pool forks --poolOptions.forks.singleFork

# To run in browser mode
vitest --inspect-brk --browser --no-file-parallelism
```

If you are using Vitest 1.1 or higher, you can also just provide `--no-file-parallelism` flag:
Expand Down
23 changes: 22 additions & 1 deletion packages/browser/src/node/pool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,23 @@ export function createBrowserPool(ctx: Vitest): ProcessPool {
)
}

async function setBreakpoint(contextId: string, file: string) {
if (!project.config.inspector.waitForDebugger) {
return
}

if (!provider.getCDPSession) {
throw new Error('Unable to set breakpoint, CDP not supported')
}

const session = await provider.getCDPSession(contextId)
await session.send('Debugger.enable', {})
await session.send('Debugger.setBreakpointByUrl', {
lineNumber: 0,
urlRegex: escapePathToRegexp(file),
})
}

const filesPerThread = Math.ceil(files.length / threadsCount)

// TODO: make it smarter,
Expand Down Expand Up @@ -83,7 +100,7 @@ export function createBrowserPool(ctx: Vitest): ProcessPool {
const url = new URL('/', origin)
url.searchParams.set('contextId', contextId)
const page = provider
.openPage(contextId, url.toString())
.openPage(contextId, url.toString(), () => setBreakpoint(contextId, files[0]))
.then(() => waitPromise)
promises.push(page)
}
Expand Down Expand Up @@ -145,3 +162,7 @@ export function createBrowserPool(ctx: Vitest): ProcessPool {
collectTests: files => runWorkspaceTests('collect', files),
}
}

function escapePathToRegexp(path: string): string {
return path.replace(/[/\\.?*()^${}|[\]+]/g, '\\$&')
}
3 changes: 2 additions & 1 deletion packages/browser/src/node/providers/playwright.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,8 +184,9 @@ export class PlaywrightBrowserProvider implements BrowserProvider {
return page
}

async openPage(contextId: string, url: string) {
async openPage(contextId: string, url: string, beforeNavigate?: () => Promise<void>) {
const browserPage = await this.openBrowserPage(contextId)
await beforeNavigate?.()
await browserPage.goto(url)
}

Expand Down
3 changes: 1 addition & 2 deletions packages/vitest/src/node/config/resolveConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,9 +213,8 @@ export function resolveConfig(
&& resolved.poolOptions?.threads?.singleThread
const isSingleFork
= resolved.pool === 'forks' && resolved.poolOptions?.forks?.singleFork
const isBrowser = resolved.browser.enabled

if (resolved.fileParallelism && !isSingleThread && !isSingleFork && !isBrowser) {
if (resolved.fileParallelism && !isSingleThread && !isSingleFork) {
const inspectOption = `--inspect${resolved.inspectBrk ? '-brk' : ''}`
throw new Error(
`You cannot use ${inspectOption} without "--no-file-parallelism", "poolOptions.threads.singleThread" or "poolOptions.forks.singleFork"`,
Expand Down
2 changes: 1 addition & 1 deletion packages/vitest/src/node/types/browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export interface BrowserProvider {
beforeCommand?: (command: string, args: unknown[]) => Awaitable<void>
afterCommand?: (command: string, args: unknown[]) => Awaitable<void>
getCommandsContext: (contextId: string) => Record<string, unknown>
openPage: (contextId: string, url: string) => Promise<void>
openPage: (contextId: string, url: string, beforeNavigate?: () => Promise<void>) => Promise<void>
getCDPSession?: (contextId: string) => Promise<CDPSession>
close: () => Awaitable<void>
// eslint-disable-next-line ts/method-signature-style -- we want to allow extended options
Expand Down
3 changes: 3 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions test/browser/fixtures/inspect/math.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { expect, test } from "vitest";

test("sum", () => {
expect(1 + 1).toBe(2)
})
13 changes: 13 additions & 0 deletions test/browser/fixtures/inspect/vitest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { defineConfig } from 'vitest/config';

export default defineConfig({
server: { port: 5199 },
test: {
watch: false,
browser: {
provider: "playwright",
name: "chromium",
headless: true,
},
},
});
3 changes: 2 additions & 1 deletion test/browser/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"url": "^0.11.3",
"vitest": "workspace:*",
"vitest-browser-react": "^0.0.1",
"webdriverio": "^8.32.2"
"webdriverio": "^8.32.2",
"ws": "^8.18.0"
}
}
115 changes: 115 additions & 0 deletions test/browser/specs/inspect.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import type { InspectorNotification } from 'node:inspector'
import { expect, test, vi } from 'vitest'
import WebSocket from 'ws'

import { runVitestCli } from '../../test-utils'

type Message = Partial<InspectorNotification<any>>

const IS_PLAYWRIGHT_CHROMIUM = process.env.BROWSER === 'chromium' && process.env.PROVIDER === 'playwright'
const REMOTE_DEBUG_URL = '127.0.0.1:9123'

test.runIf(IS_PLAYWRIGHT_CHROMIUM || !process.env.CI)('--inspect-brk stops at test file', async () => {
const { vitest, waitForClose } = await runVitestCli(
'--root',
'fixtures/inspect',
'--browser',
'--no-file-parallelism',
'--inspect-brk',
REMOTE_DEBUG_URL,
)

await vitest.waitForStdout(`Debugger listening on ws://${REMOTE_DEBUG_URL}`)

const url = await vi.waitFor(() =>
fetch(`http://${REMOTE_DEBUG_URL}/json/list`)
.then(response => response.json())
.then(json => json[0].webSocketDebuggerUrl))

const { receive, send } = await createChannel(url)

const paused = receive('Debugger.paused')
send({ method: 'Debugger.enable' })
send({ method: 'Runtime.enable' })

await receive('Runtime.executionContextCreated')
send({ method: 'Runtime.runIfWaitingForDebugger' })

const { params } = await paused
const scriptId = params.callFrames[0].functionLocation.scriptId

// Verify that debugger paused on test file
const { result } = await send({ method: 'Debugger.getScriptSource', params: { scriptId } })

expect(result.scriptSource).toContain('test("sum", () => {')
expect(result.scriptSource).toContain('expect(1 + 1).toBe(2)')

send({ method: 'Debugger.resume' })

await vitest.waitForStdout('Test Files 1 passed (1)')
await waitForClose()
})

async function createChannel(url: string) {
const ws = new WebSocket(url)

let id = 1
let listeners = []

ws.onmessage = (message) => {
const response = JSON.parse(message.data.toString())
listeners.forEach(listener => listener(response))
}

async function receive(methodOrId?: string | { id: number }): Promise<Message> {
const { promise, resolve, reject } = withResolvers()
listeners.push(listener)
ws.onerror = reject

function listener(message) {
const filter = typeof methodOrId === 'string' ? { method: methodOrId } : { id: methodOrId.id }

const methodMatch = message.method && message.method === filter.method
const idMatch = message.id && message.id === filter.id

if (methodMatch || idMatch) {
resolve(message)
listeners = listeners.filter(l => l !== listener)
ws.onerror = undefined
}
else if (!filter.id && !filter.method) {
resolve(message)
}
}

return promise
}

async function send(message: Message): Promise<any> {
const currentId = id++
const json = JSON.stringify({ ...message, id: currentId })

const receiver = receive({ id: currentId })
ws.send(json)

return receiver
}

await new Promise((resolve, reject) => {
ws.onerror = reject
ws.on('open', resolve)
})

return { receive, send }
}

function withResolvers() {
let reject: (error: unknown) => void
let resolve: (response: Message) => void

const promise: Promise<Message> = new Promise((...args) => {
[resolve, reject] = args
})

return { promise, resolve, reject }
}
13 changes: 13 additions & 0 deletions test/config/test/failures.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,12 +61,24 @@ test('inspect cannot be used with multi-threading', async () => {
expect(stderr).toMatch('Error: You cannot use --inspect without "--no-file-parallelism", "poolOptions.threads.singleThread" or "poolOptions.forks.singleFork"')
})

test('inspect in browser mode requires no-file-parallelism', async () => {
const { stderr } = await runVitest({ inspect: true, browser: { enabled: true, name: 'chromium', provider: 'playwright' } })

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

test('inspect-brk cannot be used with multi processing', async () => {
const { stderr } = await runVitest({ inspect: true, pool: 'forks', poolOptions: { forks: { singleFork: false } } })

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

test('inspect-brk in browser mode requires no-file-parallelism', async () => {
const { stderr } = await runVitest({ inspectBrk: true, browser: { enabled: true, name: 'chromium', provider: 'playwright' } })

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

test('inspect and --inspect-brk cannot be used when not playwright + chromium', async () => {
for (const option of ['inspect', 'inspectBrk']) {
const cli = `--inspect${option === 'inspectBrk' ? '-brk' : ''}`
Expand All @@ -78,6 +90,7 @@ test('inspect and --inspect-brk cannot be used when not playwright + chromium',

const { stderr } = await runVitest({
[option]: true,
fileParallelism: false,
browser: {
enabled: true,
provider,
Expand Down

0 comments on commit 7ab0f4a

Please sign in to comment.