From 1e50aa742f05b29e723c2d5f5bc34bcbc40f3a18 Mon Sep 17 00:00:00 2001
From: cpadm <57954026+cpAdm@users.noreply.github.com>
Date: Fri, 31 Oct 2025 19:35:23 +0100
Subject: [PATCH 1/4] feat(trace-viewer): Collapse sections inside network
request
---
.../src/ui/networkResourceDetails.css | 4 +-
.../src/ui/networkResourceDetails.tsx | 76 +++++++++++++------
.../ui-mode-test-network-tab.spec.ts | 46 +++++++++--
3 files changed, 96 insertions(+), 30 deletions(-)
diff --git a/packages/trace-viewer/src/ui/networkResourceDetails.css b/packages/trace-viewer/src/ui/networkResourceDetails.css
index 26557db1e5fc9..9698a72f70b4d 100644
--- a/packages/trace-viewer/src/ui/networkResourceDetails.css
+++ b/packages/trace-viewer/src/ui/networkResourceDetails.css
@@ -17,7 +17,7 @@
.network-request-details-tab {
user-select: text;
line-height: 24px;
- margin-left: 10px;
+ margin-left: 12px;
overflow: auto;
}
@@ -40,6 +40,8 @@
.network-request-details-header {
margin: 3px 0;
font-weight: bold;
+ user-select: none;
+ cursor: pointer;
}
.network-request-details-general {
diff --git a/packages/trace-viewer/src/ui/networkResourceDetails.tsx b/packages/trace-viewer/src/ui/networkResourceDetails.tsx
index 1184b9141ec4d..7018449539b57 100644
--- a/packages/trace-viewer/src/ui/networkResourceDetails.tsx
+++ b/packages/trace-viewer/src/ui/networkResourceDetails.tsx
@@ -24,7 +24,7 @@ import { generateCurlCommand, generateFetchCall } from '../third_party/devtools'
import { CopyToClipboardTextButton } from './copyToClipboard';
import { getAPIRequestCodeGen } from './codegen';
import type { Language } from '@isomorphic/locatorGenerators';
-import { msToString, useAsyncMemo } from '@web/uiUtils';
+import { msToString, useAsyncMemo, useSetting } from '@web/uiUtils';
import type { Entry } from '@trace/har';
import { useTraceModel } from './traceModelContext';
@@ -105,33 +105,60 @@ const CopyDropdown: React.FC<{
);
};
+const DetailsSection: React.FC<{
+ title: string;
+ children?: React.ReactNode
+}> = ({ title, children }) => {
+ const [isOpen, setIsOpen] = useSetting(`trace-viewer-network-details-${title.replaceAll(' ', '-')}`, true);
+
+ return (
+
+ {
+ event.preventDefault();
+ setIsOpen(!isOpen);
+ }}>
+ {title}
+
+ {children}
+
+ );
+};
+
const RequestTab: React.FunctionComponent<{
resource: ResourceSnapshot;
startTimeOffset: number;
requestBody: RequestBody,
}> = ({ resource, startTimeOffset, requestBody }) => {
return
-
General
-
{`URL: ${resource.request.url}`}
-
{`Method: ${resource.request.method}`}
- {resource.response.status !== -1 &&
- Status Code:
- {`${resource.response.status} ${resource.response.statusText}`}
-
}
- {resource.request.queryString.length ? <>
-
Query String Parameters
-
- {resource.request.queryString.map(param => `${param.name}: ${param.value}`).join('\n')}
-
- > : null}
-
Request Headers
-
{resource.request.headers.map(pair => `${pair.name}: ${pair.value}`).join('\n')}
-
Time
-
{`Start: ${msToString(startTimeOffset)}`}
-
{`Duration: ${msToString(resource.time)}`}
-
- {requestBody &&
Request Body
}
- {requestBody &&
}
+
+ {`URL: ${resource.request.url}`}
+ {`Method: ${resource.request.method}`}
+ {resource.response.status !== -1 &&
+ Status Code:
+ {`${resource.response.status} ${resource.response.statusText}`}
+
}
+
+
+ {resource.request.queryString.length ?
+
+
+ {resource.request.queryString.map(param => `${param.name}: ${param.value}`).join('\n')}
+
+
+ : null}
+
+
+ {resource.request.headers.map(pair => `${pair.name}: ${pair.value}`).join('\n')}
+
+
+
+ {`Start: ${msToString(startTimeOffset)}`}
+ {`Duration: ${msToString(resource.time)}`}
+
+
+ {requestBody &&
+
+ }
;
};
@@ -139,8 +166,9 @@ const ResponseTab: React.FunctionComponent<{
resource: ResourceSnapshot;
}> = ({ resource }) => {
return
-
Response Headers
-
{resource.response.headers.map(pair => `${pair.name}: ${pair.value}`).join('\n')}
+
+ {resource.response.headers.map(pair => `${pair.name}: ${pair.value}`).join('\n')}
+
;
};
diff --git a/tests/playwright-test/ui-mode-test-network-tab.spec.ts b/tests/playwright-test/ui-mode-test-network-tab.spec.ts
index 1f7a918c21895..3f0d5840e3605 100644
--- a/tests/playwright-test/ui-mode-test-network-tab.spec.ts
+++ b/tests/playwright-test/ui-mode-test-network-tab.spec.ts
@@ -196,14 +196,14 @@ test('should display list of query parameters (only if present)', async ({ runUI
await page.getByText('call-with-query-params').click();
- await expect(page.getByText('Query String Parameters')).toBeVisible();
- await expect(page.getByText('param1: value1')).toBeVisible();
- await expect(page.getByText('param1: value2')).toBeVisible();
- await expect(page.getByText('param2: value2')).toBeVisible();
+ const group = page.getByRole('group', { name: 'Query String Parameters' });
+ await expect(group.getByText('param1: value1')).toBeVisible();
+ await expect(group.getByText('param1: value2')).toBeVisible();
+ await expect(group.getByText('param2: value2')).toBeVisible();
await page.getByText('endpoint').click();
- await expect(page.getByText('Query String Parameters')).not.toBeVisible();
+ await expect(group).toBeHidden();
});
test('should not duplicate network entries from beforeAll', {
@@ -241,3 +241,39 @@ test('should not duplicate network entries from beforeAll', {
await page.getByText('Network', { exact: true }).click();
await expect(page.getByRole('list', { name: 'Network requests' }).getByText('empty.html')).toHaveCount(1);
});
+
+test('should toggle sections inside network details', async ({ runUITest, server }) => {
+ const { page } = await runUITest({
+ 'network-tab.test.ts': `
+ import { test, expect } from '@playwright/test';
+ test('network tab test', async ({ page }) => {
+ await page.goto('${server.PREFIX}/network-tab/network.html');
+ await page.evaluate(() => (window as any).donePromise);
+ });
+ `,
+ });
+
+ await page.getByRole('treeitem', { name: 'network tab test' }).dblclick();
+ await page.getByRole('tab', { name: 'Network' }).click();
+ await page.getByRole('listitem').filter({ hasText: 'post-data-1' }).click();
+ const requestPanel = page.getByRole('tabpanel', { name: 'Request' });
+
+ // Make sure to assert with useInnerText, because .textContent always includes text even if details is collapsed
+ await requestPanel.getByText('Request Headers').click();
+ await expect(requestPanel.getByRole('group', { name: 'Request Headers' })).toHaveText('Request Headers', { useInnerText: true });
+ await expect(requestPanel.getByRole('group', { name: 'Time' })).toHaveText(/Time\nStart: .+\nDuration: \d+ms/, { useInnerText: true });
+
+ await requestPanel.getByText('Time').click();
+ await expect(requestPanel.getByRole('group', { name: 'Request Headers' })).toHaveText('Request Headers', { useInnerText: true });
+ await expect(requestPanel.getByRole('group', { name: 'Time' })).toHaveText('Time', { useInnerText: true });
+
+ await requestPanel.getByText('Time').click();
+ await expect(requestPanel.getByRole('group', { name: 'Request Headers' })).toHaveText('Request Headers', { useInnerText: true });
+ await expect(requestPanel.getByRole('group', { name: 'Time' })).toHaveText(/Time\nStart: .+\nDuration: \d+ms/, { useInnerText: true });
+
+ // Re-opening should preserve open state
+ await page.getByRole('tabpanel', { name: 'Network' }).getByRole('button', { name: 'Close' }).click();
+ await page.getByRole('listitem').filter({ hasText: 'post-data-1' }).click();
+ await expect(requestPanel.getByRole('group', { name: 'Request Headers' })).toHaveText('Request Headers', { useInnerText: true });
+ await expect(requestPanel.getByRole('group', { name: 'Time' })).toHaveText(/Time\nStart: .+\nDuration: \d+ms/, { useInnerText: true });
+});
From d6926713d796690085bb4a60e931bf069039fbf0 Mon Sep 17 00:00:00 2001
From: cpadm <57954026+cpAdm@users.noreply.github.com>
Date: Sun, 2 Nov 2025 14:39:47 +0100
Subject: [PATCH 2/4] fix(trace-viewer): Re-use existing Expandable and make
sure codemirror gets the correct height
---
.../src/ui/networkResourceDetails.css | 27 ++++++++--
.../src/ui/networkResourceDetails.tsx | 49 +++++++++----------
packages/web/src/components/expandable.css | 4 ++
packages/web/src/components/expandable.tsx | 8 +--
.../ui-mode-test-network-tab.spec.ts | 33 ++++++-------
5 files changed, 70 insertions(+), 51 deletions(-)
diff --git a/packages/trace-viewer/src/ui/networkResourceDetails.css b/packages/trace-viewer/src/ui/networkResourceDetails.css
index 9698a72f70b4d..d27d90d4ff219 100644
--- a/packages/trace-viewer/src/ui/networkResourceDetails.css
+++ b/packages/trace-viewer/src/ui/networkResourceDetails.css
@@ -16,8 +16,6 @@
.network-request-details-tab {
user-select: text;
- line-height: 24px;
- margin-left: 12px;
overflow: auto;
}
@@ -37,11 +35,18 @@
margin-left: 10px;
}
+.expandable-title {
+ padding-left: 3px;
+}
+
+.expandable-content {
+ margin-left: 0;
+ padding-left: 28px;
+}
+
.network-request-details-header {
- margin: 3px 0;
+ margin: 3px 0 3px 14px;
font-weight: bold;
- user-select: none;
- cursor: pointer;
}
.network-request-details-general {
@@ -53,6 +58,18 @@
overflow: hidden;
}
+.expandable:has(.cm-wrapper) {
+ max-height: 100%;
+}
+
+.expandable-content:has(.cm-wrapper) {
+ height: 100%;
+}
+
+.expandable-content {
+ line-height: 24px;
+}
+
.network-font-preview {
font-family: font-preview;
font-size: 30px;
diff --git a/packages/trace-viewer/src/ui/networkResourceDetails.tsx b/packages/trace-viewer/src/ui/networkResourceDetails.tsx
index 7018449539b57..7da3253fa23ce 100644
--- a/packages/trace-viewer/src/ui/networkResourceDetails.tsx
+++ b/packages/trace-viewer/src/ui/networkResourceDetails.tsx
@@ -27,6 +27,7 @@ import type { Language } from '@isomorphic/locatorGenerators';
import { msToString, useAsyncMemo, useSetting } from '@web/uiUtils';
import type { Entry } from '@trace/har';
import { useTraceModel } from './traceModelContext';
+import { Expandable } from '@web/components/expandable';
type RequestBody = { text: string, mimeType?: string } | null;
@@ -105,23 +106,19 @@ const CopyDropdown: React.FC<{
);
};
-const DetailsSection: React.FC<{
+const ExpandableSection: React.FC<{
title: string;
children?: React.ReactNode
}> = ({ title, children }) => {
- const [isOpen, setIsOpen] = useSetting(`trace-viewer-network-details-${title.replaceAll(' ', '-')}`, true);
-
- return (
-
- {
- event.preventDefault();
- setIsOpen(!isOpen);
- }}>
- {title}
-
- {children}
-
- );
+ const [expanded, setExpanded] = useSetting(`trace-viewer-network-details-${title.replaceAll(' ', '-')}`, true);
+ return {title}}
+ >
+ {children}
+ ;
};
const RequestTab: React.FunctionComponent<{
@@ -130,35 +127,35 @@ const RequestTab: React.FunctionComponent<{
requestBody: RequestBody,
}> = ({ resource, startTimeOffset, requestBody }) => {
return
-
+
{`URL: ${resource.request.url}`}
{`Method: ${resource.request.method}`}
{resource.response.status !== -1 &&
Status Code:
{`${resource.response.status} ${resource.response.statusText}`}
}
-
+
{resource.request.queryString.length ?
-
+
{resource.request.queryString.map(param => `${param.name}: ${param.value}`).join('\n')}
-
+
: null}
-
+
{resource.request.headers.map(pair => `${pair.name}: ${pair.value}`).join('\n')}
-
+
-
+
{`Start: ${msToString(startTimeOffset)}`}
{`Duration: ${msToString(resource.time)}`}
-
+
- {requestBody &&
+ {requestBody &&
- }
+ }
;
};
@@ -166,9 +163,9 @@ const ResponseTab: React.FunctionComponent<{
resource: ResourceSnapshot;
}> = ({ resource }) => {
return
-
+
{resource.response.headers.map(pair => `${pair.name}: ${pair.value}`).join('\n')}
-
+
;
};
diff --git a/packages/web/src/components/expandable.css b/packages/web/src/components/expandable.css
index ba0bb408b4368..5127c0df7dc94 100644
--- a/packages/web/src/components/expandable.css
+++ b/packages/web/src/components/expandable.css
@@ -29,3 +29,7 @@
user-select: none;
cursor: pointer;
}
+
+.expandable-content {
+ margin-left: 25px;
+}
diff --git a/packages/web/src/components/expandable.tsx b/packages/web/src/components/expandable.tsx
index e3b581824d3fd..15e52a9f7f35d 100644
--- a/packages/web/src/components/expandable.tsx
+++ b/packages/web/src/components/expandable.tsx
@@ -24,7 +24,8 @@ export const Expandable: React.FunctionComponent> = ({ title, children, setExpanded, expanded, expandOnTitleClick }) => {
- const id = React.useId();
+ const titleId = React.useId();
+ const regionId = React.useId();
const onClick = React.useCallback(() => setExpanded(!expanded), [expanded, setExpanded]);
@@ -36,9 +37,10 @@ export const Expandable: React.FunctionComponent
{expandOnTitleClick ?
{chevron}
@@ -48,6 +50,6 @@ export const Expandable: React.FunctionComponent
}
- {expanded && {children}
}
+ {expanded && {children}
}
;
};
diff --git a/tests/playwright-test/ui-mode-test-network-tab.spec.ts b/tests/playwright-test/ui-mode-test-network-tab.spec.ts
index 3f0d5840e3605..c0fb4669c6f81 100644
--- a/tests/playwright-test/ui-mode-test-network-tab.spec.ts
+++ b/tests/playwright-test/ui-mode-test-network-tab.spec.ts
@@ -196,14 +196,14 @@ test('should display list of query parameters (only if present)', async ({ runUI
await page.getByText('call-with-query-params').click();
- const group = page.getByRole('group', { name: 'Query String Parameters' });
- await expect(group.getByText('param1: value1')).toBeVisible();
- await expect(group.getByText('param1: value2')).toBeVisible();
- await expect(group.getByText('param2: value2')).toBeVisible();
+ const region = page.getByRole('region', { name: 'Query String Parameters' });
+ await expect(region.getByText('param1: value1')).toBeVisible();
+ await expect(region.getByText('param1: value2')).toBeVisible();
+ await expect(region.getByText('param2: value2')).toBeVisible();
await page.getByText('endpoint').click();
- await expect(group).toBeHidden();
+ await expect(region).toBeHidden();
});
test('should not duplicate network entries from beforeAll', {
@@ -258,22 +258,21 @@ test('should toggle sections inside network details', async ({ runUITest, server
await page.getByRole('listitem').filter({ hasText: 'post-data-1' }).click();
const requestPanel = page.getByRole('tabpanel', { name: 'Request' });
- // Make sure to assert with useInnerText, because .textContent always includes text even if details is collapsed
- await requestPanel.getByText('Request Headers').click();
- await expect(requestPanel.getByRole('group', { name: 'Request Headers' })).toHaveText('Request Headers', { useInnerText: true });
- await expect(requestPanel.getByRole('group', { name: 'Time' })).toHaveText(/Time\nStart: .+\nDuration: \d+ms/, { useInnerText: true });
+ await requestPanel.getByRole('button', { name: 'Request Headers' }).click();
+ await expect(requestPanel.getByRole('region', { name: 'Request Headers' })).toBeHidden();
+ await expect(requestPanel.getByRole('region', { name: 'Time' })).toHaveText(/Start: .+Duration: \d+ms/);
- await requestPanel.getByText('Time').click();
- await expect(requestPanel.getByRole('group', { name: 'Request Headers' })).toHaveText('Request Headers', { useInnerText: true });
- await expect(requestPanel.getByRole('group', { name: 'Time' })).toHaveText('Time', { useInnerText: true });
+ await requestPanel.getByRole('button', { name: 'Time' }).click();
+ await expect(requestPanel.getByRole('region', { name: 'Request Headers' })).toBeHidden();
+ await expect(requestPanel.getByRole('region', { name: 'Time' })).toBeHidden();
- await requestPanel.getByText('Time').click();
- await expect(requestPanel.getByRole('group', { name: 'Request Headers' })).toHaveText('Request Headers', { useInnerText: true });
- await expect(requestPanel.getByRole('group', { name: 'Time' })).toHaveText(/Time\nStart: .+\nDuration: \d+ms/, { useInnerText: true });
+ await requestPanel.getByRole('button', { name: 'Time' }).click();
+ await expect(requestPanel.getByRole('region', { name: 'Request Headers' })).toBeHidden();
+ await expect(requestPanel.getByRole('region', { name: 'Time' })).toHaveText(/Start: .+Duration: \d+ms/);
// Re-opening should preserve open state
await page.getByRole('tabpanel', { name: 'Network' }).getByRole('button', { name: 'Close' }).click();
await page.getByRole('listitem').filter({ hasText: 'post-data-1' }).click();
- await expect(requestPanel.getByRole('group', { name: 'Request Headers' })).toHaveText('Request Headers', { useInnerText: true });
- await expect(requestPanel.getByRole('group', { name: 'Time' })).toHaveText(/Time\nStart: .+\nDuration: \d+ms/, { useInnerText: true });
+ await expect(requestPanel.getByRole('region', { name: 'Request Headers' })).toBeHidden();
+ await expect(requestPanel.getByRole('region', { name: 'Time' })).toHaveText(/Start: .+Duration: \d+ms/);
});
From b609c8e70b4682b87bbec732af8a433826363f7d Mon Sep 17 00:00:00 2001
From: cpadm <57954026+cpAdm@users.noreply.github.com>
Date: Fri, 7 Nov 2025 14:49:10 +0100
Subject: [PATCH 3/4] refactor: Give request-body section explicit class name
---
packages/trace-viewer/src/ui/networkResourceDetails.css | 4 ++--
packages/trace-viewer/src/ui/networkResourceDetails.tsx | 6 ++++--
packages/web/src/components/expandable.tsx | 5 +++--
3 files changed, 9 insertions(+), 6 deletions(-)
diff --git a/packages/trace-viewer/src/ui/networkResourceDetails.css b/packages/trace-viewer/src/ui/networkResourceDetails.css
index d27d90d4ff219..1d8423d8ca932 100644
--- a/packages/trace-viewer/src/ui/networkResourceDetails.css
+++ b/packages/trace-viewer/src/ui/networkResourceDetails.css
@@ -58,11 +58,11 @@
overflow: hidden;
}
-.expandable:has(.cm-wrapper) {
+.network-request-request-body {
max-height: 100%;
}
-.expandable-content:has(.cm-wrapper) {
+.network-request-request-body .expandable-content {
height: 100%;
}
diff --git a/packages/trace-viewer/src/ui/networkResourceDetails.tsx b/packages/trace-viewer/src/ui/networkResourceDetails.tsx
index 7da3253fa23ce..50f4a9eb42340 100644
--- a/packages/trace-viewer/src/ui/networkResourceDetails.tsx
+++ b/packages/trace-viewer/src/ui/networkResourceDetails.tsx
@@ -109,13 +109,15 @@ const CopyDropdown: React.FC<{
const ExpandableSection: React.FC<{
title: string;
children?: React.ReactNode
-}> = ({ title, children }) => {
+ className?: string;
+}> = ({ title, children, className }) => {
const [expanded, setExpanded] = useSetting(`trace-viewer-network-details-${title.replaceAll(' ', '-')}`, true);
return {title}}
+ className={className}
>
{children}
;
@@ -153,7 +155,7 @@ const RequestTab: React.FunctionComponent<{
{`Duration: ${msToString(resource.time)}`}
- {requestBody &&
+ {requestBody &&
}
;
diff --git a/packages/web/src/components/expandable.tsx b/packages/web/src/components/expandable.tsx
index 15e52a9f7f35d..7bf551bc319fb 100644
--- a/packages/web/src/components/expandable.tsx
+++ b/packages/web/src/components/expandable.tsx
@@ -23,7 +23,8 @@ export const Expandable: React.FunctionComponent void,
expanded: boolean,
expandOnTitleClick?: boolean,
-}>> = ({ title, children, setExpanded, expanded, expandOnTitleClick }) => {
+ className?: string;
+}>> = ({ title, children, setExpanded, expanded, expandOnTitleClick, className }) => {
const titleId = React.useId();
const regionId = React.useId();
@@ -34,7 +35,7 @@ export const Expandable: React.FunctionComponent;
- return
+ return
{expandOnTitleClick ?
Date: Sat, 15 Nov 2025 11:23:04 +0100
Subject: [PATCH 4/4] fix: Scope expandable css properly
---
packages/trace-viewer/src/ui/networkResourceDetails.css | 9 +++------
1 file changed, 3 insertions(+), 6 deletions(-)
diff --git a/packages/trace-viewer/src/ui/networkResourceDetails.css b/packages/trace-viewer/src/ui/networkResourceDetails.css
index 1d8423d8ca932..9310a6c553894 100644
--- a/packages/trace-viewer/src/ui/networkResourceDetails.css
+++ b/packages/trace-viewer/src/ui/networkResourceDetails.css
@@ -35,13 +35,14 @@
margin-left: 10px;
}
-.expandable-title {
+.network-request-details-tab .expandable-title {
padding-left: 3px;
}
-.expandable-content {
+.network-request-details-tab .expandable-content {
margin-left: 0;
padding-left: 28px;
+ line-height: 24px;
}
.network-request-details-header {
@@ -66,10 +67,6 @@
height: 100%;
}
-.expandable-content {
- line-height: 24px;
-}
-
.network-font-preview {
font-family: font-preview;
font-size: 30px;