diff --git a/docs/webapi/dontSeeElement.mustache b/docs/webapi/dontSeeElement.mustache index 4a6fd28d6..b7639a299 100644 --- a/docs/webapi/dontSeeElement.mustache +++ b/docs/webapi/dontSeeElement.mustache @@ -1,8 +1,12 @@ Opposite to `seeElement`. Checks that element is not visible (or in DOM) +The second parameter is a context (CSS or XPath locator) to narrow the search. + ```js I.dontSeeElement('.modal'); // modal is not shown +I.dontSeeElement('.modal', '#container'); ``` @param {CodeceptJS.LocatorOrString} locator located by CSS|XPath|Strict locator. +@param {?CodeceptJS.LocatorOrString} [context=null] (optional, `null` by default) element located by CSS | XPath | strict locator. @returns {void} automatically synchronized promise through #recorder diff --git a/docs/webapi/fillField.mustache b/docs/webapi/fillField.mustache index db5700b31..f78e3458a 100644 --- a/docs/webapi/fillField.mustache +++ b/docs/webapi/fillField.mustache @@ -1,6 +1,8 @@ Fills a text field or textarea, after clearing its value, with the given string. Field is located by name, label, CSS, or XPath. +The third parameter is a context (CSS or XPath locator) to narrow the search. + ```js // by label I.fillField('Email', 'hello@world.com'); @@ -10,7 +12,10 @@ I.fillField('password', secret('123456')); I.fillField('form#login input[name=username]', 'John'); // or by strict locator I.fillField({css: 'form#login input[name=username]'}, 'John'); +// within a context +I.fillField('Name', 'John', '#section2'); ``` @param {CodeceptJS.LocatorOrString} field located by label|name|CSS|XPath|strict locator. @param {CodeceptJS.StringOrSecret} value text value to fill. +@param {?CodeceptJS.LocatorOrString} [context=null] (optional, `null` by default) element located by CSS | XPath | strict locator. @returns {void} automatically synchronized promise through #recorder diff --git a/docs/webapi/seeElement.mustache b/docs/webapi/seeElement.mustache index 93a375904..ba7cf8491 100644 --- a/docs/webapi/seeElement.mustache +++ b/docs/webapi/seeElement.mustache @@ -1,8 +1,12 @@ Checks that a given Element is visible Element is located by CSS or XPath. +The second parameter is a context (CSS or XPath locator) to narrow the search. + ```js I.seeElement('#modal'); +I.seeElement('#modal', '#container'); ``` @param {CodeceptJS.LocatorOrString} locator located by CSS|XPath|strict locator. +@param {?CodeceptJS.LocatorOrString} [context=null] (optional, `null` by default) element located by CSS | XPath | strict locator. @returns {void} automatically synchronized promise through #recorder diff --git a/docs/webapi/selectOption.mustache b/docs/webapi/selectOption.mustache index 63b05946b..587a2a26a 100644 --- a/docs/webapi/selectOption.mustache +++ b/docs/webapi/selectOption.mustache @@ -2,6 +2,8 @@ Selects an option in a drop-down select. Field is searched by label | name | CSS | XPath. Option is selected by visible text or by value. +The third parameter is a context (CSS or XPath locator) to narrow the search. + ```js I.selectOption('Choose Plan', 'Monthly'); // select by label I.selectOption('subscription', 'Monthly'); // match option by text @@ -9,6 +11,8 @@ I.selectOption('subscription', '0'); // or by value I.selectOption('//form/select[@name=account]','Premium'); I.selectOption('form select[name=account]', 'Premium'); I.selectOption({css: 'form select[name=account]'}, 'Premium'); +// within a context +I.selectOption('age', '21-60', '#section2'); ``` Provide an array for the second argument to select multiple options. @@ -18,4 +22,5 @@ I.selectOption('Which OS do you use?', ['Android', 'iOS']); ``` @param {LocatorOrString} select field located by label|name|CSS|XPath|strict locator. @param {string|Array<*>} option visible text or value of option. +@param {?CodeceptJS.LocatorOrString} [context=null] (optional, `null` by default) element located by CSS | XPath | strict locator. @returns {void} automatically synchronized promise through #recorder diff --git a/lib/helper/Appium.js b/lib/helper/Appium.js index 2a52f8196..d0908b67f 100644 --- a/lib/helper/Appium.js +++ b/lib/helper/Appium.js @@ -1543,8 +1543,8 @@ class Appium extends Webdriver { /** * {{> dontSeeElement }} */ - async dontSeeElement(locator) { - if (this.isWeb) return super.dontSeeElement(locator) + async dontSeeElement(locator, context = null) { + if (this.isWeb) return super.dontSeeElement(locator, context) // For mobile native apps, use safe isDisplayed wrapper const parsedLocator = parseLocator.call(this, locator) @@ -1589,9 +1589,9 @@ class Appium extends Webdriver { * {{> fillField }} * */ - async fillField(field, value) { + async fillField(field, value, context = null) { value = value.toString() - if (this.isWeb) return super.fillField(field, value) + if (this.isWeb) return super.fillField(field, value, context) return super.fillField(parseLocator.call(this, field), value) } @@ -1706,8 +1706,8 @@ class Appium extends Webdriver { * {{> seeElement }} * */ - async seeElement(locator) { - if (this.isWeb) return super.seeElement(locator) + async seeElement(locator, context = null) { + if (this.isWeb) return super.seeElement(locator, context) // For mobile native apps, use safe isDisplayed wrapper const parsedLocator = parseLocator.call(this, locator) @@ -1754,8 +1754,8 @@ class Appium extends Webdriver { * * Supported only for web testing */ - async selectOption(select, option) { - if (this.isWeb) return super.selectOption(select, option) + async selectOption(select, option, context = null) { + if (this.isWeb) return super.selectOption(select, option, context) throw new Error("Should be used only in Web context. In native context use 'click' method instead") } diff --git a/lib/helper/Playwright.js b/lib/helper/Playwright.js index 2ecbf0336..9b032d56a 100644 --- a/lib/helper/Playwright.js +++ b/lib/helper/Playwright.js @@ -1944,8 +1944,15 @@ class Playwright extends Helper { * {{> seeElement }} * */ - async seeElement(locator) { - let els = await this._locate(locator) + async seeElement(locator, context = null) { + let els + if (context) { + const contextEls = await this._locate(context) + assertElementExists(contextEls, context, 'Context element') + els = await findElements.call(this, contextEls[0], locator) + } else { + els = await this._locate(locator) + } els = await Promise.all(els.map(el => el.isVisible())) try { return empty('visible elements').negate(els.filter(v => v).fill('ELEMENT')) @@ -1958,8 +1965,15 @@ class Playwright extends Helper { * {{> dontSeeElement }} * */ - async dontSeeElement(locator) { - let els = await this._locate(locator) + async dontSeeElement(locator, context = null) { + let els + if (context) { + const contextEls = await this._locate(context) + assertElementExists(contextEls, context, 'Context element') + els = await findElements.call(this, contextEls[0], locator) + } else { + els = await this._locate(locator) + } els = await Promise.all(els.map(el => el.isVisible())) try { return empty('visible elements').assert(els.filter(v => v).fill('ELEMENT')) @@ -2245,8 +2259,8 @@ class Playwright extends Helper { * {{> fillField }} * */ - async fillField(field, value) { - const els = await findFields.call(this, field) + async fillField(field, value, context = null) { + const els = await findFields.call(this, field, context) assertElementExists(els, field, 'Field') if (this.options.strict) assertOnlyOneElement(els, field) const el = els[0] @@ -2340,31 +2354,39 @@ class Playwright extends Helper { /** * {{> selectOption }} */ - async selectOption(select, option) { - const context = await this.context + async selectOption(select, option, context = null) { + const pageContext = await this.context const matchedLocator = new Locator(select) + let contextEl + if (context) { + const contextEls = await this._locate(context) + assertElementExists(contextEls, context, 'Context element') + contextEl = contextEls[0] + } + // Strict locator if (!matchedLocator.isFuzzy()) { this.debugSection('SelectOption', `Strict: ${JSON.stringify(select)}`) - const els = await this._locate(matchedLocator) + const els = contextEl ? await findElements.call(this, contextEl, matchedLocator) : await this._locate(matchedLocator) assertElementExists(els, select, 'Selectable element') - return proceedSelect.call(this, context, els[0], option) + return proceedSelect.call(this, pageContext, els[0], option) } // Fuzzy: try combobox this.debugSection('SelectOption', `Fuzzy: "${matchedLocator.value}"`) - let els = await findByRole(context, { role: 'combobox', name: matchedLocator.value }) - if (els?.length) return proceedSelect.call(this, context, els[0], option) + const comboboxSearchCtx = contextEl || pageContext + let els = await findByRole(comboboxSearchCtx, { role: 'combobox', name: matchedLocator.value }) + if (els?.length) return proceedSelect.call(this, pageContext, els[0], option) // Fuzzy: try listbox - els = await findByRole(context, { role: 'listbox', name: matchedLocator.value }) - if (els?.length) return proceedSelect.call(this, context, els[0], option) + els = await findByRole(comboboxSearchCtx, { role: 'listbox', name: matchedLocator.value }) + if (els?.length) return proceedSelect.call(this, pageContext, els[0], option) // Fuzzy: try native select - els = await findFields.call(this, select) + els = await findFields.call(this, select, context) assertElementExists(els, select, 'Selectable element') - return proceedSelect.call(this, context, els[0], option) + return proceedSelect.call(this, pageContext, els[0], option) } /** @@ -4355,34 +4377,45 @@ async function proceedIsChecked(assertType, option) { return truth(`checkable ${option}`, 'to be checked')[assertType](selected) } -async function findFields(locator) { +async function findFields(locator, context = null) { + let contextEl + if (context) { + const contextEls = await this._locate(context) + assertElementExists(contextEls, context, 'Context element') + contextEl = contextEls[0] + } + + const locateFn = contextEl + ? loc => findElements.call(this, contextEl, loc) + : loc => this._locate(loc) + // Handle role locators with text/exact options if (isRoleLocatorObject(locator)) { - const page = await this.page - const roleElements = await handleRoleLocator(page, locator) + const matcher = contextEl || (await this.page) + const roleElements = await handleRoleLocator(matcher, locator) if (roleElements) return roleElements } const matchedLocator = new Locator(locator) if (!matchedLocator.isFuzzy()) { - return this._locate(matchedLocator) + return locateFn(matchedLocator) } const literal = xpathLocator.literal(locator) - let els = await this._locate({ xpath: Locator.field.labelEquals(literal) }) + let els = await locateFn({ xpath: Locator.field.labelEquals(literal) }) if (els.length) { return els } - els = await this._locate({ xpath: Locator.field.labelContains(literal) }) + els = await locateFn({ xpath: Locator.field.labelContains(literal) }) if (els.length) { return els } - els = await this._locate({ xpath: Locator.field.byName(literal) }) + els = await locateFn({ xpath: Locator.field.byName(literal) }) if (els.length) { return els } - return this._locate({ css: locator }) + return locateFn({ css: locator }) } async function proceedSelect(context, el, option) { diff --git a/lib/helper/Puppeteer.js b/lib/helper/Puppeteer.js index 0bf8e465c..0acb033c9 100644 --- a/lib/helper/Puppeteer.js +++ b/lib/helper/Puppeteer.js @@ -1158,8 +1158,16 @@ class Puppeteer extends Helper { * {{> seeElement }} * {{ react }} */ - async seeElement(locator) { - let els = await this._locate(locator) + async seeElement(locator, context = null) { + let els + if (context) { + const contextPage = await this.context + const contextEls = await findElements.call(this, contextPage, context) + assertElementExists(contextEls, context, 'Context element') + els = await findElements.call(this, contextEls[0], locator) + } else { + els = await this._locate(locator) + } els = (await Promise.all(els.map(el => el.boundingBox() && el))).filter(v => v) // Puppeteer visibility was ignored? | Remove when Puppeteer is fixed els = await Promise.all(els.map(async el => (await el.evaluate(node => window.getComputedStyle(node).visibility !== 'hidden' && window.getComputedStyle(node).display !== 'none')) && el)) @@ -1174,8 +1182,16 @@ class Puppeteer extends Helper { * {{> dontSeeElement }} * {{ react }} */ - async dontSeeElement(locator) { - let els = await this._locate(locator) + async dontSeeElement(locator, context = null) { + let els + if (context) { + const contextPage = await this.context + const contextEls = await findElements.call(this, contextPage, context) + assertElementExists(contextEls, context, 'Context element') + els = await findElements.call(this, contextEls[0], locator) + } else { + els = await this._locate(locator) + } els = (await Promise.all(els.map(el => el.boundingBox() && el))).filter(v => v) // Puppeteer visibility was ignored? | Remove when Puppeteer is fixed els = await Promise.all(els.map(async el => (await el.evaluate(node => window.getComputedStyle(node).visibility !== 'hidden' && window.getComputedStyle(node).display !== 'none')) && el)) @@ -1541,8 +1557,8 @@ class Puppeteer extends Helper { * {{> fillField }} * {{ react }} */ - async fillField(field, value) { - const els = await findVisibleFields.call(this, field) + async fillField(field, value, context = null) { + const els = await findVisibleFields.call(this, field, context) assertElementExists(els, field, 'Field') const el = els[0] const tag = await el.getProperty('tagName').then(el => el.jsonValue()) @@ -1616,31 +1632,39 @@ class Puppeteer extends Helper { /** * {{> selectOption }} */ - async selectOption(select, option) { - const context = await this._getContext() + async selectOption(select, option, context = null) { + const pageContext = await this._getContext() const matchedLocator = new Locator(select) + let contextEl + if (context) { + const contextEls = await findElements.call(this, pageContext, context) + assertElementExists(contextEls, context, 'Context element') + contextEl = contextEls[0] + } + // Strict locator if (!matchedLocator.isFuzzy()) { this.debugSection('SelectOption', `Strict: ${JSON.stringify(select)}`) - const els = await this._locate(select) + const els = contextEl ? await findElements.call(this, contextEl, select) : await this._locate(select) assertElementExists(els, select, 'Selectable element') - return proceedSelect.call(this, context, els[0], option) + return proceedSelect.call(this, pageContext, els[0], option) } // Fuzzy: try combobox this.debugSection('SelectOption', `Fuzzy: "${matchedLocator.value}"`) - let els = await findByRole(context, { role: 'combobox', name: matchedLocator.value }) - if (els?.length) return proceedSelect.call(this, context, els[0], option) + const comboboxSearchCtx = contextEl || pageContext + let els = await findByRole(comboboxSearchCtx, { role: 'combobox', name: matchedLocator.value }) + if (els?.length) return proceedSelect.call(this, pageContext, els[0], option) // Fuzzy: try listbox - els = await findByRole(context, { role: 'listbox', name: matchedLocator.value }) - if (els?.length) return proceedSelect.call(this, context, els[0], option) + els = await findByRole(comboboxSearchCtx, { role: 'listbox', name: matchedLocator.value }) + if (els?.length) return proceedSelect.call(this, pageContext, els[0], option) // Fuzzy: try native select - const visibleEls = await findVisibleFields.call(this, select) + const visibleEls = await findVisibleFields.call(this, select, context) assertElementExists(visibleEls, select, 'Selectable field') - return proceedSelect.call(this, context, visibleEls[0], option) + return proceedSelect.call(this, pageContext, visibleEls[0], option) } /** @@ -3160,43 +3184,57 @@ async function proceedIsChecked(assertType, option) { return truth(`checkable ${option}`, 'to be checked')[assertType](selected) } -async function findVisibleFields(locator) { - const els = await findFields.call(this, locator) +async function findVisibleFields(locator, context = null) { + const els = await findFields.call(this, locator, context) const visible = await Promise.all(els.map(el => el.boundingBox())) return els.filter((el, index) => visible[index]) } -async function findFields(locator) { +async function findFields(locator, context = null) { + let contextEl + if (context) { + const contextPage = await this.context + const contextEls = await findElements.call(this, contextPage, context) + assertElementExists(contextEls, context, 'Context element') + contextEl = contextEls[0] + } + + const locateFn = contextEl + ? loc => findElements.call(this, contextEl, loc) + : loc => this._locate(loc) + const matchedLocator = new Locator(locator) if (!matchedLocator.isFuzzy()) { - return this._locate(matchedLocator) + return locateFn(matchedLocator) } const literal = xpathLocator.literal(matchedLocator.value) - let els = await this._locate({ xpath: Locator.field.labelEquals(literal) }) + let els = await locateFn({ xpath: Locator.field.labelEquals(literal) }) if (els.length) { return els } - els = await this._locate({ xpath: Locator.field.labelContains(literal) }) + els = await locateFn({ xpath: Locator.field.labelContains(literal) }) if (els.length) { return els } - els = await this._locate({ xpath: Locator.field.byName(literal) }) + els = await locateFn({ xpath: Locator.field.byName(literal) }) if (els.length) { return els } // Try ARIA selector for accessible name - try { - const page = await this.context - els = await page.$$(`::-p-aria(${matchedLocator.value})`) - if (els.length) return els - } catch (err) { - // ARIA selector not supported or failed + if (!contextEl) { + try { + const page = await this.context + els = await page.$$(`::-p-aria(${matchedLocator.value})`) + if (els.length) return els + } catch (err) { + // ARIA selector not supported or failed + } } - return this._locate({ css: matchedLocator.value }) + return locateFn({ css: matchedLocator.value }) } async function proceedDragAndDrop(sourceLocator, destinationLocator) { diff --git a/lib/helper/WebDriver.js b/lib/helper/WebDriver.js index e07234a53..cd673da08 100644 --- a/lib/helper/WebDriver.js +++ b/lib/helper/WebDriver.js @@ -1255,8 +1255,8 @@ class WebDriver extends Helper { * {{ custom }} * */ - async fillField(field, value) { - const res = await findFields.call(this, field) + async fillField(field, value, context = null) { + const res = await findFields.call(this, field, context) assertElementExists(res, field, 'Field') const elem = usingFirstElement(res) highlightActiveElement.call(this, elem) @@ -1301,13 +1301,14 @@ class WebDriver extends Helper { /** * {{> selectOption }} */ - async selectOption(select, option) { + async selectOption(select, option, context = null) { + const locateFn = prepareLocateFn.call(this, context) const matchedLocator = new Locator(select) // Strict locator if (!matchedLocator.isFuzzy()) { this.debugSection('SelectOption', `Strict: ${JSON.stringify(select)}`) - const els = await this._locate(select) + const els = await locateFn(select) assertElementExists(els, select, 'Selectable element') return proceedSelectOption.call(this, usingFirstElement(els), option) } @@ -1322,7 +1323,7 @@ class WebDriver extends Helper { if (els?.length) return proceedSelectOption.call(this, usingFirstElement(els), option) // Fuzzy: try native select - const res = await findFields.call(this, select) + const res = await findFields.call(this, select, context) assertElementExists(res, select, 'Selectable field') return proceedSelectOption.call(this, usingFirstElement(res), option) } @@ -1621,8 +1622,9 @@ class WebDriver extends Helper { * {{ react }} * */ - async seeElement(locator) { - const res = await this._locate(locator, true) + async seeElement(locator, context = null) { + const locateFn = prepareLocateFn.call(this, context) + const res = context ? await locateFn(locator) : await this._locate(locator, true) assertElementExists(res, locator) const selected = await forEachAsync(res, async el => el.isDisplayed()) try { @@ -1636,8 +1638,9 @@ class WebDriver extends Helper { * {{> dontSeeElement }} * {{ react }} */ - async dontSeeElement(locator) { - const res = await this._locate(locator, false) + async dontSeeElement(locator, context = null) { + const locateFn = prepareLocateFn.call(this, context) + const res = context ? await locateFn(locator) : await this._locate(locator, false) if (!res || res.length === 0) { return truth(`elements of ${new Locator(locator)}`, 'to be seen').negate(false) } @@ -3010,28 +3013,29 @@ async function findClickable(locator, locateFn) { return await locateFn(locator.value) // by css or xpath } -async function findFields(locator) { +async function findFields(locator, context = null) { + const locateFn = prepareLocateFn.call(this, context) locator = new Locator(locator) if (this._isCustomLocator(locator)) { - return this._locate(locator) + return locateFn(locator) } - if (locator.isAccessibilityId() && !this.isWeb) return this._locate(locator, true) - if (locator.isRole()) return this._locate(locator, true) - if (!locator.isFuzzy()) return this._locate(locator, true) + if (locator.isAccessibilityId() && !this.isWeb) return locateFn(locator) + if (locator.isRole()) return locateFn(locator) + if (!locator.isFuzzy()) return locateFn(locator) const literal = xpathLocator.literal(locator.value) - let els = await this._locate(Locator.field.labelEquals(literal)) + let els = await locateFn(Locator.field.labelEquals(literal)) if (els.length) return els - els = await this._locate(Locator.field.labelContains(literal)) + els = await locateFn(Locator.field.labelContains(literal)) if (els.length) return els - els = await this._locate(Locator.field.byName(literal)) + els = await locateFn(Locator.field.byName(literal)) if (els.length) return els - return await this._locate(locator.value) // by css or xpath + return await locateFn(locator.value) // by css or xpath } async function proceedSeeField(assertType, field, value) { diff --git a/test/data/app/view/form/context.php b/test/data/app/view/form/context.php new file mode 100644 index 000000000..9abcf8129 --- /dev/null +++ b/test/data/app/view/form/context.php @@ -0,0 +1,24 @@ + +
+