Skip to content

Commit

Permalink
feat(pause): page._pause to wait for user to click resume (#5050)
Browse files Browse the repository at this point in the history
  • Loading branch information
JoelEinbinder committed Jan 23, 2021
1 parent a2422a4 commit 3e4e511
Show file tree
Hide file tree
Showing 8 changed files with 120 additions and 1 deletion.
6 changes: 6 additions & 0 deletions src/client/page.ts
Expand Up @@ -639,6 +639,12 @@ export class Page extends ChannelOwner<channels.PageChannel, channels.PageInitia
return this;
}

async _pause() {
return this._wrapApiCall('page.pause', async () => {
await this._channel.pause();
});
}

async _pdf(options: PDFOptions = {}): Promise<Buffer> {
return this._wrapApiCall('page.pdf', async () => {
const transportOptions: channels.PagePdfParams = { ...options } as channels.PagePdfParams;
Expand Down
4 changes: 4 additions & 0 deletions src/dispatchers/pageDispatcher.ts
Expand Up @@ -237,6 +237,10 @@ export class PageDispatcher extends Dispatcher<Page, channels.PageInitializer> i
return { entries: await coverage.stopCSSCoverage() };
}

async pause() {
await this._page.pause();
}

_onFrameAttached(frame: Frame) {
this._dispatchEvent('frameAttached', { frame: FrameDispatcher.from(this._scope, frame) });
}
Expand Down
4 changes: 4 additions & 0 deletions src/protocol/channels.ts
Expand Up @@ -770,6 +770,7 @@ export interface PageChannel extends Channel {
mouseClick(params: PageMouseClickParams, metadata?: Metadata): Promise<PageMouseClickResult>;
touchscreenTap(params: PageTouchscreenTapParams, metadata?: Metadata): Promise<PageTouchscreenTapResult>;
accessibilitySnapshot(params: PageAccessibilitySnapshotParams, metadata?: Metadata): Promise<PageAccessibilitySnapshotResult>;
pause(params?: PagePauseParams, metadata?: Metadata): Promise<PagePauseResult>;
pdf(params: PagePdfParams, metadata?: Metadata): Promise<PagePdfResult>;
crStartJSCoverage(params: PageCrStartJSCoverageParams, metadata?: Metadata): Promise<PageCrStartJSCoverageResult>;
crStopJSCoverage(params?: PageCrStopJSCoverageParams, metadata?: Metadata): Promise<PageCrStopJSCoverageResult>;
Expand Down Expand Up @@ -1066,6 +1067,9 @@ export type PageAccessibilitySnapshotOptions = {
export type PageAccessibilitySnapshotResult = {
rootAXNode?: AXNode,
};
export type PagePauseParams = {};
export type PagePauseOptions = {};
export type PagePauseResult = void;
export type PagePdfParams = {
scale?: number,
displayHeaderFooter?: boolean,
Expand Down
2 changes: 2 additions & 0 deletions src/protocol/protocol.yml
Expand Up @@ -842,6 +842,8 @@ Page:
returns:
rootAXNode: AXNode?

pause:

pdf:
parameters:
scale: number?
Expand Down
1 change: 1 addition & 0 deletions src/protocol/validator.ts
Expand Up @@ -443,6 +443,7 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
interestingOnly: tOptional(tBoolean),
root: tOptional(tChannel('ElementHandle')),
});
scheme.PagePauseParams = tOptional(tObject({}));
scheme.PagePdfParams = tObject({
scale: tOptional(tNumber),
displayHeaderFooter: tOptional(tBoolean),
Expand Down
23 changes: 22 additions & 1 deletion src/server/frames.ts
Expand Up @@ -32,7 +32,10 @@ type ContextData = {
contextPromise: Promise<dom.FrameExecutionContext>;
contextResolveCallback: (c: dom.FrameExecutionContext) => void;
context: dom.FrameExecutionContext | null;
rerunnableTasks: Set<RerunnableTask>;
rerunnableTasks: Set<{
rerun(context: dom.FrameExecutionContext): Promise<void>;
terminate(error: Error): void;
}>;
};

type DocumentInfo = {
Expand Down Expand Up @@ -1046,6 +1049,24 @@ export class Frame extends EventEmitter {
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> {
const data = this._contextData.get(world)!;
const rerunnableTask = new RerunnableTask(data, progress, task, true /* returnByValue */);
Expand Down
25 changes: 25 additions & 0 deletions src/server/page.ts
Expand Up @@ -492,6 +492,31 @@ export class Page extends EventEmitter {
const identifier = PageBinding.identifier(name, world);
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 {
Expand Down
56 changes: 56 additions & 0 deletions test/pause.spec.ts
@@ -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);
});

0 comments on commit 3e4e511

Please sign in to comment.