diff --git a/packages/trace-viewer/src/ui/networkFilters.css b/packages/trace-viewer/src/ui/networkFilters.css
new file mode 100644
index 0000000000..836c7102c9
--- /dev/null
+++ b/packages/trace-viewer/src/ui/networkFilters.css
@@ -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);
+}
diff --git a/packages/trace-viewer/src/ui/networkFilters.tsx b/packages/trace-viewer/src/ui/networkFilters.tsx
new file mode 100644
index 0000000000..de6c827e2b
--- /dev/null
+++ b/packages/trace-viewer/src/ui/networkFilters.tsx
@@ -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 (
+
+
onFilterStateChange({ ...filterState, searchValue: e.target.value })}
+ />
+
+
+ {resourceTypes.map(resourceType => (
+
onFilterStateChange({ ...filterState, resourceType })}
+ className={`network-filters-resource-type ${filterState.resourceType === resourceType ? 'selected' : ''}`}
+ >
+ {resourceType}
+
+ ))}
+
+
+ );
+};
diff --git a/packages/trace-viewer/src/ui/networkTab.tsx b/packages/trace-viewer/src/ui/networkTab.tsx
index 36bb54547e..207dd33547 100644
--- a/packages/trace-viewer/src/ui/networkTab.tsx
+++ b/packages/trace-viewer/src/ui/networkTab.tsx
@@ -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(undefined);
const [selectedEntry, setSelectedEntry] = React.useState(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>(() => {
return new Map(allColumns().map(column => [column, columnWidth(column)]));
});
+ const onFilterStateChange = React.useCallback((newFilterState: FilterState) => {
+ setFilterState(newFilterState);
+ setSelectedEntry(undefined);
+ }, []);
+
if (!networkModel.resources.length)
return ;
@@ -100,6 +107,7 @@ export const NetworkTab: React.FunctionComponent<{
setSorting={setSorting}
/>;
return <>
+
{!selectedEntry && grid}
{selectedEntry &&
a.contextId.localeCompare(b.contextId);
}
+
+const resourceTypePredicates: Record 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());
+ };
+}
diff --git a/tests/assets/network-tab/font.woff2 b/tests/assets/network-tab/font.woff2
new file mode 100644
index 0000000000..ceba03549a
Binary files /dev/null and b/tests/assets/network-tab/font.woff2 differ
diff --git a/tests/assets/network-tab/image.png b/tests/assets/network-tab/image.png
new file mode 100644
index 0000000000..3942859ccc
Binary files /dev/null and b/tests/assets/network-tab/image.png differ
diff --git a/tests/assets/network-tab/network.html b/tests/assets/network-tab/network.html
new file mode 100644
index 0000000000..d46ff846dc
--- /dev/null
+++ b/tests/assets/network-tab/network.html
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+ Network Tab Test
+
+
+
diff --git a/tests/assets/network-tab/script.js b/tests/assets/network-tab/script.js
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/tests/assets/network-tab/style.css b/tests/assets/network-tab/style.css
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/tests/library/trace-viewer.spec.ts b/tests/library/trace-viewer.spec.ts
index d8386d1684..e8471c16d6 100644
--- a/tests/library/trace-viewer.spec.ts
+++ b/tests/library/trace-viewer.spec.ts
@@ -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());
diff --git a/tests/playwright-test/ui-mode-test-network-tab.spec.ts b/tests/playwright-test/ui-mode-test-network-tab.spec.ts
new file mode 100644
index 0000000000..45d77aa528
--- /dev/null
+++ b/tests/playwright-test/ui-mode-test-network-tab.spec.ts
@@ -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();
+});