mirror of
https://github.com/microsoft/playwright.git
synced 2025-06-26 21:40:17 +00:00
feat(codegen): generate locators and frame locators (#11873)
This commit is contained in:
parent
46dfa45b4e
commit
f82e09be04
@ -44,7 +44,7 @@ export class Highlight {
|
||||
this._innerGlassPaneElement.appendChild(this._tooltipElement);
|
||||
|
||||
// Use a closed shadow root to prevent selectors matching our internal previews.
|
||||
this._glassPaneShadow = this._outerGlassPaneElement.attachShadow({ mode: this._isUnderTest ? 'open' : 'closed' });
|
||||
this._glassPaneShadow = this._outerGlassPaneElement.attachShadow({ mode: 'closed' });
|
||||
this._glassPaneShadow.appendChild(this._innerGlassPaneElement);
|
||||
this._glassPaneShadow.appendChild(this._actionPointElement);
|
||||
const styleElement = document.createElement('style');
|
||||
@ -105,6 +105,8 @@ export class Highlight {
|
||||
this._actionPointElement.style.top = y + 'px';
|
||||
this._actionPointElement.style.left = x + 'px';
|
||||
this._actionPointElement.hidden = false;
|
||||
if (this._isUnderTest)
|
||||
console.error('Action point for test: ' + JSON.stringify({ x, y })); // eslint-disable-line no-console
|
||||
}
|
||||
|
||||
hideActionPoint() {
|
||||
@ -162,6 +164,9 @@ export class Highlight {
|
||||
highlightElement.style.height = box.height + 'px';
|
||||
highlightElement.style.display = 'block';
|
||||
this._highlightElements.push(highlightElement);
|
||||
|
||||
if (this._isUnderTest)
|
||||
console.error('Highlight box for test: ' + JSON.stringify({ x: box.x, y: box.y, width: box.width, height: box.height })); // eslint-disable-line no-console
|
||||
}
|
||||
|
||||
for (const highlightElement of pool) {
|
||||
|
||||
@ -124,6 +124,10 @@ export class InjectedScript {
|
||||
return result;
|
||||
}
|
||||
|
||||
generateSelector(targetElement: Element): string {
|
||||
return generateSelector(this, targetElement, true).selector;
|
||||
}
|
||||
|
||||
querySelector(selector: ParsedSelector, root: Node, strict: boolean): Element | undefined {
|
||||
if (!(root as any)['querySelector'])
|
||||
throw this.createStacklessError('Node is not queryable.');
|
||||
@ -848,7 +852,7 @@ export class InjectedScript {
|
||||
strictModeViolationError(selector: ParsedSelector, matches: Element[]): Error {
|
||||
const infos = matches.slice(0, 10).map(m => ({
|
||||
preview: this.previewNode(m),
|
||||
selector: generateSelector(this, m, true).selector
|
||||
selector: this.generateSelector(m),
|
||||
}));
|
||||
const lines = infos.map((info, i) => `\n ${i + 1}) ${info.preview} aka playwright.$("${info.selector}")`);
|
||||
if (infos.length < matches.length)
|
||||
|
||||
@ -18,14 +18,10 @@ import { EventEmitter } from 'events';
|
||||
import type { BrowserContextOptions, LaunchOptions } from '../../../..';
|
||||
import { Frame } from '../../frames';
|
||||
import { LanguageGenerator, LanguageGeneratorOptions } from './language';
|
||||
import { Action, Signal } from './recorderActions';
|
||||
import { describeFrame } from './utils';
|
||||
import { Action, Signal, FrameDescription } from './recorderActions';
|
||||
|
||||
export type ActionInContext = {
|
||||
pageAlias: string;
|
||||
frameName?: string;
|
||||
frameUrl: string;
|
||||
isMainFrame: boolean;
|
||||
frame: FrameDescription;
|
||||
action: Action;
|
||||
committed?: boolean;
|
||||
};
|
||||
@ -82,10 +78,10 @@ export class CodeGenerator extends EventEmitter {
|
||||
didPerformAction(actionInContext: ActionInContext) {
|
||||
if (!this._enabled)
|
||||
return;
|
||||
const { action, pageAlias } = actionInContext;
|
||||
const action = actionInContext.action;
|
||||
let eraseLastAction = false;
|
||||
if (this._lastAction && this._lastAction.pageAlias === pageAlias) {
|
||||
const { action: lastAction } = this._lastAction;
|
||||
if (this._lastAction && this._lastAction.frame.pageAlias === actionInContext.frame.pageAlias) {
|
||||
const lastAction = this._lastAction.action;
|
||||
// We augment last action based on the type.
|
||||
if (this._lastAction && action.name === 'fill' && lastAction.name === 'fill') {
|
||||
if (action.selector === lastAction.selector)
|
||||
@ -148,8 +144,11 @@ export class CodeGenerator extends EventEmitter {
|
||||
|
||||
if (signal.name === 'navigation') {
|
||||
this.addAction({
|
||||
pageAlias,
|
||||
...describeFrame(frame),
|
||||
frame: {
|
||||
pageAlias,
|
||||
isMainFrame: frame._page.mainFrame() === frame,
|
||||
url: frame.url(),
|
||||
},
|
||||
committed: true,
|
||||
action: {
|
||||
name: 'navigate',
|
||||
|
||||
@ -28,7 +28,8 @@ export class CSharpLanguageGenerator implements LanguageGenerator {
|
||||
highlighter = 'csharp';
|
||||
|
||||
generateAction(actionInContext: ActionInContext): string {
|
||||
const { action, pageAlias } = actionInContext;
|
||||
const action = actionInContext.action;
|
||||
const pageAlias = actionInContext.frame.pageAlias;
|
||||
const formatter = new CSharpFormatter(8);
|
||||
formatter.newLine();
|
||||
formatter.add('// ' + actionTitle(action));
|
||||
@ -40,10 +41,17 @@ export class CSharpLanguageGenerator implements LanguageGenerator {
|
||||
return formatter.format();
|
||||
}
|
||||
|
||||
const subject = actionInContext.isMainFrame ? pageAlias :
|
||||
(actionInContext.frameName ?
|
||||
`${pageAlias}.Frame(${quote(actionInContext.frameName)})` :
|
||||
`${pageAlias}.FrameByUrl(${quote(actionInContext.frameUrl)})`);
|
||||
let subject: string;
|
||||
if (actionInContext.frame.isMainFrame) {
|
||||
subject = pageAlias;
|
||||
} else if (actionInContext.frame.selectorsChain && action.name !== 'navigate') {
|
||||
const locators = actionInContext.frame.selectorsChain.map(selector => '.' + asLocator(selector, 'FrameLocator'));
|
||||
subject = `${pageAlias}${locators.join('')}`;
|
||||
} else if (actionInContext.frame.name) {
|
||||
subject = `${pageAlias}.Frame(${quote(actionInContext.frame.name)})`;
|
||||
} else {
|
||||
subject = `${pageAlias}.FrameByUrl(${quote(actionInContext.frame.url)})`;
|
||||
}
|
||||
|
||||
const signals = toSignalMap(action);
|
||||
|
||||
@ -58,12 +66,12 @@ export class CSharpLanguageGenerator implements LanguageGenerator {
|
||||
}
|
||||
|
||||
const lines: string[] = [];
|
||||
const actionCall = this._generateActionCall(action, actionInContext.isMainFrame);
|
||||
const actionCall = this._generateActionCall(action, actionInContext.frame.isMainFrame);
|
||||
if (signals.waitForNavigation) {
|
||||
lines.push(`await ${pageAlias}.RunAndWaitForNavigationAsync(async () =>`);
|
||||
lines.push(`{`);
|
||||
lines.push(` await ${subject}.${actionCall};`);
|
||||
lines.push(`}/*, new ${actionInContext.isMainFrame ? 'Page' : 'Frame'}WaitForNavigationOptions`);
|
||||
lines.push(`}/*, new ${actionInContext.frame.isMainFrame ? 'Page' : 'Frame'}WaitForNavigationOptions`);
|
||||
lines.push(`{`);
|
||||
lines.push(` UrlString = ${quote(signals.waitForNavigation.url)}`);
|
||||
lines.push(`}*/);`);
|
||||
@ -110,27 +118,27 @@ export class CSharpLanguageGenerator implements LanguageGenerator {
|
||||
if (action.position)
|
||||
options.position = action.position;
|
||||
if (!Object.entries(options).length)
|
||||
return `${method}Async(${quote(action.selector)})`;
|
||||
const optionsString = formatObject(options, ' ', (isPage ? 'Page' : 'Frame') + method + 'Options');
|
||||
return `${method}Async(${quote(action.selector)}, ${optionsString})`;
|
||||
return asLocator(action.selector) + `.${method}Async()`;
|
||||
const optionsString = formatObject(options, ' ', 'Locator' + method + 'Options');
|
||||
return asLocator(action.selector) + `.${method}Async(${optionsString})`;
|
||||
}
|
||||
case 'check':
|
||||
return `CheckAsync(${quote(action.selector)})`;
|
||||
return asLocator(action.selector) + `.CheckAsync()`;
|
||||
case 'uncheck':
|
||||
return `UncheckAsync(${quote(action.selector)})`;
|
||||
return asLocator(action.selector) + `.UncheckAsync()`;
|
||||
case 'fill':
|
||||
return `FillAsync(${quote(action.selector)}, ${quote(action.text)})`;
|
||||
return asLocator(action.selector) + `.FillAsync(${quote(action.text)})`;
|
||||
case 'setInputFiles':
|
||||
return `SetInputFilesAsync(${quote(action.selector)}, ${formatObject(action.files)})`;
|
||||
return asLocator(action.selector) + `.SetInputFilesAsync(${formatObject(action.files)})`;
|
||||
case 'press': {
|
||||
const modifiers = toModifiers(action.modifiers);
|
||||
const shortcut = [...modifiers, action.key].join('+');
|
||||
return `PressAsync(${quote(action.selector)}, ${quote(shortcut)})`;
|
||||
return asLocator(action.selector) + `.PressAsync(${quote(shortcut)})`;
|
||||
}
|
||||
case 'navigate':
|
||||
return `GotoAsync(${quote(action.url)})`;
|
||||
case 'select':
|
||||
return `SelectOptionAsync(${quote(action.selector)}, ${formatObject(action.options)})`;
|
||||
return asLocator(action.selector) + `.SelectOptionAsync(${formatObject(action.options)})`;
|
||||
}
|
||||
}
|
||||
|
||||
@ -270,4 +278,13 @@ class CSharpFormatter {
|
||||
|
||||
function quote(text: string) {
|
||||
return escapeWithQuotes(text, '\"');
|
||||
}
|
||||
}
|
||||
|
||||
function asLocator(selector: string, locatorFn = 'Locator') {
|
||||
const match = selector.match(/(.*)\s+>>\s+nth=(\d+)$/);
|
||||
if (!match)
|
||||
return `${locatorFn}(${quote(selector)})`;
|
||||
if (+match[2] === 0)
|
||||
return `${locatorFn}(${quote(match[1])}).First`;
|
||||
return `${locatorFn}(${quote(match[1])}).Nth(${match[2]})`;
|
||||
}
|
||||
|
||||
@ -29,7 +29,8 @@ export class JavaLanguageGenerator implements LanguageGenerator {
|
||||
highlighter = 'java';
|
||||
|
||||
generateAction(actionInContext: ActionInContext): string {
|
||||
const { action, pageAlias } = actionInContext;
|
||||
const action = actionInContext.action;
|
||||
const pageAlias = actionInContext.frame.pageAlias;
|
||||
const formatter = new JavaScriptFormatter(6);
|
||||
formatter.newLine();
|
||||
formatter.add('// ' + actionTitle(action));
|
||||
@ -41,10 +42,17 @@ export class JavaLanguageGenerator implements LanguageGenerator {
|
||||
return formatter.format();
|
||||
}
|
||||
|
||||
const subject = actionInContext.isMainFrame ? pageAlias :
|
||||
(actionInContext.frameName ?
|
||||
`${pageAlias}.frame(${quote(actionInContext.frameName)})` :
|
||||
`${pageAlias}.frameByUrl(${quote(actionInContext.frameUrl)})`);
|
||||
let subject: string;
|
||||
if (actionInContext.frame.isMainFrame) {
|
||||
subject = pageAlias;
|
||||
} else if (actionInContext.frame.selectorsChain && action.name !== 'navigate') {
|
||||
const locators = actionInContext.frame.selectorsChain.map(selector => '.' + asLocator(selector, 'frameLocator'));
|
||||
subject = `${pageAlias}${locators.join('')}`;
|
||||
} else if (actionInContext.frame.name) {
|
||||
subject = `${pageAlias}.frame(${quote(actionInContext.frame.name)})`;
|
||||
} else {
|
||||
subject = `${pageAlias}.frameByUrl(${quote(actionInContext.frame.url)})`;
|
||||
}
|
||||
|
||||
const signals = toSignalMap(action);
|
||||
|
||||
@ -55,7 +63,7 @@ export class JavaLanguageGenerator implements LanguageGenerator {
|
||||
});`);
|
||||
}
|
||||
|
||||
const actionCall = this._generateActionCall(action, actionInContext.isMainFrame);
|
||||
const actionCall = this._generateActionCall(action);
|
||||
let code = `${subject}.${actionCall};`;
|
||||
|
||||
if (signals.popup) {
|
||||
@ -85,7 +93,7 @@ export class JavaLanguageGenerator implements LanguageGenerator {
|
||||
return formatter.format();
|
||||
}
|
||||
|
||||
private _generateActionCall(action: Action, isPage: boolean): string {
|
||||
private _generateActionCall(action: Action): string {
|
||||
switch (action.name) {
|
||||
case 'openPage':
|
||||
throw Error('Not reached');
|
||||
@ -105,26 +113,26 @@ export class JavaLanguageGenerator implements LanguageGenerator {
|
||||
options.clickCount = action.clickCount;
|
||||
if (action.position)
|
||||
options.position = action.position;
|
||||
const optionsText = formatClickOptions(options, isPage);
|
||||
return `${method}(${quote(action.selector)}${optionsText ? ', ' : ''}${optionsText})`;
|
||||
const optionsText = formatClickOptions(options);
|
||||
return asLocator(action.selector) + `.${method}(${optionsText})`;
|
||||
}
|
||||
case 'check':
|
||||
return `check(${quote(action.selector)})`;
|
||||
return asLocator(action.selector) + `.check()`;
|
||||
case 'uncheck':
|
||||
return `uncheck(${quote(action.selector)})`;
|
||||
return asLocator(action.selector) + `.uncheck()`;
|
||||
case 'fill':
|
||||
return `fill(${quote(action.selector)}, ${quote(action.text)})`;
|
||||
return asLocator(action.selector) + `.fill(${quote(action.text)})`;
|
||||
case 'setInputFiles':
|
||||
return `setInputFiles(${quote(action.selector)}, ${formatPath(action.files.length === 1 ? action.files[0] : action.files)})`;
|
||||
return asLocator(action.selector) + `.setInputFiles(${formatPath(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)})`;
|
||||
return asLocator(action.selector) + `.press(${quote(shortcut)})`;
|
||||
}
|
||||
case 'navigate':
|
||||
return `navigate(${quote(action.url)})`;
|
||||
case 'select':
|
||||
return `selectOption(${quote(action.selector)}, ${formatSelectOption(action.options.length > 1 ? action.options : action.options[0])})`;
|
||||
return asLocator(action.selector) + `.selectOption(${formatSelectOption(action.options.length > 1 ? action.options : action.options[0])})`;
|
||||
}
|
||||
}
|
||||
|
||||
@ -217,7 +225,7 @@ function formatContextOptions(contextOptions: BrowserContextOptions, deviceName:
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
function formatClickOptions(options: MouseClickOptions, isPage: boolean) {
|
||||
function formatClickOptions(options: MouseClickOptions) {
|
||||
const lines = [];
|
||||
if (options.button)
|
||||
lines.push(` .setButton(MouseButton.${options.button.toUpperCase()})`);
|
||||
@ -229,10 +237,19 @@ function formatClickOptions(options: MouseClickOptions, isPage: boolean) {
|
||||
lines.push(` .setPosition(${options.position.x}, ${options.position.y})`);
|
||||
if (!lines.length)
|
||||
return '';
|
||||
lines.unshift(`new ${isPage ? 'Page' : 'Frame'}.ClickOptions()`);
|
||||
lines.unshift(`new Locator.ClickOptions()`);
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
function quote(text: string) {
|
||||
return escapeWithQuotes(text, '\"');
|
||||
}
|
||||
|
||||
function asLocator(selector: string, locatorFn = 'locator') {
|
||||
const match = selector.match(/(.*)\s+>>\s+nth=(\d+)$/);
|
||||
if (!match)
|
||||
return `${locatorFn}(${quote(selector)})`;
|
||||
if (+match[2] === 0)
|
||||
return `${locatorFn}(${quote(match[1])}).first()`;
|
||||
return `${locatorFn}(${quote(match[1])}).nth(${match[2]})`;
|
||||
}
|
||||
|
||||
@ -35,7 +35,8 @@ export class JavaScriptLanguageGenerator implements LanguageGenerator {
|
||||
}
|
||||
|
||||
generateAction(actionInContext: ActionInContext): string {
|
||||
const { action, pageAlias } = actionInContext;
|
||||
const action = actionInContext.action;
|
||||
const pageAlias = actionInContext.frame.pageAlias;
|
||||
const formatter = new JavaScriptFormatter(2);
|
||||
formatter.newLine();
|
||||
formatter.add('// ' + actionTitle(action));
|
||||
@ -49,10 +50,17 @@ export class JavaScriptLanguageGenerator implements LanguageGenerator {
|
||||
return formatter.format();
|
||||
}
|
||||
|
||||
const subject = actionInContext.isMainFrame ? pageAlias :
|
||||
(actionInContext.frameName ?
|
||||
`${pageAlias}.frame(${formatObject({ name: actionInContext.frameName })})` :
|
||||
`${pageAlias}.frame(${formatObject({ url: actionInContext.frameUrl })})`);
|
||||
let subject: string;
|
||||
if (actionInContext.frame.isMainFrame) {
|
||||
subject = pageAlias;
|
||||
} else if (actionInContext.frame.selectorsChain && action.name !== 'navigate') {
|
||||
const locators = actionInContext.frame.selectorsChain.map(selector => '.' + asLocator(selector, 'frameLocator'));
|
||||
subject = `${pageAlias}${locators.join('')}`;
|
||||
} else if (actionInContext.frame.name) {
|
||||
subject = `${pageAlias}.frame(${formatObject({ name: actionInContext.frame.name })})`;
|
||||
} else {
|
||||
subject = `${pageAlias}.frame(${formatObject({ url: actionInContext.frame.url })})`;
|
||||
}
|
||||
|
||||
const signals = toSignalMap(action);
|
||||
|
||||
@ -123,26 +131,26 @@ export class JavaScriptLanguageGenerator implements LanguageGenerator {
|
||||
options.clickCount = action.clickCount;
|
||||
if (action.position)
|
||||
options.position = action.position;
|
||||
const optionsString = formatOptions(options);
|
||||
return `${method}(${quote(action.selector)}${optionsString})`;
|
||||
const optionsString = formatOptions(options, false);
|
||||
return asLocator(action.selector) + `.${method}(${optionsString})`;
|
||||
}
|
||||
case 'check':
|
||||
return `check(${quote(action.selector)})`;
|
||||
return asLocator(action.selector) + `.check()`;
|
||||
case 'uncheck':
|
||||
return `uncheck(${quote(action.selector)})`;
|
||||
return asLocator(action.selector) + `.uncheck()`;
|
||||
case 'fill':
|
||||
return `fill(${quote(action.selector)}, ${quote(action.text)})`;
|
||||
return asLocator(action.selector) + `.fill(${quote(action.text)})`;
|
||||
case 'setInputFiles':
|
||||
return `setInputFiles(${quote(action.selector)}, ${formatObject(action.files.length === 1 ? action.files[0] : action.files)})`;
|
||||
return asLocator(action.selector) + `.setInputFiles(${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)})`;
|
||||
return asLocator(action.selector) + `.press(${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])})`;
|
||||
return asLocator(action.selector) + `.selectOption(${formatObject(action.options.length > 1 ? action.options : action.options[0])})`;
|
||||
}
|
||||
}
|
||||
|
||||
@ -192,11 +200,20 @@ ${useText ? '\ntest.use(' + useText + ');\n' : ''}
|
||||
}
|
||||
}
|
||||
|
||||
function formatOptions(value: any): string {
|
||||
function asLocator(selector: string, locatorFn = 'locator') {
|
||||
const match = selector.match(/(.*)\s+>>\s+nth=(\d+)$/);
|
||||
if (!match)
|
||||
return `${locatorFn}(${quote(selector)})`;
|
||||
if (+match[2] === 0)
|
||||
return `${locatorFn}(${quote(match[1])}).first()`;
|
||||
return `${locatorFn}(${quote(match[1])}).nth(${match[2]})`;
|
||||
}
|
||||
|
||||
function formatOptions(value: any, hasArguments: boolean): string {
|
||||
const keys = Object.keys(value);
|
||||
if (!keys.length)
|
||||
return '';
|
||||
return ', ' + formatObject(value);
|
||||
return (hasArguments ? ', ' : '') + formatObject(value);
|
||||
}
|
||||
|
||||
function formatObject(value: any, indent = ' '): string {
|
||||
|
||||
@ -40,7 +40,8 @@ export class PythonLanguageGenerator implements LanguageGenerator {
|
||||
}
|
||||
|
||||
generateAction(actionInContext: ActionInContext): string {
|
||||
const { action, pageAlias } = actionInContext;
|
||||
const action = actionInContext.action;
|
||||
const pageAlias = actionInContext.frame.pageAlias;
|
||||
const formatter = new PythonFormatter(4);
|
||||
formatter.newLine();
|
||||
formatter.add('# ' + actionTitle(action));
|
||||
@ -52,10 +53,17 @@ export class PythonLanguageGenerator implements LanguageGenerator {
|
||||
return formatter.format();
|
||||
}
|
||||
|
||||
const subject = actionInContext.isMainFrame ? pageAlias :
|
||||
(actionInContext.frameName ?
|
||||
`${pageAlias}.frame(${formatOptions({ name: actionInContext.frameName }, false)})` :
|
||||
`${pageAlias}.frame(${formatOptions({ url: actionInContext.frameUrl }, false)})`);
|
||||
let subject: string;
|
||||
if (actionInContext.frame.isMainFrame) {
|
||||
subject = pageAlias;
|
||||
} else if (actionInContext.frame.selectorsChain && action.name !== 'navigate') {
|
||||
const locators = actionInContext.frame.selectorsChain.map(selector => '.' + asLocator(selector, 'frame_locator'));
|
||||
subject = `${pageAlias}${locators.join('')}`;
|
||||
} else if (actionInContext.frame.name) {
|
||||
subject = `${pageAlias}.frame(${formatOptions({ name: actionInContext.frame.name }, false)})`;
|
||||
} else {
|
||||
subject = `${pageAlias}.frame(${formatOptions({ url: actionInContext.frame.url }, false)})`;
|
||||
}
|
||||
|
||||
const signals = toSignalMap(action);
|
||||
|
||||
@ -114,26 +122,26 @@ export class PythonLanguageGenerator implements LanguageGenerator {
|
||||
options.clickCount = action.clickCount;
|
||||
if (action.position)
|
||||
options.position = action.position;
|
||||
const optionsString = formatOptions(options, true);
|
||||
return `${method}(${quote(action.selector)}${optionsString})`;
|
||||
const optionsString = formatOptions(options, false);
|
||||
return asLocator(action.selector) + `.${method}(${optionsString})`;
|
||||
}
|
||||
case 'check':
|
||||
return `check(${quote(action.selector)})`;
|
||||
return asLocator(action.selector) + `.check()`;
|
||||
case 'uncheck':
|
||||
return `uncheck(${quote(action.selector)})`;
|
||||
return asLocator(action.selector) + `.uncheck()`;
|
||||
case 'fill':
|
||||
return `fill(${quote(action.selector)}, ${quote(action.text)})`;
|
||||
return asLocator(action.selector) + `.fill(${quote(action.text)})`;
|
||||
case 'setInputFiles':
|
||||
return `set_input_files(${quote(action.selector)}, ${formatValue(action.files.length === 1 ? action.files[0] : action.files)})`;
|
||||
return asLocator(action.selector) + `.set_input_files(${formatValue(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)})`;
|
||||
return asLocator(action.selector) + `.press(${quote(shortcut)})`;
|
||||
}
|
||||
case 'navigate':
|
||||
return `goto(${quote(action.url)})`;
|
||||
case 'select':
|
||||
return `select_option(${quote(action.selector)}, ${formatValue(action.options.length === 1 ? action.options[0] : action.options)})`;
|
||||
return asLocator(action.selector) + `.select_option(${formatValue(action.options.length === 1 ? action.options[0] : action.options)})`;
|
||||
}
|
||||
}
|
||||
|
||||
@ -272,3 +280,12 @@ class PythonFormatter {
|
||||
function quote(text: string) {
|
||||
return escapeWithQuotes(text, '\"');
|
||||
}
|
||||
|
||||
function asLocator(selector: string, locatorFn = 'locator') {
|
||||
const match = selector.match(/(.*)\s+>>\s+nth=(\d+)$/);
|
||||
if (!match)
|
||||
return `${locatorFn}(${quote(selector)})`;
|
||||
if (+match[2] === 0)
|
||||
return `${locatorFn}(${quote(match[1])}).first`;
|
||||
return `${locatorFn}(${quote(match[1])}).nth(${match[2]})`;
|
||||
}
|
||||
|
||||
@ -121,6 +121,14 @@ export type DialogSignal = BaseSignal & {
|
||||
|
||||
export type Signal = NavigationSignal | PopupSignal | DownloadSignal | DialogSignal;
|
||||
|
||||
export type FrameDescription = {
|
||||
pageAlias: string;
|
||||
isMainFrame: boolean;
|
||||
url: string;
|
||||
name?: string;
|
||||
selectorsChain?: string[];
|
||||
};
|
||||
|
||||
export function actionTitle(action: Action): string {
|
||||
switch (action.name) {
|
||||
case 'openPage':
|
||||
|
||||
@ -48,13 +48,3 @@ export function toModifiers(modifiers: number): ('Alt' | 'Control' | 'Meta' | 'S
|
||||
result.push('Shift');
|
||||
return result;
|
||||
}
|
||||
|
||||
export function describeFrame(frame: Frame): { frameName?: string, frameUrl: string, isMainFrame: boolean } {
|
||||
const page = frame._page;
|
||||
if (page.mainFrame() === frame)
|
||||
return { isMainFrame: true, frameUrl: frame.url() };
|
||||
const frames = page.frames().filter(f => f.name() === frame.name());
|
||||
if (frames.length === 1 && frames[0] === frame)
|
||||
return { isMainFrame: false, frameUrl: frame.url(), frameName: frame.name() };
|
||||
return { isMainFrame: false, frameUrl: frame.url() };
|
||||
}
|
||||
|
||||
@ -18,7 +18,7 @@ import * as fs from 'fs';
|
||||
import * as actions from './recorder/recorderActions';
|
||||
import type * as channels from '../../protocol/channels';
|
||||
import { CodeGenerator, ActionInContext } from './recorder/codeGenerator';
|
||||
import { describeFrame, toClickOptions, toModifiers } from './recorder/utils';
|
||||
import { toClickOptions, toModifiers } from './recorder/utils';
|
||||
import { Page } from '../page';
|
||||
import { Frame } from '../frames';
|
||||
import { BrowserContext } from '../browserContext';
|
||||
@ -36,6 +36,7 @@ import { createGuid, isUnderTest, monotonicTime } from '../../utils/utils';
|
||||
import { metadataToCallLog } from './recorder/recorderUtils';
|
||||
import { Debugger } from './debugger';
|
||||
import { EventEmitter } from 'events';
|
||||
import { raceAgainstTimeout } from '../../utils/async';
|
||||
|
||||
type BindingSource = { frame: Frame, page: Page };
|
||||
|
||||
@ -380,16 +381,15 @@ class ContextRecorder extends EventEmitter {
|
||||
// First page is called page, others are called popup1, popup2, etc.
|
||||
const frame = page.mainFrame();
|
||||
page.on('close', () => {
|
||||
this._pageAliases.delete(page);
|
||||
this._generator.addAction({
|
||||
pageAlias,
|
||||
...describeFrame(page.mainFrame()),
|
||||
frame: this._describeMainFrame(page),
|
||||
committed: true,
|
||||
action: {
|
||||
name: 'closePage',
|
||||
signals: [],
|
||||
}
|
||||
});
|
||||
this._pageAliases.delete(page);
|
||||
});
|
||||
frame.on(Frame.Events.Navigation, () => this._onFrameNavigated(frame, page));
|
||||
page.on(Page.Events.Download, () => this._onDownload(page));
|
||||
@ -402,8 +402,7 @@ class ContextRecorder extends EventEmitter {
|
||||
this._onPopup(page.opener()!, page);
|
||||
} else {
|
||||
this._generator.addAction({
|
||||
pageAlias,
|
||||
...describeFrame(page.mainFrame()),
|
||||
frame: this._describeMainFrame(page),
|
||||
committed: true,
|
||||
action: {
|
||||
name: 'openPage',
|
||||
@ -422,14 +421,69 @@ class ContextRecorder extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
private _describeMainFrame(page: Page): actions.FrameDescription {
|
||||
return {
|
||||
pageAlias: this._pageAliases.get(page)!,
|
||||
isMainFrame: true,
|
||||
url: page.mainFrame().url(),
|
||||
};
|
||||
}
|
||||
|
||||
private async _describeFrame(frame: Frame): Promise<actions.FrameDescription> {
|
||||
const page = frame._page;
|
||||
const pageAlias = this._pageAliases.get(page)!;
|
||||
const chain: Frame[] = [];
|
||||
for (let ancestor: Frame | null = frame; ancestor; ancestor = ancestor.parentFrame())
|
||||
chain.push(ancestor);
|
||||
chain.reverse();
|
||||
|
||||
if (chain.length === 1)
|
||||
return this._describeMainFrame(page);
|
||||
|
||||
const hasUniqueName = page.frames().filter(f => f.name() === frame.name()).length === 1;
|
||||
const fallback: actions.FrameDescription = {
|
||||
pageAlias,
|
||||
isMainFrame: false,
|
||||
url: frame.url(),
|
||||
name: frame.name() && hasUniqueName ? frame.name() : undefined,
|
||||
};
|
||||
if (chain.length > 3)
|
||||
return fallback;
|
||||
|
||||
const selectorPromises: Promise<string | undefined>[] = [];
|
||||
for (let i = 0; i < chain.length - 1; i++)
|
||||
selectorPromises.push(this._findFrameSelector(chain[i + 1], chain[i]));
|
||||
|
||||
const result = await raceAgainstTimeout(() => Promise.all(selectorPromises), 2000);
|
||||
if (!result.timedOut && result.result.every(selector => !!selector)) {
|
||||
return {
|
||||
...fallback,
|
||||
selectorsChain: result.result as string[],
|
||||
};
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
private async _findFrameSelector(frame: Frame, parent: Frame): Promise<string | undefined> {
|
||||
try {
|
||||
const frameElement = await frame.frameElement();
|
||||
if (!frameElement)
|
||||
return;
|
||||
const utility = await parent._utilityContext();
|
||||
const injected = await utility.injectedScript();
|
||||
const selector = await injected.evaluate((injected, element) => injected.generateSelector(element as Element), frameElement);
|
||||
return selector;
|
||||
} catch (e) {
|
||||
}
|
||||
}
|
||||
|
||||
private async _performAction(frame: Frame, action: actions.Action) {
|
||||
// Commit last action so that no further signals are added to it.
|
||||
this._generator.commitLastAction();
|
||||
|
||||
const page = frame._page;
|
||||
const frameDescription = await this._describeFrame(frame);
|
||||
const actionInContext: ActionInContext = {
|
||||
pageAlias: this._pageAliases.get(page)!,
|
||||
...describeFrame(frame),
|
||||
frame: frameDescription,
|
||||
action
|
||||
};
|
||||
|
||||
@ -476,20 +530,20 @@ class ContextRecorder extends EventEmitter {
|
||||
const kActionTimeout = 5000;
|
||||
if (action.name === 'click') {
|
||||
const { options } = toClickOptions(action);
|
||||
await perform('click', { selector: action.selector }, callMetadata => frame.click(callMetadata, action.selector, { ...options, timeout: kActionTimeout }));
|
||||
await perform('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('+');
|
||||
await perform('press', { selector: action.selector, key: shortcut }, callMetadata => frame.press(callMetadata, action.selector, shortcut, { timeout: kActionTimeout }));
|
||||
await perform('press', { selector: action.selector, key: shortcut }, callMetadata => frame.press(callMetadata, action.selector, shortcut, { timeout: kActionTimeout, strict: true }));
|
||||
}
|
||||
if (action.name === 'check')
|
||||
await perform('check', { selector: action.selector }, callMetadata => frame.check(callMetadata, action.selector, { timeout: kActionTimeout }));
|
||||
await perform('check', { selector: action.selector }, callMetadata => frame.check(callMetadata, action.selector, { timeout: kActionTimeout, strict: true }));
|
||||
if (action.name === 'uncheck')
|
||||
await perform('uncheck', { selector: action.selector }, callMetadata => frame.uncheck(callMetadata, action.selector, { timeout: kActionTimeout }));
|
||||
await perform('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 }));
|
||||
await perform('selectOption', { selector: action.selector, values }, callMetadata => frame.selectOption(callMetadata, action.selector, [], values, { timeout: kActionTimeout }));
|
||||
await perform('selectOption', { selector: action.selector, values }, callMetadata => frame.selectOption(callMetadata, action.selector, [], values, { timeout: kActionTimeout, strict: true }));
|
||||
}
|
||||
}
|
||||
|
||||
@ -497,11 +551,12 @@ class ContextRecorder extends EventEmitter {
|
||||
// Commit last action so that no further signals are added to it.
|
||||
this._generator.commitLastAction();
|
||||
|
||||
this._generator.addAction({
|
||||
pageAlias: this._pageAliases.get(frame._page)!,
|
||||
...describeFrame(frame),
|
||||
const frameDescription = await this._describeFrame(frame);
|
||||
const actionInContext: ActionInContext = {
|
||||
frame: frameDescription,
|
||||
action
|
||||
});
|
||||
};
|
||||
this._generator.addAction(actionInContext);
|
||||
}
|
||||
|
||||
private _onFrameNavigated(frame: Frame, page: Page) {
|
||||
|
||||
@ -36,23 +36,23 @@ test.describe('cli codegen', () => {
|
||||
|
||||
expect(sources.get('JavaScript').text).toContain(`
|
||||
// Click text=Submit
|
||||
await page.click('text=Submit');`);
|
||||
await page.locator('text=Submit').click();`);
|
||||
|
||||
expect(sources.get('Python').text).toContain(`
|
||||
# Click text=Submit
|
||||
page.click("text=Submit")`);
|
||||
page.locator("text=Submit").click()`);
|
||||
|
||||
expect(sources.get('Python Async').text).toContain(`
|
||||
# Click text=Submit
|
||||
await page.click("text=Submit")`);
|
||||
await page.locator("text=Submit").click()`);
|
||||
|
||||
expect(sources.get('Java').text).toContain(`
|
||||
// Click text=Submit
|
||||
page.click("text=Submit");`);
|
||||
page.locator("text=Submit").click();`);
|
||||
|
||||
expect(sources.get('C#').text).toContain(`
|
||||
// Click text=Submit
|
||||
await page.ClickAsync("text=Submit");`);
|
||||
await page.Locator("text=Submit").ClickAsync();`);
|
||||
|
||||
expect(message.text()).toBe('click');
|
||||
});
|
||||
@ -84,7 +84,7 @@ test.describe('cli codegen', () => {
|
||||
|
||||
expect(sources.get('JavaScript').text).toContain(`
|
||||
// Click text=Submit
|
||||
await page.click('text=Submit');`);
|
||||
await page.locator('text=Submit').click();`);
|
||||
expect(message.text()).toBe('click');
|
||||
});
|
||||
|
||||
@ -115,7 +115,7 @@ test.describe('cli codegen', () => {
|
||||
|
||||
expect(sources.get('JavaScript').text).toContain(`
|
||||
// Click canvas
|
||||
await page.click('canvas', {
|
||||
await page.locator('canvas').click({
|
||||
position: {
|
||||
x: 250,
|
||||
y: 250
|
||||
@ -124,20 +124,20 @@ test.describe('cli codegen', () => {
|
||||
|
||||
expect(sources.get('Python').text).toContain(`
|
||||
# Click canvas
|
||||
page.click("canvas", position={"x":250,"y":250})`);
|
||||
page.locator("canvas").click(position={"x":250,"y":250})`);
|
||||
|
||||
expect(sources.get('Python Async').text).toContain(`
|
||||
# Click canvas
|
||||
await page.click("canvas", position={"x":250,"y":250})`);
|
||||
await page.locator("canvas").click(position={"x":250,"y":250})`);
|
||||
|
||||
expect(sources.get('Java').text).toContain(`
|
||||
// Click canvas
|
||||
page.click("canvas", new Page.ClickOptions()
|
||||
page.locator("canvas").click(new Locator.ClickOptions()
|
||||
.setPosition(250, 250));`);
|
||||
|
||||
expect(sources.get('C#').text).toContain(`
|
||||
// Click canvas
|
||||
await page.ClickAsync("canvas", new PageClickOptions
|
||||
await page.Locator("canvas").ClickAsync(new LocatorClickOptions
|
||||
{
|
||||
Position = new Position
|
||||
{
|
||||
@ -170,23 +170,23 @@ test.describe('cli codegen', () => {
|
||||
|
||||
expect(sources.get('JavaScript').text).toContain(`
|
||||
// Click text=Submit
|
||||
await page.click('text=Submit');`);
|
||||
await page.locator('text=Submit').click();`);
|
||||
|
||||
expect(sources.get('Python').text).toContain(`
|
||||
# Click text=Submit
|
||||
page.click("text=Submit")`);
|
||||
page.locator("text=Submit").click()`);
|
||||
|
||||
expect(sources.get('Python Async').text).toContain(`
|
||||
# Click text=Submit
|
||||
await page.click("text=Submit")`);
|
||||
await page.locator("text=Submit").click()`);
|
||||
|
||||
expect(sources.get('Java').text).toContain(`
|
||||
// Click text=Submit
|
||||
page.click("text=Submit");`);
|
||||
page.locator("text=Submit").click();`);
|
||||
|
||||
expect(sources.get('C#').text).toContain(`
|
||||
// Click text=Submit
|
||||
await page.ClickAsync("text=Submit");`);
|
||||
await page.Locator("text=Submit").ClickAsync();`);
|
||||
|
||||
expect(message.text()).toBe('click');
|
||||
});
|
||||
@ -221,7 +221,7 @@ test.describe('cli codegen', () => {
|
||||
]);
|
||||
expect(sources.get('JavaScript').text).toContain(`
|
||||
// Click text=Some long text here
|
||||
await page.click('text=Some long text here');`);
|
||||
await page.locator('text=Some long text here').click();`);
|
||||
expect(message.text()).toBe('click');
|
||||
});
|
||||
|
||||
@ -240,22 +240,22 @@ test.describe('cli codegen', () => {
|
||||
|
||||
expect(sources.get('JavaScript').text).toContain(`
|
||||
// Fill input[name="name"]
|
||||
await page.fill('input[name="name"]', 'John');`);
|
||||
await page.locator('input[name="name"]').fill('John');`);
|
||||
expect(sources.get('Java').text).toContain(`
|
||||
// Fill input[name="name"]
|
||||
page.fill("input[name=\\\"name\\\"]", "John");`);
|
||||
page.locator("input[name=\\\"name\\\"]").fill("John");`);
|
||||
|
||||
expect(sources.get('Python').text).toContain(`
|
||||
# Fill input[name="name"]
|
||||
page.fill(\"input[name=\\\"name\\\"]\", \"John\")`);
|
||||
page.locator(\"input[name=\\\"name\\\"]\").fill(\"John\")`);
|
||||
|
||||
expect(sources.get('Python Async').text).toContain(`
|
||||
# Fill input[name="name"]
|
||||
await page.fill(\"input[name=\\\"name\\\"]\", \"John\")`);
|
||||
await page.locator(\"input[name=\\\"name\\\"]\").fill(\"John\")`);
|
||||
|
||||
expect(sources.get('C#').text).toContain(`
|
||||
// Fill input[name="name"]
|
||||
await page.FillAsync(\"input[name=\\\"name\\\"]\", \"John\");`);
|
||||
await page.Locator(\"input[name=\\\"name\\\"]\").FillAsync(\"John\");`);
|
||||
|
||||
expect(message.text()).toBe('John');
|
||||
});
|
||||
@ -274,7 +274,7 @@ test.describe('cli codegen', () => {
|
||||
]);
|
||||
expect(sources.get('JavaScript').text).toContain(`
|
||||
// Fill textarea[name="name"]
|
||||
await page.fill('textarea[name="name"]', 'John');`);
|
||||
await page.locator('textarea[name="name"]').fill('John');`);
|
||||
expect(message.text()).toBe('John');
|
||||
});
|
||||
|
||||
@ -296,23 +296,23 @@ test.describe('cli codegen', () => {
|
||||
|
||||
expect(sources.get('JavaScript').text).toContain(`
|
||||
// Press Enter with modifiers
|
||||
await page.press('input[name="name"]', 'Shift+Enter');`);
|
||||
await page.locator('input[name="name"]').press('Shift+Enter');`);
|
||||
|
||||
expect(sources.get('Java').text).toContain(`
|
||||
// Press Enter with modifiers
|
||||
page.press("input[name=\\\"name\\\"]", "Shift+Enter");`);
|
||||
page.locator("input[name=\\\"name\\\"]").press("Shift+Enter");`);
|
||||
|
||||
expect(sources.get('Python').text).toContain(`
|
||||
# Press Enter with modifiers
|
||||
page.press(\"input[name=\\\"name\\\"]\", \"Shift+Enter\")`);
|
||||
page.locator(\"input[name=\\\"name\\\"]\").press(\"Shift+Enter\")`);
|
||||
|
||||
expect(sources.get('Python Async').text).toContain(`
|
||||
# Press Enter with modifiers
|
||||
await page.press(\"input[name=\\\"name\\\"]\", \"Shift+Enter\")`);
|
||||
await page.locator(\"input[name=\\\"name\\\"]\").press(\"Shift+Enter\")`);
|
||||
|
||||
expect(sources.get('C#').text).toContain(`
|
||||
// Press Enter with modifiers
|
||||
await page.PressAsync(\"input[name=\\\"name\\\"]\", \"Shift+Enter\");`);
|
||||
await page.Locator(\"input[name=\\\"name\\\"]\").PressAsync(\"Shift+Enter\");`);
|
||||
|
||||
expect(messages[0].text()).toBe('press');
|
||||
});
|
||||
@ -338,15 +338,15 @@ test.describe('cli codegen', () => {
|
||||
const text = recorder.sources().get('JavaScript').text;
|
||||
expect(text).toContain(`
|
||||
// Fill input[name="one"]
|
||||
await page.fill('input[name="one"]', 'foobar123');`);
|
||||
await page.locator('input[name="one"]').fill('foobar123');`);
|
||||
|
||||
expect(text).toContain(`
|
||||
// Press Tab
|
||||
await page.press('input[name="one"]', 'Tab');`);
|
||||
await page.locator('input[name="one"]').press('Tab');`);
|
||||
|
||||
expect(text).toContain(`
|
||||
// Fill input[name="two"]
|
||||
await page.fill('input[name="two"]', 'barfoo321');`);
|
||||
await page.locator('input[name="two"]').fill('barfoo321');`);
|
||||
});
|
||||
|
||||
test('should record ArrowDown', async ({ page, openRecorder }) => {
|
||||
@ -368,7 +368,7 @@ test.describe('cli codegen', () => {
|
||||
]);
|
||||
expect(sources.get('JavaScript').text).toContain(`
|
||||
// Press ArrowDown
|
||||
await page.press('input[name="name"]', 'ArrowDown');`);
|
||||
await page.locator('input[name="name"]').press('ArrowDown');`);
|
||||
expect(messages[0].text()).toBe('press:ArrowDown');
|
||||
});
|
||||
|
||||
@ -392,7 +392,7 @@ test.describe('cli codegen', () => {
|
||||
]);
|
||||
expect(sources.get('JavaScript').text).toContain(`
|
||||
// Press ArrowDown
|
||||
await page.press('input[name="name"]', 'ArrowDown');`);
|
||||
await page.locator('input[name="name"]').press('ArrowDown');`);
|
||||
expect(messages.length).toBe(2);
|
||||
expect(messages[0].text()).toBe('down:ArrowDown');
|
||||
expect(messages[1].text()).toBe('up:ArrowDown');
|
||||
@ -414,23 +414,23 @@ test.describe('cli codegen', () => {
|
||||
|
||||
expect(sources.get('JavaScript').text).toContain(`
|
||||
// Check input[name="accept"]
|
||||
await page.check('input[name="accept"]');`);
|
||||
await page.locator('input[name="accept"]').check();`);
|
||||
|
||||
expect(sources.get('Java').text).toContain(`
|
||||
// Check input[name="accept"]
|
||||
page.check("input[name=\\\"accept\\\"]");`);
|
||||
page.locator("input[name=\\\"accept\\\"]").check();`);
|
||||
|
||||
expect(sources.get('Python').text).toContain(`
|
||||
# Check input[name="accept"]
|
||||
page.check(\"input[name=\\\"accept\\\"]\")`);
|
||||
page.locator(\"input[name=\\\"accept\\\"]\").check()`);
|
||||
|
||||
expect(sources.get('Python Async').text).toContain(`
|
||||
# Check input[name="accept"]
|
||||
await page.check(\"input[name=\\\"accept\\\"]\")`);
|
||||
await page.locator(\"input[name=\\\"accept\\\"]\").check()`);
|
||||
|
||||
expect(sources.get('C#').text).toContain(`
|
||||
// Check input[name="accept"]
|
||||
await page.CheckAsync(\"input[name=\\\"accept\\\"]\");`);
|
||||
await page.Locator(\"input[name=\\\"accept\\\"]\").CheckAsync();`);
|
||||
|
||||
expect(message.text()).toBe('true');
|
||||
});
|
||||
@ -451,7 +451,7 @@ test.describe('cli codegen', () => {
|
||||
|
||||
expect(sources.get('JavaScript').text).toContain(`
|
||||
// Check input[name="accept"]
|
||||
await page.check('input[name="accept"]');`);
|
||||
await page.locator('input[name="accept"]').check();`);
|
||||
expect(message.text()).toBe('true');
|
||||
});
|
||||
|
||||
@ -471,23 +471,23 @@ test.describe('cli codegen', () => {
|
||||
|
||||
expect(sources.get('JavaScript').text).toContain(`
|
||||
// Uncheck input[name="accept"]
|
||||
await page.uncheck('input[name="accept"]');`);
|
||||
await page.locator('input[name="accept"]').uncheck();`);
|
||||
|
||||
expect(sources.get('Java').text).toContain(`
|
||||
// Uncheck input[name="accept"]
|
||||
page.uncheck("input[name=\\\"accept\\\"]");`);
|
||||
page.locator("input[name=\\\"accept\\\"]").uncheck();`);
|
||||
|
||||
expect(sources.get('Python').text).toContain(`
|
||||
# Uncheck input[name="accept"]
|
||||
page.uncheck(\"input[name=\\\"accept\\\"]\")`);
|
||||
page.locator(\"input[name=\\\"accept\\\"]\").uncheck()`);
|
||||
|
||||
expect(sources.get('Python Async').text).toContain(`
|
||||
# Uncheck input[name="accept"]
|
||||
await page.uncheck(\"input[name=\\\"accept\\\"]\")`);
|
||||
await page.locator(\"input[name=\\\"accept\\\"]\").uncheck()`);
|
||||
|
||||
expect(sources.get('C#').text).toContain(`
|
||||
// Uncheck input[name="accept"]
|
||||
await page.UncheckAsync(\"input[name=\\\"accept\\\"]\");`);
|
||||
await page.Locator(\"input[name=\\\"accept\\\"]\").UncheckAsync();`);
|
||||
|
||||
expect(message.text()).toBe('false');
|
||||
});
|
||||
@ -508,23 +508,23 @@ test.describe('cli codegen', () => {
|
||||
|
||||
expect(sources.get('JavaScript').text).toContain(`
|
||||
// Select 2
|
||||
await page.selectOption('select', '2');`);
|
||||
await page.locator('select').selectOption('2');`);
|
||||
|
||||
expect(sources.get('Java').text).toContain(`
|
||||
// Select 2
|
||||
page.selectOption("select", "2");`);
|
||||
page.locator("select").selectOption("2");`);
|
||||
|
||||
expect(sources.get('Python').text).toContain(`
|
||||
# Select 2
|
||||
page.select_option(\"select\", \"2\")`);
|
||||
page.locator(\"select\").select_option(\"2\")`);
|
||||
|
||||
expect(sources.get('Python Async').text).toContain(`
|
||||
# Select 2
|
||||
await page.select_option(\"select\", \"2\")`);
|
||||
await page.locator(\"select\").select_option(\"2\")`);
|
||||
|
||||
expect(sources.get('C#').text).toContain(`
|
||||
// Select 2
|
||||
await page.SelectOptionAsync(\"select\", new[] { \"2\" });`);
|
||||
await page.Locator(\"select\").SelectOptionAsync(new[] { \"2\" });`);
|
||||
|
||||
expect(message.text()).toBe('2');
|
||||
});
|
||||
@ -548,31 +548,31 @@ test.describe('cli codegen', () => {
|
||||
// Click text=link
|
||||
const [page1] = await Promise.all([
|
||||
page.waitForEvent('popup'),
|
||||
page.click('text=link')
|
||||
page.locator('text=link').click()
|
||||
]);`);
|
||||
|
||||
expect(sources.get('Java').text).toContain(`
|
||||
// Click text=link
|
||||
Page page1 = page.waitForPopup(() -> {
|
||||
page.click("text=link");
|
||||
page.locator("text=link").click();
|
||||
});`);
|
||||
|
||||
expect(sources.get('Python').text).toContain(`
|
||||
# Click text=link
|
||||
with page.expect_popup() as popup_info:
|
||||
page.click(\"text=link\")
|
||||
page.locator(\"text=link\").click()
|
||||
page1 = popup_info.value`);
|
||||
|
||||
expect(sources.get('Python Async').text).toContain(`
|
||||
# Click text=link
|
||||
async with page.expect_popup() as popup_info:
|
||||
await page.click(\"text=link\")
|
||||
await page.locator(\"text=link\").click()
|
||||
page1 = await popup_info.value`);
|
||||
|
||||
expect(sources.get('C#').text).toContain(`
|
||||
var page1 = await page.RunAndWaitForPopupAsync(async () =>
|
||||
{
|
||||
await page.ClickAsync(\"text=link\");
|
||||
await page.Locator(\"text=link\").ClickAsync();
|
||||
});`);
|
||||
|
||||
expect(popup.url()).toBe('about:blank');
|
||||
@ -593,32 +593,32 @@ test.describe('cli codegen', () => {
|
||||
|
||||
expect(sources.get('JavaScript').text).toContain(`
|
||||
// Click text=link
|
||||
await page.click('text=link');
|
||||
await page.locator('text=link').click();
|
||||
// assert.equal(page.url(), 'about:blank#foo');`);
|
||||
|
||||
expect(sources.get('Playwright Test').text).toContain(`
|
||||
// Click text=link
|
||||
await page.click('text=link');
|
||||
await page.locator('text=link').click();
|
||||
await expect(page).toHaveURL('about:blank#foo');`);
|
||||
|
||||
expect(sources.get('Java').text).toContain(`
|
||||
// Click text=link
|
||||
page.click("text=link");
|
||||
page.locator("text=link").click();
|
||||
// assert page.url().equals("about:blank#foo");`);
|
||||
|
||||
expect(sources.get('Python').text).toContain(`
|
||||
# Click text=link
|
||||
page.click(\"text=link\")
|
||||
page.locator(\"text=link\").click()
|
||||
# assert page.url == \"about:blank#foo\"`);
|
||||
|
||||
expect(sources.get('Python Async').text).toContain(`
|
||||
# Click text=link
|
||||
await page.click(\"text=link\")
|
||||
await page.locator(\"text=link\").click()
|
||||
# assert page.url == \"about:blank#foo\"`);
|
||||
|
||||
expect(sources.get('C#').text).toContain(`
|
||||
// Click text=link
|
||||
await page.ClickAsync(\"text=link\");
|
||||
await page.Locator(\"text=link\").ClickAsync();
|
||||
// Assert.AreEqual(\"about:blank#foo\", page.Url);`);
|
||||
|
||||
expect(page.url()).toContain('about:blank#foo');
|
||||
@ -643,33 +643,33 @@ test.describe('cli codegen', () => {
|
||||
// Click text=link
|
||||
await Promise.all([
|
||||
page.waitForNavigation(/*{ url: 'about:blank#foo' }*/),
|
||||
page.click('text=link')
|
||||
page.locator('text=link').click()
|
||||
]);`);
|
||||
|
||||
expect(sources.get('Java').text).toContain(`
|
||||
// Click text=link
|
||||
// page.waitForNavigation(new Page.WaitForNavigationOptions().setUrl("about:blank#foo"), () ->
|
||||
page.waitForNavigation(() -> {
|
||||
page.click("text=link");
|
||||
page.locator("text=link").click();
|
||||
});`);
|
||||
|
||||
expect(sources.get('Python').text).toContain(`
|
||||
# Click text=link
|
||||
# with page.expect_navigation(url=\"about:blank#foo\"):
|
||||
with page.expect_navigation():
|
||||
page.click(\"text=link\")`);
|
||||
page.locator(\"text=link\").click()`);
|
||||
|
||||
expect(sources.get('Python Async').text).toContain(`
|
||||
# Click text=link
|
||||
# async with page.expect_navigation(url=\"about:blank#foo\"):
|
||||
async with page.expect_navigation():
|
||||
await page.click(\"text=link\")`);
|
||||
await page.locator(\"text=link\").click()`);
|
||||
|
||||
expect(sources.get('C#').text).toContain(`
|
||||
// Click text=link
|
||||
await page.RunAndWaitForNavigationAsync(async () =>
|
||||
{
|
||||
await page.ClickAsync(\"text=link\");
|
||||
await page.Locator(\"text=link\").ClickAsync();
|
||||
}/*, new PageWaitForNavigationOptions
|
||||
{
|
||||
UrlString = \"about:blank#foo\"
|
||||
@ -688,8 +688,8 @@ test.describe('cli codegen', () => {
|
||||
await recorder.page.keyboard.insertText('@');
|
||||
await recorder.page.keyboard.type('example.com');
|
||||
await recorder.waitForOutput('JavaScript', 'example.com');
|
||||
expect(recorder.sources().get('JavaScript').text).not.toContain(`await page.press('input', 'AltGraph');`);
|
||||
expect(recorder.sources().get('JavaScript').text).toContain(`await page.fill('input', 'playwright@example.com');`);
|
||||
expect(recorder.sources().get('JavaScript').text).not.toContain(`await page.locator('input').press('AltGraph');`);
|
||||
expect(recorder.sources().get('JavaScript').text).toContain(`await page.locator('input').fill('playwright@example.com');`);
|
||||
});
|
||||
|
||||
test('should middle click', async ({ page, openRecorder, server }) => {
|
||||
@ -703,22 +703,22 @@ test.describe('cli codegen', () => {
|
||||
]);
|
||||
|
||||
expect(sources.get('JavaScript').text).toContain(`
|
||||
await page.click('text=Click me', {
|
||||
await page.locator('text=Click me').click({
|
||||
button: 'middle'
|
||||
});`);
|
||||
|
||||
expect(sources.get('Python').text).toContain(`
|
||||
page.click("text=Click me", button="middle")`);
|
||||
page.locator("text=Click me").click(button="middle")`);
|
||||
|
||||
expect(sources.get('Python Async').text).toContain(`
|
||||
await page.click("text=Click me", button="middle")`);
|
||||
await page.locator("text=Click me").click(button="middle")`);
|
||||
|
||||
expect(sources.get('Java').text).toContain(`
|
||||
page.click("text=Click me", new Page.ClickOptions()
|
||||
page.locator("text=Click me").click(new Locator.ClickOptions()
|
||||
.setButton(MouseButton.MIDDLE));`);
|
||||
|
||||
expect(sources.get('C#').text).toContain(`
|
||||
await page.ClickAsync("text=Click me", new PageClickOptions
|
||||
await page.Locator("text=Click me").ClickAsync(new LocatorClickOptions
|
||||
{
|
||||
Button = MouseButton.Middle,
|
||||
});`);
|
||||
|
||||
@ -133,23 +133,23 @@ test.describe('cli codegen', () => {
|
||||
|
||||
expect(sources.get('JavaScript').text).toContain(`
|
||||
// Upload file-to-upload.txt
|
||||
await page.setInputFiles('input[type="file"]', 'file-to-upload.txt');`);
|
||||
await page.locator('input[type="file"]').setInputFiles('file-to-upload.txt');`);
|
||||
|
||||
expect(sources.get('Java').text).toContain(`
|
||||
// Upload file-to-upload.txt
|
||||
page.setInputFiles("input[type=\\\"file\\\"]", Paths.get("file-to-upload.txt"));`);
|
||||
page.locator("input[type=\\\"file\\\"]").setInputFiles(Paths.get("file-to-upload.txt"));`);
|
||||
|
||||
expect(sources.get('Python').text).toContain(`
|
||||
# Upload file-to-upload.txt
|
||||
page.set_input_files(\"input[type=\\\"file\\\"]\", \"file-to-upload.txt\")`);
|
||||
page.locator(\"input[type=\\\"file\\\"]\").set_input_files(\"file-to-upload.txt\")`);
|
||||
|
||||
expect(sources.get('Python Async').text).toContain(`
|
||||
# Upload file-to-upload.txt
|
||||
await page.set_input_files(\"input[type=\\\"file\\\"]\", \"file-to-upload.txt\")`);
|
||||
await page.locator(\"input[type=\\\"file\\\"]\").set_input_files(\"file-to-upload.txt\")`);
|
||||
|
||||
expect(sources.get('C#').text).toContain(`
|
||||
// Upload file-to-upload.txt
|
||||
await page.SetInputFilesAsync(\"input[type=\\\"file\\\"]\", new[] { \"file-to-upload.txt\" });`);
|
||||
await page.Locator(\"input[type=\\\"file\\\"]\").SetInputFilesAsync(new[] { \"file-to-upload.txt\" });`);
|
||||
});
|
||||
|
||||
test('should upload multiple files', async ({ page, openRecorder, browserName, asset }) => {
|
||||
@ -170,23 +170,23 @@ test.describe('cli codegen', () => {
|
||||
|
||||
expect(sources.get('JavaScript').text).toContain(`
|
||||
// Upload file-to-upload.txt, file-to-upload-2.txt
|
||||
await page.setInputFiles('input[type=\"file\"]', ['file-to-upload.txt', 'file-to-upload-2.txt']);`);
|
||||
await page.locator('input[type=\"file\"]').setInputFiles(['file-to-upload.txt', 'file-to-upload-2.txt']);`);
|
||||
|
||||
expect(sources.get('Java').text).toContain(`
|
||||
// Upload file-to-upload.txt, file-to-upload-2.txt
|
||||
page.setInputFiles("input[type=\\\"file\\\"]", new Path[] {Paths.get("file-to-upload.txt"), Paths.get("file-to-upload-2.txt")});`);
|
||||
page.locator("input[type=\\\"file\\\"]").setInputFiles(new Path[] {Paths.get("file-to-upload.txt"), Paths.get("file-to-upload-2.txt")});`);
|
||||
|
||||
expect(sources.get('Python').text).toContain(`
|
||||
# Upload file-to-upload.txt, file-to-upload-2.txt
|
||||
page.set_input_files(\"input[type=\\\"file\\\"]\", [\"file-to-upload.txt\", \"file-to-upload-2.txt\"]`);
|
||||
page.locator(\"input[type=\\\"file\\\"]\").set_input_files([\"file-to-upload.txt\", \"file-to-upload-2.txt\"]`);
|
||||
|
||||
expect(sources.get('Python Async').text).toContain(`
|
||||
# Upload file-to-upload.txt, file-to-upload-2.txt
|
||||
await page.set_input_files(\"input[type=\\\"file\\\"]\", [\"file-to-upload.txt\", \"file-to-upload-2.txt\"]`);
|
||||
await page.locator(\"input[type=\\\"file\\\"]\").set_input_files([\"file-to-upload.txt\", \"file-to-upload-2.txt\"]`);
|
||||
|
||||
expect(sources.get('C#').text).toContain(`
|
||||
// Upload file-to-upload.txt, file-to-upload-2.txt
|
||||
await page.SetInputFilesAsync(\"input[type=\\\"file\\\"]\", new[] { \"file-to-upload.txt\", \"file-to-upload-2.txt\" });`);
|
||||
await page.Locator(\"input[type=\\\"file\\\"]\").SetInputFilesAsync(new[] { \"file-to-upload.txt\", \"file-to-upload-2.txt\" });`);
|
||||
});
|
||||
|
||||
test('should clear files', async ({ page, openRecorder, browserName, asset }) => {
|
||||
@ -207,23 +207,23 @@ test.describe('cli codegen', () => {
|
||||
|
||||
expect(sources.get('JavaScript').text).toContain(`
|
||||
// Clear selected files
|
||||
await page.setInputFiles('input[type=\"file\"]', []);`);
|
||||
await page.locator('input[type=\"file\"]').setInputFiles([]);`);
|
||||
|
||||
expect(sources.get('Java').text).toContain(`
|
||||
// Clear selected files
|
||||
page.setInputFiles("input[type=\\\"file\\\"]", new Path[0]);`);
|
||||
page.locator("input[type=\\\"file\\\"]").setInputFiles(new Path[0]);`);
|
||||
|
||||
expect(sources.get('Python').text).toContain(`
|
||||
# Clear selected files
|
||||
page.set_input_files(\"input[type=\\\"file\\\"]\", []`);
|
||||
page.locator(\"input[type=\\\"file\\\"]\").set_input_files([])`);
|
||||
|
||||
expect(sources.get('Python Async').text).toContain(`
|
||||
# Clear selected files
|
||||
await page.set_input_files(\"input[type=\\\"file\\\"]\", []`);
|
||||
await page.locator(\"input[type=\\\"file\\\"]\").set_input_files([])`);
|
||||
|
||||
expect(sources.get('C#').text).toContain(`
|
||||
// Clear selected files
|
||||
await page.SetInputFilesAsync(\"input[type=\\\"file\\\"]\", new[] { });`);
|
||||
await page.Locator(\"input[type=\\\"file\\\"]\").SetInputFilesAsync(new[] { });`);
|
||||
|
||||
});
|
||||
|
||||
@ -257,7 +257,7 @@ test.describe('cli codegen', () => {
|
||||
// Click text=Download
|
||||
const [download] = await Promise.all([
|
||||
page.waitForEvent('download'),
|
||||
page.click('text=Download')
|
||||
page.locator('text=Download').click()
|
||||
]);`);
|
||||
|
||||
expect(sources.get('Java').text).toContain(`
|
||||
@ -265,7 +265,7 @@ test.describe('cli codegen', () => {
|
||||
expect(sources.get('Java').text).toContain(`
|
||||
// Click text=Download
|
||||
Download download = page.waitForDownload(() -> {
|
||||
page.click("text=Download");
|
||||
page.locator("text=Download").click();
|
||||
});`);
|
||||
|
||||
expect(sources.get('Python').text).toContain(`
|
||||
@ -273,7 +273,7 @@ test.describe('cli codegen', () => {
|
||||
expect(sources.get('Python').text).toContain(`
|
||||
# Click text=Download
|
||||
with page.expect_download() as download_info:
|
||||
page.click(\"text=Download\")
|
||||
page.locator(\"text=Download\").click()
|
||||
download = download_info.value`);
|
||||
|
||||
expect(sources.get('Python Async').text).toContain(`
|
||||
@ -281,7 +281,7 @@ test.describe('cli codegen', () => {
|
||||
expect(sources.get('Python Async').text).toContain(`
|
||||
# Click text=Download
|
||||
async with page.expect_download() as download_info:
|
||||
await page.click(\"text=Download\")
|
||||
await page.locator(\"text=Download\").click()
|
||||
download = await download_info.value`);
|
||||
|
||||
expect(sources.get('C#').text).toContain(`
|
||||
@ -290,7 +290,7 @@ test.describe('cli codegen', () => {
|
||||
// Click text=Download
|
||||
var download1 = await page.RunAndWaitForDownloadAsync(async () =>
|
||||
{
|
||||
await page.ClickAsync(\"text=Download\");
|
||||
await page.Locator(\"text=Download\").ClickAsync();
|
||||
});`);
|
||||
});
|
||||
|
||||
@ -314,7 +314,7 @@ test.describe('cli codegen', () => {
|
||||
console.log(\`Dialog message: \${dialog.message()}\`);
|
||||
dialog.dismiss().catch(() => {});
|
||||
});
|
||||
await page.click('text=click me');`);
|
||||
await page.locator('text=click me').click();`);
|
||||
|
||||
expect(sources.get('Java').text).toContain(`
|
||||
// Click text=click me
|
||||
@ -322,17 +322,17 @@ test.describe('cli codegen', () => {
|
||||
System.out.println(String.format("Dialog message: %s", dialog.message()));
|
||||
dialog.dismiss();
|
||||
});
|
||||
page.click("text=click me");`);
|
||||
page.locator("text=click me").click();`);
|
||||
|
||||
expect(sources.get('Python').text).toContain(`
|
||||
# Click text=click me
|
||||
page.once(\"dialog\", lambda dialog: dialog.dismiss())
|
||||
page.click(\"text=click me\")`);
|
||||
page.locator(\"text=click me\").click()`);
|
||||
|
||||
expect(sources.get('Python Async').text).toContain(`
|
||||
# Click text=click me
|
||||
page.once(\"dialog\", lambda dialog: dialog.dismiss())
|
||||
await page.click(\"text=click me\")`);
|
||||
await page.locator(\"text=click me\").click()`);
|
||||
|
||||
expect(sources.get('C#').text).toContain(`
|
||||
// Click text=click me
|
||||
@ -343,7 +343,7 @@ test.describe('cli codegen', () => {
|
||||
page.Dialog -= page_Dialog1_EventHandler;
|
||||
}
|
||||
page.Dialog += page_Dialog1_EventHandler;
|
||||
await page.ClickAsync(\"text=click me\");`);
|
||||
await page.Locator(\"text=click me\").ClickAsync();`);
|
||||
|
||||
});
|
||||
|
||||
@ -393,7 +393,7 @@ test.describe('cli codegen', () => {
|
||||
// Click text=link
|
||||
const [page1] = await Promise.all([
|
||||
page.waitForEvent('popup'),
|
||||
page.click('text=link', {
|
||||
page.locator('text=link').click({
|
||||
modifiers: ['${platform === 'darwin' ? 'Meta' : 'Control'}']
|
||||
})
|
||||
]);`);
|
||||
@ -423,20 +423,20 @@ test.describe('cli codegen', () => {
|
||||
await recorder.waitForOutput('JavaScript', 'TextB');
|
||||
|
||||
const sources = recorder.sources();
|
||||
expect(sources.get('JavaScript').text).toContain(`await page1.fill('input', 'TextA');`);
|
||||
expect(sources.get('JavaScript').text).toContain(`await page2.fill('input', 'TextB');`);
|
||||
expect(sources.get('JavaScript').text).toContain(`await page1.locator('input').fill('TextA');`);
|
||||
expect(sources.get('JavaScript').text).toContain(`await page2.locator('input').fill('TextB');`);
|
||||
|
||||
expect(sources.get('Java').text).toContain(`page1.fill("input", "TextA");`);
|
||||
expect(sources.get('Java').text).toContain(`page2.fill("input", "TextB");`);
|
||||
expect(sources.get('Java').text).toContain(`page1.locator("input").fill("TextA");`);
|
||||
expect(sources.get('Java').text).toContain(`page2.locator("input").fill("TextB");`);
|
||||
|
||||
expect(sources.get('Python').text).toContain(`page1.fill(\"input\", \"TextA\")`);
|
||||
expect(sources.get('Python').text).toContain(`page2.fill(\"input\", \"TextB\")`);
|
||||
expect(sources.get('Python').text).toContain(`page1.locator(\"input\").fill(\"TextA\")`);
|
||||
expect(sources.get('Python').text).toContain(`page2.locator(\"input\").fill(\"TextB\")`);
|
||||
|
||||
expect(sources.get('Python Async').text).toContain(`await page1.fill(\"input\", \"TextA\")`);
|
||||
expect(sources.get('Python Async').text).toContain(`await page2.fill(\"input\", \"TextB\")`);
|
||||
expect(sources.get('Python Async').text).toContain(`await page1.locator(\"input\").fill(\"TextA\")`);
|
||||
expect(sources.get('Python Async').text).toContain(`await page2.locator(\"input\").fill(\"TextB\")`);
|
||||
|
||||
expect(sources.get('C#').text).toContain(`await page1.FillAsync(\"input\", \"TextA\");`);
|
||||
expect(sources.get('C#').text).toContain(`await page2.FillAsync(\"input\", \"TextB\");`);
|
||||
expect(sources.get('C#').text).toContain(`await page1.Locator(\"input\").FillAsync(\"TextA\");`);
|
||||
expect(sources.get('C#').text).toContain(`await page2.Locator(\"input\").FillAsync(\"TextB\");`);
|
||||
});
|
||||
|
||||
test('click should emit events in order', async ({ page, openRecorder }) => {
|
||||
@ -458,7 +458,7 @@ test.describe('cli codegen', () => {
|
||||
});
|
||||
await Promise.all([
|
||||
page.click('button'),
|
||||
recorder.waitForOutput('JavaScript', 'page.click')
|
||||
recorder.waitForOutput('JavaScript', '.click(')
|
||||
]);
|
||||
expect(messages).toEqual(['mousedown', 'mouseup', 'click']);
|
||||
});
|
||||
@ -496,99 +496,6 @@ test.describe('cli codegen', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
test('should prefer frame name', async ({ page, openRecorder, server }) => {
|
||||
const recorder = await openRecorder();
|
||||
await recorder.setContentAndWait(`
|
||||
<iframe src='./frames/frame.html' name='one'></iframe>
|
||||
<iframe src='./frames/frame.html' name='two'></iframe>
|
||||
<iframe src='./frames/frame.html'></iframe>
|
||||
`, server.EMPTY_PAGE, 4);
|
||||
const frameOne = page.frame({ name: 'one' });
|
||||
const frameTwo = page.frame({ name: 'two' });
|
||||
const otherFrame = page.frames().find(f => f !== page.mainFrame() && !f.name());
|
||||
|
||||
let [sources] = await Promise.all([
|
||||
recorder.waitForOutput('JavaScript', 'one'),
|
||||
frameOne.click('div'),
|
||||
]);
|
||||
|
||||
expect(sources.get('JavaScript').text).toContain(`
|
||||
// Click text=Hi, I'm frame
|
||||
await page.frame({
|
||||
name: 'one'
|
||||
}).click('text=Hi, I\\'m frame');`);
|
||||
|
||||
expect(sources.get('Java').text).toContain(`
|
||||
// Click text=Hi, I'm frame
|
||||
page.frame("one").click("text=Hi, I'm frame");`);
|
||||
|
||||
expect(sources.get('Python').text).toContain(`
|
||||
# Click text=Hi, I'm frame
|
||||
page.frame(name=\"one\").click(\"text=Hi, I'm frame\")`);
|
||||
|
||||
expect(sources.get('Python Async').text).toContain(`
|
||||
# Click text=Hi, I'm frame
|
||||
await page.frame(name=\"one\").click(\"text=Hi, I'm frame\")`);
|
||||
|
||||
expect(sources.get('C#').text).toContain(`
|
||||
// Click text=Hi, I'm frame
|
||||
await page.Frame(\"one\").ClickAsync(\"text=Hi, I'm frame\");`);
|
||||
|
||||
[sources] = await Promise.all([
|
||||
recorder.waitForOutput('JavaScript', 'two'),
|
||||
frameTwo.click('div'),
|
||||
]);
|
||||
|
||||
expect(sources.get('JavaScript').text).toContain(`
|
||||
// Click text=Hi, I'm frame
|
||||
await page.frame({
|
||||
name: 'two'
|
||||
}).click('text=Hi, I\\'m frame');`);
|
||||
|
||||
expect(sources.get('Java').text).toContain(`
|
||||
// Click text=Hi, I'm frame
|
||||
page.frame("two").click("text=Hi, I'm frame");`);
|
||||
|
||||
expect(sources.get('Python').text).toContain(`
|
||||
# Click text=Hi, I'm frame
|
||||
page.frame(name=\"two\").click(\"text=Hi, I'm frame\")`);
|
||||
|
||||
expect(sources.get('Python Async').text).toContain(`
|
||||
# Click text=Hi, I'm frame
|
||||
await page.frame(name=\"two\").click(\"text=Hi, I'm frame\")`);
|
||||
|
||||
expect(sources.get('C#').text).toContain(`
|
||||
// Click text=Hi, I'm frame
|
||||
await page.Frame(\"two\").ClickAsync(\"text=Hi, I'm frame\");`);
|
||||
|
||||
[sources] = await Promise.all([
|
||||
recorder.waitForOutput('JavaScript', 'url: \''),
|
||||
otherFrame.click('div'),
|
||||
]);
|
||||
|
||||
expect(sources.get('JavaScript').text).toContain(`
|
||||
// Click text=Hi, I'm frame
|
||||
await page.frame({
|
||||
url: 'http://localhost:${server.PORT}/frames/frame.html'
|
||||
}).click('text=Hi, I\\'m frame');`);
|
||||
|
||||
expect(sources.get('Java').text).toContain(`
|
||||
// Click text=Hi, I'm frame
|
||||
page.frameByUrl("http://localhost:${server.PORT}/frames/frame.html").click("text=Hi, I'm frame");`);
|
||||
|
||||
expect(sources.get('Python').text).toContain(`
|
||||
# Click text=Hi, I'm frame
|
||||
page.frame(url=\"http://localhost:${server.PORT}/frames/frame.html\").click(\"text=Hi, I'm frame\")`);
|
||||
|
||||
expect(sources.get('Python Async').text).toContain(`
|
||||
# Click text=Hi, I'm frame
|
||||
await page.frame(url=\"http://localhost:${server.PORT}/frames/frame.html\").click(\"text=Hi, I'm frame\")`);
|
||||
|
||||
expect(sources.get('C#').text).toContain(`
|
||||
// Click text=Hi, I'm frame
|
||||
await page.FrameByUrl(\"http://localhost:${server.PORT}/frames/frame.html\").ClickAsync(\"text=Hi, I'm frame\");`);
|
||||
});
|
||||
|
||||
test('should record navigations after identical pushState', async ({ page, openRecorder, server }) => {
|
||||
const recorder = await openRecorder();
|
||||
server.setRoute('/page2.html', (req, res) => {
|
||||
@ -655,22 +562,23 @@ test.describe('cli codegen', () => {
|
||||
|
||||
expect(sources.get('JavaScript').text).toContain(`
|
||||
// Fill textarea[name="name"]
|
||||
await page.fill('textarea[name="name"]', 'Hello\\'"\`\\nWorld');`);
|
||||
await page.locator('textarea[name="name"]').fill('Hello\\'"\`\\nWorld');`);
|
||||
|
||||
expect(sources.get('Java').text).toContain(`
|
||||
// Fill textarea[name="name"]
|
||||
page.fill("textarea[name=\\\"name\\\"]", "Hello'\\"\`\\nWorld");`);
|
||||
page.locator("textarea[name=\\\"name\\\"]").fill("Hello'\\"\`\\nWorld");`);
|
||||
|
||||
expect(sources.get('Python').text).toContain(`
|
||||
# Fill textarea[name="name"]
|
||||
page.fill(\"textarea[name=\\\"name\\\"]\", \"Hello'\\"\`\\nWorld\")`);
|
||||
page.locator(\"textarea[name=\\\"name\\\"]\").fill(\"Hello'\\"\`\\nWorld\")`);
|
||||
|
||||
expect(sources.get('Python Async').text).toContain(`
|
||||
# Fill textarea[name="name"]
|
||||
await page.fill(\"textarea[name=\\\"name\\\"]\", \"Hello'\\"\`\\nWorld\")`);
|
||||
await page.locator(\"textarea[name=\\\"name\\\"]\").fill(\"Hello'\\"\`\\nWorld\")`);
|
||||
|
||||
expect(sources.get('C#').text).toContain(`
|
||||
// Fill textarea[name="name"]
|
||||
await page.FillAsync(\"textarea[name=\\\"name\\\"]\", \"Hello'\\"\`\\nWorld\");`);
|
||||
await page.Locator(\"textarea[name=\\\"name\\\"]\").FillAsync(\"Hello'\\"\`\\nWorld\");`);
|
||||
|
||||
expect(message.text()).toBe('Hello\'\"\`\nWorld');
|
||||
});
|
||||
|
||||
235
tests/inspector/cli-codegen-3.spec.ts
Normal file
235
tests/inspector/cli-codegen-3.spec.ts
Normal file
@ -0,0 +1,235 @@
|
||||
/**
|
||||
* 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 { test, expect } from './inspectorTest';
|
||||
|
||||
test.describe('cli codegen', () => {
|
||||
test.skip(({ mode }) => mode !== 'default');
|
||||
|
||||
test('should click locator.first', async ({ page, openRecorder }) => {
|
||||
const recorder = await openRecorder();
|
||||
|
||||
await recorder.setContentAndWait(`
|
||||
<button onclick="console.log('click1')">Submit</button>
|
||||
<button onclick="console.log('click2')">Submit</button>
|
||||
`);
|
||||
|
||||
const selector = await recorder.hoverOverElement('button');
|
||||
expect(selector).toBe('text=Submit >> nth=0');
|
||||
|
||||
const [message, sources] = await Promise.all([
|
||||
page.waitForEvent('console', msg => msg.type() !== 'error'),
|
||||
recorder.waitForOutput('JavaScript', 'click'),
|
||||
page.dispatchEvent('button', 'click', { detail: 1 })
|
||||
]);
|
||||
|
||||
expect(sources.get('JavaScript').text).toContain(`
|
||||
// Click text=Submit >> nth=0
|
||||
await page.locator('text=Submit').first().click();`);
|
||||
|
||||
expect(sources.get('Python').text).toContain(`
|
||||
# Click text=Submit >> nth=0
|
||||
page.locator("text=Submit").first.click()`);
|
||||
|
||||
expect(sources.get('Python Async').text).toContain(`
|
||||
# Click text=Submit >> nth=0
|
||||
await page.locator("text=Submit").first.click()`);
|
||||
|
||||
expect(sources.get('Java').text).toContain(`
|
||||
// Click text=Submit >> nth=0
|
||||
page.locator("text=Submit").first().click();`);
|
||||
|
||||
expect(sources.get('C#').text).toContain(`
|
||||
// Click text=Submit >> nth=0
|
||||
await page.Locator("text=Submit").First.ClickAsync();`);
|
||||
|
||||
expect(message.text()).toBe('click1');
|
||||
});
|
||||
|
||||
test('should click locator.nth', async ({ page, openRecorder }) => {
|
||||
const recorder = await openRecorder();
|
||||
|
||||
await recorder.setContentAndWait(`
|
||||
<button onclick="console.log('click1')">Submit</button>
|
||||
<button onclick="console.log('click2')">Submit</button>
|
||||
`);
|
||||
|
||||
const selector = await recorder.hoverOverElement('button >> nth=1');
|
||||
expect(selector).toBe('text=Submit >> nth=1');
|
||||
|
||||
const [message, sources] = await Promise.all([
|
||||
page.waitForEvent('console', msg => msg.type() !== 'error'),
|
||||
recorder.waitForOutput('JavaScript', 'click'),
|
||||
page.dispatchEvent('button', 'click', { detail: 1 })
|
||||
]);
|
||||
|
||||
expect(sources.get('JavaScript').text).toContain(`
|
||||
// Click text=Submit >> nth=1
|
||||
await page.locator('text=Submit').nth(1).click();`);
|
||||
|
||||
expect(sources.get('Python').text).toContain(`
|
||||
# Click text=Submit >> nth=1
|
||||
page.locator("text=Submit").nth(1).click()`);
|
||||
|
||||
expect(sources.get('Python Async').text).toContain(`
|
||||
# Click text=Submit >> nth=1
|
||||
await page.locator("text=Submit").nth(1).click()`);
|
||||
|
||||
expect(sources.get('Java').text).toContain(`
|
||||
// Click text=Submit >> nth=1
|
||||
page.locator("text=Submit").nth(1).click();`);
|
||||
|
||||
expect(sources.get('C#').text).toContain(`
|
||||
// Click text=Submit >> nth=1
|
||||
await page.Locator("text=Submit").Nth(1).ClickAsync();`);
|
||||
|
||||
expect(message.text()).toBe('click2');
|
||||
});
|
||||
|
||||
test('should generate frame locators', async ({ page, openRecorder, server }) => {
|
||||
const recorder = await openRecorder();
|
||||
/*
|
||||
iframe
|
||||
div Hello1
|
||||
iframe
|
||||
div Hello2
|
||||
iframe[name=one]
|
||||
div HelloNameOne
|
||||
iframe[name=two]
|
||||
dev HelloNameTwo
|
||||
iframe
|
||||
dev HelloAnonymous
|
||||
*/
|
||||
await recorder.setContentAndWait(`
|
||||
<iframe id=frame1 srcdoc="<div>Hello1</div><iframe srcdoc='<div>Hello2</div><iframe name=one></iframe><iframe name=two></iframe><iframe></iframe>'>">
|
||||
`, server.EMPTY_PAGE, 6);
|
||||
const frameHello1 = page.mainFrame().childFrames()[0];
|
||||
const frameHello2 = frameHello1.childFrames()[0];
|
||||
const frameOne = page.frame({ name: 'one' });
|
||||
await frameOne.setContent(`<div>HelloNameOne</div>`);
|
||||
const frameTwo = page.frame({ name: 'two' });
|
||||
await frameTwo.setContent(`<div>HelloNameTwo</div>`);
|
||||
const frameAnonymous = frameHello2.childFrames().find(f => !f.name());
|
||||
await frameAnonymous.setContent(`<div>HelloNameAnonymous</div>`);
|
||||
|
||||
let [sources] = await Promise.all([
|
||||
recorder.waitForOutput('JavaScript', 'Hello1'),
|
||||
frameHello1.click('text=Hello1'),
|
||||
]);
|
||||
|
||||
expect(sources.get('JavaScript').text).toContain(`
|
||||
// Click text=Hello1
|
||||
await page.frameLocator('#frame1').locator('text=Hello1').click();`);
|
||||
|
||||
expect(sources.get('Java').text).toContain(`
|
||||
// Click text=Hello1
|
||||
page.frameLocator("#frame1").locator("text=Hello1").click();`);
|
||||
|
||||
expect(sources.get('Python').text).toContain(`
|
||||
# Click text=Hello1
|
||||
page.frame_locator("#frame1").locator("text=Hello1").click()`);
|
||||
|
||||
expect(sources.get('Python Async').text).toContain(`
|
||||
# Click text=Hello1
|
||||
await page.frame_locator("#frame1").locator("text=Hello1").click()`);
|
||||
|
||||
expect(sources.get('C#').text).toContain(`
|
||||
// Click text=Hello1
|
||||
await page.FrameLocator("#frame1").Locator("text=Hello1").ClickAsync();`);
|
||||
|
||||
|
||||
[sources] = await Promise.all([
|
||||
recorder.waitForOutput('JavaScript', 'Hello2'),
|
||||
frameHello2.click('text=Hello2'),
|
||||
]);
|
||||
|
||||
expect(sources.get('JavaScript').text).toContain(`
|
||||
// Click text=Hello2
|
||||
await page.frameLocator('#frame1').frameLocator('iframe').locator('text=Hello2').click();`);
|
||||
|
||||
expect(sources.get('Java').text).toContain(`
|
||||
// Click text=Hello2
|
||||
page.frameLocator("#frame1").frameLocator("iframe").locator("text=Hello2").click();`);
|
||||
|
||||
expect(sources.get('Python').text).toContain(`
|
||||
# Click text=Hello2
|
||||
page.frame_locator("#frame1").frame_locator("iframe").locator("text=Hello2").click()`);
|
||||
|
||||
expect(sources.get('Python Async').text).toContain(`
|
||||
# Click text=Hello2
|
||||
await page.frame_locator("#frame1").frame_locator("iframe").locator("text=Hello2").click()`);
|
||||
|
||||
expect(sources.get('C#').text).toContain(`
|
||||
// Click text=Hello2
|
||||
await page.FrameLocator("#frame1").FrameLocator("iframe").Locator("text=Hello2").ClickAsync();`);
|
||||
|
||||
|
||||
[sources] = await Promise.all([
|
||||
recorder.waitForOutput('JavaScript', 'one'),
|
||||
frameOne.click('text=HelloNameOne'),
|
||||
]);
|
||||
|
||||
expect(sources.get('JavaScript').text).toContain(`
|
||||
// Click text=HelloNameOne
|
||||
await page.frame({
|
||||
name: 'one'
|
||||
}).locator('text=HelloNameOne').click();`);
|
||||
|
||||
expect(sources.get('Java').text).toContain(`
|
||||
// Click text=HelloNameOne
|
||||
page.frame("one").locator("text=HelloNameOne").click();`);
|
||||
|
||||
expect(sources.get('Python').text).toContain(`
|
||||
# Click text=HelloNameOne
|
||||
page.frame(name=\"one\").locator(\"text=HelloNameOne\").click()`);
|
||||
|
||||
expect(sources.get('Python Async').text).toContain(`
|
||||
# Click text=HelloNameOne
|
||||
await page.frame(name=\"one\").locator(\"text=HelloNameOne\").click()`);
|
||||
|
||||
expect(sources.get('C#').text).toContain(`
|
||||
// Click text=HelloNameOne
|
||||
await page.Frame(\"one\").Locator(\"text=HelloNameOne\").ClickAsync();`);
|
||||
|
||||
|
||||
[sources] = await Promise.all([
|
||||
recorder.waitForOutput('JavaScript', 'url:'),
|
||||
frameAnonymous.click('text=HelloNameAnonymous'),
|
||||
]);
|
||||
|
||||
expect(sources.get('JavaScript').text).toContain(`
|
||||
// Click text=HelloNameAnonymous
|
||||
await page.frame({
|
||||
url: 'about:blank'
|
||||
}).locator('text=HelloNameAnonymous').click();`);
|
||||
|
||||
expect(sources.get('Java').text).toContain(`
|
||||
// Click text=HelloNameAnonymous
|
||||
page.frameByUrl("about:blank").locator("text=HelloNameAnonymous").click();`);
|
||||
|
||||
expect(sources.get('Python').text).toContain(`
|
||||
# Click text=HelloNameAnonymous
|
||||
page.frame(url=\"about:blank\").locator(\"text=HelloNameAnonymous\").click()`);
|
||||
|
||||
expect(sources.get('Python Async').text).toContain(`
|
||||
# Click text=HelloNameAnonymous
|
||||
await page.frame(url=\"about:blank\").locator(\"text=HelloNameAnonymous\").click()`);
|
||||
|
||||
expect(sources.get('C#').text).toContain(`
|
||||
// Click text=HelloNameAnonymous
|
||||
await page.FrameByUrl(\"about:blank\").Locator(\"text=HelloNameAnonymous\").ClickAsync();`);
|
||||
});
|
||||
});
|
||||
@ -115,6 +115,7 @@ it.describe('pause', () => {
|
||||
});
|
||||
|
||||
it('should highlight pointer', async ({ page, recorderPageGetter }) => {
|
||||
const actionPointPromise = waitForTestLog<{ x: number, y: number }>(page, 'Action point for test: ');
|
||||
await page.setContent('<button>Submit</button>');
|
||||
const scriptPromise = (async () => {
|
||||
await page.pause();
|
||||
@ -123,18 +124,15 @@ it.describe('pause', () => {
|
||||
const recorderPage = await recorderPageGetter();
|
||||
await recorderPage.click('[title="Step over"]');
|
||||
|
||||
const point = await page.waitForSelector('x-pw-action-point');
|
||||
const { x, y } = await actionPointPromise;
|
||||
const button = await page.waitForSelector('button');
|
||||
const box1 = await button.boundingBox();
|
||||
const box2 = await point.boundingBox();
|
||||
|
||||
const x1 = box1.x + box1.width / 2;
|
||||
const y1 = box1.y + box1.height / 2;
|
||||
const x2 = box2.x + box2.width / 2;
|
||||
const y2 = box2.y + box2.height / 2;
|
||||
|
||||
expect(Math.abs(x1 - x2) < 2).toBeTruthy();
|
||||
expect(Math.abs(y1 - y2) < 2).toBeTruthy();
|
||||
expect(Math.abs(x1 - x) < 2).toBeTruthy();
|
||||
expect(Math.abs(y1 - y) < 2).toBeTruthy();
|
||||
|
||||
await recorderPage.click('[title=Resume]');
|
||||
await scriptPromise;
|
||||
@ -331,14 +329,13 @@ it.describe('pause', () => {
|
||||
await page.pause();
|
||||
})();
|
||||
const recorderPage = await recorderPageGetter();
|
||||
const [element] = await Promise.all([
|
||||
page.waitForSelector('x-pw-highlight:visible'),
|
||||
const [box1] = await Promise.all([
|
||||
waitForTestLog<Box>(page, 'Highlight box for test: '),
|
||||
recorderPage.fill('input[placeholder="Playwright Selector"]', 'text=Submit'),
|
||||
]);
|
||||
const button = await page.$('text=Submit');
|
||||
const box1 = await element.boundingBox();
|
||||
const box2 = await button.boundingBox();
|
||||
expect(box1).toEqual(box2);
|
||||
expect(roundBox(box1)).toEqual(roundBox(box2));
|
||||
await recorderPage.click('[title=Resume]');
|
||||
await scriptPromise;
|
||||
});
|
||||
@ -395,3 +392,25 @@ async function sanitizeLog(recorderPage: Page): Promise<string[]> {
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
function waitForTestLog<T>(page: Page, prefix: string): Promise<T> {
|
||||
return new Promise<T>(resolve => {
|
||||
page.on('console', message => {
|
||||
const text = message.text();
|
||||
if (text.startsWith(prefix)) {
|
||||
const json = text.substring(prefix.length);
|
||||
resolve(JSON.parse(json));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
type Box = { x: number, y: number, width: number, height: number };
|
||||
function roundBox(box: Box): Box {
|
||||
return {
|
||||
x: Math.round(box.x * 1000),
|
||||
y: Math.round(box.y * 1000),
|
||||
width: Math.round(box.width * 1000),
|
||||
height: Math.round(box.height * 1000),
|
||||
};
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user