LibWeb: Give Element a CustomStateSet, exposed by ElementInternals

This commit is contained in:
Sam Atkins 2025-07-04 14:03:44 +01:00 committed by Tim Ledbetter
parent e63d81b36e
commit b6ffea8990
Notes: github-actions[bot] 2025-07-04 17:11:47 +00:00
10 changed files with 209 additions and 1 deletions

View file

@ -46,6 +46,7 @@
#include <LibWeb/HTML/CustomElements/CustomElementName.h>
#include <LibWeb/HTML/CustomElements/CustomElementReactionNames.h>
#include <LibWeb/HTML/CustomElements/CustomElementRegistry.h>
#include <LibWeb/HTML/CustomElements/CustomStateSet.h>
#include <LibWeb/HTML/EventLoop/EventLoop.h>
#include <LibWeb/HTML/HTMLAnchorElement.h>
#include <LibWeb/HTML/HTMLAreaElement.h>
@ -118,6 +119,7 @@ void Element::visit_edges(Cell::Visitor& visitor)
visitor.visit(m_class_list);
visitor.visit(m_shadow_root);
visitor.visit(m_custom_element_definition);
visitor.visit(m_custom_state_set);
visitor.visit(m_cascaded_properties);
visitor.visit(m_computed_properties);
if (m_pseudo_element_data) {
@ -3824,6 +3826,13 @@ auto Element::ensure_custom_element_reaction_queue() -> CustomElementReactionQue
return *m_custom_element_reaction_queue;
}
HTML::CustomStateSet& Element::ensure_custom_state_set()
{
if (!m_custom_state_set)
m_custom_state_set = HTML::CustomStateSet::create(realm(), *this);
return *m_custom_state_set;
}
CSS::StyleSheetList& Element::document_or_shadow_root_style_sheets()
{
auto& root_node = root();

View file

@ -351,6 +351,9 @@ public:
CustomElementReactionQueue const* custom_element_reaction_queue() const { return m_custom_element_reaction_queue; }
CustomElementReactionQueue& ensure_custom_element_reaction_queue();
HTML::CustomStateSet const* custom_state_set() const { return m_custom_state_set; }
HTML::CustomStateSet& ensure_custom_state_set();
JS::ThrowCompletionOr<void> upgrade_element(GC::Ref<HTML::CustomElementDefinition> custom_element_definition);
void try_to_upgrade();
@ -587,6 +590,9 @@ private:
// https://dom.spec.whatwg.org/#concept-element-is-value
Optional<String> m_is_value;
// https://html.spec.whatwg.org/multipage/custom-elements.html#states-set
GC::Ptr<HTML::CustomStateSet> m_custom_state_set;
// https://www.w3.org/TR/intersection-observer/#dom-element-registeredintersectionobservers-slot
// Element objects have an internal [[RegisteredIntersectionObservers]] slot, which is initialized to an empty list.
OwnPtr<Vector<IntersectionObserver::IntersectionObserverRegistration>> m_registered_intersection_observers;

View file

@ -208,6 +208,13 @@ WebIDL::ExceptionOr<GC::Ptr<DOM::NodeList>> ElementInternals::labels()
return WebIDL::NotSupportedError::create(realm(), "FIXME: ElementInternals::labels()"_string);
}
// https://html.spec.whatwg.org/multipage/custom-elements.html#dom-elementinternals-states
GC::Ptr<CustomStateSet> ElementInternals::states()
{
// The states getter steps are to return this's target element's states set.
return m_target_element->ensure_custom_state_set();
}
void ElementInternals::initialize(JS::Realm& realm)
{
WEB_SET_PROTOTYPE_FOR_INTERFACE(ElementInternals);

View file

@ -57,6 +57,7 @@ public:
WebIDL::ExceptionOr<bool> report_validity() const;
WebIDL::ExceptionOr<GC::Ptr<DOM::NodeList>> labels();
GC::Ptr<CustomStateSet> states();
private:
explicit ElementInternals(JS::Realm&, HTMLElement& target_element);

View file

@ -1,4 +1,5 @@
#import <DOM/ShadowRoot.idl>
#import <HTML/CustomElements/CustomStateSet.idl>
// https://html.spec.whatwg.org/multipage/custom-elements.html#elementinternals
[Exposed=Window]
@ -24,7 +25,7 @@ interface ElementInternals {
readonly attribute NodeList labels;
// Custom state pseudo-class
[FIXME, SameObject] readonly attribute CustomStateSet states;
[SameObject] readonly attribute CustomStateSet states;
};
// Accessibility semantics

View file

@ -0,0 +1,9 @@
Harness status: OK
Found 4 tests
4 Pass
Pass CustomStateSet behavior of ElementInternals.states: Initial state
Pass CustomStateSet behavior of ElementInternals.states: Exceptions
Pass CustomStateSet behavior of ElementInternals.states: Modifications
Pass Updating a CustomStateSet while iterating it should work

View file

@ -0,0 +1,6 @@
Harness status: OK
Found 1 tests
1 Pass
Pass customstateset doesn't crash after GC on detached node

View file

@ -0,0 +1,52 @@
/**
* Does a best-effort attempt at invoking garbage collection. Attempts to use
* the standardized `TestUtils.gc()` function, but falls back to other
* environment-specific nonstandard functions, with a final result of just
* creating a lot of garbage (in which case you will get a console warning).
*
* This should generally only be used to attempt to trigger bugs and crashes
* inside tests, i.e. cases where if garbage collection happened, then this
* should not trigger some misbehavior. You cannot rely on garbage collection
* successfully trigger, or that any particular unreachable object will be
* collected.
*
* @returns {Promise<undefined>} A promise you should await to ensure garbage
* collection has had a chance to complete.
*/
self.garbageCollect = async () => {
// https://testutils.spec.whatwg.org/#the-testutils-namespace
if (self.TestUtils?.gc) {
return TestUtils.gc();
}
// Use --expose_gc for V8 (and Node.js)
// to pass this flag at chrome launch use: --js-flags="--expose-gc"
// Exposed in SpiderMonkey shell as well
if (self.gc) {
return self.gc();
}
// Present in some WebKit development environments
if (self.GCController) {
return GCController.collect();
}
console.warn(
'Tests are running without the ability to do manual garbage collection. ' +
'They will still work, but coverage will be suboptimal.');
for (var i = 0; i < 1000; i++) {
gcRec(10);
}
function gcRec(n) {
if (n < 1) {
return {};
}
let temp = { i: "ab" + i + i / 100000 };
temp += "foo";
gcRec(n - 1);
}
};

View file

@ -0,0 +1,83 @@
<!DOCTYPE html>
<link rel=help href="https://html.spec.whatwg.org/multipage/custom-elements.html#custom-state-pseudo-class">
<script src="../../resources/testharness.js"></script>
<script src="../../resources/testharnessreport.js"></script>
<script>
class TestElement extends HTMLElement {
constructor() {
super();
this._internals = this.attachInternals();
}
get internals() {
return this._internals;
}
}
customElements.define("test-element", TestElement);
test(() => {
let i = (new TestElement()).internals;
assert_true(i.states instanceof CustomStateSet);
assert_equals(i.states.size, 0);
assert_false(i.states.has('foo'));
assert_false(i.states.has('--foo'));
assert_equals(i.states.toString(), '[object CustomStateSet]');
}, 'CustomStateSet behavior of ElementInternals.states: Initial state');
test(() => {
let i = (new TestElement()).internals;
assert_throws_js(TypeError, () => { i.states.supports('foo'); });
i.states.add(''); // should not throw.
i.states.add('--a\tb'); // should not throw.
}, 'CustomStateSet behavior of ElementInternals.states: Exceptions');
test(() => {
let i = (new TestElement()).internals;
i.states.add('--foo');
i.states.add('--bar');
i.states.add('--foo');
assert_equals(i.states.size, 2);
assert_true(i.states.has('--foo'));
assert_true(i.states.has('--bar'));
assert_array_equals([...i.states], ['--foo', '--bar']);
i.states.delete('--foo');
assert_array_equals([...i.states], ['--bar']);
i.states.add('--foo');
assert_array_equals([...i.states], ['--bar', '--foo']);
i.states.delete('--bar');
i.states.add('--baz');
assert_array_equals([...i.states], ['--foo', '--baz']);
}, 'CustomStateSet behavior of ElementInternals.states: Modifications');
test(() => {
let i = (new TestElement()).internals;
i.states.add('--one');
i.states.add('--two');
i.states.add('--three');
let iter = i.states.values();
// Delete the next item.
i.states.delete('--one');
let item = iter.next();
assert_false(item.done);
assert_equals(item.value, '--two');
// Clear the set.
i.states.clear();
item = iter.next();
assert_true(item.done);
// Delete the previous item.
i.states.add('--one');
i.states.add('--two');
i.states.add('--three');
iter = i.states.values();
item = iter.next();
assert_equals(item.value, '--one');
i.states.delete('--one');
item = iter.next();
assert_equals(item.value, '--two');
}, 'Updating a CustomStateSet while iterating it should work');
</script>

View file

@ -0,0 +1,34 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="timeout" content="long">
<meta name="author" title="Keith Cirkel" href="mailto:wpt@keithcirkel.co.uk" />
<link rel="help" href="https://html.spec.whatwg.org/multipage/custom-elements.html#custom-state-pseudo-class" />
<title>CustomStateSet doesn't crash after GC on detached node</title>
<script src="../../resources/testharness.js"></script>
<script src="../../resources/testharnessreport.js"></script>
<script src="../../common/gc.js"></script>
</head>
<body>
<custom-state id="myCE"></custom-state>
<script>
customElements.define('custom-state', class extends HTMLElement {
connectedCallback() {
this.elementInternals = this.attachInternals();
}
});
promise_test(async function() {
const states = myCE.elementInternals.states;
myCE.remove();
await garbageCollect();
states.add('still-works');
assert_equals(states.size, 1);
assert_true(states.delete('still-works'));
assert_equals(states.size, 0);
}, "customstateset doesn't crash after GC on detached node");
</script>
</body>
</html>