From 7ec3a93db3607e39b001e700020635bb017b4508 Mon Sep 17 00:00:00 2001 From: Kuba Janik Date: Tue, 6 Aug 2024 23:52:35 +0200 Subject: [PATCH] feat(ui-mode): add filters to network tab (#31956) --- .../trace-viewer/src/ui/networkFilters.css | 46 +++++++++ .../trace-viewer/src/ui/networkFilters.tsx | 57 +++++++++++ packages/trace-viewer/src/ui/networkTab.tsx | 30 +++++- tests/assets/network-tab/font.woff2 | Bin 0 -> 2656 bytes tests/assets/network-tab/image.png | Bin 0 -> 312 bytes tests/assets/network-tab/network.html | 21 ++++ tests/assets/network-tab/script.js | 0 tests/assets/network-tab/style.css | 0 tests/library/trace-viewer.spec.ts | 61 +++++++++++ .../ui-mode-test-network-tab.spec.ts | 95 ++++++++++++++++++ 10 files changed, 308 insertions(+), 2 deletions(-) create mode 100644 packages/trace-viewer/src/ui/networkFilters.css create mode 100644 packages/trace-viewer/src/ui/networkFilters.tsx create mode 100644 tests/assets/network-tab/font.woff2 create mode 100644 tests/assets/network-tab/image.png create mode 100644 tests/assets/network-tab/network.html create mode 100644 tests/assets/network-tab/script.js create mode 100644 tests/assets/network-tab/style.css create mode 100644 tests/playwright-test/ui-mode-test-network-tab.spec.ts 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 0000000000000000000000000000000000000000..ceba03549a59bd18dea3d36df96a2438540c9c50 GIT binary patch literal 2656 zcmV-m3ZM0NPew8T0RR910199L4*&oF02hz|0162J0RR9100000000000000000000 z0000SR0d!Gg9Zo=37iZO2nvJ%gb51>00A}vBm)ctAO(d@2R00W92eRJdEd>ugcKqIOq} z9QL2)O8eg=DUndb!lN*CZf>%(-9O18f+~SW1xS*>wMGE>6S7LP^*x4L9>c#?8co@( zWFJvKBm#J8Wf&D*v|Mz#U<$-c`@ZLo86I)#t$7 z;g^@cX%htsOB0X#n(2j;d`*CmUN=9>7X2pA0g+%uU_m2DCSG=kImYk==V-jK7{WH* zNKga_&CkQops~i|Oo7!*O=DJ`<}{5KPK&#@o= zN^~yM_*OO=H@>30{&CBYwAc2#Z1P6k+tw1zb$FyJk*TDoVBO?7Ac~|9I&8;FkT1Y;+uraB5sS$`;4T3UDuLQENQM* z6i1cScgL=ZBuGv+R?~Cd3^h9csSsOhzhkPmsl0LZT~3yL7LkX9MAszxIIb3O0S_-s z^t3U#-?+!32`pArYlF0rgsU0wKw5|_u74Y832qaSl=-n`uIY`um^>50MLh3~hJ4+P z$j{ipy(JnD<3)?npEcBU49l%iiL*?kwvx~;^yk|jOXj-K)NTNJrIe*g^G?2Hj!Ey) zo8+DOeBKAL(Nxb7lX)J<0g@!L`3!E8$hMftnZrGDvIDsoyESrC{Wj7b{h3@Ajk|Ox zxnvKMhvy$sGo5)pNoLo!0{KgMZ`1SlPPFrmmA`ZW8Z)zxp}gYuNRH&j3`BWs*%Uc9 zM@kdg-=rqOf*~p6ZQW5$j88a+oJ5_uN2Aj=||1}UoiDp}Yr)7$oJi!oN73LYBCtzal}CoZ4Jfg&`D3xVRH z5GoOhN`X?e@Lbd`MRL&7zlDE7TLe6hbGqB ze@D&1HnL+C^N{?*QP8bwsDa`IDEv^=LQw}rJroU4G(yn?MKd%v4E4rJE!7JpNi&=E zX6>0HiEwuE#^WRBI_cB$TXp>Y+b_JEz!vIdSi+m&c*HY*g1bU9bh z@2H!;i*q}n8(OD#VH=EcbhzCkQ4htsE#-Fvh(55}tuqwWJSyE#ua)jeK>UURJ?IYf zfnK?6glpON+J~m3Jf%)6u!!=f-Tnds`MDOYkmsy|XS2~*j!-gbtUOOy}X18la48Ur>amti)R*Q`g0 z6ijsl!dVykO0>&i3UXku8CH>mRV7(}d-cSjSiGXp64V~KdJ~4-k^~z0hVYm=_2wOF zg^^C-h8u1B+_0y2TLLZEYe&~7%9y!Fj6|y8gZ=>vPTIJ^_AQCP)HUEZFDYhvy@)+okd#f4+195ab)xEZG?3JsA&5l-OS{O^&HT|m5 z%PJWb%dL#FzNuGdw|D&=UxeoE^9ZFm{}ZbYtg54~!uIX$w)0&j-e(*k5EhujHSQ9O<{t^BMNCH>v5q-H#}gUa zAx_vsIB5_0rzRMs(-1^(Msw+`BXlm2;a%apJ&X(XFfQ7|xTJwDJ3?0+;j1ul1lQ~l zT(?JX!yds+4Rp&9y6s5ro#4B^JGE6(cKN$Tjqe}x*Ob@9KtkD&P&@|)3rPpu23SMvS#n*g3=H7Wj#3xr(@p`x1-tcQU z>*B`WX5HER$E-`6evdhLX=3iIFc*2|Hmq~&sC(A+;@kDI_I0LtS(&+IxtWh24=!5u z`q|mxiT7G*VfKhtaEGEpQ#+kiJTqzPiL=}C%4ON^_{N$ z#om}ZI?r8Pzke41^H24cx)EP(WC^&%ja)=4kJ6rtUa@gwyJsvBBT9=(;&Hh=aXd6C z0e)bvwUreMB0b#=^%bp^t$u&L-=F8RD*}-oTNGG|87!9=u}l)tTH=W41X}dkKyy)H zTe!I^(CJhcHWf9M7P{?{00Id7|6(z*GkfqKceb7b_-jq=J478V%`r3&1I9lk;26gYnBsb1I~&?7CtsC zvT?I>k(U6;d;7@eF&Z%*V3wD9i-a7>UZlv9YS!R{*nl&%$f8JAvJK9}A}^cd$UZ*m z<(Gf*oif(s{OR3|Pe$t(WliL5(CUeK^@L>*^cR%}o#0SEOPSFYZe8s5rn8zyjwjoX z9-V%!F?^9Q_+2#m8J=#3dThoxz(G!Nm>H7n<|q!0jM0QcJFQu9I}YO{-Qj@3ooCoj zg1tB-IL1+SGx-SN2;phu{S2P6Bo-ea%tSHrJ{GUDl9L>>cA4O?pyzqsG*-L!W>|;s z&_x{UrU*L OX|-JQJ!{e?8U+Ay83p73 literal 0 HcmV?d00001 diff --git a/tests/assets/network-tab/image.png b/tests/assets/network-tab/image.png new file mode 100644 index 0000000000000000000000000000000000000000..3942859ccc78c007fbc35d0ae9e065d031ed3de2 GIT binary patch literal 312 zcmeAS@N?(olHy`uVBq!ia0vp^Mj*@xBpA$Gw#oo0g=CK)Uj~LMH3o);76yi2K%s^g z3=E|P3=FRl7#OT(FffQ0%-I!a1C(GY3GxeOU?`h>)&j&!@^*J&V7%KUyadQ&FY)ws zWq-lJ$i-k(?SFbNP>9dd#W6(Ua&m$MYcnSU1LFh+#-p=udje%tOI#yLQW8s2t&)pU zffR$0fsu)>frYMtVThrDm4Shksev|-G%!$-p7sSrLvDUbW?Cg~4Z&`D9zYEma2rZ8 db5n~;5_1c1>tPAzpAOW+;OXk;vd$@?2>=(ZN{Rpg literal 0 HcmV?d00001 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(); +});