feat(codegen): generate locators and frame locators (#11873)

This commit is contained in:
Dmitry Gozman 2022-02-04 19:27:45 -08:00 committed by GitHub
parent 46dfa45b4e
commit f82e09be04
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 611 additions and 320 deletions

View File

@ -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) {

View File

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

View File

@ -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',

View File

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

View File

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

View File

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

View File

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

View File

@ -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':

View File

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

View File

@ -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) {

View File

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

View File

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

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

View File

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