[READ-ONLY] Mirror of https://github.com/vitest-dev/vitest. Next generation testing framework powered by Vite. vitest.dev
test testing-tools vite
12

Configure Feed

Select the types of activity you want to include in your feed.

feat(browser): support `--inspect-brk` (#6434)

authored by

Ari Perkkiö and committed by
GitHub
(Sep 8, 2024, 7:37 PM +0200) 7ab0f4a8 8d883cf0

+187 -12
+3
pnpm-lock.yaml
··· 1160 1160 webdriverio: 1161 1161 specifier: ^8.32.2 1162 1162 version: 8.32.2(typescript@5.5.4) 1163 + ws: 1164 + specifier: ^8.18.0 1165 + version: 8.18.0 1163 1166 1164 1167 test/cli: 1165 1168 devDependencies:
+10 -6
docs/guide/debugging.md
··· 40 40 41 41 ### Browser mode 42 42 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: 44 44 45 45 ::: code-group 46 46 ```bash [CLI] 47 - vitest --inspect --browser 47 + vitest --inspect-brk --browser --no-file-parallelism 48 48 ``` 49 49 ```ts [vitest.config.js] 50 50 import { defineConfig } from 'vitest/config' 51 51 52 52 export default defineConfig({ 53 53 test: { 54 - inspect: true, 54 + inspectBrk: true, 55 + fileParallelism: false, 55 56 browser: { 56 57 name: 'chromium', 57 58 provider: 'playwright', ··· 61 62 ``` 62 63 ::: 63 64 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`: 65 66 66 67 ```bash 67 - vitest --inspect=127.0.0.1:3000 --browser 68 + vitest --inspect-brk=127.0.0.1:3000 --browser --no-file-parallelism 68 69 ``` 69 70 70 71 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 80 "name": "Run Vitest Browser", 80 81 "program": "${workspaceRoot}/node_modules/vitest/vitest.mjs", 81 82 "console": "integratedTerminal", 82 - "args": ["--inspect", "--browser"] 83 + "args": ["--inspect-brk", "--browser", "--no-file-parallelism"] 83 84 }, 84 85 { 85 86 "type": "chrome", ··· 120 121 121 122 # To run in a single child process 122 123 vitest --inspect-brk --pool forks --poolOptions.forks.singleFork 124 + 125 + # To run in browser mode 126 + vitest --inspect-brk --browser --no-file-parallelism 123 127 ``` 124 128 125 129 If you are using Vitest 1.1 or higher, you can also just provide `--no-file-parallelism` flag:
+2 -1
test/browser/package.json
··· 32 32 "url": "^0.11.3", 33 33 "vitest": "workspace:*", 34 34 "vitest-browser-react": "^0.0.1", 35 - "webdriverio": "^8.32.2" 35 + "webdriverio": "^8.32.2", 36 + "ws": "^8.18.0" 36 37 } 37 38 }
+115
test/browser/specs/inspect.test.ts
··· 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 + }
+13
test/config/test/failures.test.ts
··· 61 61 expect(stderr).toMatch('Error: You cannot use --inspect without "--no-file-parallelism", "poolOptions.threads.singleThread" or "poolOptions.forks.singleFork"') 62 62 }) 63 63 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 + 64 70 test('inspect-brk cannot be used with multi processing', async () => { 65 71 const { stderr } = await runVitest({ inspect: true, pool: 'forks', poolOptions: { forks: { singleFork: false } } }) 66 72 67 73 expect(stderr).toMatch('Error: You cannot use --inspect without "--no-file-parallelism", "poolOptions.threads.singleThread" or "poolOptions.forks.singleFork"') 74 + }) 75 + 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"') 68 80 }) 69 81 70 82 test('inspect and --inspect-brk cannot be used when not playwright + chromium', async () => { ··· 78 90 79 91 const { stderr } = await runVitest({ 80 92 [option]: true, 93 + fileParallelism: false, 81 94 browser: { 82 95 enabled: true, 83 96 provider,
+22 -1
packages/browser/src/node/pool.ts
··· 37 37 ) 38 38 } 39 39 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 + 40 57 const filesPerThread = Math.ceil(files.length / threadsCount) 41 58 42 59 // TODO: make it smarter, ··· 83 100 const url = new URL('/', origin) 84 101 url.searchParams.set('contextId', contextId) 85 102 const page = provider 86 - .openPage(contextId, url.toString()) 103 + .openPage(contextId, url.toString(), () => setBreakpoint(contextId, files[0])) 87 104 .then(() => waitPromise) 88 105 promises.push(page) 89 106 } ··· 144 161 runTests: files => runWorkspaceTests('run', files), 145 162 collectTests: files => runWorkspaceTests('collect', files), 146 163 } 164 + } 165 + 166 + function escapePathToRegexp(path: string): string { 167 + return path.replace(/[/\\.?*()^${}|[\]+]/g, '\\$&') 147 168 }
+5
test/browser/fixtures/inspect/math.test.ts
··· 1 + import { expect, test } from "vitest"; 2 + 3 + test("sum", () => { 4 + expect(1 + 1).toBe(2) 5 + })
+13
test/browser/fixtures/inspect/vitest.config.ts
··· 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 + });
+2 -1
packages/browser/src/node/providers/playwright.ts
··· 184 184 return page 185 185 } 186 186 187 - async openPage(contextId: string, url: string) { 187 + async openPage(contextId: string, url: string, beforeNavigate?: () => Promise<void>) { 188 188 const browserPage = await this.openBrowserPage(contextId) 189 + await beforeNavigate?.() 189 190 await browserPage.goto(url) 190 191 } 191 192
+1 -2
packages/vitest/src/node/config/resolveConfig.ts
··· 213 213 && resolved.poolOptions?.threads?.singleThread 214 214 const isSingleFork 215 215 = resolved.pool === 'forks' && resolved.poolOptions?.forks?.singleFork 216 - const isBrowser = resolved.browser.enabled 217 216 218 - if (resolved.fileParallelism && !isSingleThread && !isSingleFork && !isBrowser) { 217 + if (resolved.fileParallelism && !isSingleThread && !isSingleFork) { 219 218 const inspectOption = `--inspect${resolved.inspectBrk ? '-brk' : ''}` 220 219 throw new Error( 221 220 `You cannot use ${inspectOption} without "--no-file-parallelism", "poolOptions.threads.singleThread" or "poolOptions.forks.singleFork"`,
+1 -1
packages/vitest/src/node/types/browser.ts
··· 27 27 beforeCommand?: (command: string, args: unknown[]) => Awaitable<void> 28 28 afterCommand?: (command: string, args: unknown[]) => Awaitable<void> 29 29 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> 31 31 getCDPSession?: (contextId: string) => Promise<CDPSession> 32 32 close: () => Awaitable<void> 33 33 // eslint-disable-next-line ts/method-signature-style -- we want to allow extended options