From 15be2b7fddf78e6d96818d5478b367d0985bb36f Mon Sep 17 00:00:00 2001 From: DavertMik Date: Tue, 17 Mar 2026 04:45:15 +0200 Subject: [PATCH 1/2] feat: add optional context parameter to moveCursorTo Allow scoping moveCursorTo to a parent element by passing a context locator as the second argument, matching the pattern used by click, seeElement, etc. When the second arg is non-number, it is treated as context instead of offsetX. Co-Authored-By: Claude Opus 4.6 (1M context) --- docs/webapi/moveCursorTo.mustache | 6 +++++- lib/helper/Playwright.js | 19 +++++++++++++++++-- lib/helper/Puppeteer.js | 24 +++++++++++++++++++++--- lib/helper/WebDriver.js | 18 ++++++++++++++++-- test/helper/Playwright_test.js | 5 +++++ test/helper/Puppeteer_test.js | 5 +++++ test/helper/WebDriver_test.js | 6 ++++++ 7 files changed, 75 insertions(+), 8 deletions(-) diff --git a/docs/webapi/moveCursorTo.mustache b/docs/webapi/moveCursorTo.mustache index 2067d894f..b5d212669 100644 --- a/docs/webapi/moveCursorTo.mustache +++ b/docs/webapi/moveCursorTo.mustache @@ -1,12 +1,16 @@ Moves cursor to element matched by locator. Extra shift can be set with offsetX and offsetY options. +An optional `context` (as a second parameter) can be specified to narrow the search to an element within a parent. +When the second argument is a non-number (string or locator object), it is treated as context. + ```js I.moveCursorTo('.tooltip'); I.moveCursorTo('#submit', 5,5); +I.moveCursorTo('#submit', '.container'); ``` @param {CodeceptJS.LocatorOrString} locator located by CSS|XPath|strict locator. -@param {number} [offsetX=0] (optional, `0` by default) X-axis offset. +@param {number|CodeceptJS.LocatorOrString} [offsetX=0] (optional, `0` by default) X-axis offset or context locator. @param {number} [offsetY=0] (optional, `0` by default) Y-axis offset. @returns {void} automatically synchronized promise through #recorder diff --git a/lib/helper/Playwright.js b/lib/helper/Playwright.js index 2afcafd33..034e5053e 100644 --- a/lib/helper/Playwright.js +++ b/lib/helper/Playwright.js @@ -1495,8 +1495,23 @@ class Playwright extends Helper { * */ async moveCursorTo(locator, offsetX = 0, offsetY = 0) { - const el = await this._locateElement(locator) - assertElementExists(el, locator) + let context = null + if (typeof offsetX !== 'number') { + context = offsetX + offsetX = 0 + } + + let el + if (context) { + const contextEls = await this._locate(context) + assertElementExists(contextEls, context, 'Context element') + el = await findElements.call(this, contextEls[0], locator) + assertElementExists(el, locator) + el = el[0] + } else { + el = await this._locateElement(locator) + assertElementExists(el, locator) + } // Use manual mouse.move instead of .hover() so the offset can be added to the coordinates const { x, y } = await clickablePoint(el) diff --git a/lib/helper/Puppeteer.js b/lib/helper/Puppeteer.js index a19a25935..f2ebd9ed5 100644 --- a/lib/helper/Puppeteer.js +++ b/lib/helper/Puppeteer.js @@ -821,9 +821,27 @@ class Puppeteer extends Helper { * {{ react }} */ async moveCursorTo(locator, offsetX = 0, offsetY = 0) { - const el = await this._locateElement(locator) - if (!el) { - throw new ElementNotFound(locator, 'Element to move cursor to') + let context = null + if (typeof offsetX !== 'number') { + context = offsetX + offsetX = 0 + } + + let el + if (context) { + const contextPage = await this.context + const contextEls = await findElements.call(this, contextPage, context) + assertElementExists(contextEls, context, 'Context element') + const els = await findElements.call(this, contextEls[0], locator) + if (!els || els.length === 0) { + throw new ElementNotFound(locator, 'Element to move cursor to') + } + el = els[0] + } else { + el = await this._locateElement(locator) + if (!el) { + throw new ElementNotFound(locator, 'Element to move cursor to') + } } // Use manual mouse.move instead of .hover() so the offset can be added to the coordinates diff --git a/lib/helper/WebDriver.js b/lib/helper/WebDriver.js index 3169fba0a..74107ced8 100644 --- a/lib/helper/WebDriver.js +++ b/lib/helper/WebDriver.js @@ -1976,8 +1976,22 @@ class WebDriver extends Helper { * {{> moveCursorTo }} */ async moveCursorTo(locator, xOffset, yOffset) { - const res = await this._locate(withStrictLocator(locator), true) - assertElementExists(res, locator) + let context = null + if (typeof xOffset !== 'number' && xOffset !== undefined) { + context = xOffset + xOffset = undefined + } + + let res + if (context) { + const contextRes = await this._locate(withStrictLocator(context), true) + assertElementExists(contextRes, context, 'Context element') + res = await contextRes[0].$$(withStrictLocator(locator)) + assertElementExists(res, locator) + } else { + res = await this._locate(withStrictLocator(locator), true) + assertElementExists(res, locator) + } const elem = usingFirstElement(res) try { await elem.moveTo({ xOffset, yOffset }) diff --git a/test/helper/Playwright_test.js b/test/helper/Playwright_test.js index 3d111a9dc..e5edb5eb3 100644 --- a/test/helper/Playwright_test.js +++ b/test/helper/Playwright_test.js @@ -276,6 +276,11 @@ describe('Playwright', function () { I.amOnPage('/form/hover') .then(() => I.moveCursorTo('#hover', 100, 100)) .then(() => I.dontSee('Hovered', '#show'))) + + it('should trigger hover event within a context', () => + I.amOnPage('/form/hover') + .then(() => I.moveCursorTo('#hover', 'body')) + .then(() => I.see('Hovered', '#show'))) }) describe('#switchToNextTab, #switchToPreviousTab, #openNewTab, #closeCurrentTab, #closeOtherTabs, #grabNumberOfOpenTabs, #waitForNumberOfTabs', () => { diff --git a/test/helper/Puppeteer_test.js b/test/helper/Puppeteer_test.js index ebafa1ebe..796178066 100644 --- a/test/helper/Puppeteer_test.js +++ b/test/helper/Puppeteer_test.js @@ -240,6 +240,11 @@ describe('Puppeteer', function () { I.amOnPage('/form/hover') .then(() => I.moveCursorTo('#hover', 100, 100)) .then(() => I.dontSee('Hovered', '#show'))) + + it('should trigger hover event within a context', () => + I.amOnPage('/form/hover') + .then(() => I.moveCursorTo('#hover', 'body')) + .then(() => I.see('Hovered', '#show'))) }) describe('#switchToNextTab, #switchToPreviousTab, #openNewTab, #closeCurrentTab, #closeOtherTabs, #grabNumberOfOpenTabs, #waitForNumberOfTabs', () => { diff --git a/test/helper/WebDriver_test.js b/test/helper/WebDriver_test.js index b81321dab..8a15ea0ff 100644 --- a/test/helper/WebDriver_test.js +++ b/test/helper/WebDriver_test.js @@ -596,6 +596,12 @@ describe('WebDriver', function () { await wd.moveCursorTo('#hover', 100, 100) await wd.dontSee('Hovered', '#show') }) + + it('should trigger hover event within a context', async () => { + await wd.amOnPage('/form/hover') + await wd.moveCursorTo('#hover', 'body') + await wd.see('Hovered', '#show') + }) }) describe('#switchToNextTab, #switchToPreviousTab, #openNewTab, #closeCurrentTab, #closeOtherTabs, #grabNumberOfOpenTabs, #waitForNumberOfTabs', () => { From e5dca2a04450dd3d0482c92415a706f8baf8fe86 Mon Sep 17 00:00:00 2001 From: DavertMik Date: Tue, 17 Mar 2026 22:03:05 +0200 Subject: [PATCH 2/2] fix: use page instead of context for moveCursorTo context lookup in Puppeteer In Puppeteer, this.context is scoped to , so searching for "body" within it returns empty. Use this.page directly to find the context element from the document root. Co-Authored-By: Claude Opus 4.6 (1M context) --- lib/helper/Puppeteer.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/helper/Puppeteer.js b/lib/helper/Puppeteer.js index f2ebd9ed5..c9ea57806 100644 --- a/lib/helper/Puppeteer.js +++ b/lib/helper/Puppeteer.js @@ -829,8 +829,7 @@ class Puppeteer extends Helper { let el if (context) { - const contextPage = await this.context - const contextEls = await findElements.call(this, contextPage, context) + const contextEls = await findElements.call(this, this.page, context) assertElementExists(contextEls, context, 'Context element') const els = await findElements.call(this, contextEls[0], locator) if (!els || els.length === 0) {