feat(pause): page._pause to wait for user to click resume (#5050)
This commit is contained in:
parent
a2422a40ec
commit
3e4e511d84
|
|
@ -639,6 +639,12 @@ export class Page extends ChannelOwner<channels.PageChannel, channels.PageInitia
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async _pause() {
|
||||||
|
return this._wrapApiCall('page.pause', async () => {
|
||||||
|
await this._channel.pause();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async _pdf(options: PDFOptions = {}): Promise<Buffer> {
|
async _pdf(options: PDFOptions = {}): Promise<Buffer> {
|
||||||
return this._wrapApiCall('page.pdf', async () => {
|
return this._wrapApiCall('page.pdf', async () => {
|
||||||
const transportOptions: channels.PagePdfParams = { ...options } as channels.PagePdfParams;
|
const transportOptions: channels.PagePdfParams = { ...options } as channels.PagePdfParams;
|
||||||
|
|
|
||||||
|
|
@ -237,6 +237,10 @@ export class PageDispatcher extends Dispatcher<Page, channels.PageInitializer> i
|
||||||
return { entries: await coverage.stopCSSCoverage() };
|
return { entries: await coverage.stopCSSCoverage() };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async pause() {
|
||||||
|
await this._page.pause();
|
||||||
|
}
|
||||||
|
|
||||||
_onFrameAttached(frame: Frame) {
|
_onFrameAttached(frame: Frame) {
|
||||||
this._dispatchEvent('frameAttached', { frame: FrameDispatcher.from(this._scope, frame) });
|
this._dispatchEvent('frameAttached', { frame: FrameDispatcher.from(this._scope, frame) });
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -770,6 +770,7 @@ export interface PageChannel extends Channel {
|
||||||
mouseClick(params: PageMouseClickParams, metadata?: Metadata): Promise<PageMouseClickResult>;
|
mouseClick(params: PageMouseClickParams, metadata?: Metadata): Promise<PageMouseClickResult>;
|
||||||
touchscreenTap(params: PageTouchscreenTapParams, metadata?: Metadata): Promise<PageTouchscreenTapResult>;
|
touchscreenTap(params: PageTouchscreenTapParams, metadata?: Metadata): Promise<PageTouchscreenTapResult>;
|
||||||
accessibilitySnapshot(params: PageAccessibilitySnapshotParams, metadata?: Metadata): Promise<PageAccessibilitySnapshotResult>;
|
accessibilitySnapshot(params: PageAccessibilitySnapshotParams, metadata?: Metadata): Promise<PageAccessibilitySnapshotResult>;
|
||||||
|
pause(params?: PagePauseParams, metadata?: Metadata): Promise<PagePauseResult>;
|
||||||
pdf(params: PagePdfParams, metadata?: Metadata): Promise<PagePdfResult>;
|
pdf(params: PagePdfParams, metadata?: Metadata): Promise<PagePdfResult>;
|
||||||
crStartJSCoverage(params: PageCrStartJSCoverageParams, metadata?: Metadata): Promise<PageCrStartJSCoverageResult>;
|
crStartJSCoverage(params: PageCrStartJSCoverageParams, metadata?: Metadata): Promise<PageCrStartJSCoverageResult>;
|
||||||
crStopJSCoverage(params?: PageCrStopJSCoverageParams, metadata?: Metadata): Promise<PageCrStopJSCoverageResult>;
|
crStopJSCoverage(params?: PageCrStopJSCoverageParams, metadata?: Metadata): Promise<PageCrStopJSCoverageResult>;
|
||||||
|
|
@ -1066,6 +1067,9 @@ export type PageAccessibilitySnapshotOptions = {
|
||||||
export type PageAccessibilitySnapshotResult = {
|
export type PageAccessibilitySnapshotResult = {
|
||||||
rootAXNode?: AXNode,
|
rootAXNode?: AXNode,
|
||||||
};
|
};
|
||||||
|
export type PagePauseParams = {};
|
||||||
|
export type PagePauseOptions = {};
|
||||||
|
export type PagePauseResult = void;
|
||||||
export type PagePdfParams = {
|
export type PagePdfParams = {
|
||||||
scale?: number,
|
scale?: number,
|
||||||
displayHeaderFooter?: boolean,
|
displayHeaderFooter?: boolean,
|
||||||
|
|
|
||||||
|
|
@ -842,6 +842,8 @@ Page:
|
||||||
returns:
|
returns:
|
||||||
rootAXNode: AXNode?
|
rootAXNode: AXNode?
|
||||||
|
|
||||||
|
pause:
|
||||||
|
|
||||||
pdf:
|
pdf:
|
||||||
parameters:
|
parameters:
|
||||||
scale: number?
|
scale: number?
|
||||||
|
|
|
||||||
|
|
@ -443,6 +443,7 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
|
||||||
interestingOnly: tOptional(tBoolean),
|
interestingOnly: tOptional(tBoolean),
|
||||||
root: tOptional(tChannel('ElementHandle')),
|
root: tOptional(tChannel('ElementHandle')),
|
||||||
});
|
});
|
||||||
|
scheme.PagePauseParams = tOptional(tObject({}));
|
||||||
scheme.PagePdfParams = tObject({
|
scheme.PagePdfParams = tObject({
|
||||||
scale: tOptional(tNumber),
|
scale: tOptional(tNumber),
|
||||||
displayHeaderFooter: tOptional(tBoolean),
|
displayHeaderFooter: tOptional(tBoolean),
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,10 @@ type ContextData = {
|
||||||
contextPromise: Promise<dom.FrameExecutionContext>;
|
contextPromise: Promise<dom.FrameExecutionContext>;
|
||||||
contextResolveCallback: (c: dom.FrameExecutionContext) => void;
|
contextResolveCallback: (c: dom.FrameExecutionContext) => void;
|
||||||
context: dom.FrameExecutionContext | null;
|
context: dom.FrameExecutionContext | null;
|
||||||
rerunnableTasks: Set<RerunnableTask>;
|
rerunnableTasks: Set<{
|
||||||
|
rerun(context: dom.FrameExecutionContext): Promise<void>;
|
||||||
|
terminate(error: Error): void;
|
||||||
|
}>;
|
||||||
};
|
};
|
||||||
|
|
||||||
type DocumentInfo = {
|
type DocumentInfo = {
|
||||||
|
|
@ -1046,6 +1049,24 @@ export class Frame extends EventEmitter {
|
||||||
this._parentFrame = null;
|
this._parentFrame = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async evaluateSurvivingNavigations<T>(callback: (context: dom.FrameExecutionContext) => Promise<T>, world: types.World) {
|
||||||
|
return new Promise<T>((resolve, terminate) => {
|
||||||
|
const data = this._contextData.get(world)!;
|
||||||
|
const task = {
|
||||||
|
terminate,
|
||||||
|
async rerun(context: dom.FrameExecutionContext) {
|
||||||
|
try {
|
||||||
|
resolve(await callback(context));
|
||||||
|
data.rerunnableTasks.delete(task);
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
data.rerunnableTasks.add(task);
|
||||||
|
if (data.context)
|
||||||
|
task.rerun(data.context);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private _scheduleRerunnableTask<T>(progress: Progress, world: types.World, task: dom.SchedulableTask<T>): Promise<T> {
|
private _scheduleRerunnableTask<T>(progress: Progress, world: types.World, task: dom.SchedulableTask<T>): Promise<T> {
|
||||||
const data = this._contextData.get(world)!;
|
const data = this._contextData.get(world)!;
|
||||||
const rerunnableTask = new RerunnableTask(data, progress, task, true /* returnByValue */);
|
const rerunnableTask = new RerunnableTask(data, progress, task, true /* returnByValue */);
|
||||||
|
|
|
||||||
|
|
@ -492,6 +492,31 @@ export class Page extends EventEmitter {
|
||||||
const identifier = PageBinding.identifier(name, world);
|
const identifier = PageBinding.identifier(name, world);
|
||||||
return this._pageBindings.get(identifier) || this._browserContext._pageBindings.get(identifier);
|
return this._pageBindings.get(identifier) || this._browserContext._pageBindings.get(identifier);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async pause() {
|
||||||
|
if (!this._browserContext._browser._options.headful)
|
||||||
|
throw new Error('Cannot pause in headless mode.');
|
||||||
|
await this.mainFrame().evaluateSurvivingNavigations(async context => {
|
||||||
|
await context.evaluateInternal(async () => {
|
||||||
|
const element = document.createElement('playwright-resume');
|
||||||
|
element.style.position = 'absolute';
|
||||||
|
element.style.top = '10px';
|
||||||
|
element.style.left = '10px';
|
||||||
|
element.style.zIndex = '2147483646';
|
||||||
|
element.style.opacity = '0.9';
|
||||||
|
element.setAttribute('role', 'button');
|
||||||
|
element.tabIndex = 0;
|
||||||
|
element.style.fontSize = '50px';
|
||||||
|
element.textContent = '▶️';
|
||||||
|
element.title = 'Resume script';
|
||||||
|
document.body.appendChild(element);
|
||||||
|
await new Promise(x => {
|
||||||
|
element.onclick = x;
|
||||||
|
});
|
||||||
|
element.remove();
|
||||||
|
});
|
||||||
|
}, 'utility');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Worker extends EventEmitter {
|
export class Worker extends EventEmitter {
|
||||||
|
|
|
||||||
56
test/pause.spec.ts
Normal file
56
test/pause.spec.ts
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
/**
|
||||||
|
* 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 { folio } from './fixtures';
|
||||||
|
const extended = folio.extend();
|
||||||
|
extended.browserOptions.override(({browserOptions}, runTest) => {
|
||||||
|
return 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('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);
|
||||||
|
await new Promise(x => setTimeout(x, 0));
|
||||||
|
expect(resolved).toBe(false);
|
||||||
|
await page.goto(server.EMPTY_PAGE);
|
||||||
|
await page.click('playwright-resume');
|
||||||
|
await resumePromise;
|
||||||
|
expect(resolved).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
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);
|
||||||
|
await new Promise(x => setTimeout(x, 0));
|
||||||
|
expect(resolved).toBe(false);
|
||||||
|
await page.click('playwright-resume');
|
||||||
|
await resumePromise;
|
||||||
|
expect(resolved).toBe(true);
|
||||||
|
});
|
||||||
Loading…
Reference in a new issue