diff --git a/packages/trace-viewer/src/ui/networkResourceDetails.css b/packages/trace-viewer/src/ui/networkResourceDetails.css index 26557db1e5fc9..9310a6c553894 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: 10px; overflow: auto; } @@ -37,8 +35,18 @@ margin-left: 10px; } +.network-request-details-tab .expandable-title { + padding-left: 3px; +} + +.network-request-details-tab .expandable-content { + margin-left: 0; + padding-left: 28px; + line-height: 24px; +} + .network-request-details-header { - margin: 3px 0; + margin: 3px 0 3px 14px; font-weight: bold; } @@ -51,6 +59,14 @@ overflow: hidden; } +.network-request-request-body { + max-height: 100%; +} + +.network-request-request-body .expandable-content { + height: 100%; +} + .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 1184b9141ec4d..50f4a9eb42340 100644 --- a/packages/trace-viewer/src/ui/networkResourceDetails.tsx +++ b/packages/trace-viewer/src/ui/networkResourceDetails.tsx @@ -24,9 +24,10 @@ 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'; +import { Expandable } from '@web/components/expandable'; type RequestBody = { text: string, mimeType?: string } | null; @@ -105,33 +106,58 @@ const CopyDropdown: React.FC<{ ); }; +const ExpandableSection: React.FC<{ + title: string; + children?: React.ReactNode + className?: string; +}> = ({ title, children, className }) => { + const [expanded, setExpanded] = useSetting(`trace-viewer-network-details-${title.replaceAll(' ', '-')}`, true); + return {title}} + className={className} + > + {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 +165,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/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..7bf551bc319fb 100644 --- a/packages/web/src/components/expandable.tsx +++ b/packages/web/src/components/expandable.tsx @@ -23,8 +23,10 @@ export const Expandable: React.FunctionComponent void, expanded: boolean, expandOnTitleClick?: boolean, -}>> = ({ title, children, setExpanded, expanded, expandOnTitleClick }) => { - const id = React.useId(); + className?: string; +}>> = ({ title, children, setExpanded, expanded, expandOnTitleClick, className }) => { + const titleId = React.useId(); + const regionId = React.useId(); const onClick = React.useCallback(() => setExpanded(!expanded), [expanded, setExpanded]); @@ -33,12 +35,13 @@ export const Expandable: React.FunctionComponent; - return
+ return
{expandOnTitleClick ?
{chevron} @@ -48,6 +51,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 1f7a918c21895..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(); - 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 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(page.getByText('Query String Parameters')).not.toBeVisible(); + await expect(region).toBeHidden(); }); test('should not duplicate network entries from beforeAll', { @@ -241,3 +241,38 @@ 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' }); + + 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.getByRole('button', { name: 'Time' }).click(); + await expect(requestPanel.getByRole('region', { name: 'Request Headers' })).toBeHidden(); + await expect(requestPanel.getByRole('region', { name: 'Time' })).toBeHidden(); + + 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('region', { name: 'Request Headers' })).toBeHidden(); + await expect(requestPanel.getByRole('region', { name: 'Time' })).toHaveText(/Start: .+Duration: \d+ms/); +});