From 6a7315631c38faefe919e9d104eeee3c37ba4977 Mon Sep 17 00:00:00 2001 From: DavertMik Date: Thu, 10 Sep 2026 16:06:48 +0300 Subject: [PATCH] fix(helpers): scroll element into view in moveCursorTo moveCursorTo moved the mouse to the element's center coordinates without scrolling first. For an element outside the viewport the cursor landed off-screen and no hover fired, with no error. Playwright now calls scrollIntoViewIfNeeded() before reading the point. Puppeteer scrolls the element to the center when it is not fully in the viewport, the same check its own ElementHandle.hover() performs, using public API only. WebDriver needs no change: WebdriverIO's moveTo already scrolls into view. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01VmYyNa6z4o3hxPkdpDiUmB --- lib/helper/Playwright.js | 1 + lib/helper/Puppeteer.js | 3 +++ test/data/app/view/form/hover.php | 3 +++ test/helper/Playwright_test.js | 12 ++++++++++++ test/helper/Puppeteer_test.js | 12 ++++++++++++ 5 files changed, 31 insertions(+) diff --git a/lib/helper/Playwright.js b/lib/helper/Playwright.js index 33399fdef..b8f3902ed 100644 --- a/lib/helper/Playwright.js +++ b/lib/helper/Playwright.js @@ -1525,6 +1525,7 @@ class Playwright extends Helper { assertElementExists(el, locator) } + await el.scrollIntoViewIfNeeded() // Use manual mouse.move instead of .hover() so the offset can be added to the coordinates const { x, y } = await clickablePoint(el) await this.page.mouse.move(x + offsetX, y + offsetY) diff --git a/lib/helper/Puppeteer.js b/lib/helper/Puppeteer.js index 3d1f7a02e..4ba0b7974 100644 --- a/lib/helper/Puppeteer.js +++ b/lib/helper/Puppeteer.js @@ -845,6 +845,9 @@ class Puppeteer extends Helper { } } + if (!(await el.isIntersectingViewport({ threshold: 1 }))) { + await el.evaluate(el => el.scrollIntoView({ block: 'center', inline: 'center' })) + } // Use manual mouse.move instead of .hover() so the offset can be added to the coordinates const { x, y } = await getClickablePoint(el) await this.page.mouse.move(x + offsetX, y + offsetY) diff --git a/test/data/app/view/form/hover.php b/test/data/app/view/form/hover.php index bbf3534f9..fbbf12f1a 100644 --- a/test/data/app/view/form/hover.php +++ b/test/data/app/view/form/hover.php @@ -7,5 +7,8 @@
+
+Hover me too! + diff --git a/test/helper/Playwright_test.js b/test/helper/Playwright_test.js index 21bd02deb..45c3cec99 100644 --- a/test/helper/Playwright_test.js +++ b/test/helper/Playwright_test.js @@ -395,6 +395,18 @@ describe('Playwright', function () { I.amOnPage('/form/hover') .then(() => I.moveCursorTo('#hover', 'body')) .then(() => I.see('Hovered', '#show'))) + + it('should scroll element into view before hovering', async () => { + await I.amOnPage('/form/hover') + await I.moveCursorTo('#offscreen_hover') + await I.see('Hovered offscreen', '#offscreen_show') + }) + + it('should scroll element into view before hovering within a context', async () => { + await I.amOnPage('/form/hover') + await I.moveCursorTo('#offscreen_hover', 'body') + await I.see('Hovered offscreen', '#offscreen_show') + }) }) describe('#switchToNextTab, #switchToPreviousTab, #openNewTab, #closeCurrentTab, #closeOtherTabs, #grabNumberOfOpenTabs, #waitForNumberOfTabs', () => { diff --git a/test/helper/Puppeteer_test.js b/test/helper/Puppeteer_test.js index 796178066..cbb83b925 100644 --- a/test/helper/Puppeteer_test.js +++ b/test/helper/Puppeteer_test.js @@ -245,6 +245,18 @@ describe('Puppeteer', function () { I.amOnPage('/form/hover') .then(() => I.moveCursorTo('#hover', 'body')) .then(() => I.see('Hovered', '#show'))) + + it('should scroll element into view before hovering', async () => { + await I.amOnPage('/form/hover') + await I.moveCursorTo('#offscreen_hover') + await I.see('Hovered offscreen', '#offscreen_show') + }) + + it('should scroll element into view before hovering within a context', async () => { + await I.amOnPage('/form/hover') + await I.moveCursorTo('#offscreen_hover', 'body') + await I.see('Hovered offscreen', '#offscreen_show') + }) }) describe('#switchToNextTab, #switchToPreviousTab, #openNewTab, #closeCurrentTab, #closeOtherTabs, #grabNumberOfOpenTabs, #waitForNumberOfTabs', () => {