chrome-devtools-frontend
Version:
Chrome DevTools UI
1,138 lines (988 loc) • 83.5 kB
text/typescript
// 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 * as Common from '../../../core/common/common.js';
import * as Platform from '../../../core/platform/platform.js';
import * as SDK from '../../../core/sdk/sdk.js';
import type * as Protocol from '../../../generated/protocol.js';
import * as Bindings from '../../../models/bindings/bindings.js';
import * as Breakpoints from '../../../models/breakpoints/breakpoints.js';
import * as TextUtils from '../../../models/text_utils/text_utils.js';
import * as Workspace from '../../../models/workspace/workspace.js';
import {
assertElements,
dispatchClickEvent,
dispatchKeyDownEvent,
renderElementIntoDOM,
} from '../../../testing/DOMHelpers.js';
import {
createTarget,
describeWithEnvironment,
} from '../../../testing/EnvironmentHelpers.js';
import {describeWithMockConnection} from '../../../testing/MockConnection.js';
import {MockProtocolBackend} from '../../../testing/MockScopeChain.js';
import {
createContentProviderUISourceCode,
setupMockedUISourceCode,
} from '../../../testing/UISourceCodeHelpers.js';
import * as RenderCoordinator from '../../../ui/components/render_coordinator/render_coordinator.js';
import * as UI from '../../../ui/legacy/legacy.js';
import * as SourcesComponents from './components.js';
const {urlString} = Platform.DevToolsPath;
const DETAILS_SELECTOR = 'details';
const EXPANDED_GROUPS_SELECTOR = 'details[open]';
const COLLAPSED_GROUPS_SELECTOR = 'details:not([open])';
const CODE_SNIPPET_SELECTOR = '.code-snippet';
const GROUP_NAME_SELECTOR = '.group-header-title';
const BREAKPOINT_ITEM_SELECTOR = '.breakpoint-item';
const HIT_BREAKPOINT_SELECTOR = BREAKPOINT_ITEM_SELECTOR + '.hit';
const BREAKPOINT_LOCATION_SELECTOR = '.location';
const REMOVE_FILE_BREAKPOINTS_SELECTOR = '.group-hover-actions > button[data-remove-breakpoint]';
const REMOVE_SINGLE_BREAKPOINT_SELECTOR = '.breakpoint-item-location-or-actions > button[data-remove-breakpoint]';
const EDIT_SINGLE_BREAKPOINT_SELECTOR = 'button[data-edit-breakpoint]';
const PAUSE_ON_UNCAUGHT_EXCEPTIONS_SELECTOR = '.pause-on-uncaught-exceptions';
const PAUSE_ON_CAUGHT_EXCEPTIONS_SELECTOR = '.pause-on-caught-exceptions';
const TABBABLE_SELECTOR = '[tabindex="0"]';
const SUMMARY_SELECTOR = 'summary';
const GROUP_DIFFERENTIATOR_SELECTOR = '.group-header-differentiator';
const HELLO_JS_FILE = 'hello.js';
const TEST_JS_FILE = 'test.js';
interface LocationTestData {
url: Platform.DevToolsPath.UrlString;
lineNumber: number;
columnNumber: number;
enabled: boolean;
content: string;
condition: Breakpoints.BreakpointManager.UserCondition;
isLogpoint: boolean;
hoverText?: string;
}
function createBreakpointLocations(testData: LocationTestData[]): Breakpoints.BreakpointManager.BreakpointLocation[] {
const breakpointLocations = testData.map(data => {
const mocked = setupMockedUISourceCode(data.url);
const mockedContent =
Promise.resolve(new TextUtils.ContentData.ContentData(data.content, /* isBase64 */ false, 'text/plain'));
sinon.stub(mocked.sut, 'requestContentData').returns(mockedContent);
const uiLocation = new Workspace.UISourceCode.UILocation(mocked.sut, data.lineNumber, data.columnNumber);
const breakpoint = sinon.createStubInstance(Breakpoints.BreakpointManager.Breakpoint);
breakpoint.enabled.returns(data.enabled);
breakpoint.condition.returns(data.condition);
breakpoint.isLogpoint.returns(data.isLogpoint);
breakpoint.breakpointStorageId.returns(`${data.url}:${data.lineNumber}:${data.columnNumber}`);
return new Breakpoints.BreakpointManager.BreakpointLocation(breakpoint, uiLocation);
});
return breakpointLocations;
}
function createStubBreakpointManagerAndSettings() {
const breakpointManager = sinon.createStubInstance(Breakpoints.BreakpointManager.BreakpointManager);
breakpointManager.supportsConditionalBreakpoints.returns(true);
const dummyStorage = new Common.Settings.SettingsStorage({});
const settings = Common.Settings.Settings.instance({
forceNew: true,
syncedStorage: dummyStorage,
globalStorage: dummyStorage,
localStorage: dummyStorage,
});
return {breakpointManager, settings};
}
function createStubBreakpointManagerAndSettingsWithMockdata(testData: LocationTestData[]): {
breakpointManager: sinon.SinonStubbedInstance<Breakpoints.BreakpointManager.BreakpointManager>,
settings: Common.Settings.Settings,
} {
const {breakpointManager, settings} = createStubBreakpointManagerAndSettings();
sinon.stub(Breakpoints.BreakpointManager.BreakpointManager, 'instance').returns(breakpointManager);
const breakpointLocations = createBreakpointLocations(testData);
breakpointManager.allBreakpointLocations.returns(breakpointLocations);
return {breakpointManager, settings};
}
function createLocationTestData(
url: string, lineNumber: number, columnNumber: number, enabled = true, content = '',
condition: Breakpoints.BreakpointManager.UserCondition = Breakpoints.BreakpointManager.EMPTY_BREAKPOINT_CONDITION,
isLogpoint = false, hoverText?: string): LocationTestData {
return {
url: urlString`${url}`,
lineNumber,
columnNumber,
enabled,
content,
condition,
isLogpoint,
hoverText,
};
}
async function setUpTestWithOneBreakpointLocation(
params: {file: string, lineNumber: number, columnNumber: number, enabled?: boolean, snippet?: string} = {
file: HELLO_JS_FILE,
lineNumber: 10,
columnNumber: 3,
enabled: true,
snippet: 'const a;',
}) {
const testData = [
createLocationTestData(params.file, params.lineNumber, params.columnNumber, params.enabled, params.snippet),
];
const {breakpointManager, settings} = createStubBreakpointManagerAndSettingsWithMockdata(testData);
const controller = SourcesComponents.BreakpointsView.BreakpointsSidebarController.instance(
{forceNew: true, breakpointManager, settings});
const data = await controller.getUpdatedBreakpointViewData();
assert.lengthOf(data.groups, 1);
assert.lengthOf(data.groups[0].breakpointItems, 1);
const locations = Breakpoints.BreakpointManager.BreakpointManager.instance().allBreakpointLocations();
assert.lengthOf(locations, 1);
return {controller, groups: data.groups, location: locations[0]};
}
class MockRevealer<T> implements Common.Revealer.Revealer<T> {
async reveal(_revealable: T, _omitFocus?: boolean): Promise<void> {
}
}
async function createAndInitializeBreakpointsView(): Promise<SourcesComponents.BreakpointsView.BreakpointsView> {
// Force creation of a new BreakpointsView singleton so that it gets correctly re-wired with
// the current controller singleton (to pick up the latest breakpoint state).
const component = SourcesComponents.BreakpointsView.BreakpointsView.instance({forceNew: true});
await RenderCoordinator.done(); // Wait until the initial rendering finishes.
renderElementIntoDOM(component);
return component;
}
async function renderNoBreakpoints({pauseOnUncaughtExceptions, pauseOnCaughtExceptions}:
{pauseOnUncaughtExceptions: boolean, pauseOnCaughtExceptions: boolean}):
Promise<SourcesComponents.BreakpointsView.BreakpointsView> {
const component = await createAndInitializeBreakpointsView();
component.data = {
breakpointsActive: true,
pauseOnUncaughtExceptions,
pauseOnCaughtExceptions,
groups: [],
};
await RenderCoordinator.done();
return component;
}
async function renderSingleBreakpoint(
type: SDK.DebuggerModel.BreakpointType = SDK.DebuggerModel.BreakpointType.REGULAR_BREAKPOINT,
hoverText?: string): Promise<{
component: SourcesComponents.BreakpointsView.BreakpointsView,
data: SourcesComponents.BreakpointsView.BreakpointsViewData,
}> {
// Only provide a hover text if it's not a regular breakpoint.
assert.isTrue(!hoverText || type !== SDK.DebuggerModel.BreakpointType.REGULAR_BREAKPOINT);
const component = await createAndInitializeBreakpointsView();
const data: SourcesComponents.BreakpointsView.BreakpointsViewData = {
breakpointsActive: true,
pauseOnUncaughtExceptions: false,
pauseOnCaughtExceptions: false,
groups: [
{
name: 'test1.js',
url: urlString`https://google.com/test1.js`,
editable: true,
expanded: true,
breakpointItems: [
{
id: '1',
location: '1',
codeSnippet: 'const a = 0;',
isHit: true,
status: SourcesComponents.BreakpointsView.BreakpointStatus.ENABLED,
type,
hoverText,
},
],
},
],
};
component.data = data;
await RenderCoordinator.done();
return {component, data};
}
async function renderMultipleBreakpoints(): Promise<{
component: SourcesComponents.BreakpointsView.BreakpointsView,
data: SourcesComponents.BreakpointsView.BreakpointsViewData,
}> {
const component = await createAndInitializeBreakpointsView();
const data: SourcesComponents.BreakpointsView.BreakpointsViewData = {
breakpointsActive: true,
pauseOnUncaughtExceptions: false,
pauseOnCaughtExceptions: false,
groups: [
{
name: 'test1.js',
url: urlString`https://google.com/test1.js`,
editable: true,
expanded: true,
breakpointItems: [
{
id: '1',
type: SDK.DebuggerModel.BreakpointType.REGULAR_BREAKPOINT,
location: '234',
codeSnippet: 'const a = x;',
isHit: false,
status: SourcesComponents.BreakpointsView.BreakpointStatus.ENABLED,
},
{
id: '2',
type: SDK.DebuggerModel.BreakpointType.REGULAR_BREAKPOINT,
location: '3:3',
codeSnippet: 'if (x > a) {',
isHit: true,
status: SourcesComponents.BreakpointsView.BreakpointStatus.DISABLED,
},
],
},
{
name: 'test2.js',
url: urlString`https://google.com/test2.js`,
editable: false,
expanded: true,
breakpointItems: [
{
id: '3',
type: SDK.DebuggerModel.BreakpointType.REGULAR_BREAKPOINT,
location: '11',
codeSnippet: 'const y;',
isHit: false,
status: SourcesComponents.BreakpointsView.BreakpointStatus.ENABLED,
},
],
},
{
name: 'main.js',
url: urlString`https://test.com/main.js`,
editable: true,
expanded: false,
breakpointItems: [
{
id: '4',
type: SDK.DebuggerModel.BreakpointType.REGULAR_BREAKPOINT,
location: '3',
codeSnippet: 'if (a == 0) {',
isHit: false,
status: SourcesComponents.BreakpointsView.BreakpointStatus.ENABLED,
},
],
},
],
};
component.data = data;
await RenderCoordinator.done();
return {component, data};
}
function extractBreakpointItems(data: SourcesComponents.BreakpointsView.BreakpointsViewData):
SourcesComponents.BreakpointsView.BreakpointItem[] {
const breakpointItems = data.groups.flatMap(group => group.breakpointItems);
assert.isAbove(breakpointItems.length, 0);
return breakpointItems;
}
function checkCodeSnippet(
renderedBreakpointItem: HTMLDivElement, breakpointItem: SourcesComponents.BreakpointsView.BreakpointItem): void {
const snippetElement = renderedBreakpointItem.querySelector(CODE_SNIPPET_SELECTOR);
assert.instanceOf(snippetElement, HTMLSpanElement);
assert.strictEqual(snippetElement.textContent, breakpointItem.codeSnippet);
}
function checkCheckboxState(
checkbox: HTMLInputElement, breakpointItem: SourcesComponents.BreakpointsView.BreakpointItem): void {
const checked = checkbox.checked;
const indeterminate = checkbox.indeterminate;
if (breakpointItem.status === SourcesComponents.BreakpointsView.BreakpointStatus.INDETERMINATE) {
assert.isTrue(indeterminate);
} else {
assert.isFalse(indeterminate);
assert.strictEqual((breakpointItem.status === SourcesComponents.BreakpointsView.BreakpointStatus.ENABLED), checked);
}
}
function checkGroupNames(
renderedGroupElements: Element[], breakpointGroups: SourcesComponents.BreakpointsView.BreakpointGroup[]): void {
assert.lengthOf(renderedGroupElements, breakpointGroups.length);
for (let i = 0; i < renderedGroupElements.length; ++i) {
const renderedGroup = renderedGroupElements[i];
assert.instanceOf(renderedGroup, HTMLDetailsElement);
const titleElement = renderedGroup.querySelector(GROUP_NAME_SELECTOR);
assert.instanceOf(titleElement, HTMLSpanElement);
assert.strictEqual(titleElement.textContent, breakpointGroups[i].name);
}
}
function hover(component: SourcesComponents.BreakpointsView.BreakpointsView, selector: string): Promise<void> {
assert.isNotNull(component.shadowRoot);
// Dispatch a mouse over.
component.shadowRoot.querySelector(selector)?.dispatchEvent(new Event('mouseover'));
// Wait until the re-rendering has happened.
return RenderCoordinator.done();
}
describeWithEnvironment('BreakpointsSidebarController', () => {
after(() => {
SourcesComponents.BreakpointsView.BreakpointsSidebarController.removeInstance();
});
it('can remove a breakpoint', async () => {
const {groups, location} = await setUpTestWithOneBreakpointLocation();
const breakpoint = location.breakpoint as sinon.SinonStubbedInstance<Breakpoints.BreakpointManager.Breakpoint>;
const breakpointItem = groups[0].breakpointItems[0];
SourcesComponents.BreakpointsView.BreakpointsSidebarController.instance().breakpointsRemoved([breakpointItem]);
assert.isTrue(breakpoint.remove.calledOnceWith(false));
});
it('changes breakpoint state', async () => {
const {groups, location} = await setUpTestWithOneBreakpointLocation();
const breakpointItem = groups[0].breakpointItems[0];
assert.strictEqual(breakpointItem.status, SourcesComponents.BreakpointsView.BreakpointStatus.ENABLED);
const breakpoint = location.breakpoint as sinon.SinonStubbedInstance<Breakpoints.BreakpointManager.Breakpoint>;
SourcesComponents.BreakpointsView.BreakpointsSidebarController.instance().breakpointStateChanged(
breakpointItem, false);
sinon.assert.calledWith(breakpoint.setEnabled, false);
});
it('correctly reveals source location', async () => {
const {groups, location: {uiLocation}} = await setUpTestWithOneBreakpointLocation();
const breakpointItem = groups[0].breakpointItems[0];
const revealer = sinon.createStubInstance(MockRevealer<Workspace.UISourceCode.UILocation>);
Common.Revealer.registerRevealer({
contextTypes() {
return [Workspace.UISourceCode.UILocation];
},
destination: Common.Revealer.RevealerDestination.SOURCES_PANEL,
async loadRevealer() {
return revealer;
},
});
await SourcesComponents.BreakpointsView.BreakpointsSidebarController.instance().jumpToSource(breakpointItem);
assert.isTrue(revealer.reveal.calledOnceWith(uiLocation));
});
it('correctly reveals breakpoint editor', async () => {
const {groups, location} = await setUpTestWithOneBreakpointLocation();
const breakpointItem = groups[0].breakpointItems[0];
const revealer = sinon.createStubInstance(MockRevealer<Breakpoints.BreakpointManager.BreakpointLocation>);
Common.Revealer.registerRevealer({
contextTypes() {
return [Breakpoints.BreakpointManager.BreakpointLocation];
},
destination: Common.Revealer.RevealerDestination.SOURCES_PANEL,
async loadRevealer() {
return revealer;
},
});
await SourcesComponents.BreakpointsView.BreakpointsSidebarController.instance().breakpointEdited(
breakpointItem, false /* editButtonClicked */);
assert.isTrue(revealer.reveal.calledOnceWith(location));
});
describe('getUpdatedBreakpointViewData', () => {
it('extracts breakpoint data', async () => {
const testData = [
createLocationTestData(HELLO_JS_FILE, 3, 10),
createLocationTestData(TEST_JS_FILE, 1, 1),
];
const {breakpointManager, settings} = createStubBreakpointManagerAndSettingsWithMockdata(testData);
const controller = SourcesComponents.BreakpointsView.BreakpointsSidebarController.instance(
{forceNew: true, breakpointManager, settings});
const actual = await controller.getUpdatedBreakpointViewData();
const createExpectedBreakpointGroups = (testData: LocationTestData) => {
const status = testData.enabled ? SourcesComponents.BreakpointsView.BreakpointStatus.ENABLED :
SourcesComponents.BreakpointsView.BreakpointStatus.DISABLED;
let type = SDK.DebuggerModel.BreakpointType.REGULAR_BREAKPOINT;
if (testData.condition) {
if (testData.isLogpoint) {
type = SDK.DebuggerModel.BreakpointType.LOGPOINT;
} else {
type = SDK.DebuggerModel.BreakpointType.CONDITIONAL_BREAKPOINT;
}
}
return {
name: testData.url as string,
url: testData.url,
editable: true,
expanded: true,
breakpointItems: [
{
id: `${testData.url}:${testData.lineNumber}:${testData.columnNumber}`,
location: `${testData.lineNumber + 1}`,
codeSnippet: '',
isHit: false,
status,
type,
hoverText: testData.hoverText,
},
],
};
};
const expected: SourcesComponents.BreakpointsView.BreakpointsViewData = {
breakpointsActive: true,
pauseOnUncaughtExceptions: false,
pauseOnCaughtExceptions: false,
groups: testData.map(createExpectedBreakpointGroups),
};
assert.deepEqual(actual, expected);
});
it('respects the breakpointsActive setting', async () => {
const {breakpointManager, settings} = createStubBreakpointManagerAndSettingsWithMockdata([]);
const controller = SourcesComponents.BreakpointsView.BreakpointsSidebarController.instance(
{forceNew: true, breakpointManager, settings});
settings.moduleSetting('breakpoints-active').set(true);
let data = await controller.getUpdatedBreakpointViewData();
assert.isTrue(data.breakpointsActive);
settings.moduleSetting('breakpoints-active').set(false);
data = await controller.getUpdatedBreakpointViewData();
assert.isFalse(data.breakpointsActive);
});
it('marks groups as editable based on conditional breakpoint support', async () => {
const testData = [
createLocationTestData(HELLO_JS_FILE, 3, 10),
createLocationTestData(TEST_JS_FILE, 1, 1),
];
const {breakpointManager, settings} = createStubBreakpointManagerAndSettingsWithMockdata(testData);
const controller = SourcesComponents.BreakpointsView.BreakpointsSidebarController.instance(
{forceNew: true, breakpointManager, settings});
breakpointManager.supportsConditionalBreakpoints.returns(false);
for (const group of (await controller.getUpdatedBreakpointViewData()).groups) {
assert.isFalse(group.editable);
}
breakpointManager.supportsConditionalBreakpoints.returns(true);
for (const group of (await controller.getUpdatedBreakpointViewData()).groups) {
assert.isTrue(group.editable);
}
});
it('groups breakpoints that are in the same file', async () => {
const testData = [
createLocationTestData(HELLO_JS_FILE, 3, 10),
createLocationTestData(TEST_JS_FILE, 1, 1),
];
const {breakpointManager, settings} = createStubBreakpointManagerAndSettingsWithMockdata(testData);
const controller = SourcesComponents.BreakpointsView.BreakpointsSidebarController.instance(
{forceNew: true, breakpointManager, settings});
const actualViewData = await controller.getUpdatedBreakpointViewData();
assert.lengthOf(actualViewData.groups, 2);
assert.lengthOf(actualViewData.groups[0].breakpointItems, 1);
assert.lengthOf(actualViewData.groups[1].breakpointItems, 1);
});
it('correctly sets the name of the group', async () => {
const {groups} = await setUpTestWithOneBreakpointLocation(
{file: HELLO_JS_FILE, lineNumber: 0, columnNumber: 0, enabled: false});
assert.strictEqual(groups[0].name, HELLO_JS_FILE);
});
it('only extracts the line number as location if one breakpoint is on that line', async () => {
const {groups} = await setUpTestWithOneBreakpointLocation(
{file: HELLO_JS_FILE, lineNumber: 4, columnNumber: 0, enabled: false});
assert.strictEqual(groups[0].breakpointItems[0].location, '5');
});
it('extracts the line number and column number as location if more than one breakpoint is on that line',
async () => {
const testData = [
createLocationTestData(HELLO_JS_FILE, 3, 10),
createLocationTestData(HELLO_JS_FILE, 3, 15),
];
const {breakpointManager, settings} = createStubBreakpointManagerAndSettingsWithMockdata(testData);
const controller = SourcesComponents.BreakpointsView.BreakpointsSidebarController.instance(
{forceNew: true, breakpointManager, settings});
const actualViewData = await controller.getUpdatedBreakpointViewData();
assert.lengthOf(actualViewData.groups, 1);
assert.lengthOf(actualViewData.groups[0].breakpointItems, 2);
assert.strictEqual(actualViewData.groups[0].breakpointItems[0].location, '4:11');
assert.strictEqual(actualViewData.groups[0].breakpointItems[1].location, '4:16');
});
it('orders breakpoints within a file by location', async () => {
const testData = [
createLocationTestData(HELLO_JS_FILE, 3, 15),
createLocationTestData(HELLO_JS_FILE, 3, 10),
];
const {breakpointManager, settings} = createStubBreakpointManagerAndSettingsWithMockdata(testData);
const controller = SourcesComponents.BreakpointsView.BreakpointsSidebarController.instance(
{forceNew: true, breakpointManager, settings});
const actualViewData = await controller.getUpdatedBreakpointViewData();
assert.lengthOf(actualViewData.groups, 1);
assert.lengthOf(actualViewData.groups[0].breakpointItems, 2);
assert.strictEqual(actualViewData.groups[0].breakpointItems[0].location, '4:11');
assert.strictEqual(actualViewData.groups[0].breakpointItems[1].location, '4:16');
});
it('orders breakpoints within groups by location', async () => {
const testData = [
createLocationTestData(TEST_JS_FILE, 3, 15),
createLocationTestData(HELLO_JS_FILE, 3, 10),
];
const {breakpointManager, settings} = createStubBreakpointManagerAndSettingsWithMockdata(testData);
const controller = SourcesComponents.BreakpointsView.BreakpointsSidebarController.instance(
{forceNew: true, breakpointManager, settings});
const actualViewData = await controller.getUpdatedBreakpointViewData();
assert.lengthOf(actualViewData.groups, 2);
const names = actualViewData.groups.map(group => group.name);
assert.deepEqual(names, [HELLO_JS_FILE, TEST_JS_FILE]);
});
it('merges breakpoints mapping to the same location into one', async () => {
const testData = [
createLocationTestData(TEST_JS_FILE, 3, 15),
createLocationTestData(TEST_JS_FILE, 3, 15),
];
const {breakpointManager, settings} = createStubBreakpointManagerAndSettingsWithMockdata(testData);
const controller = SourcesComponents.BreakpointsView.BreakpointsSidebarController.instance(
{forceNew: true, breakpointManager, settings});
const actualViewData = await controller.getUpdatedBreakpointViewData();
assert.lengthOf(actualViewData.groups, 1);
assert.lengthOf(actualViewData.groups[0].breakpointItems, 1);
});
it('correctly extracts the enabled state', async () => {
const {groups} =
await setUpTestWithOneBreakpointLocation({file: '', lineNumber: 0, columnNumber: 0, enabled: true});
const breakpointItem = groups[0].breakpointItems[0];
assert.strictEqual(breakpointItem.status, SourcesComponents.BreakpointsView.BreakpointStatus.ENABLED);
});
it('correctly extracts the enabled state', async () => {
const {groups} =
await setUpTestWithOneBreakpointLocation({file: '', lineNumber: 0, columnNumber: 0, enabled: false});
const breakpointItem = groups[0].breakpointItems[0];
assert.strictEqual(breakpointItem.status, SourcesComponents.BreakpointsView.BreakpointStatus.DISABLED);
});
it('correctly extracts the enabled state', async () => {
const testData = [
createLocationTestData(TEST_JS_FILE, 3, 15, true /* enabled */),
createLocationTestData(TEST_JS_FILE, 3, 15, false /* enabled */),
];
const {breakpointManager, settings} = createStubBreakpointManagerAndSettingsWithMockdata(testData);
const controller = SourcesComponents.BreakpointsView.BreakpointsSidebarController.instance(
{forceNew: true, breakpointManager, settings});
const actualViewData = await controller.getUpdatedBreakpointViewData();
assert.lengthOf(actualViewData.groups, 1);
assert.lengthOf(actualViewData.groups[0].breakpointItems, 1);
assert.strictEqual(
actualViewData.groups[0].breakpointItems[0].status,
SourcesComponents.BreakpointsView.BreakpointStatus.INDETERMINATE);
});
it('correctly extracts the disabled state', async () => {
const snippet = 'const a = x;';
const {groups} =
await setUpTestWithOneBreakpointLocation({file: '', lineNumber: 0, columnNumber: 0, enabled: false, snippet});
assert.strictEqual(groups[0].breakpointItems[0].codeSnippet, snippet);
});
it('correctly extracts the indeterminate state', async () => {
const testData = [
createLocationTestData(TEST_JS_FILE, 3, 15, true /* enabled */),
createLocationTestData(TEST_JS_FILE, 3, 15, false /* enabled */),
];
const {breakpointManager, settings} = createStubBreakpointManagerAndSettingsWithMockdata(testData);
const controller = SourcesComponents.BreakpointsView.BreakpointsSidebarController.instance(
{forceNew: true, breakpointManager, settings});
const actualViewData = await controller.getUpdatedBreakpointViewData();
assert.lengthOf(actualViewData.groups, 1);
assert.lengthOf(actualViewData.groups[0].breakpointItems, 1);
assert.strictEqual(
actualViewData.groups[0].breakpointItems[0].status,
SourcesComponents.BreakpointsView.BreakpointStatus.INDETERMINATE);
});
it('correctly extracts conditional breakpoints', async () => {
const condition = 'x < a' as Breakpoints.BreakpointManager.UserCondition;
const testData = [
createLocationTestData(
TEST_JS_FILE, 3, 15, true /* enabled */, '', condition, false /* isLogpoint */, condition),
];
const {breakpointManager, settings} = createStubBreakpointManagerAndSettingsWithMockdata(testData);
const controller = SourcesComponents.BreakpointsView.BreakpointsSidebarController.instance(
{forceNew: true, breakpointManager, settings});
const actualViewData = await controller.getUpdatedBreakpointViewData();
assert.lengthOf(actualViewData.groups, 1);
assert.lengthOf(actualViewData.groups[0].breakpointItems, 1);
const breakpointItem = actualViewData.groups[0].breakpointItems[0];
assert.strictEqual(breakpointItem.type, SDK.DebuggerModel.BreakpointType.CONDITIONAL_BREAKPOINT);
assert.strictEqual(breakpointItem.hoverText, condition);
});
it('correctly extracts logpoints', async () => {
const logExpression = 'x' as Breakpoints.BreakpointManager.UserCondition;
const testData = [
createLocationTestData(
TEST_JS_FILE, 3, 15, true /* enabled */, '', logExpression, true /* isLogpoint */, logExpression),
];
const {breakpointManager, settings} = createStubBreakpointManagerAndSettingsWithMockdata(testData);
const controller = SourcesComponents.BreakpointsView.BreakpointsSidebarController.instance(
{forceNew: true, breakpointManager, settings});
const actualViewData = await controller.getUpdatedBreakpointViewData();
assert.lengthOf(actualViewData.groups, 1);
assert.lengthOf(actualViewData.groups[0].breakpointItems, 1);
const breakpointItem = actualViewData.groups[0].breakpointItems[0];
assert.strictEqual(breakpointItem.type, SDK.DebuggerModel.BreakpointType.LOGPOINT);
assert.strictEqual(breakpointItem.hoverText, logExpression);
});
describe('breakpoint groups', () => {
it('are expanded by default', async () => {
const {controller} = await setUpTestWithOneBreakpointLocation();
const actualViewData = await controller.getUpdatedBreakpointViewData();
assert.isTrue(actualViewData.groups[0].expanded);
});
it('are collapsed if user collapses it', async () => {
const {controller, groups} = await setUpTestWithOneBreakpointLocation();
controller.expandedStateChanged(groups[0].url, false /* expanded */);
const actualViewData = await controller.getUpdatedBreakpointViewData();
assert.isFalse(actualViewData.groups[0].expanded);
});
it('are expanded if user expands it', async () => {
const {controller, groups} = await setUpTestWithOneBreakpointLocation();
controller.expandedStateChanged(groups[0].url, true /* expanded */);
const actualViewData = await controller.getUpdatedBreakpointViewData();
assert.isTrue(actualViewData.groups[0].expanded);
});
it('remember the collapsed state', async () => {
{
const {controller, groups} = await setUpTestWithOneBreakpointLocation();
controller.expandedStateChanged(groups[0].url, false /* expanded */);
const actualViewData = await controller.getUpdatedBreakpointViewData();
assert.isFalse(actualViewData.groups[0].expanded);
}
// A new controller is created and initialized with the expanded settings.
{const breakpointManager = Breakpoints.BreakpointManager.BreakpointManager.instance();
const settings = Common.Settings.Settings.instance();
const controller = SourcesComponents.BreakpointsView.BreakpointsSidebarController.instance({
forceNew: true,
breakpointManager,
settings,
});
const actualViewData = await controller.getUpdatedBreakpointViewData();
assert.isFalse(actualViewData.groups[0].expanded);}
});
it('remember the expanded state', async () => {
{
const {controller, groups} = await setUpTestWithOneBreakpointLocation();
controller.expandedStateChanged(groups[0].url, true /* expanded */);
const actualViewData = await controller.getUpdatedBreakpointViewData();
assert.isTrue(actualViewData.groups[0].expanded);
}
// A new controller is created and initialized with the expanded settings.
{
const controller = SourcesComponents.BreakpointsView.BreakpointsSidebarController.instance({
forceNew: true,
breakpointManager: Breakpoints.BreakpointManager.BreakpointManager.instance(),
settings: Common.Settings.Settings.instance(),
});
const actualViewData = await controller.getUpdatedBreakpointViewData();
assert.isTrue(actualViewData.groups[0].expanded);
}
});
});
});
});
describeWithMockConnection('BreakpointsSidebarController', () => {
beforeEach(() => {
const workspace = Workspace.Workspace.WorkspaceImpl.instance();
const targetManager = SDK.TargetManager.TargetManager.instance();
const resourceMapping = new Bindings.ResourceMapping.ResourceMapping(targetManager, workspace);
const debuggerWorkspaceBinding = Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding.instance({
forceNew: true,
resourceMapping,
targetManager,
});
Breakpoints.BreakpointManager.BreakpointManager.instance(
{forceNew: true, targetManager, workspace, debuggerWorkspaceBinding});
});
const DEFAULT_BREAKPOINT:
[Breakpoints.BreakpointManager.UserCondition, boolean, boolean, Breakpoints.BreakpointManager.BreakpointOrigin] =
[
Breakpoints.BreakpointManager.EMPTY_BREAKPOINT_CONDITION,
true, // enabled
false, // isLogpoint
Breakpoints.BreakpointManager.BreakpointOrigin.USER_ACTION,
];
// Flaky
it.skip('[crbug.com/345456307] auto-expands if a user adds a new breakpoint', async () => {
const breakpointManager = Breakpoints.BreakpointManager.BreakpointManager.instance();
const settings = Common.Settings.Settings.instance();
const {uiSourceCode, project} =
createContentProviderUISourceCode({url: urlString`test.js`, mimeType: 'text/javascript'});
const controller = SourcesComponents.BreakpointsView.BreakpointsSidebarController.instance(
{forceNew: true, breakpointManager, settings});
// Add one breakpoint and collapse the tree.
const b1 = await breakpointManager.setBreakpoint(uiSourceCode, 0, 0, ...DEFAULT_BREAKPOINT);
assert.exists(b1);
{
controller.expandedStateChanged(uiSourceCode.url(), false /* expanded */);
const data = await controller.getUpdatedBreakpointViewData();
assert.lengthOf(data.groups, 1);
assert.lengthOf(data.groups[0].breakpointItems, 1);
assert.isFalse(data.groups[0].expanded);
}
// Add a new breakpoint and check if it's expanded as expected.
const b2 = await breakpointManager.setBreakpoint(uiSourceCode, 0, 3, ...DEFAULT_BREAKPOINT);
assert.exists(b2);
{
const data = await controller.getUpdatedBreakpointViewData();
assert.lengthOf(data.groups, 1);
assert.lengthOf(data.groups[0].breakpointItems, 2);
assert.isTrue(data.groups[0].expanded);
}
// Clean up.
await b1.remove(false /* keepInStorage */);
await b2.remove(false /* keepInStorage */);
Workspace.Workspace.WorkspaceImpl.instance().removeProject(project);
});
it('does not auto-expand if a breakpoint was not triggered by user action', async () => {
const breakpointManager = Breakpoints.BreakpointManager.BreakpointManager.instance();
const settings = Common.Settings.Settings.instance();
const {uiSourceCode, project} =
createContentProviderUISourceCode({url: urlString`test.js`, mimeType: 'text/javascript'});
const controller = SourcesComponents.BreakpointsView.BreakpointsSidebarController.instance(
{forceNew: true, breakpointManager, settings});
// Add one breakpoint and collapse the tree.
const b1 = await breakpointManager.setBreakpoint(uiSourceCode, 0, 0, ...DEFAULT_BREAKPOINT);
assert.exists(b1);
{
controller.expandedStateChanged(uiSourceCode.url(), false /* expanded */);
const data = await controller.getUpdatedBreakpointViewData();
assert.lengthOf(data.groups, 1);
assert.lengthOf(data.groups[0].breakpointItems, 1);
assert.isFalse(data.groups[0].expanded);
}
// Add a new non-user triggered breakpoint and check if it's still collapsed.
const b2 = await breakpointManager.setBreakpoint(
uiSourceCode, 0, 3, Breakpoints.BreakpointManager.EMPTY_BREAKPOINT_CONDITION, true, false,
Breakpoints.BreakpointManager.BreakpointOrigin.OTHER);
assert.exists(b2);
{
const data = await controller.getUpdatedBreakpointViewData();
assert.lengthOf(data.groups, 1);
assert.lengthOf(data.groups[0].breakpointItems, 2);
assert.isFalse(data.groups[0].expanded);
}
// Clean up.
await b1.remove(false /* keepInStorage */);
await b2.remove(false /* keepInStorage */);
Workspace.Workspace.WorkspaceImpl.instance().removeProject(project);
});
it('auto-expands if a breakpoint was hit', async () => {
sinon.stub(
Common.Revealer.RevealerRegistry.instance(),
'reveal'); // Prevent pending reveal promises after tests are done.
const backend = new MockProtocolBackend();
const target = createTarget();
target.targetManager().setScopeTarget(target);
const breakpointManager = Breakpoints.BreakpointManager.BreakpointManager.instance();
const sourceRoot = 'http://example.com';
const scriptInfo = {
url: `${sourceRoot}/foo.js`,
content: 'foo();\n',
};
const debuggerWorkspaceBinding = Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding.instance();
const uiSourceCodePromise =
debuggerWorkspaceBinding.waitForUISourceCodeAdded(urlString`${`${sourceRoot}/foo.js`}`, target);
const script = await backend.addScript(target, scriptInfo, null);
await uiSourceCodePromise;
const uiSourceCode = debuggerWorkspaceBinding.uiSourceCodeForScript(script);
assert.exists(uiSourceCode);
// Set up sdk and ui location, and a mapping between them, such that we can identify that
// the hit breakpoint is the one we are adding.
const responderPromise = backend.responderToBreakpointByUrlRequest(uiSourceCode.url(), 0)({
breakpointId: 'DUMMY_BREAKPOINT' as Protocol.Debugger.BreakpointId,
locations: [{
scriptId: script.scriptId,
lineNumber: 0,
columnNumber: 0,
}]
});
const b1 = await breakpointManager.setBreakpoint(uiSourceCode, 0, 0, ...DEFAULT_BREAKPOINT);
assert.exists(b1);
await responderPromise;
const controller = SourcesComponents.BreakpointsView.BreakpointsSidebarController.instance(
{forceNew: true, breakpointManager, settings: Common.Settings.Settings.instance()});
controller.expandedStateChanged(uiSourceCode.url(), false /* expanded */);
// Double check that the group is collapsed.
{
const data = await controller.getUpdatedBreakpointViewData();
assert.isFalse(data.groups[0].expanded);
}
// Simulating a breakpoint hit. Update the DebuggerPausedDetails to contain the info on the hit breakpoint.
const callFrame = sinon.createStubInstance(SDK.DebuggerModel.CallFrame);
callFrame.location.returns(new SDK.DebuggerModel.Location(script.debuggerModel, script.scriptId, 0, 0));
const pausedDetails = sinon.createStubInstance(SDK.DebuggerModel.DebuggerPausedDetails);
pausedDetails.callFrames = [callFrame];
// Instead of setting the flavor, directly call `flavorChanged` on the controller and mock what it's set to.
// Setting the flavor would have other listeners listening to it, and would cause undesirable side effects.
sinon.stub(UI.Context.Context.instance(), 'flavor')
.callsFake(flavorType => flavorType === SDK.DebuggerModel.DebuggerPausedDetails ? pausedDetails : null);
{
const data = await controller.getUpdatedBreakpointViewData();
// Assert that the breakpoint is hit and the group is expanded.
assert.isTrue(data.groups[0].breakpointItems[0].isHit);
assert.isTrue(data.groups[0].expanded);
}
// Clean up.
await b1.remove(false /* keepInStorage */);
});
it('changes pause on exception state', async () => {
const {breakpointManager, settings} = createStubBreakpointManagerAndSettings();
breakpointManager.allBreakpointLocations.returns([]);
const controller = SourcesComponents.BreakpointsView.BreakpointsSidebarController.instance(
{forceNew: true, breakpointManager, settings});
for (const pauseOnUncaughtExceptions of [true, false]) {
for (const pauseOnCaughtExceptions of [true, false]) {
controller.setPauseOnUncaughtExceptions(pauseOnUncaughtExceptions);
controller.setPauseOnCaughtExceptions(pauseOnCaughtExceptions);
const data = await controller.getUpdatedBreakpointViewData();
assert.strictEqual(data.pauseOnUncaughtExceptions, pauseOnUncaughtExceptions);
assert.strictEqual(data.pauseOnCaughtExceptions, pauseOnCaughtExceptions);
assert.strictEqual(settings.moduleSetting('pause-on-uncaught-exception').get(), pauseOnUncaughtExceptions);
assert.strictEqual(settings.moduleSetting('pause-on-caught-exception').get(), pauseOnCaughtExceptions);
}
}
});
});
describeWithMockConnection('BreakpointsView', () => {
beforeEach(() => {
const workspace = Workspace.Workspace.WorkspaceImpl.instance();
const targetManager = SDK.TargetManager.TargetManager.instance();
const resourceMapping = new Bindings.ResourceMapping.ResourceMapping(targetManager, workspace);
const debuggerWorkspaceBinding = Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding.instance({
forceNew: true,
resourceMapping,
targetManager,
});
Breakpoints.BreakpointManager.BreakpointManager.instance(
{forceNew: true, targetManager, workspace, debuggerWorkspaceBinding});
});
it('correctly expands breakpoint groups', async () => {
const {component, data} = await renderMultipleBreakpoints();
assert.isNotNull(component.shadowRoot);
const expandedGroups = data.groups.filter(group => group.expanded);
assert.isAbove(expandedGroups.length, 0);
const renderedExpandedGroups = Array.from(component.shadowRoot.querySelectorAll(EXPANDED_GROUPS_SELECTOR));
assert.lengthOf(renderedExpandedGroups, expandedGroups.length);
checkGroupNames(renderedExpandedGroups, expandedGroups);
});
it('correctly collapses breakpoint groups', async () => {
const {component, data} = await renderMultipleBreakpoints();
assert.isNotNull(component.shadowRoot);
const collapsedGroups = data.groups.filter(group => !group.expanded);
assert.isAbove(collapsedGroups.length, 0);
const renderedCollapsedGroups = Array.from(component.shadowRoot.querySelectorAll(COLLAPSED_GROUPS_SELECTOR));
checkGroupNames(renderedCollapsedGroups, collapsedGroups);
});
it('renders the group names', async () => {
const {component, data} = await renderMultipleBreakpoints();
assert.isNotNull(component.shadowRoot);
const renderedGroupNames = component.shadowRoot.querySelectorAll(GROUP_NAME_SELECTOR);
assertElements(renderedGroupNames, HTMLSpanElement);
const expectedNames = data.groups.flatMap(group => group.name);
const actualNames = [];
for (const renderedGroupName of renderedGroupNames.values()) {
actualNames.push(renderedGroupName.textContent);
}
assert.deepEqual(actualNames, expectedNames);
});
it('renders the breakpoints with their checkboxes', async () => {
const {component, data} = await renderMultipleBreakpoints();
assert.isNotNull(component.shadowRoot);
const renderedBreakpointItems = Array.from(component.shadowRoot.querySelectorAll(BREAKPOINT_ITEM_SELECTOR));
const breakpointItems = extractBreakpointItems(data);
assert.lengthOf(renderedBreakpointItems, breakpointItems.length);
for (let i = 0; i < renderedBreakpointItems.length; ++i) {
const renderedItem = renderedBreakpointItems[i];
assert.instanceOf(renderedItem, HTMLDivElement);
const inputElement = renderedItem.querySelector('input');
assert.instanceOf(inputElement, HTMLInputElement);
checkCheckboxState(inputElement, breakpointItems[i]);
}
});
it('renders breakpoints with their code snippet', async () => {
const {component, data} = await renderMultipleBreakpoints();
assert.isNotNull(component.shadowRoot);
const renderedBreakpointItems = Array.from(component.shadowRoot.querySelectorAll(BREAKPOINT_ITEM_SELECTOR));
const breakpointItems = extractBreakpointItems(data);
assert.lengthOf(renderedBreakpointItems, breakpointItems.length);
for (let i = 0; i < renderedBreakpointItems.length; ++i) {
const renderedBreakpointItem = renderedBreakpointItems[i];
assert.instanceOf(renderedBreakpointItem, HTMLDivElement);
checkCodeSnippet(renderedBreakpointItem, breakpointItems[i]);
}
});
it('renders breakpoint groups with a differentiator if the file names are not unique', async () => {
const component = await createAndInitializeBreakpointsView();
const groupTemplate = {
name: 'index.js',
url: urlString``,
editable: true,
expanded: true,
breakpointItems: [
{
id: '1',
type: SDK.DebuggerModel.BreakpointType.REGULAR_BREAKPOINT,
location: '234',
codeSnippet: 'const a = x;',
isHit: false,
status: SourcesComponents.BreakpointsView.BreakpointStatus.ENABLED,
},
],
};
// Create two groups with the same file name, but different url.
const group1 = {...groupTemplate};
group1.url = urlString`https://google.com/lib/index.js`;
const group2 = {...groupTemplate};
group2.url = urlString`https://google.com/src/index.js`;
const data: SourcesComponents.BreakpointsView.BreakpointsViewData = {
breakpointsActive: true,
pauseOnUncaughtExceptions: false,
pauseOnCaughtExceptions: false,
groups: [
group1,
group2,
],
};
component.data = data;
await RenderCoordinator.done();
assert.isNotNull(component.shadowRoot);
const groupSummaries = Array.from(component.shadowRoot.querySelectorAll(SUMMARY_SELECTOR));
const differentiatingPath = groupSummaries.map(group => {
const differentiatorElement = group.querySelector(GROUP_DIFFERENTIATOR_SELECTOR);
assert.instanceOf(differentiatorElement, HTMLSpanElement);
return differentiatorElement.textContent;
});
assert.deepEqual(differentiatingPath, ['lib/', 'src/']);
});
it('renders breakpoints with a differentiating path', async () => {
const {component, data} = await renderMultipleBreakpoints();
assert.isNotNull(component.shadowRoot);
const renderedBreakpointItems = Array.from(component.shadowRoot.querySelectorAll(BREAKPOINT_ITEM_SELECTOR));
const breakpointItems = extractBreakpointItems(data);
assert.lengthOf(renderedBreakpointItems, breakpointItems.length);
for (let i = 0; i < renderedBreakpointItems.length; ++i) {
const renderedBreakpointItem = renderedBreakpointItems[i];
assert.instanceOf(renderedBreakpointItem, HTMLDivElement);
const locationElement = renderedBreakpointItem.querySelector(BREAKPOINT_LOCATION_SELECTOR);
assert.instanceOf(locationElement, HTMLSpanElement);
const actualLocation = locationElement.textContent;
const expectedLocation = breakpointItems[i].location;
assert.strictEqual(actualLocation, expectedLocation);
}
});
it('triggers an event on clicking the checkbox of a breakpoint', async () => {
const {component, data} = await renderSingleBreakpoint();
assert.isNotNull(component.shadowRoot);
const renderedItem = component.shadowRoot.querySelector(BREAKPOINT_ITEM_SELECTOR);
assert.instanceOf(renderedItem, HTMLDivElement);
const checkbox = renderedItem.querySelector('input');
assert.instanceOf(checkbox, HTMLInputElement);
const checked = checkbox.checked;
const controller = SourcesComponents.BreakpointsView.BreakpointsSidebarController.instance();
const breakpointStateChanged = sinon.stub(controller, 'breakpointStateChanged');
checkbox.click();
assert.isTrue(breakpointStateChanged.calledOnceWith(data.groups[0].breakpointItems[0], !checked));
});
it('triggers an event on clicking on the snippet text', async () => {
const {component, data} = await renderSingleBreakpoint();
assert.isNotNull(component.shadowRoot);
const snippet = component.shadowRoot.querySelector(CODE_SNIPPET_SELECTOR);
assert.instanceOf(snippet, HTMLSpanElement);
const controller = SourcesComponents.BreakpointsView.BreakpointsSidebarController.instance();
const jumpToSource = sinon.stub(controller, 'jumpToSource');
snippet.click();
assert.isTrue(jumpToSource.calledOnceWith(data.groups[0].breakpointItems[0]));
});
it('triggers an event on expanding/unexpanding', async () => {
const {component, data} = await renderSingleBreakpoint();
assert.isNotNull(component.shadowRoot);
const renderedGroupName = component.shadowRoot.querySelector(GROUP_NAME_SELECTOR);
assert.instanceOf(renderedGroupName, HTMLSpanElement);
const expandedInitialValue = data.groups[0].expanded;
const controller = SourcesComponents.BreakpointsView.BreakpointsSidebarController.instance();
const expandedStateChanged = sinon.stub(controller, 'expandedStateChanged');
renderedGroupName.click();
await new Promise(resolve => setTimeout(resolve, 0));
const group = data.groups[0];
assert.isTrue(expandedStateChanged.calledOnceWith(group.url, group.expanded));
assert.notStrictEqual(group.expanded, expandedInitialValue);
});
it('highlights breakpoint if it is set to be hit', async () => {
const {component} = await renderSingleBreakpoint();
assert.isNotNull(component.shadowRoot);
const renderedBreakpointItem = component.shadowRoot.querySelector(HIT_BREAKPOINT_SELECTOR);
assert.instanceOf(renderedBreakpointItem, HTMLDivElement);
});
it('triggers an event on removing file breakpoints', async () => {
const {component, data} = await renderMultipleBreakpoints();
assert.isNotNull(component.shadowRoot);
await hover(component, SUMMARY_SELECTOR);
const removeFileBreakpointsButton = component.shadowRoot.query