codeceptjs 4.0.0-rc.18 → 4.0.0-rc.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/codecept.js +5 -1
- package/bin/codeceptq.js +49 -0
- package/bin/mcp-server.js +250 -82
- package/docs/advanced.md +201 -0
- package/docs/agents.md +159 -0
- package/docs/ai.md +537 -0
- package/docs/aitrace.md +266 -0
- package/docs/api.md +332 -0
- package/docs/assertions.md +415 -0
- package/docs/auth.md +318 -0
- package/docs/basics.md +424 -0
- package/docs/bdd.md +539 -0
- package/docs/best.md +240 -0
- package/docs/bootstrap.md +132 -0
- package/docs/commands.md +352 -0
- package/docs/community-helpers.md +63 -0
- package/docs/configuration.md +230 -0
- package/docs/continuous-integration.md +497 -0
- package/docs/custom-helpers.md +297 -0
- package/docs/data.md +448 -0
- package/docs/debugging.md +332 -0
- package/docs/detox.md +235 -0
- package/docs/docker.md +136 -0
- package/docs/effects.md +179 -0
- package/docs/element-based-testing.md +295 -0
- package/docs/element-selection.md +125 -0
- package/docs/els.md +328 -0
- package/docs/examples.md +161 -0
- package/docs/heal.md +213 -0
- package/docs/helpers/ApiDataFactory.md +267 -0
- package/docs/helpers/Appium.md +1405 -0
- package/docs/helpers/Detox.md +665 -0
- package/docs/helpers/ExpectHelper.md +275 -0
- package/docs/helpers/FileSystem.md +152 -0
- package/docs/helpers/GraphQL.md +152 -0
- package/docs/helpers/GraphQLDataFactory.md +226 -0
- package/docs/helpers/JSONResponse.md +255 -0
- package/docs/helpers/Mochawesome.md +8 -0
- package/docs/helpers/MockRequest.md +377 -0
- package/docs/helpers/MockServer.md +212 -0
- package/docs/helpers/Playwright.md +2969 -0
- package/docs/helpers/Polly.md +44 -0
- package/docs/helpers/Protractor.md +1769 -0
- package/docs/helpers/Puppeteer-firefox.md +86 -0
- package/docs/helpers/Puppeteer.md +2690 -0
- package/docs/helpers/REST.md +289 -0
- package/docs/helpers/SoftExpectHelper.md +352 -0
- package/docs/helpers/WebDriver.md +2682 -0
- package/docs/hooks.md +339 -0
- package/docs/index.md +111 -0
- package/docs/installation.md +83 -0
- package/docs/internal-api.md +265 -0
- package/docs/internal-test-server.md +89 -0
- package/docs/locators.md +355 -0
- package/docs/mcp.md +485 -0
- package/docs/migration-4.md +556 -0
- package/docs/mobile.md +338 -0
- package/docs/pageobjects.md +399 -0
- package/docs/parallel.md +585 -0
- package/docs/playwright.md +714 -0
- package/docs/plugins.md +866 -0
- package/docs/puppeteer.md +314 -0
- package/docs/quickstart.md +120 -0
- package/docs/react.md +70 -0
- package/docs/reports.md +483 -0
- package/docs/retry.md +274 -0
- package/docs/secrets.md +150 -0
- package/docs/sessions.md +80 -0
- package/docs/shadow.md +68 -0
- package/docs/test-structure.md +275 -0
- package/docs/timeouts.md +183 -0
- package/docs/translation.md +247 -0
- package/docs/tutorial.md +271 -0
- package/docs/typescript.md +374 -0
- package/docs/web-element.md +251 -0
- package/docs/webdriver.md +708 -0
- package/docs/within.md +55 -0
- package/lib/command/dryRun.js +9 -3
- package/lib/command/init.js +247 -266
- package/lib/command/query.js +218 -0
- package/lib/config.js +9 -0
- package/lib/element/WebElement.js +37 -0
- package/lib/globals.js +11 -10
- package/lib/helper/Playwright.js +4 -1
- package/lib/html.js +3 -0
- package/lib/index.js +9 -1
- package/lib/locator.js +2 -2
- package/lib/mocha/factory.js +5 -1
- package/lib/mocha/inject.js +1 -1
- package/lib/parser.js +2 -2
- package/lib/plugin/browser.js +2 -1
- package/lib/plugin/expose.js +159 -0
- package/lib/workers.js +1 -15
- package/package.json +7 -5
- package/docs/webapi/amOnPage.mustache +0 -11
- package/docs/webapi/appendField.mustache +0 -16
- package/docs/webapi/attachFile.mustache +0 -24
- package/docs/webapi/blur.mustache +0 -18
- package/docs/webapi/checkOption.mustache +0 -13
- package/docs/webapi/clearCookie.mustache +0 -9
- package/docs/webapi/clearField.mustache +0 -14
- package/docs/webapi/click.mustache +0 -29
- package/docs/webapi/clickLink.mustache +0 -8
- package/docs/webapi/closeCurrentTab.mustache +0 -7
- package/docs/webapi/closeOtherTabs.mustache +0 -8
- package/docs/webapi/dontSee.mustache +0 -11
- package/docs/webapi/dontSeeCheckboxIsChecked.mustache +0 -10
- package/docs/webapi/dontSeeCookie.mustache +0 -8
- package/docs/webapi/dontSeeCurrentPathEquals.mustache +0 -10
- package/docs/webapi/dontSeeCurrentUrlEquals.mustache +0 -10
- package/docs/webapi/dontSeeElement.mustache +0 -12
- package/docs/webapi/dontSeeElementInDOM.mustache +0 -8
- package/docs/webapi/dontSeeInCurrentUrl.mustache +0 -4
- package/docs/webapi/dontSeeInField.mustache +0 -16
- package/docs/webapi/dontSeeInSource.mustache +0 -8
- package/docs/webapi/dontSeeInTitle.mustache +0 -8
- package/docs/webapi/dontSeeTraffic.mustache +0 -13
- package/docs/webapi/doubleClick.mustache +0 -13
- package/docs/webapi/downloadFile.mustache +0 -12
- package/docs/webapi/dragAndDrop.mustache +0 -9
- package/docs/webapi/dragSlider.mustache +0 -11
- package/docs/webapi/executeAsyncScript.mustache +0 -24
- package/docs/webapi/executeScript.mustache +0 -26
- package/docs/webapi/fillField.mustache +0 -21
- package/docs/webapi/flushNetworkTraffics.mustache +0 -5
- package/docs/webapi/focus.mustache +0 -13
- package/docs/webapi/forceClick.mustache +0 -28
- package/docs/webapi/forceRightClick.mustache +0 -18
- package/docs/webapi/grabAllWindowHandles.mustache +0 -7
- package/docs/webapi/grabAttributeFrom.mustache +0 -10
- package/docs/webapi/grabAttributeFromAll.mustache +0 -9
- package/docs/webapi/grabBrowserLogs.mustache +0 -9
- package/docs/webapi/grabCookie.mustache +0 -11
- package/docs/webapi/grabCssPropertyFrom.mustache +0 -11
- package/docs/webapi/grabCssPropertyFromAll.mustache +0 -10
- package/docs/webapi/grabCurrentUrl.mustache +0 -9
- package/docs/webapi/grabCurrentWindowHandle.mustache +0 -6
- package/docs/webapi/grabDataFromPerformanceTiming.mustache +0 -20
- package/docs/webapi/grabElementBoundingRect.mustache +0 -20
- package/docs/webapi/grabGeoLocation.mustache +0 -8
- package/docs/webapi/grabHTMLFrom.mustache +0 -10
- package/docs/webapi/grabHTMLFromAll.mustache +0 -9
- package/docs/webapi/grabNumberOfOpenTabs.mustache +0 -8
- package/docs/webapi/grabNumberOfVisibleElements.mustache +0 -9
- package/docs/webapi/grabPageScrollPosition.mustache +0 -8
- package/docs/webapi/grabPopupText.mustache +0 -5
- package/docs/webapi/grabRecordedNetworkTraffics.mustache +0 -10
- package/docs/webapi/grabSource.mustache +0 -8
- package/docs/webapi/grabTextFrom.mustache +0 -10
- package/docs/webapi/grabTextFromAll.mustache +0 -9
- package/docs/webapi/grabTitle.mustache +0 -8
- package/docs/webapi/grabValueFrom.mustache +0 -9
- package/docs/webapi/grabValueFromAll.mustache +0 -8
- package/docs/webapi/grabWebElement.mustache +0 -9
- package/docs/webapi/grabWebElements.mustache +0 -9
- package/docs/webapi/moveCursorTo.mustache +0 -16
- package/docs/webapi/openNewTab.mustache +0 -7
- package/docs/webapi/pressKey.mustache +0 -12
- package/docs/webapi/pressKeyDown.mustache +0 -12
- package/docs/webapi/pressKeyUp.mustache +0 -12
- package/docs/webapi/pressKeyWithKeyNormalization.mustache +0 -60
- package/docs/webapi/refreshPage.mustache +0 -6
- package/docs/webapi/resizeWindow.mustache +0 -6
- package/docs/webapi/rightClick.mustache +0 -14
- package/docs/webapi/saveElementScreenshot.mustache +0 -10
- package/docs/webapi/saveScreenshot.mustache +0 -12
- package/docs/webapi/say.mustache +0 -10
- package/docs/webapi/scrollIntoView.mustache +0 -11
- package/docs/webapi/scrollPageToBottom.mustache +0 -6
- package/docs/webapi/scrollPageToTop.mustache +0 -6
- package/docs/webapi/scrollTo.mustache +0 -12
- package/docs/webapi/see.mustache +0 -11
- package/docs/webapi/seeAttributesOnElements.mustache +0 -9
- package/docs/webapi/seeCheckboxIsChecked.mustache +0 -10
- package/docs/webapi/seeCookie.mustache +0 -8
- package/docs/webapi/seeCssPropertiesOnElements.mustache +0 -9
- package/docs/webapi/seeCurrentPathEquals.mustache +0 -10
- package/docs/webapi/seeCurrentUrlEquals.mustache +0 -11
- package/docs/webapi/seeElement.mustache +0 -12
- package/docs/webapi/seeElementInDOM.mustache +0 -8
- package/docs/webapi/seeFileDownloaded.mustache +0 -23
- package/docs/webapi/seeInCurrentUrl.mustache +0 -8
- package/docs/webapi/seeInField.mustache +0 -17
- package/docs/webapi/seeInPopup.mustache +0 -8
- package/docs/webapi/seeInSource.mustache +0 -7
- package/docs/webapi/seeInTitle.mustache +0 -8
- package/docs/webapi/seeNumberOfElements.mustache +0 -11
- package/docs/webapi/seeNumberOfVisibleElements.mustache +0 -10
- package/docs/webapi/seeTextEquals.mustache +0 -9
- package/docs/webapi/seeTitleEquals.mustache +0 -8
- package/docs/webapi/seeTraffic.mustache +0 -36
- package/docs/webapi/selectOption.mustache +0 -26
- package/docs/webapi/setCookie.mustache +0 -16
- package/docs/webapi/setGeoLocation.mustache +0 -12
- package/docs/webapi/startRecordingTraffic.mustache +0 -8
- package/docs/webapi/startRecordingWebSocketMessages.mustache +0 -8
- package/docs/webapi/stopRecordingTraffic.mustache +0 -5
- package/docs/webapi/stopRecordingWebSocketMessages.mustache +0 -7
- package/docs/webapi/switchTo.mustache +0 -9
- package/docs/webapi/switchToNextTab.mustache +0 -10
- package/docs/webapi/switchToPreviousTab.mustache +0 -10
- package/docs/webapi/type.mustache +0 -21
- package/docs/webapi/uncheckOption.mustache +0 -13
- package/docs/webapi/wait.mustache +0 -8
- package/docs/webapi/waitForClickable.mustache +0 -11
- package/docs/webapi/waitForCookie.mustache +0 -9
- package/docs/webapi/waitForDetached.mustache +0 -10
- package/docs/webapi/waitForDisabled.mustache +0 -6
- package/docs/webapi/waitForElement.mustache +0 -11
- package/docs/webapi/waitForEnabled.mustache +0 -6
- package/docs/webapi/waitForFunction.mustache +0 -17
- package/docs/webapi/waitForInvisible.mustache +0 -10
- package/docs/webapi/waitForNumberOfTabs.mustache +0 -9
- package/docs/webapi/waitForText.mustache +0 -13
- package/docs/webapi/waitForValue.mustache +0 -10
- package/docs/webapi/waitForVisible.mustache +0 -10
- package/docs/webapi/waitInUrl.mustache +0 -9
- package/docs/webapi/waitNumberOfVisibleElements.mustache +0 -10
- package/docs/webapi/waitToHide.mustache +0 -10
- package/docs/webapi/waitUrlEquals.mustache +0 -10
package/docs/effects.md
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
---
|
|
2
|
+
permalink: /effects
|
|
3
|
+
title: Effects
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Effects
|
|
7
|
+
|
|
8
|
+
Effects are functions that can modify scenario flow. They provide ways to handle conditional steps, retries, scoped contexts, and test flow control.
|
|
9
|
+
|
|
10
|
+
## Installation
|
|
11
|
+
|
|
12
|
+
Effects can be imported directly from CodeceptJS:
|
|
13
|
+
|
|
14
|
+
```js
|
|
15
|
+
import { tryTo, retryTo, hopeThat, within } from 'codeceptjs/effects'
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
> 📝 Note: Prior to v4, `tryTo` and `retryTo` were enabled via plugins (`tryTo`, `retryTo`) that registered them as globals. Those plugins are removed in v4 — import effects from `codeceptjs/effects` instead.
|
|
19
|
+
|
|
20
|
+
## tryTo
|
|
21
|
+
|
|
22
|
+
The `tryTo` effect allows you to attempt steps that may fail without stopping test execution. It's useful for handling optional steps or conditions that aren't critical for the test flow.
|
|
23
|
+
|
|
24
|
+
```js
|
|
25
|
+
import { tryTo } from 'codeceptjs/effects'
|
|
26
|
+
|
|
27
|
+
// inside a test
|
|
28
|
+
const success = await tryTo(() => {
|
|
29
|
+
// These steps may fail but won't stop the test
|
|
30
|
+
I.see('Cookie banner')
|
|
31
|
+
I.click('Accept cookies')
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
if (!success) {
|
|
35
|
+
I.say('Cookie banner was not found')
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
If the steps inside `tryTo` fail:
|
|
40
|
+
|
|
41
|
+
- The test will continue execution
|
|
42
|
+
- The failure will be logged in debug output
|
|
43
|
+
- `tryTo` returns `false`
|
|
44
|
+
- Auto-retries are disabled inside `tryTo` blocks
|
|
45
|
+
|
|
46
|
+
## hopeThat
|
|
47
|
+
|
|
48
|
+
`hopeThat` is the soft-assertion effect. It wraps a block of steps; if any step inside fails, the failure is recorded as a note on the test and `hopeThat` returns `false`, but the scenario keeps running. Call `hopeThat.noErrors()` once at the end to fail the scenario if any soft assertion failed.
|
|
49
|
+
|
|
50
|
+
```js
|
|
51
|
+
import { hopeThat } from 'codeceptjs/effects'
|
|
52
|
+
|
|
53
|
+
Scenario('form shows every validation error', ({ I }) => {
|
|
54
|
+
I.amOnPage('/signup')
|
|
55
|
+
I.click('Submit')
|
|
56
|
+
|
|
57
|
+
await hopeThat(() => I.see('Email is required', '#email-error'))
|
|
58
|
+
await hopeThat(() => I.see('Password is required', '#password-error'))
|
|
59
|
+
await hopeThat(() => I.see('You must accept the terms', '#terms-error'))
|
|
60
|
+
|
|
61
|
+
hopeThat.noErrors() // throws once, listing every recorded failure
|
|
62
|
+
})
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
`hopeThat` returns `Promise<boolean>` — `true` on success, `false` on caught failure — which is handy for branching:
|
|
66
|
+
|
|
67
|
+
```js
|
|
68
|
+
const cookieAccepted = await hopeThat(() => I.click('Accept cookies'))
|
|
69
|
+
if (!cookieAccepted) I.say('No cookie banner')
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
> 💡 In 3.x, soft assertions were provided by `SoftExpectHelper` (`I.softAssert`, `I.softExpectEqual`, `I.flushSoftAssertions`). That helper is gone in 4.x — use `hopeThat()` and `hopeThat.noErrors()` instead. `hopeThat` works with **any** assertion you can write inside a step: built-in `I.see*`, custom-helper assertions, `expect()` from your own assertion library, plain `assert` from Node — anything that throws on failure.
|
|
73
|
+
|
|
74
|
+
The same `hopeThat` is also re-exported from `codeceptjs/assertions` if you prefer that subpath:
|
|
75
|
+
|
|
76
|
+
```js
|
|
77
|
+
import { hopeThat } from 'codeceptjs/assertions'
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## retryTo
|
|
81
|
+
|
|
82
|
+
The `retryTo` effect allows you to retry a set of steps multiple times until they succeed. This is useful for handling flaky elements or conditions that may need multiple attempts.
|
|
83
|
+
|
|
84
|
+
```js
|
|
85
|
+
import { retryTo } from 'codeceptjs/effects'
|
|
86
|
+
|
|
87
|
+
// Retry up to 5 times with 200ms between attempts
|
|
88
|
+
await retryTo(() => {
|
|
89
|
+
I.switchTo('#editor-frame')
|
|
90
|
+
I.fillField('textarea', 'Hello world')
|
|
91
|
+
}, 5)
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Parameters:
|
|
95
|
+
|
|
96
|
+
- `callback` - Function containing steps to retry
|
|
97
|
+
- `maxTries` - Maximum number of retry attempts
|
|
98
|
+
- `pollInterval` - (optional) Delay between retries in milliseconds (default: 200ms)
|
|
99
|
+
|
|
100
|
+
The callback receives the current retry count as an argument:
|
|
101
|
+
|
|
102
|
+
```js
|
|
103
|
+
import { retryTo } from 'codeceptjs/effects'
|
|
104
|
+
|
|
105
|
+
// inside a test...
|
|
106
|
+
await retryTo(tries => {
|
|
107
|
+
I.say(`Attempt ${tries}`)
|
|
108
|
+
I.click('Submit')
|
|
109
|
+
I.see('Success')
|
|
110
|
+
}, 3)
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## within
|
|
114
|
+
|
|
115
|
+
The `within` effect scopes all actions inside it to a specific element on the page — useful when working with repeated UI components or narrowing interaction to a specific section.
|
|
116
|
+
|
|
117
|
+
```js
|
|
118
|
+
import { within } from 'codeceptjs/effects'
|
|
119
|
+
|
|
120
|
+
// inside a test...
|
|
121
|
+
await within('.js-signup-form', () => {
|
|
122
|
+
I.fillField('user[login]', 'User')
|
|
123
|
+
I.fillField('user[email]', 'user@user.com')
|
|
124
|
+
I.fillField('user[password]', 'user@user.com')
|
|
125
|
+
I.click('button')
|
|
126
|
+
})
|
|
127
|
+
I.see('There were problems creating your account.')
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
> ⚠ `within` can cause problems when used incorrectly. If you see unexpected behavior, refactor to use the context parameter on individual actions instead (e.g. `I.click('Login', '.nav')`). Keep `within` for the simplest cases.
|
|
131
|
+
|
|
132
|
+
> ⚠ Since `within` returns a Promise, always `await` it when you need its return value.
|
|
133
|
+
|
|
134
|
+
### IFrames
|
|
135
|
+
|
|
136
|
+
Use a `frame` locator to scope actions inside an iframe:
|
|
137
|
+
|
|
138
|
+
```js
|
|
139
|
+
await within({ frame: '#editor' }, () => {
|
|
140
|
+
I.see('Page')
|
|
141
|
+
I.fillField('Body', 'Hello world')
|
|
142
|
+
})
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Nested iframes _(WebDriver & Puppeteer only)_:
|
|
146
|
+
|
|
147
|
+
```js
|
|
148
|
+
await within({ frame: ['.content', '#editor'] }, () => {
|
|
149
|
+
I.see('Page')
|
|
150
|
+
})
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
> ℹ IFrames can also be accessed via `I.switchTo` command.
|
|
154
|
+
|
|
155
|
+
### Returning Values
|
|
156
|
+
|
|
157
|
+
`within` can return a value for use in the scenario:
|
|
158
|
+
|
|
159
|
+
```js
|
|
160
|
+
const val = await within('#sidebar', () => {
|
|
161
|
+
return I.grabTextFrom({ css: 'h1' })
|
|
162
|
+
})
|
|
163
|
+
I.fillField('Description', val)
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
When running steps inside a `within` block, they will be shown indented in the output.
|
|
167
|
+
|
|
168
|
+
## Usage with TypeScript
|
|
169
|
+
|
|
170
|
+
Effects are fully typed and work well with TypeScript:
|
|
171
|
+
|
|
172
|
+
```ts
|
|
173
|
+
import { tryTo, retryTo, within } from 'codeceptjs/effects'
|
|
174
|
+
|
|
175
|
+
const success = await tryTo(async () => {
|
|
176
|
+
await I.see('Element')
|
|
177
|
+
})
|
|
178
|
+
```
|
|
179
|
+
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
# Element-Based Testing
|
|
2
|
+
|
|
3
|
+
CodeceptJS offers multiple ways to write tests. While the traditional `I.*` actions provide a clean, readable syntax, element-based testing gives you more control and flexibility when working with complex DOM structures.
|
|
4
|
+
|
|
5
|
+
## Why Element-Based Testing?
|
|
6
|
+
|
|
7
|
+
Element-based testing is useful when:
|
|
8
|
+
|
|
9
|
+
- **You need direct access to DOM properties** - Inspect attributes, computed styles, or form values
|
|
10
|
+
- **Working with lists and collections** - Iterate over multiple elements with custom logic
|
|
11
|
+
- **Complex assertions** - Validate conditions that built-in methods don't cover
|
|
12
|
+
- **Performance optimization** - Reduce redundant lookups by reusing element references
|
|
13
|
+
- **Custom interactions** - Perform actions not available in standard helper methods
|
|
14
|
+
|
|
15
|
+
## The CodeceptJS Hybrid Approach
|
|
16
|
+
|
|
17
|
+
CodeceptJS uniquely combines both styles. You can freely mix `I.*` actions with element-based operations in the same test:
|
|
18
|
+
|
|
19
|
+
```js
|
|
20
|
+
// Import element functions
|
|
21
|
+
import { element, eachElement, expectElement } from 'codeceptjs/els'
|
|
22
|
+
|
|
23
|
+
Scenario('checkout flow', async ({ I }) => {
|
|
24
|
+
// Use I.* for navigation and high-level actions
|
|
25
|
+
I.amOnPage('/products')
|
|
26
|
+
I.click('Add to Cart')
|
|
27
|
+
|
|
28
|
+
// Use element-based for detailed validation
|
|
29
|
+
await element('.cart-summary', async cart => {
|
|
30
|
+
const total = await cart.getAttribute('data-total')
|
|
31
|
+
console.log('Cart total:', total)
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
// Continue with I.* actions
|
|
35
|
+
I.click('Checkout')
|
|
36
|
+
})
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
This hybrid approach gives you the best of both worlds - readable high-level actions mixed with low-level control when needed.
|
|
40
|
+
|
|
41
|
+
## Quick Comparison
|
|
42
|
+
|
|
43
|
+
### Traditional I.* Approach
|
|
44
|
+
|
|
45
|
+
```js
|
|
46
|
+
Scenario('form validation', async ({ I }) => {
|
|
47
|
+
I.amOnPage('/register')
|
|
48
|
+
I.fillField('Email', 'test@example.com')
|
|
49
|
+
I.fillField('Password', 'secret123')
|
|
50
|
+
I.click('Register')
|
|
51
|
+
I.see('Welcome')
|
|
52
|
+
})
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Element-Based Approach
|
|
56
|
+
|
|
57
|
+
```js
|
|
58
|
+
import { element, expectElement } from 'codeceptjs/els'
|
|
59
|
+
|
|
60
|
+
Scenario('form validation', async ({ I }) => {
|
|
61
|
+
I.amOnPage('/register')
|
|
62
|
+
|
|
63
|
+
// Direct form manipulation
|
|
64
|
+
await element('#email', async input => {
|
|
65
|
+
await input.type('test@example.com')
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
await element('#password', async input => {
|
|
69
|
+
await input.type('secret123')
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
await element('button[type="submit"]', async btn => {
|
|
73
|
+
await btn.click()
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
// Custom assertion
|
|
77
|
+
await expectElement('.welcome-message', async msg => {
|
|
78
|
+
const text = await msg.getText()
|
|
79
|
+
return text.includes('Welcome')
|
|
80
|
+
})
|
|
81
|
+
})
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### When to Use Each
|
|
85
|
+
|
|
86
|
+
| Use `I.*` actions when... | Use element-based when... |
|
|
87
|
+
|---------------------------|---------------------------|
|
|
88
|
+
| Simple navigation and clicks | Complex DOM traversal |
|
|
89
|
+
| Standard form interactions | Custom validation logic |
|
|
90
|
+
| Built-in assertions suffice | Need specific element properties |
|
|
91
|
+
| Readability is priority | Working with element collections |
|
|
92
|
+
| Single-step operations | Chaining multiple operations on same element |
|
|
93
|
+
|
|
94
|
+
## Element Chaining
|
|
95
|
+
|
|
96
|
+
Element-based testing allows you to chain queries to find child elements, reducing redundant lookups:
|
|
97
|
+
|
|
98
|
+
```js
|
|
99
|
+
import { element } from 'codeceptjs/els'
|
|
100
|
+
|
|
101
|
+
Scenario('product list', async ({ I }) => {
|
|
102
|
+
I.amOnPage('/products')
|
|
103
|
+
|
|
104
|
+
// Chain into child elements
|
|
105
|
+
await element('.product-list', async list => {
|
|
106
|
+
const firstProduct = await list.$('.product-item')
|
|
107
|
+
const title = await firstProduct.$('.title')
|
|
108
|
+
const price = await firstProduct.$('.price')
|
|
109
|
+
|
|
110
|
+
const titleText = await title.getText()
|
|
111
|
+
const priceValue = await price.getText()
|
|
112
|
+
|
|
113
|
+
console.log(`${titleText}: ${priceValue}`)
|
|
114
|
+
})
|
|
115
|
+
})
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Real-World Examples
|
|
119
|
+
|
|
120
|
+
### Example 1: Form Validation
|
|
121
|
+
|
|
122
|
+
Validate complex form requirements that built-in methods don't cover:
|
|
123
|
+
|
|
124
|
+
```js
|
|
125
|
+
import { element, eachElement } from 'codeceptjs/els'
|
|
126
|
+
import { expect } from 'chai'
|
|
127
|
+
|
|
128
|
+
Scenario('validate form fields', async ({ I }) => {
|
|
129
|
+
I.amOnPage('/register')
|
|
130
|
+
|
|
131
|
+
// Check all required fields are properly marked
|
|
132
|
+
await eachElement('[required]', async field => {
|
|
133
|
+
const ariaRequired = await field.getAttribute('aria-required')
|
|
134
|
+
const required = await field.getAttribute('required')
|
|
135
|
+
if (!ariaRequired && !required) {
|
|
136
|
+
throw new Error('Required field missing indicators')
|
|
137
|
+
}
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
// Fill form with custom validation
|
|
141
|
+
await element('#email', async input => {
|
|
142
|
+
await input.type('test@example.com')
|
|
143
|
+
const value = await input.getValue()
|
|
144
|
+
expect(value).to.include('@')
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
I.click('Submit')
|
|
148
|
+
})
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### Example 2: Data Table Processing
|
|
152
|
+
|
|
153
|
+
Work with tabular data using iteration and child element queries:
|
|
154
|
+
|
|
155
|
+
```js
|
|
156
|
+
import { eachElement, element } from 'codeceptjs/els'
|
|
157
|
+
|
|
158
|
+
Scenario('verify table data', async ({ I }) => {
|
|
159
|
+
I.amOnPage('/dashboard')
|
|
160
|
+
|
|
161
|
+
// Get table row count
|
|
162
|
+
await element('table tbody', async tbody => {
|
|
163
|
+
const rows = await tbody.$$('tr')
|
|
164
|
+
console.log(`Table has ${rows.length} rows`)
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
// Verify each row has expected structure
|
|
168
|
+
await eachElement('table tbody tr', async (row, index) => {
|
|
169
|
+
const cells = await row.$$('td')
|
|
170
|
+
if (cells.length < 3) {
|
|
171
|
+
throw new Error(`Row ${index} should have at least 3 columns`)
|
|
172
|
+
}
|
|
173
|
+
})
|
|
174
|
+
})
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### Example 3: Dynamic Content Waiting
|
|
178
|
+
|
|
179
|
+
Wait for and validate dynamic content with custom conditions:
|
|
180
|
+
|
|
181
|
+
```js
|
|
182
|
+
import { element, expectElement } from 'codeceptjs/els'
|
|
183
|
+
|
|
184
|
+
Scenario('wait for dynamic content', async ({ I }) => {
|
|
185
|
+
I.amOnPage('/search')
|
|
186
|
+
I.fillField('query', 'test')
|
|
187
|
+
I.click('Search')
|
|
188
|
+
|
|
189
|
+
// Wait for results with custom validation
|
|
190
|
+
await expectElement('.search-results', async results => {
|
|
191
|
+
const items = await results.$$('.result-item')
|
|
192
|
+
return items.length > 0
|
|
193
|
+
})
|
|
194
|
+
})
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### Example 4: Shopping Cart Operations
|
|
198
|
+
|
|
199
|
+
Calculate and verify cart totals by iterating through items:
|
|
200
|
+
|
|
201
|
+
```js
|
|
202
|
+
import { element, eachElement } from 'codeceptjs/els'
|
|
203
|
+
import { expect } from 'chai'
|
|
204
|
+
|
|
205
|
+
Scenario('calculate cart total', async ({ I }) => {
|
|
206
|
+
I.amOnPage('/cart')
|
|
207
|
+
|
|
208
|
+
let total = 0
|
|
209
|
+
|
|
210
|
+
// Sum up all item prices
|
|
211
|
+
await eachElement('.cart-item .price', async priceEl => {
|
|
212
|
+
const priceText = await priceEl.getText()
|
|
213
|
+
const price = parseFloat(priceText.replace('$', ''))
|
|
214
|
+
total += price
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
// Verify displayed total matches calculated sum
|
|
218
|
+
await element('.cart-total', async totalEl => {
|
|
219
|
+
const displayedTotal = await totalEl.getText()
|
|
220
|
+
const displayedValue = parseFloat(displayedTotal.replace('$', ''))
|
|
221
|
+
expect(displayedValue).to.equal(total)
|
|
222
|
+
})
|
|
223
|
+
})
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
### Example 5: List Filtering and Validation
|
|
227
|
+
|
|
228
|
+
Validate filtered results meet specific criteria:
|
|
229
|
+
|
|
230
|
+
```js
|
|
231
|
+
import { element, eachElement, expectAnyElement } from 'codeceptjs/els'
|
|
232
|
+
import { expect } from 'chai'
|
|
233
|
+
|
|
234
|
+
Scenario('filter products by price', async ({ I }) => {
|
|
235
|
+
I.amOnPage('/products')
|
|
236
|
+
I.click('Under $100')
|
|
237
|
+
|
|
238
|
+
// Verify all displayed products are under $100
|
|
239
|
+
await eachElement('.product-item', async product => {
|
|
240
|
+
const priceEl = await product.$('.price')
|
|
241
|
+
const priceText = await priceEl.getText()
|
|
242
|
+
const price = parseFloat(priceText.replace('$', ''))
|
|
243
|
+
expect(price).to.be.below(100)
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
// Check at least one product exists
|
|
247
|
+
await expectAnyElement('.product-item', async () => true)
|
|
248
|
+
})
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
## Best Practices
|
|
252
|
+
|
|
253
|
+
1. **Mix styles appropriately** - Use `I.*` for navigation and high-level actions, element-based for complex validation
|
|
254
|
+
|
|
255
|
+
2. **Use descriptive purposes** - Add purpose strings for better debugging logs:
|
|
256
|
+
```js
|
|
257
|
+
await element(
|
|
258
|
+
'verify discount applied',
|
|
259
|
+
'.price',
|
|
260
|
+
async el => { /* ... */ }
|
|
261
|
+
)
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
3. **Reuse element references** - Chain `$(locator)` to avoid redundant lookups
|
|
265
|
+
|
|
266
|
+
4. **Handle empty results** - Always check if elements exist before accessing properties
|
|
267
|
+
|
|
268
|
+
5. **Prefer standard assertions** - Use `I.see()`, `I.dontSee()` when possible for readability
|
|
269
|
+
|
|
270
|
+
6. **Consider page objects** - Combine with Page Objects for reusable element logic
|
|
271
|
+
|
|
272
|
+
## API Reference
|
|
273
|
+
|
|
274
|
+
- **[Element Access](els.md)** - Complete reference for `element()`, `eachElement()`, `expectElement()`, `expectAnyElement()`, `expectAllElements()` functions
|
|
275
|
+
- **[WebElement API](web-element.md)** - Complete reference for WebElement class methods (`getText()`, `getAttribute()`, `click()`, `$$()`, etc.)
|
|
276
|
+
|
|
277
|
+
## Portability
|
|
278
|
+
|
|
279
|
+
Elements are wrapped in a `WebElement` class that provides a consistent API across all helpers (Playwright, WebDriver, Puppeteer). Your element-based tests will work the same way regardless of which helper you're using:
|
|
280
|
+
|
|
281
|
+
```js
|
|
282
|
+
// This test works identically with Playwright, WebDriver, or Puppeteer
|
|
283
|
+
import { element } from 'codeceptjs/els'
|
|
284
|
+
|
|
285
|
+
Scenario('portable test', async ({ I }) => {
|
|
286
|
+
I.amOnPage('/')
|
|
287
|
+
|
|
288
|
+
await element('.main-title', async title => {
|
|
289
|
+
const text = await title.getText() // Works on all helpers
|
|
290
|
+
const className = await title.getAttribute('class')
|
|
291
|
+
const visible = await title.isVisible()
|
|
292
|
+
const enabled = await title.isEnabled()
|
|
293
|
+
})
|
|
294
|
+
})
|
|
295
|
+
```
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
---
|
|
2
|
+
permalink: /element-selection
|
|
3
|
+
title: Element Selection
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Element Selection
|
|
7
|
+
|
|
8
|
+
When you write `I.click('a')` and there are multiple links on a page, CodeceptJS clicks the **first** one it finds. Most of the time this is exactly what you need — your locators are specific enough that there's only one match, or the first match happens to be the right one.
|
|
9
|
+
|
|
10
|
+
But what happens when it's not?
|
|
11
|
+
|
|
12
|
+
## Picking a Specific Element
|
|
13
|
+
|
|
14
|
+
Say you have a list of items and you want to click the second one. You could write a more specific CSS selector, but sometimes the simplest approach is to tell CodeceptJS which element you want by position:
|
|
15
|
+
|
|
16
|
+
```js
|
|
17
|
+
import step from 'codeceptjs/steps'
|
|
18
|
+
|
|
19
|
+
// click the 2nd link
|
|
20
|
+
I.click('a', step.opts({ elementIndex: 2 }))
|
|
21
|
+
|
|
22
|
+
// click the last link
|
|
23
|
+
I.click('a', step.opts({ elementIndex: 'last' }))
|
|
24
|
+
|
|
25
|
+
// fill the last matching input
|
|
26
|
+
I.fillField('.email-input', 'test@example.com', step.opts({ elementIndex: -1 }))
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
The `elementIndex` option accepts:
|
|
30
|
+
|
|
31
|
+
* **Positive numbers** (1-based) — `1` is first, `2` is second, `3` is third
|
|
32
|
+
* **Negative numbers** — `-1` is last, `-2` is second-to-last
|
|
33
|
+
* **`'first'`** and **`'last'`** as readable aliases
|
|
34
|
+
|
|
35
|
+
This works with any action that targets a single element: `click`, `doubleClick`, `rightClick`, `fillField`, `appendField`, `clearField`, `checkOption`, `selectOption`, `attachFile`, and others.
|
|
36
|
+
|
|
37
|
+
If only one element matches the locator, `elementIndex` is silently ignored — you always get that single element regardless of the index value. This is convenient when the number of matches depends on page state: you won't get an error if the list happens to have just one item.
|
|
38
|
+
|
|
39
|
+
When multiple elements exist but the index is out of range, CodeceptJS throws a clear error:
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
elementIndex 100 exceeds the number of elements found (3) for "a"
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
You can combine `elementIndex` with other step options:
|
|
46
|
+
|
|
47
|
+
```js
|
|
48
|
+
I.click('a', step.opts({ elementIndex: 2 }).timeout(5).retry(3))
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Strict Mode
|
|
52
|
+
|
|
53
|
+
If you'd rather not silently click the first of many matches, enable `strict: true` in your helper configuration. This makes CodeceptJS throw an error whenever a locator matches more than one element, forcing you to write precise locators:
|
|
54
|
+
|
|
55
|
+
```js
|
|
56
|
+
// codecept.conf.js
|
|
57
|
+
helpers: {
|
|
58
|
+
Playwright: {
|
|
59
|
+
url: 'http://localhost',
|
|
60
|
+
browser: 'chromium',
|
|
61
|
+
strict: true,
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Now any ambiguous locator will fail immediately:
|
|
67
|
+
|
|
68
|
+
```js
|
|
69
|
+
I.click('a') // MultipleElementsFound: Multiple elements (3) found for "a" in strict mode
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
This is useful on projects where you want to catch accidental matches early — clicking the wrong button because of a vague locator is a common source of flaky tests.
|
|
73
|
+
|
|
74
|
+
When a test fails in strict mode, the error includes a `fetchDetails()` method that lists the matched elements with their XPath and simplified HTML, so you can see exactly what was found and write a better locator:
|
|
75
|
+
|
|
76
|
+
```js
|
|
77
|
+
// Multiple elements (3) found for "a" in strict mode. Call fetchDetails() for full information.
|
|
78
|
+
// After fetchDetails():
|
|
79
|
+
// /html/body/div/a[1] <a id="first-link">First</a>
|
|
80
|
+
// /html/body/div/a[2] <a id="second-link">Second</a>
|
|
81
|
+
// /html/body/div/a[3] <a id="third-link">Third</a>
|
|
82
|
+
// Use a more specific locator or grabWebElements() to work with multiple elements
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Strict mode is supported in **Playwright**, **Puppeteer**, and **WebDriver** helpers.
|
|
86
|
+
|
|
87
|
+
### Per-Step Strict Mode with `exact`
|
|
88
|
+
|
|
89
|
+
You don't have to enable strict mode globally. Use `exact: true` to enforce it on a single step — handy when most of your tests are fine with default behavior but a particular action needs to be precise:
|
|
90
|
+
|
|
91
|
+
```js
|
|
92
|
+
import step from 'codeceptjs/steps'
|
|
93
|
+
|
|
94
|
+
I.click('a', step.opts({ exact: true }))
|
|
95
|
+
// throws MultipleElementsFound if more than one link matches
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
`strictMode: true` is an alias if you prefer a more descriptive name:
|
|
99
|
+
|
|
100
|
+
```js
|
|
101
|
+
I.click('a', step.opts({ strictMode: true }))
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
It works the other way too. If your helper has `strict: true` globally but you need to relax it for one step, use `exact: false`:
|
|
105
|
+
|
|
106
|
+
```js
|
|
107
|
+
// strict: true in config, but this step allows multiple matches
|
|
108
|
+
I.click('a', step.opts({ exact: false }))
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
And when you know there are multiple matches and want a specific one, `elementIndex` also overrides the strict check — no error is thrown because you've explicitly chosen which element to use:
|
|
112
|
+
|
|
113
|
+
```js
|
|
114
|
+
// strict: true in config, but this works without error
|
|
115
|
+
I.click('a', step.opts({ elementIndex: 2 }))
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Summary
|
|
119
|
+
|
|
120
|
+
| Situation | Approach |
|
|
121
|
+
|-----------|----------|
|
|
122
|
+
| You want to catch ambiguous locators early | Enable `strict: true` in helper config |
|
|
123
|
+
| You need a specific element from a known list | Use `step.opts({ elementIndex: N })` |
|
|
124
|
+
| You want to iterate over all matching elements | Use [`eachElement`](/els) from the `els` module |
|
|
125
|
+
| You need full control over element inspection | Use [`grabWebElements`](/WebElement) to get all matches |
|