From 0b5456d00b61435e5141445cf691846d731ce5f1 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Tue, 27 Aug 2024 17:17:57 -0700 Subject: [PATCH] chore: perform action based on frame path (#32347) --- .../playwright-core/src/server/recorder.ts | 5 +- .../src/server/recorder/contextRecorder.ts | 2 +- .../src/server/recorder/recorderActions.ts | 34 ++++----- .../src/server/recorder/recorderRunner.ts | 72 +++++++++++-------- .../src/server/recorder/recorderUtils.ts | 4 ++ 5 files changed, 63 insertions(+), 54 deletions(-) diff --git a/packages/playwright-core/src/server/recorder.ts b/packages/playwright-core/src/server/recorder.ts index 17c38187e8..857df392b3 100644 --- a/packages/playwright-core/src/server/recorder.ts +++ b/packages/playwright-core/src/server/recorder.ts @@ -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) => { diff --git a/packages/playwright-core/src/server/recorder/contextRecorder.ts b/packages/playwright-core/src/server/recorder/contextRecorder.ts index 72f972e6b5..0d55a2bf32 100644 --- a/packages/playwright-core/src/server/recorder/contextRecorder.ts +++ b/packages/playwright-core/src/server/recorder/contextRecorder.ts @@ -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); diff --git a/packages/playwright-core/src/server/recorder/recorderActions.ts b/packages/playwright-core/src/server/recorder/recorderActions.ts index c048d21bd3..9447f32457 100644 --- a/packages/playwright-core/src/server/recorder/recorderActions.ts +++ b/packages/playwright-core/src/server/recorder/recorderActions.ts @@ -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; diff --git a/packages/playwright-core/src/server/recorder/recorderRunner.ts b/packages/playwright-core/src/server/recorder/recorderRunner.ts index beb74c5a6d..b6bdfd1a72 100644 --- a/packages/playwright-core/src/server/recorder/recorderRunner.ts +++ b/packages/playwright-core/src/server/recorder/recorderRunner.ts @@ -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): Promise { +async function innerPerformAction(mainFrame: Frame, action: string, params: any, cb: (callMetadata: CallMetadata) => Promise): Promise { 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 { +export async function performAction(pageAliases: Map, actionInContext: ActionInContext): Promise { + 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, diff --git a/packages/playwright-core/src/server/recorder/recorderUtils.ts b/packages/playwright-core/src/server/recorder/recorderUtils.ts index d6237b4899..b044da87ac 100644 --- a/packages/playwright-core/src/server/recorder/recorderUtils.ts +++ b/packages/playwright-core/src/server/recorder/recorderUtils.ts @@ -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 >> '); +}