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 type { Protocol } from 'devtools-protocol'
|
||||||
import { imageSize } from 'image-size'
|
import { imageSize } from 'image-size'
|
||||||
import { getCDPSessionForPage } from './cdp-session.js'
|
import { getCDPSessionForPage } from './cdp-session.js'
|
||||||
|
import { Debugger } from './debugger.js'
|
||||||
import { startPlayWriterCDPRelayServer, type RelayServer } from './extension/cdp-relay.js'
|
import { startPlayWriterCDPRelayServer, type RelayServer } from './extension/cdp-relay.js'
|
||||||
import { createFileLogger } from './create-logger.js'
|
import { createFileLogger } from './create-logger.js'
|
||||||
import type { CDPCommand } from './cdp-types.js'
|
import type { CDPCommand } from './cdp-types.js'
|
||||||
@@ -1782,7 +1783,7 @@ describe('CDP Session Tests', () => {
|
|||||||
return testCtx.browserContext
|
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 browserContext = getBrowserContext()
|
||||||
const serviceWorker = await getExtensionServiceWorker(browserContext)
|
const serviceWorker = await getExtensionServiceWorker(browserContext)
|
||||||
|
|
||||||
@@ -1801,11 +1802,15 @@ describe('CDP Session Tests', () => {
|
|||||||
|
|
||||||
const wsUrl = getCdpUrl()
|
const wsUrl = getCdpUrl()
|
||||||
const cdpSession = await getCDPSessionForPage({ page: cdpPage!, wsUrl })
|
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) => {
|
await dbg.enable()
|
||||||
cdpSession.on('Debugger.paused', (params) => {
|
|
||||||
resolve(params as Protocol.Debugger.PausedEvent)
|
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() {
|
(function testFunction() {
|
||||||
const localVar = 'hello';
|
const localVar = 'hello';
|
||||||
const numberVar = 42;
|
const numberVar = 42;
|
||||||
const objVar = { key: 'value', nested: { a: 1 } };
|
|
||||||
debugger;
|
debugger;
|
||||||
return localVar + numberVar;
|
return localVar + numberVar;
|
||||||
})()
|
})()
|
||||||
`)
|
`)
|
||||||
|
|
||||||
const pausedEvent = await Promise.race([
|
await Promise.race([
|
||||||
pausedPromise,
|
pausedPromise,
|
||||||
new Promise<never>((_, reject) => setTimeout(() => reject(new Error('Debugger.paused timeout')), 5000))
|
new Promise<never>((_, reject) => setTimeout(() => reject(new Error('Debugger.paused timeout')), 5000))
|
||||||
])
|
])
|
||||||
|
|
||||||
const stackTrace = pausedEvent.callFrames.map(frame => ({
|
expect(dbg.isPaused()).toBe(true)
|
||||||
functionName: frame.functionName || '(anonymous)',
|
|
||||||
lineNumber: frame.location.lineNumber,
|
|
||||||
columnNumber: frame.location.columnNumber,
|
|
||||||
}))
|
|
||||||
|
|
||||||
expect({
|
const location = await dbg.getLocation()
|
||||||
reason: pausedEvent.reason,
|
expect(location.callstack[0].functionName).toBe('testFunction')
|
||||||
stackTrace: stackTrace.slice(0, 3),
|
expect(location.sourceContext).toContain('debugger')
|
||||||
}).toMatchInlineSnapshot(`
|
|
||||||
|
const vars = await dbg.inspectVariables({ scope: 'local' })
|
||||||
|
expect(vars.local).toMatchInlineSnapshot(`
|
||||||
{
|
{
|
||||||
"reason": "other",
|
"localVar": "hello",
|
||||||
"stackTrace": [
|
"numberVar": 42,
|
||||||
{
|
|
||||||
"columnNumber": 16,
|
|
||||||
"functionName": "testFunction",
|
|
||||||
"lineNumber": 4,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"columnNumber": 14,
|
|
||||||
"functionName": "(anonymous)",
|
|
||||||
"lineNumber": 6,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"columnNumber": 29,
|
|
||||||
"functionName": "evaluate",
|
|
||||||
"lineNumber": 289,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
}
|
||||||
`)
|
`)
|
||||||
|
|
||||||
const topFrame = pausedEvent.callFrames[0]
|
const evalResult = await dbg.evaluate({ expression: 'localVar + " world"' })
|
||||||
const scopeChain = topFrame.scopeChain
|
expect(evalResult.value).toBe('hello world')
|
||||||
|
|
||||||
const localScope = scopeChain.find(s => s.type === 'local')
|
await dbg.resume()
|
||||||
const localVars: Record<string, unknown> = {}
|
expect(dbg.isPaused()).toBe(false)
|
||||||
|
|
||||||
if (localScope?.object.objectId) {
|
cdpSession.close()
|
||||||
const propsResult = await cdpSession.send('Runtime.getProperties', {
|
await browser.close()
|
||||||
objectId: localScope.object.objectId,
|
await page.close()
|
||||||
ownProperties: true,
|
}, 60000)
|
||||||
})
|
|
||||||
|
|
||||||
for (const prop of propsResult.result) {
|
it('should list scripts with Debugger class', async () => {
|
||||||
if (prop.value) {
|
const browserContext = getBrowserContext()
|
||||||
localVars[prop.name] = prop.value.type === 'object'
|
const serviceWorker = await getExtensionServiceWorker(browserContext)
|
||||||
? `[object ${prop.value.className || prop.value.subtype || 'Object'}]`
|
|
||||||
: prop.value.value
|
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({
|
const scripts = dbg.listScripts()
|
||||||
scopeTypes: scopeChain.map(s => s.type),
|
expect(scripts.length).toBeGreaterThan(0)
|
||||||
localVariables: localVars,
|
expect(scripts[0]).toHaveProperty('scriptId')
|
||||||
}).toMatchInlineSnapshot(`
|
expect(scripts[0]).toHaveProperty('url')
|
||||||
{
|
|
||||||
"localVariables": {
|
|
||||||
"localVar": "hello",
|
|
||||||
"numberVar": 42,
|
|
||||||
"objVar": "[object Object]",
|
|
||||||
},
|
|
||||||
"scopeTypes": [
|
|
||||||
"local",
|
|
||||||
"global",
|
|
||||||
],
|
|
||||||
}
|
|
||||||
`)
|
|
||||||
|
|
||||||
const evalResult = await cdpSession.send('Debugger.evaluateOnCallFrame', {
|
const hnScripts = dbg.listScripts({ search: 'hn' })
|
||||||
callFrameId: topFrame.callFrameId,
|
expect(hnScripts.length).toBeGreaterThan(0)
|
||||||
expression: 'localVar + " world " + numberVar',
|
|
||||||
|
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({
|
await dbg.deleteBreakpoint({ breakpointId: bpId })
|
||||||
evaluatedExpression: 'localVar + " world " + numberVar',
|
expect(dbg.listBreakpoints()).toHaveLength(0)
|
||||||
result: evalResult.result.value,
|
|
||||||
type: evalResult.result.type,
|
cdpSession.close()
|
||||||
}).toMatchInlineSnapshot(`
|
await browser.close()
|
||||||
{
|
await page.close()
|
||||||
"evaluatedExpression": "localVar + " world " + numberVar",
|
}, 60000)
|
||||||
"result": "hello world 42",
|
|
||||||
"type": "string",
|
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 pausedPromise
|
||||||
await cdpSession.send('Debugger.disable')
|
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()
|
cdpSession.close()
|
||||||
await browser.close()
|
await browser.close()
|
||||||
await page.close()
|
await page.close()
|
||||||
|
|||||||
Reference in New Issue
Block a user