diff --git a/packages/trace-viewer/src/ui/settingsView.css b/packages/trace-viewer/src/ui/settingsView.css new file mode 100644 index 0000000000..cc41911066 --- /dev/null +++ b/packages/trace-viewer/src/ui/settingsView.css @@ -0,0 +1,30 @@ +/* + 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. +*/ + +.settings-view { + flex: none; +} + +.settings-view .setting label { + display: flex; + flex-direction: row; + align-items: center; + margin: 6px 2px; +} + +.settings-view .setting input { + margin-right: 5px; +} diff --git a/packages/trace-viewer/src/ui/settingsView.tsx b/packages/trace-viewer/src/ui/settingsView.tsx new file mode 100644 index 0000000000..5f803d229f --- /dev/null +++ b/packages/trace-viewer/src/ui/settingsView.tsx @@ -0,0 +1,36 @@ +/** + * 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 * as React from 'react'; +import type { Setting } from '@web/uiUtils'; +import './settingsView.css'; + +export const SettingsView: React.FunctionComponent<{ + settings: Setting[], +}> = ({ settings }) => { + return
+ {settings.map(setting => { + return
+ +
; + })} +
; +}; diff --git a/packages/trace-viewer/src/ui/workbench.tsx b/packages/trace-viewer/src/ui/workbench.tsx index 0fd305280f..0a5775bfbc 100644 --- a/packages/trace-viewer/src/ui/workbench.tsx +++ b/packages/trace-viewer/src/ui/workbench.tsx @@ -41,6 +41,7 @@ import type { Entry } from '@trace/har'; import './workbench.css'; import { testStatusIcon, testStatusText } from './testUtils'; import type { UITestStatus } from './testUtils'; +import { SettingsView } from './settingsView'; export const Workbench: React.FunctionComponent<{ model?: MultiTraceModel, @@ -66,6 +67,11 @@ export const Workbench: React.FunctionComponent<{ const activeAction = model ? highlightedAction || selectedAction : undefined; const [selectedTime, setSelectedTime] = React.useState(); const [sidebarLocation, setSidebarLocation] = useSetting<'bottom' | 'right'>('propertiesSidebarLocation', 'bottom'); + const [showRouteActions, , showRouteActionsSetting] = useSetting('show-route-actions', true, 'Show route actions'); + + const filteredActions = React.useMemo(() => { + return (model?.actions || []).filter(action => showRouteActions || action.class !== 'Route'); + }, [model, showRouteActions]); const setSelectedAction = React.useCallback((action: ActionTraceEventInContext | undefined) => { setSelectedActionImpl(action); @@ -261,7 +267,7 @@ export const Workbench: React.FunctionComponent<{ } }, + { + id: 'settings', + title: 'Settings', + component: , + } ]} - selectedTab={selectedNavigatorTab} setSelectedTab={setSelectedNavigatorTab}/> + selectedTab={selectedNavigatorTab} + setSelectedTab={setSelectedNavigatorTab} + /> void, - style?: React.CSSProperties + style?: React.CSSProperties, + testId?: string, } export const ToolbarButton: React.FC> = ({ @@ -35,6 +36,7 @@ export const ToolbarButton: React.FC toggled = false, onClick = () => {}, style, + testId, }) => { let className = `toolbar-button ${icon}`; if (toggled) @@ -47,6 +49,7 @@ export const ToolbarButton: React.FC title={title} disabled={!!disabled} style={style} + data-testId={testId} > {icon && } {children} diff --git a/packages/web/src/uiUtils.ts b/packages/web/src/uiUtils.ts index 95d0d42a53..de28a807c1 100644 --- a/packages/web/src/uiUtils.ts +++ b/packages/web/src/uiUtils.ts @@ -139,15 +139,29 @@ export function copy(text: string) { textArea.remove(); } -export function useSetting(name: string | undefined, defaultValue: S): [S, React.Dispatch>] { - const value = name ? settings.getObject(name, defaultValue) : defaultValue; - const [state, setState] = React.useState(value); - const setStateWrapper = (value: React.SetStateAction) => { +export type Setting = { + value: T; + set: (value: T) => void; + name: string; + title: string; +}; + +export function useSetting(name: string | undefined, defaultValue: S, title?: string): [S, React.Dispatch>, Setting] { + if (name) + defaultValue = settings.getObject(name, defaultValue); + const [value, setValue] = React.useState(defaultValue); + const setValueWrapper = React.useCallback((value: React.SetStateAction) => { if (name) settings.setObject(name, value); - setState(value); + setValue(value); + }, [name, setValue]); + const setting = { + value, + set: setValueWrapper, + name: name || '', + title: title || name || '', }; - return [state, setStateWrapper]; + return [value, setValueWrapper, setting]; } export class Settings { diff --git a/tests/library/trace-viewer.spec.ts b/tests/library/trace-viewer.spec.ts index 1b31c904c1..822c50cc09 100644 --- a/tests/library/trace-viewer.spec.ts +++ b/tests/library/trace-viewer.spec.ts @@ -1332,3 +1332,39 @@ test('should show correct request start time', { expect(parseMillis(duration)).toBeGreaterThan(1000); expect(parseMillis(start)).toBeLessThan(1000); }); + +test('should allow hiding route actions', { + annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/30970' }, +}, async ({ page, runAndTrace, server }) => { + const traceViewer = await runAndTrace(async () => { + await page.route('**/*', async route => { + await route.fulfill({ contentType: 'text/html', body: 'Yo, page!' }); + }); + await page.goto(server.EMPTY_PAGE); + }); + + // Routes are visible by default. + await expect(traceViewer.actionTitles).toHaveText([ + /page.route/, + /page.goto.*empty.html/, + /route.fulfill/, + ]); + + await traceViewer.page.getByText('Settings').click(); + await expect(traceViewer.page.getByRole('checkbox', { name: 'Show route actions' })).toBeChecked(); + await traceViewer.page.getByRole('checkbox', { name: 'Show route actions' }).uncheck(); + await traceViewer.page.getByText('Actions', { exact: true }).click(); + await expect(traceViewer.actionTitles).toHaveText([ + /page.route/, + /page.goto.*empty.html/, + ]); + + await traceViewer.page.getByText('Settings').click(); + await traceViewer.page.getByRole('checkbox', { name: 'Show route actions' }).check(); + await traceViewer.page.getByText('Actions', { exact: true }).click(); + await expect(traceViewer.actionTitles).toHaveText([ + /page.route/, + /page.goto.*empty.html/, + /route.fulfill/, + ]); +});