Skip to content

Commit

Permalink
feat(recorder); highlight current line
Browse files Browse the repository at this point in the history
  • Loading branch information
ruifigueira committed Dec 9, 2024
1 parent 1f89676 commit 639ac2d
Show file tree
Hide file tree
Showing 4 changed files with 90 additions and 14 deletions.
6 changes: 5 additions & 1 deletion examples/recorder-crx/src/crxRecorder.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,10 @@ export const CrxRecorder: React.FC = ({
window.dispatch({ event: 'codeChanged', params: { code } });
}, []);

const dispatchCursorActivity = React.useCallback((position: { line: number }) => {
window.dispatch({ event: 'cursorActivity', params: { position } });
}, []);

return <>
<div>
<Dialog title="Preferences" isOpen={showPreferences} onClose={() => setShowPreferences(false)}>
Expand All @@ -176,7 +180,7 @@ export const CrxRecorder: React.FC = ({
<ToolbarButton icon='settings-gear' title='Preferences' onClick={() => setShowPreferences(true)}></ToolbarButton>
</Toolbar>
</>}
<Recorder sources={sources} paused={paused} log={log} mode={mode} onEditedCode={dispatchEditedCode} />
<Recorder sources={sources} paused={paused} log={log} mode={mode} onEditedCode={dispatchEditedCode} onCursorActivity={dispatchCursorActivity} />
</div>
</>;
};
5 changes: 4 additions & 1 deletion playwright/packages/recorder/src/recorder.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ export const Recorder: React.FC<RecorderProps> = ({
const [selectedTab, setSelectedTab] = useSetting<string>('recorderPropertiesTab', 'log');
const [ariaSnapshot, setAriaSnapshot] = React.useState<string | undefined>();
const [ariaSnapshotErrors, setAriaSnapshotErrors] = React.useState<SourceHighlight[]>();
const [selectorFocusOnChange, setSelectorFocusOnChange] = React.useState<boolean | undefined>(true);

const fileId = selectedFileId || runningFileId || sources[0]?.id;

Expand All @@ -72,6 +73,8 @@ export const Recorder: React.FC<RecorderProps> = ({
setLocator(asLocator(language, elementInfo.selector));
setAriaSnapshot(elementInfo.ariaSnapshot);
setAriaSnapshotErrors([]);
setSelectorFocusOnChange(userGesture);

if (userGesture && selectedTab !== 'locator' && selectedTab !== 'aria')
setSelectedTab('locator');

Expand Down Expand Up @@ -194,7 +197,7 @@ export const Recorder: React.FC<RecorderProps> = ({
{
id: 'locator',
title: 'Locator',
render: () => <CodeMirrorWrapper text={locator} placeholder='Type locator to inspect' language={source.language} focusOnChange={true} onChange={onEditorChange} wrapLines={true} />
render: () => <CodeMirrorWrapper text={locator} placeholder='Type locator to inspect' language={source.language} focusOnChange={selectorFocusOnChange} onChange={onEditorChange} wrapLines={true} />
},
{
id: 'log',
Expand Down
38 changes: 29 additions & 9 deletions src/server/recorder/crxRecorderApp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import type { ActionInContextWithLocation } from './parser';
import { PopupRecorderWindow } from './popupRecorderWindow';
import { SidepanelRecorderWindow } from './sidepanelRecorderWindow';
import type { IRecorderApp } from 'playwright-core/lib/server/recorder/recorderFrontend';
import type { ActionInContext } from '@recorder/actions';
import type { ActionInContext, ActionWithSelector } from '@recorder/actions';
import { parse } from './parser';
import { languageSet } from 'playwright-core/lib/server/codegen/languages';
import type { Crx } from '../crx';
Expand All @@ -37,7 +37,7 @@ export type RecorderMessage = { type: 'recorder' } & (
| { method: 'elementPicked', elementInfo: ElementInfo, userGesture?: boolean }
);

export type RecorderEventData = (EventData | { event: 'codeChanged', params: any }) & { type: string };
export type RecorderEventData = (EventData | { event: 'codeChanged' | 'cursorActivity', params: any }) & { type: string };

export interface RecorderWindow {
isClosed(): boolean;
Expand All @@ -61,6 +61,7 @@ export class CrxRecorderApp extends EventEmitter implements IRecorderApp {
private _editedCode?: EditedCode;
private _recordedActions: ActionInContextWithLocation[] = [];
private _playInIncognito = false;
private _currentCursorPosition: { line: number } | undefined;

constructor(crx: Crx, recorder: Recorder, player: CrxPlayer) {
super();
Expand Down Expand Up @@ -154,8 +155,6 @@ export class CrxRecorderApp extends EventEmitter implements IRecorderApp {
if (this._recorder.mode() === 'inspecting') {
this._recorder.setMode('standby');
this._window?.focus();
} else {
this._recorder.setMode('recording');
}
}
this._sendMessage({ type: 'recorder', method: 'elementPicked', elementInfo, userGesture });
Expand All @@ -182,7 +181,20 @@ export class CrxRecorderApp extends EventEmitter implements IRecorderApp {
if (!code || this._recorder._isRecording())
return;

this._editedCode = new EditedCode(this._recorder, code);
this._editedCode = new EditedCode(this._recorder, code, () => this._updateLocator(this._currentCursorPosition));
}

private async _updateLocator(position?: { line: number}) {
if (!position)
return;

// codemirror line is 0-based while action line is 1-based
const action = this._getActions(true).find(a => a.location?.line === position.line + 1);
if (!action || !(action.action as ActionWithSelector).selector)
return;
const selector = (action.action as ActionWithSelector).selector;
this.elementPicked({ selector, ariaSnapshot: '' }, false);
this._onMessage({ type: 'recorderEvent', event: 'highlightRequested', params: { selector } });
}

private _onMessage({ type, event, params }: RecorderEventData) {
Expand All @@ -200,6 +212,10 @@ export class CrxRecorderApp extends EventEmitter implements IRecorderApp {
case 'codeChanged':
this._updateCode(params.code);
break;
case 'cursorActivity':
this._currentCursorPosition = params.position;
this._updateLocator(this._currentCursorPosition);
break;
case 'resume':
case 'step':
this._run().catch(() => {});
Expand Down Expand Up @@ -237,8 +253,8 @@ export class CrxRecorderApp extends EventEmitter implements IRecorderApp {
await this._recorder._uninstallInjectedRecorder(page);
}

private _getActions(): ActionInContextWithLocation[] {
if (this._editedCode) {
private _getActions(skipLoad = false): ActionInContextWithLocation[] {
if (this._editedCode && !skipLoad) {
// this will indirectly refresh sources
this._editedCode.load();
const actions = this._editedCode.actions();
Expand All @@ -251,7 +267,7 @@ export class CrxRecorderApp extends EventEmitter implements IRecorderApp {
if (!source)
return [];

const actions = this._editedCode && !this._editedCode.hasErrors() ? this._editedCode.actions() : this._recordedActions;
const actions = this._editedCode?.hasLoaded() && !this._editedCode.hasErrors() ? this._editedCode.actions() : this._recordedActions;

const { header } = source;
const languageGenerator = [...languageSet()].find(l => l.id === this._filename)!;
Expand Down Expand Up @@ -281,10 +297,12 @@ class EditedCode {
private _actions: ActionInContextWithLocation[] = [];
private _highlight: SourceHighlight[] = [];
private _codeLoadDebounceTimeout: NodeJS.Timeout | undefined;
private _onLoaded?: () => any;

constructor(recorder: Recorder, code: string) {
constructor(recorder: Recorder, code: string, onLoaded?: () => any) {
this.code = code;
this._recorder = recorder;
this._onLoaded = onLoaded;
this._codeLoadDebounceTimeout = setTimeout(this.load.bind(this), 500);
}

Expand Down Expand Up @@ -333,5 +351,7 @@ class EditedCode {
this._highlight = [{ line, type: 'error', message: error.message }];
this._recorder.loadScript({ actions: this._actions, deviceName: '', contextOptions: {}, text: this.code, highlight: this._highlight });
}

this._onLoaded?.();
}
}
55 changes: 52 additions & 3 deletions tests/crx/recorder-edit.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,21 @@ import { test, expect } from './crxRecorderTest';
import type { CrxApplication } from '../../test';

async function editCode(recorderPage: Page, code: string) {
const editor = recorderPage.locator('.CodeMirror textarea');
const editor = recorderPage.locator('.CodeMirror textarea').first();
await editor.press('ControlOrMeta+a');
await editor.fill(code);
}

async function getCode(recorderPage: Page): Promise<string> {
return await recorderPage.locator('.CodeMirror').evaluate(elem => (elem as any).CodeMirror.getValue());
return await recorderPage.locator('.CodeMirror').first().evaluate((elem: any) => elem.CodeMirror.getValue());
}

async function moveCursorToLine(recorderPage: Page, line: number) {
await recorderPage.locator('.CodeMirror').first().evaluate((elem: any, line) => elem.CodeMirror.setCursor({
// codemirror line is 0-based
line: line - 1,
ch: 0,
}), line);
}

function editorLine(recorderPage: Page, linenumber: number) {
Expand Down Expand Up @@ -88,7 +96,13 @@ test('test', async ({ page }) => {
await page.goto('${baseURL}/input/textarea.html');
await page.locator('textarea').wrongAction('te
});`);

// validates parsing debouncing: after editing, it should wait for a while before showing the error
// we wait a bit but less than the debounce time to ensure it's still hidden
await page.waitForTimeout(250);
await expect(recorderPage.locator('.source-line-error-widget')).toBeHidden();

// eventually it should show the error
await expect(recorderPage.locator('.source-line-error .CodeMirror-line')).toHaveText(` await page.locator('textarea').wrongAction('te`);
await expect(recorderPage.locator('.source-line-error-widget')).toHaveText('Unterminated string constant (5:45)');
});
Expand Down Expand Up @@ -248,4 +262,39 @@ test('test', async ({ page }) => {
});

await expect(recorderPage.locator('.source-line-error-widget')).toHaveText('Invalid locator (4:8)');
});
});

test('should highlight selector at cursor line', async ({ page, attachRecorder, baseURL }) => {
const recorderPage = await attachRecorder(page);
await recorderPage.getByTitle('Record').click();

// ensure locator tab is selected
await recorderPage.getByRole('tab', { name: 'Locator' }).click();

await page.goto(`${baseURL}/input/textarea.html`);

await editCode(recorderPage, `import { test, expect } from '@playwright/test';
test('test', async ({ page }) => {
await page.goto('${baseURL}/input/textarea.html');
await page.locator('textarea').fill('some test');
await page.getByRole('textbox').fill('another test');
});`);

await moveCursorToLine(recorderPage, 6);

await expect(recorderPage.getByRole('tabpanel', { name: 'Locator' }).locator('.CodeMirror-code')).toHaveText(`getByRole('textbox')`);
expect(await page.evaluate(() => [
...document.querySelector('x-pw-glass')!.shadowRoot!.querySelectorAll('x-pw-tooltip-line')].map(e => e.textContent)
)).toEqual([
`getByRole('textbox') [1 of 2]`,
`getByRole('textbox') [2 of 2]`,
]);

await moveCursorToLine(recorderPage, 5);

await expect(recorderPage.getByRole('tabpanel', { name: 'Locator' }).locator('.CodeMirror-code')).toHaveText(`locator('textarea')`);
expect(await page.evaluate(() => [
...document.querySelector('x-pw-glass')!.shadowRoot!.querySelectorAll('x-pw-tooltip-line')].map(e => e.textContent)
)).toEqual([`locator('textarea')`]);
});

0 comments on commit 639ac2d

Please sign in to comment.