Skip to content

feat: [#2387] option:checked matches a selected <option> - #2388

Open
marciomazza wants to merge 1 commit into
capricorn86:masterfrom
marciomazza:feat/pseudo-class-option-checked
Open

feat: [#2387] option:checked matches a selected <option>#2388
marciomazza wants to merge 1 commit into
capricorn86:masterfrom
marciomazza:feat/pseudo-class-option-checked

Conversation

@marciomazza

Copy link
Copy Markdown

Description

Resolves #2387:checked is defined to match a selected <option>, not just a
checked <input>, but option:checked currently matches nothing in happy-dom.

const div = document.createElement('div');
div.innerHTML = `<select><option>a</option><option selected>b</option></select>`;
div.querySelectorAll('option:checked').length; // happy-dom: 0 — real browsers: 1

Root cause: SelectorItem.matchPseudoItem's checked case returns a match only
when tagName === 'INPUT', so an <option> always falls through to null.

Fix: the checked case now also returns a match for an OPTION element whose
selected is true.

Depends on #2268: without it, a cached option:checked result goes stale after a
programmatic selection change (option.selected / select.value /
select.selectedIndex) — the same cache-invalidation gap as #1594. The content
attribute path (selected) already clears the cache, so it is correct on its own.

Tests

Added 1 test in QuerySelector.test.ts: option[value='b']:checked is warmed with
nothing selected (0 matches), then setAttribute('selected', ''), then re-queried
and asserted to return the newly selected option — covers the new match and cache
freshness on the attribute path. Fails before, passes after. Full npm test passes.

AI

I used Claude Code to write this, over multiple iterations. I guided and reviewed
it myself at every step.

Before submitting the PR, please make sure you do the following:

  • Read the contributing guidelines.
  • It's really useful if your PR references an issue where it is discussed ahead of time.
  • Please check Allow edits by maintainers to make review process faster. Note that this option is not available for repositories that are owned by Github organizations.

Tests

  • Make sure to add tests for your changes. Run your test in a real browser to make sure that the test tests what a real browser would do (e.g. by running the code in the browser console).
    • Ran the repro in Chrome (headless) console: option:checked warmed at 0 matches, then option.setAttribute('selected', ''), then re-query returns 1 element with value b. Matches the fixed behavior.
  • Run the tests with npm test locally to make sure that all tests pass before submitting the PR.

Title

  • The title of the pull request should be in the format of "type: [#issue] description". The type can be feat, fix, chore or BREAKING CHANGE. The issue is optional and can be omitted if the pull request does not relate to an issue.
  • The title should be concise and descriptive. The title will be used when generating release notes. Make sure that the title is easily understood by users of the library.

AI tools

  • Please disclose in the PR description that you used AI tools to generate code. This is important for transparency and to ensure that the generated code meets the quality standards of the project.

Per spec, :checked matches a selected <option> in addition to a checked
checkbox/radio <input>, but SelectorItem's 'checked' case only ever checked
the INPUT tag.

Attribute-driven selection (the "selected" content attribute) invalidates
the query selector cache, so "option:checked" queries stay fresh. Selection changed
programmatically (option.selected / select.value / select.selectedIndex)
does not yet invalidate the cache - a pre-existing gap that also affects
select.selectedOptions (capricorn86#1594), fixed separately.
@marciomazza
marciomazza force-pushed the feat/pseudo-class-option-checked branch from eabea8b to ab710f6 Compare September 8, 2026 14:19
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.

option:checked never matches a selected <option>

1 participant