Skip to content

Commit

Permalink
chore: generate simple dom descriptions in codegen
Browse files Browse the repository at this point in the history
  • Loading branch information
pavelfeldman committed Aug 27, 2024
1 parent 177576a commit ca86d77
Show file tree
Hide file tree
Showing 8 changed files with 162 additions and 69 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export type FrameDescription = {

export type ActionInContext = {
frame: FrameDescription;
description?: string;
action: Action;
committed?: boolean;
};
Expand Down
8 changes: 7 additions & 1 deletion packages/playwright-core/src/server/codegen/javascript.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ export class JavaScriptLanguageGenerator implements LanguageGenerator {
if (signals.download)
formatter.add(`const download${signals.download.downloadAlias}Promise = ${pageAlias}.waitForEvent('download');`);

formatter.add(this._generateActionCall(subject, actionInContext));
formatter.add(wrapWithStep(actionInContext.description, this._generateActionCall(subject, actionInContext)));

if (signals.popup)
formatter.add(`const ${signals.popup.popupAlias} = await ${signals.popup.popupAlias}Promise;`);
Expand Down Expand Up @@ -259,3 +259,9 @@ export class JavaScriptFormatter {
function quote(text: string) {
return escapeWithQuotes(text, '\'');
}

function wrapWithStep(description: string | undefined, body: string) {
return description ? `await test.step(\`${description}\`, async () => {
${body}
});` : body;
}
27 changes: 19 additions & 8 deletions packages/playwright-core/src/server/injected/.eslintrc.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,21 @@
const path = require('path');

module.exports = {
rules: {
"no-restricted-globals": [
"error",
{ "name": "window" },
{ "name": "document" },
{ "name": "globalThis" },
]
}
parser: "@typescript-eslint/parser",
plugins: ["@typescript-eslint", "notice"],
parserOptions: {
ecmaVersion: 9,
sourceType: "module",
project: path.join(__dirname, '../../../../../tsconfig.json'),
},
rules: {
"no-restricted-globals": [
"error",
{ "name": "window" },
{ "name": "document" },
{ "name": "globalThis" },
],
'@typescript-eslint/no-floating-promises': 'error',
"@typescript-eslint/no-unnecessary-boolean-literal-compare": 2,
},
};
2 changes: 1 addition & 1 deletion packages/playwright-core/src/server/injected/clock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ export class ClockController {
const sinceLastSync = now - this._realTime!.lastSyncTicks;
this._realTime!.lastSyncTicks = now;
// eslint-disable-next-line no-console
this._runTo(shiftTicks(this._now.ticks, sinceLastSync)).catch(e => console.error(e)).then(() => this._updateRealTimeTimer());
void this._runTo(shiftTicks(this._now.ticks, sinceLastSync)).catch(e => console.error(e)).then(() => this._updateRealTimeTimer());
}, callAt - this._now.ticks),
};
}
Expand Down
133 changes: 88 additions & 45 deletions packages/playwright-core/src/server/injected/recorder/recorder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,12 @@ import type { Mode, OverlayState, UIState } from '@recorder/recorderTypes';
import type { ElementText } from '../selectorUtils';
import type { Highlight, HighlightOptions } from '../highlight';
import clipPaths from './clipPaths';
import type { SimpleDomNode } from './simpleDom';
import { generateSimpleDomNode } from './simpleDom';

interface RecorderDelegate {
performAction?(action: actions.PerformOnRecordAction): Promise<void>;
recordAction?(action: actions.Action): Promise<void>;
performAction?(action: actions.PerformOnRecordAction, simpleDomNode?: SimpleDomNode): Promise<void>;
recordAction?(action: actions.Action, simpleDomNode?: SimpleDomNode): Promise<void>;
setSelector?(selector: string): Promise<void>;
setMode?(mode: Mode): Promise<void>;
setOverlayState?(state: OverlayState): Promise<void>;
Expand Down Expand Up @@ -168,7 +170,7 @@ class InspectTool implements RecorderTool {
if (this._hoveredModel?.tooltipListItemSelected)
this._reset(true);
else if (this._assertVisibility)
this._recorder.delegate.setMode?.('recording');
this._recorder.setMode('recording');
}
}

Expand All @@ -182,15 +184,15 @@ class InspectTool implements RecorderTool {

private _commit(selector: string) {
if (this._assertVisibility) {
this._recorder.delegate.recordAction?.({
this._recorder.recordAction({
name: 'assertVisible',
selector,
signals: [],
});
this._recorder.delegate.setMode?.('recording');
this._recorder.setMode('recording');
this._recorder.overlay?.flashToolSucceeded('assertingVisibility');
} else {
this._recorder.delegate.setSelector?.(selector);
this._recorder.setSelector(selector);
}
}

Expand Down Expand Up @@ -338,7 +340,7 @@ class RecordActionTool implements RecorderTool {
const target = this._recorder.deepEventTarget(event);

if (target.nodeName === 'INPUT' && (target as HTMLInputElement).type.toLowerCase() === 'file') {
this._recorder.delegate.recordAction?.({
this._recorder.recordAction({
name: 'setInputFiles',
selector: this._activeModel!.selector,
signals: [],
Expand All @@ -348,7 +350,7 @@ class RecordActionTool implements RecorderTool {
}

if (isRangeInput(target)) {
this._recorder.delegate.recordAction?.({
this._recorder.recordAction({
name: 'fill',
// must use hoveredModel instead of activeModel for it to work in webkit
selector: this._hoveredModel!.selector,
Expand All @@ -367,7 +369,7 @@ class RecordActionTool implements RecorderTool {
// Non-navigating actions are simply recorded by Playwright.
if (this._consumedDueWrongTarget(event))
return;
this._recorder.delegate.recordAction?.({
this._recorder.recordAction({
name: 'fill',
selector: this._activeModel!.selector,
signals: [],
Expand Down Expand Up @@ -483,26 +485,27 @@ class RecordActionTool implements RecorderTool {
return true;
}

private async _performAction(action: actions.PerformOnRecordAction) {
private _performAction(action: actions.PerformOnRecordAction) {
this._hoveredElement = null;
this._hoveredModel = null;
this._activeModel = null;
this._recorder.updateHighlight(null, false);
this._performingAction = true;
await this._recorder.delegate.performAction?.(action).catch(() => {});
this._performingAction = false;

// If that was a keyboard action, it similarly requires new selectors for active model.
this._onFocus(false);

if (this._recorder.injectedScript.isUnderTest) {
// Serialize all to string as we cannot attribute console message to isolated world
// in Firefox.
console.error('Action performed for test: ' + JSON.stringify({ // eslint-disable-line no-console
hovered: this._hoveredModel ? (this._hoveredModel as any).selector : null,
active: this._activeModel ? (this._activeModel as any).selector : null,
}));
}
void this._recorder.performAction(action).then(() => {
this._performingAction = false;

// If that was a keyboard action, it similarly requires new selectors for active model.
this._onFocus(false);

if (this._recorder.injectedScript.isUnderTest) {
// Serialize all to string as we cannot attribute console message to isolated world
// in Firefox.
console.error('Action performed for test: ' + JSON.stringify({ // eslint-disable-line no-console
hovered: this._hoveredModel ? (this._hoveredModel as any).selector : null,
active: this._activeModel ? (this._activeModel as any).selector : null,
}));
}
});
}

private _shouldGenerateKeyPressFor(event: KeyboardEvent): boolean {
Expand Down Expand Up @@ -613,7 +616,7 @@ class TextAssertionTool implements RecorderTool {

onKeyDown(event: KeyboardEvent) {
if (event.key === 'Escape')
this._recorder.delegate.setMode?.('recording');
this._recorder.setMode('recording');
consumeEvent(event);
}

Expand Down Expand Up @@ -680,8 +683,8 @@ class TextAssertionTool implements RecorderTool {
if (!this._action || !this._dialog.isShowing())
return;
this._dialog.close();
this._recorder.delegate.recordAction?.(this._action);
this._recorder.delegate.setMode?.('recording');
this._recorder.recordAction(this._action);
this._recorder.setMode('recording');
}

private _showDialog() {
Expand Down Expand Up @@ -726,8 +729,8 @@ class TextAssertionTool implements RecorderTool {
const action = this._generateAction();
if (!action)
return;
this._recorder.delegate.recordAction?.(action);
this._recorder.delegate.setMode?.('recording');
this._recorder.recordAction(action);
this._recorder.setMode('recording');
this._recorder.overlay?.flashToolSucceeded('assertingValue');
}
}
Expand Down Expand Up @@ -799,7 +802,7 @@ class Overlay {
this._dragState = { offsetX: this._offsetX, dragStart: { x: (event as MouseEvent).clientX, y: 0 } };
}),
addEventListener(this._recordToggle, 'click', () => {
this._recorder.delegate.setMode?.(this._recorder.state.mode === 'none' || this._recorder.state.mode === 'standby' || this._recorder.state.mode === 'inspecting' ? 'recording' : 'standby');
this._recorder.setMode(this._recorder.state.mode === 'none' || this._recorder.state.mode === 'standby' || this._recorder.state.mode === 'inspecting' ? 'recording' : 'standby');
}),
addEventListener(this._pickLocatorToggle, 'click', () => {
const newMode: Record<Mode, Mode> = {
Expand All @@ -812,19 +815,19 @@ class Overlay {
'assertingVisibility': 'recording-inspecting',
'assertingValue': 'recording-inspecting',
};
this._recorder.delegate.setMode?.(newMode[this._recorder.state.mode]);
this._recorder.setMode(newMode[this._recorder.state.mode]);
}),
addEventListener(this._assertVisibilityToggle, 'click', () => {
if (!this._assertVisibilityToggle.classList.contains('disabled'))
this._recorder.delegate.setMode?.(this._recorder.state.mode === 'assertingVisibility' ? 'recording' : 'assertingVisibility');
this._recorder.setMode(this._recorder.state.mode === 'assertingVisibility' ? 'recording' : 'assertingVisibility');
}),
addEventListener(this._assertTextToggle, 'click', () => {
if (!this._assertTextToggle.classList.contains('disabled'))
this._recorder.delegate.setMode?.(this._recorder.state.mode === 'assertingText' ? 'recording' : 'assertingText');
this._recorder.setMode(this._recorder.state.mode === 'assertingText' ? 'recording' : 'assertingText');
}),
addEventListener(this._assertValuesToggle, 'click', () => {
if (!this._assertValuesToggle.classList.contains('disabled'))
this._recorder.delegate.setMode?.(this._recorder.state.mode === 'assertingValue' ? 'recording' : 'assertingValue');
this._recorder.setMode(this._recorder.state.mode === 'assertingValue' ? 'recording' : 'assertingValue');
}),
];
}
Expand Down Expand Up @@ -890,7 +893,7 @@ class Overlay {
const halfGapSize = (this._recorder.injectedScript.window.innerWidth - this._measure.width) / 2 - 10;
this._offsetX = Math.max(-halfGapSize, Math.min(halfGapSize, this._offsetX));
this._updateVisualPosition();
this._recorder.delegate.setOverlayState?.({ offsetX: this._offsetX });
this._recorder.setOverlayState({ offsetX: this._offsetX });
consumeEvent(event);
return true;
}
Expand Down Expand Up @@ -924,9 +927,15 @@ export class Recorder {
readonly highlight: Highlight;
readonly overlay: Overlay | undefined;
private _stylesheet: CSSStyleSheet;
state: UIState = { mode: 'none', testIdAttributeName: 'data-testid', language: 'javascript', overlay: { offsetX: 0 } };
state: UIState = {
mode: 'none',
testIdAttributeName: 'data-testid',
language: 'javascript',
overlay: { offsetX: 0 },
generateSimpleDom: false,
};
readonly document: Document;
delegate: RecorderDelegate = {};
private _delegate: RecorderDelegate = {};

constructor(injectedScript: InjectedScript) {
this.document = injectedScript.document;
Expand Down Expand Up @@ -994,7 +1003,7 @@ export class Recorder {
}

setUIState(state: UIState, delegate: RecorderDelegate) {
this.delegate = delegate;
this._delegate = delegate;

if (state.actionPoint && this.state.actionPoint && state.actionPoint.x === this.state.actionPoint.x && state.actionPoint.y === this.state.actionPoint.y) {
// All good.
Expand Down Expand Up @@ -1155,7 +1164,7 @@ export class Recorder {
tooltipText = this.injectedScript.utils.asLocator(this.state.language, model.selector);
this.highlight.updateHighlight(model?.elements || [], { ...model, tooltipText });
if (userGesture)
this.delegate.highlightUpdated?.();
this._delegate.highlightUpdated?.();
}

private _ignoreOverlayEvent(event: Event) {
Expand All @@ -1172,6 +1181,40 @@ export class Recorder {
}
return event.composedPath()[0] as HTMLElement;
}

setMode(mode: Mode) {
void this._delegate.setMode?.(mode);
}

async performAction(action: actions.PerformOnRecordAction) {
const simpleDomNode = this._generateSimpleDomNode(action);
await this._delegate.performAction?.(action, simpleDomNode).catch(() => {});
}

recordAction(action: actions.Action) {
const simpleDomNode = this._generateSimpleDomNode(action);
void this._delegate.recordAction?.(action, simpleDomNode);
}

setOverlayState(state: { offsetX: number; }) {
void this._delegate.setOverlayState?.(state);
}

setSelector(selector: string) {
void this._delegate.setSelector?.(selector);
}

private _generateSimpleDomNode(action: actions.Action): SimpleDomNode | undefined {
if (!this.state.generateSimpleDom)
return;
if (!('selector' in action))
return;

const element = this.injectedScript.querySelector(this.injectedScript.parseSelector(action.selector), this.document.documentElement, true);
if (!element)
return;
return generateSimpleDomNode(this.document, element);
}
}

class Dialog {
Expand Down Expand Up @@ -1361,8 +1404,8 @@ function createSvgElement(doc: Document, { tagName, attrs, children }: SvgJson):
}

interface Embedder {
__pw_recorderPerformAction(action: actions.PerformOnRecordAction): Promise<void>;
__pw_recorderRecordAction(action: actions.Action): Promise<void>;
__pw_recorderPerformAction(action: actions.PerformOnRecordAction, simpleDomNode?: SimpleDomNode): Promise<void>;
__pw_recorderRecordAction(action: actions.Action, simpleDomNode?: SimpleDomNode): Promise<void>;
__pw_recorderState(): Promise<UIState>;
__pw_recorderSetSelector(selector: string): Promise<void>;
__pw_recorderSetMode(mode: Mode): Promise<void>;
Expand Down Expand Up @@ -1407,12 +1450,12 @@ export class PollingRecorder implements RecorderDelegate {
this._pollRecorderModeTimer = this._recorder.injectedScript.builtinSetTimeout(() => this._pollRecorderMode(), pollPeriod);
}

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

async recordAction(action: actions.Action): Promise<void> {
await this._embedder.__pw_recorderRecordAction(action);
async recordAction(action: actions.Action, simpleDomNode?: SimpleDomNode): Promise<void> {
await this._embedder.__pw_recorderRecordAction(action, simpleDomNode);
}

async setSelector(selector: string): Promise<void> {
Expand Down
Loading

0 comments on commit ca86d77

Please sign in to comment.