From 2e89c60c73ad8f9ec9745df7acbf6e1d4c7d2b02 Mon Sep 17 00:00:00 2001 From: Jaromir Obr Date: Fri, 6 Nov 2026 13:18:04 +0100 Subject: [PATCH 1/3] fix(typings): type grabWebElement(s) as CodeceptJS.WebElement (#5786) Generate a CodeceptJS.WebElement declaration from lib/element/WebElement.js and use it as the return type of grabWebElement() and grabWebElements(). Fix WebElement JSDoc that jsdoc could not parse or that referenced undefined types, and mark click()/type() options as optional. Co-Authored-By: Claude Opus 5.5 --- docs/webapi/grabWebElement.mustache | 2 +- docs/webapi/grabWebElements.mustache | 2 +- lib/element/WebElement.js | 10 +++++----- typings/jsdoc.conf.json | 1 + typings/tests/helpers/Playwright.types.ts | 13 +++++++++++++ typings/tests/helpers/PlaywrightTs.types.ts | 2 ++ 6 files changed, 23 insertions(+), 7 deletions(-) diff --git a/docs/webapi/grabWebElement.mustache b/docs/webapi/grabWebElement.mustache index 75171b69f..c2bfcf1a1 100644 --- a/docs/webapi/grabWebElement.mustache +++ b/docs/webapi/grabWebElement.mustache @@ -6,4 +6,4 @@ const webElement = await I.grabWebElement('#button'); ``` @param {CodeceptJS.LocatorOrString} locator element located by CSS|XPath|strict locator. -@returns {Promise<*>} WebElement of being used Web helper +@returns {Promise} WebElement of being used Web helper diff --git a/docs/webapi/grabWebElements.mustache b/docs/webapi/grabWebElements.mustache index 414cd9b53..fd91aa96b 100644 --- a/docs/webapi/grabWebElements.mustache +++ b/docs/webapi/grabWebElements.mustache @@ -6,4 +6,4 @@ const webElements = await I.grabWebElements('#button'); ``` @param {CodeceptJS.LocatorOrString} locator element located by CSS|XPath|strict locator. -@returns {Promise<*>} WebElement of being used Web helper +@returns {Promise} WebElements of being used Web helper diff --git a/lib/element/WebElement.js b/lib/element/WebElement.js index 0e64aeb69..2ff68286d 100644 --- a/lib/element/WebElement.js +++ b/lib/element/WebElement.js @@ -29,7 +29,7 @@ class WebElement { /** * Get the native element instance - * @returns {ElementHandle|WebElement|ElementHandle} Native element + * @returns {any} Native element (Playwright/Puppeteer ElementHandle or WebdriverIO element) */ getNativeElement() { return this.element @@ -220,7 +220,7 @@ class WebElement { /** * Click the element - * @param {Object} options Click options + * @param {Object} [options] Click options * @returns {Promise} */ async click(options = {}) { @@ -239,7 +239,7 @@ class WebElement { /** * Type text into the element * @param {string} text Text to type - * @param {Object} options Type options + * @param {Object} [options] Type options * @returns {Promise} */ async type(text, options = {}) { @@ -362,7 +362,7 @@ class WebElement { * Treat this element as an iframe; invoke `fn` with a WebElement wrapping * the iframe body. For WebDriver this switches the browser into the frame * for the duration of the callback and switches back on exit. - * @param {(body: WebElement) => Promise} fn + * @param {function(WebElement): Promise} fn * @returns {Promise} Return value of fn */ async inIframe(fn) { @@ -530,7 +530,7 @@ class WebElement { * @param {object} [opts] * @param {number} [opts.maxHtmlLength=300] passed through to toSimplifiedHTML * @param {string[]} [opts.attrs] attribute names to surface - * @returns {Promise<{text?: string, html?: string, visible?: boolean, enabled?: boolean, attrs?: object}>} + * @returns {Promise<{text: (string|undefined), html: (string|undefined), visible: (boolean|undefined), enabled: (boolean|undefined), attrs: (Object|undefined)}>} */ async describe({ maxHtmlLength = 300, attrs = ['id', 'class', 'name', 'role', 'type', 'href', 'value', 'aria-label', 'placeholder', 'data-testid'] } = {}) { const out = {} diff --git a/typings/jsdoc.conf.json b/typings/jsdoc.conf.json index 4c7b241e8..8220df787 100644 --- a/typings/jsdoc.conf.json +++ b/typings/jsdoc.conf.json @@ -10,6 +10,7 @@ "./lib/data/table.js", "./lib/data/dataTableArgument.js", "./lib/effects.js", + "./lib/element/WebElement.js", "./lib/event.js", "./lib/index.js", "./lib/locator.js", diff --git a/typings/tests/helpers/Playwright.types.ts b/typings/tests/helpers/Playwright.types.ts index c3ee53627..2976e6d74 100644 --- a/typings/tests/helpers/Playwright.types.ts +++ b/typings/tests/helpers/Playwright.types.ts @@ -76,6 +76,8 @@ expectType(playwright.dontSeeInField(str, str)); expectType(playwright.attachFile(str, str)); expectType(playwright.selectOption(str, str)); expectType>(playwright.grabNumberOfVisibleElements(str)); +expectType>(playwright.grabWebElements(str)); +expectType>(playwright.grabWebElement(str)); expectType(playwright.seeInCurrentUrl(str)); expectType(playwright.dontSeeInCurrentUrl(str)); expectType(playwright.seeCurrentUrlEquals(str)); @@ -148,3 +150,14 @@ expectType(playwright.dontSeeTraffic({ name: str, url: str })); expectType(playwright.dontSeeTraffic({ name: str, url: /hello/ })); expectError(playwright.dontSeeTraffic({ name: str })); expectError(playwright.dontSeeTraffic({ url: str })); + +async function webElementTypes(el: CodeceptJS.WebElement) { + expectType(await el.getText()); + expectType(await el.getAttribute(str)); + expectType(await el.isVisible()); + expectType(await el.click()); + expectType(await el.$(str)); + expectType(await el.$$(str)); + expectError(el.getAtribute(str)); + expectError(el.getAttribute(num)); +} diff --git a/typings/tests/helpers/PlaywrightTs.types.ts b/typings/tests/helpers/PlaywrightTs.types.ts index da631d33f..82b2dc2bb 100644 --- a/typings/tests/helpers/PlaywrightTs.types.ts +++ b/typings/tests/helpers/PlaywrightTs.types.ts @@ -74,6 +74,8 @@ expectType>(playwright.dontSeeInField(str, str)); expectType>(playwright.attachFile(str, str)); expectType>(playwright.selectOption(str, str)); expectType>(playwright.grabNumberOfVisibleElements(str)); +expectType>(playwright.grabWebElements(str)); +expectType>(playwright.grabWebElement(str)); expectType>(playwright.seeInCurrentUrl(str)); expectType>(playwright.dontSeeInCurrentUrl(str)); expectType>(playwright.seeCurrentUrlEquals(str)); From d26bf86b6ecae58635f77626c0d88b3d6b2c77f0 Mon Sep 17 00:00:00 2001 From: Jaromir Obr Date: Fri, 6 Nov 2026 13:49:58 +0100 Subject: [PATCH 2/3] fix(Puppeteer): return WebElement from grabWebElement() A duplicate grabWebElement() overrode the WebElement-returning one and returned the raw ElementHandle, contradicting the new typings. Co-Authored-By: Claude Opus 5.5 --- lib/helper/Puppeteer.js | 6 ------ test/helper/webapi.js | 2 +- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/lib/helper/Puppeteer.js b/lib/helper/Puppeteer.js index 7102f35da..2f3daab1c 100644 --- a/lib/helper/Puppeteer.js +++ b/lib/helper/Puppeteer.js @@ -1097,12 +1097,6 @@ class Puppeteer extends Helper { return new WebElement(elements[0], this) } - async grabWebElement(locator) { - const els = await this._locate(locator) - assertElementExists(els, locator) - return els[0] - } - /** * Switch focus to a particular tab by its number. It waits tabs loading and then switch tab * diff --git a/test/helper/webapi.js b/test/helper/webapi.js index 9f1b08ad9..64492b89d 100644 --- a/test/helper/webapi.js +++ b/test/helper/webapi.js @@ -2738,7 +2738,7 @@ export function tests() { // Test grabbing specific element if (!isHelper('WebDriver')) { const submitButton = await I.grabWebElement({ role: 'button', text: 'Submit' }) - assert.ok(submitButton) + assert.equal(await submitButton.getText(), 'Submit') } // Test grabbing text from role elements From 542b60a82ce7b0330672c93fe3da55e5c2c7a080 Mon Sep 17 00:00:00 2001 From: Jaromir Obr Date: Fri, 6 Nov 2026 14:00:56 +0100 Subject: [PATCH 3/3] test(webapi): check grabWebElement() result via toOuterHTML() getText() is not implemented for CDPBrowser's WebElement, which broke the Lightpanda, Moli and Obscura jobs. toOuterHTML() works on all helpers and still fails on a raw Puppeteer ElementHandle. Co-Authored-By: Claude Opus 5.5 --- test/helper/webapi.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/helper/webapi.js b/test/helper/webapi.js index 64492b89d..9449cddbd 100644 --- a/test/helper/webapi.js +++ b/test/helper/webapi.js @@ -2738,7 +2738,7 @@ export function tests() { // Test grabbing specific element if (!isHelper('WebDriver')) { const submitButton = await I.grabWebElement({ role: 'button', text: 'Submit' }) - assert.equal(await submitButton.getText(), 'Submit') + assert.match(await submitButton.toOuterHTML(), /id="submitBtn"/) } // Test grabbing text from role elements