mirror of
https://github.com/microsoft/playwright.git
synced 2025-06-26 21:40:17 +00:00
feat(trace viewer): allow hiding route actions (#31726)
Adds a new settings tab above the actions list. <img width="307" alt="settings tab" src="https://github.com/user-attachments/assets/792212b7-e2fd-4a5c-8878-654e2e060505"> Toggling the "Show route actions" checkbox hides all route calls: `continue`, `fulfill`, `fallback`, `abort` and `fetch`. References #30970.
This commit is contained in:
parent
e269092ef9
commit
d87cb7a303
30
packages/trace-viewer/src/ui/settingsView.css
Normal file
30
packages/trace-viewer/src/ui/settingsView.css
Normal file
@ -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;
|
||||||
|
}
|
||||||
36
packages/trace-viewer/src/ui/settingsView.tsx
Normal file
36
packages/trace-viewer/src/ui/settingsView.tsx
Normal file
@ -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<boolean>[],
|
||||||
|
}> = ({ settings }) => {
|
||||||
|
return <div className='vbox settings-view'>
|
||||||
|
{settings.map(setting => {
|
||||||
|
return <div key={setting.name} className='setting'>
|
||||||
|
<label>
|
||||||
|
<input type='checkbox' checked={setting.value} onClick={() => {
|
||||||
|
setting.set(!setting.value);
|
||||||
|
}}/>
|
||||||
|
{setting.title}
|
||||||
|
</label>
|
||||||
|
</div>;
|
||||||
|
})}
|
||||||
|
</div>;
|
||||||
|
};
|
||||||
@ -41,6 +41,7 @@ import type { Entry } from '@trace/har';
|
|||||||
import './workbench.css';
|
import './workbench.css';
|
||||||
import { testStatusIcon, testStatusText } from './testUtils';
|
import { testStatusIcon, testStatusText } from './testUtils';
|
||||||
import type { UITestStatus } from './testUtils';
|
import type { UITestStatus } from './testUtils';
|
||||||
|
import { SettingsView } from './settingsView';
|
||||||
|
|
||||||
export const Workbench: React.FunctionComponent<{
|
export const Workbench: React.FunctionComponent<{
|
||||||
model?: MultiTraceModel,
|
model?: MultiTraceModel,
|
||||||
@ -66,6 +67,11 @@ export const Workbench: React.FunctionComponent<{
|
|||||||
const activeAction = model ? highlightedAction || selectedAction : undefined;
|
const activeAction = model ? highlightedAction || selectedAction : undefined;
|
||||||
const [selectedTime, setSelectedTime] = React.useState<Boundaries | undefined>();
|
const [selectedTime, setSelectedTime] = React.useState<Boundaries | undefined>();
|
||||||
const [sidebarLocation, setSidebarLocation] = useSetting<'bottom' | 'right'>('propertiesSidebarLocation', 'bottom');
|
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) => {
|
const setSelectedAction = React.useCallback((action: ActionTraceEventInContext | undefined) => {
|
||||||
setSelectedActionImpl(action);
|
setSelectedActionImpl(action);
|
||||||
@ -261,7 +267,7 @@ export const Workbench: React.FunctionComponent<{
|
|||||||
</div>}
|
</div>}
|
||||||
<ActionList
|
<ActionList
|
||||||
sdkLanguage={sdkLanguage}
|
sdkLanguage={sdkLanguage}
|
||||||
actions={model?.actions || []}
|
actions={filteredActions}
|
||||||
selectedAction={model ? selectedAction : undefined}
|
selectedAction={model ? selectedAction : undefined}
|
||||||
selectedTime={selectedTime}
|
selectedTime={selectedTime}
|
||||||
setSelectedTime={setSelectedTime}
|
setSelectedTime={setSelectedTime}
|
||||||
@ -277,8 +283,15 @@ export const Workbench: React.FunctionComponent<{
|
|||||||
title: 'Metadata',
|
title: 'Metadata',
|
||||||
component: <MetadataView model={model}/>
|
component: <MetadataView model={model}/>
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'settings',
|
||||||
|
title: 'Settings',
|
||||||
|
component: <SettingsView settings={[showRouteActionsSetting]}/>,
|
||||||
|
}
|
||||||
]}
|
]}
|
||||||
selectedTab={selectedNavigatorTab} setSelectedTab={setSelectedNavigatorTab}/>
|
selectedTab={selectedNavigatorTab}
|
||||||
|
setSelectedTab={setSelectedNavigatorTab}
|
||||||
|
/>
|
||||||
</SplitView>
|
</SplitView>
|
||||||
<TabbedPane
|
<TabbedPane
|
||||||
tabs={tabs}
|
tabs={tabs}
|
||||||
|
|||||||
@ -24,7 +24,8 @@ export interface ToolbarButtonProps {
|
|||||||
disabled?: boolean,
|
disabled?: boolean,
|
||||||
toggled?: boolean,
|
toggled?: boolean,
|
||||||
onClick: (e: React.MouseEvent) => void,
|
onClick: (e: React.MouseEvent) => void,
|
||||||
style?: React.CSSProperties
|
style?: React.CSSProperties,
|
||||||
|
testId?: string,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ToolbarButton: React.FC<React.PropsWithChildren<ToolbarButtonProps>> = ({
|
export const ToolbarButton: React.FC<React.PropsWithChildren<ToolbarButtonProps>> = ({
|
||||||
@ -35,6 +36,7 @@ export const ToolbarButton: React.FC<React.PropsWithChildren<ToolbarButtonProps>
|
|||||||
toggled = false,
|
toggled = false,
|
||||||
onClick = () => {},
|
onClick = () => {},
|
||||||
style,
|
style,
|
||||||
|
testId,
|
||||||
}) => {
|
}) => {
|
||||||
let className = `toolbar-button ${icon}`;
|
let className = `toolbar-button ${icon}`;
|
||||||
if (toggled)
|
if (toggled)
|
||||||
@ -47,6 +49,7 @@ export const ToolbarButton: React.FC<React.PropsWithChildren<ToolbarButtonProps>
|
|||||||
title={title}
|
title={title}
|
||||||
disabled={!!disabled}
|
disabled={!!disabled}
|
||||||
style={style}
|
style={style}
|
||||||
|
data-testId={testId}
|
||||||
>
|
>
|
||||||
{icon && <span className={`codicon codicon-${icon}`} style={children ? { marginRight: 5 } : {}}></span>}
|
{icon && <span className={`codicon codicon-${icon}`} style={children ? { marginRight: 5 } : {}}></span>}
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@ -139,15 +139,29 @@ export function copy(text: string) {
|
|||||||
textArea.remove();
|
textArea.remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useSetting<S>(name: string | undefined, defaultValue: S): [S, React.Dispatch<React.SetStateAction<S>>] {
|
export type Setting<T> = {
|
||||||
const value = name ? settings.getObject(name, defaultValue) : defaultValue;
|
value: T;
|
||||||
const [state, setState] = React.useState<S>(value);
|
set: (value: T) => void;
|
||||||
const setStateWrapper = (value: React.SetStateAction<S>) => {
|
name: string;
|
||||||
|
title: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function useSetting<S>(name: string | undefined, defaultValue: S, title?: string): [S, React.Dispatch<React.SetStateAction<S>>, Setting<S>] {
|
||||||
|
if (name)
|
||||||
|
defaultValue = settings.getObject(name, defaultValue);
|
||||||
|
const [value, setValue] = React.useState<S>(defaultValue);
|
||||||
|
const setValueWrapper = React.useCallback((value: React.SetStateAction<S>) => {
|
||||||
if (name)
|
if (name)
|
||||||
settings.setObject(name, value);
|
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 {
|
export class Settings {
|
||||||
|
|||||||
@ -1332,3 +1332,39 @@ test('should show correct request start time', {
|
|||||||
expect(parseMillis(duration)).toBeGreaterThan(1000);
|
expect(parseMillis(duration)).toBeGreaterThan(1000);
|
||||||
expect(parseMillis(start)).toBeLessThan(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/,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user