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 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.
|
||||
// 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);
|
||||
|
||||
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 (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);
|
||||
|
||||
if (partiallyUncaptured)
|
||||
canvas.title = `Playwright couldn't capture full canvas contents because it's located partially outside the viewport.`;
|
||||
else
|
||||
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))));
|
||||
|
||||
if (partiallyUncaptured)
|
||||
canvas.title = `Playwright couldn't capture full canvas contents because it's located partially outside the viewport.`;
|
||||
else
|
||||
canvas.title = `Canvas contents are displayed on a best-effort basis based on viewport screenshots taken during test execution.`;
|
||||
}
|
||||
};
|
||||
img.onerror = () => {
|
||||
|
|
|
@ -7,3 +7,4 @@
|
|||
../geometry.ts
|
||||
../../../playwright/src/isomorphic/**
|
||||
../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 { Toolbar } from '@web/components/toolbar';
|
||||
import { ToolbarButton, ToolbarSeparator } from '@web/components/toolbarButton';
|
||||
import { toggleTheme } from '@web/theme';
|
||||
import { copy, useSetting } from '@web/uiUtils';
|
||||
import * as React from 'react';
|
||||
import { ConsoleTab, useConsoleTabModel } from '../consoleTab';
|
||||
|
@ -37,6 +36,7 @@ import './recorderView.css';
|
|||
import { ActionListView } from './actionListView';
|
||||
import { BackendContext, BackendProvider } from './backendContext';
|
||||
import type { Language } from '@isomorphic/locatorGenerators';
|
||||
import { SettingsToolbarButton } from '../settingsToolbarButton';
|
||||
|
||||
export const RecorderView: React.FunctionComponent = () => {
|
||||
const searchParams = new URLSearchParams(window.location.search);
|
||||
|
@ -148,7 +148,7 @@ export const Workbench: React.FunctionComponent = () => {
|
|||
<SourceChooser fileId={fileId} sources={backend?.sources || []} setFileId={fileId => {
|
||||
setFileId(fileId);
|
||||
}} />
|
||||
<ToolbarButton icon='color-mode' title='Toggle color mode' toggled={false} onClick={() => toggleTheme()}></ToolbarButton>
|
||||
<SettingsToolbarButton />
|
||||
</Toolbar>;
|
||||
|
||||
const sidebarTabbedPane = <TabbedPane tabs={[actionsTab]} />;
|
||||
|
@ -271,6 +271,10 @@ const TraceView: React.FunctionComponent<{
|
|||
setHighlightedLocator,
|
||||
}) => {
|
||||
const model = React.useContext(ModelContext);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const [shouldPopulateCanvasFromScreenshot, _] = useSetting('shouldPopulateCanvasFromScreenshot', false);
|
||||
|
||||
const action = React.useMemo(() => {
|
||||
return model?.actions.find(a => a.callId === callId);
|
||||
}, [model, callId]);
|
||||
|
@ -280,8 +284,8 @@ const TraceView: React.FunctionComponent<{
|
|||
return snapshot.action || snapshot.after || snapshot.before;
|
||||
}, [action]);
|
||||
const snapshotUrls = React.useMemo(() => {
|
||||
return snapshot ? extendSnapshot(snapshot) : undefined;
|
||||
}, [snapshot]);
|
||||
return snapshot ? extendSnapshot(snapshot, shouldPopulateCanvasFromScreenshot) : undefined;
|
||||
}, [snapshot, shouldPopulateCanvasFromScreenshot]);
|
||||
|
||||
return <SnapshotView
|
||||
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 {
|
||||
display: flex;
|
||||
flex: none;
|
||||
padding: 4px 0px;
|
||||
row-gap: 8px;
|
||||
|
|
|
@ -18,20 +18,24 @@ import * as React from 'react';
|
|||
|
||||
export interface DialogProps {
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
open: boolean;
|
||||
width: number;
|
||||
verticalOffset?: number;
|
||||
requestClose?: () => void;
|
||||
anchor?: React.RefObject<HTMLElement>;
|
||||
dataTestId?: string;
|
||||
}
|
||||
|
||||
export const Dialog: React.FC<React.PropsWithChildren<DialogProps>> = ({
|
||||
className,
|
||||
style: externalStyle,
|
||||
open,
|
||||
width,
|
||||
verticalOffset,
|
||||
requestClose,
|
||||
anchor,
|
||||
dataTestId,
|
||||
children,
|
||||
}) => {
|
||||
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
|
||||
const [_, setRecalculateDimensionsCount] = React.useState(0);
|
||||
|
||||
let style: React.CSSProperties | undefined = undefined;
|
||||
let style: React.CSSProperties | undefined = externalStyle;
|
||||
|
||||
if (anchor?.current) {
|
||||
const bounds = anchor.current.getBoundingClientRect();
|
||||
|
||||
style = {
|
||||
position: 'fixed',
|
||||
margin: 0,
|
||||
top: bounds.bottom + (verticalOffset ?? 0),
|
||||
left: buildTopLeftCoord(bounds, width),
|
||||
width,
|
||||
zIndex: 1,
|
||||
...externalStyle
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -92,7 +98,7 @@ export const Dialog: React.FC<React.PropsWithChildren<DialogProps>> = ({
|
|||
|
||||
return (
|
||||
open && (
|
||||
<dialog ref={dialogRef} style={style} className={className} open>
|
||||
<dialog ref={dialogRef} style={style} className={className} data-testid={dataTestId} open>
|
||||
{children}
|
||||
</dialog>
|
||||
)
|
||||
|
|
|
@ -20,7 +20,7 @@ import type { ActionTraceEvent } from '@trace/trace';
|
|||
import { context, type MultiTraceModel, prevInList } from './modelUtil';
|
||||
import { Toolbar } from '@web/components/toolbar';
|
||||
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 { Recorder } from '@injected/recorder/recorder';
|
||||
import ConsoleAPI from '@injected/consoleApi';
|
||||
|
@ -43,13 +43,16 @@ export const SnapshotTabsView: React.FunctionComponent<{
|
|||
}> = ({ action, sdkLanguage, testIdAttributeName, isInspecting, setIsInspecting, highlightedLocator, setHighlightedLocator }) => {
|
||||
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(() => {
|
||||
return collectSnapshots(action);
|
||||
}, [action]);
|
||||
const snapshotUrls = React.useMemo(() => {
|
||||
const snapshot = snapshots[snapshotTab];
|
||||
return snapshot ? extendSnapshot(snapshot) : undefined;
|
||||
}, [snapshots, snapshotTab]);
|
||||
return snapshot ? extendSnapshot(snapshot, shouldPopulateCanvasFromScreenshot) : undefined;
|
||||
}, [snapshots, snapshotTab, shouldPopulateCanvasFromScreenshot]);
|
||||
|
||||
return <div className='snapshot-tab vbox'>
|
||||
<Toolbar>
|
||||
|
@ -327,7 +330,7 @@ export function collectSnapshots(action: ActionTraceEvent | undefined): Snapshot
|
|||
const isUnderTest = new URLSearchParams(window.location.search).has('isUnderTest');
|
||||
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();
|
||||
params.set('trace', context(snapshot.action).traceUrl);
|
||||
params.set('name', snapshot.snapshotName);
|
||||
|
@ -339,6 +342,9 @@ export function extendSnapshot(snapshot: Snapshot): SnapshotUrls {
|
|||
if (snapshot.hasInputTarget)
|
||||
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 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 type { XtermDataSource } from '@web/components/xtermWrapper';
|
||||
import { XtermWrapper } from '@web/components/xtermWrapper';
|
||||
import { useDarkModeSetting } from '@web/theme';
|
||||
import { clsx, settings, useSetting } from '@web/uiUtils';
|
||||
import { statusEx, TestTree } from '@testIsomorphic/testTree';
|
||||
import type { TreeItem } from '@testIsomorphic/testTree';
|
||||
|
@ -37,6 +36,7 @@ import { FiltersView } from './uiModeFiltersView';
|
|||
import { TestListView } from './uiModeTestListView';
|
||||
import { TraceView } from './uiModeTraceView';
|
||||
import { SettingsView } from './settingsView';
|
||||
import { DefaultSettingsView } from './defaultSettingsView';
|
||||
|
||||
let xtermSize = { cols: 80, rows: 24 };
|
||||
const xtermDataSource: XtermDataSource = {
|
||||
|
@ -104,7 +104,6 @@ export const UIModeView: React.FC<{}> = ({
|
|||
const [singleWorker, setSingleWorker] = React.useState(false);
|
||||
const [showBrowser, setShowBrowser] = React.useState(false);
|
||||
const [updateSnapshots, setUpdateSnapshots] = React.useState(false);
|
||||
const [darkMode, setDarkMode] = useDarkModeSetting();
|
||||
|
||||
const inputRef = React.useRef<HTMLInputElement>(null);
|
||||
|
||||
|
@ -521,9 +520,7 @@ export const UIModeView: React.FC<{}> = ({
|
|||
/>
|
||||
<div className='section-title'>Settings</div>
|
||||
</Toolbar>
|
||||
{settingsVisible && <SettingsView settings={[
|
||||
{ value: darkMode, set: setDarkMode, name: 'Dark mode' },
|
||||
]} />}
|
||||
{settingsVisible && <DefaultSettingsView />}
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
|
|
|
@ -14,14 +14,13 @@
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { ToolbarButton } from '@web/components/toolbarButton';
|
||||
import * as React from 'react';
|
||||
import type { ContextEntry } from '../types/entries';
|
||||
import { MultiTraceModel } from './modelUtil';
|
||||
import './workbenchLoader.css';
|
||||
import { toggleTheme } from '@web/theme';
|
||||
import { Workbench } from './workbench';
|
||||
import { TestServerConnection, WebSocketTestServerTransport } from '@testIsomorphic/testServerConnection';
|
||||
import { SettingsToolbarButton } from './settingsToolbarButton';
|
||||
|
||||
export const WorkbenchLoader: React.FunctionComponent<{
|
||||
}> = () => {
|
||||
|
@ -161,7 +160,7 @@ export const WorkbenchLoader: React.FunctionComponent<{
|
|||
<div className='product'>Playwright</div>
|
||||
{model.title && <div className='title'>{model.title}</div>}
|
||||
<div className='spacer'></div>
|
||||
<ToolbarButton icon='color-mode' title='Toggle color mode' toggled={false} onClick={() => toggleTheme()}></ToolbarButton>
|
||||
<SettingsToolbarButton />
|
||||
</div>
|
||||
<div className='progress'>
|
||||
<div className='inner-progress' style={{ width: progress.total ? (100 * progress.done / progress.total) + '%' : 0 }}></div>
|
||||
|
|
|
@ -31,7 +31,7 @@ export interface ToolbarButtonProps {
|
|||
ariaLabel?: string,
|
||||
}
|
||||
|
||||
export const ToolbarButton: React.FC<React.PropsWithChildren<ToolbarButtonProps>> = ({
|
||||
export const ToolbarButton = React.forwardRef<HTMLButtonElement, React.PropsWithChildren<ToolbarButtonProps>>(function ToolbarButton({
|
||||
children,
|
||||
title = '',
|
||||
icon,
|
||||
|
@ -42,8 +42,9 @@ export const ToolbarButton: React.FC<React.PropsWithChildren<ToolbarButtonProps>
|
|||
testId,
|
||||
className,
|
||||
ariaLabel,
|
||||
}) => {
|
||||
}, ref) {
|
||||
return <button
|
||||
ref={ref}
|
||||
className={clsx(className, 'toolbar-button', icon, toggled && 'toggled')}
|
||||
onMouseDown={preventDefault}
|
||||
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>}
|
||||
{children}
|
||||
</button>;
|
||||
};
|
||||
});
|
||||
|
||||
export const ToolbarSeparator: React.FC<{ style?: React.CSSProperties }> = ({
|
||||
style,
|
||||
|
|
|
@ -49,6 +49,10 @@ class TraceViewerPage {
|
|||
snapshotContainer: Locator;
|
||||
sourceCodeTab: Locator;
|
||||
|
||||
settingsDialog: Locator;
|
||||
darkModeSetting: Locator;
|
||||
displayCanvasContentSetting: Locator;
|
||||
|
||||
constructor(public page: Page) {
|
||||
this.actionTitles = page.locator('.action-title');
|
||||
this.actionsTree = page.getByTestId('actions-tree');
|
||||
|
@ -63,6 +67,10 @@ class TraceViewerPage {
|
|||
this.snapshotContainer = page.locator('.snapshot-container iframe.snapshot-visible[name=snapshot]');
|
||||
this.metadataTab = page.getByTestId('metadata-view');
|
||||
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) {
|
||||
|
@ -115,6 +123,10 @@ class TraceViewerPage {
|
|||
await this.page.click('text="Metadata"');
|
||||
}
|
||||
|
||||
async showSettings() {
|
||||
await this.page.locator('.settings-gear').click();
|
||||
}
|
||||
|
||||
@step
|
||||
async snapshotFrame(actionName: string, ordinal: number = 0, hasSubframe: boolean = false): Promise<FrameLocator> {
|
||||
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 });
|
||||
});
|
||||
|
||||
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 }) => {
|
||||
const traceViewer = await runAndTrace(async () => {
|
||||
await page.goto(server.PREFIX + '/screenshots/canvas.html#canvas-on-edge');
|
||||
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:') });
|
||||
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);
|
||||
});
|
||||
|
||||
// Enable canvas display
|
||||
await traceViewer.showSettings();
|
||||
await traceViewer.displayCanvasContentSetting.click();
|
||||
|
||||
const msg = await traceViewer.page.waitForEvent('console', { predicate: msg => msg.text().startsWith('canvas drawn:') });
|
||||
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');
|
||||
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