diff --git a/packages/playwright-core/src/server/DEPS.list b/packages/playwright-core/src/server/DEPS.list index bc32bb8486..0e2b8301d6 100644 --- a/packages/playwright-core/src/server/DEPS.list +++ b/packages/playwright-core/src/server/DEPS.list @@ -20,3 +20,10 @@ ./electron/ ./firefox/ ./webkit/ + +[recorder.ts] +./codegen/codeGenerator.ts +./codegen/languages.ts + +[recorderRunner.ts] +./codegen/language.ts diff --git a/packages/playwright-core/src/server/codegen/DEPS.list b/packages/playwright-core/src/server/codegen/DEPS.list new file mode 100644 index 0000000000..58432390fd --- /dev/null +++ b/packages/playwright-core/src/server/codegen/DEPS.list @@ -0,0 +1,3 @@ +[*] +../../utils/ +../deviceDescriptors.ts diff --git a/packages/playwright-core/src/server/recorder/codeGenerator.ts b/packages/playwright-core/src/server/codegen/codeGenerator.ts similarity index 97% rename from packages/playwright-core/src/server/recorder/codeGenerator.ts rename to packages/playwright-core/src/server/codegen/codeGenerator.ts index 0185302a08..bfc640b38e 100644 --- a/packages/playwright-core/src/server/recorder/codeGenerator.ts +++ b/packages/playwright-core/src/server/codegen/codeGenerator.ts @@ -15,10 +15,15 @@ */ import { EventEmitter } from 'events'; -import type { BrowserContextOptions, LaunchOptions } from '../../..'; +import type { BrowserContextOptions, LaunchOptions } from '../../../types/types'; import type { Frame } from '../frames'; import type { LanguageGenerator, LanguageGeneratorOptions } from './language'; -import type { Action, Signal, FrameDescription } from './recorderActions'; +import type { Action, Signal } from '../recorder/recorderActions'; + +export type FrameDescription = { + pageAlias: string; + framePath: string[]; +}; export type ActionInContext = { frame: FrameDescription; diff --git a/packages/playwright-core/src/server/recorder/csharp.ts b/packages/playwright-core/src/server/codegen/csharp.ts similarity index 93% rename from packages/playwright-core/src/server/recorder/csharp.ts rename to packages/playwright-core/src/server/codegen/csharp.ts index 504995c9a8..41f91d259b 100644 --- a/packages/playwright-core/src/server/recorder/csharp.ts +++ b/packages/playwright-core/src/server/codegen/csharp.ts @@ -14,13 +14,10 @@ * limitations under the License. */ -import type { BrowserContextOptions } from '../../..'; -import type { Language, LanguageGenerator, LanguageGeneratorOptions } from './language'; -import { sanitizeDeviceOptions, toSignalMap } from './language'; +import type { BrowserContextOptions } from '../../../types/types'; import type { ActionInContext } from './codeGenerator'; -import type { Action } from './recorderActions'; -import type { MouseClickOptions } from './utils'; -import { toModifiers } from './utils'; +import type { Language, LanguageGenerator, LanguageGeneratorOptions } from './language'; +import { sanitizeDeviceOptions, toClickOptions, toKeyboardModifiers, toSignalMap } from './language'; import { escapeWithQuotes, asLocator } from '../../utils'; import { deviceDescriptors } from '../deviceDescriptors'; @@ -87,7 +84,7 @@ export class CSharpLanguageGenerator implements LanguageGenerator { } const lines: string[] = []; - lines.push(this._generateActionCall(subject, action)); + lines.push(this._generateActionCall(subject, actionInContext)); if (signals.download) { lines.unshift(`var download${signals.download.downloadAlias} = await ${pageAlias}.RunAndWaitForDownloadAsync(async () =>\n{`); @@ -105,7 +102,8 @@ export class CSharpLanguageGenerator implements LanguageGenerator { return formatter.format(); } - private _generateActionCall(subject: string, action: Action): string { + private _generateActionCall(subject: string, actionInContext: ActionInContext): string { + const action = actionInContext.action; switch (action.name) { case 'openPage': throw Error('Not reached'); @@ -115,16 +113,7 @@ export class CSharpLanguageGenerator implements LanguageGenerator { let method = 'Click'; if (action.clickCount === 2) method = 'DblClick'; - const modifiers = toModifiers(action.modifiers); - const options: MouseClickOptions = {}; - if (action.button !== 'left') - options.button = action.button; - if (modifiers.length) - options.modifiers = modifiers; - if (action.clickCount > 2) - options.clickCount = action.clickCount; - if (action.position) - options.position = action.position; + const options = toClickOptions(action); if (!Object.entries(options).length) return `await ${subject}.${this._asLocator(action.selector)}.${method}Async();`; const optionsString = formatObject(options, ' ', 'Locator' + method + 'Options'); @@ -139,7 +128,7 @@ export class CSharpLanguageGenerator implements LanguageGenerator { case 'setInputFiles': return `await ${subject}.${this._asLocator(action.selector)}.SetInputFilesAsync(${formatObject(action.files)});`; case 'press': { - const modifiers = toModifiers(action.modifiers); + const modifiers = toKeyboardModifiers(action.modifiers); const shortcut = [...modifiers, action.key].join('+'); return `await ${subject}.${this._asLocator(action.selector)}.PressAsync(${quote(shortcut)});`; } diff --git a/packages/playwright-core/src/server/recorder/java.ts b/packages/playwright-core/src/server/codegen/java.ts similarity index 91% rename from packages/playwright-core/src/server/recorder/java.ts rename to packages/playwright-core/src/server/codegen/java.ts index e6f0b3f0ed..a59546f7ba 100644 --- a/packages/playwright-core/src/server/recorder/java.ts +++ b/packages/playwright-core/src/server/codegen/java.ts @@ -14,13 +14,11 @@ * limitations under the License. */ -import type { BrowserContextOptions } from '../../..'; -import type { Language, LanguageGenerator, LanguageGeneratorOptions } from './language'; -import { toSignalMap } from './language'; +import type { BrowserContextOptions } from '../../../types/types'; +import type * as types from '../types'; import type { ActionInContext } from './codeGenerator'; -import type { Action } from './recorderActions'; -import type { MouseClickOptions } from './utils'; -import { toModifiers } from './utils'; +import type { Language, LanguageGenerator, LanguageGeneratorOptions } from './language'; +import { toClickOptions, toKeyboardModifiers, toSignalMap } from './language'; import { deviceDescriptors } from '../deviceDescriptors'; import { JavaScriptFormatter } from './javascript'; import { escapeWithQuotes, asLocator } from '../../utils'; @@ -74,7 +72,7 @@ export class JavaLanguageGenerator implements LanguageGenerator { });`); } - let code = this._generateActionCall(subject, action, !!actionInContext.frame.framePath.length); + let code = this._generateActionCall(subject, actionInContext, !!actionInContext.frame.framePath.length); if (signals.popup) { code = `Page ${signals.popup.popupAlias} = ${pageAlias}.waitForPopup(() -> { @@ -93,7 +91,8 @@ export class JavaLanguageGenerator implements LanguageGenerator { return formatter.format(); } - private _generateActionCall(subject: string, action: Action, inFrameLocator: boolean): string { + private _generateActionCall(subject: string, actionInContext: ActionInContext, inFrameLocator: boolean): string { + const action = actionInContext.action; switch (action.name) { case 'openPage': throw Error('Not reached'); @@ -103,16 +102,7 @@ export class JavaLanguageGenerator implements LanguageGenerator { let method = 'click'; if (action.clickCount === 2) method = 'dblclick'; - const modifiers = toModifiers(action.modifiers); - const options: MouseClickOptions = {}; - if (action.button !== 'left') - options.button = action.button; - if (modifiers.length) - options.modifiers = modifiers; - if (action.clickCount > 2) - options.clickCount = action.clickCount; - if (action.position) - options.position = action.position; + const options = toClickOptions(action); const optionsText = formatClickOptions(options); return `${subject}.${this._asLocator(action.selector, inFrameLocator)}.${method}(${optionsText});`; } @@ -125,7 +115,7 @@ export class JavaLanguageGenerator implements LanguageGenerator { case 'setInputFiles': return `${subject}.${this._asLocator(action.selector, inFrameLocator)}.setInputFiles(${formatPath(action.files.length === 1 ? action.files[0] : action.files)});`; case 'press': { - const modifiers = toModifiers(action.modifiers); + const modifiers = toKeyboardModifiers(action.modifiers); const shortcut = [...modifiers, action.key].join('+'); return `${subject}.${this._asLocator(action.selector, inFrameLocator)}.press(${quote(shortcut)});`; } @@ -271,7 +261,7 @@ function formatContextOptions(contextOptions: BrowserContextOptions, deviceName: return lines.join('\n'); } -function formatClickOptions(options: MouseClickOptions) { +function formatClickOptions(options: types.MouseClickOptions) { const lines = []; if (options.button) lines.push(` .setButton(MouseButton.${options.button.toUpperCase()})`); diff --git a/packages/playwright-core/src/server/recorder/javascript.ts b/packages/playwright-core/src/server/codegen/javascript.ts similarity index 91% rename from packages/playwright-core/src/server/recorder/javascript.ts rename to packages/playwright-core/src/server/codegen/javascript.ts index 37fd3c25a9..bc0f20e97a 100644 --- a/packages/playwright-core/src/server/recorder/javascript.ts +++ b/packages/playwright-core/src/server/codegen/javascript.ts @@ -14,13 +14,10 @@ * limitations under the License. */ -import type { BrowserContextOptions } from '../../..'; -import type { Language, LanguageGenerator, LanguageGeneratorOptions } from './language'; -import { sanitizeDeviceOptions, toSignalMap } from './language'; +import type { BrowserContextOptions } from '../../../types/types'; import type { ActionInContext } from './codeGenerator'; -import type { Action } from './recorderActions'; -import type { MouseClickOptions } from './utils'; -import { toModifiers } from './utils'; +import type { Language, LanguageGenerator, LanguageGeneratorOptions } from './language'; +import { sanitizeDeviceOptions, toSignalMap, toKeyboardModifiers, toClickOptions } from './language'; import { deviceDescriptors } from '../deviceDescriptors'; import { escapeWithQuotes, asLocator } from '../../utils'; @@ -68,7 +65,7 @@ export class JavaScriptLanguageGenerator implements LanguageGenerator { if (signals.download) formatter.add(`const download${signals.download.downloadAlias}Promise = ${pageAlias}.waitForEvent('download');`); - formatter.add(this._generateActionCall(subject, action)); + formatter.add(this._generateActionCall(subject, actionInContext)); if (signals.popup) formatter.add(`const ${signals.popup.popupAlias} = await ${signals.popup.popupAlias}Promise;`); @@ -78,7 +75,8 @@ export class JavaScriptLanguageGenerator implements LanguageGenerator { return formatter.format(); } - private _generateActionCall(subject: string, action: Action): string { + private _generateActionCall(subject: string, actionInContext: ActionInContext): string { + const action = actionInContext.action; switch (action.name) { case 'openPage': throw Error('Not reached'); @@ -88,16 +86,7 @@ export class JavaScriptLanguageGenerator implements LanguageGenerator { let method = 'click'; if (action.clickCount === 2) method = 'dblclick'; - const modifiers = toModifiers(action.modifiers); - const options: MouseClickOptions = {}; - if (action.button !== 'left') - options.button = action.button; - if (modifiers.length) - options.modifiers = modifiers; - if (action.clickCount > 2) - options.clickCount = action.clickCount; - if (action.position) - options.position = action.position; + const options = toClickOptions(action); const optionsString = formatOptions(options, false); return `await ${subject}.${this._asLocator(action.selector)}.${method}(${optionsString});`; } @@ -110,7 +99,7 @@ export class JavaScriptLanguageGenerator implements LanguageGenerator { case 'setInputFiles': return `await ${subject}.${this._asLocator(action.selector)}.setInputFiles(${formatObject(action.files.length === 1 ? action.files[0] : action.files)});`; case 'press': { - const modifiers = toModifiers(action.modifiers); + const modifiers = toKeyboardModifiers(action.modifiers); const shortcut = [...modifiers, action.key].join('+'); return `await ${subject}.${this._asLocator(action.selector)}.press(${quote(shortcut)});`; } diff --git a/packages/playwright-core/src/server/recorder/jsonl.ts b/packages/playwright-core/src/server/codegen/jsonl.ts similarity index 100% rename from packages/playwright-core/src/server/recorder/jsonl.ts rename to packages/playwright-core/src/server/codegen/jsonl.ts diff --git a/packages/playwright-core/src/server/recorder/language.ts b/packages/playwright-core/src/server/codegen/language.ts similarity index 66% rename from packages/playwright-core/src/server/recorder/language.ts rename to packages/playwright-core/src/server/codegen/language.ts index cee2b22163..78414733d7 100644 --- a/packages/playwright-core/src/server/recorder/language.ts +++ b/packages/playwright-core/src/server/codegen/language.ts @@ -16,8 +16,9 @@ import type { BrowserContextOptions, LaunchOptions } from '../../..'; import type { Language } from '../../utils'; +import type * as actions from '../recorder/recorderActions'; +import type * as types from '../types'; import type { ActionInContext } from './codeGenerator'; -import type { Action, DialogSignal, DownloadSignal, PopupSignal } from './recorderActions'; export type { Language } from '../../utils'; export type LanguageGeneratorOptions = { @@ -51,10 +52,10 @@ export function sanitizeDeviceOptions(device: any, options: BrowserContextOption return cleanedOptions; } -export function toSignalMap(action: Action) { - let popup: PopupSignal | undefined; - let download: DownloadSignal | undefined; - let dialog: DialogSignal | undefined; +export function toSignalMap(action: actions.Action) { + let popup: actions.PopupSignal | undefined; + let download: actions.DownloadSignal | undefined; + let dialog: actions.DialogSignal | undefined; for (const signal of action.signals) { if (signal.name === 'popup') popup = signal; @@ -69,3 +70,30 @@ export function toSignalMap(action: Action) { dialog, }; } + +export function toKeyboardModifiers(modifiers: number): types.SmartKeyboardModifier[] { + const result: types.SmartKeyboardModifier[] = []; + if (modifiers & 1) + result.push('Alt'); + if (modifiers & 2) + result.push('ControlOrMeta'); + if (modifiers & 4) + result.push('ControlOrMeta'); + if (modifiers & 8) + result.push('Shift'); + return result; +} + +export function toClickOptions(action: actions.ClickAction): types.MouseClickOptions { + const modifiers = toKeyboardModifiers(action.modifiers); + const options: types.MouseClickOptions = {}; + if (action.button !== 'left') + options.button = action.button; + if (modifiers.length) + options.modifiers = modifiers; + if (action.clickCount > 2) + options.clickCount = action.clickCount; + if (action.position) + options.position = action.position; + return options; +} diff --git a/packages/playwright-core/src/server/codegen/languages.ts b/packages/playwright-core/src/server/codegen/languages.ts new file mode 100644 index 0000000000..d379be6be7 --- /dev/null +++ b/packages/playwright-core/src/server/codegen/languages.ts @@ -0,0 +1,37 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { JavaLanguageGenerator } from './java'; +import { JavaScriptLanguageGenerator } from './javascript'; +import { JsonlLanguageGenerator } from './jsonl'; +import { CSharpLanguageGenerator } from './csharp'; +import { PythonLanguageGenerator } from './python'; + +export function languageSet() { + return new Set([ + new JavaLanguageGenerator('junit'), + new JavaLanguageGenerator('library'), + new JavaScriptLanguageGenerator(/* isPlaywrightTest */false), + new JavaScriptLanguageGenerator(/* isPlaywrightTest */true), + new PythonLanguageGenerator(/* isAsync */false, /* isPytest */true), + new PythonLanguageGenerator(/* isAsync */false, /* isPytest */false), + new PythonLanguageGenerator(/* isAsync */true, /* isPytest */false), + new CSharpLanguageGenerator('mstest'), + new CSharpLanguageGenerator('nunit'), + new CSharpLanguageGenerator('library'), + new JsonlLanguageGenerator(), + ]); +} diff --git a/packages/playwright-core/src/server/recorder/python.ts b/packages/playwright-core/src/server/codegen/python.ts similarity index 92% rename from packages/playwright-core/src/server/recorder/python.ts rename to packages/playwright-core/src/server/codegen/python.ts index 3302089fbd..98949320e7 100644 --- a/packages/playwright-core/src/server/recorder/python.ts +++ b/packages/playwright-core/src/server/codegen/python.ts @@ -14,13 +14,10 @@ * limitations under the License. */ -import type { BrowserContextOptions } from '../../..'; -import type { Language, LanguageGenerator, LanguageGeneratorOptions } from './language'; -import { sanitizeDeviceOptions, toSignalMap } from './language'; +import type { BrowserContextOptions } from '../../../types/types'; import type { ActionInContext } from './codeGenerator'; -import type { Action } from './recorderActions'; -import type { MouseClickOptions } from './utils'; -import { toModifiers } from './utils'; +import type { Language, LanguageGenerator, LanguageGeneratorOptions } from './language'; +import { sanitizeDeviceOptions, toSignalMap, toKeyboardModifiers, toClickOptions } from './language'; import { escapeWithQuotes, toSnakeCase, asLocator } from '../../utils'; import { deviceDescriptors } from '../deviceDescriptors'; @@ -66,7 +63,7 @@ export class PythonLanguageGenerator implements LanguageGenerator { if (signals.dialog) formatter.add(` ${pageAlias}.once("dialog", lambda dialog: dialog.dismiss())`); - let code = `${this._awaitPrefix}${this._generateActionCall(subject, action)}`; + let code = `${this._awaitPrefix}${this._generateActionCall(subject, actionInContext)}`; if (signals.popup) { code = `${this._asyncPrefix}with ${pageAlias}.expect_popup() as ${signals.popup.popupAlias}_info { @@ -87,7 +84,8 @@ export class PythonLanguageGenerator implements LanguageGenerator { return formatter.format(); } - private _generateActionCall(subject: string, action: Action): string { + private _generateActionCall(subject: string, actionInContext: ActionInContext): string { + const action = actionInContext.action; switch (action.name) { case 'openPage': throw Error('Not reached'); @@ -97,16 +95,7 @@ export class PythonLanguageGenerator implements LanguageGenerator { let method = 'click'; if (action.clickCount === 2) method = 'dblclick'; - const modifiers = toModifiers(action.modifiers); - const options: MouseClickOptions = {}; - if (action.button !== 'left') - options.button = action.button; - if (modifiers.length) - options.modifiers = modifiers; - if (action.clickCount > 2) - options.clickCount = action.clickCount; - if (action.position) - options.position = action.position; + const options = toClickOptions(action); const optionsString = formatOptions(options, false); return `${subject}.${this._asLocator(action.selector)}.${method}(${optionsString})`; } @@ -119,7 +108,7 @@ export class PythonLanguageGenerator implements LanguageGenerator { case 'setInputFiles': return `${subject}.${this._asLocator(action.selector)}.set_input_files(${formatValue(action.files.length === 1 ? action.files[0] : action.files)})`; case 'press': { - const modifiers = toModifiers(action.modifiers); + const modifiers = toKeyboardModifiers(action.modifiers); const shortcut = [...modifiers, action.key].join('+'); return `${subject}.${this._asLocator(action.selector)}.press(${quote(shortcut)})`; } diff --git a/packages/playwright-core/src/server/recorder.ts b/packages/playwright-core/src/server/recorder.ts index 96e24e3210..e4e26b0e98 100644 --- a/packages/playwright-core/src/server/recorder.ts +++ b/packages/playwright-core/src/server/recorder.ts @@ -17,17 +17,11 @@ import * as fs from 'fs'; import type * as actions from './recorder/recorderActions'; import type * as channels from '@protocol/channels'; -import type { ActionInContext } from './recorder/codeGenerator'; -import { CodeGenerator } from './recorder/codeGenerator'; -import { toClickOptions, toModifiers } from './recorder/utils'; +import type { ActionInContext, FrameDescription } from './codegen/codeGenerator'; +import { CodeGenerator } from './codegen/codeGenerator'; import { Page } from './page'; import { Frame } from './frames'; import { BrowserContext } from './browserContext'; -import { JavaLanguageGenerator } from './recorder/java'; -import { JavaScriptLanguageGenerator } from './recorder/javascript'; -import { JsonlLanguageGenerator } from './recorder/jsonl'; -import { CSharpLanguageGenerator } from './recorder/csharp'; -import { PythonLanguageGenerator } from './recorder/python'; import * as recorderSource from '../generated/recorderSource'; import * as consoleApiSource from '../generated/consoleApiSource'; import { EmptyRecorderApp } from './recorder/recorderApp'; @@ -36,15 +30,17 @@ import { RecorderApp } from './recorder/recorderApp'; import type { CallMetadata, InstrumentationListener, SdkObject } from './instrumentation'; import type { Point } from '../common/types'; import type { CallLog, CallLogStatus, EventData, Mode, OverlayState, Source, UIState } from '@recorder/recorderTypes'; -import { createGuid, isUnderTest, monotonicTime, serializeExpectedTextValues } from '../utils'; +import { isUnderTest, monotonicTime } from '../utils'; import { metadataToCallLog } from './recorder/recorderUtils'; import { Debugger } from './debugger'; import { EventEmitter } from 'events'; import { raceAgainstDeadline } from '../utils/timeoutRunner'; -import type { Language, LanguageGenerator } from './recorder/language'; +import { type Language, type LanguageGenerator } from './codegen/language'; import { locatorOrSelectorAsSelector } from '../utils/isomorphic/locatorParser'; import { quoteCSSAttributeValue, eventsHelper, type RegisteredListener } from '../utils'; import type { Dialog } from './dialog'; +import { performAction } from './recorderRunner'; +import { languageSet } from './codegen/languages'; type BindingSource = { frame: Frame, page: Page }; @@ -425,19 +421,7 @@ class ContextRecorder extends EventEmitter { } setOutput(codegenId: string, outputFile?: string) { - const languages = new Set([ - new JavaLanguageGenerator('junit'), - new JavaLanguageGenerator('library'), - new JavaScriptLanguageGenerator(/* isPlaywrightTest */false), - new JavaScriptLanguageGenerator(/* isPlaywrightTest */true), - new PythonLanguageGenerator(/* isAsync */false, /* isPytest */true), - new PythonLanguageGenerator(/* isAsync */false, /* isPytest */false), - new PythonLanguageGenerator(/* isAsync */true, /* isPytest */false), - new CSharpLanguageGenerator('mstest'), - new CSharpLanguageGenerator('nunit'), - new CSharpLanguageGenerator('library'), - new JsonlLanguageGenerator(), - ]); + const languages = languageSet(); const primaryLanguage = [...languages].find(l => l.id === codegenId); if (!primaryLanguage) throw new Error(`\n===============================\nUnsupported language: '${codegenId}'\n===============================\n`); @@ -530,14 +514,14 @@ class ContextRecorder extends EventEmitter { } } - private _describeMainFrame(page: Page): actions.FrameDescription { + private _describeMainFrame(page: Page): FrameDescription { return { pageAlias: this._pageAliases.get(page)!, framePath: [], }; } - private async _describeFrame(frame: Frame): Promise { + private async _describeFrame(frame: Frame): Promise { return { pageAlias: this._pageAliases.get(frame._page)!, framePath: await generateFrameSelector(frame), @@ -690,98 +674,3 @@ async function generateFrameSelectorInParent(parent: Frame, frame: Frame): Promi return `iframe[name=${quoteCSSAttributeValue(frame.name())}]`; return `iframe[src=${quoteCSSAttributeValue(frame.url())}]`; } - -async function innerPerformAction(frame: 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, - startTime: monotonicTime(), - endTime: 0, - type: 'Frame', - method: action, - params, - log: [], - }; - - try { - await frame.instrumentation.onBeforeCall(frame, callMetadata); - await cb(callMetadata); - } catch (e) { - callMetadata.endTime = monotonicTime(); - await frame.instrumentation.onAfterCall(frame, callMetadata); - return false; - } - - callMetadata.endTime = monotonicTime(); - await frame.instrumentation.onAfterCall(frame, callMetadata); - return true; -} - -async function performAction(frame: Frame, action: actions.Action): Promise { - const kActionTimeout = 5000; - 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 })); - } - if (action.name === 'press') { - const modifiers = toModifiers(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 })); - } - 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 })); - 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 })); - if (action.name === 'check') - return await innerPerformAction(frame, 'check', { selector: action.selector }, callMetadata => frame.check(callMetadata, action.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 })); - 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 })); - } - 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, - 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, - 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, - 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, - expression: 'to.be.visible', - isNot: false, - timeout: kActionTimeout, - })); - } - throw new Error('Internal error: unexpected action ' + (action as any).name); -} diff --git a/packages/playwright-core/src/server/recorder/recorderActions.ts b/packages/playwright-core/src/server/recorder/recorderActions.ts index 3ab7fb91ba..c048d21bd3 100644 --- a/packages/playwright-core/src/server/recorder/recorderActions.ts +++ b/packages/playwright-core/src/server/recorder/recorderActions.ts @@ -149,8 +149,3 @@ export type DialogSignal = BaseSignal & { }; export type Signal = NavigationSignal | PopupSignal | DownloadSignal | DialogSignal; - -export type FrameDescription = { - pageAlias: string; - framePath: string[]; -}; diff --git a/packages/playwright-core/src/server/recorder/utils.ts b/packages/playwright-core/src/server/recorder/utils.ts deleted file mode 100644 index 883a8ab129..0000000000 --- a/packages/playwright-core/src/server/recorder/utils.ts +++ /dev/null @@ -1,51 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import type { Frame } from '../frames'; -import type { SmartKeyboardModifier } from '../types'; -import type * as actions from './recorderActions'; - -export type MouseClickOptions = Parameters[2]; - -export function toClickOptions(action: actions.ClickAction): { method: 'click' | 'dblclick', options: MouseClickOptions } { - let method: 'click' | 'dblclick' = 'click'; - if (action.clickCount === 2) - method = 'dblclick'; - const modifiers = toModifiers(action.modifiers); - const options: MouseClickOptions = {}; - if (action.button !== 'left') - options.button = action.button; - if (modifiers.length) - options.modifiers = modifiers; - if (action.clickCount > 2) - options.clickCount = action.clickCount; - if (action.position) - options.position = action.position; - return { method, options }; -} - -export function toModifiers(modifiers: number): SmartKeyboardModifier[] { - const result: SmartKeyboardModifier[] = []; - if (modifiers & 1) - result.push('Alt'); - if (modifiers & 2) - result.push('ControlOrMeta'); - if (modifiers & 4) - result.push('ControlOrMeta'); - if (modifiers & 8) - result.push('Shift'); - return result; -} diff --git a/packages/playwright-core/src/server/recorderRunner.ts b/packages/playwright-core/src/server/recorderRunner.ts new file mode 100644 index 0000000000..4058ae4053 --- /dev/null +++ b/packages/playwright-core/src/server/recorderRunner.ts @@ -0,0 +1,116 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { createGuid, monotonicTime, serializeExpectedTextValues } from '../utils'; +import { toClickOptions, toKeyboardModifiers } from './codegen/language'; +import type { Frame } from './frames'; +import type { CallMetadata } from './instrumentation'; +import type * as actions from './recorder/recorderActions'; + +async function innerPerformAction(frame: 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, + startTime: monotonicTime(), + endTime: 0, + type: 'Frame', + method: action, + params, + log: [], + }; + + try { + await frame.instrumentation.onBeforeCall(frame, callMetadata); + await cb(callMetadata); + } catch (e) { + callMetadata.endTime = monotonicTime(); + await frame.instrumentation.onAfterCall(frame, callMetadata); + return false; + } + + callMetadata.endTime = monotonicTime(); + await frame.instrumentation.onAfterCall(frame, callMetadata); + return true; +} + +export async function performAction(frame: Frame, action: actions.Action): Promise { + const kActionTimeout = 5000; + 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 })); + } + 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 })); + } + 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 })); + 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 })); + if (action.name === 'check') + return await innerPerformAction(frame, 'check', { selector: action.selector }, callMetadata => frame.check(callMetadata, action.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 })); + 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 })); + } + 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, + 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, + 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, + 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, + expression: 'to.be.visible', + isNot: false, + timeout: kActionTimeout, + })); + } + throw new Error('Internal error: unexpected action ' + (action as any).name); +}