diff --git a/packages/trace-viewer/src/ui/networkFilters.tsx b/packages/trace-viewer/src/ui/networkFilters.tsx index a88332e7c65f0..5ce9d57aa2dd6 100644 --- a/packages/trace-viewer/src/ui/networkFilters.tsx +++ b/packages/trace-viewer/src/ui/networkFilters.tsx @@ -16,15 +16,15 @@ import './networkFilters.css'; -const resourceTypes = ['All', 'Fetch', 'HTML', 'JS', 'CSS', 'Font', 'Image'] as const; +const resourceTypes = ['Fetch', 'HTML', 'JS', 'CSS', 'Font', 'Image'] as const; export type ResourceType = typeof resourceTypes[number]; export type FilterState = { searchValue: string; - resourceType: ResourceType; + resourceTypes: Set; }; -export const defaultFilterState: FilterState = { searchValue: '', resourceType: 'All' }; +export const defaultFilterState: FilterState = { searchValue: '', resourceTypes: new Set() }; export const NetworkFilters = ({ filterState, onFilterStateChange }: { filterState: FilterState, @@ -41,12 +41,28 @@ export const NetworkFilters = ({ filterState, onFilterStateChange }: { />
+
onFilterStateChange({ ...filterState, resourceTypes: new Set() })} + className={`network-filters-resource-type ${filterState.resourceTypes.size === 0 ? 'selected' : ''}`} + > + All +
+ {resourceTypes.map(resourceType => (
onFilterStateChange({ ...filterState, resourceType })} - className={`network-filters-resource-type ${filterState.resourceType === resourceType ? 'selected' : ''}`} + onClick={event => { + let newType; + if (event.ctrlKey || event.metaKey) + newType = filterState.resourceTypes.symmetricDifference(new Set([resourceType])); + else + newType = new Set([resourceType]); + + onFilterStateChange({ ...filterState, resourceTypes: newType }); + }} + className={`network-filters-resource-type ${filterState.resourceTypes.has(resourceType) ? 'selected' : ''}`} > {resourceType}
diff --git a/packages/trace-viewer/src/ui/networkTab.tsx b/packages/trace-viewer/src/ui/networkTab.tsx index 270368c997d08..f7342dfcc4a05 100644 --- a/packages/trace-viewer/src/ui/networkTab.tsx +++ b/packages/trace-viewer/src/ui/networkTab.tsx @@ -370,7 +370,6 @@ function comparator(sortBy: ColumnName) { } const resourceTypePredicates: Record boolean> = { - 'All': () => true, 'Fetch': contentType => contentType === 'application/json', 'HTML': contentType => contentType === 'text/html', 'CSS': contentType => contentType === 'text/css', @@ -379,10 +378,9 @@ const resourceTypePredicates: Record bool 'Image': contentType => contentType.includes('image'), }; -function filterEntry({ searchValue, resourceType }: FilterState) { +function filterEntry({ searchValue, resourceTypes }: FilterState) { return (entry: RenderedEntry) => { - const typePredicate = resourceTypePredicates[resourceType]; - - return typePredicate(entry.contentType) && entry.name.url.toLowerCase().includes(searchValue.toLowerCase()); + const isRightType = resourceTypes.size === 0 || Array.from(resourceTypes).some(type => resourceTypePredicates[type](entry.contentType)); + return isRightType && entry.name.url.toLowerCase().includes(searchValue.toLowerCase()); }; } diff --git a/tests/library/trace-viewer.spec.ts b/tests/library/trace-viewer.spec.ts index 477590138fe14..f5525a580b7eb 100644 --- a/tests/library/trace-viewer.spec.ts +++ b/tests/library/trace-viewer.spec.ts @@ -406,6 +406,39 @@ test('should filter network requests by resource type', async ({ page, runAndTra await expect(traceViewer.networkRequests.getByText('font.woff2')).toBeVisible(); }); +test('should filter network requests by multiple resource types', 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 page.evaluate(() => (window as any).donePromise); + }); + await traceViewer.selectAction('Navigate'); + await traceViewer.showNetworkTab(); + + const { networkRequests } = traceViewer; + + await traceViewer.page.getByText('JS', { exact: true }).click(); + await expect(networkRequests).toHaveCount(1); + await expect(networkRequests.getByText('script.js')).toBeVisible(); + + await traceViewer.page.getByText('CSS', { exact: true }).click({ modifiers: ['ControlOrMeta'] }); + await expect(networkRequests.getByText('script.js')).toBeVisible(); + await expect(networkRequests.getByText('style.css')).toBeVisible(); + await expect(networkRequests).toHaveCount(2); + + await traceViewer.page.getByText('Image', { exact: true }).click({ modifiers: ['ControlOrMeta'] }); + await expect(networkRequests.getByText('image.png')).toBeVisible(); + await expect(networkRequests).toHaveCount(3); + + await traceViewer.page.getByText('CSS', { exact: true }).click({ modifiers: ['ControlOrMeta'] }); + await expect(networkRequests).toHaveCount(2); + await expect(networkRequests.getByText('script.js')).toBeVisible(); + await expect(networkRequests.getByText('image.png')).toBeVisible(); + + await traceViewer.page.getByText('All', { exact: true }).click(); + await expect(networkRequests).toHaveCount(9); +}); + test('should show font preview', async ({ page, runAndTrace, server }) => { const traceViewer = await runAndTrace(async () => { await page.goto(`${server.PREFIX}/network-tab/network.html`); 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 14004105edf86..8ca37db175223 100644 --- a/tests/playwright-test/ui-mode-test-network-tab.spec.ts +++ b/tests/playwright-test/ui-mode-test-network-tab.spec.ts @@ -59,6 +59,47 @@ test('should filter network requests by resource type', async ({ runUITest, serv await expect(networkItems.getByText('font.woff2')).toBeVisible(); }); +test('should filter network requests by multiple resource types', 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.evaluate(() => (window as any).donePromise); + }); + `, + }); + + await page.getByText('network tab test').dblclick(); + await page.getByText('Network', { exact: true }).click(); + + const networkItems = page.getByRole('list', { name: 'Network requests' }).getByRole('listitem'); + await expect(networkItems).toHaveCount(9); + + 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({ modifiers: ['ControlOrMeta'] }); + await expect(networkItems.getByText('script.js')).toBeVisible(); + await expect(networkItems.getByText('style.css')).toBeVisible(); + await expect(networkItems).toHaveCount(2); + + await page.getByText('Image', { exact: true }).click({ modifiers: ['ControlOrMeta'] }); + await expect(networkItems.getByText('image.png')).toBeVisible(); + await expect(networkItems).toHaveCount(3); + + await page.getByText('CSS', { exact: true }).click({ modifiers: ['ControlOrMeta'] }); + await expect(networkItems).toHaveCount(2); + await expect(networkItems.getByText('script.js')).toBeVisible(); + await expect(networkItems.getByText('image.png')).toBeVisible(); + + await page.getByText('All', { exact: true }).click(); + await expect(networkItems).toHaveCount(9); +}); + test('should filter network requests by url', async ({ runUITest, server }) => { const { page } = await runUITest({ 'network-tab.test.ts': `