Skip to content
Open
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
72 changes: 65 additions & 7 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 @@ -262,6 +262,14 @@ class RecordActionTool implements RecorderTool {
return;
}

if (event.detail === 1) {
// A new click starts here, so the stalled one is not a double click after all.
this._commitPendingClickAction();
} else {
// This click continues a multi-click, which is reported by 'dblclick' instead.
this._cancelPendingClickAction();
}

const checkbox = asCheckbox(this._recorder.deepEventTarget(event));
if (checkbox && event.detail === 1) {
// Interestingly, inputElement.checked is reversed inside this event handler.
Expand All @@ -273,8 +281,6 @@ class RecordActionTool implements RecorderTool {
return;
}

this._cancelPendingClickAction();

// Stall click in case we are observing double-click.
if (event.detail === 1) {
this._pendingClickAction = {
Expand Down Expand Up @@ -741,6 +747,7 @@ class RecordActionTool implements RecorderTool {

class JsonRecordActionTool implements RecorderTool {
private _recorder: Recorder;
private _pendingClickAction: { action: actions.ClickAction, timeout: number } | undefined;

constructor(recorder: Recorder) {
this._recorder = recorder;
Expand All @@ -752,6 +759,7 @@ class JsonRecordActionTool implements RecorderTool {
}

uninstall() {
this._cancelPendingClickAction();
this._recorder.highlight.install();
}

Expand All @@ -767,6 +775,14 @@ class JsonRecordActionTool implements RecorderTool {
if (this._shouldIgnoreMouseEvent(event))
return;

if (event.detail === 1) {
// A new click starts here, so the stalled one is not a double click after all.
this._commitPendingClickAction();
} else {
// This click continues a multi-click, which is reported by 'dblclick' instead.
this._cancelPendingClickAction();
}

const checkbox = asCheckbox(element);
const { ariaSnapshot, selector, ref } = this._ariaSnapshot(element);
if (checkbox && event.detail === 1) {
Expand All @@ -781,6 +797,35 @@ class JsonRecordActionTool implements RecorderTool {
return;
}

// Stall click in case we are observing double-click.
if (event.detail === 1) {
this._pendingClickAction = {
action: {
name: 'click',
selector,
ref,
ariaSnapshot,
position: positionForEvent(event),
signals: [],
button: buttonForEvent(event),
modifiers: modifiersForEvent(event),
clickCount: event.detail,
},
timeout: this._recorder.injectedScript.utils.builtins.setTimeout(() => this._commitPendingClickAction(), 200)
};
}
}

onDblClick(event: MouseEvent) {
const element = this._recorder.deepEventTarget(event);
if (isRangeInput(element))
return;
if (this._shouldIgnoreMouseEvent(event))
return;

this._cancelPendingClickAction();

const { ariaSnapshot, selector, ref } = this._ariaSnapshot(element);
void this._recorder.recordAction({
name: 'click',
selector,
Expand All @@ -794,6 +839,18 @@ class JsonRecordActionTool implements RecorderTool {
});
}

private _commitPendingClickAction() {
if (this._pendingClickAction)
void this._recorder.recordAction(this._pendingClickAction.action);
this._cancelPendingClickAction();
}

private _cancelPendingClickAction() {
if (this._pendingClickAction)
this._recorder.injectedScript.utils.builtins.clearTimeout(this._pendingClickAction.timeout);
this._pendingClickAction = undefined;
}

onContextMenu(event: MouseEvent): void {
const element = this._recorder.deepEventTarget(event);
const { ariaSnapshot, selector, ref } = this._ariaSnapshot(element);
Expand Down Expand Up @@ -1698,13 +1755,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
23 changes: 9 additions & 14 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 @@ -143,7 +142,7 @@ export type NavigationSignal = BaseSignal & {

export type PopupSignal = BaseSignal & {
name: 'popup',
popupAlias: string,
popupPageGuid: string,
};

export type DownloadSignal = BaseSignal & {
Expand All @@ -156,24 +155,20 @@ export type DialogSignal = BaseSignal & {
dialogAlias: string,
};

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

export type FrameDescription = {
pageGuid: string;
pageAlias: string;
framePath: string[];
// 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 ActionInContext = {
frame: FrameDescription;
description?: string;
pageGuid: string;
action: Action;
startTime: number;
endTime?: number;
};

export type SignalInContext = {
frame: FrameDescription;
pageGuid: string;
signal: Signal;
timestamp: number;
};
48 changes: 29 additions & 19 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 All @@ -31,6 +31,7 @@ export class CSharpLanguageGenerator implements LanguageGenerator {
name: string;
highlighter = 'csharp' as Language;
_mode: CSharpLanguageMode;
private _pageAliases = new Map<string, string>();

constructor(mode: CSharpLanguageMode) {
if (mode === 'library') {
Expand All @@ -51,18 +52,35 @@ export class CSharpLanguageGenerator implements LanguageGenerator {
this._mode = mode;
}

generateAction(actionInContext: actions.ActionInContext): string {
const action = this._generateActionInner(actionInContext);
reset() {
this._pageAliases.clear();
}

private _pageAlias(pageGuid: string): string {
let alias = this._pageAliases.get(pageGuid);
if (!alias) {
alias = 'page' + (this._pageAliases.size || '');
// Outside of the library mode, the first page is a class member, the rest are local variables.
if (this._mode !== 'library' && alias === 'page')
alias = 'Page';
this._pageAliases.set(pageGuid, alias);
}
return alias;
}

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;
// Resolve before the early return, so that pages are named in the order they are opened.
const pageAlias = this._pageAlias(actionInContext.pageGuid);
if (this._mode !== 'library' && (action.name === 'openPage' || action.name === 'closePage'))
return '';
const pageAlias = this._formatPageAlias(actionInContext.frame.pageAlias);
const formatter = new CSharpFormatter(this._mode === 'library' ? 0 : 8);

if (action.name === 'openPage') {
Expand All @@ -72,8 +90,7 @@ export class CSharpLanguageGenerator implements LanguageGenerator {
return formatter.format();
}

const locators = actionInContext.frame.framePath.map(selector => `.${this._asLocator(selector)}.ContentFrame`);
const subject = `${pageAlias}${locators.join('')}`;
const subject = pageAlias;
const signals = toSignalMap(action);

if (signals.dialog) {
Expand All @@ -95,25 +112,18 @@ export class CSharpLanguageGenerator implements LanguageGenerator {
}

if (signals.popup) {
lines.unshift(`var ${this._formatPageAlias(signals.popup.popupAlias)} = await ${pageAlias}.RunAndWaitForPopupAsync(async () =>\n{`);
const popupAlias = this._pageAlias(signals.popup.popupPageGuid);
lines.unshift(`var ${popupAlias} = await ${pageAlias}.RunAndWaitForPopupAsync(async () =>\n{`);
lines.push(`});`);
}

for (const line of lines)
formatter.add(line);

return formatter.format();
}

private _formatPageAlias(pageAlias: string): string {
if (this._mode === 'library')
return pageAlias;
if (options.generateExpectSignal && signals.expect)
formatter.add(this.generateAction(expectSignalAction(actionInContext, signals.expect), options));

if (pageAlias === 'page')
return 'Page'; // first page is class member

// other pages are local variables
return pageAlias;
return formatter.format();
}

private _generateActionCall(subject: string, actionInContext: actions.ActionInContext): string {
Expand Down
Loading