diff --git a/docs/element-selection.md b/docs/element-selection.md index f8441b17c..bfaebda98 100644 --- a/docs/element-selection.md +++ b/docs/element-selection.md @@ -11,7 +11,28 @@ But what happens when it's not? ## Picking a Specific Element -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: +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. Chain `first()`, `last()` or `at()` to the action: + +```js +I.click('a').first() + +// click the last link +I.click('a').last() + +// click the 2nd link +I.click('a').at(2) + +// fill the second-to-last matching input +I.fillField('.email-input', 'test@example.com').at(-2) +``` + +Chain the selection directly to the action, in the same expression. `await I.click('a').last()` works too. Calling it after the step has run throws an error. + +These methods exist on helper actions only. They are not available on custom steps and page object methods. + +They differ from `locate('a').first()`, which builds a position into the locator. A chained selection picks from whatever the action matched, including fuzzy matches like `I.click('Submit').last()`. + +The same selection is available as a step option: ```js import step from 'codeceptjs/steps' @@ -108,10 +129,12 @@ It works the other way too. If your helper has `strict: true` globally but you n I.click('a', step.opts({ exact: false })) ``` -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: +And when you know there are multiple matches and want a specific one, `first()`, `last()`, `at()` and `elementIndex` override the strict check — no error is thrown because you've explicitly chosen which element to use: ```js -// strict: true in config, but this works without error +// strict: true in config, but these work without error +I.click('a').first() +I.click('a').at(2) I.click('a', step.opts({ elementIndex: 2 })) ``` @@ -120,6 +143,6 @@ I.click('a', step.opts({ elementIndex: 2 })) | Situation | Approach | |-----------|----------| | You want to catch ambiguous locators early | Enable `strict: true` in helper config | -| You need a specific element from a known list | Use `step.opts({ elementIndex: N })` | +| You need a specific element from a known list | Chain `.first()`, `.last()` or `.at(N)` to the action | | You want to iterate over all matching elements | Use [`eachElement`](/els) from the `els` module | | You need full control over element inspection | Use [`grabWebElements`](/WebElement) to get all matches | diff --git a/docs/locators.md b/docs/locators.md index 4fd3ea746..6fb586345 100644 --- a/docs/locators.md +++ b/docs/locators.md @@ -158,17 +158,15 @@ I.tap('~submit') // mobile ## Picking a specific element -When a locator matches several elements on the page, CodeceptJS acts on the first one by default. To target a different match, pass `elementIndex` via `step.opts()`: +When a locator matches several elements on the page, CodeceptJS acts on the first one by default. To target a different match, chain `first()`, `last()` or `at()` to the action: ```js -import step from 'codeceptjs/steps' - -I.click('a', step.opts({ elementIndex: 2 })) // the 2nd link -I.click('a', step.opts({ elementIndex: 'last' })) // the last link -I.fillField('.email-input', 'u@t.com', step.opts({ elementIndex: -1 })) +I.click('a').at(2) // the 2nd link +I.click('a').last() // the last link +I.fillField('.email-input', 'u@t.com').at(-1) ``` -`elementIndex` accepts positive numbers (1-based), negative numbers (`-1` is last), or the aliases `'first'` and `'last'`. It works with `click`, `fillField`, `selectOption`, `checkOption`, and other single-element actions. +`at()` accepts positive numbers (1-based) and negative numbers (`-1` is last). It works with `click`, `fillField`, `selectOption`, `checkOption`, and other single-element actions. The same selection is available as `step.opts({ elementIndex: 2 })`. To catch ambiguous locators during development rather than silently using the first match, enable `strict: true` in the helper config, or pass `step.opts({ exact: true })` on a single step: diff --git a/docs/migration-4.md b/docs/migration-4.md index eba44b583..726fae99d 100644 --- a/docs/migration-4.md +++ b/docs/migration-4.md @@ -534,6 +534,8 @@ I.retry(3).click('Submit') // I.click('Submit', step.retry(3)) I.limitTime(10).fillField('Email', 'a@b.c') // I.fillField('Email', 'a@b.c', step.timeout(10)) ``` +You can also chain them to the action: `I.click('Submit').retry(3)`, `I.fillField('Email', 'a@b.c').timeout(10)`. Pass a whole config with `I.click('Submit').stepConfig(step.retry(3).timeout(10))`. + `step.*` configs are also composable with the other step options: ```js @@ -648,11 +650,13 @@ The error includes a `fetchDetails()` method that prints XPaths and HTML for eve Pick a specific match without writing a more specific locator: ```js -I.click('a', step.opts({ elementIndex: 2 })) -I.click('a', step.opts({ elementIndex: 'last' })) -I.fillField('input', 'x', step.opts({ elementIndex: -1 })) +I.click('a').at(2) +I.click('a').last() +I.fillField('input', 'x').first() ``` +`step.opts({ elementIndex: 2 })` does the same. + ### Unfocused Element Detection `I.type()` and `I.pressKey()` throw `NonFocusedType` if no element has focus. Click or focus the field first. diff --git a/docs/retry.md b/docs/retry.md index a32d152e9..d8535b793 100644 --- a/docs/retry.md +++ b/docs/retry.md @@ -119,7 +119,7 @@ import step from 'codeceptjs/steps' Scenario('checkout', ({ I }) => { I.amOnPage('/cart') I.click('Proceed to Checkout', step.retry(5)) // retry up to 5 times - I.see('Payment') + I.see('Payment').retry(3) // same, chained to the action }) ``` diff --git a/docs/timeouts.md b/docs/timeouts.md index b23b08dda..2f82dceac 100644 --- a/docs/timeouts.md +++ b/docs/timeouts.md @@ -9,14 +9,14 @@ CodeceptJS provides multiple timeout configurations to control test execution ti ## Step Timeout -Set timeout for individual steps using `step.timeout()`: +Set timeout for individual steps by chaining `timeout()` to the action, or by passing `step.timeout()`: ```js import step from 'codeceptjs/steps' Scenario('test with step timeouts', ({ I }) => { I.amOnPage('/') - I.click('Slow Button', step.timeout(30)) + I.click('Slow Button').timeout(30) I.fillField('Email', 'user@example.com', step.timeout(10)) }) ``` diff --git a/docs/webapi/amOnPage.mustache b/docs/webapi/amOnPage.mustache index c6d58bdc9..ae1693333 100644 --- a/docs/webapi/amOnPage.mustache +++ b/docs/webapi/amOnPage.mustache @@ -8,4 +8,4 @@ I.amOnPage('/login'); // opens a login page ``` @param {string} url url path or global url. -@returns {void} automatically synchronized promise through #recorder +@returns {CodeceptJS.StepPromise} automatically synchronized promise through #recorder diff --git a/docs/webapi/appendField.mustache b/docs/webapi/appendField.mustache index 8daf7b7e2..879ee8cbd 100644 --- a/docs/webapi/appendField.mustache +++ b/docs/webapi/appendField.mustache @@ -13,4 +13,4 @@ I.appendField('name', 'John', '.form-container'); @param {CodeceptJS.LocatorOrString} field located by label|name|CSS|XPath|strict locator @param {string} value text value to append. @param {?CodeceptJS.LocatorOrString} [context=null] (optional, `null` by default) element located by CSS | XPath | strict locator. -@returns {void} automatically synchronized promise through #recorder +@returns {CodeceptJS.ElementStepPromise} automatically synchronized promise through #recorder diff --git a/docs/webapi/attachFile.mustache b/docs/webapi/attachFile.mustache index 691b57f22..97d381a3e 100644 --- a/docs/webapi/attachFile.mustache +++ b/docs/webapi/attachFile.mustache @@ -21,4 +21,4 @@ I.attachFile('#dropzone', 'data/avatar.jpg'); @param {CodeceptJS.LocatorOrString} locator field located by label|name|CSS|XPath|strict locator. @param {string} pathToFile local file path relative to codecept.conf.ts or codecept.conf.js config file. @param {?CodeceptJS.LocatorOrString} [context=null] (optional, `null` by default) element located by CSS | XPath | strict locator. -@returns {void} automatically synchronized promise through #recorder +@returns {CodeceptJS.ElementStepPromise} automatically synchronized promise through #recorder diff --git a/docs/webapi/blur.mustache b/docs/webapi/blur.mustache index e10e9b4e3..000898e0c 100644 --- a/docs/webapi/blur.mustache +++ b/docs/webapi/blur.mustache @@ -15,4 +15,4 @@ I.dontSee('#add-to-cart-btn'); @param {CodeceptJS.LocatorOrString} locator field located by label|name|CSS|XPath|strict locator. @param {any} [options] Playwright only: [Additional options](https://playwright.dev/docs/api/class-locator#locator-blur) for available options object as 2nd argument. -@returns {void} automatically synchronized promise through #recorder +@returns {CodeceptJS.ElementStepPromise} automatically synchronized promise through #recorder diff --git a/docs/webapi/checkOption.mustache b/docs/webapi/checkOption.mustache index c7a4c4582..160473697 100644 --- a/docs/webapi/checkOption.mustache +++ b/docs/webapi/checkOption.mustache @@ -10,4 +10,4 @@ I.checkOption('agree', '//form'); ``` @param {CodeceptJS.LocatorOrString} field checkbox located by label | name | 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 +@returns {CodeceptJS.ElementStepPromise} automatically synchronized promise through #recorder diff --git a/docs/webapi/clearClipboard.mustache b/docs/webapi/clearClipboard.mustache index 82fd51e4d..f33506834 100644 --- a/docs/webapi/clearClipboard.mustache +++ b/docs/webapi/clearClipboard.mustache @@ -5,4 +5,4 @@ I.clearClipboard(); I.seeClipboardEquals(''); ``` -@returns {void} automatically synchronized promise through #recorder +@returns {CodeceptJS.StepPromise} automatically synchronized promise through #recorder diff --git a/docs/webapi/clearField.mustache b/docs/webapi/clearField.mustache index fe0790120..2cd468953 100644 --- a/docs/webapi/clearField.mustache +++ b/docs/webapi/clearField.mustache @@ -11,4 +11,4 @@ I.clearField('Email', '.form-container'); ``` @param {LocatorOrString} editable field located by label|name|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. +@returns {CodeceptJS.ElementStepPromise} automatically synchronized promise through #recorder. diff --git a/docs/webapi/click.mustache b/docs/webapi/click.mustache index e9fa5cfbf..5253b660b 100644 --- a/docs/webapi/click.mustache +++ b/docs/webapi/click.mustache @@ -30,4 +30,4 @@ I.click({role: 'button', name: 'Submit'}); @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 +@returns {CodeceptJS.ElementStepPromise} automatically synchronized promise through #recorder diff --git a/docs/webapi/clickLink.mustache b/docs/webapi/clickLink.mustache index a1a6983ab..4a96d001f 100644 --- a/docs/webapi/clickLink.mustache +++ b/docs/webapi/clickLink.mustache @@ -5,4 +5,4 @@ I.clickLink('Logout', '#nav'); ``` @param {CodeceptJS.LocatorOrString} locator clickable link or button located by text, or any element located by CSS|XPath|strict locator @param {?CodeceptJS.LocatorOrString} [context=null] (optional, `null` by default) element to search in CSS|XPath|Strict locator -@returns {void} automatically synchronized promise through #recorder +@returns {CodeceptJS.ElementStepPromise} automatically synchronized promise through #recorder diff --git a/docs/webapi/closeCurrentTab.mustache b/docs/webapi/closeCurrentTab.mustache index f7fbb3f77..88ac56cd8 100644 --- a/docs/webapi/closeCurrentTab.mustache +++ b/docs/webapi/closeCurrentTab.mustache @@ -4,4 +4,4 @@ Close current tab. I.closeCurrentTab(); ``` -@returns {void} automatically synchronized promise through #recorder +@returns {CodeceptJS.StepPromise} automatically synchronized promise through #recorder diff --git a/docs/webapi/closeOtherTabs.mustache b/docs/webapi/closeOtherTabs.mustache index ac344b859..cdd18b11d 100644 --- a/docs/webapi/closeOtherTabs.mustache +++ b/docs/webapi/closeOtherTabs.mustache @@ -5,4 +5,4 @@ Close all tabs except for the current one. I.closeOtherTabs(); ``` -@returns {void} automatically synchronized promise through #recorder +@returns {CodeceptJS.StepPromise} automatically synchronized promise through #recorder diff --git a/docs/webapi/dontSee.mustache b/docs/webapi/dontSee.mustache index 120367ce5..b74db6376 100644 --- a/docs/webapi/dontSee.mustache +++ b/docs/webapi/dontSee.mustache @@ -8,4 +8,4 @@ I.dontSee('Login', '.nav'); // no login inside .nav element @param {string} text which is not present. @param {CodeceptJS.LocatorOrString} [context] (optional) element located by CSS|XPath|strict locator in which to perfrom search. -@returns {void} automatically synchronized promise through #recorder +@returns {CodeceptJS.StepPromise} automatically synchronized promise through #recorder diff --git a/docs/webapi/dontSeeCheckboxIsChecked.mustache b/docs/webapi/dontSeeCheckboxIsChecked.mustache index 64359ea38..f9226ece3 100644 --- a/docs/webapi/dontSeeCheckboxIsChecked.mustache +++ b/docs/webapi/dontSeeCheckboxIsChecked.mustache @@ -7,4 +7,4 @@ I.dontSeeCheckboxIsChecked('agree'); // located by name ``` @param {CodeceptJS.LocatorOrString} field located by label|name|CSS|XPath|strict locator. -@returns {void} automatically synchronized promise through #recorder +@returns {CodeceptJS.StepPromise} automatically synchronized promise through #recorder diff --git a/docs/webapi/dontSeeCookie.mustache b/docs/webapi/dontSeeCookie.mustache index 0614324cb..e1c172a10 100644 --- a/docs/webapi/dontSeeCookie.mustache +++ b/docs/webapi/dontSeeCookie.mustache @@ -5,4 +5,4 @@ I.dontSeeCookie('auth'); // no auth cookie ``` @param {string} name cookie name. -@returns {void} automatically synchronized promise through #recorder +@returns {CodeceptJS.StepPromise} automatically synchronized promise through #recorder diff --git a/docs/webapi/dontSeeCurrentPathEquals.mustache b/docs/webapi/dontSeeCurrentPathEquals.mustache index 7142f8560..bf1576160 100644 --- a/docs/webapi/dontSeeCurrentPathEquals.mustache +++ b/docs/webapi/dontSeeCurrentPathEquals.mustache @@ -7,4 +7,4 @@ I.dontSeeCurrentPathEquals('/'); // fails for '/', '/?user=ok', '/#top' ``` @param {string} path value to check. -@returns {void} automatically synchronized promise through #recorder +@returns {CodeceptJS.StepPromise} automatically synchronized promise through #recorder diff --git a/docs/webapi/dontSeeCurrentUrlEquals.mustache b/docs/webapi/dontSeeCurrentUrlEquals.mustache index 316312ffa..15f540352 100644 --- a/docs/webapi/dontSeeCurrentUrlEquals.mustache +++ b/docs/webapi/dontSeeCurrentUrlEquals.mustache @@ -7,4 +7,4 @@ I.dontSeeCurrentUrlEquals('http://mysite.com/login'); // absolute urls are also ``` @param {string} url value to check. -@returns {void} automatically synchronized promise through #recorder +@returns {CodeceptJS.StepPromise} automatically synchronized promise through #recorder diff --git a/docs/webapi/dontSeeElement.mustache b/docs/webapi/dontSeeElement.mustache index b7639a299..573248611 100644 --- a/docs/webapi/dontSeeElement.mustache +++ b/docs/webapi/dontSeeElement.mustache @@ -9,4 +9,4 @@ 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 +@returns {CodeceptJS.StepPromise} automatically synchronized promise through #recorder diff --git a/docs/webapi/dontSeeElementInDOM.mustache b/docs/webapi/dontSeeElementInDOM.mustache index 6b509f23f..ad6a39e8a 100644 --- a/docs/webapi/dontSeeElementInDOM.mustache +++ b/docs/webapi/dontSeeElementInDOM.mustache @@ -5,4 +5,4 @@ I.dontSeeElementInDOM('.nav'); // checks that element is not on page visible or ``` @param {CodeceptJS.LocatorOrString} locator located by CSS|XPath|Strict locator. -@returns {void} automatically synchronized promise through #recorder +@returns {CodeceptJS.StepPromise} automatically synchronized promise through #recorder diff --git a/docs/webapi/dontSeeInCurrentUrl.mustache b/docs/webapi/dontSeeInCurrentUrl.mustache index 56bd5cde2..8ca955558 100644 --- a/docs/webapi/dontSeeInCurrentUrl.mustache +++ b/docs/webapi/dontSeeInCurrentUrl.mustache @@ -1,4 +1,4 @@ Checks that current url does not contain a provided fragment. @param {string} url value to check. -@returns {void} automatically synchronized promise through #recorder +@returns {CodeceptJS.StepPromise} automatically synchronized promise through #recorder diff --git a/docs/webapi/dontSeeInField.mustache b/docs/webapi/dontSeeInField.mustache index e1d466145..d4d38bd86 100644 --- a/docs/webapi/dontSeeInField.mustache +++ b/docs/webapi/dontSeeInField.mustache @@ -13,4 +13,4 @@ I.dontSeeInField('Name', 'old_value', '.form-container'); @param {CodeceptJS.LocatorOrString} field located by label|name|CSS|XPath|strict locator. @param {CodeceptJS.StringOrSecret} value value to check. @param {?CodeceptJS.LocatorOrString} [context=null] (optional, `null` by default) element located by CSS | XPath | strict locator. -@returns {void} automatically synchronized promise through #recorder +@returns {CodeceptJS.StepPromise} automatically synchronized promise through #recorder diff --git a/docs/webapi/dontSeeInSource.mustache b/docs/webapi/dontSeeInSource.mustache index 7e6fb6b0e..7115850e1 100644 --- a/docs/webapi/dontSeeInSource.mustache +++ b/docs/webapi/dontSeeInSource.mustache @@ -5,4 +5,4 @@ I.dontSeeInSource('