chore: perform action based on frame path (#32347)
This commit is contained in:
parent
acd2a4ddad
commit
0b5456d00b
|
|
@ -28,7 +28,7 @@ import type { CallMetadata, InstrumentationListener, SdkObject } from './instrum
|
|||
import { ContextRecorder, generateFrameSelector } from './recorder/contextRecorder';
|
||||
import type { IRecorderApp } from './recorder/recorderApp';
|
||||
import { EmptyRecorderApp, RecorderApp } from './recorder/recorderApp';
|
||||
import { metadataToCallLog } from './recorder/recorderUtils';
|
||||
import { buildFullSelector, metadataToCallLog } from './recorder/recorderUtils';
|
||||
|
||||
const recorderSymbol = Symbol('recorderSymbol');
|
||||
|
||||
|
|
@ -175,8 +175,7 @@ export class Recorder implements InstrumentationListener {
|
|||
|
||||
await this._context.exposeBinding('__pw_recorderSetSelector', false, async ({ frame }, selector: string) => {
|
||||
const selectorChain = await generateFrameSelector(frame);
|
||||
selectorChain.push(selector);
|
||||
await this._recorderApp?.setSelector(selectorChain.join(' >> internal:control=enter-frame >> '), true);
|
||||
await this._recorderApp?.setSelector(buildFullSelector(selectorChain, selector), true);
|
||||
});
|
||||
|
||||
await this._context.exposeBinding('__pw_recorderSetMode', false, async ({ frame }, mode: Mode) => {
|
||||
|
|
|
|||
|
|
@ -230,7 +230,7 @@ export class ContextRecorder extends EventEmitter {
|
|||
};
|
||||
|
||||
this._collection.willPerformAction(actionInContext);
|
||||
const success = await performAction(frame, action);
|
||||
const success = await performAction(this._pageAliases, actionInContext);
|
||||
if (success) {
|
||||
this._collection.didPerformAction(actionInContext);
|
||||
this._setCommittedAfterTimeout(actionInContext);
|
||||
|
|
|
|||
|
|
@ -37,28 +37,28 @@ export type ActionBase = {
|
|||
signals: Signal[],
|
||||
};
|
||||
|
||||
export type ClickAction = ActionBase & {
|
||||
name: 'click',
|
||||
export type ActionWithSelector = ActionBase & {
|
||||
selector: string,
|
||||
};
|
||||
|
||||
export type ClickAction = ActionWithSelector & {
|
||||
name: 'click',
|
||||
button: 'left' | 'middle' | 'right',
|
||||
modifiers: number,
|
||||
clickCount: number,
|
||||
position?: Point,
|
||||
};
|
||||
|
||||
export type CheckAction = ActionBase & {
|
||||
export type CheckAction = ActionWithSelector & {
|
||||
name: 'check',
|
||||
selector: string,
|
||||
};
|
||||
|
||||
export type UncheckAction = ActionBase & {
|
||||
export type UncheckAction = ActionWithSelector & {
|
||||
name: 'uncheck',
|
||||
selector: string,
|
||||
};
|
||||
|
||||
export type FillAction = ActionBase & {
|
||||
export type FillAction = ActionWithSelector & {
|
||||
name: 'fill',
|
||||
selector: string,
|
||||
text: string,
|
||||
};
|
||||
|
||||
|
|
@ -83,40 +83,34 @@ export type PressAction = ActionBase & {
|
|||
modifiers: number,
|
||||
};
|
||||
|
||||
export type SelectAction = ActionBase & {
|
||||
export type SelectAction = ActionWithSelector & {
|
||||
name: 'select',
|
||||
selector: string,
|
||||
options: string[],
|
||||
};
|
||||
|
||||
export type SetInputFilesAction = ActionBase & {
|
||||
export type SetInputFilesAction = ActionWithSelector & {
|
||||
name: 'setInputFiles',
|
||||
selector: string,
|
||||
files: string[],
|
||||
};
|
||||
|
||||
export type AssertTextAction = ActionBase & {
|
||||
export type AssertTextAction = ActionWithSelector & {
|
||||
name: 'assertText',
|
||||
selector: string,
|
||||
text: string,
|
||||
substring: boolean,
|
||||
};
|
||||
|
||||
export type AssertValueAction = ActionBase & {
|
||||
export type AssertValueAction = ActionWithSelector & {
|
||||
name: 'assertValue',
|
||||
selector: string,
|
||||
value: string,
|
||||
};
|
||||
|
||||
export type AssertCheckedAction = ActionBase & {
|
||||
export type AssertCheckedAction = ActionWithSelector & {
|
||||
name: 'assertChecked',
|
||||
selector: string,
|
||||
checked: boolean,
|
||||
};
|
||||
|
||||
export type AssertVisibleAction = ActionBase & {
|
||||
export type AssertVisibleAction = ActionWithSelector & {
|
||||
name: 'assertVisible',
|
||||
selector: string,
|
||||
};
|
||||
|
||||
export type Action = ClickAction | CheckAction | ClosesPageAction | OpenPageAction | UncheckAction | FillAction | NavigateAction | PressAction | SelectAction | SetInputFilesAction | AssertTextAction | AssertValueAction | AssertCheckedAction | AssertVisibleAction;
|
||||
|
|
|
|||
|
|
@ -16,17 +16,19 @@
|
|||
|
||||
import { createGuid, monotonicTime, serializeExpectedTextValues } from '../../utils';
|
||||
import { toClickOptions, toKeyboardModifiers } from '../codegen/language';
|
||||
import type { ActionInContext } from '../codegen/types';
|
||||
import type { Frame } from '../frames';
|
||||
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 = {
|
||||
id: `call@${createGuid()}`,
|
||||
apiName: 'frame.' + action,
|
||||
objectId: frame.guid,
|
||||
pageId: frame._page.guid,
|
||||
frameId: frame.guid,
|
||||
objectId: mainFrame.guid,
|
||||
pageId: mainFrame._page.guid,
|
||||
frameId: mainFrame.guid,
|
||||
startTime: monotonicTime(),
|
||||
endTime: 0,
|
||||
type: 'Frame',
|
||||
|
|
@ -36,59 +38,69 @@ async function innerPerformAction(frame: Frame, action: string, params: any, cb:
|
|||
};
|
||||
|
||||
try {
|
||||
await frame.instrumentation.onBeforeCall(frame, callMetadata);
|
||||
await mainFrame.instrumentation.onBeforeCall(mainFrame, callMetadata);
|
||||
await cb(callMetadata);
|
||||
} catch (e) {
|
||||
callMetadata.endTime = monotonicTime();
|
||||
await frame.instrumentation.onAfterCall(frame, callMetadata);
|
||||
await mainFrame.instrumentation.onAfterCall(mainFrame, callMetadata);
|
||||
return false;
|
||||
}
|
||||
|
||||
callMetadata.endTime = monotonicTime();
|
||||
await frame.instrumentation.onAfterCall(frame, callMetadata);
|
||||
await mainFrame.instrumentation.onAfterCall(mainFrame, callMetadata);
|
||||
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;
|
||||
|
||||
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') {
|
||||
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') {
|
||||
const modifiers = toKeyboardModifiers(action.modifiers);
|
||||
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')
|
||||
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')
|
||||
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')
|
||||
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')
|
||||
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') {
|
||||
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') {
|
||||
return await innerPerformAction(frame, 'expect', { selector: action.selector }, callMetadata => frame.expect(callMetadata, action.selector, {
|
||||
selector: action.selector,
|
||||
return await innerPerformAction(mainFrame, 'expect', { selector }, callMetadata => mainFrame.expect(callMetadata, selector, {
|
||||
selector,
|
||||
expression: 'to.be.checked',
|
||||
isNot: !action.checked,
|
||||
timeout: kActionTimeout,
|
||||
}));
|
||||
}
|
||||
if (action.name === 'assertText') {
|
||||
return await innerPerformAction(frame, 'expect', { selector: action.selector }, callMetadata => frame.expect(callMetadata, action.selector, {
|
||||
selector: action.selector,
|
||||
return await innerPerformAction(mainFrame, 'expect', { selector }, callMetadata => mainFrame.expect(callMetadata, selector, {
|
||||
selector,
|
||||
expression: 'to.have.text',
|
||||
expectedText: serializeExpectedTextValues([action.text], { matchSubstring: true, normalizeWhiteSpace: true }),
|
||||
isNot: false,
|
||||
|
|
@ -96,8 +108,8 @@ export async function performAction(frame: Frame, action: actions.Action): Promi
|
|||
}));
|
||||
}
|
||||
if (action.name === 'assertValue') {
|
||||
return await innerPerformAction(frame, 'expect', { selector: action.selector }, callMetadata => frame.expect(callMetadata, action.selector, {
|
||||
selector: action.selector,
|
||||
return await innerPerformAction(mainFrame, 'expect', { selector }, callMetadata => mainFrame.expect(callMetadata, selector, {
|
||||
selector,
|
||||
expression: 'to.have.value',
|
||||
expectedValue: action.value,
|
||||
isNot: false,
|
||||
|
|
@ -105,8 +117,8 @@ export async function performAction(frame: Frame, action: actions.Action): Promi
|
|||
}));
|
||||
}
|
||||
if (action.name === 'assertVisible') {
|
||||
return await innerPerformAction(frame, 'expect', { selector: action.selector }, callMetadata => frame.expect(callMetadata, action.selector, {
|
||||
selector: action.selector,
|
||||
return await innerPerformAction(mainFrame, 'expect', { selector }, callMetadata => mainFrame.expect(callMetadata, selector, {
|
||||
selector,
|
||||
expression: 'to.be.visible',
|
||||
isNot: false,
|
||||
timeout: kActionTimeout,
|
||||
|
|
|
|||
|
|
@ -44,3 +44,7 @@ export function metadataToCallLog(metadata: CallMetadata, status: CallLogStatus)
|
|||
};
|
||||
return callLog;
|
||||
}
|
||||
|
||||
export function buildFullSelector(framePath: string[], selector: string) {
|
||||
return [...framePath, selector].join(' >> internal:control=enter-frame >> ');
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue