Skip to content

Commit f89bf80

Browse files
jcscottiiiDevtools-frontend LUCI CQ
authored andcommitted
DevTools: Implement CrashReportContextView component and tests
This CL introduces the CrashReportContextView component which will be used to display crash report entries for each frame in the Application panel. It uses standard Lit templates and the UI.Widget directive to render its contents. This CL also registers the visual elements logging context values for the view and the toolbar actions. Bug: 400432195 Change-Id: I072bf102110ae543b7f4f97c1192da05bbbc45cf Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/7718543 Reviewed-by: Paul Irish <paulirish@chromium.org> Commit-Queue: James Scott <jamescscott@google.com> Reviewed-by: Danil Somsikov <dsv@chromium.org>
1 parent 084eadd commit f89bf80

6 files changed

Lines changed: 448 additions & 0 deletions

File tree

config/gni/devtools_grd_files.gni

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1409,6 +1409,7 @@ grd_files_unbundled_sources = [
14091409
"front_end/panels/application/BackgroundServiceView.js",
14101410
"front_end/panels/application/BounceTrackingMitigationsTreeElement.js",
14111411
"front_end/panels/application/CookieItemsView.js",
1412+
"front_end/panels/application/CrashReportContextView.js",
14121413
"front_end/panels/application/DOMStorageItemsView.js",
14131414
"front_end/panels/application/DOMStorageModel.js",
14141415
"front_end/panels/application/DeviceBoundSessionsModel.js",

front_end/panels/application/BUILD.gn

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ devtools_ui_module("application") {
4343
"BackgroundServiceView.ts",
4444
"BounceTrackingMitigationsTreeElement.ts",
4545
"CookieItemsView.ts",
46+
"CrashReportContextView.ts",
4647
"DOMStorageItemsView.ts",
4748
"DOMStorageModel.ts",
4849
"DeviceBoundSessionsModel.ts",
@@ -169,6 +170,7 @@ devtools_ui_module("unittests") {
169170
"AppManifestView.test.ts",
170171
"ApplicationPanelSidebar.test.ts",
171172
"BackgroundServiceView.test.ts",
173+
"CrashReportContextView.test.ts",
172174
"DOMStorageModel.test.ts",
173175
"DeviceBoundSessionsModel.test.ts",
174176
"DeviceBoundSessionsTreeElement.test.ts",
Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
// Copyright 2026 The Chromium Authors
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import * as SDK from '../../core/sdk/sdk.js';
6+
import type * as Protocol from '../../generated/protocol.js';
7+
import {createTarget} from '../../testing/EnvironmentHelpers.js';
8+
import {describeWithMockConnection, setMockConnectionResponseHandler} from '../../testing/MockConnection.js';
9+
import {createViewFunctionStub} from '../../testing/ViewFunctionHelpers.js';
10+
11+
import * as Application from './application.js';
12+
13+
describeWithMockConnection('CrashReportContextView', () => {
14+
const FRAME_ID = 'frame-1' as Protocol.Page.FrameId;
15+
const ORIGIN = 'https://example.com';
16+
const URL = 'https://example.com/index.html';
17+
18+
let target: SDK.Target.Target;
19+
20+
beforeEach(() => {
21+
target = createTarget();
22+
target.model(SDK.CrashReportContextModel.CrashReportContextModel);
23+
target.model(SDK.ResourceTreeModel.ResourceTreeModel);
24+
});
25+
26+
async function createComponent() {
27+
const view = createViewFunctionStub(Application.CrashReportContextView.CrashReportContextView);
28+
const component = new Application.CrashReportContextView.CrashReportContextView(view);
29+
return {view, component};
30+
}
31+
32+
it('renders frame sections and entries', async () => {
33+
sinon.stub(SDK.FrameManager.FrameManager.instance(), 'getFrame').returns({
34+
url: URL,
35+
securityOrigin: ORIGIN,
36+
isMainFrame: () => true,
37+
displayName: () => URL,
38+
} as unknown as SDK.ResourceTreeModel.ResourceTreeFrame);
39+
40+
setMockConnectionResponseHandler(
41+
'CrashReportContext.getEntries', () => ({
42+
entries: [
43+
{key: 'user_id', value: '12345', frameId: FRAME_ID},
44+
],
45+
}));
46+
47+
const {view} = await createComponent();
48+
const input = await view.nextInput;
49+
50+
assert.lengthOf(input.frames, 1);
51+
assert.strictEqual(input.frames[0].url, URL);
52+
assert.strictEqual(input.frames[0].displayName, URL);
53+
assert.strictEqual(input.frames[0].entries[0].key, 'user_id');
54+
});
55+
56+
it('groups entries by frame', async () => {
57+
const stub = sinon.stub(SDK.FrameManager.FrameManager.instance(), 'getFrame');
58+
stub.withArgs('frame-1' as Protocol.Page.FrameId).returns({
59+
url: 'https://frame1.com',
60+
securityOrigin: 'https://frame1.com',
61+
isMainFrame: () => true,
62+
displayName: () => 'https://frame1.com',
63+
} as unknown as SDK.ResourceTreeModel.ResourceTreeFrame);
64+
stub.withArgs('frame-2' as Protocol.Page.FrameId).returns({
65+
url: 'https://frame2.com',
66+
securityOrigin: 'https://frame2.com',
67+
isMainFrame: () => false,
68+
displayName: () => 'https://frame2.com',
69+
} as unknown as SDK.ResourceTreeModel.ResourceTreeFrame);
70+
71+
setMockConnectionResponseHandler(
72+
'CrashReportContext.getEntries', () => ({
73+
entries: [
74+
{key: 'k1', value: 'v1', frameId: 'frame-1' as Protocol.Page.FrameId},
75+
{key: 'k2', value: 'v2', frameId: 'frame-2' as Protocol.Page.FrameId},
76+
],
77+
}));
78+
79+
const {view} = await createComponent();
80+
const input = await view.nextInput;
81+
82+
assert.lengthOf(input.frames, 2);
83+
assert.strictEqual(input.frames[0].url, 'https://frame1.com');
84+
assert.strictEqual(input.frames[1].url, 'https://frame2.com');
85+
});
86+
87+
it('handles unknown frames by showing a fallback URL', async () => {
88+
// Explicitly return null for the frame lookup
89+
sinon.stub(SDK.FrameManager.FrameManager.instance(), 'getFrame').returns(null);
90+
91+
setMockConnectionResponseHandler(
92+
'CrashReportContext.getEntries',
93+
() => ({
94+
entries: [
95+
{key: 'k1', value: 'v1', frameId: 'unknown-frame' as Protocol.Page.FrameId},
96+
],
97+
}));
98+
99+
const {view} = await createComponent();
100+
const input = await view.nextInput;
101+
102+
assert.lengthOf(input.frames, 1);
103+
assert.strictEqual(input.frames[0].url, 'Unknown Frame');
104+
});
105+
106+
it('disambiguates frames with the same URL', async () => {
107+
const stub = sinon.stub(SDK.FrameManager.FrameManager.instance(), 'getFrame');
108+
const SHARED_URL = 'https://shared.com';
109+
110+
stub.withArgs('frame-main' as Protocol.Page.FrameId).returns({
111+
url: SHARED_URL,
112+
securityOrigin: SHARED_URL,
113+
isMainFrame: () => true,
114+
displayName: () => SHARED_URL,
115+
} as unknown as SDK.ResourceTreeModel.ResourceTreeFrame);
116+
117+
stub.withArgs('frame-sub' as Protocol.Page.FrameId).returns({
118+
url: SHARED_URL,
119+
securityOrigin: SHARED_URL,
120+
isMainFrame: () => false,
121+
displayName: () => SHARED_URL,
122+
} as unknown as SDK.ResourceTreeModel.ResourceTreeFrame);
123+
124+
setMockConnectionResponseHandler(
125+
'CrashReportContext.getEntries', () => ({
126+
entries: [
127+
{key: 'k1', value: 'v1', frameId: 'frame-main' as Protocol.Page.FrameId},
128+
{key: 'k2', value: 'v2', frameId: 'frame-sub' as Protocol.Page.FrameId},
129+
],
130+
}));
131+
132+
const {view} = await createComponent();
133+
const input = await view.nextInput;
134+
135+
assert.lengthOf(input.frames, 2);
136+
assert.strictEqual(input.frames[0].displayName, SHARED_URL);
137+
assert.strictEqual(input.frames[1].displayName, SHARED_URL);
138+
});
139+
140+
it('uses frame.displayName() if available to render titles', async () => {
141+
const stub = sinon.stub(SDK.FrameManager.FrameManager.instance(), 'getFrame');
142+
const URL = 'https://example.com';
143+
const TITLE = 'Frame Page Title';
144+
145+
stub.withArgs('frame-1' as Protocol.Page.FrameId).returns({
146+
url: URL,
147+
securityOrigin: URL,
148+
isMainFrame: () => true,
149+
displayName: () => TITLE,
150+
} as unknown as SDK.ResourceTreeModel.ResourceTreeFrame);
151+
152+
setMockConnectionResponseHandler(
153+
'CrashReportContext.getEntries',
154+
() => ({
155+
entries: [
156+
{key: 'user_id', value: '12345', frameId: 'frame-1' as Protocol.Page.FrameId},
157+
],
158+
}));
159+
160+
const {view} = await createComponent();
161+
const input = await view.nextInput;
162+
163+
assert.lengthOf(input.frames, 1);
164+
assert.strictEqual(input.frames[0].displayName, TITLE);
165+
});
166+
167+
it('renders a placeholder when no context is available', async () => {
168+
setMockConnectionResponseHandler('CrashReportContext.getEntries', () => ({
169+
entries: [],
170+
}));
171+
172+
const {view} = await createComponent();
173+
const input = await view.nextInput;
174+
175+
assert.lengthOf(input.frames, 0);
176+
});
177+
178+
it('handles refresh and filter correctly', async () => {
179+
const {view, component} = await createComponent();
180+
const input = await view.nextInput;
181+
182+
assert.exists(input.onRefresh);
183+
assert.exists(input.onFilterChanged);
184+
assert.deepEqual(input.filters, []);
185+
186+
const updateSpy = sinon.spy(component, 'requestUpdate');
187+
188+
// Test Refresh
189+
input.onRefresh();
190+
sinon.assert.calledOnce(updateSpy);
191+
192+
// Test Filter
193+
input.onFilterChanged(new CustomEvent('change', {detail: 'test'}));
194+
sinon.assert.calledTwice(updateSpy);
195+
196+
const filteredInput = await view.nextInput;
197+
assert.lengthOf(filteredInput.filters, 1);
198+
assert.strictEqual(filteredInput.filters[0].key, 'key,value');
199+
200+
// Test Clear Filter
201+
input.onFilterChanged(new CustomEvent('change', {detail: ''}));
202+
const clearedInput = await view.nextInput;
203+
assert.deepEqual(clearedInput.filters, []);
204+
});
205+
});

0 commit comments

Comments
 (0)