chore: move codegen into its own folder (#32330)
This commit is contained in:
parent
888a5b53e7
commit
6f55b57e5a
|
|
@ -20,3 +20,10 @@
|
|||
./electron/
|
||||
./firefox/
|
||||
./webkit/
|
||||
|
||||
[recorder.ts]
|
||||
./codegen/codeGenerator.ts
|
||||
./codegen/languages.ts
|
||||
|
||||
[recorderRunner.ts]
|
||||
./codegen/language.ts
|
||||
|
|
|
|||
3
packages/playwright-core/src/server/codegen/DEPS.list
Normal file
3
packages/playwright-core/src/server/codegen/DEPS.list
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
[*]
|
||||
../../utils/
|
||||
../deviceDescriptors.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;
|
||||
|
|
@ -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)});`;
|
||||
}
|
||||
|
|
@ -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()})`);
|
||||
|
|
@ -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)});`;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
37
packages/playwright-core/src/server/codegen/languages.ts
Normal file
37
packages/playwright-core/src/server/codegen/languages.ts
Normal file
|
|
@ -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(),
|
||||
]);
|
||||
}
|
||||
|
|
@ -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)})`;
|
||||
}
|
||||
|
|
@ -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<actions.FrameDescription> {
|
||||
private async _describeFrame(frame: Frame): Promise<FrameDescription> {
|
||||
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<any>): Promise<boolean> {
|
||||
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<boolean> {
|
||||
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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -149,8 +149,3 @@ export type DialogSignal = BaseSignal & {
|
|||
};
|
||||
|
||||
export type Signal = NavigationSignal | PopupSignal | DownloadSignal | DialogSignal;
|
||||
|
||||
export type FrameDescription = {
|
||||
pageAlias: string;
|
||||
framePath: string[];
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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<Frame['click']>[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;
|
||||
}
|
||||
116
packages/playwright-core/src/server/recorderRunner.ts
Normal file
116
packages/playwright-core/src/server/recorderRunner.ts
Normal file
|
|
@ -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<any>): Promise<boolean> {
|
||||
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<boolean> {
|
||||
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);
|
||||
}
|
||||
Loading…
Reference in a new issue