From 36c4f367cf2e2a360dda5f7bb9bbceecdb188fcf Mon Sep 17 00:00:00 2001 From: DudaGod Date: Mon, 12 Feb 2024 15:42:22 +0300 Subject: [PATCH] fix: move cursor relative to the center of body when using "resetCursor" option --- README.md | 37 +++++++++- src/browser/commands/index.js | 1 + src/browser/commands/moveCursorTo.ts | 34 +++++++++ src/worker/runner/test-runner/index.js | 17 +++-- test/src/worker/runner/test-runner/index.js | 80 ++++++++++++++++----- 5 files changed, 143 insertions(+), 26 deletions(-) create mode 100644 src/browser/commands/moveCursorTo.ts diff --git a/README.md b/README.md index 578949164..a32fdc452 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,11 @@ Hermione is a utility for integration testing of web pages using [WebdriverIO](h - [Configuring .hermione.conf.js by yourself (a slow way)](#configuring-hermioneconfjs-by-yourself-a-slow-way) - [Chrome Devtools Protocol](#chrome-devtools-protocol) - [Webdriver protocol](#webdriver-protocol) +- [Commands API](#commands-api) + - [Browser commands](#browser-commands) + - [clearSession](#clearsession) + - [Element commands](#element-commands) + - [moveCursorTo](#movecursorto) - [Tests API](#tests-api) - [Arguments](#arguments) - [Hooks](#hooks) @@ -110,10 +115,15 @@ Hermione is a utility for integration testing of web pages using [WebdriverIO](h - [Reporters](#reporters) - [Require modules](#require-modules) - [Overriding settings](#overriding-settings) + - [Debug mode](#debug-mode) + - [REPL mode](#repl-mode) + - [switchToRepl](#switchtorepl) + - [Test development in runtime](#test-development-in-runtime) + - [How to set up using VSCode](#how-to-set-up-using-vscode) + - [How to set up using Webstorm](#how-to-set-up-using-webstorm) - [Environment variables](#environment-variables) - [HERMIONE_SKIP_BROWSERS](#hermione_skip_browsers) - [HERMIONE_SETS](#hermione_sets) - - [Debug mode](#debug-mode) - [Programmatic API](#programmatic-api) - [config](#config) - [events](#events) @@ -391,12 +401,14 @@ node_modules/.bin/hermione Since Hermione is based on [WebdriverIO v8](https://webdriver.io/docs/api/), all the commands provided by WebdriverIO are available in it. But Hermione also has her own commands. -### clearSession +### Browser commands + +#### clearSession Browser command that clears session state (deletes cookies, clears local and session storages). For example: ```js -it('', async ({ browser }) => { +it('test', async ({ browser }) => { await browser.url('https://github.com/gemini-testing/hermione'); (await browser.getCookies()).length; // 5 @@ -411,6 +423,25 @@ it('', async ({ browser }) => { }); ``` +### Element commands + +#### moveCursorTo + +> This command is temporary and will be removed in the next major (hermione@9). Differs from the standard [moveTo](https://webdriver.io/docs/api/element/moveTo/) in that it moves the cursor relative to the top-left corner of the element (like it was in hermione@7). + +Move the mouse by an offset of the specified element. If offset is not specified then mouse will be moved to the top-left corder of the element. + +Usage: + +```typescript +await browser.$(selector).moveCursorTo({ xOffset, yOffset }); +``` + +Available parameters: + +* **xOffset** (optional) `Number` – X offset to move to, relative to the top-left corner of the element; +* **yOffset** (optional) `Number` – Y offset to move to, relative to the top-left corner of the element. + ## Tests API ### Arguments diff --git a/src/browser/commands/index.js b/src/browser/commands/index.js index 7396925fd..36d2a5d8a 100644 --- a/src/browser/commands/index.js +++ b/src/browser/commands/index.js @@ -9,4 +9,5 @@ module.exports = [ "scrollIntoView", "openAndWait", "switchToRepl", + "moveCursorTo", ]; diff --git a/src/browser/commands/moveCursorTo.ts b/src/browser/commands/moveCursorTo.ts new file mode 100644 index 000000000..8476f2330 --- /dev/null +++ b/src/browser/commands/moveCursorTo.ts @@ -0,0 +1,34 @@ +import type { Browser } from "../types"; + +type MoveToOptions = { + xOffset?: number; + yOffset?: number; +}; + +// TODO: remove after next major version +export = async (browser: Browser): Promise => { + const { publicAPI: session } = browser; + + session.addCommand( + "moveCursorTo", + async function (this: WebdriverIO.Element, { xOffset = 0, yOffset = 0 }: MoveToOptions = {}): Promise { + if (!this.isW3C) { + return this.moveToElement(this.elementId, xOffset, yOffset); + } + + const { x, y, width, height } = await this.getElementRect(this.elementId); + const { scrollX, scrollY } = await session.execute(function (this: Window) { + return { scrollX: this.scrollX, scrollY: this.scrollY }; + }); + + const newXOffset = Math.floor(x - scrollX + (typeof xOffset === "number" ? xOffset : width / 2)); + const newYOffset = Math.floor(y - scrollY + (typeof yOffset === "number" ? yOffset : height / 2)); + + return session + .action("pointer", { parameters: { pointerType: "mouse" } }) + .move({ x: newXOffset, y: newYOffset }) + .perform(); + }, + true, + ); +}; diff --git a/src/worker/runner/test-runner/index.js b/src/worker/runner/test-runner/index.js index 6530c99d6..4446b11f5 100644 --- a/src/worker/runner/test-runner/index.js +++ b/src/worker/runner/test-runner/index.js @@ -125,11 +125,18 @@ module.exports = class TestRunner { await body.scrollIntoView(); - const { x = 0, y = 0 } = await session.execute(function () { - return document.body.getBoundingClientRect(); - }); - // x and y must be integer, wdio will throw error otherwise - await body.moveTo({ xOffset: -Math.floor(x), yOffset: -Math.floor(y) }); + if (!session.isW3C) { + const { x = 0, y = 0 } = await session.execute(function () { + return document.body.getBoundingClientRect(); + }); + + return session.moveToElement(body.elementId, -Math.floor(x), -Math.floor(y)); + } + + await session + .action("pointer", { parameters: { pointerType: "mouse" } }) + .move({ x: 0, y: 0 }) + .perform(); } }; diff --git a/test/src/worker/runner/test-runner/index.js b/test/src/worker/runner/test-runner/index.js index f6c835a66..c9b320de0 100644 --- a/test/src/worker/runner/test-runner/index.js +++ b/test/src/worker/runner/test-runner/index.js @@ -36,15 +36,31 @@ describe("worker/runner/test-runner", () => { const mkElement_ = proto => { return _.defaults(proto, { scrollIntoView: sandbox.stub().named("scrollIntoView").resolves(), - moveTo: sandbox.stub().named("moveTo").resolves(), + getSize: sandbox.stub().named("getSize").resolves({ width: 100, height: 500 }), + elementId: 100500, }); }; + const mkActionAPI_ = () => { + const actionStub = {}; + actionStub.move = sandbox.stub().named("move").returns(actionStub); + actionStub.perform = sandbox.stub().named("perform").resolves(); + + return actionStub; + }; + const mkBrowser_ = ({ prototype, config, id } = {}) => { + const actionStub = {}; + actionStub.move = sandbox.stub().named("move").returns(actionStub); + actionStub.perform = sandbox.stub().named("perform").resolves(); + const publicAPI = _.defaults(prototype, { $: sandbox.stub().named("$").resolves(mkElement_()), execute: sandbox.stub().named("execute").resolves({ x: 0, y: 0 }), assertView: sandbox.stub().named("assertView").resolves(), + moveToElement: sandbox.stub().named("moveToElement").resolves(), + action: sandbox.stub().named("getSize").returns(mkActionAPI_()), + isW3C: true, }); config = _.defaults(config, { resetCursor: true }); @@ -272,7 +288,7 @@ describe("worker/runner/test-runner", () => { it('should not move cursor to position "0,0" on body element', async () => { await run_(); - assert.notCalled(body.moveTo); + assert.notCalled(browser.publicAPI.action); }); }); @@ -309,32 +325,60 @@ describe("worker/runner/test-runner", () => { assert.calledOnceWith(body.scrollIntoView); }); - it('should move cursor to position "0,0" on body element', async () => { - await run_(); + describe("in jsonwp protocol", () => { + beforeEach(() => { + browser.publicAPI.isW3C = false; + }); - assert.calledOnceWith(body.moveTo, { xOffset: 0, yOffset: 0 }); - }); + it("should scroll before moving cursor", async () => { + await run_(); - it("should move cursor correctly if body element has negative coords", async () => { - browser.publicAPI.execute.resolves({ x: -100, y: -500 }); + assert.callOrder(body.scrollIntoView, browser.publicAPI.moveToElement); + }); - await run_(); + it('should move cursor to position "0,0"', async () => { + browser.publicAPI.execute.resolves({ x: 5, y: 5 }); + body.elementId = 12345; - assert.calledOnceWith(body.moveTo, { xOffset: 100, yOffset: 500 }); - }); + await run_(); - it("should scroll before moving cursor", async () => { - await run_(); + assert.calledOnceWith(browser.publicAPI.moveToElement, 12345, -5, -5); + }); - assert.callOrder(body.scrollIntoView, body.moveTo); + it("should floor coords if body element has fractional coords", async () => { + browser.publicAPI.execute.resolves({ x: 10.123, y: 15.899 }); + body.elementId = 12345; + + await run_(); + + assert.calledOnceWith(browser.publicAPI.moveToElement, 12345, -10, -15); + }); }); - it("should floor coords if body element has fractional coords", async () => { - browser.publicAPI.execute.resolves({ x: 10.123, y: 15.899 }); + describe("in w3c protocol", () => { + beforeEach(() => { + browser.publicAPI.isW3C = true; + }); - await run_(); + it("should scroll before moving cursor", async () => { + await run_(); - assert.calledOnceWith(body.moveTo, { xOffset: -10, yOffset: -15 }); + assert.callOrder(body.scrollIntoView, browser.publicAPI.action); + }); + + it('should move cursor to position "0,0"', async () => { + const actionAPI = mkActionAPI_(); + browser.publicAPI.action.returns(actionAPI); + + await run_(); + + assert.calledOnceWith(browser.publicAPI.action, "pointer", { + parameters: { pointerType: "mouse" }, + }); + assert.calledOnceWith(actionAPI.move, { x: 0, y: 0 }); + assert.calledOnce(actionAPI.perform); + assert.callOrder(browser.publicAPI.action, actionAPI.move, actionAPI.perform); + }); }); }); });