feat(mouse): page.mouse.wheel (#8690)

This commit is contained in:
Joel Einbinder 2021-09-14 15:22:52 -04:00 committed by GitHub
parent 351c20be48
commit afae5bef5d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 250 additions and 0 deletions

View File

@ -120,3 +120,22 @@ Dispatches a `mouseup` event.
### option: Mouse.up.button = %%-input-button-%%
### option: Mouse.up.clickCount = %%-input-click-count-%%
## async method: Mouse.wheel
Dispatches a `wheel` event.
:::note
Wheel events may cause scrolling if they are not handled, and this method does not
wait for the scrolling to finish before returning.
:::
### param: Mouse.wheel.deltaX
- `deltaX` <[float]>
Pixels to scroll horizontally.
### param: Mouse.wheel.deltaY
- `deltaY` <[float]>
Pixels to scroll vertically.

View File

@ -91,6 +91,12 @@ export class Mouse implements api.Mouse {
async dblclick(x: number, y: number, options: Omit<channels.PageMouseClickOptions, 'clickCount'> = {}) {
await this.click(x, y, { ...options, clickCount: 2 });
}
async wheel(deltaX: number, deltaY: number) {
await this._page._wrapApiCall(async channel => {
await channel.mouseWheel({ deltaX, deltaY });
});
}
}
export class Touchscreen implements api.Touchscreen {

View File

@ -192,6 +192,10 @@ export class PageDispatcher extends Dispatcher<Page, channels.PageInitializer, c
await this._page.mouse.click(params.x, params.y, params);
}
async mouseWheel(params: channels.PageMouseWheelParams, metadata: CallMetadata): Promise<void> {
await this._page.mouse.wheel(params.deltaX, params.deltaY);
}
async touchscreenTap(params: channels.PageTouchscreenTapParams, metadata: CallMetadata): Promise<void> {
await this._page.touchscreen.tap(params.x, params.y);
}

View File

@ -1105,6 +1105,7 @@ export interface PageChannel extends EventTargetChannel {
mouseDown(params: PageMouseDownParams, metadata?: Metadata): Promise<PageMouseDownResult>;
mouseUp(params: PageMouseUpParams, metadata?: Metadata): Promise<PageMouseUpResult>;
mouseClick(params: PageMouseClickParams, metadata?: Metadata): Promise<PageMouseClickResult>;
mouseWheel(params: PageMouseWheelParams, metadata?: Metadata): Promise<PageMouseWheelResult>;
touchscreenTap(params: PageTouchscreenTapParams, metadata?: Metadata): Promise<PageTouchscreenTapResult>;
accessibilitySnapshot(params: PageAccessibilitySnapshotParams, metadata?: Metadata): Promise<PageAccessibilitySnapshotResult>;
pdf(params: PagePdfParams, metadata?: Metadata): Promise<PagePdfResult>;
@ -1367,6 +1368,14 @@ export type PageMouseClickOptions = {
clickCount?: number,
};
export type PageMouseClickResult = void;
export type PageMouseWheelParams = {
deltaX: number,
deltaY: number,
};
export type PageMouseWheelOptions = {
};
export type PageMouseWheelResult = void;
export type PageTouchscreenTapParams = {
x: number,
y: number,
@ -3626,6 +3635,7 @@ export const commandsWithTracingSnapshots = new Set([
'Page.mouseDown',
'Page.mouseUp',
'Page.mouseClick',
'Page.mouseWheel',
'Page.touchscreenTap',
'Frame.evalOnSelector',
'Frame.evalOnSelectorAll',

View File

@ -1024,6 +1024,13 @@ Page:
tracing:
snapshot: true
mouseWheel:
parameters:
deltaX: number
deltaY: number
tracing:
snapshot: true
touchscreenTap:
parameters:
x: number

View File

@ -561,6 +561,10 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
button: tOptional(tEnum(['left', 'right', 'middle'])),
clickCount: tOptional(tNumber),
});
scheme.PageMouseWheelParams = tObject({
deltaX: tNumber,
deltaY: tNumber,
});
scheme.PageTouchscreenTapParams = tObject({
x: tNumber,
y: tNumber,

View File

@ -135,6 +135,17 @@ export class RawMouseImpl implements input.RawMouse {
clickCount
});
}
async wheel(x: number, y: number, buttons: Set<types.MouseButton>, modifiers: Set<types.KeyboardModifier>, deltaX: number, deltaY: number): Promise<void> {
await this._client.send('Input.dispatchMouseEvent', {
type: 'mouseWheel',
x,
y,
modifiers: toModifiersMask(modifiers),
deltaX,
deltaY,
});
}
}
export class RawTouchscreenImpl implements input.RawTouchscreen {

View File

@ -16,6 +16,7 @@
*/
import * as input from '../input';
import { Page } from '../page';
import * as types from '../types';
import { FFSession } from './ffConnection';
@ -101,6 +102,7 @@ export class RawKeyboardImpl implements input.RawKeyboard {
export class RawMouseImpl implements input.RawMouse {
private _client: FFSession;
private _page?: Page;
constructor(client: FFSession) {
this._client = client;
@ -140,6 +142,23 @@ export class RawMouseImpl implements input.RawMouse {
clickCount
});
}
async wheel(x: number, y: number, buttons: Set<types.MouseButton>, modifiers: Set<types.KeyboardModifier>, deltaX: number, deltaY: number): Promise<void> {
// Wheel events hit the compositor first, so wait one frame for it to be synced.
await this._page!.mainFrame().evaluateExpression(`new Promise(requestAnimationFrame)`, false, false, 'utility');
await this._client.send('Page.dispatchWheelEvent', {
deltaX,
deltaY,
x,
y,
deltaZ: 0,
modifiers: toModifiersMask(modifiers)
});
}
setPage(page: Page) {
this._page = page;
}
}
export class RawTouchscreenImpl implements input.RawTouchscreen {

View File

@ -63,6 +63,7 @@ export class FFPage implements PageDelegate {
this._contextIdToContext = new Map();
this._browserContext = browserContext;
this._page = new Page(this, browserContext);
this.rawMouse.setPage(this._page);
this._networkManager = new FFNetworkManager(session, this._page);
this._page.on(Page.Events.FrameDetached, frame => this._removeContextsForFrame(frame));
// TODO: remove Page.willOpenNewWindowAsynchronously from the protocol.

View File

@ -160,6 +160,7 @@ export interface RawMouse {
move(x: number, y: number, button: types.MouseButton | 'none', buttons: Set<types.MouseButton>, modifiers: Set<types.KeyboardModifier>): Promise<void>;
down(x: number, y: number, button: types.MouseButton, buttons: Set<types.MouseButton>, modifiers: Set<types.KeyboardModifier>, clickCount: number): Promise<void>;
up(x: number, y: number, button: types.MouseButton, buttons: Set<types.MouseButton>, modifiers: Set<types.KeyboardModifier>, clickCount: number): Promise<void>;
wheel(x: number, y: number, buttons: Set<types.MouseButton>, modifiers: Set<types.KeyboardModifier>, deltaX: number, deltaY: number): Promise<void>;
}
export class Mouse {
@ -232,6 +233,11 @@ export class Mouse {
async dblclick(x: number, y: number, options: { delay?: number, button?: types.MouseButton } = {}) {
await this.click(x, y, { ...options, clickCount: 2 });
}
async wheel(deltaX: number, deltaY: number) {
await this._raw.wheel(this._x, this._y, this._buttons, this._keyboard._modifiers(), deltaX, deltaY);
await this._page._doSlowMo();
}
}
const aliases = new Map<string, string[]>([

View File

@ -20,6 +20,7 @@ import * as types from '../types';
import { macEditingCommands } from '../macEditingCommands';
import { WKSession } from './wkConnection';
import { isString } from '../../utils/utils';
import type { Page } from '../page';
function toModifiersMask(modifiers: Set<types.KeyboardModifier>): number {
// From Source/WebKit/Shared/WebEvent.h
@ -101,11 +102,17 @@ export class RawKeyboardImpl implements input.RawKeyboard {
export class RawMouseImpl implements input.RawMouse {
private readonly _pageProxySession: WKSession;
private _session?: WKSession;
private _page?: Page;
constructor(session: WKSession) {
this._pageProxySession = session;
}
setSession(session: WKSession) {
this._session = session;
}
async move(x: number, y: number, button: types.MouseButton | 'none', buttons: Set<types.MouseButton>, modifiers: Set<types.KeyboardModifier>): Promise<void> {
await this._pageProxySession.send('Input.dispatchMouseEvent', {
type: 'move',
@ -140,6 +147,23 @@ export class RawMouseImpl implements input.RawMouse {
clickCount
});
}
async wheel(x: number, y: number, buttons: Set<types.MouseButton>, modifiers: Set<types.KeyboardModifier>, deltaX: number, deltaY: number): Promise<void> {
await this._session!.send('Page.updateScrollingState');
// Wheel events hit the compositor first, so wait one frame for it to be synced.
await this._page!.mainFrame().evaluateExpression(`new Promise(requestAnimationFrame)`, false, false, 'utility');
await this._pageProxySession.send('Input.dispatchWheelEvent', {
x,
y,
deltaX,
deltaY,
modifiers: toModifiersMask(modifiers),
});
}
setPage(page: Page) {
this._page = page;
}
}
export class RawTouchscreenImpl implements input.RawTouchscreen {

View File

@ -85,6 +85,7 @@ export class WKPage implements PageDelegate {
this.rawTouchscreen = new RawTouchscreenImpl(pageProxySession);
this._contextIdToContext = new Map();
this._page = new Page(this, browserContext);
this.rawMouse.setPage(this._page);
this._workers = new WKWorkers(this._page);
this._session = undefined as any as WKSession;
this._browserContext = browserContext;
@ -139,6 +140,7 @@ export class WKPage implements PageDelegate {
eventsHelper.removeEventListeners(this._sessionListeners);
this._session = session;
this.rawKeyboard.setSession(session);
this.rawMouse.setSession(session);
this._addSessionListeners();
this._workers.setSession(session);
}

127
tests/page/wheel.spec.ts Normal file
View File

@ -0,0 +1,127 @@
/**
* 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 type { Page } from '../../';
import { test as it, expect } from './pageTest';
it.skip(({isElectron, browserMajorVersion}) => {
// Old Electron has flaky wheel events.
return isElectron && browserMajorVersion <= 11;
});
it('should dispatch wheel events', async ({page, server}) => {
await page.setContent(`<div style="width: 5000px; height: 5000px;"></div>`);
await page.mouse.move(50, 60);
await listenForWheelEvents(page, 'div');
await page.mouse.wheel(0, 100);
expect(await page.evaluate('window.lastEvent')).toEqual({
deltaX: 0,
deltaY: 100,
clientX: 50,
clientY: 60,
deltaMode: 0,
ctrlKey: false,
shiftKey: false,
altKey: false,
metaKey: false,
});
await page.waitForFunction('window.scrollY === 100');
});
it('should scroll when nobody is listening', async ({page, server}) => {
await page.goto(server.PREFIX + '/input/scrollable.html');
await page.mouse.move(50, 60);
await page.mouse.wheel(0, 100);
await page.waitForFunction('window.scrollY === 100');
});
it('should set the modifiers', async ({page}) => {
await page.setContent(`<div style="width: 5000px; height: 5000px;"></div>`);
await page.mouse.move(50, 60);
await listenForWheelEvents(page, 'div');
await page.keyboard.down('Shift');
await page.mouse.wheel(0, 100);
expect(await page.evaluate('window.lastEvent')).toEqual({
deltaX: 0,
deltaY: 100,
clientX: 50,
clientY: 60,
deltaMode: 0,
ctrlKey: false,
shiftKey: true,
altKey: false,
metaKey: false,
});
});
it('should scroll horizontally', async ({page}) => {
await page.setContent(`<div style="width: 5000px; height: 5000px;"></div>`);
await page.mouse.move(50, 60);
await listenForWheelEvents(page, 'div');
await page.mouse.wheel(100, 0);
expect(await page.evaluate('window.lastEvent')).toEqual({
deltaX: 100,
deltaY: 0,
clientX: 50,
clientY: 60,
deltaMode: 0,
ctrlKey: false,
shiftKey: false,
altKey: false,
metaKey: false,
});
await page.waitForFunction('window.scrollX === 100');
});
it('should work when the event is canceled', async ({page}) => {
await page.setContent(`<div style="width: 5000px; height: 5000px;"></div>`);
await page.mouse.move(50, 60);
await listenForWheelEvents(page, 'div');
await page.evaluate(() => {
document.querySelector('div').addEventListener('wheel', e => e.preventDefault());
});
await page.mouse.wheel(0, 100);
expect(await page.evaluate('window.lastEvent')).toEqual({
deltaX: 0,
deltaY: 100,
clientX: 50,
clientY: 60,
deltaMode: 0,
ctrlKey: false,
shiftKey: false,
altKey: false,
metaKey: false,
});
// give the page a chacne to scroll
await page.waitForTimeout(100);
// ensure that it did not.
expect(await page.evaluate('window.scrollY')).toBe(0);
});
async function listenForWheelEvents(page: Page, selector: string) {
await page.evaluate(selector => {
document.querySelector(selector).addEventListener('wheel', (e: WheelEvent) => {
window['lastEvent'] = {
deltaX: e.deltaX,
deltaY: e.deltaY,
clientX: e.clientX,
clientY: e.clientY,
deltaMode: e.deltaMode,
ctrlKey: e.ctrlKey,
shiftKey: e.shiftKey,
altKey: e.altKey,
metaKey: e.metaKey,
};
}, { passive: false });
}, selector);
}

10
types/types.d.ts vendored
View File

@ -13185,6 +13185,16 @@ export interface Mouse {
*/
clickCount?: number;
}): Promise<void>;
/**
* Dispatches a `wheel` event.
*
* > NOTE: Wheel events may cause scrolling if they are not handled, and this method does not wait for the scrolling to
* finish before returning.
* @param deltaX Pixels to scroll horizontally.
* @param deltaY Pixels to scroll vertically.
*/
wheel(deltaX: number, deltaY: number): Promise<void>;
}
/**