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 { 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) => {
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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 >> ');
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue