Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion docs/webapi/click.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,13 @@ If a fuzzy locator is given, the page will be searched for a button, link, or im
For buttons, the "value" attribute, "name" attribute, and inner text are searched. For links, the link text is searched.
For images, the "alt" attribute and inner text of any parent links are searched.

If no locator is provided, defaults to clicking the body element (`'//body'`).

The second parameter is a context (CSS or XPath locator) to narrow the search.

```js
// click body element (default)
I.click();
// simple link
I.click('Logout');
// button of form
Expand All @@ -20,6 +24,6 @@ I.click('Logout', '#nav');
I.click({css: 'nav a.login'});
```

@param {CodeceptJS.LocatorOrString} locator clickable link or button located by text, or any element located by CSS|XPath|strict locator.
@param {CodeceptJS.LocatorOrString} [locator='//body'] (optional, `'//body'` by default) clickable link or button located by text, or any element located by CSS|XPath|strict locator.
@param {?CodeceptJS.LocatorOrString | null} [context=null] (optional, `null` by default) element to search in CSS|XPath|Strict locator.
@returns {void} automatically synchronized promise through #recorder
5 changes: 2 additions & 3 deletions lib/command/init.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ const packages = []
let isTypeScript = false
let extension = 'js'

const requireCodeceptConfigure = "const { setHeadlessWhen, setCommonPlugins } = require('@codeceptjs/configure');"
const importCodeceptConfigure = "import { setHeadlessWhen, setCommonPlugins } from '@codeceptjs/configure';"

const configHeader = `
Expand Down Expand Up @@ -232,9 +231,9 @@ export default async function (initPath) {
fs.writeFileSync(typeScriptconfigFile, configSource, 'utf-8')
print(`Config created at ${typeScriptconfigFile}`)
} else {
configSource = beautify(`/** @type {CodeceptJS.MainConfig} */\nexports.config = ${inspect(config, false, 4, false)}`)
configSource = beautify(`/** @type {CodeceptJS.MainConfig} */\nexport const config = ${inspect(config, false, 4, false)}`)

if (hasConfigure) configSource = requireCodeceptConfigure + configHeader + configSource
if (hasConfigure) configSource = importCodeceptConfigure + configHeader + configSource

fs.writeFileSync(configFile, configSource, 'utf-8')
print(`Config created at ${configFile}`)
Expand Down
122 changes: 65 additions & 57 deletions lib/helper/Playwright.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import ElementNotFound from './errors/ElementNotFound.js'
import RemoteBrowserConnectionRefused from './errors/RemoteBrowserConnectionRefused.js'
import Popup from './extras/Popup.js'
import Console from './extras/Console.js'
import { findReact, findVue, findByPlaywrightLocator } from './extras/PlaywrightReactVueLocator.js'
import { buildLocatorString, findElements, findElement, getVisibleElements, findReact, findVue, findByPlaywrightLocator, findByRole } from './extras/PlaywrightLocator.js'

let playwright
let perfTiming
Expand Down Expand Up @@ -1911,7 +1911,7 @@ class Playwright extends Helper {
* ```
*
*/
async click(locator, context = null, options = {}) {
async click(locator = '//body', context = null, options = {}) {
return proceedClick.call(this, locator, context, options)
}

Expand Down Expand Up @@ -2424,17 +2424,28 @@ class Playwright extends Helper {
*
*/
async grabTextFrom(locator) {
locator = this._contextLocator(locator)
const originalLocator = locator
const matchedLocator = new Locator(locator)

if (!matchedLocator.isFuzzy()) {
const els = await this._locate(matchedLocator)
assertElementExists(els, locator)
const text = await els[0].innerText()
this.debugSection('Text', text)
return text
}

const contextAwareLocator = this._contextLocator(matchedLocator.value)
let text
try {
text = await this.page.textContent(locator)
text = await this.page.textContent(contextAwareLocator)
} catch (err) {
if (err.message.includes('Timeout') || err.message.includes('exceeded')) {
throw new Error(`Element ${new Locator(locator).toString()} was not found by text|CSS|XPath`)
throw new Error(`Element ${new Locator(originalLocator).toString()} was not found by text|CSS|XPath`)
}
throw err
}
assertElementExists(text, locator)
assertElementExists(text, contextAwareLocator)
this.debugSection('Text', text)
return text
}
Expand Down Expand Up @@ -2629,6 +2640,33 @@ class Playwright extends Helper {
return array
}

/**
* Retrieves the ARIA snapshot for an element using Playwright's [`locator.ariaSnapshot`](https://playwright.dev/docs/api/class-locator#locator-aria-snapshot).
* This method returns a YAML representation of the accessibility tree that can be used for assertions.
* If no locator is provided, it captures the snapshot of the entire page body.
*
* ```js
* const snapshot = await I.grabAriaSnapshot();
* expect(snapshot).toContain('heading "Sign up"');
*
* const formSnapshot = await I.grabAriaSnapshot('#login-form');
* expect(formSnapshot).toContain('textbox "Email"');
* ```
*
* [Learn more about ARIA snapshots](https://playwright.dev/docs/aria-snapshots)
*
* @param {string|object} [locator='//body'] element located by CSS|XPath|strict locator. Defaults to body element.
* @return {Promise<string>} YAML representation of the accessibility tree
*/
async grabAriaSnapshot(locator = '//body') {
const matchedLocator = new Locator(locator)
const els = await this._locate(matchedLocator)
assertElementExists(els, locator)
const snapshot = await els[0].ariaSnapshot()
this.debugSection('Aria Snapshot', snapshot)
return snapshot
}

/**
* {{> saveElementScreenshot }}
*
Expand Down Expand Up @@ -3821,47 +3859,6 @@ class Playwright extends Helper {

export default Playwright

function buildLocatorString(locator) {
if (locator.isCustom()) {
return `${locator.type}=${locator.value}`
}
if (locator.isXPath()) {
return `xpath=${locator.value}`
}
return locator.simplify()
}

async function findElements(matcher, locator) {
if (locator.react) return findReact(matcher, locator)
if (locator.vue) return findVue(matcher, locator)
if (locator.pw) return findByPlaywrightLocator.call(this, matcher, locator)
locator = new Locator(locator, 'css')

return matcher.locator(buildLocatorString(locator)).all()
}

async function findElement(matcher, locator) {
if (locator.react) return findReact(matcher, locator)
if (locator.vue) return findVue(matcher, locator)
if (locator.pw) return findByPlaywrightLocator.call(this, matcher, locator)
locator = new Locator(locator, 'css')

return matcher.locator(buildLocatorString(locator)).first()
}

async function getVisibleElements(elements) {
const visibleElements = []
for (const element of elements) {
if (await element.isVisible()) {
visibleElements.push(element)
}
}
if (visibleElements.length === 0) {
return elements
}
return visibleElements
}

async function proceedClick(locator, context = null, options = {}) {
let matcher = await this._getContext()
if (context) {
Expand Down Expand Up @@ -3898,15 +3895,26 @@ async function proceedClick(locator, context = null, options = {}) {
}

async function findClickable(matcher, locator) {
if (locator.react) return findReact(matcher, locator)
if (locator.vue) return findVue(matcher, locator)
if (locator.pw) return findByPlaywrightLocator.call(this, matcher, locator)
const matchedLocator = new Locator(locator)

locator = new Locator(locator)
if (!locator.isFuzzy()) return findElements.call(this, matcher, locator)
if (!matchedLocator.isFuzzy()) return findElements.call(this, matcher, matchedLocator)

let els
const literal = xpathLocator.literal(locator.value)
const literal = xpathLocator.literal(matchedLocator.value)

try {
els = await matcher.getByRole('button', { name: matchedLocator.value }).all()
if (els.length) return els
} catch (err) {
// getByRole not supported or failed
}

try {
els = await matcher.getByRole('link', { name: matchedLocator.value }).all()
if (els.length) return els
} catch (err) {
// getByRole not supported or failed
}

els = await findElements.call(this, matcher, Locator.clickable.narrow(literal))
if (els.length) return els
Expand All @@ -3921,7 +3929,7 @@ async function findClickable(matcher, locator) {
// Do nothing
}

return findElements.call(this, matcher, locator.value) // by css or xpath
return findElements.call(this, matcher, matchedLocator.value) // by css or xpath
}

async function proceedSee(assertType, text, context, strict = false) {
Expand Down Expand Up @@ -3962,10 +3970,10 @@ async function findCheckable(locator, context) {

const matchedLocator = new Locator(locator)
if (!matchedLocator.isFuzzy()) {
return findElements.call(this, contextEl, matchedLocator.simplify())
return findElements.call(this, contextEl, matchedLocator)
}

const literal = xpathLocator.literal(locator)
const literal = xpathLocator.literal(matchedLocator.value)
let els = await findElements.call(this, contextEl, Locator.checkable.byText(literal))
if (els.length) {
return els
Expand All @@ -3974,7 +3982,7 @@ async function findCheckable(locator, context) {
if (els.length) {
return els
}
return findElements.call(this, contextEl, locator)
return findElements.call(this, contextEl, matchedLocator.value)
}

async function proceedIsChecked(assertType, option) {
Expand Down
Loading