chore: perform action based on frame path (#32347)

This commit is contained in:
Pavel Feldman 2024-08-27 17:17:57 -07:00 committed by GitHub
parent acd2a4ddad
commit 0b5456d00b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 63 additions and 54 deletions

View file

@ -28,7 +28,7 @@ import type { CallMetadata, InstrumentationListener, SdkObject } from './instrum
import { ContextRecorder, generateFrameSelector } from './recorder/contextRecorder'; import { ContextRecorder, generateFrameSelector } from './recorder/contextRecorder';
import type { IRecorderApp } from './recorder/recorderApp'; import type { IRecorderApp } from './recorder/recorderApp';
import { EmptyRecorderApp, RecorderApp } from './recorder/recorderApp'; import { EmptyRecorderApp, RecorderApp } from './recorder/recorderApp';
import { metadataToCallLog } from './recorder/recorderUtils'; import { buildFullSelector, metadataToCallLog } from './recorder/recorderUtils';
const recorderSymbol = Symbol('recorderSymbol'); const recorderSymbol = Symbol('recorderSymbol');
@ -175,8 +175,7 @@ export class Recorder implements InstrumentationListener {
await this._context.exposeBinding('__pw_recorderSetSelector', false, async ({ frame }, selector: string) => { await this._context.exposeBinding('__pw_recorderSetSelector', false, async ({ frame }, selector: string) => {
const selectorChain = await generateFrameSelector(frame); const selectorChain = await generateFrameSelector(frame);
selectorChain.push(selector); await this._recorderApp?.setSelector(buildFullSelector(selectorChain, selector), true);
await this._recorderApp?.setSelector(selectorChain.join(' >> internal:control=enter-frame >> '), true);
}); });
await this._context.exposeBinding('__pw_recorderSetMode', false, async ({ frame }, mode: Mode) => { await this._context.exposeBinding('__pw_recorderSetMode', false, async ({ frame }, mode: Mode) => {

View file

@ -230,7 +230,7 @@ export class ContextRecorder extends EventEmitter {
}; };
this._collection.willPerformAction(actionInContext); this._collection.willPerformAction(actionInContext);
const success = await performAction(frame, action); const success = await performAction(this._pageAliases, actionInContext);
if (success) { if (success) {
this._collection.didPerformAction(actionInContext); this._collection.didPerformAction(actionInContext);
this._setCommittedAfterTimeout(actionInContext); this._setCommittedAfterTimeout(actionInContext);

View file

@ -37,28 +37,28 @@ export type ActionBase = {
signals: Signal[], signals: Signal[],
}; };
export type ClickAction = ActionBase & { export type ActionWithSelector = ActionBase & {
name: 'click',
selector: string, selector: string,
};
export type ClickAction = ActionWithSelector & {
name: 'click',
button: 'left' | 'middle' | 'right', button: 'left' | 'middle' | 'right',
modifiers: number, modifiers: number,
clickCount: number, clickCount: number,
position?: Point, position?: Point,
}; };
export type CheckAction = ActionBase & { export type CheckAction = ActionWithSelector & {
name: 'check', name: 'check',
selector: string,
}; };
export type UncheckAction = ActionBase & { export type UncheckAction = ActionWithSelector & {
name: 'uncheck', name: 'uncheck',
selector: string,
}; };
export type FillAction = ActionBase & { export type FillAction = ActionWithSelector & {
name: 'fill', name: 'fill',
selector: string,
text: string, text: string,
}; };
@ -83,40 +83,34 @@ export type PressAction = ActionBase & {
modifiers: number, modifiers: number,
}; };
export type SelectAction = ActionBase & { export type SelectAction = ActionWithSelector & {
name: 'select', name: 'select',
selector: string,
options: string[], options: string[],
}; };
export type SetInputFilesAction = ActionBase & { export type SetInputFilesAction = ActionWithSelector & {
name: 'setInputFiles', name: 'setInputFiles',
selector: string,
files: string[], files: string[],
}; };
export type AssertTextAction = ActionBase & { export type AssertTextAction = ActionWithSelector & {
name: 'assertText', name: 'assertText',
selector: string,
text: string, text: string,
substring: boolean, substring: boolean,
}; };
export type AssertValueAction = ActionBase & { export type AssertValueAction = ActionWithSelector & {
name: 'assertValue', name: 'assertValue',
selector: string,
value: string, value: string,
}; };
export type AssertCheckedAction = ActionBase & { export type AssertCheckedAction = ActionWithSelector & {
name: 'assertChecked', name: 'assertChecked',
selector: string,
checked: boolean, checked: boolean,
}; };
export type AssertVisibleAction = ActionBase & { export type AssertVisibleAction = ActionWithSelector & {
name: 'assertVisible', name: 'assertVisible',
selector: string,
}; };
export type Action = ClickAction | CheckAction | ClosesPageAction | OpenPageAction | UncheckAction | FillAction | NavigateAction | PressAction | SelectAction | SetInputFilesAction | AssertTextAction | AssertValueAction | AssertCheckedAction | AssertVisibleAction; export type Action = ClickAction | CheckAction | ClosesPageAction | OpenPageAction | UncheckAction | FillAction | NavigateAction | PressAction | SelectAction | SetInputFilesAction | AssertTextAction | AssertValueAction | AssertCheckedAction | AssertVisibleAction;

View file

@ -16,17 +16,19 @@
import { createGuid, monotonicTime, serializeExpectedTextValues } from '../../utils'; import { createGuid, monotonicTime, serializeExpectedTextValues } from '../../utils';
import { toClickOptions, toKeyboardModifiers } from '../codegen/language'; import { toClickOptions, toKeyboardModifiers } from '../codegen/language';
import type { ActionInContext } from '../codegen/types';
import type { Frame } from '../frames'; import type { Frame } from '../frames';
import type { CallMetadata } from '../instrumentation'; import type { CallMetadata } from '../instrumentation';
import type * as actions from './recorderActions'; import type { Page } from '../page';
import { buildFullSelector } from './recorderUtils';
async function innerPerformAction(frame: Frame, action: string, params: any, cb: (callMetadata: CallMetadata) => Promise<any>): Promise<boolean> { async function innerPerformAction(mainFrame: Frame, action: string, params: any, cb: (callMetadata: CallMetadata) => Promise<any>): Promise<boolean> {
const callMetadata: CallMetadata = { const callMetadata: CallMetadata = {
id: `call@${createGuid()}`, id: `call@${createGuid()}`,
apiName: 'frame.' + action, apiName: 'frame.' + action,
objectId: frame.guid, objectId: mainFrame.guid,
pageId: frame._page.guid, pageId: mainFrame._page.guid,
frameId: frame.guid, frameId: mainFrame.guid,
startTime: monotonicTime(), startTime: monotonicTime(),
endTime: 0, endTime: 0,
type: 'Frame', type: 'Frame',
@ -36,59 +38,69 @@ async function innerPerformAction(frame: Frame, action: string, params: any, cb:
}; };
try { try {
await frame.instrumentation.onBeforeCall(frame, callMetadata); await mainFrame.instrumentation.onBeforeCall(mainFrame, callMetadata);
await cb(callMetadata); await cb(callMetadata);
} catch (e) { } catch (e) {
callMetadata.endTime = monotonicTime(); callMetadata.endTime = monotonicTime();
await frame.instrumentation.onAfterCall(frame, callMetadata); await mainFrame.instrumentation.onAfterCall(mainFrame, callMetadata);
return false; return false;
} }
callMetadata.endTime = monotonicTime(); callMetadata.endTime = monotonicTime();
await frame.instrumentation.onAfterCall(frame, callMetadata); await mainFrame.instrumentation.onAfterCall(mainFrame, callMetadata);
return true; return true;
} }
export async function performAction(frame: Frame, action: actions.Action): Promise<boolean> { export async function performAction(pageAliases: Map<Page, string>, actionInContext: ActionInContext): Promise<boolean> {
const pageAlias = actionInContext.frame.pageAlias;
const page = [...pageAliases.entries()].find(([, alias]) => pageAlias === alias)?.[0];
if (!page)
throw new Error('Internal error: page not found');
const mainFrame = page.mainFrame();
const { action } = actionInContext;
const kActionTimeout = 5000; const kActionTimeout = 5000;
if (action.name === 'navigate')
return await innerPerformAction(mainFrame, 'goto', { url: action.url }, callMetadata => mainFrame.goto(callMetadata, action.url, { timeout: kActionTimeout }));
if (action.name === 'openPage')
throw Error('Not reached');
if (action.name === 'closePage')
return await innerPerformAction(mainFrame, 'close', {}, callMetadata => mainFrame._page.close(callMetadata));
const selector = buildFullSelector(actionInContext.frame.framePath, action.selector);
if (action.name === 'click') { if (action.name === 'click') {
const options = toClickOptions(action); const options = toClickOptions(action);
return await innerPerformAction(frame, 'click', { selector: action.selector }, callMetadata => frame.click(callMetadata, action.selector, { ...options, timeout: kActionTimeout, strict: true })); return await innerPerformAction(mainFrame, 'click', { selector }, callMetadata => mainFrame.click(callMetadata, selector, { ...options, timeout: kActionTimeout, strict: true }));
} }
if (action.name === 'press') { if (action.name === 'press') {
const modifiers = toKeyboardModifiers(action.modifiers); const modifiers = toKeyboardModifiers(action.modifiers);
const shortcut = [...modifiers, action.key].join('+'); const shortcut = [...modifiers, action.key].join('+');
return await innerPerformAction(frame, 'press', { selector: action.selector, key: shortcut }, callMetadata => frame.press(callMetadata, action.selector, shortcut, { timeout: kActionTimeout, strict: true })); return await innerPerformAction(mainFrame, 'press', { selector, key: shortcut }, callMetadata => mainFrame.press(callMetadata, selector, shortcut, { timeout: kActionTimeout, strict: true }));
} }
if (action.name === 'fill') if (action.name === 'fill')
return await innerPerformAction(frame, 'fill', { selector: action.selector, text: action.text }, callMetadata => frame.fill(callMetadata, action.selector, action.text, { timeout: kActionTimeout, strict: true })); return await innerPerformAction(mainFrame, 'fill', { selector, text: action.text }, callMetadata => mainFrame.fill(callMetadata, selector, action.text, { timeout: kActionTimeout, strict: true }));
if (action.name === 'setInputFiles') if (action.name === 'setInputFiles')
return await innerPerformAction(frame, 'setInputFiles', { selector: action.selector, files: action.files }, callMetadata => frame.setInputFiles(callMetadata, action.selector, { selector: action.selector, payloads: [], timeout: kActionTimeout, strict: true })); return await innerPerformAction(mainFrame, 'setInputFiles', { selector, files: action.files }, callMetadata => mainFrame.setInputFiles(callMetadata, selector, { selector, payloads: [], timeout: kActionTimeout, strict: true }));
if (action.name === 'check') if (action.name === 'check')
return await innerPerformAction(frame, 'check', { selector: action.selector }, callMetadata => frame.check(callMetadata, action.selector, { timeout: kActionTimeout, strict: true })); return await innerPerformAction(mainFrame, 'check', { selector }, callMetadata => mainFrame.check(callMetadata, selector, { timeout: kActionTimeout, strict: true }));
if (action.name === 'uncheck') if (action.name === 'uncheck')
return await innerPerformAction(frame, 'uncheck', { selector: action.selector }, callMetadata => frame.uncheck(callMetadata, action.selector, { timeout: kActionTimeout, strict: true })); return await innerPerformAction(mainFrame, 'uncheck', { selector }, callMetadata => mainFrame.uncheck(callMetadata, selector, { timeout: kActionTimeout, strict: true }));
if (action.name === 'select') { if (action.name === 'select') {
const values = action.options.map(value => ({ value })); const values = action.options.map(value => ({ value }));
return await innerPerformAction(frame, 'selectOption', { selector: action.selector, values }, callMetadata => frame.selectOption(callMetadata, action.selector, [], values, { timeout: kActionTimeout, strict: true })); return await innerPerformAction(mainFrame, 'selectOption', { selector, values }, callMetadata => mainFrame.selectOption(callMetadata, selector, [], values, { timeout: kActionTimeout, strict: true }));
} }
if (action.name === 'navigate')
return await innerPerformAction(frame, 'goto', { url: action.url }, callMetadata => frame.goto(callMetadata, action.url, { timeout: kActionTimeout }));
if (action.name === 'closePage')
return await innerPerformAction(frame, 'close', {}, callMetadata => frame._page.close(callMetadata));
if (action.name === 'openPage')
throw Error('Not reached');
if (action.name === 'assertChecked') { if (action.name === 'assertChecked') {
return await innerPerformAction(frame, 'expect', { selector: action.selector }, callMetadata => frame.expect(callMetadata, action.selector, { return await innerPerformAction(mainFrame, 'expect', { selector }, callMetadata => mainFrame.expect(callMetadata, selector, {
selector: action.selector, selector,
expression: 'to.be.checked', expression: 'to.be.checked',
isNot: !action.checked, isNot: !action.checked,
timeout: kActionTimeout, timeout: kActionTimeout,
})); }));
} }
if (action.name === 'assertText') { if (action.name === 'assertText') {
return await innerPerformAction(frame, 'expect', { selector: action.selector }, callMetadata => frame.expect(callMetadata, action.selector, { return await innerPerformAction(mainFrame, 'expect', { selector }, callMetadata => mainFrame.expect(callMetadata, selector, {
selector: action.selector, selector,
expression: 'to.have.text', expression: 'to.have.text',
expectedText: serializeExpectedTextValues([action.text], { matchSubstring: true, normalizeWhiteSpace: true }), expectedText: serializeExpectedTextValues([action.text], { matchSubstring: true, normalizeWhiteSpace: true }),
isNot: false, isNot: false,
@ -96,8 +108,8 @@ export async function performAction(frame: Frame, action: actions.Action): Promi
})); }));
} }
if (action.name === 'assertValue') { if (action.name === 'assertValue') {
return await innerPerformAction(frame, 'expect', { selector: action.selector }, callMetadata => frame.expect(callMetadata, action.selector, { return await innerPerformAction(mainFrame, 'expect', { selector }, callMetadata => mainFrame.expect(callMetadata, selector, {
selector: action.selector, selector,
expression: 'to.have.value', expression: 'to.have.value',
expectedValue: action.value, expectedValue: action.value,
isNot: false, isNot: false,
@ -105,8 +117,8 @@ export async function performAction(frame: Frame, action: actions.Action): Promi
})); }));
} }
if (action.name === 'assertVisible') { if (action.name === 'assertVisible') {
return await innerPerformAction(frame, 'expect', { selector: action.selector }, callMetadata => frame.expect(callMetadata, action.selector, { return await innerPerformAction(mainFrame, 'expect', { selector }, callMetadata => mainFrame.expect(callMetadata, selector, {
selector: action.selector, selector,
expression: 'to.be.visible', expression: 'to.be.visible',
isNot: false, isNot: false,
timeout: kActionTimeout, timeout: kActionTimeout,

View file

@ -44,3 +44,7 @@ export function metadataToCallLog(metadata: CallMetadata, status: CallLogStatus)
}; };
return callLog; return callLog;
} }
export function buildFullSelector(framePath: string[], selector: string) {
return [...framePath, selector].join(' >> internal:control=enter-frame >> ');
}