diff --git a/docs/books.md b/docs/books.md deleted file mode 100644 index 723c4ac89..000000000 --- a/docs/books.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -permalink: /books -layout: Section -sidebar: false -title: Books & Posts -editLink: false ---- - -# Books & Posts -> Add your own books or posts to our [Wiki Page](https://github.com/codeceptjs/CodeceptJS/wiki/Books-&-Posts) -### [Practical End 2 End Testing with CodeceptJS](https://leanpub.com/codeceptjs/) - -A book by **Paul Vincent Beigang** - -[![](https://user-images.githubusercontent.com/220264/58870454-e2e8ce80-86c8-11e9-868e-7deefdde47ce.png)](https://leanpub.com/codeceptjs/) - -#### Contents: - -1. Preparation for End 2 End Testing with CodeceptJS -1. Setup CodeceptJS with WebdriverIO -1. Create Your First CodeceptJS Test -1. Run Your First CodeceptJS Test Locally -1. Run Test on BrowserStack Against with the Safari Browser -1. How to Debug & Fix a Failing E2E Test -1. Run a CodeceptJS Test in GitLab´s Continuous Integration (CI) Environment -1. Delicious Test Reports With Allure - -### Posts - -A list of good educational posts about CodeceptJS - -* [QA Automation From Zero-to-Hero with CodeceptJS End-to-End Testing](https://medium.com/@dan.ryan.emmons/qa-automation-from-zero-to-hero-with-codeceptjs-end-to-end-testing-719db9d6ff5c) by Dan Emmons -* [Effective End2End Tests with CodeceptJS](https://hackernoon.com/effective-end-2-end-testing-in-javascript-with-codeceptjs-37c8d7d6a928) by @davertmik -* [Customizing CodeceptJS Skeleton](https://medium.com/@successivetech/codeceptjs-skeleton-9ba86d3b45ec) -* [Running End to End tests as Google Cloud Functions](https://hackernoon.com/running-end-to-end-tests-as-google-cloud-functions-f5e34ffc3984) -* [End-To-End Testing With CodeceptJS](https://www.monterail.com/blog/end-to-end-testing-with-codeceptjs) by Piotr Michalski -* [Getting started with CodeceptJS and Selenium WebDriver](https://medium.com/@garrettvorce/getting-started-with-selenium-and-codeceptjs-c0698e8df677) \ No newline at end of file diff --git a/docs/bootstrap.md b/docs/bootstrap.md index 12954a453..6d13a337c 100644 --- a/docs/bootstrap.md +++ b/docs/bootstrap.md @@ -11,16 +11,14 @@ you can use the `bootstrap` and `teardown` config. Use it to start and stop a we When using the [parallel execution](/parallel) mode, there are two additional hooks available; `bootstrapAll` and `teardownAll`. See [bootstrapAll & teardownAll](#bootstrapall-teardownall) for more information. -> ⚠ In CodeceptJS 2 bootstrap could be set as a function with `done` parameter. This way of handling async function was replaced with native async functions in CodeceptJS 3. - ### Example: Bootstrap & Teardown If you are using JavaScript-style config `codecept.conf.js`, bootstrap and teardown functions can be placed inside of it: ```js -var server = require('./app_server'); +import server from './app_server.js'; -exports.config = { +export default { tests: "./*_test.js", helpers: {}, @@ -63,10 +61,10 @@ Using JavaScript-style config `codecept.conf.js`, bootstrapAll and teardownAll f ```js -const fs = require('fs'); +import fs from 'fs'; const tempFolder = process.cwd() + '/tmpFolder'; -exports.config = { +export default { tests: "./*_test.js", helpers: {}, @@ -95,13 +93,12 @@ exports.config = { ## Combining Bootstrap & BootstrapAll -It is quite common that you expect that bootstrapAll and bootstrap will do the same thing. If an application server is already started in `bootstrapAll` we should not run it again inside `bootstrap` for each worker. To avoid code duplication we can run bootstrap script only when we are not inside a worker. And we will use NodeJS `isMainThread` Workers API to detect that: +It is quite common that you expect that bootstrapAll and bootstrap will do the same thing. If an application server is already started in `bootstrapAll` we should not run it again inside `bootstrap` for each worker. To avoid code duplication we can run bootstrap script only when we are not inside a worker. CodeceptJS provides `store.workerMode` to detect if code is running in a worker process: ```js // inside codecept.conf.js -// detect if we are in a worker thread -const { isMainThread } = require('worker_threads'); +import store from 'codeceptjs/lib/store.js'; async function startServer() { // implement starting server logic here @@ -111,7 +108,7 @@ async function stopServer() { } -exports.config = { +export default { // codeceptjs config goes here async bootstrapAll() { @@ -119,12 +116,12 @@ exports.config = { }, async bootstrap() { // start a server only if we are not in worker - if (isMainThread) return startServer(); + if (!store.workerMode) return startServer(); } async teardown() { - // start a server only if we are not in worker - if (isMainThread) return stopServer(); + // stop a server only if we are not in worker + if (!store.workerMode) return stopServer(); } async teardownAll() { diff --git a/docs/data.md b/docs/data.md index f5c1fe149..cdb85548a 100644 --- a/docs/data.md +++ b/docs/data.md @@ -22,7 +22,90 @@ API is supposed to be a stable interface and it can be used by acceptance tests. ## Data Objects -For a lightweight, class-based approach to managing test data, see **[Data Objects](/pageobjects#data-objects)** in the Page Objects documentation. Data Objects let you create page object classes that manage API data with automatic cleanup via the `_after()` hook — no factory configuration needed. +Data Objects are page object classes designed to manage test data via API. They use the REST helper (through `I`) to create data in a test and clean it up automatically via the `_after()` hook. + +This is a lightweight alternative to [ApiDataFactory](#api-data-factory) — ideal when you want full control over data creation and cleanup logic without factory configuration. + +### Defining a Data Object + +```js +const { I } = inject(); + +class UserData { + constructor() { + this._created = []; + } + + async createUser(data = {}) { + const response = await I.sendPostRequest('/api/users', { + name: data.name || 'Test User', + email: data.email || `test-${Date.now()}@example.com`, + ...data, + }); + this._created.push(response.data.id); + return response.data; + } + + async createPost(userId, data = {}) { + const response = await I.sendPostRequest('/api/posts', { + userId, + title: data.title || 'Test Post', + body: data.body || 'Test body', + ...data, + }); + this._created.push({ type: 'post', id: response.data.id }); + return response.data; + } + + async _after() { + for (const record of this._created.reverse()) { + const id = typeof record === 'object' ? record.id : record; + const type = typeof record === 'object' ? record.type : 'user'; + try { + await I.sendDeleteRequest(`/api/${type}s/${id}`); + } catch (e) { + // cleanup errors should not fail the test + } + } + this._created = []; + } +} + +export default UserData +``` + +### Configuration + +Add the REST helper and the Data Object to your config: + +```js +helpers: { + Playwright: { url: 'http://localhost', browser: 'chromium' }, + REST: { + endpoint: 'http://localhost/api', + defaultHeaders: { 'Content-Type': 'application/json' }, + }, +}, +include: { + I: './steps_file.js', + userData: './data/UserData.js', +} +``` + +### Usage in Tests + +```js +Scenario('user sees their profile', async ({ I, userData }) => { + const user = await userData.createUser({ name: 'John Doe' }); + I.amOnPage(`/users/${user.id}`); + I.see('John Doe'); + // userData._after() runs automatically — deletes the created user +}); +``` + +Data Objects can use any helper methods available via `I`, including `sendGetRequest`, `sendPutRequest`, and browser actions. They combine the convenience of managed test data with the flexibility of page objects. + +**Learn more:** See [Page Objects](/pageobjects) for general page object patterns. ## REST diff --git a/docs/element-based-testing.md b/docs/element-based-testing.md index c24cfd6e5..0b64f354a 100644 --- a/docs/element-based-testing.md +++ b/docs/element-based-testing.md @@ -272,7 +272,7 @@ Scenario('filter products by price', async ({ I }) => { ## API Reference - **[Element Access](els.md)** - Complete reference for `element()`, `eachElement()`, `expectElement()`, `expectAnyElement()`, `expectAllElements()` functions -- **[WebElement API](WebElement.md)** - Complete reference for WebElement class methods (`getText()`, `getAttribute()`, `click()`, `$$()`, etc.) +- **[WebElement API](web-element.md)** - Complete reference for WebElement class methods (`getText()`, `getAttribute()`, `click()`, `$$()`, etc.) ## Portability diff --git a/docs/esm-migration.md b/docs/esm-migration.md deleted file mode 100644 index 71cee12ac..000000000 --- a/docs/esm-migration.md +++ /dev/null @@ -1,238 +0,0 @@ -# ESM Migration Guide - -This guide covers the migration to ECMAScript Modules (ESM) format in CodeceptJS v4.x, including important changes in execution behavior and how to adapt your tests. - -## Overview - -CodeceptJS v4.x introduces support for ECMAScript Modules (ESM), which brings modern JavaScript module syntax and better compatibility with the Node.js ecosystem. While most tests will continue working without changes, there are some behavioral differences to be aware of. - -## Quick Migration - -For most users, migrating to ESM is straightforward: - -1. **Add `"type": "module"` to your `package.json`:** - -```json -{ - "name": "your-project", - "type": "module", - "dependencies": { - "codeceptjs": "^4.0.0" - } -} -``` - -2. **Update import syntax in configuration files:** - -```js -// Before (CommonJS) -const { setHeadlessWhen, setCommonPlugins } = require('@codeceptjs/configure') - -// After (ESM) -import { setHeadlessWhen, setCommonPlugins } from '@codeceptjs/configure' -``` - -3. **Update helper imports:** - -```js -// Before (CommonJS) -const Helper = require('@codeceptjs/helper') - -// After (ESM) -import Helper from '@codeceptjs/helper' -``` - -## Known Changes - -### Session and Within Block Execution Order - -**⚠️ Important:** ESM migration has changed the execution timing of `session()` and `within()` blocks. - -#### What Changed - -In CommonJS, session and within blocks executed synchronously, interleaved with main test steps: - -```js -// CommonJS execution order -Scenario('test', ({ I }) => { - I.do('step1') // ← Executes first - session('user', () => { - I.do('session-step') // ← Executes second - }) - I.do('step2') // ← Executes third -}) -``` - -In ESM, session and within blocks execute after the main flow completes: - -```js -// ESM execution order -Scenario('test', ({ I }) => { - I.do('step1') // ← Executes first - session('user', () => { - I.do('session-step') // ← Executes third (after step2) - }) - I.do('step2') // ← Executes second -}) -``` - -#### Impact on Your Tests - -**✅ No Impact (99% of cases):** Most tests will continue working correctly because: - -- All steps still execute completely -- Browser interactions work as expected -- Session isolation is maintained -- Test assertions pass/fail correctly -- Final test state is identical - -**⚠️ Potential Issues (rare edge cases):** - -1. **Cross-session dependencies on immediate state:** - -```js -// POTENTIALLY PROBLEMATIC -I.createUser('alice') -session('alice', () => { - I.login('alice') // May execute before user creation completes -}) -``` - -2. **Within blocks depending on immediate DOM changes:** - -```js -// POTENTIALLY PROBLEMATIC -I.click('Show Advanced Form') -within('.advanced-form', () => { - I.fillField('setting', 'value') // May execute before form appears -}) -``` - -#### Migration Solutions - -If you encounter timing-related issues in edge cases: - -1. **Use explicit waits for dependent operations:** - -```js -// RECOMMENDED FIX -I.createUser('alice') -session('alice', () => { - I.waitForElement('.login-form') // Ensure UI is ready - I.login('alice') -}) -``` - -2. **Add explicit synchronization:** - -```js -// RECOMMENDED FIX -I.click('Show Advanced Form') -I.waitForElement('.advanced-form') // Wait for form to appear -within('.advanced-form', () => { - I.fillField('setting', 'value') -}) -``` - -3. **Use async/await for complex flows:** - -```js -// RECOMMENDED FIX -await I.createUser('alice') -await session('alice', async () => { - await I.login('alice') -}) -``` - -## Best Practices for ESM - -### 1. File Extensions - -Use `.js` extension for ESM files (not `.mjs` unless specifically needed): - -```js -// codecept.conf.js (ESM format) -export default { - tests: './*_test.js', - // ... -} -``` - -### 2. Dynamic Imports - -For conditional imports, use dynamic import syntax: - -```js -// Instead of require() conditions -let helper -if (condition) { - helper = await import('./CustomHelper.js') -} -``` - -### 3. Configuration Export - -Use default export for configuration: - -```js -// codecept.conf.js -export default { - tests: './*_test.js', - output: './output', - helpers: { - Playwright: { - url: 'http://localhost', - }, - }, -} -``` - -### 4. Helper Classes - -Export helper classes as default: - -```js -// CustomHelper.js -import { Helper } from 'codeceptjs' - -class CustomHelper extends Helper { - // helper methods -} - -export default CustomHelper -``` - -## Troubleshooting - -### Common Issues - -1. **Module not found errors:** - - Ensure `"type": "module"` is in package.json - - Use complete file paths with extensions: `import './helper.js'` - -2. **Configuration not loading:** - - Check that config uses `export default {}` - - Verify all imports use ESM syntax - -3. **Timing issues in sessions/within:** - - Add explicit waits as shown in the migration solutions above - - Consider using async/await for complex flows - -4. **Plugin compatibility:** - - Ensure all plugins support ESM - - Update plugin imports to use ESM syntax - -### Getting Help - -If you encounter issues during ESM migration: - -1. Check the [example-esm](../example-esm/) directory for working examples -2. Review error messages for import/export syntax issues -3. Consider the execution order changes for session/within blocks -4. Join the [CodeceptJS community](https://codecept.io/community/) for support - -## Conclusion - -ESM migration brings CodeceptJS into alignment with modern JavaScript standards while maintaining backward compatibility for most use cases. The execution order changes in sessions and within blocks represent internal timing adjustments that rarely affect real-world test functionality. - -The vast majority of CodeceptJS users will experience seamless migration with no functional differences in their tests. diff --git a/docs/locators.md b/docs/locators.md index bc0ca99ee..6e13f0abb 100644 --- a/docs/locators.md +++ b/docs/locators.md @@ -5,391 +5,281 @@ title: Locators # Locators -CodeceptJS provides flexible strategies for locating elements: +Locators tell CodeceptJS which element on the page a step acts on. Every action that touches the DOM — `click`, `fillField`, `see`, `waitForVisible` — accepts one. -* [CSS and XPath locators](#css-and-xpath) -* [Semantic locators](#semantic-locators): by link text, by button text, by field names, etc. -* [ARIA and role locators](#aria-and-role-locators): by ARIA role and accessible name -* [Locator Builder](#locator-builder) -* [ID locators](#id-locators): by CSS id or by accessibility id -* [Custom Locator Strategies](#custom-locators): by data attributes or whatever you prefer. -* [Shadow DOM](/shadow): to access shadow dom elements -* [React](/react): to access React elements by component names and props -* Playwright: to access locator supported by Playwright, namely [_react](https://playwright.dev/docs/other-locators#react-locator), [_vue](https://playwright.dev/docs/other-locators#vue-locator), [data-testid](https://playwright.dev/docs/locators#locate-by-test-id) +CodeceptJS accepts locators in two forms: -Most methods in CodeceptJS accept locators as strings or objects. +- **Strict locator** — an object whose single key names the strategy: `{ css: 'button' }`, `{ role: 'button', name: 'Submit' }`, `{ xpath: '//td[1]' }`, `{ id: 'email' }`. The strategy is explicit, so the helper runs exactly one query. +- **Fuzzy locator** — a plain string. CodeceptJS guesses the strategy from shape (`#foo` → id, `//td` → xpath, `.row` → css) and falls back to semantic matching (labels, button text, placeholders). Convenient, but slower and sometimes ambiguous. -If the locator is an object, it should have a single element, with the key signifying the locator type (`id`, `name`, `css`, `xpath`, `link`, `react`, `class`, `shadow`, `pw`, or `role`) and the value being the locator itself. This is called a **strict** locator. +Prefer strict locators in stable test suites. Reach for fuzzy strings when prototyping. -Examples: +Supported strategies: `css`, `xpath`, `id`, `name`, `role`, `frame`, `shadow`, `pw`. Shadow DOM and React selectors have their own pages — see [Shadow DOM](/shadow) and [React](/react). Playwright-specific locators (`_react`, `_vue`, `data-testid`) use the `pw` strategy: `{ pw: '_react=Button[name="Save"]' }`. -* `{id: 'foo'}` matches `
` -* `{name: 'foo'}` matches `
` -* `{css: 'input[type=input][value=foo]'}` matches `` -* `{xpath: "//input[@type='submit'][contains(@value, 'foo')]"}` matches `` -* `{class: 'foo'}` matches `
` -* `{ pw: '_react=t[name = "="]' }` -* `{ role: 'button', name: 'Submit' }` matches `` +## Locator types at a glance -Writing good locators can be tricky. -The Mozilla team has written an excellent guide titled [Writing reliable locators for Selenium and WebDriver tests](https://blog.mozilla.org/webqa/2013/09/26/writing-reliable-locators-for-selenium-and-webdriver-tests/). +| Type | Example | Strengths | Weaknesses | Reach for it when | +|------|---------|-----------|------------|-------------------| +| **ARIA role** | `{ role: 'button', name: 'Save' }` | Survives markup changes; matches how users and screen readers identify elements; exposes accessibility gaps | Needs correct ARIA roles and accessible names; slower than CSS | The app follows accessibility guidelines and you want tests that mirror user intent | +| **CSS** | `{ css: '.btn-save' }` or `.btn-save` | Fast; familiar to every web developer; composes with class, attribute, and pseudo-selectors | Couples tests to styling; breaks on CSS refactors; cannot match by visible text | A stable class, id, or data-attribute exists on the target | +| **XPath** | `{ xpath: '//table//tr[2]/td[last()]' }` | Walks the tree in any direction (`ancestor`, `following-sibling`); matches visible text | Verbose; slow; harder to read than CSS | You need text matching or axis navigation that CSS cannot express | +| **Semantic (fuzzy)** | `'Sign In'`, `'Email'` | No locator to maintain; reads like prose | Several lookups per call; ambiguous when labels repeat | Writing a quick scenario or prototyping | +| **ID / name** | `#email`, `{ name: 'user[email]' }` | Shortest possible locator; unambiguous | Requires an `id` or `name` attribute to exist | Forms and elements with stable ids | +| **Accessibility id** | `~login-button` | Works in both web (`aria-label`) and mobile | Mobile apps need to expose the id | Cross-platform web and mobile tests | +| **Custom (`$foo`)** | `$register_button` | Encodes team convention (`data-qa`, `data-test`) in two characters | Needs the [customLocator plugin](/plugins#customlocator) | Your team uses dedicated test attributes | -If you prefer, you may also pass a string for the locator. This is called a **fuzzy** locator. -In this case, CodeceptJS uses a variety of heuristics (depending on the exact method called) to determine what element you're referring to. If you are locating a clickable element or an input element, CodeceptJS will use [semantic locators](#semantic-locators). +## ARIA locators -For example, here's the heuristic used for the `fillField` method: - -1. Does the locator look like an ID selector (e.g. `#foo`)? If so, try to find an input element matching that ID. -2. If nothing found, check if locator looks like a CSS selector. If so, run it. -3. If nothing found, check if locator looks like an XPath expression. If so, run it. -4. If nothing found, check if there is an input element with a corresponding name. -5. If nothing found, check if there is a label with specified text for input element. -6. If nothing found, throw an `ElementNotFound` exception. - -> ⚠ Fuzzy locators can be significantly slower than strict locators. If speed is a concern, stick with explicitly specifying the locator type via object syntax. - -It is recommended to avoid using implicit CSS locators in methods like `fillField` or `click`, where semantic locators are allowed. -Use locator type to speed up search by various locator strategies. +ARIA role locators are the modern default. They identify elements the way assistive technology does — by role and accessible name — and they survive layout and class refactors that break CSS. ```js -// will search for "input[type=password]" text before trying to search by CSS -I.fillField('input[type=password]', '123456'); -// replace with strict locator -I.fillField({ css: 'input[type=password]' }, '123456'); +I.click({ role: 'button', name: 'Login' }) +I.fillField({ role: 'textbox', name: 'Email Address' }, 'user@test.com') +I.seeElement({ role: 'heading', name: 'Dashboard' }) +I.selectOption({ role: 'combobox', name: 'Country' }, 'Ukraine') ``` -## CSS and XPath +The `name` matches the element's accessible name — its visible text, `aria-label`, or the text referenced by `aria-labelledby`. -Both CSS and XPath are supported. Usually CodeceptJS can guess the locator's type: +Common roles: `button`, `link`, `textbox`, `checkbox`, `radio`, `combobox`, `listbox`, `menuitem`, `tab`, `dialog`, `alert`, `heading`, `navigation`, `banner`, `main`. -```js -// select by CSS -I.seeElement('.user .profile'); -I.seeElement('#user-name'); +**Prefer ARIA when:** -// select by XPath -I.seeElement('//table/tr/td[position()=3]'); -``` +- The element has a visible label or accessible text. +- You want the test to double as an accessibility smoke check. +- The UI is rewritten often and class names drift. -To specify exact locator type use **strict locators**: +**Reach for something else when:** -```js -// it's not clear that 'button' is actual CSS locator -I.seeElement({ css: 'button' }); - -// it's not clear that 'descendant::table/tr' is actual XPath locator -I.seeElement({ xpath: 'descendant::table/tr' }); -``` +- The element has no accessible name (purely decorative icons, unlabeled inputs). +- The page predates ARIA annotation and you cannot change it. +- A hot loop runs thousands of locator calls and needs the speed of a direct CSS query. -> ℹ Use [Locator Advicer](https://davertmik.github.io/locator/) to check quality of your locators. +> ARIA locators rely on the accessibility tree of the underlying helper. Playwright and modern WebDriver support them natively. -## Semantic Locators +## CSS selectors -CodeceptJS can guess an element's locator from context. -For example, when clicking CodeceptJS will try to find a link or button by their text. -When typing into a field, the field can be located by its name or placeholder. +CSS is the fastest locator type and most frontend developers read it fluently. ```js -I.click('Sign In'); -I.fillField('Username', 'davert'); +I.seeElement('.user-profile .avatar') +I.click('#checkout-btn') +I.fillField('input[name="email"]', 'user@test.com') ``` -Various strategies are used to locate semantic elements. However, they may run slower than specifying the locator by XPath or CSS. - -## ARIA and Role Locators - -ARIA locators find elements by their ARIA role and accessible name. They are the most resilient to markup changes and the most meaningful for accessibility. - -Pass an object with a `role` key and an optional `name` key: +Pair CSS with stable test attributes — `data-testid`, `data-qa` — rather than style classes. Style classes drift with every design update; test attributes exist to be locators. ```js -I.click({ role: 'button', name: 'Login' }); -I.fillField({ role: 'textbox', name: 'Email Address' }, 'user@test.com'); -I.seeElement({ role: 'button', name: 'Submit' }); +I.click('[data-testid="submit-order"]') ``` -The `name` matches the element's accessible name — its visible text, `aria-label`, or `aria-labelledby` target. - -Common ARIA roles include `button`, `link`, `textbox`, `checkbox`, `radio`, `combobox`, `listbox`, `menuitem`, `tab`, `dialog`, `alert`, `heading`, and `navigation`. - -> ℹ ARIA locators are supported by Playwright and other helpers that implement accessibility tree queries. - -## Locator Builder +Tie locators to structure, not to presentation: `.btn-primary` survives a redesign; `.bg-green-500` does not. -CodeceptJS provides a fluent builder to compose custom locators in JavaScript. Use `locate` function to start. - -To locate `a` element inside `label` with text: 'Hello' use: +Force CSS when a bare string would trigger fuzzy matching: ```js -locate('a') - .withAttr({ href: '#' }) - .inside(locate('label').withText('Hello')); +I.fillField({ css: 'input[type=password]' }, '123456') ``` -which will produce following XPath: +## XPath -``` -.//a[@href = '#'][ancestor::label[contains(., 'Hello')]] -``` +XPath reaches where CSS cannot. Use it for: -Locator builder accepts both XPath and CSS as parameters but converts them to XPath as more feature-rich format. -Sometimes provided locators can get very long so it's recommended to simplify the output by providing a brief description for generated XPath: +- Text matching: `//button[contains(., 'Save changes')]` +- Axis navigation: `ancestor`, `following-sibling`, `preceding-sibling` +- Positional selection deep in a table or list ```js -locate('//table') - .find('a') - .withText('Edit') - .as('edit button') -// will be printed as 'edit button' +I.click({ xpath: "//tr[td[text()='Acme Corp']]//button[contains(., 'Edit')]" }) ``` -`locate` has following methods: +Long XPath expressions become unreadable fast. The [`locate()` builder](#combining-locators) produces the same XPath with a fluent syntax — prefer it for anything beyond two conditions. -#### find +## Semantic locators -Finds an element inside a located. +When you pass a plain string to a form or click action, CodeceptJS tries several strategies in order: links, buttons, labels, placeholders, `aria-label`. ```js -// find td inside a table -locate('table').find('td'); +I.click('Sign In') // matches ,