chore: move codegen into its own folder (#32330)

This commit is contained in:
Pavel Feldman 2024-08-26 15:24:02 -07:00 committed by GitHub
parent 888a5b53e7
commit 6f55b57e5a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 246 additions and 260 deletions

View file

@ -20,3 +20,10 @@
./electron/
./firefox/
./webkit/
[recorder.ts]
./codegen/codeGenerator.ts
./codegen/languages.ts
[recorderRunner.ts]
./codegen/language.ts

View file

@ -0,0 +1,3 @@
[*]
../../utils/
../deviceDescriptors.ts

View file

@ -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;

View file

@ -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)});`;
}

View file

@ -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()})`);

View file

@ -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)});`;
}

View file

@ -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;
}

View 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(),
]);
}

View file

@ -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)})`;
}

View file

@ -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);
}

View file

@ -149,8 +149,3 @@ export type DialogSignal = BaseSignal & {
};
export type Signal = NavigationSignal | PopupSignal | DownloadSignal | DialogSignal;
export type FrameDescription = {
pageAlias: string;
framePath: string[];
};

View file

@ -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;
}

View 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);
}