Skip to content

Commit

Permalink
feat(ui): more recorder uis (#5187)
Browse files Browse the repository at this point in the history
  • Loading branch information
pavelfeldman committed Jan 28, 2021
1 parent f2ef7f5 commit e50f11c
Show file tree
Hide file tree
Showing 6 changed files with 514 additions and 170 deletions.
544 changes: 401 additions & 143 deletions src/server/supplements/injected/recorder.ts

Large diffs are not rendered by default.

21 changes: 15 additions & 6 deletions src/server/supplements/recorder/codeGenerator.ts
Expand Up @@ -35,11 +35,12 @@ export interface CodeGeneratorOutput {
}

export class CodeGenerator {
private _currentAction: ActionInContext | undefined;
private _lastAction: ActionInContext | undefined;
private _currentAction: ActionInContext | null = null;
private _lastAction: ActionInContext | null = null;
private _lastActionText: string | undefined;
private _languageGenerator: LanguageGenerator;
private _output: CodeGeneratorOutput;
private _headerText = '';
private _footerText = '';

constructor(browserName: string, generateHeaders: boolean, launchOptions: LaunchOptions, contextOptions: BrowserContextOptions, output: CodeGeneratorOutput, languageGenerator: LanguageGenerator, deviceName: string | undefined, saveStorage: string | undefined) {
Expand All @@ -48,9 +49,17 @@ export class CodeGenerator {

launchOptions = { headless: false, ...launchOptions };
if (generateHeaders) {
const header = this._languageGenerator.generateHeader(browserName, launchOptions, contextOptions, deviceName);
this._output.printLn(header);
this._headerText = this._languageGenerator.generateHeader(browserName, launchOptions, contextOptions, deviceName);
this._footerText = '\n' + this._languageGenerator.generateFooter(saveStorage);
}
this.restart();
}

restart() {
this._currentAction = null;
this._lastAction = null;
if (this._headerText) {
this._output.printLn(this._headerText);
this._output.printLn(this._footerText);
}
}
Expand All @@ -66,7 +75,7 @@ export class CodeGenerator {

performedActionFailed(action: ActionInContext) {
if (this._currentAction === action)
this._currentAction = undefined;
this._currentAction = null;
}

didPerformAction(actionInContext: ActionInContext) {
Expand Down Expand Up @@ -109,7 +118,7 @@ export class CodeGenerator {
if (eraseLastAction && this._lastActionText)
this._output.popLn(this._lastActionText);
const performingAction = !!this._currentAction;
this._currentAction = undefined;
this._currentAction = null;
this._lastAction = actionInContext;
this._lastActionText = this._languageGenerator.generateAction(actionInContext, performingAction);
this._output.printLn(this._lastActionText);
Expand Down
35 changes: 28 additions & 7 deletions src/server/supplements/recorder/outputs.ts
Expand Up @@ -61,23 +61,44 @@ export class OutputMultiplexer implements RecorderOutput {
}
}

export class BufferOutput {
lines: string[] = [];
export class BufferedOutput implements RecorderOutput {
private _lines: string[] = [];
private _buffer: string | null = null;
private _language: string | null = null;

constructor(language?: string) {
this._language = language || null;
}

printLn(text: string) {
this.lines.push(...text.trimEnd().split('\n'));
this._buffer = null;
this._lines.push(...text.trimEnd().split('\n'));
}

popLn(text: string) {
this.lines.length -= text.trimEnd().split('\n').length;
this._buffer = null;
this._lines.length -= text.trimEnd().split('\n').length;
}

buffer(): string {
return this.lines.join('\n');
if (this._buffer === null) {
this._buffer = this._lines.join('\n');
if (this._language)
this._buffer = hljs.highlight(this._language, this._buffer).value;
}
return this._buffer;
}

clear() {
this._lines = [];
this._buffer = null;
}

flush() {
}
}

export class FileOutput extends BufferOutput implements RecorderOutput {
export class FileOutput extends BufferedOutput implements RecorderOutput {
private _fileName: string;

constructor(fileName: string) {
Expand Down Expand Up @@ -147,7 +168,7 @@ export class TerminalOutput implements RecorderOutput {
flush() {}
}

export class FlushingTerminalOutput extends BufferOutput implements RecorderOutput {
export class FlushingTerminalOutput extends BufferedOutput implements RecorderOutput {
private _output: Writable

constructor(output: Writable) {
Expand Down
33 changes: 33 additions & 0 deletions src/server/supplements/recorder/state.ts
@@ -0,0 +1,33 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

export type UIState = {
mode: 'inspecting' | 'recording' | 'none',
drawerVisible: boolean
}

export type SetUIState = {
mode?: 'inspecting' | 'recording' | 'none',
drawerVisible?: boolean
}

export type State = {
canResume: boolean,
isController: boolean,
isPaused: boolean,
codegenScript: string,
uiState: UIState,
}
45 changes: 34 additions & 11 deletions src/server/supplements/recorderSupplement.ts
Expand Up @@ -28,11 +28,11 @@ import { PythonLanguageGenerator } from './recorder/python';
import { ProgressController } from '../progress';
import * as recorderSource from '../../generated/recorderSource';
import * as consoleApiSource from '../../generated/consoleApiSource';
import { FileOutput, FlushingTerminalOutput, OutputMultiplexer, RecorderOutput, TerminalOutput, Writable } from './recorder/outputs';
import { BufferedOutput, FileOutput, FlushingTerminalOutput, OutputMultiplexer, RecorderOutput, TerminalOutput, Writable } from './recorder/outputs';
import type { State, UIState } from './recorder/state';

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

const symbol = Symbol('RecorderSupplement');

Expand All @@ -45,10 +45,11 @@ export class RecorderSupplement {
private _timers = new Set<NodeJS.Timeout>();
private _context: BrowserContext;
private _resumeCallback: (() => void) | null = null;
private _recorderState: { mode: Mode };
private _recorderUIState: UIState;
private _paused = false;
private _app: App;
private _output: OutputMultiplexer;
private _bufferedOutput: BufferedOutput;

static getOrCreate(context: BrowserContext, app: App, params: channels.BrowserContextRecorderSupplementEnableParams): Promise<RecorderSupplement> {
let recorderPromise = (context as any)[symbol] as Promise<RecorderSupplement>;
Expand All @@ -63,7 +64,10 @@ export class RecorderSupplement {
constructor(context: BrowserContext, app: App, params: channels.BrowserContextRecorderSupplementEnableParams) {
this._context = context;
this._app = app;
this._recorderState = { mode: app === 'codegen' ? 'recording' : 'none' };
this._recorderUIState = {
mode: app === 'codegen' ? 'recording' : 'none',
drawerVisible: false
};
let languageGenerator: LanguageGenerator;
switch (params.language) {
case 'javascript': languageGenerator = new JavaScriptLanguageGenerator(); break;
Expand All @@ -80,6 +84,8 @@ export class RecorderSupplement {
write: (text: string) => context.emit(BrowserContext.Events.StdOut, text)
};
const outputs: RecorderOutput[] = [params.terminal ? new TerminalOutput(writable, highlighterType) : new FlushingTerminalOutput(writable)];
this._bufferedOutput = new BufferedOutput(highlighterType);
outputs.push(this._bufferedOutput);
if (params.outputFile)
outputs.push(new FileOutput(params.outputFile));
this._output = new OutputMultiplexer(outputs);
Expand Down Expand Up @@ -114,16 +120,33 @@ export class RecorderSupplement {
await this._context.exposeBinding('playwrightRecorderCommitAction', false,
(source: BindingSource, action: actions.Action) => this._generator.commitLastAction());

await this._context.exposeBinding('playwrightRecorderState', false, () => {
return {
state: this._recorderState,
app: this._app,
paused: this._paused
await this._context.exposeBinding('playwrightRecorderClearScript', false,
(source: BindingSource, action: actions.Action) => {
this._bufferedOutput.clear();
this._generator.restart();
if (this._app === 'codegen') {
for (const page of this._context.pages())
this._onFrameNavigated(page.mainFrame(), page);
}
});

await this._context.exposeBinding('playwrightRecorderState', false, ({ page }) => {
const state: State = {
isController: page === this._context.pages()[0],
uiState: this._recorderUIState,
canResume: this._app === 'pause',
isPaused: this._paused,
codegenScript: this._bufferedOutput.buffer()
};
return state;
});

await this._context.exposeBinding('playwrightRecorderSetState', false, (source, state) => {
this._recorderState = state;
await this._context.exposeBinding('playwrightRecorderSetUIState', false, (source, state: UIState) => {
const isController = source.page === this._context.pages()[0];
if (isController)
this._recorderUIState = { ...this._recorderUIState, ...state };
else
this._recorderUIState = { ...this._recorderUIState, mode: state.mode };
this._output.setEnabled(state.mode === 'recording');
});

Expand Down
6 changes: 3 additions & 3 deletions test/pause.spec.ts
Expand Up @@ -27,7 +27,7 @@ it('should pause and resume the script', async ({page}) => {
const resumePromise = (page as any)._pause().then(() => resolved = true);
await new Promise(x => setTimeout(x, 0));
expect(resolved).toBe(false);
await page.click('.playwright-resume');
await page.click('#pw-button-resume');
await resumePromise;
expect(resolved).toBe(true);
});
Expand All @@ -38,7 +38,7 @@ it('should pause through a navigation', async ({page, server}) => {
await new Promise(x => setTimeout(x, 0));
expect(resolved).toBe(false);
await page.goto(server.EMPTY_PAGE);
await page.click('.playwright-resume');
await page.click('#pw-button-resume');
await resumePromise;
expect(resolved).toBe(true);
});
Expand All @@ -50,7 +50,7 @@ it('should pause after a navigation', async ({page, server}) => {
const resumePromise = (page as any)._pause().then(() => resolved = true);
await new Promise(x => setTimeout(x, 0));
expect(resolved).toBe(false);
await page.click('.playwright-resume');
await page.click('#pw-button-resume');
await resumePromise;
expect(resolved).toBe(true);
});

0 comments on commit e50f11c

Please sign in to comment.