Skip to content

Fix retryFailedStep ignoredSteps exact-name matching#5571

Merged
DavertMik merged 2 commits into
codeceptjs:4.xfrom
gololdf1sh:fix/ignoredsteps-indexof
May 23, 2026
Merged

Fix retryFailedStep ignoredSteps exact-name matching#5571
DavertMik merged 2 commits into
codeceptjs:4.xfrom
gololdf1sh:fix/ignoredsteps-indexof

Conversation

@gololdf1sh
Copy link
Copy Markdown
Contributor

@gololdf1sh gololdf1sh commented May 18, 2026

Problem

In lib/plugin/retryFailedStep.js:119 the wildcard check uses ignored.indexOf('*') as a boolean:

} else if (ignored.indexOf('*') && step.title.startsWith(ignored.slice(0, -1))) return

String.prototype.indexOf returns -1 when the character is not found. -1 is truthy in JavaScript (only 0 is falsy), so the startsWith(ignored.slice(0, -1)) branch executes for entries that do not contain * too. On top of that, slice(0, -1) chops the last character, broadening the match further.

Per the docs, ignoredSteps should support both exact step names and wildcard prefixes ('wait*'). The exact-name mode is silently broken.

Reproduction

// codecept.conf
plugins: {
  retryFailedStep: { enabled: true, retries: 3, ignoredSteps: ['see'] }
}

// Test
Scenario('seeElement is ignored even though only "see" is in ignoredSteps', ({ I }) => {
  I.amOnPage('/index.html');
  I.seeElement('[data-test-id="non-existent"]');
});

Expected: seeElement is retried (only see is in ignoredSteps).
Actual: seeElement is ignored — single attempt, no retries.

ignoredSteps: ['see'] also silently ignores seeElement, seeInField, seeInTitle, selectOption, sendPostRequest — anything starting with se.

Fix

Compare indexOf('*') against -1 explicitly. One-character change in lib/plugin/retryFailedStep.js:119:

-} else if (ignored.indexOf('*') && step.title.startsWith(ignored.slice(0, -1))) return
+} else if (ignored.indexOf('*') !== -1 && step.title.startsWith(ignored.slice(0, -1))) return

Repro project

gololdf1sh/codeceptjs-retry-test.

The wildcard check used `ignored.indexOf('*')` as a boolean. `-1` is
truthy in JavaScript (only `0` is falsy), so entries without `*` were
matched via `startsWith(slice(0, -1))` instead of exact compare, which
also chops the last character — broadening the match further.

`ignoredSteps: ['see']` silently ignored `seeElement`, `seeInField`,
`selectOption`, `sendPostRequest` — anything starting with `se`.

Compare against `-1` explicitly so exact-name entries only match
themselves, as the docs describe.
Each call to retryFailedStep mutated the module-level defaultConfig via
Object.assign(defaultConfig, config), so config.when from a prior call leaked
into the next as customWhen and chained recursively. In tests this made
when() return undefined for closures that no longer had a live step.started
listener (e.g. after event.cleanDispatcher), causing the new regression test
to fail in the full unit suite even though it passed in isolation.

Use Object.assign({}, defaultConfig, config) so each registration gets an
independent config object. Rewrites the regression test to assert via
retryConfig.when() directly, which is now sound.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@DavertMik DavertMik merged commit 2c89828 into codeceptjs:4.x May 23, 2026
10 checks passed
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