feat(pause): make page.pause public (#5288)

This commit is contained in:
Pavel Feldman 2021-02-03 16:01:51 -08:00 committed by GitHub
parent 509c3e91b4
commit 34adc28ed3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 137 additions and 123 deletions

View File

@ -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]>

View File

@ -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>', `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 <file name>', 'saves the generated script to a file')
.option('--target <language>', `language to use, one of javascript, python, python-async, csharp`, process.env.PW_CLI_TARGET_LANG || 'javascript')
.option('--target <language>', `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';
}

View File

@ -276,16 +276,13 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel,
});
}
async _enableConsoleApi() {
await this._channel.consoleSupplementExpose();
}
async _enableRecorder(params: {
language: string,
launchOptions?: LaunchOptions,
contextOptions?: BrowserContextOptions,
device?: string,
saveStorage?: string,
startRecording?: boolean,
terminal?: boolean,
outputFile?: string
}) {

View File

@ -642,7 +642,7 @@ export class Page extends ChannelOwner<channels.PageChannel, channels.PageInitia
return this;
}
async _pause() {
async pause() {
await this.context()._pause();
}

View File

@ -22,7 +22,6 @@ import { RouteDispatcher, RequestDispatcher } from './networkDispatchers';
import { CRBrowserContext } from '../server/chromium/crBrowser';
import { CDPSessionDispatcher } from './cdpSessionDispatcher';
import { RecorderSupplement } from '../server/supplements/recorderSupplement';
import { ConsoleApiSupplement } from '../server/supplements/consoleApiSupplement';
export class BrowserContextDispatcher extends Dispatcher<BrowserContext, channels.BrowserContextInitializer> implements channels.BrowserContextChannel {
private _context: BrowserContext;
@ -129,19 +128,14 @@ export class BrowserContextDispatcher extends Dispatcher<BrowserContext, channel
await this._context.close();
}
async consoleSupplementExpose(): Promise<void> {
const consoleApi = new ConsoleApiSupplement(this._context);
await consoleApi.install();
}
async recorderSupplementEnable(params: channels.BrowserContextRecorderSupplementEnableParams): Promise<void> {
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
});

View File

@ -558,7 +558,6 @@ export interface BrowserContextChannel extends Channel {
setNetworkInterceptionEnabled(params: BrowserContextSetNetworkInterceptionEnabledParams, metadata?: Metadata): Promise<BrowserContextSetNetworkInterceptionEnabledResult>;
setOffline(params: BrowserContextSetOfflineParams, metadata?: Metadata): Promise<BrowserContextSetOfflineResult>;
storageState(params?: BrowserContextStorageStateParams, metadata?: Metadata): Promise<BrowserContextStorageStateResult>;
consoleSupplementExpose(params?: BrowserContextConsoleSupplementExposeParams, metadata?: Metadata): Promise<BrowserContextConsoleSupplementExposeResult>;
pause(params?: BrowserContextPauseParams, metadata?: Metadata): Promise<BrowserContextPauseResult>;
recorderSupplementEnable(params: BrowserContextRecorderSupplementEnableParams, metadata?: Metadata): Promise<BrowserContextRecorderSupplementEnableResult>;
crNewCDPSession(params: BrowserContextCrNewCDPSessionParams, metadata?: Metadata): Promise<BrowserContextCrNewCDPSessionResult>;
@ -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,

View File

@ -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?

View File

@ -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),

View File

@ -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);
}
}

View File

@ -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<void>;
}
}
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;

View File

@ -22,14 +22,14 @@ import type { State, SetUIState } from '../recorder/state';
declare global {
interface Window {
playwrightRecorderPerformAction: (action: actions.Action) => Promise<void>;
playwrightRecorderRecordAction: (action: actions.Action) => Promise<void>;
playwrightRecorderCommitAction: () => Promise<void>;
playwrightRecorderState: () => Promise<State>;
playwrightRecorderSetUIState: (state: SetUIState) => Promise<void>;
playwrightRecorderResume: () => Promise<boolean>;
playwrightRecorderShowRecorderPage: () => Promise<void>;
playwrightRecorderPrintSelector: (text: string) => Promise<void>;
_playwrightRecorderPerformAction: (action: actions.Action) => Promise<void>;
_playwrightRecorderRecordAction: (action: actions.Action) => Promise<void>;
_playwrightRecorderCommitAction: () => Promise<void>;
_playwrightRecorderState: () => Promise<State>;
_playwrightRecorderSetUIState: (state: SetUIState) => Promise<void>;
_playwrightRecorderShowRecorderPage: () => Promise<void>;
_playwrightRecorderPrintSelector: (text: string) => Promise<void>;
_playwrightResume: () => Promise<void>;
}
}
@ -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.

View File

@ -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<void> {
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,
});
}

View File

@ -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<void> {
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');
}

View File

@ -23,7 +23,6 @@ export type SetUIState = {
}
export type State = {
canResume: boolean,
isPaused: boolean,
uiState: UIState,
}

View File

@ -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<RecorderApp> | null = null;
private _highlighterType: string;
private _params: channels.BrowserContextRecorderSupplementEnableParams;
static getOrCreate(context: BrowserContext, app: App, params: channels.BrowserContextRecorderSupplementEnableParams): Promise<RecorderSupplement> {
static getOrCreate(context: BrowserContext, params: channels.BrowserContextRecorderSupplementEnableParams): Promise<RecorderSupplement> {
let recorderPromise = (context as any)[symbol] as Promise<RecorderSupplement>;
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);
}

View File

@ -22,8 +22,8 @@ import { Source } from '../components/source';
declare global {
interface Window {
playwrightClear(): Promise<void>
playwrightSetSource: (params: { text: string, language: string }) => void
_playwrightClear(): Promise<void>
_playwrightSetSource: (params: { text: string, language: string }) => void
}
}
@ -33,7 +33,7 @@ export interface RecorderProps {
export const Recorder: React.FC<RecorderProps> = ({
}) => {
const [source, setSource] = React.useState({ language: 'javascript', text: '' });
window.playwrightSetSource = setSource;
window._playwrightSetSource = setSource;
return <div className="recorder">
<Toolbar>
@ -41,7 +41,7 @@ export const Recorder: React.FC<RecorderProps> = ({
copy(source.text);
}}></ToolbarButton>
<ToolbarButton icon="trashcan" title="Clear" onClick={() => {
window.playwrightClear().catch(e => console.error(e));
window._playwrightClear().catch(e => console.error(e));
}}></ToolbarButton>
<div style={{flex: "auto"}}></div>
</Toolbar>

View File

@ -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();
});

View File

@ -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');

View File

@ -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();

12
types/types.d.ts vendored
View File

@ -2080,6 +2080,18 @@ export interface Page {
*/
opener(): Promise<null|Page>;
/**
* 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<void>;
/**
* Returns the PDF buffer.
*