diff --git a/.storybook/preview.js b/.storybook/preview.js
index cf7ffaf154..2b0a6c869b 100644
--- a/.storybook/preview.js
+++ b/.storybook/preview.js
@@ -27,7 +27,7 @@ export const parameters = {
addDecorator(storyFn => {
applyTheme();
- return
+ return
{storyFn()}
});
diff --git a/src/server/browser.ts b/src/server/browser.ts
index 285634fd32..5b61654f71 100644
--- a/src/server/browser.ts
+++ b/src/server/browser.ts
@@ -31,7 +31,8 @@ export interface BrowserProcess {
}
export type PlaywrightOptions = {
- contextListeners: ContextListener[]
+ contextListeners: ContextListener[],
+ isInternal: boolean
};
export type BrowserOptions = PlaywrightOptions & {
diff --git a/src/server/browserContext.ts b/src/server/browserContext.ts
index 179d0580ab..53dd515a06 100644
--- a/src/server/browserContext.ts
+++ b/src/server/browserContext.ts
@@ -244,8 +244,6 @@ export abstract class BrowserContext extends EventEmitter {
async _loadDefaultContext(progress: Progress) {
const pages = await this._loadDefaultContextAsIs(progress);
- if (pages.length !== 1 || pages[0].mainFrame().url() !== 'about:blank')
- throw new Error(`Arguments can not specify page to be opened (first url is ${pages[0].mainFrame().url()})`);
if (this._options.isMobile || this._options.locale) {
// Workaround for:
// - chromium fails to change isMobile for existing page;
diff --git a/src/server/chromium/chromium.ts b/src/server/chromium/chromium.ts
index 490137af3b..d4c79ef7ac 100644
--- a/src/server/chromium/chromium.ts
+++ b/src/server/chromium/chromium.ts
@@ -122,7 +122,7 @@ export class Chromium extends BrowserType {
}
}
-const DEFAULT_ARGS = [
+export const DEFAULT_ARGS = [
'--disable-background-networking',
'--enable-features=NetworkService,NetworkServiceInProcess',
'--disable-background-timer-throttling',
diff --git a/src/server/chromium/crPage.ts b/src/server/chromium/crPage.ts
index cb575c87ba..5aa3892b01 100644
--- a/src/server/chromium/crPage.ts
+++ b/src/server/chromium/crPage.ts
@@ -41,6 +41,7 @@ import { VideoRecorder } from './videoRecorder';
const UTILITY_WORLD_NAME = '__playwright_utility_world__';
+export type WindowBounds = { top?: number, left?: number, width?: number, height?: number };
export class CRPage implements PageDelegate {
readonly _mainFrameSession: FrameSession;
@@ -64,6 +65,11 @@ export class CRPage implements PageDelegate {
// of new popup targets.
readonly _nextWindowOpenPopupFeatures: string[][] = [];
+ static mainFrameSession(page: Page): FrameSession {
+ const crPage = page._delegate as CRPage;
+ return crPage._mainFrameSession;
+ }
+
constructor(client: CRSession, targetId: string, browserContext: CRBrowserContext, opener: CRPage | null, hasUIWindow: boolean) {
this._targetId = targetId;
this._opener = opener;
@@ -380,8 +386,8 @@ class FrameSession {
async _initialize(hasUIWindow: boolean) {
if (hasUIWindow &&
- !this._crPage._browserContext._browser.isClank() &&
- !this._crPage._browserContext._options.noDefaultViewport) {
+ !this._crPage._browserContext._browser.isClank() &&
+ !this._crPage._browserContext._options.noDefaultViewport) {
const { windowId } = await this._client.send('Browser.getWindowForTarget');
this._windowId = windowId;
}
@@ -855,14 +861,28 @@ class FrameSession {
else if (process.platform === 'darwin')
insets = { width: 2, height: 80 };
}
- promises.push(this._client.send('Browser.setWindowBounds', {
- windowId: this._windowId,
- bounds: { width: viewportSize.width + insets.width, height: viewportSize.height + insets.height }
+ promises.push(this.setWindowBounds({
+ width: viewportSize.width + insets.width,
+ height: viewportSize.height + insets.height
}));
}
await Promise.all(promises);
}
+ async windowBounds(): Promise
{
+ const { bounds } = await this._client.send('Browser.getWindowBounds', {
+ windowId: this._windowId!
+ });
+ return bounds;
+ }
+
+ async setWindowBounds(bounds: WindowBounds) {
+ return await this._client.send('Browser.setWindowBounds', {
+ windowId: this._windowId!,
+ bounds
+ });
+ }
+
async _updateEmulateMedia(initial: boolean): Promise {
if (this._crPage._browserContext._browser.isClank())
return;
diff --git a/src/server/playwright.ts b/src/server/playwright.ts
index aab14b89fd..9b1366684d 100644
--- a/src/server/playwright.ts
+++ b/src/server/playwright.ts
@@ -19,6 +19,7 @@ import { Tracer } from '../trace/tracer';
import * as browserPaths from '../utils/browserPaths';
import { Android } from './android/android';
import { AdbBackend } from './android/backendAdb';
+import { PlaywrightOptions } from './browser';
import { Chromium } from './chromium/chromium';
import { Electron } from './electron/electron';
import { Firefox } from './firefox/firefox';
@@ -34,15 +35,17 @@ export class Playwright {
readonly electron: Electron;
readonly firefox: Firefox;
readonly webkit: WebKit;
- readonly options = {
- contextListeners: [
- new InspectorController(),
- new Tracer(),
- new HarTracer()
- ]
- };
+ readonly options: PlaywrightOptions;
- constructor(packagePath: string, browsers: browserPaths.BrowserDescriptor[]) {
+ constructor(isInternal: boolean, packagePath: string, browsers: browserPaths.BrowserDescriptor[]) {
+ this.options = {
+ isInternal,
+ contextListeners: isInternal ? [] : [
+ new InspectorController(),
+ new Tracer(),
+ new HarTracer()
+ ]
+ };
const chromium = browsers.find(browser => browser.name === 'chromium');
this.chromium = new Chromium(packagePath, chromium!, this.options);
@@ -57,6 +60,6 @@ export class Playwright {
}
}
-export function createPlaywright() {
- return new Playwright(path.join(__dirname, '..', '..'), require('../../browsers.json')['browsers']);
+export function createPlaywright(isInternal = false) {
+ return new Playwright(isInternal, path.join(__dirname, '..', '..'), require('../../browsers.json')['browsers']);
}
diff --git a/src/server/supplements/injected/recorder.ts b/src/server/supplements/injected/recorder.ts
index 60c1724a2f..f9fc47c91a 100644
--- a/src/server/supplements/injected/recorder.ts
+++ b/src/server/supplements/injected/recorder.ts
@@ -28,13 +28,11 @@ declare global {
playwrightRecorderState: () => Promise;
playwrightRecorderSetUIState: (state: SetUIState) => Promise;
playwrightRecorderResume: () => Promise;
- playwrightRecorderClearScript: () => Promise;
+ playwrightRecorderShowRecorderPage: () => Promise;
}
}
const scriptSymbol = Symbol('scriptSymbol');
-const pressRecordMessageElement = html`Press ⬤ to start recording`;
-const performActionsMessageElement = html`Perform actions to record`;
export class Recorder {
private _injectedScript: InjectedScript;
@@ -51,18 +49,12 @@ export class Recorder {
private _expectProgrammaticKeyUp = false;
private _pollRecorderModeTimer: NodeJS.Timeout | undefined;
private _outerToolbarElement: HTMLElement;
- private _outerDrawerElement: HTMLElement;
private _toolbar: Element$;
- private _drawer: Element$;
- private _drawerTimeout: NodeJS.Timeout | undefined;
private _state: State = {
- codegenScript: '',
canResume: false,
uiState: {
mode: 'none',
- drawerVisible: false
},
- isController: true,
isPaused: false
};
@@ -122,10 +114,14 @@ export class Recorder {
this._toolbar = html`
${commonStyles()}
-
-
-
-
+
+
+
+
+
+
+
+
@@ -140,77 +136,12 @@ export class Recorder {
-
-
-
-
-
`;
this._outerToolbarElement = html``;
const toolbarShadow = this._outerToolbarElement.attachShadow({ mode: 'open' });
toolbarShadow.appendChild(this._toolbar);
- this._drawer = html`
-
- ${commonStyles()}
- ${highlighterStyles()}
-
- ${pressRecordMessageElement}
-
-
-
-
-
-
-
-
-
-
-
- `;
- this._outerDrawerElement = html``;
- const drawerShadow = this._outerDrawerElement.attachShadow({ mode: 'open' });
- drawerShadow.appendChild(this._drawer);
-
this._hydrate();
this._refreshListenersIfNeeded();
setInterval(() => {
@@ -218,7 +149,7 @@ export class Recorder {
if ((window as any)._recorderScriptReadyForTest)
(window as any)._recorderScriptReadyForTest();
}, 500);
- this._pollRecorderMode(true).catch(e => {});
+ this._pollRecorderMode(true).catch(e => console.log(e)); // eslint-disable-line no-console
}
private _hydrate() {
@@ -237,25 +168,12 @@ export class Recorder {
this._updateUIState({ mode: 'none' });
window.playwrightRecorderResume().catch(() => {});
});
- this._toolbar.$('#pw-button-drawer').addEventListener('click', () => {
- if (this._toolbar.$('#pw-button-drawer').classList.contains('disabled'))
+ this._toolbar.$('#pw-button-playwright').addEventListener('click', () => {
+ if (this._toolbar.$('#pw-button-playwright').classList.contains('disabled'))
return;
- this._toolbar.$('#pw-button-drawer').classList.toggle('toggled');
- this._updateUIState({ drawerVisible: this._toolbar.$('#pw-button-drawer').classList.contains('toggled') });
+ this._toolbar.$('#pw-button-playwright').classList.toggle('toggled');
+ window.playwrightRecorderShowRecorderPage().catch(() => {});
});
- this._drawer.$('#pw-button-copy').addEventListener('click', () => {
- if (this._drawer.$('#pw-button-copy').classList.contains('disabled'))
- return;
- copy(this._drawer.$('x-pw-code').textContent || '');
- });
- this._drawer.$('#pw-button-clear').addEventListener('click', () => {
- window.playwrightRecorderClearScript().catch(() => {});
- });
- this._drawer.$('#pw-button-close').addEventListener('click', () => {
- this._toolbar.$('#pw-button-drawer').classList.toggle('toggled', false);
- this._updateUIState({ drawerVisible: false });
- });
- this._drawer.$('x-pw-code span').addEventListener('click', () => this._toggleRecording());
}
private _refreshListenersIfNeeded() {
@@ -280,7 +198,6 @@ export class Recorder {
];
document.documentElement.appendChild(this._outerGlassPaneElement);
document.documentElement.appendChild(this._outerToolbarElement);
- document.documentElement.appendChild(this._outerDrawerElement);
}
private _toggleRecording() {
@@ -304,40 +221,15 @@ export class Recorder {
return;
}
- const { canResume, isController, isPaused, uiState, codegenScript } = state;
+ const { canResume, isPaused, uiState } = state;
if (uiState.mode !== this._state.uiState.mode) {
this._state.uiState.mode = uiState.mode;
this._toolbar.$('#pw-button-inspect').classList.toggle('toggled', uiState.mode === 'inspecting');
this._toolbar.$('#pw-button-record').classList.toggle('toggled', uiState.mode === 'recording');
this._toolbar.$('#pw-button-resume').classList.toggle('disabled', uiState.mode === 'recording');
- this._updateDrawerMessage();
this._clearHighlight();
}
- if (isController !== this._state.isController)
- this._toolbar.$('#pw-button-drawer-group').classList.toggle('hidden', !isController);
-
- if (isController && uiState.drawerVisible !== this._state.uiState.drawerVisible) {
- this._state.uiState.drawerVisible = uiState.drawerVisible;
- this._toolbar.$('#pw-button-drawer').classList.toggle('toggled', uiState.drawerVisible);
- if (this._drawerTimeout)
- clearTimeout(this._drawerTimeout);
- if (uiState.drawerVisible) {
- this._outerDrawerElement.style.display = 'flex';
- const show = () => this._outerDrawerElement.style.transform = 'translateX(0)';
- if (skipAnimations)
- show();
- else
- window.requestAnimationFrame(show);
- } else {
- this._outerDrawerElement.style.transform = 'translateX(400px)';
- if (!skipAnimations) {
- this._drawerTimeout = setTimeout(() => {
- this._outerDrawerElement.style.display = 'none';
- }, 300);
- }
- }
- }
if (isPaused !== this._state.isPaused) {
this._state.isPaused = isPaused;
this._toolbar.$('#pw-button-resume-group').classList.toggle('hidden', false);
@@ -349,26 +241,10 @@ export class Recorder {
this._toolbar.$('#pw-button-resume-group').classList.toggle('hidden', !canResume);
}
- if (codegenScript !== this._state.codegenScript) {
- this._state.codegenScript = codegenScript;
- this._updateDrawerMessage();
- }
this._state = state;
this._pollRecorderModeTimer = setTimeout(() => this._pollRecorderMode(), 250);
}
- private _updateDrawerMessage() {
- if (!this._state.codegenScript) {
- this._drawer.$('x-pw-code').textContent = '';
- if (this._state.uiState.mode === 'recording')
- this._drawer.$('x-pw-code').appendChild(performActionsMessageElement);
- else
- this._drawer.$('x-pw-code').appendChild(pressRecordMessageElement);
- } else {
- this._drawer.$('x-pw-code').innerHTML = this._state.codegenScript;
- }
- }
-
private _clearHighlight() {
this._hoveredModel = null;
this._activeModel = null;
@@ -712,7 +588,7 @@ export class Recorder {
private async _performAction(action: actions.Action) {
this._performingAction = true;
- await window.playwrightRecorderPerformAction(action).catch(e => {});
+ await window.playwrightRecorderPerformAction(action).catch(() => {});
this._performingAction = false;
// Action could have changed DOM, update hovered model selectors.
@@ -820,7 +696,7 @@ x-pw-button-group {
box-shadow: rgba(0, 0, 0, 0.1) 0px 0.25em 0.5em;
margin: 4px 0px;
}
-x-pw-button-group.vertical {
+x-pw-toolbar.vertical x-pw-button-group {
flex-direction: column;
}
x-pw-button {
@@ -899,51 +775,4 @@ x-pw-icon svg {
`;
}
-function highlighterStyles() {
- return html`
-`;
-}
-
export default Recorder;
diff --git a/src/server/supplements/inspectorController.ts b/src/server/supplements/inspectorController.ts
index bc17c40246..25a5779263 100644
--- a/src/server/supplements/inspectorController.ts
+++ b/src/server/supplements/inspectorController.ts
@@ -18,8 +18,6 @@ import { BrowserContext, ContextListener } from '../browserContext';
import { isDebugMode } from '../../utils/utils';
import { ConsoleApiSupplement } from './consoleApiSupplement';
import { RecorderSupplement } from './recorderSupplement';
-import { Page } from '../page';
-import { ConsoleMessage } from '../console';
export class InspectorController implements ContextListener {
async onContextCreated(context: BrowserContext): Promise {
@@ -30,9 +28,6 @@ export class InspectorController implements ContextListener {
language: 'javascript',
terminal: true,
});
- context.on(BrowserContext.Events.Page, (page: Page) => {
- page.on(Page.Events.Console, (message: ConsoleMessage) => context.emit(BrowserContext.Events.StdOut, message.text() + '\n'));
- });
}
}
async onContextWillDestroy(context: BrowserContext): Promise {}
diff --git a/src/server/supplements/recorder/outputs.ts b/src/server/supplements/recorder/outputs.ts
index 47f4c0beee..7550dfd502 100644
--- a/src/server/supplements/recorder/outputs.ts
+++ b/src/server/supplements/recorder/outputs.ts
@@ -64,15 +64,16 @@ export class OutputMultiplexer implements RecorderOutput {
export class BufferedOutput implements RecorderOutput {
private _lines: string[] = [];
private _buffer: string | null = null;
- private _language: string | null = null;
+ private _onUpdate: ((text: string) => void);
- constructor(language?: string) {
- this._language = language || null;
+ constructor(onUpdate: (text: string) => void = () => {}) {
+ this._onUpdate = onUpdate;
}
printLn(text: string) {
this._buffer = null;
this._lines.push(...text.trimEnd().split('\n'));
+ this._onUpdate(this.buffer());
}
popLn(text: string) {
@@ -81,17 +82,15 @@ export class BufferedOutput implements RecorderOutput {
}
buffer(): string {
- if (this._buffer === null) {
+ 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;
+ this._onUpdate(this.buffer());
}
flush() {
diff --git a/src/server/supplements/recorder/recorderApp.ts b/src/server/supplements/recorder/recorderApp.ts
new file mode 100644
index 0000000000..ea99c1f071
--- /dev/null
+++ b/src/server/supplements/recorder/recorderApp.ts
@@ -0,0 +1,126 @@
+/**
+ * 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.
+ */
+
+import * as os from 'os';
+import * as fs from 'fs';
+import * as path from 'path';
+import * as util from 'util';
+import { CRPage } from '../../chromium/crPage';
+import { Page } from '../../page';
+import { ProgressController } from '../../progress';
+import { createPlaywright } from '../../playwright';
+import { EventEmitter } from 'events';
+import { DEFAULT_ARGS } from '../../chromium/chromium';
+
+const readFileAsync = util.promisify(fs.readFile);
+
+export class RecorderApp extends EventEmitter {
+
+ private _page: Page;
+
+ constructor(page: Page) {
+ super();
+ this._page = page;
+ }
+
+ private async _init() {
+ const icon = await readFileAsync(require.resolve('../../../../lib/web/recorder/app_icon.png'));
+ const crPopup = this._page._delegate as CRPage;
+ await crPopup._mainFrameSession._client.send('Browser.setDockTile', {
+ image: icon.toString('base64')
+ });
+
+ await this._page._setServerRequestInterceptor(async route => {
+ if (route.request().url().startsWith('https://playwright/')) {
+ const uri = route.request().url().substring('https://playwright/'.length);
+ const file = require.resolve('../../../../lib/web/recorder/' + uri);
+ const buffer = await readFileAsync(file);
+ await route.fulfill({
+ status: 200,
+ headers: [
+ { name: 'Content-Type', value: extensionToMime[path.extname(file)] }
+ ],
+ body: buffer.toString('base64'),
+ isBase64: true
+ });
+ return;
+ }
+ await route.continue();
+ });
+
+ await this._page.exposeBinding('playwrightClear', false, (_, text: string) => {
+ this.emit('clear');
+ });
+
+ this._page.once('close', () => {
+ this.emit('close');
+ this._page.context().close().catch(e => console.error(e));
+ });
+
+ await this._page.mainFrame().goto(new ProgressController(), 'https://playwright/index.html');
+ }
+
+ static async open(inspectedPage: Page): Promise {
+ const bounds = await CRPage.mainFrameSession(inspectedPage).windowBounds();
+ const recorderPlaywright = createPlaywright(true);
+ const context = await recorderPlaywright.chromium.launchPersistentContext('', {
+ ignoreAllDefaultArgs: true,
+ args: [
+ ...DEFAULT_ARGS,
+ `--user-data-dir=${path.join(os.homedir(),'.playwright-recorder')}`,
+ '--remote-debugging-pipe',
+ '--app=data:text/html,',
+ `--window-size=300,${bounds.height}`,
+ `--window-position=${bounds.left! + bounds.width! + 1},${bounds.top!}`
+ ],
+ noDefaultViewport: true
+ });
+
+ const controller = new ProgressController();
+ await controller.run(async progress => {
+ await context._browser._defaultContext!._loadDefaultContextAsIs(progress);
+ });
+
+ const [page] = context.pages();
+ const result = new RecorderApp(page);
+ await result._init();
+ await inspectedPage.bringToFront();
+ return result;
+ }
+
+ async setScript(text: string, language: string): Promise {
+ await this._page.mainFrame()._evaluateExpression(((param: { text: string, language: string }) => {
+ (window as any).playwrightSetSource(param);
+ }).toString(), true, { text, language }, 'main');
+ }
+
+ async bringToFront() {
+ await this._page.bringToFront();
+ }
+}
+
+const extensionToMime: { [key: string]: string } = {
+ '.css': 'text/css',
+ '.html': 'text/html',
+ '.jpeg': 'image/jpeg',
+ '.js': 'application/javascript',
+ '.png': 'image/png',
+ '.ttf': 'font/ttf',
+ '.svg': 'image/svg+xml',
+ '.webp': 'image/webp',
+ '.woff': 'font/woff',
+ '.woff2': 'font/woff2',
+};
diff --git a/src/server/supplements/recorder/state.ts b/src/server/supplements/recorder/state.ts
index c69ff615b5..ead711d854 100644
--- a/src/server/supplements/recorder/state.ts
+++ b/src/server/supplements/recorder/state.ts
@@ -16,18 +16,14 @@
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,
}
diff --git a/src/server/supplements/recorderSupplement.ts b/src/server/supplements/recorderSupplement.ts
index 40fc581913..e8bde2ea14 100644
--- a/src/server/supplements/recorderSupplement.ts
+++ b/src/server/supplements/recorderSupplement.ts
@@ -30,13 +30,13 @@ 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';
type BindingSource = { frame: Frame, page: Page };
type App = 'codegen' | 'debug' | 'pause';
const symbol = Symbol('RecorderSupplement');
-
export class RecorderSupplement {
private _generator: CodeGenerator;
private _pageAliases = new Map();
@@ -50,6 +50,8 @@ export class RecorderSupplement {
private _app: App;
private _output: OutputMultiplexer;
private _bufferedOutput: BufferedOutput;
+ private _recorderApp: Promise | null = null;
+ private _highlighterType: string;
static getOrCreate(context: BrowserContext, app: App, params: channels.BrowserContextRecorderSupplementEnableParams): Promise {
let recorderPromise = (context as any)[symbol] as Promise;
@@ -66,7 +68,6 @@ export class RecorderSupplement {
this._app = app;
this._recorderUIState = {
mode: app === 'codegen' ? 'recording' : 'none',
- drawerVisible: false
};
let languageGenerator: LanguageGenerator;
switch (params.language) {
@@ -84,7 +85,13 @@ 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);
+ this._highlighterType = highlighterType;
+ this._bufferedOutput = new BufferedOutput(async text => {
+ if (this._recorderApp) {
+ const app = await this._recorderApp;
+ await app.setScript(text, highlighterType).catch(e => {});
+ }
+ });
outputs.push(this._bufferedOutput);
if (params.outputFile)
outputs.push(new FileOutput(params.outputFile));
@@ -120,33 +127,32 @@ export class RecorderSupplement {
await this._context.exposeBinding('playwrightRecorderCommitAction', false,
(source: BindingSource, action: actions.Action) => this._generator.commitLastAction());
- 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('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('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('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._recorderUIState = { ...this._recorderUIState, ...state };
this._output.setEnabled(state.mode === 'recording');
});
@@ -164,7 +170,7 @@ export class RecorderSupplement {
async pause() {
this._paused = true;
- return new Promise(f => this._resumeCallback = f);
+ return new Promise(f => this._resumeCallback = f);
}
private async _onPage(page: Page) {
@@ -208,6 +214,15 @@ export class RecorderSupplement {
}
}
+ private _clearScript(): void {
+ this._bufferedOutput.clear();
+ this._generator.restart();
+ if (this._app === 'codegen') {
+ for (const page of this._context.pages())
+ this._onFrameNavigated(page.mainFrame(), page);
+ }
+ }
+
private async _performAction(frame: Frame, action: actions.Action) {
const page = frame._page;
const controller = new ProgressController();
@@ -274,4 +289,3 @@ export class RecorderSupplement {
this._generator.signal(pageAlias, page.mainFrame(), { name: 'dialog', dialogAlias: String(++this._lastDialogOrdinal) });
}
}
-
diff --git a/src/web/components/source.stories.tsx b/src/web/components/source.stories.tsx
index cd4de274e5..82d0760305 100644
--- a/src/web/components/source.stories.tsx
+++ b/src/web/components/source.stories.tsx
@@ -33,11 +33,13 @@ const Template: Story = args => ;
export const Primary = Template.bind({});
Primary.args = {
+ language: 'javascript',
text: exampleText()
};
export const HighlightLine = Template.bind({});
HighlightLine.args = {
+ language: 'javascript',
text: exampleText(),
highlightedLine: 11
};
diff --git a/src/web/components/source.tsx b/src/web/components/source.tsx
index 18590e3d7b..0dba2e1fbc 100644
--- a/src/web/components/source.tsx
+++ b/src/web/components/source.tsx
@@ -21,18 +21,20 @@ import '../../third_party/highlightjs/highlightjs/tomorrow.css';
export interface SourceProps {
text: string,
+ language: string,
highlightedLine?: number
}
export const Source: React.FC = ({
- text = '',
+ text,
+ language,
highlightedLine = -1
}) => {
const lines = React.useMemo(() => {
const result = [];
let continuation: any;
for (const line of text.split('\n')) {
- const highlighted = highlightjs.highlight('javascript', line, true, continuation);
+ const highlighted = highlightjs.highlight(language, line, true, continuation);
continuation = highlighted.top;
result.push(highlighted.value);
}
diff --git a/src/web/recorder/app_icon.png b/src/web/recorder/app_icon.png
new file mode 100644
index 0000000000..1c89846649
Binary files /dev/null and b/src/web/recorder/app_icon.png differ
diff --git a/src/web/recorder/index.tsx b/src/web/recorder/index.tsx
index 2958bf0ba1..a84d0e3d2f 100644
--- a/src/web/recorder/index.tsx
+++ b/src/web/recorder/index.tsx
@@ -28,5 +28,5 @@ declare global {
(async () => {
applyTheme();
- ReactDOM.render(, document.querySelector('#root'));
+ ReactDOM.render(, document.querySelector('#root'));
})();
diff --git a/src/web/recorder/recorder.stories.tsx b/src/web/recorder/recorder.stories.tsx
index ae92f5bce2..0831f5ca65 100644
--- a/src/web/recorder/recorder.stories.tsx
+++ b/src/web/recorder/recorder.stories.tsx
@@ -17,7 +17,6 @@
import { Story, Meta } from '@storybook/react/types-6-0';
import React from 'react';
import { Recorder, RecorderProps } from './recorder';
-import { exampleText } from '../components/exampleText';
export default {
title: 'Recorder/Recorder',
@@ -33,5 +32,4 @@ const Template: Story = args => ;
export const Primary = Template.bind({});
Primary.args = {
- text: exampleText()
};
diff --git a/src/web/recorder/recorder.tsx b/src/web/recorder/recorder.tsx
index 82b1f50b42..8dea9fda03 100644
--- a/src/web/recorder/recorder.tsx
+++ b/src/web/recorder/recorder.tsx
@@ -20,20 +20,42 @@ import { Toolbar } from '../components/toolbar';
import { ToolbarButton } from '../components/toolbarButton';
import { Source } from '../components/source';
+declare global {
+ interface Window {
+ playwrightClear(): Promise
+ playwrightSetSource: (params: { text: string, language: string }) => void
+ }
+}
+
export interface RecorderProps {
- text: string
}
export const Recorder: React.FC = ({
- text
}) => {
+ const [source, setSource] = React.useState({ language: 'javascript', text: '' });
+ window.playwrightSetSource = setSource;
+
return
- {}}>
- {}}>
+ {
+ copy(source.text);
+ }}>
+ {
+ window.playwrightClear().catch(e => console.error(e));
+ }}>
- {}}>
-
+
;
};
+
+function copy(text: string) {
+ const textArea = document.createElement('textarea');
+ textArea.style.position = 'absolute';
+ textArea.style.zIndex = '-1000';
+ textArea.value = text;
+ document.body.appendChild(textArea);
+ textArea.select();
+ document.execCommand('copy');
+ textArea.remove();
+}
diff --git a/utils/build/build.js b/utils/build/build.js
index 6d772ddb4a..6670fd82bd 100644
--- a/utils/build/build.js
+++ b/utils/build/build.js
@@ -122,4 +122,11 @@ onChanges.push({
script: 'utils/generate_types/index.js',
});
+// Copy images.
+steps.push({
+ command: process.platform === 'win32' ? 'copy' : 'cp',
+ args: ['src/web/recorder/*.png'.replace(/\//g, path.sep), 'lib/web/recorder/'.replace(/\//g, path.sep)],
+ shell: true,
+});
+
watchMode ? runWatch() : runBuild();
diff --git a/utils/check_deps.js b/utils/check_deps.js
index bacde9ed4f..d405c91dff 100644
--- a/utils/check_deps.js
+++ b/utils/check_deps.js
@@ -154,6 +154,8 @@ DEPS['src/service.ts'] = ['src/remote/'];
// CLI should only use client-side features.
DEPS['src/cli/'] = ['src/cli/**', 'src/client/**', 'src/install/**', 'src/generated/', 'src/server/injected/', 'src/debug/injected/', 'src/trace/**', 'src/utils/**'];
+DEPS['src/server/supplements/recorder/recorderApp.ts'] = ['src/server/', 'src/server/chromium/']
+
checkDeps().catch(e => {
console.error(e && e.stack ? e.stack : e);
process.exit(1);