| // 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 {beforeEach, it} from 'mocha'; | |
| import sinon from 'sinon'; | |
| import * as SDK from '../../core/sdk/sdk.js'; | |
| import type * as Protocol from '../../generated/protocol.js'; | |
| import {createTarget, describeWithEnvironment} from '../../testing/EnvironmentHelpers.js'; | |
| import {MockCDPConnection} from '../../testing/MockCDPConnection.js'; | |
| import {createViewFunctionStub} from '../../testing/ViewFunctionHelpers.js'; | |
| import * as Application from './application.js'; | |
| describeWithEnvironment('CrashReportContextView', () => { | |
| const FRAME_ID = 'frame-1' as Protocol.Page.FrameId; | |
| const URL = 'https://example.com/index.html'; | |
| let target: SDK.Target.Target; | |
| let connection: MockCDPConnection; | |
| beforeEach(() => { | |
| connection = new MockCDPConnection(); | |
| target = createTarget({connection}); | |
| target.model(SDK.CrashReportContextModel.CrashReportContextModel); | |
| target.model(SDK.ResourceTreeModel.ResourceTreeModel); | |
| }); | |
| async function createComponent() { | |
| const view = createViewFunctionStub(Application.CrashReportContextView.CrashReportContextView); | |
| const component = new Application.CrashReportContextView.CrashReportContextView(view); | |
| return {view, component}; | |
| } | |
| it('renders frame sections and entries', async () => { | |
| sinon.stub(SDK.FrameManager.FrameManager.instance(), 'getFrame').returns({ | |
| url: URL, | |
| isMainFrame: () => true, | |
| displayName: () => URL, | |
| } as unknown as SDK.ResourceTreeModel.ResourceTreeFrame); | |
| connection.setSuccessHandler('CrashReportContext.getEntries', | |
| () => ({ | |
| entries: [ | |
| {key: 'user_id', value: '12345', frameId: FRAME_ID}, | |
| ], | |
| })); | |
| const {view} = await createComponent(); | |
| const input = await view.nextInput; | |
| assert.lengthOf(input.frames, 1); | |
| assert.strictEqual(input.frames[0].url, URL); | |
| assert.strictEqual(input.frames[0].displayName, URL); | |
| assert.strictEqual(input.frames[0].entries[0].key, 'user_id'); | |
| }); | |
| it('groups entries by frame', async () => { | |
| const stub = sinon.stub(SDK.FrameManager.FrameManager.instance(), 'getFrame'); | |
| stub.withArgs('frame-1' as Protocol.Page.FrameId).returns({ | |
| url: 'https://frame1.com', | |
| isMainFrame: () => true, | |
| displayName: () => 'https://frame1.com', | |
| } as unknown as SDK.ResourceTreeModel.ResourceTreeFrame); | |
| stub.withArgs('frame-2' as Protocol.Page.FrameId).returns({ | |
| url: 'https://frame2.com', | |
| isMainFrame: () => false, | |
| displayName: () => 'https://frame2.com', | |
| } as unknown as SDK.ResourceTreeModel.ResourceTreeFrame); | |
| connection.setSuccessHandler('CrashReportContext.getEntries', | |
| () => ({ | |
| entries: [ | |
| {key: 'k1', value: 'v1', frameId: 'frame-1' as Protocol.Page.FrameId}, | |
| {key: 'k2', value: 'v2', frameId: 'frame-2' as Protocol.Page.FrameId}, | |
| ], | |
| })); | |
| const {view} = await createComponent(); | |
| const input = await view.nextInput; | |
| assert.lengthOf(input.frames, 2); | |
| assert.strictEqual(input.frames[0].url, 'https://frame1.com'); | |
| assert.strictEqual(input.frames[1].url, 'https://frame2.com'); | |
| }); | |
| it('handles unknown frames by showing a fallback URL', async () => { | |
| // Explicitly return null for the frame lookup | |
| sinon.stub(SDK.FrameManager.FrameManager.instance(), 'getFrame').returns(null); | |
| connection.setSuccessHandler('CrashReportContext.getEntries', | |
| () => ({ | |
| entries: [ | |
| {key: 'k1', value: 'v1', frameId: 'unknown-frame' as Protocol.Page.FrameId}, | |
| ], | |
| })); | |
| const {view} = await createComponent(); | |
| const input = await view.nextInput; | |
| assert.lengthOf(input.frames, 1); | |
| assert.strictEqual(input.frames[0].url, 'Unknown frame'); | |
| }); | |
| it('disambiguates frames with the same URL', async () => { | |
| const stub = sinon.stub(SDK.FrameManager.FrameManager.instance(), 'getFrame'); | |
| const SHARED_URL = 'https://shared.com'; | |
| stub.withArgs('frame-main' as Protocol.Page.FrameId).returns({ | |
| url: SHARED_URL, | |
| isMainFrame: () => true, | |
| displayName: () => SHARED_URL, | |
| } as unknown as SDK.ResourceTreeModel.ResourceTreeFrame); | |
| stub.withArgs('frame-sub' as Protocol.Page.FrameId).returns({ | |
| url: SHARED_URL, | |
| isMainFrame: () => false, | |
| displayName: () => SHARED_URL, | |
| } as unknown as SDK.ResourceTreeModel.ResourceTreeFrame); | |
| connection.setSuccessHandler('CrashReportContext.getEntries', | |
| () => ({ | |
| entries: [ | |
| {key: 'k1', value: 'v1', frameId: 'frame-main' as Protocol.Page.FrameId}, | |
| {key: 'k2', value: 'v2', frameId: 'frame-sub' as Protocol.Page.FrameId}, | |
| ], | |
| })); | |
| const {view} = await createComponent(); | |
| const input = await view.nextInput; | |
| assert.lengthOf(input.frames, 2); | |
| assert.strictEqual(input.frames[0].displayName, SHARED_URL); | |
| assert.strictEqual(input.frames[1].displayName, SHARED_URL); | |
| }); | |
| it('uses frame.displayName() if available to render titles', async () => { | |
| const stub = sinon.stub(SDK.FrameManager.FrameManager.instance(), 'getFrame'); | |
| const URL = 'https://example.com'; | |
| const TITLE = 'Frame Page Title'; | |
| stub.withArgs('frame-1' as Protocol.Page.FrameId).returns({ | |
| url: URL, | |
| isMainFrame: () => true, | |
| displayName: () => TITLE, | |
| } as unknown as SDK.ResourceTreeModel.ResourceTreeFrame); | |
| connection.setSuccessHandler('CrashReportContext.getEntries', | |
| () => ({ | |
| entries: [ | |
| {key: 'user_id', value: '12345', frameId: 'frame-1' as Protocol.Page.FrameId}, | |
| ], | |
| })); | |
| const {view} = await createComponent(); | |
| const input = await view.nextInput; | |
| assert.lengthOf(input.frames, 1); | |
| assert.strictEqual(input.frames[0].displayName, TITLE); | |
| }); | |
| it('renders a placeholder when no context is available', async () => { | |
| connection.setSuccessHandler('CrashReportContext.getEntries', () => ({ | |
| entries: [], | |
| })); | |
| const {view} = await createComponent(); | |
| const input = await view.nextInput; | |
| assert.lengthOf(input.frames, 0); | |
| }); | |
| it('handles refresh and filter correctly', async () => { | |
| connection.setSuccessHandler('CrashReportContext.getEntries', () => ({entries: []})); | |
| const {view, component} = await createComponent(); | |
| const input = await view.nextInput; | |
| assert.exists(input.onRefresh); | |
| assert.exists(input.onFilterChanged); | |
| assert.deepEqual(input.filters, []); | |
| const updateSpy = sinon.spy(component, 'requestUpdate'); | |
| // Test Refresh | |
| input.onRefresh(); | |
| sinon.assert.calledOnce(updateSpy); | |
| // Test Filter | |
| input.onFilterChanged(new CustomEvent('change', {detail: 'test'})); | |
| sinon.assert.calledTwice(updateSpy); | |
| const filteredInput = await view.nextInput; | |
| assert.lengthOf(filteredInput.filters, 1); | |
| assert.strictEqual(filteredInput.filters[0].key, 'key,value'); | |
| // Test Clear Filter | |
| input.onFilterChanged(new CustomEvent('change', {detail: ''})); | |
| const clearedInput = await view.nextInput; | |
| assert.deepEqual(clearedInput.filters, []); | |
| }); | |
| }); |