Add Debugger class tests using CDP session directly
## Debugger Tests Summary
### 1. should use Debugger class to set breakpoints and inspect variables
Tests the core debugging workflow when paused at a debugger statement:
- Creates a Debugger instance with a CDP session
- Verifies isPaused() returns false initially
- Executes code with a debugger statement that pauses execution
- Verifies isPaused() returns true when paused
- Calls getLocation() and verifies:
- callstack[0].functionName is 'testFunction'
- sourceContext contains the debugger keyword
- Calls inspectVariables({ scope: 'local' }) and verifies local variables:
{ localVar: "hello", numberVar: 42 }
- Calls evaluate({ expression }) in paused context and verifies it can access local variables
- Calls resume() and verifies isPaused() returns false
### 2. should list scripts with Debugger class
Tests script discovery after page load:
- Navigates to news.ycombinator.com (has external JS files)
- Enables debugger, reloads page to trigger Debugger.scriptParsed events
- Calls listScripts() and verifies:
- Returns array with scriptId and url properties
- At least one script exists
- Calls listScripts({ search: 'hn' }) and verifies filtering works
### 3. should manage breakpoints with Debugger class
Tests breakpoint CRUD operations:
- Verifies listBreakpoints() returns empty array initially
- Calls setBreakpoint({ file, line }) and verifies:
- Returns a breakpoint ID string
- listBreakpoints() now has 1 entry with correct id, file, line
- Calls deleteBreakpoint({ breakpointId }) and verifies:
- listBreakpoints() returns empty array again
### 4. should step through code with Debugger class
Tests stepping methods and call stack navigation:
- Executes nested functions (outer calls inner) with debugger in inner
- When paused, verifies getLocation().callstack:
- Has at least 2 frames
- Frame 0 is inner, frame 1 is outer
- Calls stepOver() and verifies line number advances
- Calls stepOut() and verifies:
- Now in outer function (stepped out of inner)
- Calls resume() to finish
This commit is contained in:
+188
-81
@@ -11,6 +11,7 @@ import type { ExtensionState } from 'mcp-extension/src/types.js'
|
||||
import type { Protocol } from 'devtools-protocol'
|
||||
import { imageSize } from 'image-size'
|
||||
import { getCDPSessionForPage } from './cdp-session.js'
|
||||
import { Debugger } from './debugger.js'
|
||||
import { startPlayWriterCDPRelayServer, type RelayServer } from './extension/cdp-relay.js'
|
||||
import { createFileLogger } from './create-logger.js'
|
||||
import type { CDPCommand } from './cdp-types.js'
|
||||
@@ -1782,7 +1783,7 @@ describe('CDP Session Tests', () => {
|
||||
return testCtx.browserContext
|
||||
}
|
||||
|
||||
it('should enable debugger and pause on debugger statement via CDP session', async () => {
|
||||
it('should use Debugger class to set breakpoints and inspect variables', async () => {
|
||||
const browserContext = getBrowserContext()
|
||||
const serviceWorker = await getExtensionServiceWorker(browserContext)
|
||||
|
||||
@@ -1801,11 +1802,15 @@ describe('CDP Session Tests', () => {
|
||||
|
||||
const wsUrl = getCdpUrl()
|
||||
const cdpSession = await getCDPSessionForPage({ page: cdpPage!, wsUrl })
|
||||
await cdpSession.send('Debugger.enable')
|
||||
const dbg = new Debugger({ cdp: cdpSession })
|
||||
|
||||
const pausedPromise = new Promise<Protocol.Debugger.PausedEvent>((resolve) => {
|
||||
cdpSession.on('Debugger.paused', (params) => {
|
||||
resolve(params as Protocol.Debugger.PausedEvent)
|
||||
await dbg.enable()
|
||||
|
||||
expect(dbg.isPaused()).toBe(false)
|
||||
|
||||
const pausedPromise = new Promise<void>((resolve) => {
|
||||
cdpSession.on('Debugger.paused', () => {
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1813,106 +1818,208 @@ describe('CDP Session Tests', () => {
|
||||
(function testFunction() {
|
||||
const localVar = 'hello';
|
||||
const numberVar = 42;
|
||||
const objVar = { key: 'value', nested: { a: 1 } };
|
||||
debugger;
|
||||
return localVar + numberVar;
|
||||
})()
|
||||
`)
|
||||
|
||||
const pausedEvent = await Promise.race([
|
||||
await Promise.race([
|
||||
pausedPromise,
|
||||
new Promise<never>((_, reject) => setTimeout(() => reject(new Error('Debugger.paused timeout')), 5000))
|
||||
])
|
||||
|
||||
const stackTrace = pausedEvent.callFrames.map(frame => ({
|
||||
functionName: frame.functionName || '(anonymous)',
|
||||
lineNumber: frame.location.lineNumber,
|
||||
columnNumber: frame.location.columnNumber,
|
||||
}))
|
||||
expect(dbg.isPaused()).toBe(true)
|
||||
|
||||
expect({
|
||||
reason: pausedEvent.reason,
|
||||
stackTrace: stackTrace.slice(0, 3),
|
||||
}).toMatchInlineSnapshot(`
|
||||
const location = await dbg.getLocation()
|
||||
expect(location.callstack[0].functionName).toBe('testFunction')
|
||||
expect(location.sourceContext).toContain('debugger')
|
||||
|
||||
const vars = await dbg.inspectVariables({ scope: 'local' })
|
||||
expect(vars.local).toMatchInlineSnapshot(`
|
||||
{
|
||||
"reason": "other",
|
||||
"stackTrace": [
|
||||
{
|
||||
"columnNumber": 16,
|
||||
"functionName": "testFunction",
|
||||
"lineNumber": 4,
|
||||
},
|
||||
{
|
||||
"columnNumber": 14,
|
||||
"functionName": "(anonymous)",
|
||||
"lineNumber": 6,
|
||||
},
|
||||
{
|
||||
"columnNumber": 29,
|
||||
"functionName": "evaluate",
|
||||
"lineNumber": 289,
|
||||
},
|
||||
],
|
||||
"localVar": "hello",
|
||||
"numberVar": 42,
|
||||
}
|
||||
`)
|
||||
|
||||
const topFrame = pausedEvent.callFrames[0]
|
||||
const scopeChain = topFrame.scopeChain
|
||||
const evalResult = await dbg.evaluate({ expression: 'localVar + " world"' })
|
||||
expect(evalResult.value).toBe('hello world')
|
||||
|
||||
const localScope = scopeChain.find(s => s.type === 'local')
|
||||
const localVars: Record<string, unknown> = {}
|
||||
await dbg.resume()
|
||||
expect(dbg.isPaused()).toBe(false)
|
||||
|
||||
if (localScope?.object.objectId) {
|
||||
const propsResult = await cdpSession.send('Runtime.getProperties', {
|
||||
objectId: localScope.object.objectId,
|
||||
ownProperties: true,
|
||||
})
|
||||
cdpSession.close()
|
||||
await browser.close()
|
||||
await page.close()
|
||||
}, 60000)
|
||||
|
||||
for (const prop of propsResult.result) {
|
||||
if (prop.value) {
|
||||
localVars[prop.name] = prop.value.type === 'object'
|
||||
? `[object ${prop.value.className || prop.value.subtype || 'Object'}]`
|
||||
: prop.value.value
|
||||
}
|
||||
}
|
||||
it('should list scripts with Debugger class', async () => {
|
||||
const browserContext = getBrowserContext()
|
||||
const serviceWorker = await getExtensionServiceWorker(browserContext)
|
||||
|
||||
const page = await browserContext.newPage()
|
||||
await page.goto('https://news.ycombinator.com/')
|
||||
await page.bringToFront()
|
||||
|
||||
await serviceWorker.evaluate(async () => {
|
||||
await globalThis.toggleExtensionForActiveTab()
|
||||
})
|
||||
await new Promise(r => setTimeout(r, 500))
|
||||
|
||||
const browser = await chromium.connectOverCDP(getCdpUrl())
|
||||
const cdpPage = browser.contexts()[0].pages().find(p => p.url().includes('news.ycombinator.com'))
|
||||
expect(cdpPage).toBeDefined()
|
||||
|
||||
const wsUrl = getCdpUrl()
|
||||
const cdpSession = await getCDPSessionForPage({ page: cdpPage!, wsUrl })
|
||||
const dbg = new Debugger({ cdp: cdpSession })
|
||||
|
||||
await dbg.enable()
|
||||
|
||||
await cdpPage!.reload({ waitUntil: 'networkidle' })
|
||||
|
||||
for (let i = 0; i < 20; i++) {
|
||||
if (dbg.listScripts().length > 0) break
|
||||
await new Promise(r => setTimeout(r, 100))
|
||||
}
|
||||
|
||||
expect({
|
||||
scopeTypes: scopeChain.map(s => s.type),
|
||||
localVariables: localVars,
|
||||
}).toMatchInlineSnapshot(`
|
||||
{
|
||||
"localVariables": {
|
||||
"localVar": "hello",
|
||||
"numberVar": 42,
|
||||
"objVar": "[object Object]",
|
||||
},
|
||||
"scopeTypes": [
|
||||
"local",
|
||||
"global",
|
||||
],
|
||||
}
|
||||
`)
|
||||
const scripts = dbg.listScripts()
|
||||
expect(scripts.length).toBeGreaterThan(0)
|
||||
expect(scripts[0]).toHaveProperty('scriptId')
|
||||
expect(scripts[0]).toHaveProperty('url')
|
||||
|
||||
const evalResult = await cdpSession.send('Debugger.evaluateOnCallFrame', {
|
||||
callFrameId: topFrame.callFrameId,
|
||||
expression: 'localVar + " world " + numberVar',
|
||||
const hnScripts = dbg.listScripts({ search: 'hn' })
|
||||
expect(hnScripts.length).toBeGreaterThan(0)
|
||||
|
||||
cdpSession.close()
|
||||
await browser.close()
|
||||
await page.close()
|
||||
}, 60000)
|
||||
|
||||
it('should manage breakpoints with Debugger class', async () => {
|
||||
const browserContext = getBrowserContext()
|
||||
const serviceWorker = await getExtensionServiceWorker(browserContext)
|
||||
|
||||
const page = await browserContext.newPage()
|
||||
await page.setContent(`
|
||||
<html>
|
||||
<head>
|
||||
<script src="data:text/javascript,function testFunc() { return 42; }"></script>
|
||||
</head>
|
||||
<body></body>
|
||||
</html>
|
||||
`)
|
||||
await page.bringToFront()
|
||||
|
||||
await serviceWorker.evaluate(async () => {
|
||||
await globalThis.toggleExtensionForActiveTab()
|
||||
})
|
||||
await new Promise(r => setTimeout(r, 500))
|
||||
|
||||
const browser = await chromium.connectOverCDP(getCdpUrl())
|
||||
let cdpPage
|
||||
for (const p of browser.contexts()[0].pages()) {
|
||||
const html = await p.content()
|
||||
if (html.includes('testFunc')) {
|
||||
cdpPage = p
|
||||
break
|
||||
}
|
||||
}
|
||||
expect(cdpPage).toBeDefined()
|
||||
|
||||
const wsUrl = getCdpUrl()
|
||||
const cdpSession = await getCDPSessionForPage({ page: cdpPage!, wsUrl })
|
||||
const dbg = new Debugger({ cdp: cdpSession })
|
||||
|
||||
await dbg.enable()
|
||||
|
||||
expect(dbg.listBreakpoints()).toHaveLength(0)
|
||||
|
||||
const bpId = await dbg.setBreakpoint({ file: 'https://example.com/test.js', line: 1 })
|
||||
expect(typeof bpId).toBe('string')
|
||||
expect(dbg.listBreakpoints()).toHaveLength(1)
|
||||
expect(dbg.listBreakpoints()[0]).toMatchObject({
|
||||
id: bpId,
|
||||
file: 'https://example.com/test.js',
|
||||
line: 1,
|
||||
})
|
||||
|
||||
expect({
|
||||
evaluatedExpression: 'localVar + " world " + numberVar',
|
||||
result: evalResult.result.value,
|
||||
type: evalResult.result.type,
|
||||
}).toMatchInlineSnapshot(`
|
||||
{
|
||||
"evaluatedExpression": "localVar + " world " + numberVar",
|
||||
"result": "hello world 42",
|
||||
"type": "string",
|
||||
}
|
||||
await dbg.deleteBreakpoint({ breakpointId: bpId })
|
||||
expect(dbg.listBreakpoints()).toHaveLength(0)
|
||||
|
||||
cdpSession.close()
|
||||
await browser.close()
|
||||
await page.close()
|
||||
}, 60000)
|
||||
|
||||
it('should step through code with Debugger class', async () => {
|
||||
const browserContext = getBrowserContext()
|
||||
const serviceWorker = await getExtensionServiceWorker(browserContext)
|
||||
|
||||
const page = await browserContext.newPage()
|
||||
await page.goto('https://example.com/')
|
||||
await page.bringToFront()
|
||||
|
||||
await serviceWorker.evaluate(async () => {
|
||||
await globalThis.toggleExtensionForActiveTab()
|
||||
})
|
||||
await new Promise(r => setTimeout(r, 500))
|
||||
|
||||
const browser = await chromium.connectOverCDP(getCdpUrl())
|
||||
const cdpPage = browser.contexts()[0].pages().find(p => p.url().includes('example.com'))
|
||||
expect(cdpPage).toBeDefined()
|
||||
|
||||
const wsUrl = getCdpUrl()
|
||||
const cdpSession = await getCDPSessionForPage({ page: cdpPage!, wsUrl })
|
||||
const dbg = new Debugger({ cdp: cdpSession })
|
||||
|
||||
await dbg.enable()
|
||||
|
||||
const pausedPromise = new Promise<void>((resolve) => {
|
||||
cdpSession.on('Debugger.paused', () => resolve())
|
||||
})
|
||||
|
||||
cdpPage!.evaluate(`
|
||||
(function outer() {
|
||||
function inner() {
|
||||
const x = 1;
|
||||
debugger;
|
||||
const y = 2;
|
||||
return x + y;
|
||||
}
|
||||
const result = inner();
|
||||
return result;
|
||||
})()
|
||||
`)
|
||||
|
||||
await cdpSession.send('Debugger.resume')
|
||||
await cdpSession.send('Debugger.disable')
|
||||
await pausedPromise
|
||||
expect(dbg.isPaused()).toBe(true)
|
||||
|
||||
const location1 = await dbg.getLocation()
|
||||
expect(location1.callstack.length).toBeGreaterThanOrEqual(2)
|
||||
expect(location1.callstack[0].functionName).toBe('inner')
|
||||
expect(location1.callstack[1].functionName).toBe('outer')
|
||||
|
||||
const stepOverPromise = new Promise<void>((resolve) => {
|
||||
cdpSession.on('Debugger.paused', () => resolve())
|
||||
})
|
||||
await dbg.stepOver()
|
||||
await stepOverPromise
|
||||
|
||||
const location2 = await dbg.getLocation()
|
||||
expect(location2.lineNumber).toBeGreaterThan(location1.lineNumber)
|
||||
|
||||
const stepOutPromise = new Promise<void>((resolve) => {
|
||||
cdpSession.on('Debugger.paused', () => resolve())
|
||||
})
|
||||
await dbg.stepOut()
|
||||
await stepOutPromise
|
||||
|
||||
const location3 = await dbg.getLocation()
|
||||
expect(location3.callstack[0].functionName).toBe('outer')
|
||||
|
||||
await dbg.resume()
|
||||
|
||||
cdpSession.close()
|
||||
await browser.close()
|
||||
await page.close()
|
||||
|
||||
Reference in New Issue
Block a user