From c0610ccef43a48bd41f04991813713c5b098598d Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Fri, 5 Feb 2021 14:24:27 -0800 Subject: [PATCH] feat(recorder): remove recorder overlay toolbar (#5334) --- src/server/supplements/injected/recorder.ts | 295 ++---------------- .../supplements/recorder/recorderApp.ts | 50 ++- src/server/supplements/recorder/state.ts | 28 -- src/server/supplements/recorderSupplement.ts | 97 +++--- src/web/common.css | 5 + src/web/components/toolbarButton.css | 26 +- src/web/components/toolbarButton.tsx | 10 +- src/web/recorder/recorder.css | 8 + src/web/recorder/recorder.tsx | 36 ++- test/pause.spec.ts | 97 +++--- 10 files changed, 248 insertions(+), 404 deletions(-) delete mode 100644 src/server/supplements/recorder/state.ts diff --git a/src/server/supplements/injected/recorder.ts b/src/server/supplements/injected/recorder.ts index 49e8685a0f036..e5f8aa816911e 100644 --- a/src/server/supplements/injected/recorder.ts +++ b/src/server/supplements/injected/recorder.ts @@ -17,8 +17,12 @@ import type * as actions from '../recorder/recorderActions'; import type InjectedScript from '../../injected/injectedScript'; import { generateSelector } from './selectorGenerator'; -import { Element$, html } from './html'; -import type { State, SetUIState } from '../recorder/state'; +import { html } from './html'; + +type Mode = 'inspecting' | 'recording' | 'none'; +type State = { + mode: Mode, +}; declare global { interface Window { @@ -26,8 +30,6 @@ declare global { _playwrightRecorderRecordAction: (action: actions.Action) => Promise; _playwrightRecorderCommitAction: () => Promise; _playwrightRecorderState: () => Promise; - _playwrightRecorderSetUIState: (state: SetUIState) => Promise; - _playwrightRecorderShowRecorderPage: () => Promise; _playwrightRecorderPrintSelector: (text: string) => Promise; _playwrightResume: () => Promise; } @@ -49,14 +51,7 @@ export class Recorder { private _activeModel: HighlightModel | null = null; private _expectProgrammaticKeyUp = false; private _pollRecorderModeTimer: NodeJS.Timeout | undefined; - private _outerToolbarElement: HTMLElement; - private _toolbar: Element$; - private _state: State = { - uiState: { - mode: 'none', - }, - isPaused: false - }; + private _mode: 'none' | 'inspecting' | 'recording' = 'none'; constructor(injectedScript: InjectedScript) { this._injectedScript = injectedScript; @@ -110,101 +105,18 @@ export class Recorder { } `); - - this._toolbar = html` - - ${commonStyles()} - - - - - - - - - - - - -
-
-
-
-
- -
`; - - this._outerToolbarElement = html``; - const toolbarShadow = this._outerToolbarElement.attachShadow({ mode: 'open' }); - toolbarShadow.appendChild(this._toolbar); - - this._hydrate(); this._refreshListenersIfNeeded(); setInterval(() => { this._refreshListenersIfNeeded(); if ((window as any)._recorderScriptReadyForTest) (window as any)._recorderScriptReadyForTest(); }, 500); - this._pollRecorderMode(true).catch(e => console.log(e)); // eslint-disable-line no-console + this._pollRecorderMode().catch(e => console.log(e)); // eslint-disable-line no-console } - private _hydrate() { - this._toolbar.addEventListener('mousedown', e => { - if (e.target !== this._toolbar) - return; - this._outerGlassPaneElement.style.pointerEvents = 'initial'; - this._outerGlassPaneElement.style.cursor = 'grab'; - this._outerGlassPaneElement.setAttribute('tabIndex', '0'); - const offsetLeft = e.pageX - this._outerToolbarElement.offsetLeft; - const offsetTop = e.pageY - this._outerToolbarElement.offsetTop; - const toolbarWidth = this._outerToolbarElement.offsetWidth; - const toolbarHeight = this._outerToolbarElement.offsetHeight; - const glassWidth = this._outerGlassPaneElement.offsetWidth; - const glassHeight = this._outerGlassPaneElement.offsetHeight; - const maxX = glassWidth - toolbarWidth; - const maxY = glassHeight - toolbarHeight; - const onMouseMove = (e: MouseEvent) => { - this._outerToolbarElement.style.top = Math.min(maxY, Math.max(e.pageY - offsetTop, 0)) + 'px'; - this._outerToolbarElement.style.left = Math.min(maxX, Math.max(e.pageX - offsetLeft, 0)) + 'px'; - }; - const onMouseUp = () => { - this._outerGlassPaneElement.removeEventListener('mousemove', onMouseMove); - this._outerGlassPaneElement.removeEventListener('mouseup', onMouseUp); - this._outerGlassPaneElement.removeEventListener('blur', onMouseUp); - this._outerGlassPaneElement.style.pointerEvents = 'none'; - this._outerGlassPaneElement.style.background = 'initial'; - this._outerGlassPaneElement.removeAttribute('tabIndex'); - this._outerGlassPaneElement.style.cursor = 'initial'; - }; - this._outerGlassPaneElement.addEventListener('mousemove', onMouseMove); - this._outerGlassPaneElement.addEventListener('mouseup', onMouseUp); - this._outerGlassPaneElement.addEventListener('blur', onMouseUp); - }); - this._toolbar.$('#pw-button-inspect').addEventListener('click', () => { - if (this._toolbar.$('#pw-button-inspect').classList.contains('disabled')) - return; - this._toolbar.$('#pw-button-inspect').classList.toggle('toggled'); - this._updateUIState({ - mode: this._toolbar.$('#pw-button-inspect').classList.contains('toggled') ? 'inspecting' : 'none' - }); - }); - this._toolbar.$('#pw-button-record').addEventListener('click', () => this._toggleRecording()); - this._toolbar.$('#pw-button-resume').addEventListener('click', () => { - if (this._toolbar.$('#pw-button-resume').classList.contains('disabled')) - return; - this._updateUIState({ mode: 'none' }); - window._playwrightResume().catch(() => {}); - }); - this._toolbar.$('#pw-button-playwright').addEventListener('click', () => { - if (this._toolbar.$('#pw-button-playwright').classList.contains('disabled')) - return; - this._toolbar.$('#pw-button-playwright').classList.toggle('toggled'); - window._playwrightRecorderShowRecorderPage().catch(() => {}); - }); + private _setMode(mode: Mode): void { + this._clearHighlight(); + this._mode = mode; } private _refreshListenersIfNeeded() { @@ -228,61 +140,24 @@ export class Recorder { }, true), ]; document.documentElement.appendChild(this._outerGlassPaneElement); - if (window.top === window) { - let moveCount = 0; - this._listeners.push(addEventListener(document, 'mousedown', e => moveCount = 0)); - this._listeners.push(addEventListener(document, 'mousemove', e => { - ++moveCount; - if (++moveCount === 10) - this._ensureToolbarVisible(); - })); - } - } - - private _ensureToolbarVisible() { - if (!this._outerToolbarElement.parentElement) - document.documentElement.appendChild(this._outerToolbarElement); } - private _toggleRecording() { - this._toolbar.$('#pw-button-record').classList.toggle('toggled'); - this._updateUIState({ - ...this._state.uiState, - mode: this._toolbar.$('#pw-button-record').classList.contains('toggled') ? 'recording' : 'none', - }); - } - - private async _updateUIState(uiState: SetUIState) { - window._playwrightRecorderSetUIState(uiState).then(() => this._pollRecorderMode()); - } - - private async _pollRecorderMode(skipAnimations: boolean = false) { + private async _pollRecorderMode() { + const pollPeriod = 250; if (this._pollRecorderModeTimer) clearTimeout(this._pollRecorderModeTimer); const state = await window._playwrightRecorderState().catch(e => null); if (!state) { - this._pollRecorderModeTimer = setTimeout(() => this._pollRecorderMode(), 250); + this._pollRecorderModeTimer = setTimeout(() => this._pollRecorderMode(), pollPeriod); return; } - const { 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'); + const { mode } = state; + if (mode !== this._mode) { + this._mode = mode; this._clearHighlight(); } - - if (isPaused !== this._state.isPaused) { - this._state.isPaused = isPaused; - if (isPaused) - this._ensureToolbarVisible(); - this._toolbar.$('#pw-button-resume-group').classList.toggle('hidden', !isPaused); - } - - this._state = state; - this._pollRecorderModeTimer = setTimeout(() => this._pollRecorderMode(), 250); + this._pollRecorderModeTimer = setTimeout(() => this._pollRecorderMode(), pollPeriod); } private _clearHighlight() { @@ -315,7 +190,7 @@ export class Recorder { } private _onClick(event: MouseEvent) { - if (this._state.uiState.mode === 'inspecting' && !this._isInToolbar(event.target as HTMLElement)) { + if (this._mode === 'inspecting') { if (this._hoveredModel) { copy(this._hoveredModel.selector); window._playwrightRecorderPrintSelector(this._hoveredModel.selector); @@ -349,19 +224,11 @@ export class Recorder { }); } - private _isInToolbar(element: Element | undefined | null): boolean { - if (element && element.parentElement && element.parentElement.nodeName.toLowerCase().startsWith('x-pw-')) - return true; - return !!element && element.nodeName.toLowerCase().startsWith('x-pw-'); - } - private _shouldIgnoreMouseEvent(event: MouseEvent): boolean { const target = this._deepEventTarget(event); - if (this._isInToolbar(target)) - return true; - if (this._state.uiState.mode === 'none') + if (this._mode === 'none') return true; - if (this._state.uiState.mode === 'inspecting') { + if (this._mode === 'inspecting') { consumeEvent(event); return true; } @@ -389,11 +256,9 @@ export class Recorder { } private _onMouseMove(event: MouseEvent) { - if (this._state.uiState.mode === 'none') + if (this._mode === 'none') return; const target = this._deepEventTarget(event); - if (this._isInToolbar(target)) - return; if (this._hoveredElement === target) return; this._hoveredElement = target; @@ -479,7 +344,8 @@ export class Recorder { this._highlightElements = []; for (const box of boxes) { const highlightElement = pool.length ? pool.shift()! : this._createHighlightElement(); - highlightElement.style.backgroundColor = this._highlightElements.length ? '#f6b26b7f' : '#6fa8dc7f'; + const color = this._mode === 'recording' ? '#dc6f6f7f' : '#6fa8dc7f'; + highlightElement.style.backgroundColor = this._highlightElements.length ? '#f6b26b7f' : color; highlightElement.style.left = box.x + 'px'; highlightElement.style.top = box.y + 'px'; highlightElement.style.width = box.width + 'px'; @@ -509,7 +375,7 @@ export class Recorder { } private _onInput(event: Event) { - if (this._state.uiState.mode !== 'recording') + if (this._mode !== 'recording') return true; const target = this._deepEventTarget(event); if (['INPUT', 'TEXTAREA'].includes(target.nodeName)) { @@ -580,11 +446,11 @@ export class Recorder { } private _onKeyDown(event: KeyboardEvent) { - if (this._state.uiState.mode === 'inspecting') { + if (this._mode === 'inspecting') { consumeEvent(event); return; } - if (this._state.uiState.mode !== 'recording') + if (this._mode !== 'recording') return true; if (!this._shouldGenerateKeyPressFor(event)) return; @@ -712,111 +578,4 @@ function copy(text: string) { input.remove(); } -function commonStyles() { - return html` -`; -} - export default Recorder; diff --git a/src/server/supplements/recorder/recorderApp.ts b/src/server/supplements/recorder/recorderApp.ts index dc24937a5be43..355999d2f2609 100644 --- a/src/server/supplements/recorder/recorderApp.ts +++ b/src/server/supplements/recorder/recorderApp.ts @@ -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; + } +} + export class RecorderApp extends EventEmitter { private _page: Page; @@ -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; @@ -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'); @@ -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 { - const bounds = await CRPage.mainFrameSession(inspectedPage).windowBounds(); + static async open(): Promise { 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 }); @@ -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 { + async setMode(mode: 'none' | 'recording' | 'inspecting'): Promise { + await this._page.mainFrame()._evaluateExpression(((mode: Mode) => { + window.playwrightSetMode(mode); + }).toString(), true, mode, 'main').catch(() => {}); + } + + async setPaused(paused: boolean): Promise { + await this._page.mainFrame()._evaluateExpression(((paused: boolean) => { + window.playwrightSetPaused(paused); + }).toString(), true, paused, 'main').catch(() => {}); + } + + async setSource(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'); + window.playwrightSetSource(param); + }).toString(), true, { text, language }, 'main').catch(() => {}); } async bringToFront() { diff --git a/src/server/supplements/recorder/state.ts b/src/server/supplements/recorder/state.ts deleted file mode 100644 index cfd4a4ca8f788..0000000000000 --- a/src/server/supplements/recorder/state.ts +++ /dev/null @@ -1,28 +0,0 @@ -/** - * 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', -} - -export type SetUIState = { - mode?: 'inspecting' | 'recording' | 'none', -} - -export type State = { - isPaused: boolean, - uiState: UIState, -} diff --git a/src/server/supplements/recorderSupplement.ts b/src/server/supplements/recorderSupplement.ts index 593aa27339e6c..96abd90bd9189 100644 --- a/src/server/supplements/recorderSupplement.ts +++ b/src/server/supplements/recorderSupplement.ts @@ -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'); @@ -45,11 +43,11 @@ export class RecorderSupplement { private _timers = new Set(); 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 | null = null; + private _recorderApp: RecorderApp | null = null; private _highlighterType: string; private _params: channels.BrowserContextRecorderSupplementEnableParams; @@ -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; @@ -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) @@ -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 @@ -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(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(); diff --git a/src/web/common.css b/src/web/common.css index eb8ba18809219..94eb3be442f20 100644 --- a/src/web/common.css +++ b/src/web/common.css @@ -112,3 +112,8 @@ svg { ::-webkit-scrollbar-corner { background-color: var(--background); } + +.code { + font-family: var(--monospace-font); + color: yellow; +} diff --git a/src/web/components/toolbarButton.css b/src/web/components/toolbarButton.css index 2be2e8ae2ecd3..a2df752d9d251 100644 --- a/src/web/components/toolbarButton.css +++ b/src/web/components/toolbarButton.css @@ -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; +} diff --git a/src/web/components/toolbarButton.tsx b/src/web/components/toolbarButton.tsx index 88e58dc75f73b..b427e2875b7cf 100644 --- a/src/web/components/toolbarButton.tsx +++ b/src/web/components/toolbarButton.tsx @@ -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 = ({ title = '', icon = '', + disabled = false, + toggled = false, onClick = () => {}, }) => { - const className = `toolbar-button codicon codicon-${icon}`; - return ; + let className = `toolbar-button codicon codicon-${icon}`; + if (toggled) + className += ' toggled'; + return ; }; diff --git a/src/web/recorder/recorder.css b/src/web/recorder/recorder.css index 3fb35f0436472..873b20f41107c 100644 --- a/src/web/recorder/recorder.css +++ b/src/web/recorder/recorder.css @@ -19,3 +19,11 @@ flex-direction: column; flex: auto; } + +.recorder-paused-infobar { + display: flex; + color: #eee; + background-color: #333; + height: 24px; + align-items: center; +} diff --git a/src/web/recorder/recorder.tsx b/src/web/recorder/recorder.tsx index 3fd957265a55d..fc54a7dba9522 100644 --- a/src/web/recorder/recorder.tsx +++ b/src/web/recorder/recorder.tsx @@ -20,10 +20,14 @@ import { Toolbar } from '../components/toolbar'; import { ToolbarButton } from '../components/toolbarButton'; import { Source } from '../components/source'; +type Mode = 'inspecting' | 'recording' | 'none'; + declare global { interface Window { - _playwrightClear(): Promise - _playwrightSetSource: (params: { text: string, language: string }) => void + playwrightSetMode: (mode: Mode) => void; + playwrightSetPaused: (paused: boolean) => void; + playwrightSetSource: (params: { text: string, language: string }) => void; + dispatch(data: any): Promise; } } @@ -33,18 +37,36 @@ export interface RecorderProps { export const Recorder: React.FC = ({ }) => { const [source, setSource] = React.useState({ language: 'javascript', text: '' }); - window._playwrightSetSource = setSource; + const [paused, setPaused] = React.useState(false); + const [mode, setMode] = React.useState('none'); + + window.playwrightSetMode = setMode; + window.playwrightSetSource = setSource; + window.playwrightSetPaused = setPaused; return
- { - copy(source.text); + { + window.dispatch({ event: 'setMode', params: { mode: mode === 'recording' ? 'none' : 'recording' }}).catch(() => { }); + }}> + { + window.dispatch({ event: 'setMode', params: { mode: mode === 'inspecting' ? 'none' : 'inspecting' }}).catch(() => { }); }}> - { - window._playwrightClear().catch(e => console.error(e)); + { + copy(source.text); }}>
+ { + window.dispatch({ event: 'clear' }).catch(() => {}); + }}>
+
; }; diff --git a/test/pause.spec.ts b/test/pause.spec.ts index 115fd921d8758..992617c46d38a 100644 --- a/test/pause.spec.ts +++ b/test/pause.spec.ts @@ -13,56 +13,73 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { folio } from './fixtures'; -const extended = folio.extend(); -extended.browserOptions.override(({browserOptions}, runTest) => { - return runTest({ +import { ProgressController } from '../lib/server/progress'; + +const extended = folio.extend<{ + recorderFrame: () => Promise, + recorderClick: (selector: string) => Promise +}>(); + +extended.browserOptions.override(async ({browserOptions}, runTest) => { + await runTest({ ...browserOptions, headless: false, }); }); -const {it, expect } = extended.build(); -it('should pause and resume the script', async ({page}) => { - let resolved = false; - const resumePromise = (page as any).pause().then(() => resolved = true); - await new Promise(x => setTimeout(x, 0)); - expect(resolved).toBe(false); - await page.click('#pw-button-resume'); - await resumePromise; - expect(resolved).toBe(true); +extended.recorderFrame.init(async ({context, toImpl}, runTest) => { + await runTest(async () => { + while (!toImpl(context).recorderAppForTest) + await new Promise(f => setTimeout(f, 100)); + return toImpl(context).recorderAppForTest._page.mainFrame(); + }); }); -it('should resume from console', async ({page}) => { - let resolved = false; - const resumePromise = (page as any).pause().then(() => resolved = true); - await new Promise(x => setTimeout(x, 0)); - expect(resolved).toBe(false); - await page.waitForFunction(() => !!(window as any).playwright.resume); - await page.evaluate('window.playwright.resume()'); - await resumePromise; - expect(resolved).toBe(true); +extended.recorderClick.init(async ({ recorderFrame }, runTest) => { + await runTest(async (selector: string) => { + const frame = await recorderFrame(); + frame.click(new ProgressController(), selector, {}); + }); }); -it('should pause through a navigation', async ({page, server}) => { - let resolved = false; - const resumePromise = (page as any).pause().then(() => resolved = true); - await new Promise(x => setTimeout(x, 0)); - expect(resolved).toBe(false); - await page.goto(server.EMPTY_PAGE); - await page.click('#pw-button-resume'); - await resumePromise; - expect(resolved).toBe(true); -}); +const {it, expect, describe} = extended.build(); -it('should pause after a navigation', async ({page, server}) => { - await page.goto(server.EMPTY_PAGE); +describe('pause', (suite, { mode }) => { + suite.skip(mode !== 'default'); +}, () => { + it('should pause and resume the script', async ({ page, recorderClick }) => { + await Promise.all([ + page.pause(), + recorderClick('[title=Resume]') + ]); + }); + + it('should resume from console', async ({page}) => { + await Promise.all([ + page.pause(), + page.waitForFunction(() => (window as any).playwright && (window as any).playwright.resume).then(() => { + return page.evaluate('window.playwright.resume()'); + }) + ]); + }); - let resolved = false; - const resumePromise = (page as any).pause().then(() => resolved = true); - await new Promise(x => setTimeout(x, 0)); - expect(resolved).toBe(false); - await page.click('#pw-button-resume'); - await resumePromise; - expect(resolved).toBe(true); + it('should pause through a navigation', async ({page, server, recorderClick}) => { + let resolved = false; + const resumePromise = page.pause().then(() => resolved = true); + expect(resolved).toBe(false); + await page.goto(server.EMPTY_PAGE); + await recorderClick('[title=Resume]'); + await resumePromise; + expect(resolved).toBe(true); + }); + + it('should pause after a navigation', async ({page, server, recorderClick}) => { + await page.goto(server.EMPTY_PAGE); + await Promise.all([ + page.pause(), + recorderClick('[title=Resume]') + ]); + }); });