Skip to content

Commit

Permalink
feat(recorder): remove recorder overlay toolbar (#5334)
Browse files Browse the repository at this point in the history
  • Loading branch information
pavelfeldman committed Feb 5, 2021
1 parent 9c0609b commit c0610cc
Show file tree
Hide file tree
Showing 10 changed files with 248 additions and 404 deletions.
295 changes: 27 additions & 268 deletions src/server/supplements/injected/recorder.ts

Large diffs are not rendered by default.

50 changes: 38 additions & 12 deletions src/server/supplements/recorder/recorderApp.ts
Expand Up @@ -27,6 +27,21 @@ import { DEFAULT_ARGS } from '../../chromium/chromium';

const readFileAsync = util.promisify(fs.readFile);

export type Mode = 'inspecting' | 'recording' | 'none';
export type EventData = {
event: 'clear' | 'resume' | 'setMode',
params: any
};

declare global {
interface Window {
playwrightSetMode: (mode: Mode) => void;
playwrightSetPaused: (paused: boolean) => void;
playwrightSetSource: (params: { text: string, language: string }) => void;
dispatch(data: EventData): Promise<void>;
}
}

export class RecorderApp extends EventEmitter {
private _page: Page;

Expand All @@ -36,6 +51,10 @@ export class RecorderApp extends EventEmitter {
this._page = page;
}

async close() {
await this._page.context().close();
}

private async _init() {
const icon = await readFileAsync(require.resolve('../../../../lib/web/recorder/app_icon.png'));
const crPopup = this._page._delegate as CRPage;
Expand All @@ -61,9 +80,7 @@ export class RecorderApp extends EventEmitter {
await route.continue();
});

await this._page.exposeBinding('_playwrightClear', false, (_, text: string) => {
this.emit('clear');
});
await this._page.exposeBinding('dispatch', false, (_, data: any) => this.emit('event', data));

this._page.once('close', () => {
this.emit('close');
Expand All @@ -73,18 +90,16 @@ export class RecorderApp extends EventEmitter {
await this._page.mainFrame().goto(new ProgressController(), 'https://playwright/index.html');
}

static async open(inspectedPage: Page): Promise<RecorderApp> {
const bounds = await CRPage.mainFrameSession(inspectedPage).windowBounds();
static async open(): Promise<RecorderApp> {
const recorderPlaywright = createPlaywright(true);
const context = await recorderPlaywright.chromium.launchPersistentContext('', {
ignoreAllDefaultArgs: true,
args: [
...DEFAULT_ARGS,
`--user-data-dir=${path.join(os.homedir(),'.playwright-recorder')}`,
`--user-data-dir=${path.join(os.homedir(),'.playwright-app')}`,
'--remote-debugging-pipe',
'--app=data:text/html,',
`--window-size=300,${bounds.height}`,
`--window-position=${bounds.left! + bounds.width! + 1},${bounds.top!}`
`--window-size=300,800`,
],
noDefaultViewport: true
});
Expand All @@ -97,14 +112,25 @@ export class RecorderApp extends EventEmitter {
const [page] = context.pages();
const result = new RecorderApp(page);
await result._init();
await inspectedPage.bringToFront();
return result;
}

async setScript(text: string, language: string): Promise<void> {
async setMode(mode: 'none' | 'recording' | 'inspecting'): Promise<void> {
await this._page.mainFrame()._evaluateExpression(((mode: Mode) => {
window.playwrightSetMode(mode);
}).toString(), true, mode, 'main').catch(() => {});
}

async setPaused(paused: boolean): Promise<void> {
await this._page.mainFrame()._evaluateExpression(((paused: boolean) => {
window.playwrightSetPaused(paused);
}).toString(), true, paused, 'main').catch(() => {});
}

async setSource(text: string, language: string): Promise<void> {
await this._page.mainFrame()._evaluateExpression(((param: { text: string, language: string }) => {
(window as any)._playwrightSetSource(param);
}).toString(), true, { text, language }, 'main');
window.playwrightSetSource(param);
}).toString(), true, { text, language }, 'main').catch(() => {});
}

async bringToFront() {
Expand Down
28 changes: 0 additions & 28 deletions src/server/supplements/recorder/state.ts

This file was deleted.

97 changes: 53 additions & 44 deletions src/server/supplements/recorderSupplement.ts
Expand Up @@ -29,11 +29,9 @@ import { ProgressController } from '../progress';
import * as recorderSource from '../../generated/recorderSource';
import * as consoleApiSource from '../../generated/consoleApiSource';
import { BufferedOutput, FileOutput, FlushingTerminalOutput, OutputMultiplexer, RecorderOutput, TerminalOutput, Writable } from './recorder/outputs';
import type { State, UIState } from './recorder/state';
import { RecorderApp } from './recorder/recorderApp';
import { EventData, Mode, RecorderApp } from './recorder/recorderApp';

type BindingSource = { frame: Frame, page: Page };
type App = 'codegen' | 'debug' | 'pause';

const symbol = Symbol('RecorderSupplement');

Expand All @@ -45,11 +43,11 @@ export class RecorderSupplement {
private _timers = new Set<NodeJS.Timeout>();
private _context: BrowserContext;
private _resumeCallback: (() => void) | null = null;
private _recorderUIState: UIState;
private _mode: Mode;
private _paused = false;
private _output: OutputMultiplexer;
private _bufferedOutput: BufferedOutput;
private _recorderApp: Promise<RecorderApp> | null = null;
private _recorderApp: RecorderApp | null = null;
private _highlighterType: string;
private _params: channels.BrowserContextRecorderSupplementEnableParams;

Expand All @@ -66,9 +64,7 @@ export class RecorderSupplement {
constructor(context: BrowserContext, params: channels.BrowserContextRecorderSupplementEnableParams) {
this._context = context;
this._params = params;
this._recorderUIState = {
mode: params.startRecording ? 'recording' : 'none',
};
this._mode = params.startRecording ? 'recording' : 'none';
let languageGenerator: LanguageGenerator;
switch (params.language) {
case 'javascript': languageGenerator = new JavaScriptLanguageGenerator(); break;
Expand All @@ -88,10 +84,8 @@ export class RecorderSupplement {
const outputs: RecorderOutput[] = [params.terminal ? new TerminalOutput(writable, highlighterType) : new FlushingTerminalOutput(writable)];
this._highlighterType = highlighterType;
this._bufferedOutput = new BufferedOutput(async text => {
if (this._recorderApp) {
const app = await this._recorderApp;
await app.setScript(text, highlighterType).catch(e => {});
}
if (this._recorderApp)
this._recorderApp.setSource(text, highlighterType);
});
outputs.push(this._bufferedOutput);
if (params.outputFile)
Expand All @@ -105,14 +99,45 @@ export class RecorderSupplement {
}

async install() {
this._context.on('page', page => this._onPage(page));
const recorderApp = await RecorderApp.open();
this._recorderApp = recorderApp;
recorderApp.once('close', () => {
this._recorderApp = null;
});
recorderApp.on('event', (data: EventData) => {
if (data.event === 'setMode') {
this._mode = data.params.mode;
recorderApp.setMode(this._mode);
this._output.setEnabled(this._mode === 'recording');
if (this._mode !== 'none')
this._context.pages()[0].bringToFront().catch(() => {});
return;
}
if (data.event === 'resume') {
this._resume();
return;
}
if (data.event === 'clear') {
this._clearScript();
return;
}
});

await Promise.all([
recorderApp.setMode(this._mode),
recorderApp.setPaused(this._paused),
recorderApp.setSource(this._bufferedOutput.buffer(), this._highlighterType)
]);

this._context.on(BrowserContext.Events.Page, page => this._onPage(page));
for (const page of this._context.pages())
this._onPage(page);

this._context.once('close', () => {
this._context.once(BrowserContext.Events.Close, () => {
for (const timer of this._timers)
clearTimeout(timer);
this._timers.clear();
recorderApp.close().catch(() => {});
});

// Input actions that potentially lead to navigation are intercepted on the page and are
Expand All @@ -128,55 +153,39 @@ export class RecorderSupplement {
await this._context.exposeBinding('_playwrightRecorderCommitAction', false,
(source: BindingSource, action: actions.Action) => this._generator.commitLastAction());

await this._context.exposeBinding('_playwrightRecorderShowRecorderPage', false, ({ page }) => {
if (this._recorderApp) {
this._recorderApp.then(p => p.bringToFront()).catch(() => {});
return;
}
this._recorderApp = RecorderApp.open(page);
this._recorderApp.then(app => {
app.once('close', () => {
this._recorderApp = null;
});
app.on('clear', () => this._clearScript());
return app.setScript(this._bufferedOutput.buffer(), this._highlighterType);
}).catch(e => console.error(e));
});

await this._context.exposeBinding('_playwrightRecorderPrintSelector', false, (_, text) => {
this._context.emit(BrowserContext.Events.StdOut, `Selector: \x1b[38;5;130m${text}\x1b[0m\n`);
});

await this._context.exposeBinding('_playwrightRecorderState', false, () => {
const state: State = {
uiState: this._recorderUIState,
isPaused: this._paused,
};
return state;
});

await this._context.exposeBinding('_playwrightRecorderSetUIState', false, (source, state: UIState) => {
this._recorderUIState = { ...this._recorderUIState, ...state };
this._output.setEnabled(state.mode === 'recording');
return { mode: this._mode };
});

await this._context.exposeBinding('_playwrightResume', false, () => {
if (this._resumeCallback) {
this._resumeCallback();
this._resumeCallback = null;
}
this._paused = false;
this._resume().catch(() => {});
});

await this._context.extendInjectedScript(recorderSource.source);
await this._context.extendInjectedScript(consoleApiSource.source);

(this._context as any).recorderAppForTest = recorderApp;
}

async pause() {
this._paused = true;
this._recorderApp!.setPaused(true);
return new Promise<void>(f => this._resumeCallback = f);
}

private async _resume() {
if (this._resumeCallback)
this._resumeCallback();
this._resumeCallback = null;
this._paused = false;
if (this._recorderApp)
this._recorderApp.setPaused(this._paused);
}

private async _onPage(page: Page) {
// First page is called page, others are called popup1, popup2, etc.
const frame = page.mainFrame();
Expand Down
5 changes: 5 additions & 0 deletions src/web/common.css
Expand Up @@ -112,3 +112,8 @@ svg {
::-webkit-scrollbar-corner {
background-color: var(--background);
}

.code {
font-family: var(--monospace-font);
color: yellow;
}
26 changes: 23 additions & 3 deletions src/web/components/toolbarButton.css
Expand Up @@ -17,14 +17,34 @@
.toolbar-button {
border: none;
outline: none;
color: #999;
color: #777;
background: transparent;
padding: 0;
margin-left: 10px;
height: 40px;
cursor: pointer;
}

.toolbar-button:hover {
.toolbar-button:disabled {
color: #bbb !important;
cursor: default;
}

.toolbar-button:not(.disabled):hover {
color: #555;
}

.toolbar-button.toggled {
color: #1ea7fd;
}

.toolbar-button.codicon-record.toggled {
color: #fd1e1e;
}

.toolbar-button.codicon-run {
color: #4bfd1e;
}

.toolbar-button.codicon-run:hover {
color: #0f0;
}
10 changes: 8 additions & 2 deletions src/web/components/toolbarButton.tsx
Expand Up @@ -21,14 +21,20 @@ import * as React from 'react';
export interface ToolbarButtonProps {
title: string,
icon: string,
disabled?: boolean,
toggled?: boolean,
onClick: () => void
}

export const ToolbarButton: React.FC<ToolbarButtonProps> = ({
title = '',
icon = '',
disabled = false,
toggled = false,
onClick = () => {},
}) => {
const className = `toolbar-button codicon codicon-${icon}`;
return <button className={className} onClick={onClick} title={title}></button>;
let className = `toolbar-button codicon codicon-${icon}`;
if (toggled)
className += ' toggled';
return <button className={className} onClick={onClick} title={title} disabled={!!disabled}></button>;
};
8 changes: 8 additions & 0 deletions src/web/recorder/recorder.css
Expand Up @@ -19,3 +19,11 @@
flex-direction: column;
flex: auto;
}

.recorder-paused-infobar {
display: flex;
color: #eee;
background-color: #333;
height: 24px;
align-items: center;
}

0 comments on commit c0610cc

Please sign in to comment.