nicer debugger api
This commit is contained in:
@@ -26,7 +26,7 @@ async function inspectWhenPaused() {
|
|||||||
console.log('Paused at:', loc.url, 'line', loc.lineNumber)
|
console.log('Paused at:', loc.url, 'line', loc.lineNumber)
|
||||||
console.log('Source:', loc.sourceContext)
|
console.log('Source:', loc.sourceContext)
|
||||||
|
|
||||||
const vars = await dbg.inspectVariables({ scope: 'local' })
|
const vars = await dbg.inspectLocalVariables()
|
||||||
console.log('Variables:', vars)
|
console.log('Variables:', vars)
|
||||||
|
|
||||||
const result = await dbg.evaluate({ expression: 'myVar.length' })
|
const result = await dbg.evaluate({ expression: 'myVar.length' })
|
||||||
|
|||||||
+66
-46
@@ -24,9 +24,7 @@ export interface EvaluateResult {
|
|||||||
value: unknown
|
value: unknown
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface VariablesResult {
|
|
||||||
[scope: string]: Record<string, unknown>
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ScriptInfo {
|
export interface ScriptInfo {
|
||||||
scriptId: string
|
scriptId: string
|
||||||
@@ -45,7 +43,7 @@ export interface ScriptInfo {
|
|||||||
* await dbg.setBreakpoint({ file: 'https://example.com/app.js', line: 42 })
|
* await dbg.setBreakpoint({ file: 'https://example.com/app.js', line: 42 })
|
||||||
* // trigger the code path, then:
|
* // trigger the code path, then:
|
||||||
* const location = await dbg.getLocation()
|
* const location = await dbg.getLocation()
|
||||||
* const vars = await dbg.inspectVariables()
|
* const vars = await dbg.inspectLocalVariables()
|
||||||
* await dbg.resume()
|
* await dbg.resume()
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
@@ -133,14 +131,9 @@ export class Debugger {
|
|||||||
async setBreakpoint({ file, line }: { file: string; line: number }): Promise<string> {
|
async setBreakpoint({ file, line }: { file: string; line: number }): Promise<string> {
|
||||||
await this.enable()
|
await this.enable()
|
||||||
|
|
||||||
let fileUrl = file
|
|
||||||
if (!file.startsWith('file://') && !file.startsWith('http://') && !file.startsWith('https://')) {
|
|
||||||
fileUrl = `file://${file.startsWith('/') ? '' : '/'}${file}`
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await this.cdp.send('Debugger.setBreakpointByUrl', {
|
const response = await this.cdp.send('Debugger.setBreakpointByUrl', {
|
||||||
lineNumber: line - 1,
|
lineNumber: line - 1,
|
||||||
urlRegex: fileUrl.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'),
|
urlRegex: file.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'),
|
||||||
columnNumber: 0,
|
columnNumber: 0,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -181,47 +174,28 @@ export class Debugger {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Inspects variables in the current scope.
|
* Inspects local variables in the current call frame.
|
||||||
* When paused at a breakpoint with scope='local', returns variables from the call frame.
|
* Must be paused at a breakpoint. String values over 1000 chars are truncated.
|
||||||
* Otherwise returns global scope information.
|
* Use evaluate() for full control over reading specific values.
|
||||||
*
|
*
|
||||||
* @param options - Options
|
* @returns Record of variable names to values
|
||||||
* @param options.scope - 'local' for call frame variables, 'global' for global scope
|
* @throws Error if not paused or no active call frames
|
||||||
* @returns Variables grouped by scope type
|
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* ```ts
|
* ```ts
|
||||||
* // When paused at a breakpoint:
|
* const vars = await dbg.inspectLocalVariables()
|
||||||
* const vars = await dbg.inspectVariables({ scope: 'local' })
|
* // { myVar: 'hello', count: 42 }
|
||||||
* // { local: { myVar: 'hello', count: 42 }, closure: { captured: true } }
|
|
||||||
*
|
|
||||||
* // Global scope:
|
|
||||||
* const globals = await dbg.inspectVariables({ scope: 'global' })
|
|
||||||
* // { lexicalNames: [...], globalThis: { ... } }
|
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
async inspectVariables({ scope = 'local' }: { scope?: 'local' | 'global' } = {}): Promise<VariablesResult> {
|
async inspectLocalVariables(): Promise<Record<string, unknown>> {
|
||||||
await this.enable()
|
await this.enable()
|
||||||
|
|
||||||
if (scope === 'global' || !this.paused) {
|
if (!this.paused || this.currentCallFrames.length === 0) {
|
||||||
const response = await this.cdp.send('Runtime.globalLexicalScopeNames', {})
|
throw new Error('Debugger is not paused at a breakpoint')
|
||||||
const globalObjResponse = await this.cdp.send('Runtime.evaluate', {
|
|
||||||
expression: 'this',
|
|
||||||
returnByValue: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
|
||||||
lexicalNames: response.names as unknown as Record<string, unknown>,
|
|
||||||
globalThis: globalObjResponse.result.value as Record<string, unknown>,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.currentCallFrames.length === 0) {
|
|
||||||
throw new Error('No active call frames')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const frame = this.currentCallFrames[0]
|
const frame = this.currentCallFrames[0]
|
||||||
const result: VariablesResult = {}
|
const result: Record<string, unknown> = {}
|
||||||
|
|
||||||
for (const scopeObj of frame.scopeChain) {
|
for (const scopeObj of frame.scopeChain) {
|
||||||
if (scopeObj.type === 'global') {
|
if (scopeObj.type === 'global') {
|
||||||
@@ -239,19 +213,35 @@ export class Debugger {
|
|||||||
generatePreview: true,
|
generatePreview: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
const variables: Record<string, unknown> = {}
|
|
||||||
for (const prop of objProperties.result) {
|
for (const prop of objProperties.result) {
|
||||||
if (prop.value && prop.configurable) {
|
if (prop.value && prop.configurable) {
|
||||||
variables[prop.name] = this.formatPropertyValue(prop.value)
|
result[prop.name] = this.formatPropertyValue(prop.value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
result[scopeObj.type] = variables
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns global lexical scope variable names.
|
||||||
|
*
|
||||||
|
* @returns Array of global variable names
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* const globals = await dbg.inspectGlobalVariables()
|
||||||
|
* // ['myGlobal', 'CONFIG']
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
async inspectGlobalVariables(): Promise<string[]> {
|
||||||
|
await this.enable()
|
||||||
|
|
||||||
|
const response = await this.cdp.send('Runtime.globalLexicalScopeNames', {})
|
||||||
|
|
||||||
|
return response.names
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Evaluates a JavaScript expression in the current context.
|
* Evaluates a JavaScript expression in the current context.
|
||||||
* When paused at a breakpoint, evaluates in the call frame context (can access local variables).
|
* When paused at a breakpoint, evaluates in the call frame context (can access local variables).
|
||||||
@@ -459,7 +449,7 @@ export class Debugger {
|
|||||||
* @example
|
* @example
|
||||||
* ```ts
|
* ```ts
|
||||||
* if (dbg.isPaused()) {
|
* if (dbg.isPaused()) {
|
||||||
* const vars = await dbg.inspectVariables()
|
* const vars = await dbg.inspectLocalVariables()
|
||||||
* }
|
* }
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
@@ -467,6 +457,29 @@ export class Debugger {
|
|||||||
return this.paused
|
return this.paused
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configures the debugger to pause on exceptions.
|
||||||
|
*
|
||||||
|
* @param options - Options
|
||||||
|
* @param options.state - When to pause: 'none' (never), 'uncaught' (only uncaught), or 'all' (all exceptions)
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* // Pause only on uncaught exceptions
|
||||||
|
* await dbg.setPauseOnExceptions({ state: 'uncaught' })
|
||||||
|
*
|
||||||
|
* // Pause on all exceptions (caught and uncaught)
|
||||||
|
* await dbg.setPauseOnExceptions({ state: 'all' })
|
||||||
|
*
|
||||||
|
* // Disable pausing on exceptions
|
||||||
|
* await dbg.setPauseOnExceptions({ state: 'none' })
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
async setPauseOnExceptions({ state }: { state: 'none' | 'uncaught' | 'all' }): Promise<void> {
|
||||||
|
await this.enable()
|
||||||
|
await this.cdp.send('Debugger.setPauseOnExceptions', { state })
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Lists available scripts where breakpoints can be set.
|
* Lists available scripts where breakpoints can be set.
|
||||||
* Scripts are collected from Debugger.scriptParsed events after enable() is called.
|
* Scripts are collected from Debugger.scriptParsed events after enable() is called.
|
||||||
@@ -495,6 +508,13 @@ export class Debugger {
|
|||||||
return filtered.slice(0, 20)
|
return filtered.slice(0, 20)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private truncateValue(value: unknown): unknown {
|
||||||
|
if (typeof value === 'string' && value.length > 1000) {
|
||||||
|
return value.slice(0, 1000) + `... (${value.length} chars)`
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
private formatPropertyValue(value: Protocol.Runtime.RemoteObject): unknown {
|
private formatPropertyValue(value: Protocol.Runtime.RemoteObject): unknown {
|
||||||
if (value.type === 'object' && value.subtype !== 'null') {
|
if (value.type === 'object' && value.subtype !== 'null') {
|
||||||
return `[${value.subtype || value.type}]`
|
return `[${value.subtype || value.type}]`
|
||||||
@@ -503,7 +523,7 @@ export class Debugger {
|
|||||||
return '[function]'
|
return '[function]'
|
||||||
}
|
}
|
||||||
if (value.value !== undefined) {
|
if (value.value !== undefined) {
|
||||||
return value.value
|
return this.truncateValue(value.value)
|
||||||
}
|
}
|
||||||
return `[${value.type}]`
|
return `[${value.type}]`
|
||||||
}
|
}
|
||||||
|
|||||||
+144
-4
@@ -1834,8 +1834,8 @@ describe('CDP Session Tests', () => {
|
|||||||
expect(location.callstack[0].functionName).toBe('testFunction')
|
expect(location.callstack[0].functionName).toBe('testFunction')
|
||||||
expect(location.sourceContext).toContain('debugger')
|
expect(location.sourceContext).toContain('debugger')
|
||||||
|
|
||||||
const vars = await dbg.inspectVariables({ scope: 'local' })
|
const vars = await dbg.inspectLocalVariables()
|
||||||
expect(vars.local).toMatchInlineSnapshot(`
|
expect(vars).toMatchInlineSnapshot(`
|
||||||
{
|
{
|
||||||
"localVar": "hello",
|
"localVar": "hello",
|
||||||
"numberVar": 42,
|
"numberVar": 42,
|
||||||
@@ -2077,9 +2077,9 @@ describe('CDP Session Tests', () => {
|
|||||||
sampleFunctionNames: functionNames,
|
sampleFunctionNames: functionNames,
|
||||||
}).toMatchInlineSnapshot(`
|
}).toMatchInlineSnapshot(`
|
||||||
{
|
{
|
||||||
"durationMicroseconds": 11251,
|
"durationMicroseconds": 10698,
|
||||||
"hasNodes": true,
|
"hasNodes": true,
|
||||||
"nodeCount": 18,
|
"nodeCount": 19,
|
||||||
"sampleFunctionNames": [
|
"sampleFunctionNames": [
|
||||||
"(root)",
|
"(root)",
|
||||||
"(program)",
|
"(program)",
|
||||||
@@ -2288,6 +2288,146 @@ describe('CDP Session Tests', () => {
|
|||||||
await page.close()
|
await page.close()
|
||||||
}, 60000)
|
}, 60000)
|
||||||
|
|
||||||
|
it('should pause on all exceptions with setPauseOnExceptions', 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()
|
||||||
|
await dbg.setPauseOnExceptions({ state: 'all' })
|
||||||
|
|
||||||
|
const pausedPromise = new Promise<void>((resolve) => {
|
||||||
|
cdpSession.on('Debugger.paused', () => resolve())
|
||||||
|
})
|
||||||
|
|
||||||
|
cdpPage!.evaluate(`
|
||||||
|
(function() {
|
||||||
|
try {
|
||||||
|
throw new Error('Caught test error');
|
||||||
|
} catch (e) {
|
||||||
|
// caught but should still pause with state 'all'
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
`).catch(() => {})
|
||||||
|
|
||||||
|
await Promise.race([
|
||||||
|
pausedPromise,
|
||||||
|
new Promise<never>((_, reject) => setTimeout(() => reject(new Error('Debugger.paused timeout')), 5000))
|
||||||
|
])
|
||||||
|
|
||||||
|
expect(dbg.isPaused()).toBe(true)
|
||||||
|
|
||||||
|
const location = await dbg.getLocation()
|
||||||
|
expect(location.sourceContext).toContain('throw')
|
||||||
|
|
||||||
|
await dbg.resume()
|
||||||
|
|
||||||
|
await dbg.setPauseOnExceptions({ state: 'none' })
|
||||||
|
|
||||||
|
cdpSession.close()
|
||||||
|
await browser.close()
|
||||||
|
await page.close()
|
||||||
|
}, 60000)
|
||||||
|
|
||||||
|
it('should inspect local and global variables with inline snapshots', async () => {
|
||||||
|
const browserContext = getBrowserContext()
|
||||||
|
const serviceWorker = await getExtensionServiceWorker(browserContext)
|
||||||
|
|
||||||
|
const page = await browserContext.newPage()
|
||||||
|
await page.setContent(`
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<script>
|
||||||
|
const GLOBAL_CONFIG = 'production';
|
||||||
|
function runTest() {
|
||||||
|
const userName = 'Alice';
|
||||||
|
const userAge = 25;
|
||||||
|
const settings = { theme: 'dark', lang: 'en' };
|
||||||
|
const scores = [10, 20, 30];
|
||||||
|
debugger;
|
||||||
|
return userName;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<button onclick="runTest()">Run</button>
|
||||||
|
</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('runTest')) {
|
||||||
|
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()
|
||||||
|
|
||||||
|
const globalVars = await dbg.inspectGlobalVariables()
|
||||||
|
expect(globalVars).toMatchInlineSnapshot(`
|
||||||
|
[
|
||||||
|
"GLOBAL_CONFIG",
|
||||||
|
]
|
||||||
|
`)
|
||||||
|
|
||||||
|
const pausedPromise = new Promise<void>((resolve) => {
|
||||||
|
cdpSession.on('Debugger.paused', () => resolve())
|
||||||
|
})
|
||||||
|
|
||||||
|
cdpPage!.evaluate('runTest()')
|
||||||
|
|
||||||
|
await pausedPromise
|
||||||
|
expect(dbg.isPaused()).toBe(true)
|
||||||
|
|
||||||
|
const localVars = await dbg.inspectLocalVariables()
|
||||||
|
expect(localVars).toMatchInlineSnapshot(`
|
||||||
|
{
|
||||||
|
"GLOBAL_CONFIG": "production",
|
||||||
|
"scores": "[array]",
|
||||||
|
"settings": "[object]",
|
||||||
|
"userAge": 25,
|
||||||
|
"userName": "Alice",
|
||||||
|
}
|
||||||
|
`)
|
||||||
|
|
||||||
|
await dbg.resume()
|
||||||
|
|
||||||
|
cdpSession.close()
|
||||||
|
await browser.close()
|
||||||
|
await page.close()
|
||||||
|
}, 60000)
|
||||||
|
|
||||||
it('should click at correct coordinates on high-DPI simulation', async () => {
|
it('should click at correct coordinates on high-DPI simulation', async () => {
|
||||||
const browserContext = getBrowserContext()
|
const browserContext = getBrowserContext()
|
||||||
const serviceWorker = await getExtensionServiceWorker(browserContext)
|
const serviceWorker = await getExtensionServiceWorker(browserContext)
|
||||||
|
|||||||
Reference in New Issue
Block a user