mirror of
https://github.com/microsoft/playwright.git
synced 2025-06-26 21:40:17 +00:00
feat(pause): make page.pause public (#5288)
This commit is contained in:
parent
509c3e91b4
commit
34adc28ed3
@ -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]>
|
||||
|
||||
|
||||
@ -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';
|
||||
}
|
||||
|
||||
@ -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
|
||||
}) {
|
||||
|
||||
@ -642,7 +642,7 @@ export class Page extends ChannelOwner<channels.PageChannel, channels.PageInitia
|
||||
return this;
|
||||
}
|
||||
|
||||
async _pause() {
|
||||
async pause() {
|
||||
await this.context()._pause();
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
});
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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?
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
@ -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');
|
||||
}
|
||||
|
||||
|
||||
@ -23,7 +23,6 @@ export type SetUIState = {
|
||||
}
|
||||
|
||||
export type State = {
|
||||
canResume: boolean,
|
||||
isPaused: boolean,
|
||||
uiState: UIState,
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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();
|
||||
});
|
||||
|
||||
@ -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');
|
||||
|
||||
@ -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
12
types/types.d.ts
vendored
@ -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.
|
||||
*
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user