Skip to content

feat(actor): chain step options on helper actions - #5788

Merged
DenysKuchma merged 4 commits into
4.xfrom
feat/step-element-selection-chain
Nov 9, 2026
Merged

DenysKuchma merged 4 commits into
4.xfrom
feat/step-element-selection-chain

Conversation

@DavertMik

@DavertMik DavertMik commented Nov 6, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Sets step options by chaining them to the action, without step.opts():

I.click('.btn').first()
I.fillField('input', 'x').last()
await I.checkOption('.opt').at(2)   // at(-2) counts from the end
I.see('Welcome').retry(3)
I.click('Slow').timeout(30)
I.click('a').first().timeout(5).retry(3)
I.click('a').stepConfig(step.opts({ exact: true }).timeout(5))

This works because recordStep only queues the step, so a chained call runs before the step does. Selection overrides strict mode the same way elementIndex does. Passing step.opts(), step.timeout() or step.retry() as an argument keeps working, so nothing breaks at runtime.

Changes

  • lib/step/record.js: extracts applyStepConfig, used by both argument configs and the chain. Options now merge instead of replace.
  • lib/actor.js: adds stepConfig() to the promise of every helper action. first, last, at, timeout and retry are built on it. Calling any of them after the step has run throws an error.
  • lib/recorder.js: recorder.add also accepts its timeout as a function, read when the task starts. It was passed by value at queue time, so a chained timeout was ignored.
  • lib/step/retry.js: a step's retry rule is removed when that step finishes, instead of retries.pop() on the next finished step. A chained retry would otherwise leak onto the next step, and pop() could remove a rule added by something else.
  • lib/helper/Puppeteer.js: forceClick hardcoded els[0], ignoring strict mode and the index. It now uses selectElement.
  • Typings: adds CodeceptJS.StepPromise (stepConfig, timeout, retry) for action mustaches, and CodeceptJS.ElementStepPromise (adds first, last, at) for actions that enforce strict mode.
  • Docs: element-selection, locators, migration-4, retry and timeouts.

Behaviour to review

  • Default typings: 91 action mustaches now return a promise type instead of void, so projects using no-floating-promises get warnings on unawaited calls. This is the largest visible effect of the PR.

  • Grabbers: methods that return values keep their existing types, so .retry() on them works at runtime but doesn't compile in TypeScript.

  • Selection is ignored where selection isn't used:

    • non-element actions such as amOnPage
    • switchTo
    • WebDriver moveCursorTo, focus, blur, scrollTo, forceRightClick
    • Puppeteer saveElementScreenshot, clickXY

    None of these throw in strict mode on those helpers.

  • Custom steps and page object methods don't get the chain.

Testing

  • test/acceptance/step_chain_test.js: for each strict action, the step throws MultipleElementsFound without the chain, passes with .first(), and rejects .at(100). Also covers field position, timeout(), retry() and combined chains.
  • test/unit/actor_test.js: chain behaviour, including retry rule cleanup on pass and on failure, and keeping a global rule.
  • Local runs: Playwright 59 passed, Puppeteer 56 passed, unit tests 923 passed with 11 pending. within and session passed on Playwright.
  • One local Playwright run failed clickXY ... first() with a 10s test timeout. It then passed 6 of 6 alone and 59 of 59 in two full runs. I didn't find a cause; the local single-threaded php -S server is my guess.
  • tsd locally reports only 2 cross-file Config conflict errors, unrelated to this change. The Typings tests workflow doesn't start on any branch, including 4.x, because of a workflow file issue, so CI hasn't checked the typings.
  • Not run locally: WebDriver, covered by CI only. The new scenarios aren't tagged for the CDPBrowser family, and Appium runtime behaviour wasn't checked.

🤖 Generated with Claude Code

DavertMik and others added 3 commits November 6, 2026 15:42
I.click('a').first() selects a match without step.opts(), and overrides
strict mode like elementIndex does. Puppeteer forceClick now honours strict
mode and the selected index, matching the other helpers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
WebDriver grabValueFromAll drops empty values, so the array comparison
could not tell which field was filled.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
I.see('x').retry(3) and I.click('a').first().timeout(5) now share one path
with step.opts(), through applyStepConfig. The recorder reads a step timeout
when the task starts, so a chained timeout applies. A step retry rule is
removed when that step finishes, instead of popping the last rule.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@DavertMik DavertMik changed the title feat(actor): chain first(), last() and at() on helper actions feat(actor): chain step options on helper actions Nov 7, 2026
The previous commit only contains the test file rename. This one has the
implementation: applyStepConfig shared with step.opts(), the recorder reading
a step timeout when the task starts, per-step retry rule cleanup, typings
and docs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@DenysKuchma
DenysKuchma merged commit d87431c into 4.x Nov 9, 2026
16 checks passed
@DenysKuchma
DenysKuchma deleted the feat/step-element-selection-chain branch November 9, 2026 13:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants