Skip to content

Commit

Permalink
chore: allow matching aria snapshot in trace viewer (#34302)
Browse files Browse the repository at this point in the history
  • Loading branch information
pavelfeldman authored Jan 11, 2025
1 parent 0c8a6b8 commit 6179b5b
Show file tree
Hide file tree
Showing 9 changed files with 159 additions and 74 deletions.
5 changes: 4 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 7 additions & 5 deletions packages/playwright-core/src/server/injected/ariaSnapshot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,7 @@ function matchesNode(node: AriaNode | string, template: AriaTemplateNode, depth:
if (typeof node === 'string' && template.kind === 'text')
return matchesTextNode(node, template);

if (typeof node === 'object' && template.kind === 'role') {
if (node !== null && typeof node === 'object' && template.kind === 'role') {
if (template.role !== 'fragment' && template.role !== node.role)
return false;
if (template.checked !== undefined && template.checked !== node.checked)
Expand Down Expand Up @@ -285,20 +285,22 @@ function containsList(children: (AriaNode | string)[], template: AriaTemplateNod

function matchesNodeDeep(root: AriaNode, template: AriaTemplateNode, collectAll: boolean): AriaNode[] {
const results: AriaNode[] = [];
const visit = (node: AriaNode | string): boolean => {
const visit = (node: AriaNode | string, parent: AriaNode | null): boolean => {
if (matchesNode(node, template, 0)) {
results.push(node as AriaNode);
const result = typeof node === 'string' ? parent : node;
if (result)
results.push(result);
return !collectAll;
}
if (typeof node === 'string')
return false;
for (const child of node.children || []) {
if (visit(child))
if (visit(child, node))
return true;
}
return false;
};
visit(root);
visit(root, null);
return results;
}

Expand Down
13 changes: 11 additions & 2 deletions packages/playwright-core/src/utils/isomorphic/ariaSnapshot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,15 @@ export function parseAriaSnapshot(yaml: YamlLibrary, text: string, options: yaml
// - role "name": "text"
const valueIsScalar = value instanceof yaml.Scalar;
if (valueIsScalar) {
const type = typeof value.value;
if (type !== 'string' && type !== 'number' && type !== 'boolean') {
errors.push({
message: 'Node value should be a string or a sequence',
range: convertRange(((entry.value as any).range || map.range)),
});
continue;
}

container.children.push({
...childNode,
children: [{
Expand Down Expand Up @@ -193,7 +202,7 @@ export function parseAriaSnapshot(yaml: YamlLibrary, text: string, options: yaml
if (!(yamlDoc.contents instanceof yaml.YAMLSeq)) {
errors.push({
message: 'Aria snapshot must be a YAML sequence, elements starting with " -"',
range: convertRange(yamlDoc.contents!.range),
range: yamlDoc.contents ? convertRange(yamlDoc.contents!.range) : [{ line: 0, col: 0 }, { line: 0, col: 0 }],
});
}
if (errors.length)
Expand All @@ -214,7 +223,7 @@ function normalizeWhitespace(text: string) {
}

export function valueOrRegex(value: string): string | AriaRegex {
return value.startsWith('/') && value.endsWith('/') ? { pattern: value.slice(1, -1) } : normalizeWhitespace(value);
return value.startsWith('/') && value.endsWith('/') && value.length > 1 ? { pattern: value.slice(1, -1) } : normalizeWhitespace(value);
}

export class KeyParser {
Expand Down
5 changes: 4 additions & 1 deletion packages/trace-viewer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,8 @@
"name": "trace-viewer",
"private": true,
"version": "0.0.0",
"type": "module"
"type": "module",
"dependencies": {
"yaml": "^2.6.0"
}
}
44 changes: 36 additions & 8 deletions packages/trace-viewer/src/ui/inspectorTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,30 +15,58 @@
*/

import { CodeMirrorWrapper } from '@web/components/codeMirrorWrapper';
import type { Language } from '@web/components/codeMirrorWrapper';
import type { Language, SourceHighlight } from '@web/components/codeMirrorWrapper';
import { ToolbarButton } from '@web/components/toolbarButton';
import { copy } from '@web/uiUtils';
import * as React from 'react';
import type { HighlightedElement } from './snapshotTab';
import './sourceTab.css';
import { parseAriaSnapshot } from '@isomorphic/ariaSnapshot';
import yaml from 'yaml';

export const InspectorTab: React.FunctionComponent<{
sdkLanguage: Language,
setIsInspecting: (isInspecting: boolean) => void,
highlightedLocator: string,
setHighlightedLocator: (locator: string) => void,
}> = ({ sdkLanguage, setIsInspecting, highlightedLocator, setHighlightedLocator }) => {
highlightedElement: HighlightedElement,
setHighlightedElement: (element: HighlightedElement) => void,
}> = ({ sdkLanguage, setIsInspecting, highlightedElement, setHighlightedElement }) => {
const [ariaSnapshotErrors, setAriaSnapshotErrors] = React.useState<SourceHighlight[]>();
const onAriaEditorChange = React.useCallback((ariaSnapshot: string) => {
const { errors } = parseAriaSnapshot(yaml, ariaSnapshot, { prettyErrors: false });
const highlights = errors.map(error => {
const highlight: SourceHighlight = {
message: error.message,
line: error.range[1].line,
column: error.range[1].col,
type: 'subtle-error',
};
return highlight;
});
setAriaSnapshotErrors(highlights);
setHighlightedElement({ ...highlightedElement, ariaSnapshot, lastEdited: 'ariaSnapshot' });
setIsInspecting(false);
}, [highlightedElement, setHighlightedElement, setIsInspecting]);

return <div className='vbox' style={{ backgroundColor: 'var(--vscode-sideBar-background)' }}>
<div style={{ margin: '10px 0px 10px 10px', color: 'var(--vscode-editorCodeLens-foreground)', flex: 'none' }}>Locator</div>
<div style={{ margin: '0 10px 10px', flex: 'auto' }}>
<CodeMirrorWrapper text={highlightedLocator} language={sdkLanguage} focusOnChange={true} isFocused={true} wrapLines={true} onChange={text => {
<CodeMirrorWrapper text={highlightedElement.locator || ''} language={sdkLanguage} isFocused={true} wrapLines={true} onChange={text => {
// Updating text needs to go first - react can squeeze a render between the state updates.
setHighlightedLocator(text);
setHighlightedElement({ ...highlightedElement, locator: text, lastEdited: 'locator' });
setIsInspecting(false);
}}></CodeMirrorWrapper>
}} />
</div>
<div style={{ margin: '10px 0px 10px 10px', color: 'var(--vscode-editorCodeLens-foreground)', flex: 'none' }}>Aria</div>
<div style={{ margin: '0 10px 10px', flex: 'auto' }}>
<CodeMirrorWrapper
text={highlightedElement.ariaSnapshot || ''}
wrapLines={false}
highlight={ariaSnapshotErrors}
onChange={onAriaEditorChange} />
</div>
<div style={{ position: 'absolute', right: 5, top: 5 }}>
<ToolbarButton icon='files' title='Copy locator' onClick={() => {
copy(highlightedLocator);
copy(highlightedElement.locator || '');
}}></ToolbarButton>
</div>
</div>;
Expand Down
49 changes: 25 additions & 24 deletions packages/trace-viewer/src/ui/recorder/recorderView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import { ActionListView } from './actionListView';
import { BackendContext, BackendProvider } from './backendContext';
import type { Language } from '@isomorphic/locatorGenerators';
import { SettingsToolbarButton } from '../settingsToolbarButton';
import type { HighlightedElement } from '../snapshotTab';

export const RecorderView: React.FunctionComponent = () => {
const searchParams = new URLSearchParams(window.location.search);
Expand All @@ -56,8 +57,8 @@ export const Workbench: React.FunctionComponent = () => {
const [fileId, setFileId] = React.useState<string | undefined>();
const [selectedStartTime, setSelectedStartTime] = React.useState<number | undefined>(undefined);
const [isInspecting, setIsInspecting] = React.useState(false);
const [highlightedLocatorInProperties, setHighlightedLocatorInProperties] = React.useState<string>('');
const [highlightedLocatorInTrace, setHighlightedLocatorInTrace] = React.useState<string>('');
const [highlightedElementInProperties, setHighlightedElementInProperties] = React.useState<HighlightedElement>({ lastEdited: 'none' });
const [highlightedElementInTrace, setHighlightedElementInTrace] = React.useState<HighlightedElement>({ lastEdited: 'none' });
const [traceCallId, setTraceCallId] = React.useState<string | undefined>();

const setSelectedAction = React.useCallback((action: actionTypes.ActionInContext | undefined) => {
Expand Down Expand Up @@ -103,15 +104,15 @@ export const Workbench: React.FunctionComponent = () => {
return { boundaries };
}, [model]);

const locatorPickedInTrace = React.useCallback((locator: string) => {
setHighlightedLocatorInProperties(locator);
setHighlightedLocatorInTrace('');
const elementPickedInTrace = React.useCallback((element: HighlightedElement) => {
setHighlightedElementInProperties(element);
setHighlightedElementInTrace({ lastEdited: 'none' });
setIsInspecting(false);
}, []);

const locatorTypedInProperties = React.useCallback((locator: string) => {
setHighlightedLocatorInTrace(locator);
setHighlightedLocatorInProperties(locator);
const elementTypedInProperties = React.useCallback((element: HighlightedElement) => {
setHighlightedElementInTrace(element);
setHighlightedElementInProperties(element);
}, []);

const actionList = <ActionListView
Expand Down Expand Up @@ -157,14 +158,14 @@ export const Workbench: React.FunctionComponent = () => {
callId={traceCallId}
isInspecting={isInspecting}
setIsInspecting={setIsInspecting}
highlightedLocator={highlightedLocatorInTrace}
setHighlightedLocator={locatorPickedInTrace} />;
highlightedElement={highlightedElementInTrace}
setHighlightedElement={elementPickedInTrace} />;
const propertiesView = <PropertiesView
sdkLanguage={sdkLanguage}
boundaries={boundaries}
setIsInspecting={setIsInspecting}
highlightedLocator={highlightedLocatorInProperties}
setHighlightedLocator={locatorTypedInProperties}
highlightedElement={highlightedElementInProperties}
setHighlightedElement={elementTypedInProperties}
sourceLocation={sourceLocation} />;

return <div className='vbox workbench'>
Expand Down Expand Up @@ -192,15 +193,15 @@ const PropertiesView: React.FunctionComponent<{
sdkLanguage: Language,
boundaries: Boundaries,
setIsInspecting: (value: boolean) => void,
highlightedLocator: string,
setHighlightedLocator: (locator: string) => void,
highlightedElement: HighlightedElement,
setHighlightedElement: (element: HighlightedElement) => void,
sourceLocation: modelUtil.SourceLocation | undefined,
}> = ({
sdkLanguage,
boundaries,
setIsInspecting,
highlightedLocator,
setHighlightedLocator,
highlightedElement,
setHighlightedElement,
sourceLocation,
}) => {
const model = React.useContext(ModelContext);
Expand All @@ -215,8 +216,8 @@ const PropertiesView: React.FunctionComponent<{
render: () => <InspectorTab
sdkLanguage={sdkLanguage}
setIsInspecting={setIsInspecting}
highlightedLocator={highlightedLocator}
setHighlightedLocator={setHighlightedLocator} />,
highlightedElement={highlightedElement}
setHighlightedElement={setHighlightedElement} />,
};

const sourceTab: TabbedPaneTabModel = {
Expand Down Expand Up @@ -260,15 +261,15 @@ const TraceView: React.FunctionComponent<{
callId: string | undefined,
isInspecting: boolean;
setIsInspecting: (value: boolean) => void;
highlightedLocator: string;
setHighlightedLocator: (locator: string) => void;
highlightedElement: HighlightedElement;
setHighlightedElement: (element: HighlightedElement) => void;
}> = ({
sdkLanguage,
callId,
isInspecting,
setIsInspecting,
highlightedLocator,
setHighlightedLocator,
highlightedElement,
setHighlightedElement,
}) => {
const model = React.useContext(ModelContext);

Expand All @@ -292,7 +293,7 @@ const TraceView: React.FunctionComponent<{
testIdAttributeName='data-testid'
isInspecting={isInspecting}
setIsInspecting={setIsInspecting}
highlightedLocator={highlightedLocator}
setHighlightedLocator={setHighlightedLocator}
highlightedElement={highlightedElement}
setHighlightedElement={setHighlightedElement}
snapshotUrls={snapshotUrls} />;
};
Loading

0 comments on commit 6179b5b

Please sign in to comment.