feat(trace-viewer): Add setting for display canvas content in snapshots (#34010)
This commit is contained in:
parent
ff9242104b
commit
ada68cd6f0
|
@ -255,7 +255,9 @@ declare global {
|
||||||
|
|
||||||
function snapshotScript(viewport: ViewportSize, ...targetIds: (string | undefined)[]) {
|
function snapshotScript(viewport: ViewportSize, ...targetIds: (string | undefined)[]) {
|
||||||
function applyPlaywrightAttributes(unwrapPopoutUrl: (url: string) => string, viewport: ViewportSize, ...targetIds: (string | undefined)[]) {
|
function applyPlaywrightAttributes(unwrapPopoutUrl: (url: string) => string, viewport: ViewportSize, ...targetIds: (string | undefined)[]) {
|
||||||
const isUnderTest = new URLSearchParams(location.search).has('isUnderTest');
|
const searchParams = new URLSearchParams(location.search);
|
||||||
|
const shouldPopulateCanvasFromScreenshot = searchParams.has('shouldPopulateCanvasFromScreenshot');
|
||||||
|
const isUnderTest = searchParams.has('isUnderTest');
|
||||||
|
|
||||||
// info to recursively compute canvas position relative to the top snapshot frame.
|
// info to recursively compute canvas position relative to the top snapshot frame.
|
||||||
// Before rendering each iframe, its parent extracts the '__playwright_canvas_render_info__' attribute
|
// Before rendering each iframe, its parent extracts the '__playwright_canvas_render_info__' attribute
|
||||||
|
@ -512,15 +514,20 @@ function snapshotScript(viewport: ViewportSize, ...targetIds: (string | undefine
|
||||||
|
|
||||||
drawCheckerboard(context, canvas);
|
drawCheckerboard(context, canvas);
|
||||||
|
|
||||||
|
if (shouldPopulateCanvasFromScreenshot) {
|
||||||
context.drawImage(img, boundingRect.left * img.width, boundingRect.top * img.height, (boundingRect.right - boundingRect.left) * img.width, (boundingRect.bottom - boundingRect.top) * img.height, 0, 0, canvas.width, canvas.height);
|
context.drawImage(img, boundingRect.left * img.width, boundingRect.top * img.height, (boundingRect.right - boundingRect.left) * img.width, (boundingRect.bottom - boundingRect.top) * img.height, 0, 0, canvas.width, canvas.height);
|
||||||
if (isUnderTest)
|
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.log(`canvas drawn:`, JSON.stringify([boundingRect.left, boundingRect.top, (boundingRect.right - boundingRect.left), (boundingRect.bottom - boundingRect.top)].map(v => Math.floor(v * 100))));
|
|
||||||
|
|
||||||
if (partiallyUncaptured)
|
if (partiallyUncaptured)
|
||||||
canvas.title = `Playwright couldn't capture full canvas contents because it's located partially outside the viewport.`;
|
canvas.title = `Playwright couldn't capture full canvas contents because it's located partially outside the viewport.`;
|
||||||
else
|
else
|
||||||
canvas.title = `Canvas contents are displayed on a best-effort basis based on viewport screenshots taken during test execution.`;
|
canvas.title = `Canvas contents are displayed on a best-effort basis based on viewport screenshots taken during test execution.`;
|
||||||
|
} else {
|
||||||
|
canvas.title = 'Canvas content display is disabled.';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isUnderTest)
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log(`canvas drawn:`, JSON.stringify([boundingRect.left, boundingRect.top, (boundingRect.right - boundingRect.left), (boundingRect.bottom - boundingRect.top)].map(v => Math.floor(v * 100))));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
img.onerror = () => {
|
img.onerror = () => {
|
||||||
|
|
|
@ -7,3 +7,4 @@
|
||||||
../geometry.ts
|
../geometry.ts
|
||||||
../../../playwright/src/isomorphic/**
|
../../../playwright/src/isomorphic/**
|
||||||
../third_party/devtools.ts
|
../third_party/devtools.ts
|
||||||
|
./shared/**
|
|
@ -0,0 +1,45 @@
|
||||||
|
/**
|
||||||
|
* 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 { SettingsView } from './settingsView';
|
||||||
|
import { useDarkModeSetting } from '@web/theme';
|
||||||
|
import { useSetting } from '@web/uiUtils';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A view of the collection of standard settings used between various applications
|
||||||
|
*/
|
||||||
|
export const DefaultSettingsView: React.FC<{}> = () => {
|
||||||
|
const [
|
||||||
|
shouldPopulateCanvasFromScreenshot,
|
||||||
|
setShouldPopulateCanvasFromScreenshot,
|
||||||
|
] = useSetting('shouldPopulateCanvasFromScreenshot', false);
|
||||||
|
const [darkMode, setDarkMode] = useDarkModeSetting();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SettingsView
|
||||||
|
settings={[
|
||||||
|
{ value: darkMode, set: setDarkMode, name: 'Dark mode' },
|
||||||
|
{
|
||||||
|
value: shouldPopulateCanvasFromScreenshot,
|
||||||
|
set: setShouldPopulateCanvasFromScreenshot,
|
||||||
|
name: 'Display canvas content',
|
||||||
|
title: 'Attempt to display the captured canvas appearance in the snapshot preview. May not be accurate.'
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
|
@ -21,7 +21,6 @@ import type { TabbedPaneTabModel } from '@web/components/tabbedPane';
|
||||||
import { TabbedPane } from '@web/components/tabbedPane';
|
import { TabbedPane } from '@web/components/tabbedPane';
|
||||||
import { Toolbar } from '@web/components/toolbar';
|
import { Toolbar } from '@web/components/toolbar';
|
||||||
import { ToolbarButton, ToolbarSeparator } from '@web/components/toolbarButton';
|
import { ToolbarButton, ToolbarSeparator } from '@web/components/toolbarButton';
|
||||||
import { toggleTheme } from '@web/theme';
|
|
||||||
import { copy, useSetting } from '@web/uiUtils';
|
import { copy, useSetting } from '@web/uiUtils';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { ConsoleTab, useConsoleTabModel } from '../consoleTab';
|
import { ConsoleTab, useConsoleTabModel } from '../consoleTab';
|
||||||
|
@ -37,6 +36,7 @@ import './recorderView.css';
|
||||||
import { ActionListView } from './actionListView';
|
import { ActionListView } from './actionListView';
|
||||||
import { BackendContext, BackendProvider } from './backendContext';
|
import { BackendContext, BackendProvider } from './backendContext';
|
||||||
import type { Language } from '@isomorphic/locatorGenerators';
|
import type { Language } from '@isomorphic/locatorGenerators';
|
||||||
|
import { SettingsToolbarButton } from '../settingsToolbarButton';
|
||||||
|
|
||||||
export const RecorderView: React.FunctionComponent = () => {
|
export const RecorderView: React.FunctionComponent = () => {
|
||||||
const searchParams = new URLSearchParams(window.location.search);
|
const searchParams = new URLSearchParams(window.location.search);
|
||||||
|
@ -148,7 +148,7 @@ export const Workbench: React.FunctionComponent = () => {
|
||||||
<SourceChooser fileId={fileId} sources={backend?.sources || []} setFileId={fileId => {
|
<SourceChooser fileId={fileId} sources={backend?.sources || []} setFileId={fileId => {
|
||||||
setFileId(fileId);
|
setFileId(fileId);
|
||||||
}} />
|
}} />
|
||||||
<ToolbarButton icon='color-mode' title='Toggle color mode' toggled={false} onClick={() => toggleTheme()}></ToolbarButton>
|
<SettingsToolbarButton />
|
||||||
</Toolbar>;
|
</Toolbar>;
|
||||||
|
|
||||||
const sidebarTabbedPane = <TabbedPane tabs={[actionsTab]} />;
|
const sidebarTabbedPane = <TabbedPane tabs={[actionsTab]} />;
|
||||||
|
@ -271,6 +271,10 @@ const TraceView: React.FunctionComponent<{
|
||||||
setHighlightedLocator,
|
setHighlightedLocator,
|
||||||
}) => {
|
}) => {
|
||||||
const model = React.useContext(ModelContext);
|
const model = React.useContext(ModelContext);
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
const [shouldPopulateCanvasFromScreenshot, _] = useSetting('shouldPopulateCanvasFromScreenshot', false);
|
||||||
|
|
||||||
const action = React.useMemo(() => {
|
const action = React.useMemo(() => {
|
||||||
return model?.actions.find(a => a.callId === callId);
|
return model?.actions.find(a => a.callId === callId);
|
||||||
}, [model, callId]);
|
}, [model, callId]);
|
||||||
|
@ -280,8 +284,8 @@ const TraceView: React.FunctionComponent<{
|
||||||
return snapshot.action || snapshot.after || snapshot.before;
|
return snapshot.action || snapshot.after || snapshot.before;
|
||||||
}, [action]);
|
}, [action]);
|
||||||
const snapshotUrls = React.useMemo(() => {
|
const snapshotUrls = React.useMemo(() => {
|
||||||
return snapshot ? extendSnapshot(snapshot) : undefined;
|
return snapshot ? extendSnapshot(snapshot, shouldPopulateCanvasFromScreenshot) : undefined;
|
||||||
}, [snapshot]);
|
}, [snapshot, shouldPopulateCanvasFromScreenshot]);
|
||||||
|
|
||||||
return <SnapshotView
|
return <SnapshotView
|
||||||
sdkLanguage={sdkLanguage}
|
sdkLanguage={sdkLanguage}
|
||||||
|
|
|
@ -0,0 +1,52 @@
|
||||||
|
/*
|
||||||
|
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 { Dialog } from './shared/dialog';
|
||||||
|
import { ToolbarButton } from '@web/components/toolbarButton';
|
||||||
|
import { DefaultSettingsView } from './defaultSettingsView';
|
||||||
|
|
||||||
|
export const SettingsToolbarButton: React.FC<{}> = () => {
|
||||||
|
const hostingRef = React.useRef<HTMLButtonElement>(null);
|
||||||
|
|
||||||
|
const [open, setOpen] = React.useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ToolbarButton
|
||||||
|
ref={hostingRef}
|
||||||
|
icon='settings-gear'
|
||||||
|
title='Settings'
|
||||||
|
onClick={() => setOpen(current => !current)}
|
||||||
|
/>
|
||||||
|
<Dialog
|
||||||
|
style={{
|
||||||
|
backgroundColor: 'var(--vscode-sideBar-background)',
|
||||||
|
padding: '4px 8px'
|
||||||
|
}}
|
||||||
|
open={open}
|
||||||
|
width={200}
|
||||||
|
// TODO: Temporary spacing until design of toolbar buttons is revisited
|
||||||
|
verticalOffset={8}
|
||||||
|
requestClose={() => setOpen(false)}
|
||||||
|
anchor={hostingRef}
|
||||||
|
dataTestId='settings-toolbar-dialog'
|
||||||
|
>
|
||||||
|
<DefaultSettingsView />
|
||||||
|
</Dialog>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
|
@ -15,6 +15,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.settings-view {
|
.settings-view {
|
||||||
|
display: flex;
|
||||||
flex: none;
|
flex: none;
|
||||||
padding: 4px 0px;
|
padding: 4px 0px;
|
||||||
row-gap: 8px;
|
row-gap: 8px;
|
||||||
|
|
|
@ -18,20 +18,24 @@ import * as React from 'react';
|
||||||
|
|
||||||
export interface DialogProps {
|
export interface DialogProps {
|
||||||
className?: string;
|
className?: string;
|
||||||
|
style?: React.CSSProperties;
|
||||||
open: boolean;
|
open: boolean;
|
||||||
width: number;
|
width: number;
|
||||||
verticalOffset?: number;
|
verticalOffset?: number;
|
||||||
requestClose?: () => void;
|
requestClose?: () => void;
|
||||||
anchor?: React.RefObject<HTMLElement>;
|
anchor?: React.RefObject<HTMLElement>;
|
||||||
|
dataTestId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Dialog: React.FC<React.PropsWithChildren<DialogProps>> = ({
|
export const Dialog: React.FC<React.PropsWithChildren<DialogProps>> = ({
|
||||||
className,
|
className,
|
||||||
|
style: externalStyle,
|
||||||
open,
|
open,
|
||||||
width,
|
width,
|
||||||
verticalOffset,
|
verticalOffset,
|
||||||
requestClose,
|
requestClose,
|
||||||
anchor,
|
anchor,
|
||||||
|
dataTestId,
|
||||||
children,
|
children,
|
||||||
}) => {
|
}) => {
|
||||||
const dialogRef = React.useRef<HTMLDialogElement>(null);
|
const dialogRef = React.useRef<HTMLDialogElement>(null);
|
||||||
|
@ -39,17 +43,19 @@ export const Dialog: React.FC<React.PropsWithChildren<DialogProps>> = ({
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
const [_, setRecalculateDimensionsCount] = React.useState(0);
|
const [_, setRecalculateDimensionsCount] = React.useState(0);
|
||||||
|
|
||||||
let style: React.CSSProperties | undefined = undefined;
|
let style: React.CSSProperties | undefined = externalStyle;
|
||||||
|
|
||||||
if (anchor?.current) {
|
if (anchor?.current) {
|
||||||
const bounds = anchor.current.getBoundingClientRect();
|
const bounds = anchor.current.getBoundingClientRect();
|
||||||
|
|
||||||
style = {
|
style = {
|
||||||
|
position: 'fixed',
|
||||||
margin: 0,
|
margin: 0,
|
||||||
top: bounds.bottom + (verticalOffset ?? 0),
|
top: bounds.bottom + (verticalOffset ?? 0),
|
||||||
left: buildTopLeftCoord(bounds, width),
|
left: buildTopLeftCoord(bounds, width),
|
||||||
width,
|
width,
|
||||||
zIndex: 1,
|
zIndex: 1,
|
||||||
|
...externalStyle
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -92,7 +98,7 @@ export const Dialog: React.FC<React.PropsWithChildren<DialogProps>> = ({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
open && (
|
open && (
|
||||||
<dialog ref={dialogRef} style={style} className={className} open>
|
<dialog ref={dialogRef} style={style} className={className} data-testid={dataTestId} open>
|
||||||
{children}
|
{children}
|
||||||
</dialog>
|
</dialog>
|
||||||
)
|
)
|
||||||
|
|
|
@ -20,7 +20,7 @@ import type { ActionTraceEvent } from '@trace/trace';
|
||||||
import { context, type MultiTraceModel, prevInList } from './modelUtil';
|
import { context, type MultiTraceModel, prevInList } from './modelUtil';
|
||||||
import { Toolbar } from '@web/components/toolbar';
|
import { Toolbar } from '@web/components/toolbar';
|
||||||
import { ToolbarButton } from '@web/components/toolbarButton';
|
import { ToolbarButton } from '@web/components/toolbarButton';
|
||||||
import { clsx, useMeasure } from '@web/uiUtils';
|
import { clsx, useMeasure, useSetting } from '@web/uiUtils';
|
||||||
import { InjectedScript } from '@injected/injectedScript';
|
import { InjectedScript } from '@injected/injectedScript';
|
||||||
import { Recorder } from '@injected/recorder/recorder';
|
import { Recorder } from '@injected/recorder/recorder';
|
||||||
import ConsoleAPI from '@injected/consoleApi';
|
import ConsoleAPI from '@injected/consoleApi';
|
||||||
|
@ -43,13 +43,16 @@ export const SnapshotTabsView: React.FunctionComponent<{
|
||||||
}> = ({ action, sdkLanguage, testIdAttributeName, isInspecting, setIsInspecting, highlightedLocator, setHighlightedLocator }) => {
|
}> = ({ action, sdkLanguage, testIdAttributeName, isInspecting, setIsInspecting, highlightedLocator, setHighlightedLocator }) => {
|
||||||
const [snapshotTab, setSnapshotTab] = React.useState<'action'|'before'|'after'>('action');
|
const [snapshotTab, setSnapshotTab] = React.useState<'action'|'before'|'after'>('action');
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
const [shouldPopulateCanvasFromScreenshot, _] = useSetting('shouldPopulateCanvasFromScreenshot', false);
|
||||||
|
|
||||||
const snapshots = React.useMemo(() => {
|
const snapshots = React.useMemo(() => {
|
||||||
return collectSnapshots(action);
|
return collectSnapshots(action);
|
||||||
}, [action]);
|
}, [action]);
|
||||||
const snapshotUrls = React.useMemo(() => {
|
const snapshotUrls = React.useMemo(() => {
|
||||||
const snapshot = snapshots[snapshotTab];
|
const snapshot = snapshots[snapshotTab];
|
||||||
return snapshot ? extendSnapshot(snapshot) : undefined;
|
return snapshot ? extendSnapshot(snapshot, shouldPopulateCanvasFromScreenshot) : undefined;
|
||||||
}, [snapshots, snapshotTab]);
|
}, [snapshots, snapshotTab, shouldPopulateCanvasFromScreenshot]);
|
||||||
|
|
||||||
return <div className='snapshot-tab vbox'>
|
return <div className='snapshot-tab vbox'>
|
||||||
<Toolbar>
|
<Toolbar>
|
||||||
|
@ -327,7 +330,7 @@ export function collectSnapshots(action: ActionTraceEvent | undefined): Snapshot
|
||||||
const isUnderTest = new URLSearchParams(window.location.search).has('isUnderTest');
|
const isUnderTest = new URLSearchParams(window.location.search).has('isUnderTest');
|
||||||
const serverParam = new URLSearchParams(window.location.search).get('server');
|
const serverParam = new URLSearchParams(window.location.search).get('server');
|
||||||
|
|
||||||
export function extendSnapshot(snapshot: Snapshot): SnapshotUrls {
|
export function extendSnapshot(snapshot: Snapshot, shouldPopulateCanvasFromScreenshot: boolean): SnapshotUrls {
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
params.set('trace', context(snapshot.action).traceUrl);
|
params.set('trace', context(snapshot.action).traceUrl);
|
||||||
params.set('name', snapshot.snapshotName);
|
params.set('name', snapshot.snapshotName);
|
||||||
|
@ -339,6 +342,9 @@ export function extendSnapshot(snapshot: Snapshot): SnapshotUrls {
|
||||||
if (snapshot.hasInputTarget)
|
if (snapshot.hasInputTarget)
|
||||||
params.set('hasInputTarget', '1');
|
params.set('hasInputTarget', '1');
|
||||||
}
|
}
|
||||||
|
if (shouldPopulateCanvasFromScreenshot)
|
||||||
|
params.set('shouldPopulateCanvasFromScreenshot', '1');
|
||||||
|
|
||||||
const snapshotUrl = new URL(`snapshot/${snapshot.action.pageId}?${params.toString()}`, window.location.href).toString();
|
const snapshotUrl = new URL(`snapshot/${snapshot.action.pageId}?${params.toString()}`, window.location.href).toString();
|
||||||
const snapshotInfoUrl = new URL(`snapshotInfo/${snapshot.action.pageId}?${params.toString()}`, window.location.href).toString();
|
const snapshotInfoUrl = new URL(`snapshotInfo/${snapshot.action.pageId}?${params.toString()}`, window.location.href).toString();
|
||||||
|
|
||||||
|
|
|
@ -28,7 +28,6 @@ import { ToolbarButton } from '@web/components/toolbarButton';
|
||||||
import { Toolbar } from '@web/components/toolbar';
|
import { Toolbar } from '@web/components/toolbar';
|
||||||
import type { XtermDataSource } from '@web/components/xtermWrapper';
|
import type { XtermDataSource } from '@web/components/xtermWrapper';
|
||||||
import { XtermWrapper } from '@web/components/xtermWrapper';
|
import { XtermWrapper } from '@web/components/xtermWrapper';
|
||||||
import { useDarkModeSetting } from '@web/theme';
|
|
||||||
import { clsx, settings, useSetting } from '@web/uiUtils';
|
import { clsx, settings, useSetting } from '@web/uiUtils';
|
||||||
import { statusEx, TestTree } from '@testIsomorphic/testTree';
|
import { statusEx, TestTree } from '@testIsomorphic/testTree';
|
||||||
import type { TreeItem } from '@testIsomorphic/testTree';
|
import type { TreeItem } from '@testIsomorphic/testTree';
|
||||||
|
@ -37,6 +36,7 @@ import { FiltersView } from './uiModeFiltersView';
|
||||||
import { TestListView } from './uiModeTestListView';
|
import { TestListView } from './uiModeTestListView';
|
||||||
import { TraceView } from './uiModeTraceView';
|
import { TraceView } from './uiModeTraceView';
|
||||||
import { SettingsView } from './settingsView';
|
import { SettingsView } from './settingsView';
|
||||||
|
import { DefaultSettingsView } from './defaultSettingsView';
|
||||||
|
|
||||||
let xtermSize = { cols: 80, rows: 24 };
|
let xtermSize = { cols: 80, rows: 24 };
|
||||||
const xtermDataSource: XtermDataSource = {
|
const xtermDataSource: XtermDataSource = {
|
||||||
|
@ -104,7 +104,6 @@ export const UIModeView: React.FC<{}> = ({
|
||||||
const [singleWorker, setSingleWorker] = React.useState(false);
|
const [singleWorker, setSingleWorker] = React.useState(false);
|
||||||
const [showBrowser, setShowBrowser] = React.useState(false);
|
const [showBrowser, setShowBrowser] = React.useState(false);
|
||||||
const [updateSnapshots, setUpdateSnapshots] = React.useState(false);
|
const [updateSnapshots, setUpdateSnapshots] = React.useState(false);
|
||||||
const [darkMode, setDarkMode] = useDarkModeSetting();
|
|
||||||
|
|
||||||
const inputRef = React.useRef<HTMLInputElement>(null);
|
const inputRef = React.useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
@ -521,9 +520,7 @@ export const UIModeView: React.FC<{}> = ({
|
||||||
/>
|
/>
|
||||||
<div className='section-title'>Settings</div>
|
<div className='section-title'>Settings</div>
|
||||||
</Toolbar>
|
</Toolbar>
|
||||||
{settingsVisible && <SettingsView settings={[
|
{settingsVisible && <DefaultSettingsView />}
|
||||||
{ value: darkMode, set: setDarkMode, name: 'Dark mode' },
|
|
||||||
]} />}
|
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -14,14 +14,13 @@
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { ToolbarButton } from '@web/components/toolbarButton';
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import type { ContextEntry } from '../types/entries';
|
import type { ContextEntry } from '../types/entries';
|
||||||
import { MultiTraceModel } from './modelUtil';
|
import { MultiTraceModel } from './modelUtil';
|
||||||
import './workbenchLoader.css';
|
import './workbenchLoader.css';
|
||||||
import { toggleTheme } from '@web/theme';
|
|
||||||
import { Workbench } from './workbench';
|
import { Workbench } from './workbench';
|
||||||
import { TestServerConnection, WebSocketTestServerTransport } from '@testIsomorphic/testServerConnection';
|
import { TestServerConnection, WebSocketTestServerTransport } from '@testIsomorphic/testServerConnection';
|
||||||
|
import { SettingsToolbarButton } from './settingsToolbarButton';
|
||||||
|
|
||||||
export const WorkbenchLoader: React.FunctionComponent<{
|
export const WorkbenchLoader: React.FunctionComponent<{
|
||||||
}> = () => {
|
}> = () => {
|
||||||
|
@ -161,7 +160,7 @@ export const WorkbenchLoader: React.FunctionComponent<{
|
||||||
<div className='product'>Playwright</div>
|
<div className='product'>Playwright</div>
|
||||||
{model.title && <div className='title'>{model.title}</div>}
|
{model.title && <div className='title'>{model.title}</div>}
|
||||||
<div className='spacer'></div>
|
<div className='spacer'></div>
|
||||||
<ToolbarButton icon='color-mode' title='Toggle color mode' toggled={false} onClick={() => toggleTheme()}></ToolbarButton>
|
<SettingsToolbarButton />
|
||||||
</div>
|
</div>
|
||||||
<div className='progress'>
|
<div className='progress'>
|
||||||
<div className='inner-progress' style={{ width: progress.total ? (100 * progress.done / progress.total) + '%' : 0 }}></div>
|
<div className='inner-progress' style={{ width: progress.total ? (100 * progress.done / progress.total) + '%' : 0 }}></div>
|
||||||
|
|
|
@ -31,7 +31,7 @@ export interface ToolbarButtonProps {
|
||||||
ariaLabel?: string,
|
ariaLabel?: string,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ToolbarButton: React.FC<React.PropsWithChildren<ToolbarButtonProps>> = ({
|
export const ToolbarButton = React.forwardRef<HTMLButtonElement, React.PropsWithChildren<ToolbarButtonProps>>(function ToolbarButton({
|
||||||
children,
|
children,
|
||||||
title = '',
|
title = '',
|
||||||
icon,
|
icon,
|
||||||
|
@ -42,8 +42,9 @@ export const ToolbarButton: React.FC<React.PropsWithChildren<ToolbarButtonProps>
|
||||||
testId,
|
testId,
|
||||||
className,
|
className,
|
||||||
ariaLabel,
|
ariaLabel,
|
||||||
}) => {
|
}, ref) {
|
||||||
return <button
|
return <button
|
||||||
|
ref={ref}
|
||||||
className={clsx(className, 'toolbar-button', icon, toggled && 'toggled')}
|
className={clsx(className, 'toolbar-button', icon, toggled && 'toggled')}
|
||||||
onMouseDown={preventDefault}
|
onMouseDown={preventDefault}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
|
@ -57,7 +58,7 @@ export const ToolbarButton: React.FC<React.PropsWithChildren<ToolbarButtonProps>
|
||||||
{icon && <span className={`codicon codicon-${icon}`} style={children ? { marginRight: 5 } : {}}></span>}
|
{icon && <span className={`codicon codicon-${icon}`} style={children ? { marginRight: 5 } : {}}></span>}
|
||||||
{children}
|
{children}
|
||||||
</button>;
|
</button>;
|
||||||
};
|
});
|
||||||
|
|
||||||
export const ToolbarSeparator: React.FC<{ style?: React.CSSProperties }> = ({
|
export const ToolbarSeparator: React.FC<{ style?: React.CSSProperties }> = ({
|
||||||
style,
|
style,
|
||||||
|
|
|
@ -49,6 +49,10 @@ class TraceViewerPage {
|
||||||
snapshotContainer: Locator;
|
snapshotContainer: Locator;
|
||||||
sourceCodeTab: Locator;
|
sourceCodeTab: Locator;
|
||||||
|
|
||||||
|
settingsDialog: Locator;
|
||||||
|
darkModeSetting: Locator;
|
||||||
|
displayCanvasContentSetting: Locator;
|
||||||
|
|
||||||
constructor(public page: Page) {
|
constructor(public page: Page) {
|
||||||
this.actionTitles = page.locator('.action-title');
|
this.actionTitles = page.locator('.action-title');
|
||||||
this.actionsTree = page.getByTestId('actions-tree');
|
this.actionsTree = page.getByTestId('actions-tree');
|
||||||
|
@ -63,6 +67,10 @@ class TraceViewerPage {
|
||||||
this.snapshotContainer = page.locator('.snapshot-container iframe.snapshot-visible[name=snapshot]');
|
this.snapshotContainer = page.locator('.snapshot-container iframe.snapshot-visible[name=snapshot]');
|
||||||
this.metadataTab = page.getByTestId('metadata-view');
|
this.metadataTab = page.getByTestId('metadata-view');
|
||||||
this.sourceCodeTab = page.getByTestId('source-code');
|
this.sourceCodeTab = page.getByTestId('source-code');
|
||||||
|
|
||||||
|
this.settingsDialog = page.getByTestId('settings-toolbar-dialog');
|
||||||
|
this.darkModeSetting = page.locator('.setting').getByText('Dark mode');
|
||||||
|
this.displayCanvasContentSetting = page.locator('.setting').getByText('Display canvas content');
|
||||||
}
|
}
|
||||||
|
|
||||||
async actionIconsText(action: string) {
|
async actionIconsText(action: string) {
|
||||||
|
@ -115,6 +123,10 @@ class TraceViewerPage {
|
||||||
await this.page.click('text="Metadata"');
|
await this.page.click('text="Metadata"');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async showSettings() {
|
||||||
|
await this.page.locator('.settings-gear').click();
|
||||||
|
}
|
||||||
|
|
||||||
@step
|
@step
|
||||||
async snapshotFrame(actionName: string, ordinal: number = 0, hasSubframe: boolean = false): Promise<FrameLocator> {
|
async snapshotFrame(actionName: string, ordinal: number = 0, hasSubframe: boolean = false): Promise<FrameLocator> {
|
||||||
await this.selectAction(actionName, ordinal);
|
await this.selectAction(actionName, ordinal);
|
||||||
|
|
|
@ -1521,12 +1521,26 @@ test('should serve css without content-type', async ({ page, runAndTrace, server
|
||||||
await expect(snapshotFrame.locator('body')).toHaveCSS('background-color', 'rgb(255, 0, 0)', { timeout: 0 });
|
await expect(snapshotFrame.locator('body')).toHaveCSS('background-color', 'rgb(255, 0, 0)', { timeout: 0 });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('canvas disabled title', async ({ runAndTrace, page, server }) => {
|
||||||
|
const traceViewer = await runAndTrace(async () => {
|
||||||
|
await page.goto(server.PREFIX + '/screenshots/canvas.html#canvas-on-edge');
|
||||||
|
await rafraf(page, 5);
|
||||||
|
});
|
||||||
|
|
||||||
|
const snapshot = await traceViewer.snapshotFrame('page.goto');
|
||||||
|
await expect(snapshot.locator('canvas')).toHaveAttribute('title', `Canvas content display is disabled.`);
|
||||||
|
});
|
||||||
|
|
||||||
test('canvas clipping', async ({ runAndTrace, page, server }) => {
|
test('canvas clipping', async ({ runAndTrace, page, server }) => {
|
||||||
const traceViewer = await runAndTrace(async () => {
|
const traceViewer = await runAndTrace(async () => {
|
||||||
await page.goto(server.PREFIX + '/screenshots/canvas.html#canvas-on-edge');
|
await page.goto(server.PREFIX + '/screenshots/canvas.html#canvas-on-edge');
|
||||||
await rafraf(page, 5);
|
await rafraf(page, 5);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Enable canvas display
|
||||||
|
await traceViewer.showSettings();
|
||||||
|
await traceViewer.displayCanvasContentSetting.click();
|
||||||
|
|
||||||
const msg = await traceViewer.page.waitForEvent('console', { predicate: msg => msg.text().startsWith('canvas drawn:') });
|
const msg = await traceViewer.page.waitForEvent('console', { predicate: msg => msg.text().startsWith('canvas drawn:') });
|
||||||
expect(msg.text()).toEqual('canvas drawn: [0,91,11,20]');
|
expect(msg.text()).toEqual('canvas drawn: [0,91,11,20]');
|
||||||
|
|
||||||
|
@ -1543,6 +1557,10 @@ test('canvas clipping in iframe', async ({ runAndTrace, page, server }) => {
|
||||||
await rafraf(page, 5);
|
await rafraf(page, 5);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Enable canvas display
|
||||||
|
await traceViewer.showSettings();
|
||||||
|
await traceViewer.displayCanvasContentSetting.click();
|
||||||
|
|
||||||
const msg = await traceViewer.page.waitForEvent('console', { predicate: msg => msg.text().startsWith('canvas drawn:') });
|
const msg = await traceViewer.page.waitForEvent('console', { predicate: msg => msg.text().startsWith('canvas drawn:') });
|
||||||
expect(msg.text()).toEqual('canvas drawn: [1,1,11,20]');
|
expect(msg.text()).toEqual('canvas drawn: [1,1,11,20]');
|
||||||
|
|
||||||
|
@ -1593,3 +1611,60 @@ test('should show a popover', async ({ runAndTrace, page, server }) => {
|
||||||
const popover = snapshot.locator('#pop');
|
const popover = snapshot.locator('#pop');
|
||||||
await expect.poll(() => popover.evaluate(e => e.matches(':popover-open'))).toBe(true);
|
await expect.poll(() => popover.evaluate(e => e.matches(':popover-open'))).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should open settings dialog', async ({ showTraceViewer }) => {
|
||||||
|
const traceViewer = await showTraceViewer([traceFile]);
|
||||||
|
await traceViewer.selectAction('http://localhost');
|
||||||
|
await traceViewer.showSettings();
|
||||||
|
await expect(traceViewer.settingsDialog).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should toggle theme color', async ({ showTraceViewer, page }) => {
|
||||||
|
const traceViewer = await showTraceViewer([traceFile]);
|
||||||
|
await traceViewer.selectAction('http://localhost');
|
||||||
|
await traceViewer.showSettings();
|
||||||
|
|
||||||
|
await expect(traceViewer.darkModeSetting).toBeChecked({ checked: false });
|
||||||
|
|
||||||
|
await traceViewer.darkModeSetting.click();
|
||||||
|
await expect(traceViewer.darkModeSetting).toBeChecked({ checked: true });
|
||||||
|
await expect(traceViewer.page.locator('.dark-mode')).toBeVisible();
|
||||||
|
|
||||||
|
await traceViewer.darkModeSetting.click();
|
||||||
|
await expect(traceViewer.darkModeSetting).toBeChecked({ checked: false });
|
||||||
|
await expect(traceViewer.page.locator('.light-mode')).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should toggle canvas rendering', async ({ runAndTrace, page }) => {
|
||||||
|
const traceViewer = await runAndTrace(async () => {
|
||||||
|
await page.goto(`data:text/html,<!DOCTYPE html><body><div>Hello world</div><canvas /></body>`);
|
||||||
|
await page.goto(`data:text/html,<!DOCTYPE html><body><div>Hello world</div></body>`);
|
||||||
|
});
|
||||||
|
|
||||||
|
let snapshotRequestPromise = traceViewer.page.waitForRequest(request => request.url().includes('/snapshot/'));
|
||||||
|
|
||||||
|
// Click on the action with a canvas snapshot
|
||||||
|
await traceViewer.selectAction('goto', 0);
|
||||||
|
|
||||||
|
let snapshotRequest = await snapshotRequestPromise;
|
||||||
|
|
||||||
|
expect(snapshotRequest.url()).not.toContain('shouldPopulateCanvasFromScreenshot');
|
||||||
|
|
||||||
|
await traceViewer.showSettings();
|
||||||
|
|
||||||
|
await expect(traceViewer.displayCanvasContentSetting).toBeChecked({ checked: false });
|
||||||
|
await traceViewer.displayCanvasContentSetting.click();
|
||||||
|
await expect(traceViewer.displayCanvasContentSetting).toBeChecked({ checked: true });
|
||||||
|
|
||||||
|
// Deselect canvas
|
||||||
|
await traceViewer.selectAction('goto', 1);
|
||||||
|
|
||||||
|
snapshotRequestPromise = traceViewer.page.waitForRequest(request => request.url().includes('/snapshot/'));
|
||||||
|
|
||||||
|
// Select canvas again
|
||||||
|
await traceViewer.selectAction('goto', 0);
|
||||||
|
|
||||||
|
snapshotRequest = await snapshotRequestPromise;
|
||||||
|
|
||||||
|
expect(snapshotRequest.url()).toContain('shouldPopulateCanvasFromScreenshot');
|
||||||
|
});
|
||||||
|
|
Loading…
Reference in New Issue