chore: allow matching aria snapshot in trace viewer (#34302)

This commit is contained in:
Pavel Feldman 2025-01-11 10:14:21 -08:00 committed by GitHub
parent 0c8a6b80fb
commit 6179b5b1d7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 159 additions and 74 deletions

5
package-lock.json generated
View File

@ -7964,7 +7964,10 @@
}
},
"packages/trace-viewer": {
"version": "0.0.0"
"version": "0.0.0",
"dependencies": {
"yaml": "^2.6.0"
}
},
"packages/web": {
"version": "0.0.0",

View File

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

View File

@ -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: [{
@ -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)
@ -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 {

View File

@ -2,5 +2,8 @@
"name": "trace-viewer",
"private": true,
"version": "0.0.0",
"type": "module"
"type": "module",
"dependencies": {
"yaml": "^2.6.0"
}
}

View File

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

View File

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

View File

@ -30,6 +30,14 @@ import { locatorOrSelectorAsSelector } from '@isomorphic/locatorParser';
import { TabbedPaneTab } from '@web/components/tabbedPane';
import { BrowserFrame } from './browserFrame';
import type { ElementInfo } from '@recorder/recorderTypes';
import { parseAriaSnapshot } from '@isomorphic/ariaSnapshot';
import yaml from 'yaml';
export type HighlightedElement = {
locator?: string,
ariaSnapshot?: string
lastEdited: 'locator' | 'ariaSnapshot' | 'none';
};
export const SnapshotTabsView: React.FunctionComponent<{
action: ActionTraceEvent | undefined,
@ -38,9 +46,9 @@ export const SnapshotTabsView: React.FunctionComponent<{
testIdAttributeName: string,
isInspecting: boolean,
setIsInspecting: (isInspecting: boolean) => void,
highlightedLocator: string,
setHighlightedLocator: (locator: string) => void,
}> = ({ action, sdkLanguage, testIdAttributeName, isInspecting, setIsInspecting, highlightedLocator, setHighlightedLocator }) => {
highlightedElement: HighlightedElement,
setHighlightedElement: (element: HighlightedElement) => void,
}> = ({ action, sdkLanguage, testIdAttributeName, isInspecting, setIsInspecting, highlightedElement, setHighlightedElement }) => {
const [snapshotTab, setSnapshotTab] = React.useState<'action'|'before'|'after'>('action');
// eslint-disable-next-line @typescript-eslint/no-unused-vars
@ -81,8 +89,8 @@ export const SnapshotTabsView: React.FunctionComponent<{
testIdAttributeName={testIdAttributeName}
isInspecting={isInspecting}
setIsInspecting={setIsInspecting}
highlightedLocator={highlightedLocator}
setHighlightedLocator={setHighlightedLocator}
highlightedElement={highlightedElement}
setHighlightedElement={setHighlightedElement}
/>
</div>;
};
@ -93,9 +101,9 @@ export const SnapshotView: React.FunctionComponent<{
testIdAttributeName: string,
isInspecting: boolean,
setIsInspecting: (isInspecting: boolean) => void,
highlightedLocator: string,
setHighlightedLocator: (locator: string) => void,
}> = ({ snapshotUrls, sdkLanguage, testIdAttributeName, isInspecting, setIsInspecting, highlightedLocator, setHighlightedLocator }) => {
highlightedElement: HighlightedElement,
setHighlightedElement: (element: HighlightedElement) => void,
}> = ({ snapshotUrls, sdkLanguage, testIdAttributeName, isInspecting, setIsInspecting, highlightedElement, setHighlightedElement }) => {
const iframeRef0 = React.useRef<HTMLIFrameElement>(null);
const iframeRef1 = React.useRef<HTMLIFrameElement>(null);
const [snapshotInfo, setSnapshotInfo] = React.useState<SnapshotInfo>({ viewport: kDefaultViewport, url: '' });
@ -158,16 +166,16 @@ export const SnapshotView: React.FunctionComponent<{
isInspecting={isInspecting}
sdkLanguage={sdkLanguage}
testIdAttributeName={testIdAttributeName}
highlightedLocator={highlightedLocator}
setHighlightedLocator={setHighlightedLocator}
highlightedElement={highlightedElement}
setHighlightedElement={setHighlightedElement}
iframe={iframeRef0.current}
iteration={loadingRef.current.iteration} />
<InspectModeController
isInspecting={isInspecting}
sdkLanguage={sdkLanguage}
testIdAttributeName={testIdAttributeName}
highlightedLocator={highlightedLocator}
setHighlightedLocator={setHighlightedLocator}
highlightedElement={highlightedElement}
setHighlightedElement={setHighlightedElement}
iframe={iframeRef1.current}
iteration={loadingRef.current.iteration} />
<SnapshotWrapper snapshotInfo={snapshotInfo}>
@ -223,10 +231,10 @@ export const InspectModeController: React.FunctionComponent<{
isInspecting: boolean,
sdkLanguage: Language,
testIdAttributeName: string,
highlightedLocator: string,
setHighlightedLocator: (locator: string) => void,
highlightedElement: HighlightedElement,
setHighlightedElement: (element: HighlightedElement) => void,
iteration: number,
}> = ({ iframe, isInspecting, sdkLanguage, testIdAttributeName, highlightedLocator, setHighlightedLocator, iteration }) => {
}> = ({ iframe, isInspecting, sdkLanguage, testIdAttributeName, highlightedElement, setHighlightedElement, iteration }) => {
React.useEffect(() => {
const recorders: { recorder: Recorder, frameSelector: string }[] = [];
const isUnderTest = new URLSearchParams(window.location.search).get('isUnderTest') === 'true';
@ -236,17 +244,25 @@ export const InspectModeController: React.FunctionComponent<{
// Potential cross-origin exceptions.
}
const parsedSnapshot = highlightedElement.lastEdited === 'ariaSnapshot' && highlightedElement.ariaSnapshot ? parseAriaSnapshot(yaml, highlightedElement.ariaSnapshot) : undefined;
const fullSelector = highlightedElement.lastEdited === 'locator' && highlightedElement.locator ? locatorOrSelectorAsSelector(sdkLanguage, highlightedElement.locator, testIdAttributeName) : undefined;
for (const { recorder, frameSelector } of recorders) {
const actionSelector = locatorOrSelectorAsSelector(sdkLanguage, highlightedLocator, testIdAttributeName);
const actionSelector = fullSelector?.startsWith(frameSelector) ? fullSelector.substring(frameSelector.length).trim() : undefined;
const ariaTemplate = parsedSnapshot?.errors.length === 0 ? parsedSnapshot.fragment : undefined;
recorder.setUIState({
mode: isInspecting ? 'inspecting' : 'none',
actionSelector: actionSelector.startsWith(frameSelector) ? actionSelector.substring(frameSelector.length).trim() : undefined,
actionSelector,
ariaTemplate,
language: sdkLanguage,
testIdAttributeName,
overlay: { offsetX: 0 },
}, {
async elementPicked(elementInfo: ElementInfo) {
setHighlightedLocator(asLocator(sdkLanguage, frameSelector + elementInfo.selector));
setHighlightedElement({
locator: asLocator(sdkLanguage, frameSelector + elementInfo.selector),
ariaSnapshot: elementInfo.ariaSnapshot,
lastEdited: 'none',
});
},
highlightUpdated() {
for (const r of recorders) {
@ -256,7 +272,7 @@ export const InspectModeController: React.FunctionComponent<{
}
});
}
}, [iframe, isInspecting, highlightedLocator, setHighlightedLocator, sdkLanguage, testIdAttributeName, iteration]);
}, [iframe, isInspecting, highlightedElement, setHighlightedElement, sdkLanguage, testIdAttributeName, iteration]);
return <></>;
};

View File

@ -42,6 +42,7 @@ import './workbench.css';
import { testStatusIcon, testStatusText } from './testUtils';
import type { UITestStatus } from './testUtils';
import type { AfterActionTraceEventAttachment } from '@trace/trace';
import type { HighlightedElement } from './snapshotTab';
export const Workbench: React.FunctionComponent<{
model?: modelUtil.MultiTraceModel,
@ -65,7 +66,7 @@ export const Workbench: React.FunctionComponent<{
const [selectedNavigatorTab, setSelectedNavigatorTab] = React.useState<string>('actions');
const [selectedPropertiesTab, setSelectedPropertiesTab] = useSetting<string>('propertiesTab', showSourcesFirst ? 'source' : 'call');
const [isInspecting, setIsInspectingState] = React.useState(false);
const [highlightedLocator, setHighlightedLocator] = React.useState<string>('');
const [highlightedElement, setHighlightedElement] = React.useState<HighlightedElement>({ lastEdited: 'none' });
const [selectedTime, setSelectedTime] = React.useState<Boundaries | undefined>();
const [sidebarLocation, setSidebarLocation] = useSetting<'bottom' | 'right'>('propertiesSidebarLocation', 'bottom');
@ -140,8 +141,8 @@ export const Workbench: React.FunctionComponent<{
setIsInspectingState(value);
}, [setIsInspectingState, selectPropertiesTab, isInspecting]);
const locatorPicked = React.useCallback((locator: string) => {
setHighlightedLocator(locator);
const elementPicked = React.useCallback((element: HighlightedElement) => {
setHighlightedElement(element);
selectPropertiesTab('inspector');
}, [selectPropertiesTab]);
@ -170,8 +171,8 @@ export const Workbench: React.FunctionComponent<{
render: () => <InspectorTab
sdkLanguage={sdkLanguage}
setIsInspecting={setIsInspecting}
highlightedLocator={highlightedLocator}
setHighlightedLocator={setHighlightedLocator} />,
highlightedElement={highlightedElement}
setHighlightedElement={setHighlightedElement} />,
};
const callTab: TabbedPaneTabModel = {
id: 'call',
@ -342,8 +343,8 @@ export const Workbench: React.FunctionComponent<{
testIdAttributeName={model?.testIdAttributeName || 'data-testid'}
isInspecting={isInspecting}
setIsInspecting={setIsInspecting}
highlightedLocator={highlightedLocator}
setHighlightedLocator={locatorPicked} />}
highlightedElement={highlightedElement}
setHighlightedElement={elementPicked} />}
sidebar={
<TabbedPane
tabs={[actionsTab, metadataTab]}

View File

@ -24,7 +24,7 @@ import path from 'path';
import { pathToFileURL } from 'url';
import { expect, playwrightTest } from '../config/browserTest';
import type { FrameLocator } from '@playwright/test';
import { rafraf } from 'tests/page/pageTest';
import { rafraf, roundBox } from 'tests/page/pageTest';
const test = playwrightTest.extend<TraceViewerFixtures>(traceViewerFixtures);
@ -1096,19 +1096,41 @@ test('should pick locator', async ({ page, runAndTrace, server }) => {
const snapshot = await traceViewer.snapshotFrame('page.setContent');
await traceViewer.page.getByTitle('Pick locator').click();
await snapshot.locator('button').click();
await expect(traceViewer.page.locator('.cm-wrapper')).toContainText(`getByRole('button', { name: 'Submit' })`);
await expect(traceViewer.page.locator('.cm-wrapper').first()).toContainText(`getByRole('button', { name: 'Submit' })`);
await expect(traceViewer.page.locator('.cm-wrapper').last()).toContainText(`- button "Submit"`);
});
test('should update highlight when typing', async ({ page, runAndTrace, server }) => {
test('should update highlight when typing locator', async ({ page, runAndTrace, server }) => {
const traceViewer = await runAndTrace(async () => {
await page.goto(server.EMPTY_PAGE);
await page.setContent('<button>Submit</button>');
});
const snapshot = await traceViewer.snapshotFrame('page.setContent');
await traceViewer.page.getByText('Locator').click();
await traceViewer.page.locator('.CodeMirror').click();
await traceViewer.page.locator('.CodeMirror').first().click();
await traceViewer.page.keyboard.type('button');
await expect(snapshot.locator('x-pw-glass')).toBeVisible();
const buttonBox = roundBox(await snapshot.locator('button').boundingBox());
await expect(snapshot.locator('x-pw-highlight')).toBeVisible();
await expect.poll(async () => {
return roundBox(await snapshot.locator('x-pw-highlight').boundingBox());
}).toEqual(buttonBox);
});
test('should update highlight when typing snapshot', async ({ page, runAndTrace, server }) => {
const traceViewer = await runAndTrace(async () => {
await page.goto(server.EMPTY_PAGE);
await page.setContent('<button>Submit</button>');
});
const snapshot = await traceViewer.snapshotFrame('page.setContent');
await traceViewer.page.getByText('Locator').click();
await traceViewer.page.locator('.CodeMirror').last().click();
await traceViewer.page.keyboard.type('- button');
const buttonBox = roundBox(await snapshot.locator('button').boundingBox());
await expect(snapshot.locator('x-pw-highlight')).toBeVisible();
await expect.poll(async () => {
return roundBox(await snapshot.locator('x-pw-highlight').boundingBox());
}).toEqual(buttonBox);
});
test('should open trace-1.31', async ({ showTraceViewer }) => {
@ -1239,7 +1261,7 @@ test('should pick locator in iframe', async ({ page, runAndTrace, server }) => {
await page.evaluate('2+2');
});
await traceViewer.page.getByTitle('Pick locator').click();
const cmWrapper = traceViewer.page.locator('.cm-wrapper');
const cmWrapper = traceViewer.page.locator('.cm-wrapper').first();
const snapshot = await traceViewer.snapshotFrame('page.evaluate');
@ -1279,7 +1301,7 @@ test('should highlight locator in iframe while typing', async ({ page, runAndTra
const snapshot = await traceViewer.snapshotFrame('page.evaluate');
await traceViewer.page.getByText('Locator').click();
await traceViewer.page.locator('.CodeMirror').click();
await traceViewer.page.locator('.CodeMirror').first().click();
const locators = [{
text: `locator('#frame1').contentFrame().getByText('Hello1')`,