From bf8c30a88b47ed99a19dec5f8649644bff3cd488 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Sun, 31 Jan 2021 16:37:13 -0800 Subject: [PATCH] feat(ui): extract recorder sidebar into a window (#5223) --- .storybook/preview.js | 2 +- src/server/browser.ts | 3 +- src/server/browserContext.ts | 2 - src/server/chromium/chromium.ts | 2 +- src/server/chromium/crPage.ts | 30 ++- src/server/playwright.ts | 23 +- src/server/supplements/injected/recorder.ts | 205 ++---------------- src/server/supplements/inspectorController.ts | 5 - src/server/supplements/recorder/outputs.ts | 13 +- .../supplements/recorder/recorderApp.ts | 126 +++++++++++ src/server/supplements/recorder/state.ts | 4 - src/server/supplements/recorderSupplement.ts | 54 +++-- src/web/components/source.stories.tsx | 2 + src/web/components/source.tsx | 6 +- src/web/recorder/app_icon.png | Bin 0 -> 16565 bytes src/web/recorder/index.tsx | 2 +- src/web/recorder/recorder.stories.tsx | 2 - src/web/recorder/recorder.tsx | 34 ++- utils/build/build.js | 7 + utils/check_deps.js | 2 + 20 files changed, 269 insertions(+), 255 deletions(-) create mode 100644 src/server/supplements/recorder/recorderApp.ts create mode 100644 src/web/recorder/app_icon.png 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 0000000000000000000000000000000000000000..1c898466493ebb26f97a58ea33348cf34a5df648 GIT binary patch literal 16565 zcmeHu1y`F*v~_SRuEh(%o#NgS+})wLOL2FK6QsBmcPQ>qC?&YNyG!w+`SRXB@cn>$ zpRBB`^~gLYXU>_KvuE#URb?4WbW(Hx0Dvhc3sMIF;1d7)q9Vi21o!fd!G4g<6l6dE z=zpId-DN2NfP|eK=#!@R%6Tq|x2DGew7%A0!{cGyC}8~+1Nbo|)8b&`yBs4rUh?tl zMkd@qs3ozE=VFJa+8Ludo*i{mD9txuZHLsNw3eDG4wEfTCBbnw@J5^;NYX!NG1C2U zbMht!#S@9!?Qr8J_eQwJfFEYW{~iAy5B$TmY`@mm)3Zu-s627g%XG~v6q${wf(HPo zIg2`sa>SI{_fG1MdlJ(4&F_g~9;< zHlK{swBE~$tW-ZE>ZNok25{7p0szGw-ea2YYbhCSB3P)|Xt>$e8S>K}u?Zn(uQiV^Kb;Sye&3S5U>w!CfHXxuP+q|5NrV!D`n)D){xK#SyYcP@O;=g zfd@$7^%1$zOHV>m9AclWb{9X>X*D73Ik*O7U6Xl4JgFOc004=c#&r)g@d^2g{oD2* zm%Z=qb)#iE(QDS1>nx#g`LO>lOAw!zJ!LGygLQaS1sTmUXQO5#)W;`;`1T~vljXzQ zC;M(;tEH6~yN^3>a-9VG%BGQ$u3`3?yM#C&mL#%>9t97?K$6u-LLB0Fnxy=gvfv!1 z8ztw7j9Su26(j%v2-p|RJ0l4wr#$Oq+Sr-im?l&yuvGcfOlRFVy@ih9x2BmbGiY z;l_=sOGPYg8At?Yn|-8UfK9w5ivepIzQ_&QT;$DZwJ2w5oSK2S2cfNr>uz|@%qAuX zvzj_0B7!*Oi^CZs^+-}T=>Q|8xVG(Z7QsjE{67q+06=t%*lo^3*O8=bUOFRojGjbb z^^AJ0a&}caUOp^r2HtB=atLzxhz&HDkhn4;!3j2U>wJ|LGrB_+gY|Ayx;M$ANYHfT zOUc=|IxZNiJ&EnO(Ustz1ss6-ej4Hs1pk=~K}B9oVgf|lK)>uhPvYMJ0N}7(ak}Uv z>XKhFQf3CRtubp|2{4dmR-B~$ivHVYCs)Y#=R=JHotbg>;AU{2lx?dak?fAv+KHk19O<3 zk|Oy`m|&K>?l-J60J)+)GdJ^re&X_4ekpnq6LFGBXuGOnGM6xSfMjbouKe&~H4Z6E z1kX|ai+{S#;1_`$HTK(tFPpGC!ee_YIzyTAn97w1uLStT3t`vNJj5^F0RX50{>>B6 zt5hE8^Z_Sgzd=f=c3;K|901@i1LYq>lc1e~D}8JQTX#7J{PPi|N?SYO3nBnOfd47u zKt+N(Z`?lIJBI@}a$Jqzg$bbEw>2Vdo`ha$4Vp$t|46ux8-Kr_$-edp5d1x2zO(Lu zfiiB@6SQ=TQjeNSt9GWLs3R68j0Q-If#^W5S4I9c5kYitb;0V7xKeuB?-yW+qz>;` z@Y)I_VnEO|p$O)5r=iLGA@G4kV?t7r20+~ou6y@_3NzAkCBZ0ux2ks8ThDD}F0MJ| zT9M^~2Ou$1FntsIN@*ghHU+?qx~OK0k*7T6t6crlb_xK@5Mzhk*x|*N0{kW)`mP{W ziZRuScXIme9mE$Bu*CjQ;63Gh^JD&N^|JsE_MoP0xnRmju-8bLb;3fLZP2$)TmUsM z++#QUTlOnr*`S)S(e>X#6)RfW(~M?wzrGKbQZ_m(>T09!soTldFGoTvLk( zo#Mq}OIM5OZ6I6vYGsY2}RDda_H0(v=8D??=q-~t)uRrHoe;tTFW$;#0<@_8_ z;#1l;uWAz4Z}tp`{h@halK-rHae1W)0E9MNrS*^UVYY033azhD5?FDlS>3~M_rHTg z-fV@_IgRM>9-9Wo|=^}HjL`PTutSjx&$yvrN^T~`WsazyD6?zF*33>{(_`6C_HsgoRs%;M7mmfs>26xjOY%Ia--pWwy&A zk~4Y6Cai+;ZahCJ$Es=`h)Z8u@^!az3}OBG_mr!uW7GNDFC=_wYu>2G=Vt)oa_e@K zw4cE?HzEPfDI>D7?oMge_<*ED=(24Cn?-&rh@-!rBo~SyWA5(%F{6Tc%9h#fW}eXJ zdM##ca~fw@p)eEtrD~al$JyR;Y}H49MCP4EO3R~E@B@i)^mN0?Oz=7kMgSv&rWHNo;Zkvs=Oj)+}=V>BFI(y!i6|M;E!+#%% zkoL018bR=fmM*@qzT~eHHN8Xc#+w0Nytp%^gAtyS6D_&HpZ5spLje!Ozt9xcVAIWZ zPKmkw_OXPfR9InL_9SrNoS}%0^!yw{=BotU@m&`>%rR+#krC!;G1FWWU6e<71g17E z?;1p93+um=`)ja*b{!rbsSOnJWqw@Ix%`^hbQ;p!>*G>@r2R$+jaqW)-W?ID+mCqq zlt%X^G+=Jq*D-mx=1&0&K{iKNBsMgi1$cvSzLR#owt7(Bi1HfGMxKfuh=$@5J5cC} z{E_^*uN)u&*Z%zGFJM}6C_d7-^_@;DB2}EhgC)%StrEG9%a?T*v5L$huzb*wq9yIn zuejlh1G&S6#wi!L%&u&I?x)vJCC(SUU21znB;c1vcD+mDL}+4%Wp~0$~}gqBlk}fTfm&jd7p8r+2?ik zuFEfrB}MaoY+v!gzA9T6H%LHMmtT0TUysE~@gA||Qm?e_sFVfaaC`J*c7T((M6HjY zn#Hcn@cl5My1VGCzEb%5UK6VGsq(`b1bY-cnPk;2Nr;-gok;2H>Du7IgmENa)rT@l z4!FlOY{Q;sp=+w-*4yEsAXycOYi9+fE8WreJVqW_hp{mgnF+oL>Iq-m;upKxCd@M> zY3>x)sb}Nko!n}-ewp6|+VstEoJ8u@*}AO08J@N%mb)2j)Ouv=B>r;5qbc{GoUO3w z-Arh)b@5NAPh0jlg32DeKoog%8s-vy+7v6;Nl}Hd($#yG!+}bpa~|d7KU3lMyw28j zhRc3Kw02~~9|-O_z62(%$ly-l;(bf$S4*KbjP~ZBa4h1#hi`};@j_q&<5gc`=(+Uc z@!zdE2v}C=N%e5kh11GhYX3wI9MEw(MduP&w`k}P*!#4A#I$K-BLj`ReJCjV{a#^e zj_||;*zDrny%DS1?Z;enm_6(tyPj6#@-*Fi_jM@DpTgql;-vuQ8ZjuB6j~&!=xX+D z5=zw}6=8VMIMH>QlFL*5P}qRBgjg#IrOEXg>e+t*F!uRPGPH%cecpVo9D~<4(8~|b zC%ksJ3pEH;;w4KT?A zZ)^Cz`6POLYQaV?4OD|;VBLR#c)uraAYfb8S>g6=Eh!2XarT<)+lCa6uzq+1gL7qn zD8MXLLb7HZMg=;1t7Ak-L#`;0EZUaa-(!hO`y`T3m?Mn_jdi1;xmRRgYhjW%0BIAu z3W3Bp8!KuO$fQOn+h^wAQKI{}w98*0_+EG4@Y8&ukS;?3bGfv?cAX4fNNIiRokhW@ z$ktRvBC_vBd@3Hf%Y&C>xL{q-LWvK{fO3lemI)DZY}A=j{$K#SD>{}qACV)R-g_YQ z18Z7xFKZ-i+RtIwok#5M=Oe|Ja7nXs>n+Ir_br3wLhoSh*3+xy#S%IMoI}_Zw^BPQ zjLo+a=`x{H6H3vz^gt3nA@m{nRv(6(00-8r-Svor0jY&w&*BFgQHULLN;gE;RCwob zJ|bF(dz}gJ*SOB>0Bb6;B2gAKd>qq3?Vm4@*`Q+t23FHky~5y+X=k|Fl<73^{Q+}o z5+k|VJoxfox}HhJyy0KJ$+#77g7`me%oTyYR$5o==uWEdZ#PB1j~uGc`*F}wYC(+i zc&jL2ULj4Ls;^avI+O!@A8~9IZi1L+%Ln;aYL?J5apLO5n;CsmmZ;B9i1QP1hfN-t zUW86_F88l*F(QFHi^tUkrMZ=mr@7%l+m^&3@?$aOG#>b5L9$2e=#{H}6$}pLRN87{ zVX=&W+ZQeY-z%^elvNgC`|}FsjCr*pUf~C-y5;YWi*VVpIR1S|wC@^SAe!KZ3MCe; zb3*N3@_G^(kGdwY=&V;NyRSu1b>olnK2jnZ-AM_3M99%%8(zdQ&;I!d#otv3<~|ZF z4ztHf12j4vPzWav=BJYu<%ajg>1m)Jw$ZC~G-M>=^4oxI@=8+IO zO7M76y__MD=M?FL8b0QkC|B-E655Ez62xauNjnw4r5JLAS^s#BeCQ`Fcqz3L0NXYYF`6pRoZMithvI;9;j?+G6_pD2QGW zZByK|&)vFIhK)*g4x9|Dk^NDcoz1%!`$MIDUYE$NBXj%s+%pv1Ylc3&qy8WTcO>nH ztK;#^hr24=To-rFeT2mYj5;mNQ~M-n;?I4p(wt!Twb%_Sp5r?pdH27;&c(tEv6;?NIHS-6&W!vfMx&VYnY&49aq6!9Tq1M&Su;vlRWh5D@QQp>E z;X%ANCr-Euav|yvE$wUklTQm@cIvf?4mZmztvIGHsVv@IY~S`e7k<;`DB5)@F`=R% zTV2`e$YN!Om}IsdwZ;^58Zq*Hfmv2xdu>Xzk5KrueyU$`L zV0OIH;Z%WMw5#LppMjnvOT6#>@TaBl2R!#t{aG8Bc6!ZXMgC!osRKO1BgL*Ka-r9p z*SoOEIaQlw_OA1T>8btZSO{%eZFkxTy!tngFZ^JqjPAs$EUp6Q70y}K;f^p!&BF_d zS-oNOC#!2De617tO{hbMSi0V=JNgNP1^mKJa-Q_-g#~RjYK{;`-s;IOL)$k|mUvb- zWd(;;5mA8*zn3;6j@VVl_Faw=xd!3d@1wkR;#5I>iXqNfrv9T|h2bvvPxzkI!>f(0J@hq7iFtQ}HQ zD~waeRc)nQg-s+LE{1A#9NM#N?EX%-?Ta6n2GoQ*%C%}nR$8UeCb3`C(~nBBCi!~k z_J-ec-7+S@YM(;G3dTu))whn9bhLPB{U?QqI`+7Mh6_?vsxmwq0aM#cz6_UX zQP8O4G{LgN>M^nC%!VzdQonyLIv^k= znn%pmR#b^Ys*fjXRKl}H@brbj)}ACUQWltff1J$3UBK?5se^COuIrh`;4k8nZa+DDB|91gHrdW|(GlK8(YM0|~Fo+j%Ee`twx1J-lOkt|QUnGI6v z=^gaSsaAhw)Sy~Pt!ETf_-K}eG2IpM5*hC3!H*-5*qTT$n+qn%j zq-L(TF__H({w2BScmKGq+QytE3s(JU{v2rX1|4!yJq5Qqc1-Rph_2VmMAA=wuYCkmZi_snwNn0O>@Fu#>kvh#A7mkD|O5OBU#pG5~MVSj=(ezHO zpG9|2fqNK3_UBPCg#28~=qXh5tSwn+vG+1As#s-cg)&9?ZJA_~-Ey>M;!iS*IjR4BQoAD&$svYu1r_0rRPxK$lI=Dxh}U+=o= z{KeZqY6+2Jpgepp#n4pm+*+;p*ivoicwz_<;xE<+Bf31MN=Y_WLK;k$UE=%1yCOra zeOmWQM|N%{_Fg(GM}NECUwqcK>LW=zf~inK!1v&QY?{X2sWxIQ$Z4w4m}L|6rb_gn z+em;9HU^E7_bGUbyO#t9|I36|q@B`XDP_q|Qc5+#{WSap!M#-HcP%Rt-6%c;A?0u) zGtwK04~%qiE9XMFcqo(5@mMA=}xL|l*_QgVaplBn1H2#E3 z`+$5;JP|6}A|e}1*y?6TGZyAOh9dsefKSBEjc4!V@h{${kdtVtyD~jWKbnlEQQX2w zcu*raNnEj{N2#9ll3mj#LjH$J=ea(r3^i8)of^;L2{kWwAY*otU`AmIhHe-?+YBS{ zhUj;TjkMyJ|MY=)Lc`7VB6s5xt7VySV-#DrC+^3d&9bvLdvU9gCw5Dd^X|A@gTHJ6 z2%D+>y9y^ihysUr{eljiaVVT!&j~(>57tlmDC}x>1~?XcIKw3rX6i4;kzTMSx>9={ z{ISsKx)L}D$>FuzIdSglrbGL~q3Ir-_7S0{) ziW>fKVLAF7I4GjorMlbfSde3~v>0|L(83+WVLYPPWd#>K@kAihlZDlc`ZE612@u69 z+aWW}P-Z_IUman3n$B!@@e(~_sQ-r|^*#uHqQl&44$@8iB2op5_ym4lh-+8plnsfz zMBYCG=O{^rq7=%K7}{$t#ejueDc(3PZ-}qH++d&0i=mWHKcqGPi}k(U*voL!_kbLtQi^YgH@fLvirSRi7cM1gk3p z-TO#Fj(SXjTpm>6&jgb;_C?c^pYaWMdv2rNtF*t1v=17KC@R8ithTa#cw*K=46j(I z4UtFuFgs+je<^+B>`oDF9op5JC%*F3XdhlKEBm)9ej{ADhk%CMSM*d`*C_t;z7Fe# zpLyif1wrQZOQ2Je_BL|v+clZLydC`hXPe|KgBd2f-Kdw4^Tyd03 zgffQ6hfjpf{p&=1N}($A~0A+{Vv|KXgnXk^O5TozSC6JRUk};# z`)BYIe_scU2Ej)hS%?pv6IrhjE2i@ma%6rGpThrnTjwbP?zu( zFAH}|8|2CWSH#t)_|-gc500D@*LXL@N4~ex`>nlynYMH_Fa86|G;kGb&Ze1tW525= z_h7C<^n=>sE?Mh&$eo%E(U~HpMLk4IS56~pRL|_#Z+3&Q|b*$tR!|M?H10b$G5jRFzDbscpLNQbPAR} zqyz=sJDHxi{}8d68$Vy_*FFg1h`2UtseFKjafj#222E|Eh^<)FDg9lmn)NU~1#OnA z5kpS8pF2D{e_AE(KQ=qvRDil5D+{h0gM9@$%jn35te6_TnfPRfd{--UhcPH#!Gm{Q za-Y8zwM@OOPKcpYnqBZ~@;Mg-xsfjCWke6*_)$<`pW{CMOHNgdkiDkv=O+-SYT@)^ z&VvYkqQ9WTPiTfgw{NWampW$LN(^@y<+vB3_g=5fRDLOY>uX#>HKCGsl8 zV;1+6R?+-TwYTdDS(LDEVL$Knm)?UgJ8xcUdM_H*o%E$H55GrA?fH~{E~Y6wBztvY zayMv+wRIP!_{diE)gaP?o*5TZG-2)&&fK$5gCD|~M{%hN#B36>GIH$ZY*yjSdZh3Z#Z(SjjE35?&s6=S|Y(9@e5)Uf{O~?8| zgJprxD?9fasxgDdJJRebWr5NYKD)Vy=)c&4^g1fnD0Q4qjCWb8lQFWL3lH8bX5#cr zXyE#_dkKiH*;7kioVKLBQ$$g&7SyR0alr zBJu{ZW1go#S^d%RfP&?}C1rIy1 z+~dWmY6uLSp=+q%By?!kK!N-8R@B%Mn`45+Cfbh)i>bfLeZL7{Ov)ng|9%@v1}%UY0d2{~Hq@|I$-qZR+D zAWG|%p$*g&Va(ig)84jJ4X%9%6MzpGY66@H!0;3YJpuSoRM1>$MiSFt&$qNAP{ujI z>A7!>%vSD1^v|27=IBmkpT_OiA`R24l(c}|H|ee$KH#i5J@p?`5cLs=xY2UL^pDtX z#c|HsaJdt)G!!w6YWbLPFB(zXLhE1|_9St{*!=@sc6@EO%YA*li)V;`C@rg6-5*Ql z>*AIhv$DVA*$JJsI$94g^7+v}hf}0~_|LS{X|5F@o?|V8*QutftEa9OB&e?KTJKN< zUJ%hxNztV2ZBw|5=_%og@JlNtbpF|zmZ4-EoKW1gV1-&ij$yY-)$y8;QA zSXsL=#Ks(9Zi~kH4`?$(+|!8T#&w3u_@vDt4VYZ)d7Oy0e|pa2+hOY6V{tmxq}+3Q zCB6!*6V%dY=?a{iwvzQwP=mNP*}w?Ec%huVk4^@86ZlJTqgbo!V-mb2_CLcs*LNHbonfFnLJdu9mXi zovC=o!;y(pT=Y7ny`1{r-VQ?gz3cKGM-~lHQv2iP;8%}xP4DL=y4m!9&%w@(Z7si! z8KfmF%=cWD%tnJ0wM&z||JnYUt+gG^wHa+n&)JonI3tKwlo-4Pua-ZYwchyk%|e(n zowl5@Z$AVb@=*ovf_sB4dJy4oji3-k4|xn($k!vsX*o}+d#Z`*sd!Fw!A2Km$~q6; zE`Qce1&r?AA8@fKuK`0P$|%XlXgG;$#NS<3VDBCq|K@_Y##U=Iq1<*5aPK*4XX;|6 z3vafQ05LcJ5YOIDY%F##>>il5xy<{WO7D~_H~a={LYVgDHGepCmPiANvM)|eV0uV~ zHEZ=}e{44*mSp29V9KsK=H(knZytU-VS8^_B)B=4%j~v{uwcmU_9iT{ORO6f6gILqUV?FifC>3LMplIs8yOe=r)rT!%Jc0DJ`7G7Fe9 z-PdT^Y5hI=O5YfDiEFa}W$Z*4zGdY2uCG{l#nIWv-@o&>{=C*>u-sq3IVt0R5Zygi zXSgn(9eLj>1gQ`9>jyimNS8Mjn_#%E*mslcU|NH7RPj_ zk$ou)7bNx))Nr57O%IhwlpkwH-G`jqBODq=x>})JYJp@yZ7MZ8?;RcWD^4OW@GZuN z*(pWk_gn_?&XO+;rXR}w?xBUxclM%nT`z<(0`az8vQ@^uuJ93(v>muWL@eGaY>F5m zK3y5>Q}?92(iiRvo5*u4gAHVUY0L@eqiM-X5*C&)tbL;L#v~L)<%LW8mmoAHXNeH6 zG}m|B(ta#jLSRD3dZI@s4l6Z$NA}m^S^c?I!dbJic2;|hO!S#MH)_-gFEW;%9qloT zZ%JPmL&iexu`s+q?7IIhRfAB6M?{tJV$z zoEq&-I_;bF&9^RQh*pC94zgTZT#$;bqj!oK!S%*NRj~qv4T9kEw~bzHKJ7+5fs5o& z@2R^;$+Qg1$wktwcBxR#68@Phx zZGL8)lGV<_U|mXrW3Gj135B8l=IU(BRl>=Ao+-bz&43&MH-_uuOqY@5kpP=TubbTG zt`HEztj7feUNFQKOVa}FKY)FX)Ptv$T;n)L@yF?M2OKvmL$+~pvt(0|s5C+CX-seq zes0D;jZ81BHR_;DN8ndHr8~U?2N>35MY~o1CmrLFE_d}3ljkItIZj;&{W08?;ybYH zv@c+Q^s!5IQA2C&mEI7-tjQnUBHRhI_iIAj6Crb0F7dAX!PgCH7Ebb^whE-oX$W7q zY-n80|7{-INMmpDBQoF{3olDUIBgYLLK}WOiactSYaIl$F!i~7MhroPO}$Cza{rd|65~6D-Yy@N zTzyYM>2vjjf7hEl+T=HXMw~PM zd?p`3xUc6jHdDX5!hEjvch0PKXy!@<*|$8up+r+^VK|$$mP3zXvkU`w-OE%^p>y|V zdD)ygLq}HYd30-3PD)&_Q7i3kM9yGZXc!~??ne{r?Tiv`o_b~|p|?#^ipJ5C)(^GI z(11g(OQkn8Adr&&(82U9AUy#Q^ChVN0twegGS7j0HA5EeSe4no$*97mEaj^INqci30I!AAX&-17S=W$@3GE8CQhTNx$h&`|5Nm|)$SUM!-B>c6_JbVc6PUfIez z0=b5_;Y+WeHf2@q$L(A5QC`vrQjB6R=?dPGY#j8V(=PiA3@xaXV%VOH-y$LB1-#%19do#(Zqk+GhAh5m~}b zpDUdov~&52UNQk>6cXwnQ))3hn?^JDq_Dc+tog!V+?kkLw~lVjCU1bO!m1T+B|ySMtdQF#IeAXj%Wu7)z(U4OnjVhNbp9)5Q3qZG=L?^NiApD~O_tHR$Dz`01csbHTVF@-%O1Gy3xcrS0lgKFd*todGL0VJQ`Oqh* z*FT+F_w(=05(X;&hcnPq_v>kKOOza+sGA*6twzgq4N)P!sYu|dI{Q^jbclSWb4#sd zUau3JFh6uP?cCet-*n4K@&{X^7I|vgdpKw5m z_wH~W$0!{gbw9zn%k>9bDF&VtrJ`N$4?+dIwbJ!64AsBevjc()Q{D%psk%2{Oo)>T zi(f1L?T`y!aDwXnjC|CNb*r)%`0c1i+$+ylJG6P%F){Qg@2I^A!3uMZ?p|vBCG_*p z?W7H`6-%4sce0p~5zcQBLxl<#1#V*q6+!n8LzUU=h@-u`P{O@;YRu$cFKC^aM4a$G z!|$NpZ~6byqf<+r_wKH6?yA-EOnSg}Ts*{T{LR1Vbj{Xq>W^W7R8%Ay=;bVTL2#+Y zES}7#<7Qc1XRQ!4(_Yd)p3F#?H+5Nb7V{#63ep|E3HjS1$3k;aN!|THiRb$DXB+B2FEY?q;0{u>!&QPX)@F#hs%(^4RVU5NxjTVVZKGf%|>HddSocs&jXq~$eD4c z-3KG2U>~p5NyC+kDCX~MO~j8Ky*cbgxS7e*KWbG?vZDMN11enXx!=B*{}~v_HM`^t zfW-BB;tF&0!tE3|E@&Pc{2rzEBQV>NLGe`#2*yO*d5?-tN-dXTObvCjGS9JX5L#z_oz%(6N(uycS*gJ^w%1Vy?&e2Pr0hl zWqv{KvS*f{IH?)`5>nM5a`3vtvd+6XJKuV7x^NyU1RgdxY<}_UUhU$@$$xGh#YqYl z@$}-5!x+lkt&XF(Ufk~`m~~_U?jEDwUo0H$wOPnVOagaR=-Rz=rn+iU``SxU*;2WD z-&B8~{Ic7*uZD3sKuy4taBz6t$4d_xh3}?5rGxgy3N-FQGx@J<&T+C5m336qK5({# zNx8?CE2DqBq3EOij9xp%Yr_Plo83o6_+|FkZoPp_27%*dt}VhTE;|>x-tz=u3!86j zOG@G^PIdTdw^yH*)R1#aG}w0sN$d6{?F2+{;`4hs;ov?D&FzM&773;OU5GEl!L6GoJ&WgQ zbBrMcTjtF4BKDGu z3_jPE4-R6Ei+O098ZF{#b1r&*Ve2S+h|-po zCS=E9&IHm^H%C{lAeae`%-rxJb=K$7J6Y4*eE8n`Pue@zeoflrm3ov%Xu=Mt7JYj9 z_$`(->8+xOu^q9Pje{GuZt3!qDvZuo-%cq!3M5WGpTWnb(#UiUK|yK?00*%xVZLRI zPbdC-iCk>*%3bvbCaF!4iZzJHj_RhA&C4;-h%zuI3|m`a%71${W+W{23*ubgtxI%N zkZ9J%*I2pKkUJ1#Zoceu3nkM1tt4JX=Ii(~J0K6S>|S~{4U~cM{`s&+j)_{F(zasb zf0&bUcT$y0RT`71YZ<>&#l{|Rwcv`E<(_gHya_DOR-$S+~?QA?fbhN#`5p8#d#eGgf6By+K- zYFAXn9Od_)-VP-Lj~rWUAwjs)H~W2*!6C>OIQzhavaDctdTe!}mz84TL{Izxm66c~ zW7!C6(D|KlCEqo|B#7Z<#HAG7{%nFOL+6P#)qnSkWe=G#9wLaOU1_r+4atwB%WAQ9 z!ren*o#q@xPvSS>tU5?yg3?}TOaRECv6(GDsd$<=V{p{&lj88;BcuIrO!QIfQMh>1 zBkuuA1c>?&f~ubi<5t9u*v5}n0I5f5$*UQcrXUR(@)8r;_DUoFKJgjdwSM>Ww%Jo| znVyf$OG+A3J^g$izrm7hVKMw^TpYdjn-}6Xh2?wESIPO|*|wAwO}s?eLo>AdgZRag z;@~E#$U@luCjByVOXz_DjIBs#P?MXeNrqX~?A6B-8udeMMN%^NGQmwqy|RmcZD0Ld z={1?AM5>rhM*XuO9`kEzx^1$jUDdtlxwCD_X{8Aa~Tg`2lVz%uNvOL?^vtSrUHU0GLk%4*zDU?6H~1BR-dtMnm+)%H5IG z#~>Tz>VOhF(3cYkmbm}%I+k@IQsTH9f%Jvk+NBCW7RetUW%5{02e=kG!;mEyX*ln) z1r?50FC{gFRv40GjQ(3H-wJi{JlhHh&KWT^72XRNbxNnTg0ejHtXeNcv zJb1YHZ58}=Ja$MFeugkjWm2YwKO-(bPE<2PKQ>$`YHhG(jjZ{5@;%?!Y;Ja&T)rwi z>G|BcIhExMRyCa#vLktAw^A61B7#v06FB>La->ATOSyOUDY_sbjd)&r-5^vD34L}Y zI%F8lCsNw=zA1{ynNOo6K%bF!ymqWpjpXBm=DsncE4R$?OceMTj`kv$Jr+}i~z zUR07r`C*|*ZAuXGgFHQ01C{Fz1uHrun&76gg1>592}Ucyt0 z508pbAXJU+kzYv+6A62o2aH?$5@%9-W8^=6(5TBcHul`;-qU3VS6_fM3ch@FK1NAG zHO=yAciBU0d|BOImoOY+a(|5hnb|Vp4j0Y0nOq$>GKrHZaIQ+2ToP;I!~{QL3+8z5 zTvIytRfV(_vfsWHdWhN#sV&SKx@y@3oSztSzY>BzgxpYBG7H=c=yt1u0AS>ugf)jZ zv>N1^cQT-hpF;eq5!$YCVP&|Nu|glHLyaCBh!B5+nXGA{Nl{L21%{+~?|@iYpXED2 zdDt?bTCVFz?KZY-b>7SkZi}mLr_>~G*Hq3Qpt#VYsu#cQU%>*&r@n8cE)R0O+f6m; zzP&?UpSjl;+8GD#5HY3&teH~bu|zL03Y#vcNis3E+Jn8XX?)-G@uG~jqoU>qUVo05 zFMqJaeOyypV}g79B|5ce&T3`Roarx3Pwwkq!*_VTXDsy&h;DxXH4!LMnMjcMx@1?} zvF}2-*q&_Mck$>4H&9dqHOImzqZAhoSEG}dBOoF&R+Q17Op@SA4`$#VsrL z+32ilmiul~{zr6aq8xcSxc4%TVEH|9 zGiFMymGgG3cG0Z>iJ2+=i)SAo4Nb=M!1-g@sD9&uxfl%}+#}!EhuGL^sUEL0Pi=GA3)y^v2+CrE_@0fM6PHL}gybX)9-#;7tTHXCibPbt{u8^GoK^=S z4Ya?+a8IAZDQMLNhA9rGRsDOVK*9+TJIwO-C*I zF;(qeM)QczY`ochcdhJr785@2c{ePPx)@lgBw$ZyFG`x&O3IoC*$hfn6^ zCxa2@_~18?ir>|Qy?17i>8Ne|Orsk_8{{p*%MduS%IAN|cd2*CmmgDNgG#Hn;4=o% z{(EqKW7v`g`U&fIn#aYZ^jt`$iywE~{iCKufoho8;aY>7=>$ z0B%{eXA!U;|Gd$Wge>F)op4;1*>TOufgXE^L_vlPkUN=%2OC zinI9~MRSzWZvFYW*r&X?RK53&JTCWYV3BJpGYJ|%U?E;}5|!*{^^H_RIr1}L(V|hzs!xDa?*MS|pqMu)HC*C1e2y`ZOr34fM;`+`{u5q)HoYPV*98+GNR`3K7 zf1H{x%M`7k2W#RbeUt=tGSetEpv^e(d`~s?_e&oz58R0L&H~7CHF-Wy$|Ck$xPqgS zgxOez*XWjCAolOVf4F%vcfPDJ`~|`#7Vg2e4H$5jh~jryA4eUEfHzdm`=8&@m>_#Z zQSfdFihKeTKdEVREvAFEym{nfQ&kQZsi8@Gd?uYt&z`fo1(`%D*nnW>#030>vN9bp?yEKkU?%f9m-6T#)tiL8l9-~xiJ&pazhurc#%G&pZaa%y1Kp@A zARriky2)Pg{JLJQbj5oGsn}NiUZmoytSLeM`9S!ZpOatTKD?knpamWf4kGEKiBpR{ zSYzME@3!xVW#ypZm<*LbY`nH7nq|)jbLaRz;}}B!2_WHpdw%-LB=`m{Vxs9!($=r= zCenay@qEC0lTOyO19O210;ZqZ3*>MBID*yNrLYonVdqF0$CH=Ic&vhW0YdK?;Mf8F z^tS8oSG2eoxA-O+psmC+cOxbO696g~xeL5S<_rfF^=$v8pIvBD-wy$y$*utR#Dzc< zC`)P)x0WIHc%CCu#3)!^;(#BcsL^)|-SpGz)}Pl;N6OH{*JL%ikU(m+-&~raA!lMH z+=dEsAN<*c!@IfLPT>I4qBDghT6k+$##zx(c_nc5ajnOPkda z0;rwJp9|Tj-+$ELgkJgfp{J3;Pcm6d-E{IID}y`V18}_b z{iKYeOQM=khbg4UEa@uOr|}Z2+nG|JHvbVMVjhcpn)6SeA_lj*NWHiK)CHVEfi|q* zrWM6=-b>B6;o9?&MCrf3nb03!TJKGbrMKua^JY9 zPJjXzXlBgl7r&bFpnEi5UW)NGd>goB8YxyzXiTZqlZ_81do|`qFv+jkecE<{Nv^Qv z2e^eEOqgf{Nr*q-PFzK*YF?{*O2P!8S3egPCuV@_T!c4Pb*W>&|0w8Tc(MNSuv;Mc z$8_@flEWnh9sr7WX`vjC?lGnMx?Z^nq)O@jkdaZoIVP8!+ORS{wsiVdb+u$KX_!7v z+6@Wr!~;+>kx(2GBq;5$0olx4yM17A=HuxsOxM=|R&maRUkeCTJgKs}Jf@WJ9L;M19w#6lW{}Jzw%Wo#5Qaq~6~O(fT`Q>mpqCts?_u<+)@3 zQMxzrY6zIgwI$|gEdR-!S^MH>ammkWZgSBo(AhPK=jrs;%BE~|R2G$|n)4&5F)3Hl zvL&TXZva-=hrf5y|GktO+4=V=2&Yh_X8rBsnkgW-xUA$%=aL;)Ix<#X_9Zq}<#*@( zl-;85{p29w*=xsSCIo|+s|eYDP=x;r?#;DFzBTx~HJoD687_MAkPW(-f{<=U$@Iki zZ2ZdY+4e|gRbCVFRR|oO)EV__%`bur=&txrVNDGtNTgjnd!-QUXBfhD!tYLug51xl z%`?_a_z7N610+Uh&&UhRGy0}CgEKT2Bo+??6~|$z4zM70d*9FR;F>}9fwUk~D~AkS9sH{I$jt9|2P(?009jxecgoGP(jWBKe8oNEU({_w1ZWN9!>P)j(QXTP)AILa0HRlyn11+w?-AVi5+q%u VN?b*Rn)aU; { 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);