Skip to content

cy.contains(text) cannot find input[type=submit] by partial value: submit inputs go through whole-word CSS [value~=] while every other element gets a substring match #34305

Description

@haskiindahouse

Current behavior

cy.contains('User') finds <button>Username</button> and <a>Username</a> (substring
match) but does not find <input type="submit" value="Username"> with the identical
visible label. If the submit input is the only candidate, the command fails with
"Expected to find content: 'User' but never did."

Cause: getContainsSelector (packages/driver/src/dom/elements/find.ts:325) matches
every element type through the :cy-contains / :cy-contains-insensitive pseudo, a
substring test (normalizeWhitespaces(elem).includes(text)), except submit inputs, which
get a separate comma-appended branch:

return `${filter}:${expr}(${textToFind}), ${filter}[type='submit'][value~=${textToFind}]`

CSS ~= is the whitespace-token (whole-word) operator, not substring. Because
normalizeWhitespaces returns '' for an <input> (no text content), the [value~=]
branch is a submit input's only path, so any partial-word query skips it without error:

query element visible text cy.contains expected (substring)
'User' <input type=submit value="Username"> Username miss match
'name' <input type=submit value="Username"> Username miss match
've cha' <input type=submit value="Save changes"> Save changes miss match
'User' <button>Username</button> Username match match

Reproduced end-to-end in Cypress 15.18.0 (cypress run, Electron): cy.contains('User')
times out with "Expected to find content: 'User' but never did" against
<input type="submit" value="Username">, while the two controls pass in the same run
(cy.contains('Username') finds the submit input; cy.contains('User') finds
<button>Username</button>). Host-independent: pure native querySelectorAll
(no jQuery/Sizzle) confirms [type=submit][value~='User'] matches nothing for
value="Username" in any engine.

Desired behavior

Submit inputs should get the same substring semantics as every other element; the same
label text should be findable regardless of whether it renders as a <button> or an
<input type=submit>. Cypress already decided this for the RegExp path: #21285 (fixing
#21166) added if (isSubmit(elem)) return regex.test(elem.value) to the
cy-contains-regex pseudo, a proper pattern test on value. The plain-string path
stayed on whole-word [value~=] and now disagrees with that fix, with the .includes()
semantics used everywhere else, and with the docs (which advertise "Find the
input[type='submit'] by value" with no whole-word caveat).

Suggested fix, mirroring #21285: make the :cy-contains* pseudos read value for submit
inputs and drop the [value~=] branch, e.g. in the pseudo matcher:

const testText = isSubmit(elem) ? elem.value : normalizeWhitespaces(elem)

Test code to reproduce

Minimal page (index.html):

<input type="submit" value="Username">

Spec:

it('cy.contains partial text does NOT find a submit input by value — FAILS', () => {
  cy.visit('index.html')
  // "Username".includes("User") is true, but [value~='User'] is whole-word -> 0 matches
  cy.contains('User')   // fails: Expected to find content: 'User' but never did.
})

it('control: identical label as a <button> IS found', () => {
  // page: <button>Username</button>
  cy.visit('button.html')
  cy.contains('User')   // passes
})

it('control: exact single-word value IS found', () => {
  cy.visit('index.html')
  cy.contains('Username')   // passes ([value~=] exact word)
})

Cypress Version

Reproduced end-to-end on the released 15.18.0 binary (cypress run, Electron,
2026-07-20). Affected code unchanged on develop @ 37a899cab.

Debug Logs

n/a: deterministic selector-level behavior.

Other

  • Root cause: packages/driver/src/dom/elements/find.ts:325 (getContainsSelector
    submit branch, [value~=]); find.ts:255-278 (:cy-contains* pseudos, substring);
    packages/driver/src/dom/elements/utils.ts (normalizeWhitespaces returns '' for
    inputs, so [value~=] is the only submit path). End-to-end check confirmed
    getContainsSelector is the sole matcher; querying.ts only picks among
    already-matched elements and cannot re-introduce a skipped submit.
  • Note: an exact multi-word query (cy.contains('Save changes')) happens to match, but
    only via a Sizzle tokenizer quirk that degrades [value~='Save changes'] to
    [value~='Save']. Masked, not correct; the clean divergences are single-word partials
    ('User' for Username) and cross-boundary partials ('ve cha' for Save changes).
  • Honest scope: severity low. Exact single-word and regex lookups work, and a
    cy.get('input[type=submit]') workaround exists; the defect is the button-vs-submit
    inconsistency for identical labels, which fails with a "content not found" error that
    points the user at the wrong cause.

Found via property-based & differential bug-hunting, part of an effort to scale PBT (DepTyCheck-based) testing across the OSS ecosystem.

If this is intended / by-design: I'm really sorry, please just close it — no need to flag or ban me. I'm trying to scale property-based testing across the whole ecosystem and my publishing agents may have gotten this one wrong. I read every issue and follow up on each.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions