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/);
+});