blob: 87f275681fbcc0403566b0790ed798c96d753288 [file] [log] [blame]
// Copyright 2021 The Chromium Authors. All rights reserved.
// 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 {
click,
enableExperiment,
getBrowserAndPages,
getPendingEvents,
goToResource,
pasteText,
step,
typeText,
waitFor,
waitForElementWithTextContent,
waitForFunction,
waitForFunctionWithTries,
} from '../../shared/helper.js';
import {describe, it} from '../../shared/mocha-extensions.js';
import {CONSOLE_TAB_SELECTOR, focusConsolePrompt, getCurrentConsoleMessages} from '../helpers/console-helpers.js';
import {
clickNthChildOfSelectedElementNode,
focusElementsTree,
waitForContentOfSelectedElementsNode,
waitForCSSPropertyValue,
waitForElementsStyleSection,
} from '../helpers/elements-helpers.js';
import {
addBreakpointForLine,
clickOnContextMenu,
DEBUGGER_PAUSED_EVENT,
getBreakpointDecorators,
getValuesForScope,
openSourceCodeEditorForFile,
openSourcesPanel,
PAUSE_INDICATOR_SELECTOR,
refreshDevToolsAndRemoveBackendState,
removeBreakpointForLine,
RESUME_BUTTON,
retrieveTopCallFrameScriptLocation,
retrieveTopCallFrameWithoutResuming,
STEP_INTO_BUTTON,
STEP_OUT_BUTTON,
STEP_OVER_BUTTON,
} from '../helpers/sources-helpers.js';
describe('The Sources Tab', async () => {
it('sets multiple breakpoints in case of code-splitting', async () => {
const {target, frontend} = getBrowserAndPages();
await openSourceCodeEditorForFile('sourcemap-codesplit.ts', 'sourcemap-codesplit.html');
await addBreakpointForLine(frontend, 3);
for (let i = 0; i < 2; ++i) {
const scriptLocation = await retrieveTopCallFrameScriptLocation(`functions[${i}]();`, target);
assert.deepEqual(scriptLocation, 'sourcemap-codesplit.ts:3');
}
});
async function waitForStackTopMatch(matcher: RegExp) {
// The call stack is updated asynchronously, so let us wait until we see the correct one
// (or report the last one we have seen before timeout).
let stepLocation = '<no call stack>';
await waitForFunctionWithTries(async () => {
stepLocation = await retrieveTopCallFrameWithoutResuming() ?? '<invalid>';
return stepLocation?.match(matcher);
}, {tries: 10});
return stepLocation;
}
it('steps over a source line mapping to a range with several statements', async () => {
const {target, frontend} = getBrowserAndPages();
await openSourceCodeEditorForFile('sourcemap-stepping-source.js', 'sourcemap-stepping.html');
let scriptEvaluation: Promise<unknown>;
// DevTools is contracting long filenames with ellipses.
// Let us match the location with regexp to match even contracted locations.
const breakLocationRegExp = /.*source\.js:12$/;
const stepLocationRegExp = /.*source\.js:13$/;
await step('Run to breakpoint', async () => {
await addBreakpointForLine(frontend, 12);
scriptEvaluation = target.evaluate('singleline();');
const scriptLocation = await waitForStackTopMatch(breakLocationRegExp);
assert.match(scriptLocation, breakLocationRegExp);
});
await step('Step over the mapped line', async () => {
await click(STEP_OVER_BUTTON);
const stepLocation = await waitForStackTopMatch(stepLocationRegExp);
assert.match(stepLocation, stepLocationRegExp);
});
await step('Resume', async () => {
await click(RESUME_BUTTON);
await scriptEvaluation;
});
});
it('steps over a source line with mappings to several adjacent target lines', async () => {
const {target, frontend} = getBrowserAndPages();
await openSourceCodeEditorForFile('sourcemap-stepping-source.js', 'sourcemap-stepping.html');
let scriptEvaluation: Promise<unknown>;
// DevTools is contracting long filenames with ellipses.
// Let us match the location with regexp to match even contracted locations.
const breakLocationRegExp = /.*source\.js:4$/;
const stepLocationRegExp = /.*source\.js:5$/;
await step('Run to breakpoint', async () => {
await addBreakpointForLine(frontend, 4);
scriptEvaluation = target.evaluate('multiline();');
const scriptLocation = await waitForStackTopMatch(breakLocationRegExp);
assert.match(scriptLocation, breakLocationRegExp);
});
await step('Step over the mapped line', async () => {
await click(STEP_OVER_BUTTON);
const stepLocation = await waitForStackTopMatch(stepLocationRegExp);
assert.match(stepLocation, stepLocationRegExp);
});
await step('Resume', async () => {
await click(RESUME_BUTTON);
await scriptEvaluation;
});
});
it('steps out from a function, with source maps available (crbug/1283188)', async () => {
const {target, frontend} = getBrowserAndPages();
await openSourceCodeEditorForFile('sourcemap-stepping-source.js', 'sourcemap-stepping.html');
let scriptEvaluation: Promise<unknown>;
// DevTools is contracting long filenames with ellipses.
// Let us match the location with regexp to match even contracted locations.
const breakLocationRegExp = /.*source\.js:4$/;
const stepLocationRegExp = /sourcemap-stepping.html:6$/;
await step('Run to breakpoint', async () => {
await addBreakpointForLine(frontend, 4);
scriptEvaluation = target.evaluate('outer();');
const scriptLocation = await waitForStackTopMatch(breakLocationRegExp);
assert.match(scriptLocation, breakLocationRegExp);
});
await step('Step out from breakpoint', async () => {
await click(STEP_OUT_BUTTON);
const stepLocation = await waitForStackTopMatch(stepLocationRegExp);
assert.match(stepLocation, stepLocationRegExp);
});
await step('Resume', async () => {
await click(RESUME_BUTTON);
await scriptEvaluation;
});
});
it('stepping works at the end of a sourcemapped script (crbug/1305956)', async () => {
const {target} = getBrowserAndPages();
await openSourceCodeEditorForFile('sourcemap-stepping-at-end.js', 'sourcemap-stepping-at-end.html');
// DevTools is contracting long filenames with ellipses.
// Let us match the location with regexp to match even contracted locations.
const breakLocationRegExp = /.*at-end\.js:2$/;
const stepLocationRegExp = /.*at-end.html:6$/;
for (const [description, button] of [
['into', STEP_INTO_BUTTON],
['out', STEP_OUT_BUTTON],
['over', STEP_OVER_BUTTON],
]) {
let scriptEvaluation: Promise<unknown>;
await step('Run to debugger statement', async () => {
scriptEvaluation = target.evaluate('outer();');
const scriptLocation = await waitForStackTopMatch(breakLocationRegExp);
assert.match(scriptLocation, breakLocationRegExp);
});
await step(`Step ${description} from debugger statement`, async () => {
await click(button);
const stepLocation = await waitForStackTopMatch(stepLocationRegExp);
assert.match(stepLocation, stepLocationRegExp);
});
await step('Resume', async () => {
await click(RESUME_BUTTON);
await scriptEvaluation;
});
}
});
it('shows unminified identifiers in scopes and console', async () => {
const {target, frontend} = getBrowserAndPages();
await openSourceCodeEditorForFile('sourcemap-minified.js', 'sourcemap-minified.html');
let scriptEvaluation: Promise<unknown>;
const breakLocationRegExp = /sourcemap-minified\.js:1$/;
await step('Run to debugger statement', async () => {
scriptEvaluation = target.evaluate('sayHello(" world");');
const scriptLocation = await waitForStackTopMatch(breakLocationRegExp);
assert.match(scriptLocation, breakLocationRegExp);
});
await step('Check local variable is eventually un-minified', async () => {
const unminifiedVariable = 'element: div';
await clickOnContextMenu('.cm-line', 'Add source map…');
// Enter the source map URL into the appropriate input box.
await waitFor('.add-source-map');
await click('.add-source-map');
await typeText('sourcemap-minified.map');
await frontend.keyboard.press('Enter');
const scopeValues = await waitForFunction(async () => {
const values = await getValuesForScope('Local', 0, 0);
return (values && values.includes(unminifiedVariable)) ? values : undefined;
});
assert.include(scopeValues, unminifiedVariable);
});
await step('Check that expression evaluation understands unminified name', async () => {
await frontend.evaluate(() => {
// @ts-ignore
globalThis.Root.Runtime.experiments.setEnabled('evaluateExpressionsWithSourceMaps', true);
});
await click(CONSOLE_TAB_SELECTOR);
await focusConsolePrompt();
await pasteText('`Hello${text}!`');
await frontend.keyboard.press('Enter');
// Wait for the console to be usable again.
await frontend.waitForFunction(() => {
return document.querySelectorAll('.console-user-command-result').length === 1;
});
const messages = await getCurrentConsoleMessages();
assert.deepEqual(messages, ['\'Hello world!\'']);
await openSourcesPanel();
});
await step('Resume', async () => {
await click(RESUME_BUTTON);
await scriptEvaluation;
});
});
it('updates decorators for removed breakpoints in case of code-splitting (crbug.com/1251675)', async () => {
const {frontend} = getBrowserAndPages();
await openSourceCodeEditorForFile('sourcemap-disjoint.js', 'sourcemap-disjoint.html');
assert.deepEqual(await getBreakpointDecorators(), []);
await addBreakpointForLine(frontend, 2);
assert.deepEqual(await getBreakpointDecorators(), [2]);
await removeBreakpointForLine(frontend, 2);
assert.deepEqual(await getBreakpointDecorators(), []);
});
// Flaky test
it.skipOnPlatforms(
['win32'], '[crbug.com/1297070]: reliably hits breakpoints on worker with source map', async () => {
await enableExperiment('instrumentationBreakpoints');
const {target, frontend} = getBrowserAndPages();
await openSourceCodeEditorForFile('sourcemap-stepping-source.js', 'sourcemap-breakpoint.html');
await step('Add a breakpoint at first line of function multiline', async () => {
await addBreakpointForLine(frontend, 4);
});
await step('Navigate to a different site to refresh devtools and remove back-end state', async () => {
await refreshDevToolsAndRemoveBackendState(target);
});
await step('Navigate back to test page', () => {
void goToResource('sources/sourcemap-breakpoint.html');
});
await step('wait for pause and check if we stopped at line 4', async () => {
await waitForFunction(() => getPendingEvents(frontend, DEBUGGER_PAUSED_EVENT));
await waitFor(PAUSE_INDICATOR_SELECTOR);
await waitForFunction(async () => {
const topCallFrame = await retrieveTopCallFrameWithoutResuming();
return topCallFrame === 'sourcemap-stepping-source.js:4';
});
});
await step('Resume', async () => {
await click(RESUME_BUTTON);
});
});
});
describe('The Elements Tab', async () => {
it('links to the right SASS source for inline CSS with relative sourcemap (crbug.com/787792)', async () => {
await goToResource('sources/sourcemap-css-inline-relative.html');
await step('Prepare elements tab', async () => {
await waitForElementsStyleSection();
await waitForContentOfSelectedElementsNode('<body>\u200B');
await focusElementsTree();
await clickNthChildOfSelectedElementNode(1);
});
const value = await waitForCSSPropertyValue('body .text', 'color', 'green', 'app.scss:6');
await click(value, {clickOptions: {modifier: 'ControlOrMeta'}});
await waitForElementWithTextContent('Line 12, Column 9');
});
it('links to the right SASS source for inline CSS with absolute sourcemap (crbug.com/787792)', async () => {
await goToResource('sources/sourcemap-css-dynamic-link.html');
await step('Prepare elements tab', async () => {
await waitForElementsStyleSection();
await waitForContentOfSelectedElementsNode('<body>\u200B');
await focusElementsTree();
await clickNthChildOfSelectedElementNode(1);
});
const value = await waitForCSSPropertyValue('body .text', 'color', 'green', 'app.scss:6');
await click(value, {clickOptions: {modifier: 'ControlOrMeta'}});
await waitForElementWithTextContent('Line 12, Column 9');
});
it('links to the right SASS source for dynamically added CSS style tags (crbug.com/787792)', async () => {
await goToResource('sources/sourcemap-css-dynamic.html');
await step('Prepare elements tab', async () => {
await waitForElementsStyleSection();
await waitForContentOfSelectedElementsNode('<body>\u200B');
await focusElementsTree();
await clickNthChildOfSelectedElementNode(1);
});
const value = await waitForCSSPropertyValue('body .text', 'color', 'green', 'app.scss:6');
await click(value, {clickOptions: {modifier: 'ControlOrMeta'}});
await waitForElementWithTextContent('Line 12, Column 9');
});
it('links to the right SASS source for dynamically added CSS link tags (crbug.com/787792)', async () => {
await goToResource('sources/sourcemap-css-dynamic-link.html');
await step('Prepare elements tab', async () => {
await waitForElementsStyleSection();
await waitForContentOfSelectedElementsNode('<body>\u200B');
await focusElementsTree();
await clickNthChildOfSelectedElementNode(1);
});
const value = await waitForCSSPropertyValue('body .text', 'color', 'green', 'app.scss:6');
await click(value, {clickOptions: {modifier: 'ControlOrMeta'}});
await waitForElementWithTextContent('Line 12, Column 9');
});
});