feat(ui-mode): add filters to network tab (#31956)
This commit is contained in:
parent
79ca3f28c5
commit
7ec3a93db3
46
packages/trace-viewer/src/ui/networkFilters.css
Normal file
46
packages/trace-viewer/src/ui/networkFilters.css
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
Copyright (c) Microsoft Corporation.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
.network-filters {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
background-color: var(--vscode-sideBar-background);
|
||||
padding: 4px 8px;
|
||||
min-height: 32px;
|
||||
}
|
||||
|
||||
.network-filters input[type="search"] {
|
||||
padding: 0 5px;
|
||||
}
|
||||
|
||||
.network-filters-resource-types {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.network-filters-resource-type {
|
||||
cursor: pointer;
|
||||
border-radius: 2px;
|
||||
padding: 3px 8px;
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.network-filters-resource-type.selected {
|
||||
background-color: var(--vscode-list-inactiveSelectionBackground);
|
||||
}
|
||||
57
packages/trace-viewer/src/ui/networkFilters.tsx
Normal file
57
packages/trace-viewer/src/ui/networkFilters.tsx
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
/**
|
||||
* Copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import './networkFilters.css';
|
||||
|
||||
const resourceTypes = ['All', 'Fetch', 'HTML', 'JS', 'CSS', 'Font', 'Image'] as const;
|
||||
export type ResourceType = typeof resourceTypes[number];
|
||||
|
||||
export type FilterState = {
|
||||
searchValue: string;
|
||||
resourceType: ResourceType;
|
||||
};
|
||||
|
||||
export const defaultFilterState: FilterState = { searchValue: '', resourceType: 'All' };
|
||||
|
||||
export const NetworkFilters: React.FunctionComponent<{
|
||||
filterState: FilterState,
|
||||
onFilterStateChange: (filterState: FilterState) => void,
|
||||
}> = ({ filterState, onFilterStateChange }) => {
|
||||
return (
|
||||
<div className='network-filters'>
|
||||
<input
|
||||
type='search'
|
||||
placeholder='Filter network'
|
||||
spellCheck={false}
|
||||
value={filterState.searchValue}
|
||||
onChange={e => onFilterStateChange({ ...filterState, searchValue: e.target.value })}
|
||||
/>
|
||||
|
||||
<div className='network-filters-resource-types'>
|
||||
{resourceTypes.map(resourceType => (
|
||||
<div
|
||||
key={resourceType}
|
||||
title={resourceType}
|
||||
onClick={() => onFilterStateChange({ ...filterState, resourceType })}
|
||||
className={`network-filters-resource-type ${filterState.resourceType === resourceType ? 'selected' : ''}`}
|
||||
>
|
||||
{resourceType}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -25,6 +25,7 @@ import { context, type MultiTraceModel } from './modelUtil';
|
|||
import { GridView, type RenderedGridCell } from '@web/components/gridView';
|
||||
import { SplitView } from '@web/components/splitView';
|
||||
import type { ContextEntry } from '../entries';
|
||||
import { NetworkFilters, defaultFilterState, type FilterState, type ResourceType } from './networkFilters';
|
||||
|
||||
type NetworkTabModel = {
|
||||
resources: Entry[],
|
||||
|
|
@ -68,18 +69,24 @@ export const NetworkTab: React.FunctionComponent<{
|
|||
}> = ({ boundaries, networkModel, onEntryHovered }) => {
|
||||
const [sorting, setSorting] = React.useState<Sorting | undefined>(undefined);
|
||||
const [selectedEntry, setSelectedEntry] = React.useState<RenderedEntry | undefined>(undefined);
|
||||
const [filterState, setFilterState] = React.useState(defaultFilterState);
|
||||
|
||||
const { renderedEntries } = React.useMemo(() => {
|
||||
const renderedEntries = networkModel.resources.map(entry => renderEntry(entry, boundaries, networkModel.contextIdMap));
|
||||
const renderedEntries = networkModel.resources.map(entry => renderEntry(entry, boundaries, networkModel.contextIdMap)).filter(filterEntry(filterState));
|
||||
if (sorting)
|
||||
sort(renderedEntries, sorting);
|
||||
return { renderedEntries };
|
||||
}, [networkModel.resources, networkModel.contextIdMap, sorting, boundaries]);
|
||||
}, [networkModel.resources, networkModel.contextIdMap, filterState, sorting, boundaries]);
|
||||
|
||||
const [columnWidths, setColumnWidths] = React.useState<Map<ColumnName, number>>(() => {
|
||||
return new Map(allColumns().map(column => [column, columnWidth(column)]));
|
||||
});
|
||||
|
||||
const onFilterStateChange = React.useCallback((newFilterState: FilterState) => {
|
||||
setFilterState(newFilterState);
|
||||
setSelectedEntry(undefined);
|
||||
}, []);
|
||||
|
||||
if (!networkModel.resources.length)
|
||||
return <PlaceholderPanel text='No network calls' />;
|
||||
|
||||
|
|
@ -100,6 +107,7 @@ export const NetworkTab: React.FunctionComponent<{
|
|||
setSorting={setSorting}
|
||||
/>;
|
||||
return <>
|
||||
<NetworkFilters filterState={filterState} onFilterStateChange={onFilterStateChange} />
|
||||
{!selectedEntry && grid}
|
||||
{selectedEntry &&
|
||||
<SplitView
|
||||
|
|
@ -345,3 +353,21 @@ function comparator(sortBy: ColumnName) {
|
|||
if (sortBy === 'contextId')
|
||||
return (a: RenderedEntry, b: RenderedEntry) => a.contextId.localeCompare(b.contextId);
|
||||
}
|
||||
|
||||
const resourceTypePredicates: Record<ResourceType, (contentType: string) => boolean> = {
|
||||
'All': () => true,
|
||||
'Fetch': contentType => contentType === 'application/json',
|
||||
'HTML': contentType => contentType === 'text/html',
|
||||
'CSS': contentType => contentType === 'text/css',
|
||||
'JS': contentType => contentType.includes('javascript'),
|
||||
'Font': contentType => contentType.includes('font'),
|
||||
'Image': contentType => contentType.includes('image'),
|
||||
};
|
||||
|
||||
function filterEntry({ searchValue, resourceType }: FilterState) {
|
||||
return (entry: RenderedEntry) => {
|
||||
const typePredicate = resourceTypePredicates[resourceType];
|
||||
|
||||
return typePredicate(entry.contentType) && entry.name.url.toLowerCase().includes(searchValue.toLowerCase());
|
||||
};
|
||||
}
|
||||
|
|
|
|||
BIN
tests/assets/network-tab/font.woff2
Normal file
BIN
tests/assets/network-tab/font.woff2
Normal file
Binary file not shown.
BIN
tests/assets/network-tab/image.png
Normal file
BIN
tests/assets/network-tab/image.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 312 B |
21
tests/assets/network-tab/network.html
Normal file
21
tests/assets/network-tab/network.html
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
<html>
|
||||
<head>
|
||||
<link rel='stylesheet' href='style.css'>
|
||||
<style>
|
||||
@font-face {
|
||||
font-family: 'font';
|
||||
src: url('font.woff2') format('woff2');
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'font';
|
||||
}
|
||||
</style>
|
||||
<script src='script.js'></script>
|
||||
<script>fetch('/api/endpoint')</script>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Network Tab Test</h1>
|
||||
<img src="image.png" >
|
||||
</body>
|
||||
</html>
|
||||
0
tests/assets/network-tab/script.js
Normal file
0
tests/assets/network-tab/script.js
Normal file
0
tests/assets/network-tab/style.css
Normal file
0
tests/assets/network-tab/style.css
Normal file
|
|
@ -256,6 +256,67 @@ test('should have network requests', async ({ showTraceViewer }) => {
|
|||
await expect(traceViewer.networkRequests.filter({ hasText: '404' })).toHaveCSS('background-color', 'rgb(242, 222, 222)');
|
||||
});
|
||||
|
||||
test('should filter network requests by resource type', async ({ page, runAndTrace, server }) => {
|
||||
const traceViewer = await runAndTrace(async () => {
|
||||
server.setRoute('/api/endpoint', (_, res) => res.setHeader('Content-Type', 'application/json').end());
|
||||
await page.goto(`${server.PREFIX}/network-tab/network.html`);
|
||||
});
|
||||
await traceViewer.selectAction('http://localhost');
|
||||
await traceViewer.showNetworkTab();
|
||||
|
||||
await traceViewer.page.getByText('JS', { exact: true }).click();
|
||||
await expect(traceViewer.networkRequests).toHaveCount(1);
|
||||
await expect(traceViewer.networkRequests.getByText('script.js')).toBeVisible();
|
||||
|
||||
await traceViewer.page.getByText('CSS', { exact: true }).click();
|
||||
await expect(traceViewer.networkRequests).toHaveCount(1);
|
||||
await expect(traceViewer.networkRequests.getByText('style.css')).toBeVisible();
|
||||
|
||||
await traceViewer.page.getByText('Image', { exact: true }).click();
|
||||
await expect(traceViewer.networkRequests).toHaveCount(1);
|
||||
await expect(traceViewer.networkRequests.getByText('image.png')).toBeVisible();
|
||||
|
||||
await traceViewer.page.getByText('Fetch', { exact: true }).click();
|
||||
await expect(traceViewer.networkRequests).toHaveCount(1);
|
||||
await expect(traceViewer.networkRequests.getByText('endpoint')).toBeVisible();
|
||||
|
||||
await traceViewer.page.getByText('HTML', { exact: true }).click();
|
||||
await expect(traceViewer.networkRequests).toHaveCount(1);
|
||||
await expect(traceViewer.networkRequests.getByText('network.html')).toBeVisible();
|
||||
|
||||
await traceViewer.page.getByText('Font', { exact: true }).click();
|
||||
await expect(traceViewer.networkRequests).toHaveCount(1);
|
||||
await expect(traceViewer.networkRequests.getByText('font.woff2')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should filter network requests by url', async ({ page, runAndTrace, server }) => {
|
||||
const traceViewer = await runAndTrace(async () => {
|
||||
await page.goto(`${server.PREFIX}/network-tab/network.html`);
|
||||
});
|
||||
await traceViewer.selectAction('http://localhost');
|
||||
await traceViewer.showNetworkTab();
|
||||
|
||||
await traceViewer.page.getByPlaceholder('Filter network').fill('script.');
|
||||
await expect(traceViewer.networkRequests).toHaveCount(1);
|
||||
await expect(traceViewer.networkRequests.getByText('script.js')).toBeVisible();
|
||||
|
||||
await traceViewer.page.getByPlaceholder('Filter network').fill('png');
|
||||
await expect(traceViewer.networkRequests).toHaveCount(1);
|
||||
await expect(traceViewer.networkRequests.getByText('image.png')).toBeVisible();
|
||||
|
||||
await traceViewer.page.getByPlaceholder('Filter network').fill('api/');
|
||||
await expect(traceViewer.networkRequests).toHaveCount(1);
|
||||
await expect(traceViewer.networkRequests.getByText('endpoint')).toBeVisible();
|
||||
|
||||
await traceViewer.page.getByPlaceholder('Filter network').fill('End');
|
||||
await expect(traceViewer.networkRequests).toHaveCount(1);
|
||||
await expect(traceViewer.networkRequests.getByText('endpoint')).toBeVisible();
|
||||
|
||||
await traceViewer.page.getByPlaceholder('Filter network').fill('FON');
|
||||
await expect(traceViewer.networkRequests).toHaveCount(1);
|
||||
await expect(traceViewer.networkRequests.getByText('font.woff2')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should have network request overrides', async ({ page, server, runAndTrace }) => {
|
||||
const traceViewer = await runAndTrace(async () => {
|
||||
await page.route('**/style.css', route => route.abort());
|
||||
|
|
|
|||
95
tests/playwright-test/ui-mode-test-network-tab.spec.ts
Normal file
95
tests/playwright-test/ui-mode-test-network-tab.spec.ts
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
/**
|
||||
* Copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { expect, test } from './ui-mode-fixtures';
|
||||
|
||||
test('should filter network requests by resource type', async ({ runUITest, server }) => {
|
||||
server.setRoute('/api/endpoint', (_, res) => res.setHeader('Content-Type', 'application/json').end());
|
||||
|
||||
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.getByText('network tab test').dblclick();
|
||||
await page.getByText('Network', { exact: true }).click();
|
||||
|
||||
const networkItems = page.getByTestId('network-list').getByRole('listitem');
|
||||
|
||||
await page.getByText('JS', { exact: true }).click();
|
||||
await expect(networkItems).toHaveCount(1);
|
||||
await expect(networkItems.getByText('script.js')).toBeVisible();
|
||||
|
||||
await page.getByText('CSS', { exact: true }).click();
|
||||
await expect(networkItems).toHaveCount(1);
|
||||
await expect(networkItems.getByText('style.css')).toBeVisible();
|
||||
|
||||
await page.getByText('Image', { exact: true }).click();
|
||||
await expect(networkItems).toHaveCount(1);
|
||||
await expect(networkItems.getByText('image.png')).toBeVisible();
|
||||
|
||||
await page.getByText('Fetch', { exact: true }).click();
|
||||
await expect(networkItems).toHaveCount(1);
|
||||
await expect(networkItems.getByText('endpoint')).toBeVisible();
|
||||
|
||||
await page.getByText('HTML', { exact: true }).click();
|
||||
await expect(networkItems).toHaveCount(1);
|
||||
await expect(networkItems.getByText('network.html')).toBeVisible();
|
||||
|
||||
await page.getByText('Font', { exact: true }).click();
|
||||
await expect(networkItems).toHaveCount(1);
|
||||
await expect(networkItems.getByText('font.woff2')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should filter network requests by url', 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.getByText('network tab test').dblclick();
|
||||
await page.getByText('Network', { exact: true }).click();
|
||||
|
||||
const networkItems = page.getByTestId('network-list').getByRole('listitem');
|
||||
|
||||
await page.getByPlaceholder('Filter network').fill('script.');
|
||||
await expect(networkItems).toHaveCount(1);
|
||||
await expect(networkItems.getByText('script.js')).toBeVisible();
|
||||
|
||||
await page.getByPlaceholder('Filter network').fill('png');
|
||||
await expect(networkItems).toHaveCount(1);
|
||||
await expect(networkItems.getByText('image.png')).toBeVisible();
|
||||
|
||||
await page.getByPlaceholder('Filter network').fill('api/');
|
||||
await expect(networkItems).toHaveCount(1);
|
||||
await expect(networkItems.getByText('endpoint')).toBeVisible();
|
||||
|
||||
await page.getByPlaceholder('Filter network').fill('End');
|
||||
await expect(networkItems).toHaveCount(1);
|
||||
await expect(networkItems.getByText('endpoint')).toBeVisible();
|
||||
|
||||
await page.getByPlaceholder('Filter network').fill('FON');
|
||||
await expect(networkItems).toHaveCount(1);
|
||||
await expect(networkItems.getByText('font.woff2')).toBeVisible();
|
||||
});
|
||||
Loading…
Reference in a new issue