2020-12-28 23:50:12 +01:00
|
|
|
/**
|
|
|
|
|
* 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.
|
|
|
|
|
*/
|
|
|
|
|
|
2021-02-17 03:13:26 +01:00
|
|
|
import type { BrowserContextOptions } from '../../../..';
|
|
|
|
|
import { LanguageGenerator, LanguageGeneratorOptions, sanitizeDeviceOptions, toSignalMap } from './language';
|
2021-01-25 04:21:19 +01:00
|
|
|
import { ActionInContext } from './codeGenerator';
|
2021-02-17 03:13:26 +01:00
|
|
|
import { Action, actionTitle } from './recorderActions';
|
2021-01-25 04:21:19 +01:00
|
|
|
import { MouseClickOptions, toModifiers } from './utils';
|
|
|
|
|
import deviceDescriptors = require('../../deviceDescriptors');
|
2020-12-28 23:50:12 +01:00
|
|
|
|
|
|
|
|
export class JavaScriptLanguageGenerator implements LanguageGenerator {
|
2021-02-17 03:13:26 +01:00
|
|
|
id = 'javascript';
|
|
|
|
|
fileName = '<javascript>';
|
|
|
|
|
highlighter = 'javascript';
|
2020-12-28 23:50:12 +01:00
|
|
|
|
2021-02-17 03:13:26 +01:00
|
|
|
generateAction(actionInContext: ActionInContext): string {
|
2021-01-27 22:19:36 +01:00
|
|
|
const { action, pageAlias } = actionInContext;
|
2020-12-28 23:50:12 +01:00
|
|
|
const formatter = new JavaScriptFormatter(2);
|
|
|
|
|
formatter.newLine();
|
|
|
|
|
formatter.add('// ' + actionTitle(action));
|
|
|
|
|
|
|
|
|
|
if (action.name === 'openPage') {
|
|
|
|
|
formatter.add(`const ${pageAlias} = await context.newPage();`);
|
|
|
|
|
if (action.url && action.url !== 'about:blank' && action.url !== 'chrome://newtab/')
|
2021-04-21 16:59:38 +02:00
|
|
|
formatter.add(`await ${pageAlias}.goto(${quote(action.url)});`);
|
2020-12-28 23:50:12 +01:00
|
|
|
return formatter.format();
|
|
|
|
|
}
|
|
|
|
|
|
2021-01-27 22:19:36 +01:00
|
|
|
const subject = actionInContext.isMainFrame ? pageAlias :
|
|
|
|
|
(actionInContext.frameName ?
|
|
|
|
|
`${pageAlias}.frame(${formatObject({ name: actionInContext.frameName })})` :
|
|
|
|
|
`${pageAlias}.frame(${formatObject({ url: actionInContext.frameUrl })})`);
|
2020-12-28 23:50:12 +01:00
|
|
|
|
2021-02-17 03:13:26 +01:00
|
|
|
const signals = toSignalMap(action);
|
2020-12-28 23:50:12 +01:00
|
|
|
|
2021-02-17 03:13:26 +01:00
|
|
|
if (signals.dialog) {
|
2020-12-28 23:50:12 +01:00
|
|
|
formatter.add(` ${pageAlias}.once('dialog', dialog => {
|
|
|
|
|
console.log(\`Dialog message: $\{dialog.message()}\`);
|
|
|
|
|
dialog.dismiss().catch(() => {});
|
|
|
|
|
});`);
|
|
|
|
|
}
|
|
|
|
|
|
2021-02-17 03:13:26 +01:00
|
|
|
const emitPromiseAll = signals.waitForNavigation || signals.popup || signals.download;
|
2020-12-28 23:50:12 +01:00
|
|
|
if (emitPromiseAll) {
|
|
|
|
|
// Generate either await Promise.all([]) or
|
|
|
|
|
// const [popup1] = await Promise.all([]).
|
|
|
|
|
let leftHandSide = '';
|
2021-02-17 03:13:26 +01:00
|
|
|
if (signals.popup)
|
|
|
|
|
leftHandSide = `const [${signals.popup.popupAlias}] = `;
|
|
|
|
|
else if (signals.download)
|
2020-12-28 23:50:12 +01:00
|
|
|
leftHandSide = `const [download] = `;
|
|
|
|
|
formatter.add(`${leftHandSide}await Promise.all([`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Popup signals.
|
2021-02-17 03:13:26 +01:00
|
|
|
if (signals.popup)
|
2020-12-28 23:50:12 +01:00
|
|
|
formatter.add(`${pageAlias}.waitForEvent('popup'),`);
|
|
|
|
|
|
|
|
|
|
// Navigation signal.
|
2021-02-17 03:13:26 +01:00
|
|
|
if (signals.waitForNavigation)
|
|
|
|
|
formatter.add(`${pageAlias}.waitForNavigation(/*{ url: ${quote(signals.waitForNavigation.url)} }*/),`);
|
2020-12-28 23:50:12 +01:00
|
|
|
|
|
|
|
|
// Download signals.
|
2021-02-17 03:13:26 +01:00
|
|
|
if (signals.download)
|
2020-12-28 23:50:12 +01:00
|
|
|
formatter.add(`${pageAlias}.waitForEvent('download'),`);
|
|
|
|
|
|
2021-02-17 03:13:26 +01:00
|
|
|
const prefix = (signals.popup || signals.waitForNavigation || signals.download) ? '' : 'await ';
|
2020-12-28 23:50:12 +01:00
|
|
|
const actionCall = this._generateActionCall(action);
|
2021-02-17 03:13:26 +01:00
|
|
|
const suffix = (signals.waitForNavigation || emitPromiseAll) ? '' : ';';
|
2020-12-28 23:50:12 +01:00
|
|
|
formatter.add(`${prefix}${subject}.${actionCall}${suffix}`);
|
|
|
|
|
|
|
|
|
|
if (emitPromiseAll)
|
|
|
|
|
formatter.add(`]);`);
|
2021-02-17 03:13:26 +01:00
|
|
|
else if (signals.assertNavigation)
|
|
|
|
|
formatter.add(` // assert.equal(${pageAlias}.url(), ${quote(signals.assertNavigation.url)});`);
|
2020-12-28 23:50:12 +01:00
|
|
|
return formatter.format();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private _generateActionCall(action: Action): string {
|
|
|
|
|
switch (action.name) {
|
|
|
|
|
case 'openPage':
|
|
|
|
|
throw Error('Not reached');
|
|
|
|
|
case 'closePage':
|
|
|
|
|
return 'close()';
|
|
|
|
|
case 'click': {
|
|
|
|
|
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;
|
|
|
|
|
const optionsString = formatOptions(options);
|
|
|
|
|
return `${method}(${quote(action.selector)}${optionsString})`;
|
|
|
|
|
}
|
|
|
|
|
case 'check':
|
|
|
|
|
return `check(${quote(action.selector)})`;
|
|
|
|
|
case 'uncheck':
|
|
|
|
|
return `uncheck(${quote(action.selector)})`;
|
|
|
|
|
case 'fill':
|
|
|
|
|
return `fill(${quote(action.selector)}, ${quote(action.text)})`;
|
|
|
|
|
case 'setInputFiles':
|
|
|
|
|
return `setInputFiles(${quote(action.selector)}, ${formatObject(action.files.length === 1 ? action.files[0] : action.files)})`;
|
|
|
|
|
case 'press': {
|
|
|
|
|
const modifiers = toModifiers(action.modifiers);
|
|
|
|
|
const shortcut = [...modifiers, action.key].join('+');
|
|
|
|
|
return `press(${quote(action.selector)}, ${quote(shortcut)})`;
|
|
|
|
|
}
|
|
|
|
|
case 'navigate':
|
|
|
|
|
return `goto(${quote(action.url)})`;
|
|
|
|
|
case 'select':
|
|
|
|
|
return `selectOption(${quote(action.selector)}, ${formatObject(action.options.length > 1 ? action.options : action.options[0])})`;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2021-02-17 03:13:26 +01:00
|
|
|
generateHeader(options: LanguageGeneratorOptions): string {
|
2020-12-28 23:50:12 +01:00
|
|
|
const formatter = new JavaScriptFormatter();
|
|
|
|
|
formatter.add(`
|
2021-02-17 03:13:26 +01:00
|
|
|
const { ${options.browserName}${options.deviceName ? ', devices' : ''} } = require('playwright');
|
2020-12-28 23:50:12 +01:00
|
|
|
|
|
|
|
|
(async () => {
|
2021-02-17 03:13:26 +01:00
|
|
|
const browser = await ${options.browserName}.launch(${formatObjectOrVoid(options.launchOptions)});
|
|
|
|
|
const context = await browser.newContext(${formatContextOptions(options.contextOptions, options.deviceName)});`);
|
2020-12-28 23:50:12 +01:00
|
|
|
return formatter.format();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
generateFooter(saveStorage: string | undefined): string {
|
2021-01-13 21:52:03 +01:00
|
|
|
const storageStateLine = saveStorage ? `\n await context.storageState({ path: '${saveStorage}' });` : '';
|
2021-02-17 03:13:26 +01:00
|
|
|
return `\n // ---------------------${storageStateLine}
|
2020-12-28 23:50:12 +01:00
|
|
|
await context.close();
|
|
|
|
|
await browser.close();
|
|
|
|
|
})();`;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function formatOptions(value: any): string {
|
|
|
|
|
const keys = Object.keys(value);
|
|
|
|
|
if (!keys.length)
|
|
|
|
|
return '';
|
|
|
|
|
return ', ' + formatObject(value);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function formatObject(value: any, indent = ' '): string {
|
|
|
|
|
if (typeof value === 'string')
|
|
|
|
|
return quote(value);
|
|
|
|
|
if (Array.isArray(value))
|
|
|
|
|
return `[${value.map(o => formatObject(o)).join(', ')}]`;
|
|
|
|
|
if (typeof value === 'object') {
|
|
|
|
|
const keys = Object.keys(value);
|
|
|
|
|
if (!keys.length)
|
|
|
|
|
return '{}';
|
|
|
|
|
const tokens: string[] = [];
|
|
|
|
|
for (const key of keys)
|
|
|
|
|
tokens.push(`${key}: ${formatObject(value[key])}`);
|
|
|
|
|
return `{\n${indent}${tokens.join(`,\n${indent}`)}\n}`;
|
|
|
|
|
}
|
|
|
|
|
return String(value);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function formatObjectOrVoid(value: any, indent = ' '): string {
|
|
|
|
|
const result = formatObject(value, indent);
|
|
|
|
|
return result === '{}' ? '' : result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function formatContextOptions(options: BrowserContextOptions, deviceName: string | undefined): string {
|
2021-01-25 04:21:19 +01:00
|
|
|
const device = deviceName && deviceDescriptors[deviceName];
|
2020-12-28 23:50:12 +01:00
|
|
|
if (!device)
|
|
|
|
|
return formatObjectOrVoid(options);
|
|
|
|
|
// Filter out all the properties from the device descriptor.
|
2021-01-25 04:21:19 +01:00
|
|
|
let serializedObject = formatObjectOrVoid(sanitizeDeviceOptions(device, options));
|
2020-12-28 23:50:12 +01:00
|
|
|
// When there are no additional context options, we still want to spread the device inside.
|
|
|
|
|
if (!serializedObject)
|
|
|
|
|
serializedObject = '{\n}';
|
|
|
|
|
const lines = serializedObject.split('\n');
|
|
|
|
|
lines.splice(1, 0, `...devices['${deviceName}'],`);
|
|
|
|
|
return lines.join('\n');
|
|
|
|
|
}
|
|
|
|
|
|
2021-03-03 23:32:09 +01:00
|
|
|
export class JavaScriptFormatter {
|
2020-12-28 23:50:12 +01:00
|
|
|
private _baseIndent: string;
|
|
|
|
|
private _baseOffset: string;
|
|
|
|
|
private _lines: string[] = [];
|
|
|
|
|
|
|
|
|
|
constructor(offset = 0) {
|
|
|
|
|
this._baseIndent = ' '.repeat(2);
|
|
|
|
|
this._baseOffset = ' '.repeat(offset);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
prepend(text: string) {
|
|
|
|
|
this._lines = text.trim().split('\n').map(line => line.trim()).concat(this._lines);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
add(text: string) {
|
|
|
|
|
this._lines.push(...text.trim().split('\n').map(line => line.trim()));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
newLine() {
|
|
|
|
|
this._lines.push('');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
format(): string {
|
|
|
|
|
let spaces = '';
|
|
|
|
|
let previousLine = '';
|
|
|
|
|
return this._lines.map((line: string) => {
|
|
|
|
|
if (line === '')
|
|
|
|
|
return line;
|
|
|
|
|
if (line.startsWith('}') || line.startsWith(']'))
|
|
|
|
|
spaces = spaces.substring(this._baseIndent.length);
|
|
|
|
|
|
2021-03-03 23:32:09 +01:00
|
|
|
const extraSpaces = /^(for|while|if|try).*\(.*\)$/.test(previousLine) ? this._baseIndent : '';
|
2020-12-28 23:50:12 +01:00
|
|
|
previousLine = line;
|
|
|
|
|
|
2021-03-05 23:05:48 +01:00
|
|
|
const callCarryOver = line.startsWith('.set');
|
2021-03-03 23:32:09 +01:00
|
|
|
line = spaces + extraSpaces + (callCarryOver ? this._baseIndent : '') + line;
|
2020-12-28 23:50:12 +01:00
|
|
|
if (line.endsWith('{') || line.endsWith('['))
|
|
|
|
|
spaces += this._baseIndent;
|
|
|
|
|
return this._baseOffset + line;
|
|
|
|
|
}).join('\n');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function quote(text: string, char: string = '\'') {
|
|
|
|
|
if (char === '\'')
|
|
|
|
|
return char + text.replace(/[']/g, '\\\'') + char;
|
|
|
|
|
if (char === '"')
|
|
|
|
|
return char + text.replace(/["]/g, '\\"') + char;
|
|
|
|
|
if (char === '`')
|
|
|
|
|
return char + text.replace(/[`]/g, '\\`') + char;
|
|
|
|
|
throw new Error('Invalid escape char');
|
|
|
|
|
}
|