Skip to content

Commit

Permalink
chore: perform action based on frame path (#32347)
Browse files Browse the repository at this point in the history
  • Loading branch information
pavelfeldman authored Aug 28, 2024
1 parent acd2a4d commit 0b5456d
Show file tree
Hide file tree
Showing 5 changed files with 63 additions and 54 deletions.
5 changes: 2 additions & 3 deletions packages/playwright-core/src/server/recorder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');

Expand Down Expand Up @@ -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) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
34 changes: 14 additions & 20 deletions packages/playwright-core/src/server/recorder/recorderActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};

Expand All @@ -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;
Expand Down
72 changes: 42 additions & 30 deletions packages/playwright-core/src/server/recorder/recorderRunner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -36,77 +38,87 @@ 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,
timeout: kActionTimeout,
}));
}
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,
timeout: kActionTimeout,
}));
}
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,
Expand Down
4 changes: 4 additions & 0 deletions packages/playwright-core/src/server/recorder/recorderUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 >> ');
}

0 comments on commit 0b5456d

Please sign in to comment.