diff --git a/docs/src/api/class-page.md b/docs/src/api/class-page.md index bfcff235fe..6b383ce5e8 100644 --- a/docs/src/api/class-page.md +++ b/docs/src/api/class-page.md @@ -1479,6 +1479,19 @@ The page's main frame. Page is guaranteed to have a main frame which persists du Returns the opener for popup pages and `null` for others. If the opener has been closed already the returns `null`. +## async method: Page.pause + +Pauses script execution. Playwright will stop executing the script and wait for the user to either press 'Resume' +button in the page overlay or to call `playwright.resume()` in the DevTools console. + +User can inspect selectors or perform manual steps while paused. Resume will continue running the original script from +the place it was paused. + +:::note +This method requires Playwright to be started in a headed mode, with a falsy [`options: headless`] value in +the [`method: BrowserType.launch`]. +::: + ## async method: Page.pdf - returns: <[Buffer]> diff --git a/src/cli/cli.ts b/src/cli/cli.ts index 569c2c2b19..00199bf878 100755 --- a/src/cli/cli.ts +++ b/src/cli/cli.ts @@ -51,7 +51,7 @@ program .command('open [url]') .description('open page in browser specified via -b, --browser') .action(function(url, command) { - open(command.parent, url); + open(command.parent, url, language()); }).on('--help', function() { console.log(''); console.log('Examples:'); @@ -70,8 +70,9 @@ for (const {alias, name, type} of browsers) { program .command(`${alias} [url]`) .description(`open page in ${name}`) + .option('--target ', `language to use, one of javascript, python, python-async, csharp`, language()) .action(function(url, command) { - open({ ...command.parent, browser: type }, url); + open({ ...command.parent, browser: type }, url, command.target); }).on('--help', function() { console.log(''); console.log('Examples:'); @@ -84,7 +85,7 @@ program .command('codegen [url]') .description('open page and generate code for user actions') .option('-o, --output ', 'saves the generated script to a file') - .option('--target ', `language to use, one of javascript, python, python-async, csharp`, process.env.PW_CLI_TARGET_LANG || 'javascript') + .option('--target ', `language to use, one of javascript, python, python-async, csharp`, language()) .action(function(url, command) { codegen(command.parent, url, command.target, command.output); }).on('--help', function() { @@ -316,9 +317,16 @@ async function openPage(context: BrowserContext, url: string | undefined): Promi return page; } -async function open(options: Options, url: string | undefined) { - const { context } = await launchContext(options, false); - await context._enableConsoleApi(); +async function open(options: Options, url: string | undefined, language: string) { + const { context, launchOptions, contextOptions } = await launchContext(options, false); + await context._enableRecorder({ + language, + launchOptions, + contextOptions, + device: options.device, + saveStorage: options.saveStorage, + terminal: !!process.stdout.columns, + }); await openPage(context, url); if (process.env.PWCLI_EXIT_FOR_TEST) await Promise.all(context.pages().map(p => p.close())); @@ -334,6 +342,7 @@ async function codegen(options: Options, url: string | undefined, language: stri contextOptions, device: options.device, saveStorage: options.saveStorage, + startRecording: true, terminal: !!process.stdout.columns, outputFile: outputFile ? path.resolve(outputFile) : undefined }); @@ -409,3 +418,7 @@ function validateOptions(options: Options) { process.exit(0); } } + +function language(): string { + return process.env.PW_CLI_TARGET_LANG || 'javascript'; +} diff --git a/src/client/browserContext.ts b/src/client/browserContext.ts index 5b2bb29e0e..91a3ac44e9 100644 --- a/src/client/browserContext.ts +++ b/src/client/browserContext.ts @@ -276,16 +276,13 @@ export class BrowserContext extends ChannelOwner implements channels.BrowserContextChannel { private _context: BrowserContext; @@ -129,19 +128,14 @@ export class BrowserContextDispatcher extends Dispatcher { - const consoleApi = new ConsoleApiSupplement(this._context); - await consoleApi.install(); - } - async recorderSupplementEnable(params: channels.BrowserContextRecorderSupplementEnableParams): Promise { - await RecorderSupplement.getOrCreate(this._context, 'codegen', params); + await RecorderSupplement.getOrCreate(this._context, params); } async pause() { if (!this._context._browser.options.headful) return; - const recorder = await RecorderSupplement.getOrCreate(this._context, 'pause', { + const recorder = await RecorderSupplement.getOrCreate(this._context, { language: 'javascript', terminal: true }); diff --git a/src/protocol/channels.ts b/src/protocol/channels.ts index f0a8b4c693..762e249700 100644 --- a/src/protocol/channels.ts +++ b/src/protocol/channels.ts @@ -558,7 +558,6 @@ export interface BrowserContextChannel extends Channel { setNetworkInterceptionEnabled(params: BrowserContextSetNetworkInterceptionEnabledParams, metadata?: Metadata): Promise; setOffline(params: BrowserContextSetOfflineParams, metadata?: Metadata): Promise; storageState(params?: BrowserContextStorageStateParams, metadata?: Metadata): Promise; - consoleSupplementExpose(params?: BrowserContextConsoleSupplementExposeParams, metadata?: Metadata): Promise; pause(params?: BrowserContextPauseParams, metadata?: Metadata): Promise; recorderSupplementEnable(params: BrowserContextRecorderSupplementEnableParams, metadata?: Metadata): Promise; crNewCDPSession(params: BrowserContextCrNewCDPSessionParams, metadata?: Metadata): Promise; @@ -709,14 +708,12 @@ export type BrowserContextStorageStateResult = { cookies: NetworkCookie[], origins: OriginStorage[], }; -export type BrowserContextConsoleSupplementExposeParams = {}; -export type BrowserContextConsoleSupplementExposeOptions = {}; -export type BrowserContextConsoleSupplementExposeResult = void; export type BrowserContextPauseParams = {}; export type BrowserContextPauseOptions = {}; export type BrowserContextPauseResult = void; export type BrowserContextRecorderSupplementEnableParams = { language: string, + startRecording?: boolean, launchOptions?: any, contextOptions?: any, device?: string, @@ -725,6 +722,7 @@ export type BrowserContextRecorderSupplementEnableParams = { outputFile?: string, }; export type BrowserContextRecorderSupplementEnableOptions = { + startRecording?: boolean, launchOptions?: any, contextOptions?: any, device?: string, diff --git a/src/protocol/protocol.yml b/src/protocol/protocol.yml index 0e0ae188cd..cfd66fc2cf 100644 --- a/src/protocol/protocol.yml +++ b/src/protocol/protocol.yml @@ -601,9 +601,6 @@ BrowserContext: type: array items: OriginStorage - consoleSupplementExpose: - experimental: True - pause: experimental: True @@ -611,6 +608,7 @@ BrowserContext: experimental: True parameters: language: string + startRecording: boolean? launchOptions: json? contextOptions: json? device: string? diff --git a/src/protocol/validator.ts b/src/protocol/validator.ts index ee09513c60..65757b8fa1 100644 --- a/src/protocol/validator.ts +++ b/src/protocol/validator.ts @@ -337,10 +337,10 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme { offline: tBoolean, }); scheme.BrowserContextStorageStateParams = tOptional(tObject({})); - scheme.BrowserContextConsoleSupplementExposeParams = tOptional(tObject({})); scheme.BrowserContextPauseParams = tOptional(tObject({})); scheme.BrowserContextRecorderSupplementEnableParams = tObject({ language: tString, + startRecording: tOptional(tBoolean), launchOptions: tOptional(tAny), contextOptions: tOptional(tAny), device: tOptional(tString), diff --git a/src/server/supplements/consoleApiSupplement.ts b/src/server/supplements/consoleApiSupplement.ts deleted file mode 100644 index 60b342bb15..0000000000 --- a/src/server/supplements/consoleApiSupplement.ts +++ /dev/null @@ -1,30 +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. - */ - -import * as consoleApiSource from '../../generated/consoleApiSource'; -import { BrowserContext } from '../browserContext'; - -export class ConsoleApiSupplement { - private _context: BrowserContext; - - constructor(context: BrowserContext) { - this._context = context; - } - - async install() { - await this._context.extendInjectedScript(consoleApiSource.source); - } -} diff --git a/src/server/supplements/injected/consoleApi.ts b/src/server/supplements/injected/consoleApi.ts index 026d843a0e..b00bf2e776 100644 --- a/src/server/supplements/injected/consoleApi.ts +++ b/src/server/supplements/injected/consoleApi.ts @@ -17,18 +17,35 @@ import type InjectedScript from '../../injected/injectedScript'; import { generateSelector } from './selectorGenerator'; +type ConsoleAPIInterface = { + $: (selector: string) => void; + $$: (selector: string) => void; + inspect: (selector: string) => void; + selector: (element: Element) => void; + resume: () => void; +}; + +declare global { + interface Window { + playwright?: ConsoleAPIInterface; + inspect: (element: Element | undefined) => void; + _playwrightResume: () => Promise; + } +} + export class ConsoleAPI { private _injectedScript: InjectedScript; constructor(injectedScript: InjectedScript) { this._injectedScript = injectedScript; - if ((window as any).playwright) + if (window.playwright) return; - (window as any).playwright = { + window.playwright = { $: (selector: string) => this._querySelector(selector), $$: (selector: string) => this._querySelectorAll(selector), inspect: (selector: string) => this._inspect(selector), selector: (element: Element) => this._selector(element), + resume: () => this._resume(), }; } @@ -47,11 +64,9 @@ export class ConsoleAPI { } private _inspect(selector: string) { - if (typeof (window as any).inspect !== 'function') - return; if (typeof selector !== 'string') throw new Error(`Usage: playwright.inspect('Playwright >> selector').`); - (window as any).inspect(this._querySelector(selector)); + window.inspect(this._querySelector(selector)); } private _selector(element: Element) { @@ -59,6 +74,10 @@ export class ConsoleAPI { throw new Error(`Usage: playwright.selector(element).`); return generateSelector(this._injectedScript, element).selector; } + + private _resume() { + window._playwrightResume().catch(() => {}); + } } export default ConsoleAPI; diff --git a/src/server/supplements/injected/recorder.ts b/src/server/supplements/injected/recorder.ts index 633c4b2d8f..cc5c359914 100644 --- a/src/server/supplements/injected/recorder.ts +++ b/src/server/supplements/injected/recorder.ts @@ -22,14 +22,14 @@ import type { State, SetUIState } from '../recorder/state'; declare global { interface Window { - playwrightRecorderPerformAction: (action: actions.Action) => Promise; - playwrightRecorderRecordAction: (action: actions.Action) => Promise; - playwrightRecorderCommitAction: () => Promise; - playwrightRecorderState: () => Promise; - playwrightRecorderSetUIState: (state: SetUIState) => Promise; - playwrightRecorderResume: () => Promise; - playwrightRecorderShowRecorderPage: () => Promise; - playwrightRecorderPrintSelector: (text: string) => Promise; + _playwrightRecorderPerformAction: (action: actions.Action) => Promise; + _playwrightRecorderRecordAction: (action: actions.Action) => Promise; + _playwrightRecorderCommitAction: () => Promise; + _playwrightRecorderState: () => Promise; + _playwrightRecorderSetUIState: (state: SetUIState) => Promise; + _playwrightRecorderShowRecorderPage: () => Promise; + _playwrightRecorderPrintSelector: (text: string) => Promise; + _playwrightResume: () => Promise; } } @@ -52,7 +52,6 @@ export class Recorder { private _outerToolbarElement: HTMLElement; private _toolbar: Element$; private _state: State = { - canResume: false, uiState: { mode: 'none', }, @@ -167,13 +166,13 @@ export class Recorder { if (this._toolbar.$('#pw-button-resume').classList.contains('disabled')) return; this._updateUIState({ mode: 'none' }); - window.playwrightRecorderResume().catch(() => {}); + 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(() => {}); + window._playwrightRecorderShowRecorderPage().catch(() => {}); }); } @@ -211,19 +210,19 @@ export class Recorder { } private async _updateUIState(uiState: SetUIState) { - window.playwrightRecorderSetUIState(uiState).then(() => this._pollRecorderMode()); + window._playwrightRecorderSetUIState(uiState).then(() => this._pollRecorderMode()); } private async _pollRecorderMode(skipAnimations: boolean = false) { if (this._pollRecorderModeTimer) clearTimeout(this._pollRecorderModeTimer); - const state = await window.playwrightRecorderState().catch(e => null); + const state = await window._playwrightRecorderState().catch(e => null); if (!state) { this._pollRecorderModeTimer = setTimeout(() => this._pollRecorderMode(), 250); return; } - const { canResume, isPaused, uiState } = state; + 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'); @@ -234,13 +233,7 @@ export class Recorder { if (isPaused !== this._state.isPaused) { this._state.isPaused = isPaused; - this._toolbar.$('#pw-button-resume-group').classList.toggle('hidden', false); - this._toolbar.$('#pw-button-resume').classList.toggle('disabled', !isPaused); - } - - if (canResume !== this._state.canResume) { - this._state.canResume = canResume; - this._toolbar.$('#pw-button-resume-group').classList.toggle('hidden', !canResume); + this._toolbar.$('#pw-button-resume-group').classList.toggle('hidden', !isPaused); } this._state = state; @@ -280,7 +273,7 @@ export class Recorder { if (this._state.uiState.mode === 'inspecting' && !this._isInToolbar(event.target as HTMLElement)) { if (this._hoveredModel) { copy(this._hoveredModel.selector); - window.playwrightRecorderPrintSelector(this._hoveredModel.selector); + window._playwrightRecorderPrintSelector(this._hoveredModel.selector); } } if (this._shouldIgnoreMouseEvent(event)) @@ -389,7 +382,7 @@ export class Recorder { const { selector, elements } = generateSelector(this._injectedScript, hoveredElement); if ((this._hoveredModel && this._hoveredModel.selector === selector) || this._hoveredElement !== hoveredElement) return; - window.playwrightRecorderCommitAction(); + window._playwrightRecorderCommitAction(); this._hoveredModel = selector ? { selector, elements } : null; this._updateHighlight(); if ((window as any)._highlightUpdatedForTest) @@ -483,7 +476,7 @@ export class Recorder { } if (elementType === 'file') { - window.playwrightRecorderRecordAction({ + window._playwrightRecorderRecordAction({ name: 'setInputFiles', selector: this._activeModel!.selector, signals: [], @@ -495,7 +488,7 @@ export class Recorder { // Non-navigating actions are simply recorded by Playwright. if (this._consumedDueWrongTarget(event)) return; - window.playwrightRecorderRecordAction({ + window._playwrightRecorderRecordAction({ name: 'fill', selector: this._activeModel!.selector, signals: [], @@ -592,7 +585,7 @@ export class Recorder { private async _performAction(action: actions.Action) { this._performingAction = true; - await window.playwrightRecorderPerformAction(action).catch(() => {}); + await window._playwrightRecorderPerformAction(action).catch(() => {}); this._performingAction = false; // Action could have changed DOM, update hovered model selectors. diff --git a/src/server/supplements/inspectorController.ts b/src/server/supplements/inspectorController.ts index 25a5779263..a74244c93d 100644 --- a/src/server/supplements/inspectorController.ts +++ b/src/server/supplements/inspectorController.ts @@ -16,16 +16,13 @@ import { BrowserContext, ContextListener } from '../browserContext'; import { isDebugMode } from '../../utils/utils'; -import { ConsoleApiSupplement } from './consoleApiSupplement'; import { RecorderSupplement } from './recorderSupplement'; export class InspectorController implements ContextListener { async onContextCreated(context: BrowserContext): Promise { if (isDebugMode()) { - const consoleApi = new ConsoleApiSupplement(context); - await consoleApi.install(); - RecorderSupplement.getOrCreate(context, 'debug', { - language: 'javascript', + RecorderSupplement.getOrCreate(context, { + language: process.env.PW_CLI_TARGET_LANG || 'javascript', terminal: true, }); } diff --git a/src/server/supplements/recorder/recorderApp.ts b/src/server/supplements/recorder/recorderApp.ts index 59ca1cb0cf..dc24937a5b 100644 --- a/src/server/supplements/recorder/recorderApp.ts +++ b/src/server/supplements/recorder/recorderApp.ts @@ -61,7 +61,7 @@ export class RecorderApp extends EventEmitter { await route.continue(); }); - await this._page.exposeBinding('playwrightClear', false, (_, text: string) => { + await this._page.exposeBinding('_playwrightClear', false, (_, text: string) => { this.emit('clear'); }); @@ -103,7 +103,7 @@ export class RecorderApp extends EventEmitter { async setScript(text: string, language: string): Promise { await this._page.mainFrame()._evaluateExpression(((param: { text: string, language: string }) => { - (window as any).playwrightSetSource(param); + (window as any)._playwrightSetSource(param); }).toString(), true, { text, language }, 'main'); } diff --git a/src/server/supplements/recorder/state.ts b/src/server/supplements/recorder/state.ts index ead711d854..cfd4a4ca8f 100644 --- a/src/server/supplements/recorder/state.ts +++ b/src/server/supplements/recorder/state.ts @@ -23,7 +23,6 @@ export type SetUIState = { } export type State = { - canResume: boolean, isPaused: boolean, uiState: UIState, } diff --git a/src/server/supplements/recorderSupplement.ts b/src/server/supplements/recorderSupplement.ts index ecfd16dafb..593aa27339 100644 --- a/src/server/supplements/recorderSupplement.ts +++ b/src/server/supplements/recorderSupplement.ts @@ -47,27 +47,27 @@ export class RecorderSupplement { private _resumeCallback: (() => void) | null = null; private _recorderUIState: UIState; private _paused = false; - private _app: App; private _output: OutputMultiplexer; private _bufferedOutput: BufferedOutput; private _recorderApp: Promise | null = null; private _highlighterType: string; + private _params: channels.BrowserContextRecorderSupplementEnableParams; - static getOrCreate(context: BrowserContext, app: App, params: channels.BrowserContextRecorderSupplementEnableParams): Promise { + static getOrCreate(context: BrowserContext, params: channels.BrowserContextRecorderSupplementEnableParams): Promise { let recorderPromise = (context as any)[symbol] as Promise; if (!recorderPromise) { - const recorder = new RecorderSupplement(context, app, params); + const recorder = new RecorderSupplement(context, params); recorderPromise = recorder.install().then(() => recorder); (context as any)[symbol] = recorderPromise; } return recorderPromise; } - constructor(context: BrowserContext, app: App, params: channels.BrowserContextRecorderSupplementEnableParams) { + constructor(context: BrowserContext, params: channels.BrowserContextRecorderSupplementEnableParams) { this._context = context; - this._app = app; + this._params = params; this._recorderUIState = { - mode: app === 'codegen' ? 'recording' : 'none', + mode: params.startRecording ? 'recording' : 'none', }; let languageGenerator: LanguageGenerator; switch (params.language) { @@ -97,10 +97,10 @@ export class RecorderSupplement { if (params.outputFile) outputs.push(new FileOutput(params.outputFile)); this._output = new OutputMultiplexer(outputs); - this._output.setEnabled(app === 'codegen'); + this._output.setEnabled(!!params.startRecording); context.on(BrowserContext.Events.BeforeClose, () => this._output.flush()); - const generator = new CodeGenerator(context._browser.options.name, app === 'codegen', params.launchOptions || {}, params.contextOptions || {}, this._output, languageGenerator, params.device, params.saveStorage); + const generator = new CodeGenerator(context._browser.options.name, !!params.startRecording, params.launchOptions || {}, params.contextOptions || {}, this._output, languageGenerator, params.device, params.saveStorage); this._generator = generator; } @@ -117,18 +117,18 @@ export class RecorderSupplement { // Input actions that potentially lead to navigation are intercepted on the page and are // performed by the Playwright. - await this._context.exposeBinding('playwrightRecorderPerformAction', false, + await this._context.exposeBinding('_playwrightRecorderPerformAction', false, (source: BindingSource, action: actions.Action) => this._performAction(source.frame, action)); // Other non-essential actions are simply being recorded. - await this._context.exposeBinding('playwrightRecorderRecordAction', false, + await this._context.exposeBinding('_playwrightRecorderRecordAction', false, (source: BindingSource, action: actions.Action) => this._recordAction(source.frame, action)); // Commits last action so that no further signals are added to it. - await this._context.exposeBinding('playwrightRecorderCommitAction', false, + await this._context.exposeBinding('_playwrightRecorderCommitAction', false, (source: BindingSource, action: actions.Action) => this._generator.commitLastAction()); - await this._context.exposeBinding('playwrightRecorderShowRecorderPage', false, ({ page }) => { + await this._context.exposeBinding('_playwrightRecorderShowRecorderPage', false, ({ page }) => { if (this._recorderApp) { this._recorderApp.then(p => p.bringToFront()).catch(() => {}); return; @@ -143,25 +143,24 @@ export class RecorderSupplement { }).catch(e => console.error(e)); }); - await this._context.exposeBinding('playwrightRecorderPrintSelector', false, (_, text) => { + 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, () => { + await this._context.exposeBinding('_playwrightRecorderState', false, () => { const state: State = { uiState: this._recorderUIState, - canResume: this._app === 'pause', isPaused: this._paused, }; return state; }); - await this._context.exposeBinding('playwrightRecorderSetUIState', false, (source, state: UIState) => { + await this._context.exposeBinding('_playwrightRecorderSetUIState', false, (source, state: UIState) => { this._recorderUIState = { ...this._recorderUIState, ...state }; this._output.setEnabled(state.mode === 'recording'); }); - await this._context.exposeBinding('playwrightRecorderResume', false, () => { + await this._context.exposeBinding('_playwrightResume', false, () => { if (this._resumeCallback) { this._resumeCallback(); this._resumeCallback = null; @@ -222,7 +221,7 @@ export class RecorderSupplement { private _clearScript(): void { this._bufferedOutput.clear(); this._generator.restart(); - if (this._app === 'codegen') { + if (!!this._params.startRecording) { for (const page of this._context.pages()) this._onFrameNavigated(page.mainFrame(), page); } diff --git a/src/web/recorder/recorder.tsx b/src/web/recorder/recorder.tsx index 8dea9fda03..3fd957265a 100644 --- a/src/web/recorder/recorder.tsx +++ b/src/web/recorder/recorder.tsx @@ -22,8 +22,8 @@ import { Source } from '../components/source'; declare global { interface Window { - playwrightClear(): Promise - playwrightSetSource: (params: { text: string, language: string }) => void + _playwrightClear(): Promise + _playwrightSetSource: (params: { text: string, language: string }) => void } } @@ -33,7 +33,7 @@ export interface RecorderProps { export const Recorder: React.FC = ({ }) => { const [source, setSource] = React.useState({ language: 'javascript', text: '' }); - window.playwrightSetSource = setSource; + window._playwrightSetSource = setSource; return
@@ -41,7 +41,7 @@ export const Recorder: React.FC = ({ copy(source.text); }}> { - window.playwrightClear().catch(e => console.error(e)); + window._playwrightClear().catch(e => console.error(e)); }}>
diff --git a/test/cli/cli.fixtures.ts b/test/cli/cli.fixtures.ts index b38c0de0cc..f87220543f 100644 --- a/test/cli/cli.fixtures.ts +++ b/test/cli/cli.fixtures.ts @@ -39,7 +39,7 @@ fixtures.contextWrapper.init(async ({ browser }, runTest) => { const context = await browser.newContext() as BrowserContext; const outputBuffer = new WritableBuffer(); (context as any)._stdout = outputBuffer; - await (context as any)._enableRecorder({ language: 'javascript' }); + await (context as any)._enableRecorder({ language: 'javascript', startRecording: true }); await runTest({ context, output: outputBuffer }); await context.close(); }); diff --git a/test/pause.spec.ts b/test/pause.spec.ts index 01f75da2cc..115fd921d8 100644 --- a/test/pause.spec.ts +++ b/test/pause.spec.ts @@ -22,9 +22,10 @@ extended.browserOptions.override(({browserOptions}, runTest) => { }); }); 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); + 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'); @@ -32,9 +33,20 @@ it('should pause and resume the script', async ({page}) => { expect(resolved).toBe(true); }); +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); +}); + it('should pause through a navigation', async ({page, server}) => { let resolved = false; - const resumePromise = (page as any)._pause().then(() => resolved = true); + 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); @@ -47,7 +59,7 @@ it('should pause after a navigation', async ({page, server}) => { await page.goto(server.EMPTY_PAGE); let resolved = false; - const resumePromise = (page as any)._pause().then(() => resolved = true); + 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'); diff --git a/test/selector-generator.spec.ts b/test/selector-generator.spec.ts index 40ab8d5be8..5c010d6015 100644 --- a/test/selector-generator.spec.ts +++ b/test/selector-generator.spec.ts @@ -19,7 +19,7 @@ import type { Page, Frame } from '..'; const fixtures = folio.extend(); fixtures.context.override(async ({ context }, run) => { - await (context as any)._enableConsoleApi(); + await (context as any)._enableRecorder({ language: 'javascript' }); await run(context); }); const { describe, it, expect } = fixtures.build(); diff --git a/types/types.d.ts b/types/types.d.ts index d4b526e1b6..04e0be5a13 100644 --- a/types/types.d.ts +++ b/types/types.d.ts @@ -2080,6 +2080,18 @@ export interface Page { */ opener(): Promise; + /** + * Pauses script execution. Playwright will stop executing the script and wait for the user to either press 'Resume' button + * in the page overlay or to call `playwright.resume()` in the DevTools console. + * + * User can inspect selectors or perform manual steps while paused. Resume will continue running the original script from + * the place it was paused. + * + * > NOTE: This method requires Playwright to be started in a headed mode, with a falsy [`options: headless`] value in the + * [browserType.launch([options])](https://playwright.dev/docs/api/class-browsertype#browsertypelaunchoptions). + */ + pause(): Promise; + /** * Returns the PDF buffer. *