Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions packages/injected/src/recorder/pollingRecorder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import type * as actions from '@isomorphic/codegen/actions';
import type { ElementInfo, Mode, OverlayState, UIState } from '@recorder/recorderTypes';

interface Embedder {
__pw_recorderPerformAction(action: actions.PerformOnRecordAction): Promise<void>;
__pw_recorderPerformAction(action: actions.PerformOnRecordAction, preconditionSelector?: string): Promise<void>;
__pw_recorderRecordAction(action: actions.Action): Promise<void>;
__pw_recorderState(): Promise<UIState>;
__pw_recorderElementPicked(element: { selector: string, ariaSnapshot?: string }): Promise<void>;
Expand Down Expand Up @@ -76,8 +76,8 @@ export class PollingRecorder implements RecorderDelegate {
this._pollRecorderModeTimer = this._recorder.injectedScript.utils.builtins.setTimeout(() => this._pollRecorderMode(), pollPeriod);
}

async performAction(action: actions.PerformOnRecordAction) {
await this._embedder.__pw_recorderPerformAction(action);
async performAction(action: actions.PerformOnRecordAction, preconditionSelector?: string) {
await this._embedder.__pw_recorderPerformAction(action, preconditionSelector);
}

async recordAction(action: actions.Action): Promise<void> {
Expand Down
11 changes: 6 additions & 5 deletions packages/injected/src/recorder/recorder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ const HighlightColors = {
};

export interface RecorderDelegate {
performAction?(action: actions.PerformOnRecordAction): Promise<void>;
performAction?(action: actions.PerformOnRecordAction, preconditionSelector?: string): Promise<void>;
recordAction?(action: actions.Action): Promise<void>;
elementPicked?(elementInfo: ElementInfo): Promise<void>;
setMode?(mode: Mode): Promise<void>;
Expand Down Expand Up @@ -1698,13 +1698,14 @@ export class Recorder {
async performAction(action: actions.PerformOnRecordAction) {
const previousSnapshot = this._lastActionAutoexpectSnapshot;
this._lastActionAutoexpectSnapshot = this._captureAutoExpectSnapshot();
let preconditionSelector: string | undefined;
if (!isAssertAction(action) && this._lastActionAutoexpectSnapshot) {
const element = this.injectedScript.utils.findNewElement(previousSnapshot?.root, this._lastActionAutoexpectSnapshot?.root);
action.preconditionSelector = element ? this.injectedScript.generateSelector(element, { testIdAttributeName: this.state.testIdAttributeName }).selector : undefined;
if (action.preconditionSelector === action.selector)
action.preconditionSelector = undefined;
preconditionSelector = element ? this.injectedScript.generateSelector(element, { testIdAttributeName: this.state.testIdAttributeName }).selector : undefined;
if (preconditionSelector === action.selector)
preconditionSelector = undefined;
}
await this._delegate.performAction?.(action).catch(() => {});
await this._delegate.performAction?.(action, preconditionSelector).catch(() => {});
}

async recordAction(action: actions.Action) {
Expand Down
9 changes: 7 additions & 2 deletions packages/isomorphic/codegen/actions.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ export type ActionBase = {
name: ActionName,
signals: Signal[],
ariaSnapshot?: string,
preconditionSelector?: string,
};

export type ActionWithSelector = ActionBase & {
Expand Down Expand Up @@ -156,7 +155,13 @@ export type DialogSignal = BaseSignal & {
dialogAlias: string,
};

export type Signal = NavigationSignal | PopupSignal | DownloadSignal | DialogSignal;
// An element that appeared since the previous action, asserted before this action runs.
export type ExpectSignal = BaseSignal & {
name: 'expect',
selector: string,
};

export type Signal = NavigationSignal | PopupSignal | DownloadSignal | DialogSignal | ExpectSignal;

export type FrameDescription = {
pageGuid: string;
Expand Down
11 changes: 7 additions & 4 deletions packages/isomorphic/codegen/csharp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

import { asLocator } from '../locatorGenerators';
import { escapeWithQuotes } from '../stringUtils';
import { sanitizeDeviceOptions, toClickOptionsForSourceCode, toKeyboardModifiers, toSignalMap } from './language';
import { expectSignalAction, sanitizeDeviceOptions, toClickOptionsForSourceCode, toKeyboardModifiers, toSignalMap } from './language';
import { deviceDescriptors } from '../deviceDescriptors';

import type { Language, LanguageGenerator, LanguageGeneratorOptions } from './types';
Expand Down Expand Up @@ -51,14 +51,14 @@ export class CSharpLanguageGenerator implements LanguageGenerator {
this._mode = mode;
}

generateAction(actionInContext: actions.ActionInContext): string {
const action = this._generateActionInner(actionInContext);
generateAction(actionInContext: actions.ActionInContext, options: LanguageGeneratorOptions): string {
const action = this._generateActionInner(actionInContext, options);
if (action)
return action;
return '';
}

_generateActionInner(actionInContext: actions.ActionInContext): string {
_generateActionInner(actionInContext: actions.ActionInContext, options: LanguageGeneratorOptions): string {
const action = actionInContext.action;
if (this._mode !== 'library' && (action.name === 'openPage' || action.name === 'closePage'))
return '';
Expand Down Expand Up @@ -101,6 +101,9 @@ export class CSharpLanguageGenerator implements LanguageGenerator {
for (const line of lines)
formatter.add(line);

if (options.generateExpectSignal && signals.expect)
formatter.add(this.generateAction(expectSignalAction(actionInContext, signals.expect), options));

return formatter.format();
}

Expand Down
7 changes: 5 additions & 2 deletions packages/isomorphic/codegen/java.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

import { asLocator } from '../locatorGenerators';
import { escapeWithQuotes } from '../stringUtils';
import { toClickOptionsForSourceCode, toKeyboardModifiers, toSignalMap } from './language';
import { expectSignalAction, toClickOptionsForSourceCode, toKeyboardModifiers, toSignalMap } from './language';
import { deviceDescriptors } from '../deviceDescriptors';
import { JavaScriptFormatter } from './javascript';

Expand Down Expand Up @@ -47,7 +47,7 @@ export class JavaLanguageGenerator implements LanguageGenerator {
this._mode = mode;
}

generateAction(actionInContext: actions.ActionInContext): string {
generateAction(actionInContext: actions.ActionInContext, options: LanguageGeneratorOptions): string {
const action = actionInContext.action;
const pageAlias = actionInContext.frame.pageAlias;
const offset = this._mode === 'junit' ? 4 : 6;
Expand Down Expand Up @@ -89,6 +89,9 @@ export class JavaLanguageGenerator implements LanguageGenerator {

formatter.add(code);

if (options.generateExpectSignal && signals.expect)
formatter.add(this.generateAction(expectSignalAction(actionInContext, signals.expect), options));

return formatter.format();
}

Expand Down
6 changes: 4 additions & 2 deletions packages/isomorphic/codegen/javascript.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

import { asLocator } from '../locatorGenerators';
import { escapeWithQuotes, formatObject, formatObjectOrVoid } from '../stringUtils';
import { sanitizeDeviceOptions, toClickOptionsForSourceCode, toKeyboardModifiers, toSignalMap } from './language';
import { expectSignalAction, sanitizeDeviceOptions, toClickOptionsForSourceCode, toKeyboardModifiers, toSignalMap } from './language';
import { deviceDescriptors } from '../deviceDescriptors';

import type { Language, LanguageGenerator, LanguageGeneratorOptions } from './types';
Expand All @@ -36,7 +36,7 @@ export class JavaScriptLanguageGenerator implements LanguageGenerator {
this._isTest = isTest;
}

generateAction(actionInContext: actions.ActionInContext): string {
generateAction(actionInContext: actions.ActionInContext, options: LanguageGeneratorOptions): string {
const action = actionInContext.action;
if (this._isTest && (action.name === 'openPage' || action.name === 'closePage'))
return '';
Expand Down Expand Up @@ -72,6 +72,8 @@ export class JavaScriptLanguageGenerator implements LanguageGenerator {
formatter.add(`const ${signals.popup.popupAlias} = await ${signals.popup.popupAlias}Promise;`);
if (signals.download)
formatter.add(`const download${signals.download.downloadAlias} = await download${signals.download.downloadAlias}Promise;`);
if (options.generateExpectSignal && signals.expect)
formatter.add(this.generateAction(expectSignalAction(actionInContext, signals.expect), options));

return formatter.format();
}
Expand Down
9 changes: 7 additions & 2 deletions packages/isomorphic/codegen/jsonl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
*/

import { asLocator } from '../locatorGenerators';
import { expectSignalAction, toSignalMap } from './language';

import type { Language, LanguageGenerator, LanguageGeneratorOptions } from './types';
import type * as actions from './actions';
Expand All @@ -25,15 +26,19 @@ export class JsonlLanguageGenerator implements LanguageGenerator {
name = 'JSONL';
highlighter = 'javascript' as Language;

generateAction(actionInContext: actions.ActionInContext): string {
generateAction(actionInContext: actions.ActionInContext, options: LanguageGeneratorOptions): string {
const locator = (actionInContext.action as any).selector ? JSON.parse(asLocator('jsonl', (actionInContext.action as any).selector)) : undefined;
const entry = {
...actionInContext.action,
...actionInContext.frame,
locator,
ariaSnapshot: undefined,
};
return JSON.stringify(entry);
const lines = [JSON.stringify(entry)];
const expect = toSignalMap(actionInContext.action).expect;
if (options.generateExpectSignal && expect)
lines.push(this.generateAction(expectSignalAction(actionInContext, expect), options));
return lines.join('\n');
}

generateHeader(options: LanguageGeneratorOptions): string {
Expand Down
37 changes: 16 additions & 21 deletions packages/isomorphic/codegen/language.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,31 +22,22 @@ import type * as actions from './actions';
export function generateCode(actions: actions.ActionInContext[], languageGenerator: LanguageGenerator, options: LanguageGeneratorOptions) {
const header = languageGenerator.generateHeader(options);
const footer = languageGenerator.generateFooter(options.saveStorage);
const actionTexts = actions.map(a => generateActionText(languageGenerator, a, !!options.generateAutoExpect)).filter(Boolean) as string[];
const actionTexts = actions.map(a => languageGenerator.generateAction(a, options)).filter(Boolean);
const text = [header, ...actionTexts, footer].join('\n');
return { header, footer, actionTexts, text };
}

function generateActionText(generator: LanguageGenerator, action: actions.ActionInContext, generateAutoExpect: boolean): string | undefined {
let text = generator.generateAction(action);
if (!text)
return;
if (generateAutoExpect && action.action.preconditionSelector) {
const expectAction: actions.ActionInContext = {
frame: action.frame,
startTime: action.startTime,
endTime: action.startTime,
action: {
name: 'assertVisible',
selector: action.action.preconditionSelector,
signals: [],
},
};
const expectText = generator.generateAction(expectAction);
if (expectText)
text = expectText + '\n\n' + text;
}
return text;
export function expectSignalAction(actionInContext: actions.ActionInContext, signal: actions.ExpectSignal): actions.ActionInContext {
return {
frame: actionInContext.frame,
startTime: actionInContext.startTime,
endTime: actionInContext.startTime,
action: {
name: 'assertVisible',
selector: signal.selector,
signals: [],
},
};
}

export function sanitizeDeviceOptions(device: any, options: BrowserContextOptions): BrowserContextOptions {
Expand All @@ -63,18 +54,22 @@ export function toSignalMap(action: actions.Action) {
let popup: actions.PopupSignal | undefined;
let download: actions.DownloadSignal | undefined;
let dialog: actions.DialogSignal | undefined;
let expect: actions.ExpectSignal | undefined;
for (const signal of action.signals) {
if (signal.name === 'popup')
popup = signal;
else if (signal.name === 'download')
download = signal;
else if (signal.name === 'dialog')
dialog = signal;
else if (signal.name === 'expect')
expect = signal;
}
return {
popup,
download,
dialog,
expect,
};
}

Expand Down
7 changes: 5 additions & 2 deletions packages/isomorphic/codegen/python.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

import { asLocator } from '../locatorGenerators';
import { escapeWithQuotes, toSnakeCase } from '../stringUtils';
import { sanitizeDeviceOptions, toClickOptionsForSourceCode, toKeyboardModifiers, toSignalMap } from './language';
import { expectSignalAction, sanitizeDeviceOptions, toClickOptionsForSourceCode, toKeyboardModifiers, toSignalMap } from './language';
import { deviceDescriptors } from '../deviceDescriptors';

import type { Language, LanguageGenerator, LanguageGeneratorOptions } from './types';
Expand All @@ -43,7 +43,7 @@ export class PythonLanguageGenerator implements LanguageGenerator {
this._asyncPrefix = isAsync ? 'async ' : '';
}

generateAction(actionInContext: actions.ActionInContext): string {
generateAction(actionInContext: actions.ActionInContext, options: LanguageGeneratorOptions): string {
const action = actionInContext.action;
if (this._isPyTest && (action.name === 'openPage' || action.name === 'closePage'))
return '';
Expand Down Expand Up @@ -82,6 +82,9 @@ export class PythonLanguageGenerator implements LanguageGenerator {

formatter.add(code);

if (options.generateExpectSignal && signals.expect)
formatter.add(this.generateAction(expectSignalAction(actionInContext, signals.expect), options));

return formatter.format();
}

Expand Down
4 changes: 2 additions & 2 deletions packages/isomorphic/codegen/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export type LanguageGeneratorOptions = {
contextOptions: BrowserContextOptions;
deviceName?: string;
saveStorage?: string;
generateAutoExpect?: boolean;
generateExpectSignal?: boolean;
};

export interface LanguageGenerator {
Expand All @@ -45,6 +45,6 @@ export interface LanguageGenerator {
name: string;
highlighter: Language;
generateHeader(options: LanguageGeneratorOptions): string;
generateAction(actionInContext: actions.ActionInContext): string;
generateAction(actionInContext: actions.ActionInContext, options: LanguageGeneratorOptions): string;
generateFooter(saveStorage: string | undefined): string;
}
2 changes: 1 addition & 1 deletion packages/playwright-core/src/server/debugController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ function wireListeners(recorder: Recorder, debugController: DebugController) {
browserName: 'chromium',
launchOptions: {},
contextOptions: {},
generateAutoExpect: debugController._generateAutoExpect,
generateExpectSignal: debugController._generateAutoExpect,
});
debugController.emit(DebugController.Events.SourceChanged, { text, header, footer, actions: actionTexts });
};
Expand Down
23 changes: 11 additions & 12 deletions packages/playwright-core/src/server/recorder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@ export class Recorder extends EventEmitter<RecorderEventMap> implements Instrume
// Input actions that potentially lead to navigation are intercepted on the page and are
// performed by the Playwright.
await this._context.exposeBinding(progress, '__pw_recorderPerformAction',
(source: BindingSource, action: actions.PerformOnRecordAction) => this._performAction(progress, source.frame, action));
(source: BindingSource, action: actions.PerformOnRecordAction, preconditionSelector?: string) => this._performAction(progress, source.frame, action, preconditionSelector));

// Other non-essential actions are simply being recorded.
await this._context.exposeBinding(progress, '__pw_recorderRecordAction',
Expand Down Expand Up @@ -559,14 +559,9 @@ export class Recorder extends EventEmitter<RecorderEventMap> implements Instrume
return this._params.testIdAttributeName || this._context.selectors().testIdAttributeName() || 'data-testid';
}

private async _appendContextToAction(progress: Progress, frame: Frame, action: actions.Action): Promise<actions.ActionInContext> {
const framePath = await generateFrameSelector(progress, frame);
if (framePath.length) {
if ('selector' in action)
action.selector = buildFullSelector(framePath, action.selector);
if (action.preconditionSelector)
action.preconditionSelector = buildFullSelector(framePath, action.preconditionSelector);
}
private _appendContextToAction(frame: Frame, action: actions.Action, framePath: string[]): actions.ActionInContext {
if (framePath.length && 'selector' in action)
action.selector = buildFullSelector(framePath, action.selector);
const actionInContext: actions.ActionInContext = {
frame: this._describeMainFrame(frame._page),
action,
Expand All @@ -575,8 +570,11 @@ export class Recorder extends EventEmitter<RecorderEventMap> implements Instrume
return actionInContext;
}

private async _performAction(progress: Progress, frame: Frame, action: actions.PerformOnRecordAction) {
const actionInContext = await this._appendContextToAction(progress, frame, action);
private async _performAction(progress: Progress, frame: Frame, action: actions.PerformOnRecordAction, preconditionSelector?: string) {
const framePath = await generateFrameSelector(progress, frame);
if (preconditionSelector)
this._signalProcessor.signal(this._pageAliases.get(frame._page)!, frame, { name: 'expect', selector: buildFullSelector(framePath, preconditionSelector) });
const actionInContext = this._appendContextToAction(frame, action, framePath);
this._signalProcessor.addAction(actionInContext);
try {
if (actionInContext.action.name !== 'openPage' && actionInContext.action.name !== 'closePage')
Expand All @@ -587,7 +585,8 @@ export class Recorder extends EventEmitter<RecorderEventMap> implements Instrume
}

private async _recordAction(progress: Progress, frame: Frame, action: actions.Action) {
const actionInContext = await this._appendContextToAction(progress, frame, action);
const framePath = await generateFrameSelector(progress, frame);
const actionInContext = this._appendContextToAction(frame, action, framePath);
this._signalProcessor.addAction(actionInContext);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ export class RecorderApp {
}
},
setAutoExpect: async (params: { autoExpect: boolean }) => {
this._languageGeneratorOptions.generateAutoExpect = params.autoExpect;
this._languageGeneratorOptions.generateExpectSignal = params.autoExpect;
this._updateActions();
},
setMode: async (params: { mode: Mode }) => {
Expand Down
Loading
Loading