blob: 3db58b36d459c86299cffcfc22400c6323008bf8 [file] [edit]
// 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 * 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: API.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) => {
// eslint-disable-next-line @devtools/no-imperative-dom-api
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.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.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.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.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.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.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.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.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.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.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.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.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.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.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.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.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.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.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.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.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();
}
}
});
});