Skip to content

Commit

Permalink
interstitital
Browse files Browse the repository at this point in the history
  • Loading branch information
dgozman committed Dec 22, 2023
1 parent ff99aa3 commit 933a411
Show file tree
Hide file tree
Showing 12 changed files with 273 additions and 2 deletions.
23 changes: 23 additions & 0 deletions docs/src/api/class-page.md
Original file line number Diff line number Diff line change
Expand Up @@ -3130,6 +3130,29 @@ return value resolves to `[]`.
### param: Page.querySelectorAll.selector = %%-query-selector-%%
* since: v1.9


## async method: Page.registerInterstitial
* since: v1.42

Registers a handler for an interstitial element that might block the following actions: check, click, dblclick, hover, setChecked, tap, uncheck. The handler should get rid of the interstitial so that an action may proceed.

The handler will only be run before/during one of the actions listed above. When no actions from the list are executed, the handler will not be run at all, even if the interstitial element appears on the page.

Note that execution time of the handler counts towards the timeout of any action, e.g. click, that executed the handler.

### param: Page.registerInterstitial.locator
* since: v1.42
- `locator` <[Locator]>

Locator that triggers the handler.

### param: Page.registerInterstitial.handler
* since: v1.42
- `handler` <[function]>

Function that should be run once [`param: locator`] appears. This function should get rid of the interstitial element that blocks actions like click.


## async method: Page.reload
* since: v1.8
- returns: <[null]|[Response]>
Expand Down
19 changes: 19 additions & 0 deletions packages/playwright-core/src/client/page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,8 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
_closeWasCalled: boolean = false;
private _harRouters: HarRouter[] = [];

private _interstitials = new Map<number, Function>();

static from(page: channels.PageChannel): Page {
return (page as any)._object;
}
Expand Down Expand Up @@ -133,6 +135,7 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
this._channel.on('fileChooser', ({ element, isMultiple }) => this.emit(Events.Page.FileChooser, new FileChooser(this, ElementHandle.from(element), isMultiple)));
this._channel.on('frameAttached', ({ frame }) => this._onFrameAttached(Frame.from(frame)));
this._channel.on('frameDetached', ({ frame }) => this._onFrameDetached(Frame.from(frame)));
this._channel.on('interstitialAppeared', ({ uid }) => this._onInterstitialAppeared(uid));
this._channel.on('route', ({ route }) => this._onRoute(Route.from(route)));
this._channel.on('video', ({ artifact }) => {
const artifactObject = Artifact.from(artifact);
Expand Down Expand Up @@ -360,6 +363,22 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
return Response.fromNullable((await this._channel.reload({ ...options, waitUntil })).response);
}

async registerInterstitial(locator: Locator, handler: Function): Promise<void> {
if (locator._frame !== this._mainFrame)
throw new Error(`Interstitial locator must belong to the main frame of this page`);
const { uid } = await this._channel.registerInterstitial({ selector: locator._selector });
this._interstitials.set(uid, handler);
}

private async _onInterstitialAppeared(uid: number) {
try {
const handler = this._interstitials.get(uid);
await handler?.();
} catch {
}
this._channel.resolveInterstitialNoReply({ uid }).catch(() => {});
}

async waitForLoadState(state?: LifecycleEvent, options?: { timeout?: number }): Promise<void> {
return await this._mainFrame.waitForLoadState(state, options);
}
Expand Down
13 changes: 13 additions & 0 deletions packages/playwright-core/src/protocol/validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -983,6 +983,9 @@ scheme.PageFrameAttachedEvent = tObject({
scheme.PageFrameDetachedEvent = tObject({
frame: tChannel(['Frame']),
});
scheme.PageInterstitialAppearedEvent = tObject({
uid: tNumber,
});
scheme.PageRouteEvent = tObject({
route: tChannel(['Route']),
});
Expand Down Expand Up @@ -1038,6 +1041,16 @@ scheme.PageGoForwardParams = tObject({
scheme.PageGoForwardResult = tObject({
response: tOptional(tChannel(['Response'])),
});
scheme.PageRegisterInterstitialParams = tObject({
selector: tString,
});
scheme.PageRegisterInterstitialResult = tObject({
uid: tNumber,
});
scheme.PageResolveInterstitialNoReplyParams = tObject({
uid: tNumber,
});
scheme.PageResolveInterstitialNoReplyResult = tOptional(tObject({}));
scheme.PageReloadParams = tObject({
timeout: tOptional(tNumber),
waitUntil: tOptional(tType('LifecycleEvent')),
Expand Down
10 changes: 10 additions & 0 deletions packages/playwright-core/src/server/dispatchers/pageDispatcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ export class PageDispatcher extends Dispatcher<Page, channels.PageChannel, Brows
}));
this.addObjectListener(Page.Events.FrameAttached, frame => this._onFrameAttached(frame));
this.addObjectListener(Page.Events.FrameDetached, frame => this._onFrameDetached(frame));
this.addObjectListener(Page.Events.InterstitialAppeared, (uid: number) => this._dispatchEvent('interstitialAppeared', { uid }));
this.addObjectListener(Page.Events.WebSocket, webSocket => this._dispatchEvent('webSocket', { webSocket: new WebSocketDispatcher(this, webSocket) }));
this.addObjectListener(Page.Events.Worker, worker => this._dispatchEvent('worker', { worker: new WorkerDispatcher(this, worker) }));
this.addObjectListener(Page.Events.Video, (artifact: Artifact) => this._dispatchEvent('video', { artifact: ArtifactDispatcher.from(parentScope, artifact) }));
Expand Down Expand Up @@ -136,6 +137,15 @@ export class PageDispatcher extends Dispatcher<Page, channels.PageChannel, Brows
return { response: ResponseDispatcher.fromNullable(this.parentScope(), await this._page.goForward(metadata, params)) };
}

async registerInterstitial(params: channels.PageRegisterInterstitialParams, metadata: CallMetadata): Promise<channels.PageRegisterInterstitialResult> {
const uid = this._page.registerInterstitial(params.selector);
return { uid };
}

async resolveInterstitialNoReply(params: channels.PageResolveInterstitialNoReplyParams, metadata: CallMetadata): Promise<void> {
this._page.resolveInterstitial(params.uid);
}

async emulateMedia(params: channels.PageEmulateMediaParams, metadata: CallMetadata): Promise<void> {
await this._page.emulateMedia({
media: params.media,
Expand Down
3 changes: 3 additions & 0 deletions packages/playwright-core/src/server/dom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,9 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
if (result === 'error:notconnected')
return result;
}
// Only perform interstitial checkpoints upon a retry, to make happy-path actions faster.
if (actionName !== 'move and down' && actionName !== 'move and up')
await this._frame._page.performInterstitialCheckpoint(progress);
} else {
progress.log(`attempting ${actionName} action${options.trial ? ' (trial run)' : ''}`);
}
Expand Down
10 changes: 8 additions & 2 deletions packages/playwright-core/src/server/frames.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1269,6 +1269,12 @@ export class Frame extends SdkObject {
async isVisible(metadata: CallMetadata, selector: string, options: types.StrictOptions = {}, scope?: dom.ElementHandle): Promise<boolean> {
const controller = new ProgressController(metadata, this);
return controller.run(async progress => {
return await this.isVisibleInternal(progress, selector, options, scope);
}, this._page._timeoutSettings.timeout({}));
}

async isVisibleInternal(progress: Progress, selector: string, options: types.StrictOptions = {}, scope?: dom.ElementHandle): Promise<boolean> {
try {
progress.log(` checking visibility of ${this._asLocator(selector)}`);
const resolved = await this.selectors.resolveInjectedForSelector(selector, options, scope);
if (!resolved)
Expand All @@ -1278,11 +1284,11 @@ export class Frame extends SdkObject {
const state = element ? injected.elementState(element, 'visible') : false;
return state === 'error:notconnected' ? false : state;
}, { info: resolved.info, root: resolved.frame === this ? scope : undefined });
}, this._page._timeoutSettings.timeout({})).catch(e => {
} catch (e) {
if (js.isJavaScriptErrorInEvaluate(e) || isInvalidSelectorError(e) || isSessionClosedError(e))
throw e;
return false;
});
}
}

async isHidden(metadata: CallMetadata, selector: string, options: types.StrictOptions = {}, scope?: dom.ElementHandle): Promise<boolean> {
Expand Down
30 changes: 30 additions & 0 deletions packages/playwright-core/src/server/page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ export class Page extends SdkObject {
FrameAttached: 'frameattached',
FrameDetached: 'framedetached',
InternalFrameNavigatedToNewDocument: 'internalframenavigatedtonewdocument',
InterstitialAppeared: 'interstitialappeared',
ScreencastFrame: 'screencastframe',
Video: 'video',
WebSocket: 'websocket',
Expand Down Expand Up @@ -168,6 +169,8 @@ export class Page extends SdkObject {
_video: Artifact | null = null;
_opener: Page | undefined;
private _isServerSideOnly = false;
private _interstitials = new Map<number, { selector: string, resolved?: ManualPromise<void> }>();
private _lastInterstitialUid = 0;

// Aiming at 25 fps by default - each frame is 40ms, but we give some slack with 35ms.
// When throttling for tracing, 200ms between frames, except for 10 frames around the action.
Expand Down Expand Up @@ -249,6 +252,7 @@ export class Page extends SdkObject {
async resetForReuse(metadata: CallMetadata) {
this.setDefaultNavigationTimeout(undefined);
this.setDefaultTimeout(undefined);
this._interstitials.clear();

await this._removeExposedBindings();
await this._removeInitScripts();
Expand Down Expand Up @@ -441,6 +445,32 @@ export class Page extends SdkObject {
await this._delegate.updateEmulateMedia();
}

registerInterstitial(selector: string) {
const uid = ++this._lastInterstitialUid;
this._interstitials.set(uid, { selector });
return uid;
}

resolveInterstitial(uid: number) {
const interstitial = this._interstitials.get(uid);
if (interstitial) {
interstitial.resolved?.resolve();
interstitial.resolved = undefined;
}
}

async performInterstitialCheckpoint(progress: Progress) {
for (const [uid, interstitial] of this._interstitials) {
if (!interstitial.resolved) {
if (await this.mainFrame().isVisibleInternal(progress, interstitial.selector, { strict: true })) {
interstitial.resolved = new ManualPromise();
this.emit(Page.Events.InterstitialAppeared, uid);
}
}
await interstitial.resolved;
}
}

emulatedMedia(): EmulatedMedia {
const contextOptions = this._browserContext._options;
return {
Expand Down
15 changes: 15 additions & 0 deletions packages/playwright-core/types/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3522,6 +3522,21 @@ export interface Page {
timeout?: number;
}): Promise<void>;

/**
* Registers a handler for an interstitial element that might block the following actions: check, click, dblclick,
* hover, setChecked, tap, uncheck. The handler should get rid of the interstitial so that an action may proceed.
*
* The handler will only be run before/during one of the actions listed above. When no actions from the list are
* executed, the handler will not be run at all, even if the interstitial element appears on the page.
*
* Note that execution time of the handler counts towards the timeout of any action, e.g. click, that executed the
* handler.
* @param locator Locator that triggers the handler.
* @param handler Function that should be run once `locator` appears. This function should get rid of the interstitial element that
* blocks actions like click.
*/
registerInterstitial(locator: Locator, handler: Function): Promise<void>;

/**
* This method reloads the current page, in the same way as if the user had triggered a browser refresh. Returns the
* main resource response. In case of multiple redirects, the navigation will resolve with the response of the last
Expand Down
23 changes: 23 additions & 0 deletions packages/protocol/src/channels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1761,6 +1761,7 @@ export interface PageEventTarget {
on(event: 'fileChooser', callback: (params: PageFileChooserEvent) => void): this;
on(event: 'frameAttached', callback: (params: PageFrameAttachedEvent) => void): this;
on(event: 'frameDetached', callback: (params: PageFrameDetachedEvent) => void): this;
on(event: 'interstitialAppeared', callback: (params: PageInterstitialAppearedEvent) => void): this;
on(event: 'route', callback: (params: PageRouteEvent) => void): this;
on(event: 'video', callback: (params: PageVideoEvent) => void): this;
on(event: 'webSocket', callback: (params: PageWebSocketEvent) => void): this;
Expand All @@ -1776,6 +1777,8 @@ export interface PageChannel extends PageEventTarget, EventTargetChannel {
exposeBinding(params: PageExposeBindingParams, metadata?: CallMetadata): Promise<PageExposeBindingResult>;
goBack(params: PageGoBackParams, metadata?: CallMetadata): Promise<PageGoBackResult>;
goForward(params: PageGoForwardParams, metadata?: CallMetadata): Promise<PageGoForwardResult>;
registerInterstitial(params: PageRegisterInterstitialParams, metadata?: CallMetadata): Promise<PageRegisterInterstitialResult>;
resolveInterstitialNoReply(params: PageResolveInterstitialNoReplyParams, metadata?: CallMetadata): Promise<PageResolveInterstitialNoReplyResult>;
reload(params: PageReloadParams, metadata?: CallMetadata): Promise<PageReloadResult>;
expectScreenshot(params: PageExpectScreenshotParams, metadata?: CallMetadata): Promise<PageExpectScreenshotResult>;
screenshot(params: PageScreenshotParams, metadata?: CallMetadata): Promise<PageScreenshotResult>;
Expand Down Expand Up @@ -1822,6 +1825,9 @@ export type PageFrameAttachedEvent = {
export type PageFrameDetachedEvent = {
frame: FrameChannel,
};
export type PageInterstitialAppearedEvent = {
uid: number,
};
export type PageRouteEvent = {
route: RouteChannel,
};
Expand Down Expand Up @@ -1907,6 +1913,22 @@ export type PageGoForwardOptions = {
export type PageGoForwardResult = {
response?: ResponseChannel,
};
export type PageRegisterInterstitialParams = {
selector: string,
};
export type PageRegisterInterstitialOptions = {

};
export type PageRegisterInterstitialResult = {
uid: number,
};
export type PageResolveInterstitialNoReplyParams = {
uid: number,
};
export type PageResolveInterstitialNoReplyOptions = {

};
export type PageResolveInterstitialNoReplyResult = void;
export type PageReloadParams = {
timeout?: number,
waitUntil?: LifecycleEvent,
Expand Down Expand Up @@ -2258,6 +2280,7 @@ export interface PageEvents {
'fileChooser': PageFileChooserEvent;
'frameAttached': PageFrameAttachedEvent;
'frameDetached': PageFrameDetachedEvent;
'interstitialAppeared': PageInterstitialAppearedEvent;
'route': PageRouteEvent;
'video': PageVideoEvent;
'webSocket': PageWebSocketEvent;
Expand Down
14 changes: 14 additions & 0 deletions packages/protocol/src/protocol.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1349,6 +1349,16 @@ Page:
slowMo: true
snapshot: true

registerInterstitial:
parameters:
selector: string
returns:
uid: number

resolveInterstitialNoReply:
parameters:
uid: number

reload:
parameters:
timeout: number?
Expand Down Expand Up @@ -1668,6 +1678,10 @@ Page:
parameters:
frame: Frame

interstitialAppeared:
parameters:
uid: number

route:
parameters:
route: Route
Expand Down
62 changes: 62 additions & 0 deletions tests/assets/input/interstitial.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<!DOCTYPE html>
<html>
<head>
<title>Interstitial test</title>
</head>
<body>
<style>
body {
position: relative;
}
#interstitial {
position: absolute;
top: 0;
left: 0;
width: 300px;
height: 300px;
border: 1px solid black;
background: rgba(255, 180, 180);
display: none;
}
#interstitial.visible {
display: block;
}
#close {
margin: 50px;
}
</style>
<div><button id="target">Click me</button></div>
<div id="aside">A place on the side to hover</div>
<div id="interstitial">
<div>This interstitial covers the button</div>
<button id="close">Close the interstitial</button>
</div>
<script>
const target = document.querySelector('#target');
const interstitial = document.querySelector('#interstitial');
const close = document.querySelector('#close');

target.addEventListener('click', () => {
window.clicked = (window.clicked ?? 0) + 1;
}, false);

close.addEventListener('click', () => interstitial.classList.remove('visible'));

let timesToShow = 0;
function setupAnnoyingInterstitial(event, capture, times) {
timesToShow = times;
const listener = () => {
timesToShow--;
interstitial.classList.add('visible');
interstitial.getBoundingClientRect();
if (!timesToShow && event !== 'none')
target.removeEventListener(event, listener, capture === 'capture');
};
if (event === 'none')
listener();
else
target.addEventListener(event, listener, capture === 'capture');
}
</script>
</body>
</html>
Loading

0 comments on commit 933a411

Please sign in to comment.