| // Copyright 2026 The Chromium Authors | |
| // Use of this source code is governed by a BSD-style license that can be | |
| // found in the LICENSE file. | |
| import {assert} from 'chai'; | |
| import {describe, type InspectedPage, it, setup} from '../../../test/api/mocha-interface.js'; | |
| import * as Platform from '../../core/platform/platform.js'; | |
| import * as SDK from '../../core/sdk/sdk.js'; | |
| import * as Formatter from '../../models/formatter/formatter.js'; | |
| import {encodeSourceMap} from '../../testing/SourceMapEncoder.js'; | |
| import * as ScopesCodec from '../../third_party/source-map-scopes-codec/source-map-scopes-codec.js'; | |
| import * as SourceMapScopes from './source_map_scopes.js'; | |
| function createSourceMapUrl(builder: ScopesCodec.ScopeInfoBuilder, textMap: string[] = []): string { | |
| const baseMap = textMap.length > 0 ? encodeSourceMap(textMap) : { | |
| version: 3, | |
| sources: ['index.ts'], | |
| mappings: '', | |
| }; | |
| const map = ScopesCodec.encode(builder.build(), baseMap as ScopesCodec.SourceMapJson); | |
| const json = JSON.stringify(map); | |
| return `data:application/json;charset=utf-8;base64,${Platform.StringUtilities.toBase64(json)}`; | |
| } | |
| async function setupScriptAndPause( | |
| inspectedPage: InspectedPage, | |
| debuggerModel: SDK.DebuggerModel.DebuggerModel, | |
| code: string, | |
| builder: ScopesCodec.ScopeInfoBuilder, | |
| textMap: string[] = [], | |
| ): Promise<{callFrame: SDK.DebuggerModel.CallFrame, sourceMap: SDK.SourceMap.SourceMap}> { | |
| const sourceMapUrl = createSourceMapUrl(builder, textMap); | |
| const scriptContent = `${code}\n//# sourceURL=test.js\n//# sourceMappingURL=${sourceMapUrl}`; | |
| const pausedPromise = debuggerModel.once(SDK.DebuggerModel.Events.DebuggerPaused); | |
| await inspectedPage.evaluate((src: string) => { | |
| const script = document.createElement('script'); | |
| script.textContent = src; | |
| document.body.appendChild(script); | |
| }, scriptContent); | |
| void inspectedPage.evaluate(() => { | |
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | |
| setTimeout((window as any).runTest, 0); | |
| }); | |
| await pausedPromise; | |
| const callFrames = debuggerModel.debuggerPausedDetails()?.callFrames; | |
| assert.isDefined(callFrames, 'Expected callFrames to exist'); | |
| assert.isNotEmpty(callFrames, 'Expected callFrames not to be empty'); | |
| const callFrame = callFrames[0]; | |
| const sourceMap = await debuggerModel.sourceMapManager().sourceMapForClientPromise(callFrame.script); | |
| assert.isDefined(sourceMap, 'Expected source map to attach'); | |
| return {callFrame, sourceMap}; | |
| } | |
| function formatRemoteObject(value?: SDK.RemoteObject.RemoteObject): string { | |
| if (!value) { | |
| return '<unavailable>'; | |
| } | |
| if (value.type === 'undefined') { | |
| return 'undefined'; | |
| } | |
| if (value.type === 'object' && value.subtype !== 'null') { | |
| return `<${value.className ?? value.description ?? 'object'}>`; | |
| } | |
| return JSON.stringify(value.value); | |
| } | |
| async function stringifyScopeChain(scopeChain: SDK.DebuggerModel.ScopeChainEntry[]): Promise<string> { | |
| const lines: string[] = []; | |
| for (const scope of scopeChain) { | |
| const title = scope.name() ? `${scope.typeName()} (${scope.name()})` : scope.typeName(); | |
| if (scope.type() === 'global' && !(scope instanceof SDK.SourceMapScopeChainEntry.SourceMapScopeChainEntry)) { | |
| lines.push(`${title} (<global>)`); | |
| continue; | |
| } | |
| lines.push(title); | |
| for (const extra of scope.extraProperties()) { | |
| lines.push(` [${extra.name}]: ${formatRemoteObject(extra.value)}`); | |
| } | |
| const {properties} = await scope.object().getAllProperties(false, false); | |
| for (const prop of properties ?? []) { | |
| lines.push(` ${prop.name}: ${formatRemoteObject(prop.value)}`); | |
| } | |
| } | |
| return lines.join('\n'); | |
| } | |
| describe('NamesResolver API Test', () => { | |
| setup({ | |
| creationOptions: { | |
| hostConfig: { | |
| devToolsSourceMapScopesInSourcesPanel: { | |
| enabled: true, | |
| }, | |
| }, | |
| }, | |
| }); | |
| it('builds function, block, and global scopes with mapped variable values', async ({inspectedPage, universe}) => { | |
| const primaryTarget = universe.targetManager.primaryPageTarget(); | |
| assert.isNotNull(primaryTarget); | |
| const debuggerModel = primaryTarget.model(SDK.DebuggerModel.DebuggerModel); | |
| assert.isNotNull(debuggerModel); | |
| await inspectedPage.goToHtml('<!DOCTYPE html><html><body></body></html>'); | |
| const builder = new ScopesCodec.ScopeInfoBuilder(); | |
| builder.startSource(); | |
| builder.startScope(0, 0, {kind: 'global', variables: ['authoredGlobalVar'], key: 'global'}); | |
| builder.startScope( | |
| 0, 0, {kind: 'function', name: 'testLocalAndBlock', isStackFrame: true, variables: ['outerVar'], key: 'fn'}); | |
| builder.startScope(2, 2, {kind: 'block', variables: ['innerVar'], key: 'block'}); | |
| builder.endScope(5, 3); | |
| builder.endScope(6, 1); | |
| builder.endScope(8, 0); | |
| builder.endSource(); | |
| builder.startRange(0, 0, {scopeKey: 'global', values: ['"fromAuthoredGlobal"']}); | |
| builder.startRange(0, 0, {scopeKey: 'fn', isStackFrame: true, values: ['outerVar']}); | |
| builder.startRange(2, 2, {scopeKey: 'block', values: ['innerVar']}); | |
| builder.endRange(5, 3); | |
| builder.endRange(6, 1); | |
| builder.endRange(8, 0); | |
| const code = [ | |
| 'function testLocalAndBlock() {', | |
| ' const outerVar = 10;', | |
| ' {', | |
| ' const innerVar = 20;', | |
| ' debugger;', | |
| ' }', | |
| '}', | |
| 'window.runTest = testLocalAndBlock;', | |
| ].join('\n'); | |
| const {callFrame} = await setupScriptAndPause(inspectedPage, debuggerModel, code, builder); | |
| try { | |
| const scopeChain = | |
| await SourceMapScopes.NamesResolver.resolveScopeChain(callFrame, universe.debuggerWorkspaceBinding); | |
| assert.strictEqual(await stringifyScopeChain(scopeChain), [ | |
| 'Block', | |
| ' innerVar: 20', | |
| 'Local (testLocalAndBlock)', | |
| ' outerVar: 10', | |
| 'Global', | |
| ' authoredGlobalVar: "fromAuthoredGlobal"', | |
| 'Global (<global>)', | |
| ].join('\n')); | |
| } finally { | |
| if (debuggerModel.isPaused()) { | |
| await debuggerModel.resume(); | |
| } | |
| } | |
| }); | |
| it('evaluates shadowed variables in parent closure scope using V8 scopeNumber', async ({inspectedPage, universe}) => { | |
| const primaryTarget = universe.targetManager.primaryPageTarget(); | |
| assert.isNotNull(primaryTarget); | |
| const debuggerModel = primaryTarget.model(SDK.DebuggerModel.DebuggerModel); | |
| assert.isNotNull(debuggerModel); | |
| await inspectedPage.goToHtml('<!DOCTYPE html><html><body></body></html>'); | |
| const builder = new ScopesCodec.ScopeInfoBuilder(); | |
| builder.startSource(); | |
| builder.startScope(0, 0, {kind: 'global', key: 'global'}); | |
| builder.startScope(0, 0, | |
| {kind: 'function', name: 'testShadowing', isStackFrame: true, variables: ['x'], key: 'outerFn'}); | |
| builder.startScope(3, 2, {kind: 'function', name: 'innerFn', isStackFrame: true, variables: ['x'], key: 'innerFn'}); | |
| builder.endScope(6, 3); | |
| builder.endScope(8, 1); | |
| builder.endScope(10, 0); | |
| builder.endSource(); | |
| builder.startRange(0, 0, {scopeKey: 'global'}); | |
| builder.startRange(0, 0, {scopeKey: 'outerFn', isStackFrame: true, values: ['x']}); | |
| builder.startRange(3, 2, {scopeKey: 'innerFn', isStackFrame: true, values: ['x']}); | |
| builder.endRange(6, 3); | |
| builder.endRange(8, 1); | |
| builder.endRange(10, 0); | |
| const code = [ | |
| 'function testShadowing() {', | |
| ' const x = "outer";', | |
| ' eval("");', | |
| ' function innerFn() {', | |
| ' const x = "inner";', | |
| ' debugger;', | |
| ' }', | |
| ' innerFn();', | |
| '}', | |
| 'window.runTest = testShadowing;', | |
| ].join('\n'); | |
| const {callFrame} = await setupScriptAndPause(inspectedPage, debuggerModel, code, builder); | |
| try { | |
| const scopeChain = | |
| await SourceMapScopes.NamesResolver.resolveScopeChain(callFrame, universe.debuggerWorkspaceBinding); | |
| assert.strictEqual(await stringifyScopeChain(scopeChain), [ | |
| 'Local (innerFn)', | |
| ' x: "inner"', | |
| 'Closure (testShadowing)', | |
| ' x: "outer"', | |
| 'Global', | |
| 'Global (<global>)', | |
| ].join('\n')); | |
| } finally { | |
| if (debuggerModel.isPaused()) { | |
| await debuggerModel.resume(); | |
| } | |
| } | |
| }); | |
| it('evaluates multiple nested block scopes with variable shadowing', async ({inspectedPage, universe}) => { | |
| const primaryTarget = universe.targetManager.primaryPageTarget(); | |
| assert.isNotNull(primaryTarget); | |
| const debuggerModel = primaryTarget.model(SDK.DebuggerModel.DebuggerModel); | |
| assert.isNotNull(debuggerModel); | |
| await inspectedPage.goToHtml('<!DOCTYPE html><html><body></body></html>'); | |
| const builder = new ScopesCodec.ScopeInfoBuilder(); | |
| builder.startSource(); | |
| builder.startScope(0, 0, {kind: 'global', key: 'global'}); | |
| builder.startScope(0, 0, | |
| {kind: 'function', name: 'testNestedBlocks', isStackFrame: true, variables: ['x'], key: 'fn'}); | |
| builder.startScope(2, 2, {kind: 'block', variables: ['x'], key: 'outerBlock'}); | |
| builder.startScope(4, 4, {kind: 'block', variables: ['x'], key: 'innerBlock'}); | |
| builder.endScope(7, 5); | |
| builder.endScope(8, 3); | |
| builder.endScope(9, 1); | |
| builder.endScope(11, 0); | |
| builder.endSource(); | |
| builder.startRange(0, 0, {scopeKey: 'global'}); | |
| builder.startRange(0, 0, {scopeKey: 'fn', isStackFrame: true, values: ['x']}); | |
| builder.startRange(2, 2, {scopeKey: 'outerBlock', values: ['x']}); | |
| builder.startRange(4, 4, {scopeKey: 'innerBlock', values: ['x']}); | |
| builder.endRange(7, 5); | |
| builder.endRange(8, 3); | |
| builder.endRange(9, 1); | |
| builder.endRange(11, 0); | |
| const code = [ | |
| 'function testNestedBlocks() {', | |
| ' const x = "fn";', | |
| ' {', | |
| ' const x = "outerBlock";', | |
| ' {', | |
| ' const x = "innerBlock";', | |
| ' debugger;', | |
| ' }', | |
| ' }', | |
| '}', | |
| 'window.runTest = testNestedBlocks;', | |
| ].join('\n'); | |
| const {callFrame} = await setupScriptAndPause(inspectedPage, debuggerModel, code, builder); | |
| try { | |
| const scopeChain = | |
| await SourceMapScopes.NamesResolver.resolveScopeChain(callFrame, universe.debuggerWorkspaceBinding); | |
| assert.strictEqual(await stringifyScopeChain(scopeChain), [ | |
| 'Block', | |
| ' x: "innerBlock"', | |
| 'Block', | |
| ' x: "outerBlock"', | |
| 'Local (testNestedBlocks)', | |
| ' x: "fn"', | |
| 'Global', | |
| 'Global (<global>)', | |
| ].join('\n')); | |
| } finally { | |
| if (debuggerModel.isPaused()) { | |
| await debuggerModel.resume(); | |
| } | |
| } | |
| }); | |
| it('matches multiple nested empty scopes reported by V8 CDP', async ({inspectedPage, universe}) => { | |
| const primaryTarget = universe.targetManager.primaryPageTarget(); | |
| assert.isNotNull(primaryTarget); | |
| const debuggerModel = primaryTarget.model(SDK.DebuggerModel.DebuggerModel); | |
| assert.isNotNull(debuggerModel); | |
| await inspectedPage.goToHtml('<!DOCTYPE html><html><body></body></html>'); | |
| const builder = new ScopesCodec.ScopeInfoBuilder(); | |
| builder.startSource(); | |
| builder.startScope(0, 0, {kind: 'global', key: 'global'}); | |
| builder.startScope(0, 0, | |
| {kind: 'function', name: 'testNestedEmpty', isStackFrame: true, variables: ['val'], key: 'fn'}); | |
| builder.startScope(2, 2, {kind: 'block', variables: [], key: 'empty1'}); | |
| builder.startScope(3, 4, {kind: 'block', variables: [], key: 'empty2'}); | |
| builder.endScope(5, 5); | |
| builder.endScope(6, 3); | |
| builder.endScope(7, 1); | |
| builder.endScope(9, 0); | |
| builder.endSource(); | |
| builder.startRange(0, 0, {scopeKey: 'global'}); | |
| builder.startRange(0, 0, {scopeKey: 'fn', isStackFrame: true, values: ['val']}); | |
| builder.startRange(2, 2, {scopeKey: 'empty1', values: []}); | |
| builder.startRange(3, 4, {scopeKey: 'empty2', values: []}); | |
| builder.endRange(5, 5); | |
| builder.endRange(6, 3); | |
| builder.endRange(7, 1); | |
| builder.endRange(9, 0); | |
| const code = [ | |
| 'function testNestedEmpty() {', | |
| ' const val = 42;', | |
| ' {', | |
| ' {', | |
| ' debugger;', | |
| ' }', | |
| ' }', | |
| '}', | |
| 'window.runTest = testNestedEmpty;', | |
| ].join('\n'); | |
| const {callFrame} = await setupScriptAndPause(inspectedPage, debuggerModel, code, builder); | |
| try { | |
| const scopeChain = | |
| await SourceMapScopes.NamesResolver.resolveScopeChain(callFrame, universe.debuggerWorkspaceBinding); | |
| assert.strictEqual(await stringifyScopeChain(scopeChain), [ | |
| 'Block', | |
| 'Block', | |
| 'Local (testNestedEmpty)', | |
| ' val: 42', | |
| 'Global', | |
| 'Global (<global>)', | |
| ].join('\n')); | |
| } finally { | |
| if (debuggerModel.isPaused()) { | |
| await debuggerModel.resume(); | |
| } | |
| } | |
| }); | |
| it('resolves multi-level inlined functions and caller virtual call frames', async ({inspectedPage, universe}) => { | |
| const primaryTarget = universe.targetManager.primaryPageTarget(); | |
| assert.isNotNull(primaryTarget); | |
| const debuggerModel = primaryTarget.model(SDK.DebuggerModel.DebuggerModel); | |
| assert.isNotNull(debuggerModel); | |
| await inspectedPage.goToHtml('<!DOCTYPE html><html><body></body></html>'); | |
| const builder = new ScopesCodec.ScopeInfoBuilder(); | |
| builder.startSource(); | |
| builder.startScope(0, 0, {kind: 'global', key: 'global'}); | |
| builder.startScope(0, 0, | |
| {kind: 'function', name: 'rootCaller', isStackFrame: true, variables: ['rootVar'], key: 'root'}); | |
| builder.startScope(1, 2, | |
| {kind: 'function', name: 'inlinedMid', isStackFrame: true, variables: ['midVar'], key: 'mid'}); | |
| builder.startScope( | |
| 2, 2, {kind: 'function', name: 'inlinedLeaf', isStackFrame: true, variables: ['leafVar'], key: 'leaf'}); | |
| builder.endScope(3, 3); | |
| builder.endScope(4, 3); | |
| builder.endScope(5, 1); | |
| builder.endScope(7, 0); | |
| builder.endSource(); | |
| builder.startRange(0, 0, {scopeKey: 'global'}); | |
| builder.startRange(0, 0, {scopeKey: 'root', isStackFrame: true, values: ['rootVar']}); | |
| builder.startRange(2, 2, { | |
| scopeKey: 'mid', | |
| isStackFrame: false, | |
| callSite: {sourceIndex: 0, line: 1, column: 2}, | |
| values: ['midVar'], | |
| }); | |
| builder.startRange(3, 2, { | |
| scopeKey: 'leaf', | |
| isStackFrame: false, | |
| callSite: {sourceIndex: 0, line: 2, column: 2}, | |
| values: ['leafVar'], | |
| }); | |
| builder.endRange(4, 12); | |
| builder.endRange(4, 12); | |
| builder.endRange(5, 1); | |
| builder.endRange(7, 0); | |
| const code = [ | |
| 'function rootCaller() {', | |
| ' const rootVar = 1;', | |
| ' const midVar = 2;', | |
| ' const leafVar = 3;', | |
| ' debugger;', | |
| '}', | |
| 'window.runTest = rootCaller;', | |
| ].join('\n'); | |
| const {callFrame} = await setupScriptAndPause(inspectedPage, debuggerModel, code, builder); | |
| try { | |
| const scopeChain = | |
| await SourceMapScopes.NamesResolver.resolveScopeChain(callFrame, universe.debuggerWorkspaceBinding); | |
| assert.strictEqual(await stringifyScopeChain(scopeChain), [ | |
| 'Local (inlinedLeaf)', | |
| ' leafVar: 3', | |
| 'Closure (inlinedMid)', | |
| ' midVar: 2', | |
| 'Closure (rootCaller)', | |
| ' rootVar: 1', | |
| 'Global', | |
| 'Global (<global>)', | |
| ].join('\n')); | |
| const midFrame = callFrame.createVirtualCallFrame(1, 'inlinedMid'); | |
| const midScopeChain = | |
| await SourceMapScopes.NamesResolver.resolveScopeChain(midFrame, universe.debuggerWorkspaceBinding); | |
| assert.strictEqual(await stringifyScopeChain(midScopeChain), [ | |
| 'Local (inlinedMid)', | |
| ' midVar: 2', | |
| 'Closure (rootCaller)', | |
| ' rootVar: 1', | |
| 'Global', | |
| 'Global (<global>)', | |
| ].join('\n')); | |
| const rootFrame = callFrame.createVirtualCallFrame(2, 'rootCaller'); | |
| const rootScopeChain = | |
| await SourceMapScopes.NamesResolver.resolveScopeChain(rootFrame, universe.debuggerWorkspaceBinding); | |
| assert.strictEqual(await stringifyScopeChain(rootScopeChain), [ | |
| 'Local (rootCaller)', | |
| ' rootVar: 1', | |
| 'Global', | |
| 'Global (<global>)', | |
| ].join('\n')); | |
| } finally { | |
| if (debuggerModel.isPaused()) { | |
| await debuggerModel.resume(); | |
| } | |
| } | |
| }); | |
| it('distinguishes between variables with actual undefined value and throwing ReferenceError in V8', | |
| async ({inspectedPage, universe}) => { | |
| const primaryTarget = universe.targetManager.primaryPageTarget(); | |
| assert.isNotNull(primaryTarget); | |
| const debuggerModel = primaryTarget.model(SDK.DebuggerModel.DebuggerModel); | |
| assert.isNotNull(debuggerModel); | |
| await inspectedPage.goToHtml('<!DOCTYPE html><html><body></body></html>'); | |
| const builder = new ScopesCodec.ScopeInfoBuilder(); | |
| builder.startSource(); | |
| builder.startScope(0, 0, {kind: 'global', key: 'global'}); | |
| builder.startScope(0, 0, { | |
| kind: 'function', | |
| name: 'testUndefinedAndThrowing', | |
| isStackFrame: true, | |
| variables: ['legitUndefined', 'nonExistentVar'], | |
| key: 'fn', | |
| }); | |
| builder.endScope(4, 1); | |
| builder.endScope(6, 0); | |
| builder.endSource(); | |
| builder.startRange(0, 0, {scopeKey: 'global'}); | |
| builder.startRange(0, 0, {scopeKey: 'fn', isStackFrame: true, values: ['legitUndefined', 'nonExistentVar']}); | |
| builder.endRange(4, 1); | |
| builder.endRange(6, 0); | |
| const code = [ | |
| 'function testUndefinedAndThrowing() {', | |
| ' const legitUndefined = undefined;', | |
| ' debugger;', | |
| '}', | |
| 'window.runTest = testUndefinedAndThrowing;', | |
| ].join('\n'); | |
| const {callFrame} = await setupScriptAndPause(inspectedPage, debuggerModel, code, builder); | |
| try { | |
| const scopeChain = | |
| await SourceMapScopes.NamesResolver.resolveScopeChain(callFrame, universe.debuggerWorkspaceBinding); | |
| assert.strictEqual(await stringifyScopeChain(scopeChain), [ | |
| 'Local (testUndefinedAndThrowing)', | |
| ' legitUndefined: undefined', | |
| ' nonExistentVar: <unavailable>', | |
| 'Global', | |
| 'Global (<global>)', | |
| ].join('\n')); | |
| } finally { | |
| if (debuggerModel.isPaused()) { | |
| await debuggerModel.resume(); | |
| } | |
| } | |
| }); | |
| it('evaluates binding expressions without variable shadowing when runtime code defines __res', | |
| async ({inspectedPage, universe}) => { | |
| const primaryTarget = universe.targetManager.primaryPageTarget(); | |
| assert.isNotNull(primaryTarget); | |
| const debuggerModel = primaryTarget.model(SDK.DebuggerModel.DebuggerModel); | |
| assert.isNotNull(debuggerModel); | |
| await inspectedPage.goToHtml('<!DOCTYPE html><html><body></body></html>'); | |
| const builder = new ScopesCodec.ScopeInfoBuilder(); | |
| builder.startSource(); | |
| builder.startScope(0, 0, {kind: 'global', key: 'global'}); | |
| builder.startScope(0, 0, { | |
| kind: 'function', | |
| name: 'testNoShadowing', | |
| isStackFrame: true, | |
| variables: ['__res', 'computed'], | |
| key: 'fn', | |
| }); | |
| builder.endScope(4, 1); | |
| builder.endScope(6, 0); | |
| builder.endSource(); | |
| builder.startRange(0, 0, {scopeKey: 'global'}); | |
| builder.startRange(0, 0, {scopeKey: 'fn', isStackFrame: true, values: ['__res', 'other']}); | |
| builder.endRange(4, 1); | |
| builder.endRange(6, 0); | |
| const code = [ | |
| 'function testNoShadowing() {', | |
| ' const __res = "user_res";', | |
| ' const other = __res + "_ok";', | |
| ' debugger;', | |
| '}', | |
| 'window.runTest = testNoShadowing;', | |
| ].join('\n'); | |
| const {callFrame} = await setupScriptAndPause(inspectedPage, debuggerModel, code, builder); | |
| try { | |
| const scopeChain = | |
| await SourceMapScopes.NamesResolver.resolveScopeChain(callFrame, universe.debuggerWorkspaceBinding); | |
| assert.strictEqual(await stringifyScopeChain(scopeChain), [ | |
| 'Local (testNoShadowing)', | |
| ' __res: "user_res"', | |
| ' computed: "user_res_ok"', | |
| 'Global', | |
| 'Global (<global>)', | |
| ].join('\n')); | |
| } finally { | |
| if (debuggerModel.isPaused()) { | |
| await debuggerModel.resume(); | |
| } | |
| } | |
| }); | |
| it('drops inner block scopes and populates returnValue extraProperties when paused at V8 return point', | |
| async ({inspectedPage, universe}) => { | |
| const primaryTarget = universe.targetManager.primaryPageTarget(); | |
| assert.isNotNull(primaryTarget); | |
| const debuggerModel = primaryTarget.model(SDK.DebuggerModel.DebuggerModel); | |
| assert.isNotNull(debuggerModel); | |
| await inspectedPage.goToHtml('<!DOCTYPE html><html><body></body></html>'); | |
| const builder = new ScopesCodec.ScopeInfoBuilder(); | |
| builder.startSource(); | |
| builder.startScope(0, 0, {kind: 'global', key: 'global'}); | |
| builder.startScope(0, 0, | |
| {kind: 'function', name: 'testReturn', isStackFrame: true, variables: ['fnVar'], key: 'fn'}); | |
| builder.startScope(2, 2, {kind: 'block', variables: ['blockVar'], key: 'block'}); | |
| builder.endScope(7, 1); | |
| builder.endScope(7, 1); | |
| builder.endScope(9, 0); | |
| builder.endSource(); | |
| builder.startRange(0, 0, {scopeKey: 'global'}); | |
| builder.startRange(0, 0, {scopeKey: 'fn', isStackFrame: true, values: ['fnVar']}); | |
| builder.startRange(2, 2, {scopeKey: 'block', values: ['blockVar']}); | |
| builder.endRange(7, 1); | |
| builder.endRange(7, 1); | |
| builder.endRange(9, 0); | |
| const code = [ | |
| 'function testReturn() {', | |
| ' const fnVar = 10;', | |
| ' {', | |
| ' const blockVar = 20;', | |
| ' debugger;', | |
| ' return fnVar + blockVar;', | |
| ' }', | |
| '}', | |
| 'window.runTest = testReturn;', | |
| ].join('\n'); | |
| const {callFrame} = await setupScriptAndPause(inspectedPage, debuggerModel, code, builder); | |
| try { | |
| const initialScopeChain = | |
| await SourceMapScopes.NamesResolver.resolveScopeChain(callFrame, universe.debuggerWorkspaceBinding); | |
| assert.strictEqual(await stringifyScopeChain(initialScopeChain), [ | |
| 'Block', | |
| ' blockVar: 20', | |
| 'Local (testReturn)', | |
| ' fnVar: 10', | |
| 'Global', | |
| 'Global (<global>)', | |
| ].join('\n')); | |
| // Step until paused at return point (where returnValue is non-null) | |
| let currentFrame: SDK.DebuggerModel.CallFrame = callFrame; | |
| while (currentFrame.returnValue() === null) { | |
| const stepPaused = debuggerModel.once(SDK.DebuggerModel.Events.DebuggerPaused); | |
| void debuggerModel.stepOver(); | |
| await stepPaused; | |
| const frames: SDK.DebuggerModel.CallFrame[]|undefined = debuggerModel.debuggerPausedDetails()?.callFrames; | |
| assert.isDefined(frames); | |
| assert.isNotEmpty(frames); | |
| currentFrame = frames[0]; | |
| } | |
| const returnScopeChain = | |
| await SourceMapScopes.NamesResolver.resolveScopeChain(currentFrame, universe.debuggerWorkspaceBinding); | |
| assert.strictEqual(await stringifyScopeChain(returnScopeChain), [ | |
| 'Local (testReturn)', | |
| ' [Return value]: 30', | |
| ' fnVar: 10', | |
| 'Global', | |
| 'Global (<global>)', | |
| ].join('\n')); | |
| assert.isDefined(returnScopeChain[0].extraProperties()[0].syntheticSetter); | |
| } finally { | |
| if (debuggerModel.isPaused()) { | |
| await debuggerModel.resume(); | |
| } | |
| } | |
| }); | |
| it('does not evaluate binding expressions in target page when source map has USER provenance', | |
| async ({inspectedPage, universe}) => { | |
| const primaryTarget = universe.targetManager.primaryPageTarget(); | |
| assert.isNotNull(primaryTarget); | |
| const debuggerModel = primaryTarget.model(SDK.DebuggerModel.DebuggerModel); | |
| assert.isNotNull(debuggerModel); | |
| await inspectedPage.goToHtml('<!DOCTYPE html><html><body></body></html>'); | |
| const builder = new ScopesCodec.ScopeInfoBuilder(); | |
| builder.startSource(); | |
| builder.startScope(0, 0, {kind: 'global', key: 'global'}); | |
| builder.startScope( | |
| 0, 0, {kind: 'function', name: 'authoredFn', isStackFrame: true, variables: ['evilVar'], key: 'fn'}); | |
| builder.endScope(3, 1); | |
| builder.endScope(5, 0); | |
| builder.endSource(); | |
| builder.startRange(0, 0, {scopeKey: 'global'}); | |
| builder.startRange(0, 0, {scopeKey: 'fn', isStackFrame: true, values: ['(window.evilExecuted = true, 999)']}); | |
| builder.endRange(3, 1); | |
| builder.endRange(5, 0); | |
| const code = [ | |
| 'function compiledFn() {', | |
| ' const a = 456;', | |
| ' debugger;', | |
| '}', | |
| 'window.runTest = compiledFn;', | |
| ].join('\n'); | |
| const {callFrame, sourceMap} = | |
| await setupScriptAndPause(inspectedPage, debuggerModel, code, builder, | |
| ['0:9 => index.ts:0:9@authoredFn', '1:8 => index.ts:1:8@safeMappedVar']); | |
| try { | |
| debuggerModel.setSourceMapURL(callFrame.script, sourceMap.url(), SDK.SourceMap.SourceMapProvenance.USER); | |
| const userSourceMap = await debuggerModel.sourceMapManager().sourceMapForClientPromise(callFrame.script); | |
| assert.strictEqual(userSourceMap?.provenance(), SDK.SourceMap.SourceMapProvenance.USER); | |
| const scopeChain = | |
| await SourceMapScopes.NamesResolver.resolveScopeChain(callFrame, universe.debuggerWorkspaceBinding); | |
| assert.strictEqual(await stringifyScopeChain(scopeChain), [ | |
| 'Local (compiledFn)', | |
| ' [this]: <Window>', | |
| ' safeMappedVar: 456', | |
| 'Global (<global>)', | |
| ].join('\n')); | |
| await debuggerModel.resume(); | |
| const evilExecuted = | |
| await inspectedPage.evaluate(() => Boolean((window as unknown as {evilExecuted?: boolean}).evilExecuted)); | |
| assert.isFalse(evilExecuted, 'Binding expression from USER source map must NOT be evaluated'); | |
| } finally { | |
| if (debuggerModel.isPaused()) { | |
| await debuggerModel.resume(); | |
| } | |
| Formatter.FormatterWorkerPool.FormatterWorkerPool.removeInstance(); | |
| } | |
| }); | |
| it('resolves scope variables on pages with CSP blocking unsafe-eval and isolates runtime and syntax errors', | |
| async ({inspectedPage, universe}) => { | |
| const primaryTarget = universe.targetManager.primaryPageTarget(); | |
| assert.isNotNull(primaryTarget); | |
| const debuggerModel = primaryTarget.model(SDK.DebuggerModel.DebuggerModel); | |
| assert.isNotNull(debuggerModel); | |
| // Page with a strict Content-Security-Policy that allows inline scripts but blocks `eval()` (no 'unsafe-eval'). | |
| await inspectedPage.goToHtml([ | |
| '<!DOCTYPE html>', | |
| '<html>', | |
| '<head>', | |
| ' <meta http-equiv="Content-Security-Policy" content="script-src \'unsafe-inline\'">', | |
| '</head>', | |
| '<body></body>', | |
| '</html>', | |
| ].join('\n')); | |
| // First scope (Block): All expressions are valid or throw runtime errors (batched execution succeeds in one call). | |
| // Also verifies `evalBlocked` is true (confirming `eval()` is blocked by CSP in the page & frame). | |
| // Second scope (Local): Contains a syntax error ('brokenSyntax)') alongside valid and runtime-throwing expressions | |
| // (batch fails to parse and falls back to per-variable evaluation without losing valid variables). | |
| const builder = new ScopesCodec.ScopeInfoBuilder(); | |
| builder.startSource(); | |
| builder.startScope(0, 0, {kind: 'global', key: 'global'}); | |
| builder.startScope(0, 0, { | |
| kind: 'function', | |
| name: 'testCspAndErrors', | |
| isStackFrame: true, | |
| variables: ['fnValid', 'fnSyntaxError', 'fnRuntimeError', 'fnAfterErrors'], | |
| key: 'fn', | |
| }); | |
| builder.startScope(2, 2, { | |
| kind: 'block', | |
| variables: ['evalBlocked', 'blockValid', 'blockThrowing', 'blockUndefined'], | |
| key: 'block', | |
| }); | |
| builder.endScope(5, 3); | |
| builder.endScope(6, 1); | |
| builder.endScope(8, 0); | |
| builder.endSource(); | |
| builder.startRange(0, 0, {scopeKey: 'global'}); | |
| builder.startRange(0, 0, { | |
| scopeKey: 'fn', | |
| isStackFrame: true, | |
| values: ['a + 1', 'brokenSyntax)', 'nonExistentFnVar.prop', 'b * 2'], | |
| }); | |
| builder.startRange(2, 2, { | |
| scopeKey: 'block', | |
| values: ['evalBlocked', 'a', '(() => { throw new Error("boom"); })()', 'undefVal'], | |
| }); | |
| builder.endRange(5, 3); | |
| builder.endRange(6, 1); | |
| builder.endRange(8, 0); | |
| const code = [ | |
| 'function testCspAndErrors() {', | |
| ' const a = 10, b = 20, undefVal = undefined;', | |
| ' {', | |
| ' let evalBlocked = false; try { eval("1 + 1"); } catch (e) { evalBlocked = e instanceof EvalError; }', | |
| ' debugger;', | |
| ' }', | |
| '}', | |
| 'window.runTest = testCspAndErrors;', | |
| ].join('\n'); | |
| const {callFrame} = await setupScriptAndPause(inspectedPage, debuggerModel, code, builder); | |
| try { | |
| const scopeChain = | |
| await SourceMapScopes.NamesResolver.resolveScopeChain(callFrame, universe.debuggerWorkspaceBinding); | |
| assert.strictEqual(await stringifyScopeChain(scopeChain), [ | |
| 'Block', | |
| ' evalBlocked: true', | |
| ' blockValid: 10', | |
| ' blockThrowing: <unavailable>', | |
| ' blockUndefined: undefined', | |
| 'Local (testCspAndErrors)', | |
| ' fnValid: 11', | |
| ' fnSyntaxError: <unavailable>', | |
| ' fnRuntimeError: <unavailable>', | |
| ' fnAfterErrors: 40', | |
| 'Global', | |
| 'Global (<global>)', | |
| ].join('\n')); | |
| } finally { | |
| if (debuggerModel.isPaused()) { | |
| await debuggerModel.resume(); | |
| } | |
| } | |
| }); | |
| }); |