Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion packages/trace-viewer/src/ui/networkResourceDetails.css
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
.network-request-details-tab {
user-select: text;
line-height: 24px;
margin-left: 10px;
margin-left: 12px;
overflow: auto;
}

Expand All @@ -40,6 +40,8 @@
.network-request-details-header {
margin: 3px 0;
font-weight: bold;
user-select: none;
cursor: pointer;
}

.network-request-details-general {
Expand Down
76 changes: 52 additions & 24 deletions packages/trace-viewer/src/ui/networkResourceDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -105,42 +105,70 @@ 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 (
<details className='network-request-details-section' open={isOpen} aria-label={title}>
<summary className='network-request-details-header' onClick={event => {
event.preventDefault();
setIsOpen(!isOpen);
}}>
{title}
</summary>
{children}
</details>
);
};

const RequestTab: React.FunctionComponent<{
resource: ResourceSnapshot;
startTimeOffset: number;
requestBody: RequestBody,
}> = ({ resource, startTimeOffset, requestBody }) => {
return <div className='vbox network-request-details-tab'>
<div className='network-request-details-header'>General</div>
<div className='network-request-details-url'>{`URL: ${resource.request.url}`}</div>
<div className='network-request-details-general'>{`Method: ${resource.request.method}`}</div>
{resource.response.status !== -1 && <div className='network-request-details-general' style={{ display: 'flex' }}>
Status Code: <span className={statusClass(resource.response.status)} style={{ display: 'inline-flex' }}>
{`${resource.response.status} ${resource.response.statusText}`}
</span></div>}
{resource.request.queryString.length ? <>
<div className='network-request-details-header'>Query String Parameters</div>
<div className='network-request-details-headers'>
{resource.request.queryString.map(param => `${param.name}: ${param.value}`).join('\n')}
</div>
</> : null}
<div className='network-request-details-header'>Request Headers</div>
<div className='network-request-details-headers'>{resource.request.headers.map(pair => `${pair.name}: ${pair.value}`).join('\n')}</div>
<div className='network-request-details-header'>Time</div>
<div className='network-request-details-general'>{`Start: ${msToString(startTimeOffset)}`}</div>
<div className='network-request-details-general'>{`Duration: ${msToString(resource.time)}`}</div>

{requestBody && <div className='network-request-details-header'>Request Body</div>}
{requestBody && <CodeMirrorWrapper text={requestBody.text} mimeType={requestBody.mimeType} readOnly lineNumbers={true}/>}
<DetailsSection title='General'>
<div className='network-request-details-url'>{`URL: ${resource.request.url}`}</div>
<div className='network-request-details-general'>{`Method: ${resource.request.method}`}</div>
{resource.response.status !== -1 && <div className='network-request-details-general' style={{ display: 'flex' }}>
Status Code: <span className={statusClass(resource.response.status)} style={{ display: 'inline-flex' }}>
{`${resource.response.status} ${resource.response.statusText}`}
</span></div>}
</DetailsSection>

{resource.request.queryString.length ?
<DetailsSection title='Query String Parameters'>
<div className='network-request-details-headers'>
{resource.request.queryString.map(param => `${param.name}: ${param.value}`).join('\n')}
</div>
</DetailsSection>
: null}

<DetailsSection title='Request Headers'>
<div className='network-request-details-headers'>{resource.request.headers.map(pair => `${pair.name}: ${pair.value}`).join('\n')}</div>
</DetailsSection>

<DetailsSection title='Time'>
<div className='network-request-details-general'>{`Start: ${msToString(startTimeOffset)}`}</div>
<div className='network-request-details-general'>{`Duration: ${msToString(resource.time)}`}</div>
</DetailsSection>

{requestBody && <DetailsSection title='Request Body'>
<CodeMirrorWrapper text={requestBody.text} mimeType={requestBody.mimeType} readOnly lineNumbers={true}/>
</DetailsSection>}
</div>;
};

const ResponseTab: React.FunctionComponent<{
resource: ResourceSnapshot;
}> = ({ resource }) => {
return <div className='vbox network-request-details-tab'>
<div className='network-request-details-header'>Response Headers</div>
<div className='network-request-details-headers'>{resource.response.headers.map(pair => `${pair.name}: ${pair.value}`).join('\n')}</div>
<DetailsSection title='Response Headers'>
<div className='network-request-details-headers'>{resource.response.headers.map(pair => `${pair.name}: ${pair.value}`).join('\n')}</div>
</DetailsSection>
</div>;
};

Expand Down
46 changes: 41 additions & 5 deletions tests/playwright-test/ui-mode-test-network-tab.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', {
Expand Down Expand Up @@ -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 });
});