mirror of
https://github.com/microsoft/playwright.git
synced 2025-01-07 11:46:42 +03:00
feat(mouse): page.mouse.wheel (#8690)
This commit is contained in:
parent
351c20be48
commit
afae5bef5d
@ -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.
|
||||
|
@ -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 {
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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',
|
||||
|
@ -1024,6 +1024,13 @@ Page:
|
||||
tracing:
|
||||
snapshot: true
|
||||
|
||||
mouseWheel:
|
||||
parameters:
|
||||
deltaX: number
|
||||
deltaY: number
|
||||
tracing:
|
||||
snapshot: true
|
||||
|
||||
touchscreenTap:
|
||||
parameters:
|
||||
x: number
|
||||
|
@ -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,
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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.
|
||||
|
@ -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[]>([
|
||||
|
@ -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 {
|
||||
|
@ -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
127
tests/page/wheel.spec.ts
Normal 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
10
types/types.d.ts
vendored
@ -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>;
|
||||
}
|
||||
|
||||
/**
|
||||
|
Loading…
Reference in New Issue
Block a user