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;