Skip to content

Commit

Permalink
feat(trace-viewer): Add setting for display canvas content in snapsho…
Browse files Browse the repository at this point in the history
…ts (#34010)
  • Loading branch information
agg23 authored Jan 8, 2025
1 parent ff92421 commit ada68cd
Show file tree
Hide file tree
Showing 13 changed files with 234 additions and 28 deletions.
21 changes: 14 additions & 7 deletions packages/trace-viewer/src/sw/snapshotRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 = () => {
Expand Down
1 change: 1 addition & 0 deletions packages/trace-viewer/src/ui/DEPS.list
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@
../geometry.ts
../../../playwright/src/isomorphic/**
../third_party/devtools.ts
./shared/**
45 changes: 45 additions & 0 deletions packages/trace-viewer/src/ui/defaultSettingsView.tsx
Original file line number Diff line number Diff line change
@@ -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.'
},
]}
/>
);
};
12 changes: 8 additions & 4 deletions packages/trace-viewer/src/ui/recorder/recorderView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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);
Expand Down Expand Up @@ -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]} />;
Expand Down Expand Up @@ -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]);
Expand All @@ -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}
Expand Down
52 changes: 52 additions & 0 deletions packages/trace-viewer/src/ui/settingsToolbarButton.tsx
Original file line number Diff line number Diff line change
@@ -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>
</>
);
};
1 change: 1 addition & 0 deletions packages/trace-viewer/src/ui/settingsView.css
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
*/

.settings-view {
display: flex;
flex: none;
padding: 4px 0px;
row-gap: 8px;
Expand Down
10 changes: 8 additions & 2 deletions packages/trace-viewer/src/ui/shared/dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,38 +18,44 @@ 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);

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

Expand Down Expand Up @@ -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>
)
Expand Down
14 changes: 10 additions & 4 deletions packages/trace-viewer/src/ui/snapshotTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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>
Expand Down Expand Up @@ -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);
Expand All @@ -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();

Expand Down
7 changes: 2 additions & 5 deletions packages/trace-viewer/src/ui/uiModeView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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 = {
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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>
}
/>
Expand Down
5 changes: 2 additions & 3 deletions packages/trace-viewer/src/ui/workbenchLoader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<{
}> = () => {
Expand Down Expand Up @@ -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>
Expand Down
7 changes: 4 additions & 3 deletions packages/web/src/components/toolbarButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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}
Expand All @@ -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,
Expand Down
Loading

0 comments on commit ada68cd

Please sign in to comment.