Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/isomorphic/trace/traceModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@ export class TraceModel {
readonly errorDescriptors: ErrorDescription[];
readonly hasSource: boolean;
readonly hasStepData: boolean;
readonly hasDomSnapshots: boolean;
readonly hasAriaSnapshots: boolean;
readonly sdkLanguage: Language | undefined;
readonly testIdAttributeName: string | undefined;
readonly sources: Map<string, SourceModel>;
Expand Down Expand Up @@ -131,6 +133,8 @@ export class TraceModel {
this._ariaSnapshots.set(`${event.callId}/${event.phase}`, event);
this.videos.push(...(context.videos || []));
}
this.hasDomSnapshots = this.actions.some(action => !!action.beforeSnapshot || !!action.afterSnapshot || !!action.inputSnapshot);
this.hasAriaSnapshots = !!this._screenshots.size || !!this._ariaSnapshots.size;
this.attachments = this.actions.flatMap(action => action.attachments?.map(attachment => ({ ...attachment, callId: action.callId, traceUri })) ?? []);
this.visibleAttachments = this.attachments.filter(attachment => !attachment.name.startsWith('_'));

Expand Down
8 changes: 8 additions & 0 deletions packages/trace-viewer/src/ui/ariaModeView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,14 @@ export type AriaModeTargets = {
after?: AriaModeTarget;
};

export function canToggleAriaMode(model: TraceModel | undefined): boolean {
return !!model?.hasDomSnapshots && !!model?.hasAriaSnapshots;
}

export function shouldDisplayAriaMode(model: TraceModel | undefined, setting: boolean): boolean {
return canToggleAriaMode(model) ? setting : !!model?.hasAriaSnapshots;
}

// Mirrors the fallback logic in collectSnapshots(), but for screenshot / aria-snapshot events.
export function collectAriaModeTargets(model: TraceModel, action: ActionTraceEvent | undefined): AriaModeTargets {
if (!action)
Expand Down
16 changes: 12 additions & 4 deletions packages/trace-viewer/src/ui/defaultSettingsView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,20 +18,25 @@ import * as React from 'react';
import { type Setting, SettingsView } from './settingsView';
import { kThemeOptions, type Theme, useThemeSetting } from '@web/theme';
import { useSetting } from '@web/uiUtils';
import { canToggleAriaMode, shouldDisplayAriaMode } from './ariaModeView';

import type { TraceModel } from '@isomorphic/trace/traceModel';

/**
* A view of the collection of standard settings used between various applications
*/
export const DefaultSettingsView: React.FC<{
location: 'ui-mode' | 'trace-viewer'
}> = ({ location }) => {
location: 'ui-mode' | 'trace-viewer',
model?: TraceModel,
}> = ({ location, model }) => {
const [
shouldPopulateCanvasFromScreenshot,
setShouldPopulateCanvasFromScreenshot,
] = useSetting('shouldPopulateCanvasFromScreenshot', false);
const [displayAriaMode, setDisplayAriaMode] = useSetting('displayAriaMode', false);
const [theme, setTheme] = useThemeSetting();
const [mergeFiles, setMergeFiles] = useSetting('mergeFiles', false);
const canToggleAria = canToggleAriaMode(model);

return (
<SettingsView
Expand All @@ -58,10 +63,13 @@ export const DefaultSettingsView: React.FC<{
},
{
type: 'check',
value: displayAriaMode,
value: shouldDisplayAriaMode(model, displayAriaMode),
set: setDisplayAriaMode,
name: 'Display Aria',
title: 'Display the action screenshot and aria snapshot instead of the DOM snapshot. Requires a trace recorded with screen and aria snapshots.',
disabled: !canToggleAria,
title: canToggleAria
? 'Display the action screenshot and aria snapshot instead of the DOM snapshot.'
: 'The trace does not have both DOM and aria snapshots, so there is nothing to switch between.',
},
]}
/>
Expand Down
8 changes: 8 additions & 0 deletions packages/trace-viewer/src/ui/settingsView.css
Original file line number Diff line number Diff line change
Expand Up @@ -64,3 +64,11 @@
align-items: center;
justify-content: center;
}

.settings-view .setting-disabled {
opacity: 0.5;
}

.settings-view .setting-disabled label {
cursor: default;
}
7 changes: 5 additions & 2 deletions packages/trace-viewer/src/ui/settingsView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,14 @@
*/

import * as React from 'react';
import { clsx } from '@web/uiUtils';
import './settingsView.css';

export type Setting<Value extends string = string> = {
name: string;
title?: string;
count?: number;
disabled?: boolean;
} & ({
type: 'check',
value: boolean;
Expand All @@ -41,7 +43,7 @@ export const SettingsView = <Value extends string>(
const labelId = `setting-${setting.name.replaceAll(/\s+/g, '-')}`;

return (
<div key={setting.name} className={`setting setting-${setting.type}`} title={setting.title}>
<div key={setting.name} className={clsx('setting', `setting-${setting.type}`, setting.disabled && 'setting-disabled')} title={setting.title}>
{renderSetting(setting, labelId)}
</div>
);
Expand All @@ -59,6 +61,7 @@ const renderSetting = <Value extends string>(setting: Setting<Value>, labelId: s
type='checkbox'
id={labelId}
checked={setting.value}
disabled={setting.disabled}
onChange={() => setting.set(!setting.value)}
/>
<label htmlFor={labelId}>{setting.name}{!!setting.count && <span className='setting-counter'>{setting.count}</span>}</label>
Expand All @@ -68,7 +71,7 @@ const renderSetting = <Value extends string>(setting: Setting<Value>, labelId: s
return (
<>
<label htmlFor={labelId}>{setting.name}:{!!setting.count && <span className='setting-counter'>{setting.count}</span>}</label>
<select id={labelId} value={setting.value} onChange={e => setting.set(e.target.value as Value)}>
<select id={labelId} value={setting.value} disabled={setting.disabled} onChange={e => setting.set(e.target.value as Value)}>
{setting.options.map(option => (
<option key={option.value} value={option.value}>
{option.label}
Expand Down
5 changes: 3 additions & 2 deletions packages/trace-viewer/src/ui/snapshotTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ import { parseAriaSnapshot } from '@isomorphic/ariaSnapshot';
import yaml from 'yaml';
import { PlaybackButtons } from './playbackControl';
import type { PlaybackState } from './playbackControl';
import { AriaModeView, collectAriaModeTargets } from './ariaModeView';
import { AriaModeView, collectAriaModeTargets, shouldDisplayAriaMode } from './ariaModeView';

export type HighlightedElement = {
locator?: string,
Expand All @@ -57,7 +57,8 @@ export const SnapshotTabsView: React.FunctionComponent<{
const [snapshotTab, setSnapshotTab] = React.useState<'action'|'before'|'after'>('action');

const [shouldPopulateCanvasFromScreenshot] = useSetting('shouldPopulateCanvasFromScreenshot', false);
const [displayAriaMode] = useSetting('displayAriaMode', false);
const [displayAriaModeSetting] = useSetting('displayAriaMode', false);
const displayAriaMode = shouldDisplayAriaMode(model, displayAriaModeSetting);

const snapshots = React.useMemo(() => {
return collectSnapshots(action);
Expand Down
7 changes: 6 additions & 1 deletion packages/trace-viewer/src/ui/uiModeTraceView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ export const TraceView: React.FC<{
onOpenExternally?: (location: SourceLocation) => void,
revealSource?: boolean,
pathSeparator: string,
}> = ({ item, rootDir, onOpenExternally, revealSource, pathSeparator }) => {
onModelChange?: (model: TraceModel | undefined) => void,
}> = ({ item, rootDir, onOpenExternally, revealSource, pathSeparator, onModelChange }) => {
const [model, setModel] = React.useState<{ model: TraceModel, isLive: boolean } | undefined>(undefined);
const [counter, setCounter] = React.useState(0);
const pollTimer = React.useRef<NodeJS.Timeout | null>(null);
Expand Down Expand Up @@ -88,6 +89,10 @@ export const TraceView: React.FC<{
};
}, [outputDir, item, setModel, counter, setCounter, pathSeparator]);

React.useEffect(() => {
onModelChange?.(model?.model);
}, [model, onModelChange]);

return <Workbench
model={model?.model}
key='workbench'
Expand Down
6 changes: 4 additions & 2 deletions packages/trace-viewer/src/ui/uiModeView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import { TeleSuiteUpdater, type TeleSuiteUpdaterProgress, type TeleSuiteUpdaterT
import type { TeleTestCase } from '@testIsomorphic/teleReceiver';
import type * as reporterTypes from 'playwright/types/testReporter';
import { SplitView } from '@web/components/splitView';
import type { SourceLocation } from '@isomorphic/trace/traceModel';
import type { SourceLocation, TraceModel } from '@isomorphic/trace/traceModel';
import './uiModeView.css';
import { ToolbarButton } from '@web/components/toolbarButton';
import { Toolbar } from '@web/components/toolbar';
Expand Down Expand Up @@ -125,6 +125,7 @@ export const UIModeView: React.FC<{}> = ({
const [settingsVisible, setSettingsVisible] = React.useState(false);
const [testingOptionsVisible, setTestingOptionsVisible] = React.useState(false);
const [revealSource, setRevealSource] = React.useState(false);
const [traceModel, setTraceModel] = React.useState<TraceModel | undefined>();
const onRevealSource = React.useCallback(() => setRevealSource(true), [setRevealSource]);

const [singleWorker, setSingleWorker] = useSetting<boolean>('single-worker', false);
Expand Down Expand Up @@ -479,6 +480,7 @@ export const UIModeView: React.FC<{}> = ({
<TraceView
pathSeparator={queryParams.pathSeparator}
item={selectedItem}
onModelChange={setTraceModel}
rootDir={testModel?.config?.rootDir}
revealSource={revealSource}
onOpenExternally={location => testServerConnection?.openNoReply({ location: { file: location.file, line: location.line, column: location.column } })}
Expand Down Expand Up @@ -565,7 +567,7 @@ export const UIModeView: React.FC<{}> = ({
/>
<div className='section-title'>Settings</div>
</Toolbar>
{settingsVisible && <DefaultSettingsView location='ui-mode' />}
{settingsVisible && <DefaultSettingsView location='ui-mode' model={traceModel} />}
</div>
}
/>
Expand Down
2 changes: 1 addition & 1 deletion packages/trace-viewer/src/ui/workbenchLoader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ export const WorkbenchLoader: React.FunctionComponent<{
{model.title && <div className='title'>{model.title}</div>}
<div className='spacer'></div>
<DialogToolbarButton icon='settings-gear' title='Settings' dialogDataTestId='settings-toolbar-dialog'>
<DefaultSettingsView location='trace-viewer' />
<DefaultSettingsView location='trace-viewer' model={model} />
</DialogToolbarButton>
</div>
<Workbench model={model} inert={showFileUploadDropArea} />
Expand Down
28 changes: 28 additions & 0 deletions tests/library/trace-viewer.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2236,6 +2236,34 @@ test('should display aria mode', async ({ runAndTrace, page }) => {
await expect(traceViewer.snapshotContainer).toBeVisible();
});

test('should disable aria setting when there are no aria snapshots', async ({ runAndTrace, page }) => {
const traceViewer = await runAndTrace(async () => {
await page.setContent('<!DOCTYPE html><button>Click me</button>');
await page.locator('button').click();
}, { snapshots: { dom: true } });

await traceViewer.selectAction('Click');
await expect(traceViewer.snapshotContainer).toBeVisible();

await traceViewer.showSettings();
await expect(traceViewer.displayAriaSetting).toBeDisabled();
await expect(traceViewer.displayAriaSetting).toBeChecked({ checked: false });
});

test('should force aria mode when there are no dom snapshots', async ({ runAndTrace, page }) => {
const traceViewer = await runAndTrace(async () => {
await page.setContent('<!DOCTYPE html><button>Click me</button>');
await page.locator('button').click();
}, { snapshots: { aria: true, screen: true } });

await traceViewer.selectAction('Click');
await expect(traceViewer.page.locator('.aria-mode-view')).toContainText('button "Click me"');

await traceViewer.showSettings();
await expect(traceViewer.displayAriaSetting).toBeDisabled();
await expect(traceViewer.displayAriaSetting).toBeChecked({ checked: true });
});

test('should render blob trace received from message', async ({ showTraceViewer }) => {
const traceViewer = await showTraceViewer(undefined, { host: 'localhost' });

Expand Down
Loading