diff --git a/front_end/panels/sources/CallStackSidebarPane.ts b/front_end/panels/sources/CallStackSidebarPane.ts index 46b2f2a4a2..a757a591df 100644 --- a/front_end/panels/sources/CallStackSidebarPane.ts +++ b/front_end/panels/sources/CallStackSidebarPane.ts @@ -107,6 +107,7 @@ let callstackSidebarPaneInstance: CallStackSidebarPane; export class CallStackSidebarPane extends UI.View.SimpleView implements UI.ContextFlavorListener.ContextFlavorListener, UI.ListControl.ListDelegate { private readonly ignoreListMessageElement: Element; + private readonly ignoreListCheckboxElement: HTMLInputElement; private readonly notPausedMessageElement: HTMLElement; private readonly callFrameWarningsElement: HTMLElement; private readonly items: UI.ListModel.ListModel; @@ -124,14 +125,15 @@ export class CallStackSidebarPane extends UI.View.SimpleView implements UI.Conte private constructor() { super(i18nString(UIStrings.callStack), true); - this.ignoreListMessageElement = this.createIgnoreListMessageElement(); + ({element: this.ignoreListMessageElement, checkbox: this.ignoreListCheckboxElement} = + this.createIgnoreListMessageElementAndCheckbox()); this.contentElement.appendChild(this.ignoreListMessageElement); this.notPausedMessageElement = this.contentElement.createChild('div', 'gray-info-message'); this.notPausedMessageElement.textContent = i18nString(UIStrings.notPaused); this.notPausedMessageElement.tabIndex = -1; - this.callFrameWarningsElement = this.contentElement.createChild('div', 'ignore-listed-message'); + this.callFrameWarningsElement = this.contentElement.createChild('div', 'call-frame-warnings-message'); const icon = UI.Icon.Icon.create('smallicon-warning', 'call-frame-warning-icon'); this.callFrameWarningsElement.appendChild(icon); this.callFrameWarningsElement.appendChild(document.createTextNode(i18nString(UIStrings.callFrameWarnings))); @@ -180,6 +182,7 @@ export class CallStackSidebarPane extends UI.View.SimpleView implements UI.Conte flavorChanged(_object: Object|null): void { this.showIgnoreListed = false; + this.ignoreListCheckboxElement.checked = false; this.maxAsyncStackChainDepth = defaultMaxAsyncStackChainDepth; this.update(); } @@ -278,6 +281,9 @@ export class CallStackSidebarPane extends UI.View.SimpleView implements UI.Conte } this.showMoreMessageElement.classList.toggle('hidden', !asyncStackTrace); this.items.replaceAll(items); + for (const item of this.items) { + this.refreshItem(item); + } if (this.maxAsyncStackChainDepth === defaultMaxAsyncStackChainDepth) { this.list.selectNextItem(true /* canWrap */, false /* center */); const selectedItem = this.list.selectedItem(); @@ -305,6 +311,7 @@ export class CallStackSidebarPane extends UI.View.SimpleView implements UI.Conte } this.ignoreListMessageElement.classList.toggle('hidden', true); } else { + this.showIgnoreListed = this.ignoreListCheckboxElement.checked; const itemsSet = new Set(items); let hasIgnoreListed = false; for (let i = 0; i < this.items.length; ++i) { @@ -314,7 +321,7 @@ export class CallStackSidebarPane extends UI.View.SimpleView implements UI.Conte } hasIgnoreListed = hasIgnoreListed || item.isIgnoreListed; } - this.ignoreListMessageElement.classList.toggle('hidden', this.showIgnoreListed || !hasIgnoreListed); + this.ignoreListMessageElement.classList.toggle('hidden', !hasIgnoreListed); } delete this.muteActivateItem; }); @@ -387,24 +394,24 @@ export class CallStackSidebarPane extends UI.View.SimpleView implements UI.Conte return true; } - private createIgnoreListMessageElement(): Element { + private createIgnoreListMessageElementAndCheckbox(): {element: Element, checkbox: HTMLInputElement} { const element = document.createElement('div'); element.classList.add('ignore-listed-message'); - element.createChild('span'); - const showAllLink = element.createChild('span', 'link'); - showAllLink.textContent = i18nString(UIStrings.showIgnorelistedFrames); - UI.ARIAUtils.markAsLink(showAllLink); - showAllLink.tabIndex = 0; + const label = element.createChild('label'); + label.classList.add('ignore-listed-message-label'); + const checkbox = label.createChild('input') as HTMLInputElement; + checkbox.tabIndex = 0; + checkbox.type = 'checkbox'; + checkbox.classList.add('ignore-listed-checkbox'); + label.append(i18nString(UIStrings.showIgnorelistedFrames)); const showAll = (): void => { - this.showIgnoreListed = true; + this.showIgnoreListed = checkbox.checked; for (const item of this.items) { this.refreshItem(item); } - this.ignoreListMessageElement.classList.toggle('hidden', true); }; - showAllLink.addEventListener('click', showAll); - showAllLink.addEventListener('keydown', event => event.key === 'Enter' && showAll()); - return element; + checkbox.addEventListener('click', showAll); + return {element, checkbox}; } private createShowMoreMessageElement(): Element { diff --git a/front_end/panels/sources/callStackSidebarPane.css b/front_end/panels/sources/callStackSidebarPane.css index 9da1c25745..1cb7f499b2 100644 --- a/front_end/panels/sources/callStackSidebarPane.css +++ b/front_end/panels/sources/callStackSidebarPane.css @@ -4,7 +4,7 @@ * found in the LICENSE file. */ -.ignore-listed-message { +.call-frame-warnings-message { --override-ignore-message-background-color: #ffffc2; text-align: center; @@ -14,6 +14,16 @@ background-color: var(--override-ignore-message-background-color); } +.ignore-listed-message { + padding: 1px; +} + +.ignore-listed-message-label { + color: var(--color-text-secondary); + align-items: center; + display: flex; +} + .-theme-with-dark-background .ignore-listed-message, :host-context(.-theme-with-dark-background) .ignore-listed-message { --override-ignore-message-background-color: rgb(72 72 0); @@ -23,14 +33,6 @@ margin-left: 5px; } -.ignore-listed-message > .link { - margin-left: 5px; -} - -.ignore-listed-message > .link:focus { - outline-width: unset; -} - .show-more-message { text-align: center; font-style: italic; @@ -80,6 +82,10 @@ background-color: var(--legacy-focus-bg-color); } +.ignore-listed-checkbox:focus-visible { + outline-width: unset; +} + .call-frame-item:not(.async-header):hover { background-color: var(--color-background-elevation-1); } diff --git a/test/e2e/extensions/debugger-language-plugins_test.ts b/test/e2e/extensions/debugger-language-plugins_test.ts index b034469119..a108af3607 100644 --- a/test/e2e/extensions/debugger-language-plugins_test.ts +++ b/test/e2e/extensions/debugger-language-plugins_test.ts @@ -499,7 +499,7 @@ describe('The Debugger Language Plugins', async () => { const text = await details.evaluate(e => e.textContent); assert.deepEqual(text, 'Failed to load debug file "test.wasm".'); - const banners = await $$('.ignore-listed-message'); + const banners = await $$('.call-frame-warnings-message'); const bannerTexts = await Promise.all(banners.map(e => e.evaluate(e => e.textContent))); assert.include(bannerTexts, 'Some call frames have warnings'); @@ -569,7 +569,7 @@ describe('The Debugger Language Plugins', async () => { const text = await details.evaluate(e => e.textContent); assert.deepEqual(text, 'Failed to load debug file "test.dwo".'); - const banners = await $$('.ignore-listed-message'); + const banners = await $$('.call-frame-warnings-message'); const bannerTexts = await Promise.all(banners.map(e => e.evaluate(e => e.textContent))); assert.include(bannerTexts, 'Some call frames have warnings'); diff --git a/test/e2e/helpers/settings-helpers.ts b/test/e2e/helpers/settings-helpers.ts index c6d1f0869e..6b1bf88dbf 100644 --- a/test/e2e/helpers/settings-helpers.ts +++ b/test/e2e/helpers/settings-helpers.ts @@ -39,6 +39,10 @@ export const openSettingsTab = async (tabTitle: string) => { await waitFor(panelSelector); }; +export const closeSettings = async () => { + await click('.dialog-close-button'); +}; + export const togglePreferenceInSettingsTab = async (label: string) => { await openSettingsTab('Preferences'); @@ -55,5 +59,16 @@ export const togglePreferenceInSettingsTab = async (label: string) => { return newValue !== value; }); - await click('.dialog-close-button'); + await closeSettings(); +}; + +export const setIgnoreListPattern = async (pattern: string) => { + await openSettingsTab('Ignore List'); + await click('[aria-label="Add filename pattern"]'); + const textBox = await waitFor('[aria-label="Pattern"]'); + await click(textBox); + await textBox.type(pattern); + await textBox.type('\n'); + await waitFor(`[title="Ignore scripts whose names match '${pattern}'"]`); + await closeSettings(); }; diff --git a/test/e2e/helpers/sources-helpers.ts b/test/e2e/helpers/sources-helpers.ts index 2c5419bc3d..7eba5bfc1d 100644 --- a/test/e2e/helpers/sources-helpers.ts +++ b/test/e2e/helpers/sources-helpers.ts @@ -280,8 +280,9 @@ export async function executionLineHighlighted() { } export async function getCallFrameNames() { - await waitFor('.call-frame-item-title'); - const items = await $$('.call-frame-item-title'); + const selector = '.call-frame-item:not(.hidden) .call-frame-item-title'; + await waitFor(selector); + const items = await $$(selector); const promises = items.map(handle => handle.evaluate(el => el.textContent as string)); const results = []; for (const promise of promises) { @@ -291,8 +292,9 @@ export async function getCallFrameNames() { } export async function getCallFrameLocations() { - await waitFor('.call-frame-location'); - const items = await $$('.call-frame-location'); + const selector = '.call-frame-item:not(.hidden) .call-frame-location'; + await waitFor(selector); + const items = await $$(selector); const promises = items.map(handle => handle.evaluate(el => el.textContent as string)); const results = []; for (const promise of promises) { @@ -484,10 +486,35 @@ export async function readSourcesTreeView(): Promise { return results.map(item => item.replace(/localhost:[0-9]+/, 'localhost:XXXX')); } +async function hasPausedEvents(frontend: puppeteer.Page): Promise { + const events = await getPendingEvents(frontend, DEBUGGER_PAUSED_EVENT); + return Boolean(events && events.length); +} + export async function stepThroughTheCode() { const {frontend} = getBrowserAndPages(); + await getPendingEvents(frontend, DEBUGGER_PAUSED_EVENT); await frontend.keyboard.press('F9'); - await waitForFunction(() => getPendingEvents(frontend, DEBUGGER_PAUSED_EVENT)); + await waitForFunction(() => hasPausedEvents(frontend)); + await waitFor(PAUSE_INDICATOR_SELECTOR); +} + +export async function stepIn() { + const {frontend} = getBrowserAndPages(); + await getPendingEvents(frontend, DEBUGGER_PAUSED_EVENT); + await frontend.keyboard.press('F11'); + await waitForFunction(() => hasPausedEvents(frontend)); + await waitFor(PAUSE_INDICATOR_SELECTOR); +} + +export async function stepOut() { + const {frontend} = getBrowserAndPages(); + await getPendingEvents(frontend, DEBUGGER_PAUSED_EVENT); + await frontend.keyboard.down('Shift'); + await frontend.keyboard.press('F11'); + await frontend.keyboard.up('Shift'); + await waitForFunction(() => hasPausedEvents(frontend)); + await waitFor(PAUSE_INDICATOR_SELECTOR); } diff --git a/test/e2e/resources/sources/BUILD.gn b/test/e2e/resources/sources/BUILD.gn index f99e9e6012..950dcc15ac 100644 --- a/test/e2e/resources/sources/BUILD.gn +++ b/test/e2e/resources/sources/BUILD.gn @@ -33,6 +33,9 @@ copy_to_gen("sources") { "memory-workers.rawresponse", "minified-sourcecode.html", "minified-sourcecode.js", + "multi-files-mycode.js", + "multi-files-thirdparty.js", + "multi-files.html", "multi-workers-sourcemap.html", "multi-workers.html", "multi-workers.js", diff --git a/test/e2e/resources/sources/multi-files-mycode.js b/test/e2e/resources/sources/multi-files-mycode.js new file mode 100644 index 0000000000..16991abe94 --- /dev/null +++ b/test/e2e/resources/sources/multi-files-mycode.js @@ -0,0 +1,9 @@ +import {callfunc} from './multi-files-thirdparty.js'; + +function inner() { + console.log('Hello'); +} + +export function outer() { + callfunc(inner); +} diff --git a/test/e2e/resources/sources/multi-files-thirdparty.js b/test/e2e/resources/sources/multi-files-thirdparty.js new file mode 100644 index 0000000000..378af8474e --- /dev/null +++ b/test/e2e/resources/sources/multi-files-thirdparty.js @@ -0,0 +1,17 @@ + +/** + * @param {() => void} func + * @returns void + */ + +function innercall(func) { + func(); +} + +/** + * @param {() => void} func + * @returns void + */ +export function callfunc(func) { + innercall(func); +} diff --git a/test/e2e/resources/sources/multi-files.html b/test/e2e/resources/sources/multi-files.html new file mode 100644 index 0000000000..d1a9925f2c --- /dev/null +++ b/test/e2e/resources/sources/multi-files.html @@ -0,0 +1,6 @@ + diff --git a/test/e2e/sources/BUILD.gn b/test/e2e/sources/BUILD.gn index ec936057ad..0f8875828a 100644 --- a/test/e2e/sources/BUILD.gn +++ b/test/e2e/sources/BUILD.gn @@ -20,6 +20,7 @@ node_ts_library("sources") { "debug-raw-wasm_test.ts", "header-overrides_test.ts", "icon-row-bucket_test.ts", + "ignore-list_test.ts", "inline-variable_test.ts", "link-to-correct-source-line_test.ts", "live-edit-moving-breakpoint_test.ts", diff --git a/test/e2e/sources/ignore-list_test.ts b/test/e2e/sources/ignore-list_test.ts new file mode 100644 index 0000000000..a2c95bd01f --- /dev/null +++ b/test/e2e/sources/ignore-list_test.ts @@ -0,0 +1,103 @@ +// Copyright 2022 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, + getBrowserAndPages, + getPendingEvents, + installEventListener, + step, + waitFor, + waitForFunction, + waitForNone, +} from '../../shared/helper.js'; +import {setIgnoreListPattern} from '../helpers/settings-helpers.js'; +import { + addBreakpointForLine, + DEBUGGER_PAUSED_EVENT, + getCallFrameNames, + openSourceCodeEditorForFile, + PAUSE_INDICATOR_SELECTOR, + RESUME_BUTTON, + stepIn, + stepOut, +} from '../helpers/sources-helpers.js'; + +describe('Ignore list', async function() { + it('can be toggled on and off in call stack', async function() { + await setIgnoreListPattern('thirdparty'); + const {target, frontend} = getBrowserAndPages(); + + await openSourceCodeEditorForFile('multi-files-mycode.js', 'multi-files.html'); + await addBreakpointForLine(frontend, 4); + + const scriptEvaluation = target.evaluate('f();'); + await waitFor(RESUME_BUTTON); + await waitFor('.ignore-listed-call-frame.hidden'); + assert.deepEqual(await getCallFrameNames(), ['inner', 'outer', '(anonymous)']); + + // Toggle to show ignore-listed frames + await click('.ignore-listed-message-label'); + await waitFor('.ignore-listed-call-frame:not(.hidden)'); + assert.deepEqual(await getCallFrameNames(), ['inner', 'innercall', 'callfunc', 'outer', '(anonymous)']); + + // Toggle back off + await click('.ignore-listed-message-label'); + await waitFor('.ignore-listed-call-frame.hidden'); + assert.deepEqual(await getCallFrameNames(), ['inner', 'outer', '(anonymous)']); + + await click(RESUME_BUTTON); + await scriptEvaluation; + }); + + it('shows no toggle when everything is ignore-listed', async function() { + await setIgnoreListPattern('multi|puppeteer'); + const {target, frontend} = getBrowserAndPages(); + + await openSourceCodeEditorForFile('multi-files-mycode.js', 'multi-files.html'); + await addBreakpointForLine(frontend, 4); + + const scriptEvaluation = target.evaluate('f();'); + await waitFor(RESUME_BUTTON); + + // Toggle is not shown + await waitFor('.ignore-listed-message.hidden'); + // There are visible ignore-listed frames + await waitFor('.ignore-listed-call-frame:not(.hidden)'); + // All frames are shown + assert.deepEqual(await getCallFrameNames(), ['inner', 'innercall', 'callfunc', 'outer', '(anonymous)']); + + await click(RESUME_BUTTON); + await scriptEvaluation; + }); + + it('skips frames when stepping in and out', async function() { + await setIgnoreListPattern('thirdparty'); + const {target, frontend} = getBrowserAndPages(); + installEventListener(frontend, DEBUGGER_PAUSED_EVENT); + + await openSourceCodeEditorForFile('multi-files-mycode.js', 'multi-files.html'); + await addBreakpointForLine(frontend, 8); + + const scriptEvaluation = target.evaluate('f();'); + await waitFor(RESUME_BUTTON); + await waitFor(PAUSE_INDICATOR_SELECTOR); + await waitForFunction(async () => await getPendingEvents(frontend, DEBUGGER_PAUSED_EVENT)); + assert.deepEqual(await getCallFrameNames(), ['outer', '(anonymous)']); + + await step('Step in', stepIn); + await waitFor('.call-frame-title-text[title="inner"]'); + await waitFor('.ignore-listed-call-frame.hidden'); + assert.deepEqual(await getCallFrameNames(), ['inner', 'outer', '(anonymous)']); + + await step('Step out', stepOut); + await waitForNone('.call-frame-title-text[title="inner"]'); + assert.deepEqual(await getCallFrameNames(), ['outer', '(anonymous)']); + + await click(RESUME_BUTTON); + await scriptEvaluation; + }); +}); diff --git a/test/shared/helper.ts b/test/shared/helper.ts index 8ade9ac38b..e9cdc38073 100644 --- a/test/shared/helper.ts +++ b/test/shared/helper.ts @@ -552,6 +552,30 @@ export const enableCDPLogging = async () => { }); }; +export const enableCDPTracking = async () => { + const {frontend} = getBrowserAndPages(); + await frontend.evaluate(() => { + globalThis.__messageMapForTest = new Map(); + globalThis.ProtocolClient.test.onMessageSent = (message: {method: string, id: number}) => { + globalThis.__messageMapForTest.set(message.id, message.method); + }; + globalThis.ProtocolClient.test.onMessageReceived = (message: {id?: number}) => { + if (message.id) { + globalThis.__messageMapForTest.delete(message.id); + } + }; + }); +}; + +export const logOutstandingCDP = async () => { + const {frontend} = getBrowserAndPages(); + await frontend.evaluate(() => { + for (const entry of globalThis.__messageMapForTest) { + console.error(entry); + } + }); +}; + export const selectOption = async (select: puppeteer.ElementHandle, value: string) => { await select.evaluate(async (node: HTMLSelectElement, _value: string) => { node.value = _value;