Skip to content

Commit eabea8b

Browse files
committed
feat: :checked pseudo-class support for <option>
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 (#1594), fixed separately.
1 parent eac5a38 commit eabea8b

2 files changed

Lines changed: 33 additions & 3 deletions

File tree

packages/happy-dom/src/query-selector/SelectorItem.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import * as PropertySymbol from '../PropertySymbol.js';
22
import type Element from '../nodes/element/Element.js';
33
import type HTMLInputElement from '../nodes/html-input-element/HTMLInputElement.js';
4+
import type HTMLOptionElement from '../nodes/html-option-element/HTMLOptionElement.js';
45
import SelectorCombinatorEnum from './SelectorCombinatorEnum.js';
56
import type ISelectorAttribute from './ISelectorAttribute.js';
67
import type ISelectorMatch from './ISelectorMatch.js';
@@ -270,9 +271,13 @@ export default class SelectorItem {
270271
}
271272
return isFound ? { priorityWeight: 10 } : null;
272273
case 'checked':
273-
return element[PropertySymbol.tagName] === 'INPUT' && (<HTMLInputElement>element).checked
274-
? { priorityWeight: 10 }
275-
: null;
274+
if (element[PropertySymbol.tagName] === 'INPUT') {
275+
return (<HTMLInputElement>element).checked ? { priorityWeight: 10 } : null;
276+
}
277+
if (element[PropertySymbol.tagName] === 'OPTION') {
278+
return (<HTMLOptionElement>element).selected ? { priorityWeight: 10 } : null;
279+
}
280+
return null;
276281
case 'disabled':
277282
return 'disabled' in element && element.hasAttribute('disabled')
278283
? { priorityWeight: 10 }

packages/happy-dom/test/query-selector/QuerySelector.test.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type Document from '../../src/nodes/document/Document.js';
44
import QuerySelectorHTML from './data/QuerySelectorHTML.js';
55
import QuerySelectorNthChildHTML from './data/QuerySelectorNthChildHTML.js';
66
import type HTMLInputElement from '../../src/nodes/html-input-element/HTMLInputElement.js';
7+
import type HTMLOptionElement from '../../src/nodes/html-option-element/HTMLOptionElement.js';
78
import { beforeEach, describe, it, expect } from 'vitest';
89
import QuerySelector from '../../src/query-selector/QuerySelector.js';
910

@@ -970,6 +971,30 @@ describe('QuerySelector', () => {
970971
expect((<HTMLInputElement>elements[0]).value).toBe('two');
971972
});
972973

974+
it('Returns all option elements matching "option:checked".', () => {
975+
const container = document.createElement('div');
976+
container.innerHTML = `
977+
<select id="s">
978+
<option value="a">a</option>
979+
<option value="b">b</option>
980+
</select>
981+
`;
982+
document.body.appendChild(container);
983+
984+
const optionB = <HTMLOptionElement>container.querySelectorAll('option')[1];
985+
986+
// Warms the cache for this exact selector string while nothing is selected.
987+
expect(container.querySelectorAll("option[value='b']:checked").length).toBe(0);
988+
989+
optionB.setAttribute('selected', '');
990+
991+
// Same selector string as above - must reflect the new selection, not a stale cached result.
992+
const elements = container.querySelectorAll("option[value='b']:checked");
993+
994+
expect(elements.length).toBe(1);
995+
expect((<HTMLOptionElement>elements[0]).value).toBe('b');
996+
});
997+
973998
it('Returns all elements matching ":disabled".', () => {
974999
const container = document.createElement('div');
9751000
container.innerHTML = `

0 commit comments

Comments
 (0)