feat(trace-viewer): Add setting for display canvas content in snapshots (#34010)

This commit is contained in:
Adam Gastineau 2025-01-08 05:08:00 -08:00 committed by GitHub
parent ff9242104b
commit ada68cd6f0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 234 additions and 28 deletions

View File

@ -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 = () => {

View File

@ -7,3 +7,4 @@
../geometry.ts ../geometry.ts
../../../playwright/src/isomorphic/** ../../../playwright/src/isomorphic/**
../third_party/devtools.ts ../third_party/devtools.ts
./shared/**

View File

@ -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.'
},
]}
/>
);
};

View File

@ -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}

View File

@ -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>
</>
);
};

View File

@ -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;

View File

@ -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>
) )

View File

@ -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();

View File

@ -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>
} }
/> />

View File

@ -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>

View File

@ -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,

View File

@ -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);

View File

@ -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');
});