From 60bbcbca245ee6eeee7f31ac805600eee5de4b28 Mon Sep 17 00:00:00 2001
From: jwxbond
Date: Wed, 28 Jan 2026 21:49:10 +0800
Subject: [PATCH 01/16] fix(css-selector): order exported styles by cascade
priority, add `has-basic.ts` tests and snapshots
---
.../css/check_pseudo_has_argument_context.h | 235 ++++++++-
.../css/check_pseudo_has_traversal_iterator.h | 86 +++-
bridge/core/css/resolver/style_cascade.cc | 15 +-
bridge/core/dom/element.cc | 46 +-
docs/CSS_SELECTORS_L4_HAS_PLAN.md | 117 +++++
docs/TAILWINDCSS_SUPPORT_PLAN.md | 6 +-
.../css-selectors/has-basic.ts.0939efdd1.png | Bin 0 -> 2412 bytes
.../css-selectors/has-basic.ts.18ff8aed1.png | Bin 0 -> 2415 bytes
.../css-selectors/has-basic.ts.2f79ef791.png | Bin 0 -> 2421 bytes
.../css-selectors/has-basic.ts.30aca6431.png | Bin 0 -> 2408 bytes
.../css-selectors/has-basic.ts.3eb0b88a1.png | Bin 0 -> 2406 bytes
.../css-selectors/has-basic.ts.408ddde91.png | Bin 0 -> 2418 bytes
.../css-selectors/has-basic.ts.5b0607b81.png | Bin 0 -> 2402 bytes
.../css-selectors/has-basic.ts.5ce78b901.png | Bin 0 -> 2403 bytes
.../css-selectors/has-basic.ts.6ed3146e1.png | Bin 0 -> 2418 bytes
.../css-selectors/has-basic.ts.79d56e171.png | Bin 0 -> 4298 bytes
.../css-selectors/has-basic.ts.85a326141.png | Bin 0 -> 2415 bytes
.../css-selectors/has-basic.ts.91127d8c1.png | Bin 0 -> 2428 bytes
.../css-selectors/has-basic.ts.95c226b01.png | Bin 0 -> 2412 bytes
.../css-selectors/has-basic.ts.c713df221.png | Bin 0 -> 2761 bytes
.../css-selectors/has-basic.ts.d395e40c1.png | Bin 0 -> 3399 bytes
.../css-selectors/has-basic.ts.e06d14041.png | Bin 0 -> 2407 bytes
.../css-selectors/has-basic.ts.f2c499201.png | Bin 0 -> 2406 bytes
.../css-selectors/has-basic.ts.f70261ee1.png | Bin 0 -> 2422 bytes
.../css-selectors/has-basic.ts.f89e13111.png | Bin 0 -> 2402 bytes
.../css-selectors/has-basic.ts.f93e75831.png | Bin 0 -> 2411 bytes
.../css-selectors/has-basic.ts.fe50857e1.png | Bin 0 -> 2424 bytes
.../specs/css/css-selectors/has-basic.ts | 463 ++++++++++++++++++
28 files changed, 933 insertions(+), 35 deletions(-)
create mode 100644 docs/CSS_SELECTORS_L4_HAS_PLAN.md
create mode 100644 integration_tests/snapshots/css/css-selectors/has-basic.ts.0939efdd1.png
create mode 100644 integration_tests/snapshots/css/css-selectors/has-basic.ts.18ff8aed1.png
create mode 100644 integration_tests/snapshots/css/css-selectors/has-basic.ts.2f79ef791.png
create mode 100644 integration_tests/snapshots/css/css-selectors/has-basic.ts.30aca6431.png
create mode 100644 integration_tests/snapshots/css/css-selectors/has-basic.ts.3eb0b88a1.png
create mode 100644 integration_tests/snapshots/css/css-selectors/has-basic.ts.408ddde91.png
create mode 100644 integration_tests/snapshots/css/css-selectors/has-basic.ts.5b0607b81.png
create mode 100644 integration_tests/snapshots/css/css-selectors/has-basic.ts.5ce78b901.png
create mode 100644 integration_tests/snapshots/css/css-selectors/has-basic.ts.6ed3146e1.png
create mode 100644 integration_tests/snapshots/css/css-selectors/has-basic.ts.79d56e171.png
create mode 100644 integration_tests/snapshots/css/css-selectors/has-basic.ts.85a326141.png
create mode 100644 integration_tests/snapshots/css/css-selectors/has-basic.ts.91127d8c1.png
create mode 100644 integration_tests/snapshots/css/css-selectors/has-basic.ts.95c226b01.png
create mode 100644 integration_tests/snapshots/css/css-selectors/has-basic.ts.c713df221.png
create mode 100644 integration_tests/snapshots/css/css-selectors/has-basic.ts.d395e40c1.png
create mode 100644 integration_tests/snapshots/css/css-selectors/has-basic.ts.e06d14041.png
create mode 100644 integration_tests/snapshots/css/css-selectors/has-basic.ts.f2c499201.png
create mode 100644 integration_tests/snapshots/css/css-selectors/has-basic.ts.f70261ee1.png
create mode 100644 integration_tests/snapshots/css/css-selectors/has-basic.ts.f89e13111.png
create mode 100644 integration_tests/snapshots/css/css-selectors/has-basic.ts.f93e75831.png
create mode 100644 integration_tests/snapshots/css/css-selectors/has-basic.ts.fe50857e1.png
create mode 100644 integration_tests/specs/css/css-selectors/has-basic.ts
diff --git a/bridge/core/css/check_pseudo_has_argument_context.h b/bridge/core/css/check_pseudo_has_argument_context.h
index 242e0f2ac7..f14571e1ba 100644
--- a/bridge/core/css/check_pseudo_has_argument_context.h
+++ b/bridge/core/css/check_pseudo_has_argument_context.h
@@ -6,6 +6,8 @@
#define WEBF_CORE_CSS_CHECK_PSEUDO_HAS_ARGUMENT_CONTEXT_H_
#include "core/css/css_selector.h"
+#include "core/css/css_selector_list.h"
+#include "core/dom/has_invalidation_flags.h"
#include "foundation/macros.h"
#include
@@ -19,28 +21,229 @@ using Vector = std::vector;
class CheckPseudoHasArgumentContext {
WEBF_STACK_ALLOCATED();
public:
- explicit CheckPseudoHasArgumentContext(const CSSSelector* selector)
- : selector_(selector) {}
-
- CSSSelector::RelationType LeftmostRelation() const {
- // Return a default relation type
- return CSSSelector::kRelativeDescendant;
+ explicit CheckPseudoHasArgumentContext(const CSSSelector* selector)
+ : selector_(selector) {
+ BuildContext();
}
- int AdjacentDistanceLimit() const { return 0; }
- bool AdjacentDistanceFixed() const { return false; }
- int DepthLimit() const { return 0; }
- bool DepthFixed() const { return false; }
- bool AllowSiblingsAffectedByHas() const { return false; }
- unsigned GetSiblingsAffectedByHasFlags() const { return 0; }
- bool SiblingCombinatorAtRightmost() const { return false; }
- bool SiblingCombinatorBetweenChildOrDescendantCombinator() const { return false; }
- Vector GetPseudoHasArgumentHashes() const { return Vector(); }
+ CSSSelector::RelationType LeftmostRelation() const { return leftmost_relation_; }
+
+ int AdjacentDistanceLimit() const { return adjacent_distance_limit_; }
+ bool AdjacentDistanceFixed() const { return adjacent_distance_fixed_; }
+ int DepthLimit() const { return depth_limit_; }
+ bool DepthFixed() const { return depth_fixed_; }
+ bool AllowSiblingsAffectedByHas() const { return allow_siblings_affected_by_has_; }
+ unsigned GetSiblingsAffectedByHasFlags() const { return siblings_affected_by_has_flags_; }
+ bool SiblingCombinatorAtRightmost() const { return sibling_combinator_at_rightmost_; }
+ bool SiblingCombinatorBetweenChildOrDescendantCombinator() const {
+ return sibling_combinator_between_child_or_descendant_;
+ }
+ Vector GetPseudoHasArgumentHashes() const { return pseudo_has_argument_hashes_; }
private:
+ static inline bool IsRelativeRelation(CSSSelector::RelationType relation) {
+ return relation == CSSSelector::kRelativeDescendant || relation == CSSSelector::kRelativeChild ||
+ relation == CSSSelector::kRelativeDirectAdjacent || relation == CSSSelector::kRelativeIndirectAdjacent;
+ }
+
+ static inline bool IsAdjacentRelation(CSSSelector::RelationType relation) {
+ return relation == CSSSelector::kDirectAdjacent || relation == CSSSelector::kIndirectAdjacent ||
+ relation == CSSSelector::kRelativeDirectAdjacent || relation == CSSSelector::kRelativeIndirectAdjacent;
+ }
+
+ static inline bool IsIndirectAdjacent(CSSSelector::RelationType relation) {
+ return relation == CSSSelector::kIndirectAdjacent || relation == CSSSelector::kRelativeIndirectAdjacent;
+ }
+
+ static inline bool IsRelativeAdjacent(CSSSelector::RelationType relation) {
+ return relation == CSSSelector::kRelativeDirectAdjacent || relation == CSSSelector::kRelativeIndirectAdjacent;
+ }
+
+ static inline bool IsDescendantRelation(CSSSelector::RelationType relation) {
+ return relation == CSSSelector::kDescendant || relation == CSSSelector::kRelativeDescendant;
+ }
+
+ static inline bool IsChildRelation(CSSSelector::RelationType relation) {
+ return relation == CSSSelector::kChild || relation == CSSSelector::kRelativeChild;
+ }
+
+ static inline bool IsChildOrDescendantRelation(CSSSelector::RelationType relation) {
+ return IsDescendantRelation(relation) || IsChildRelation(relation);
+ }
+
+ void BuildContext() {
+ leftmost_relation_ = CSSSelector::kRelativeDescendant;
+ adjacent_distance_limit_ = 0;
+ adjacent_distance_fixed_ = true;
+ depth_limit_ = 0;
+ depth_fixed_ = true;
+ allow_siblings_affected_by_has_ = false;
+ siblings_affected_by_has_flags_ = kNoSiblingsAffectedByHasFlags;
+ sibling_combinator_at_rightmost_ = false;
+ sibling_combinator_between_child_or_descendant_ = false;
+ pseudo_has_argument_hashes_.clear();
+
+ if (!selector_) {
+ return;
+ }
+
+ // Collect relations from rightmost to leftmost (towards the relative anchor).
+ Vector relations;
+ bool found_relative_relation = false;
+ for (const CSSSelector* current = selector_; current; current = current->NextSimpleSelector()) {
+ AddHashesForSimpleSelector(*current);
+
+ CSSSelector::RelationType relation = current->Relation();
+ if (relation == CSSSelector::kSubSelector || relation == CSSSelector::kScopeActivation) {
+ continue;
+ }
+ relations.push_back(relation);
+ if (IsRelativeRelation(relation)) {
+ found_relative_relation = true;
+ break;
+ }
+ }
+
+ if (relations.empty()) {
+ return;
+ }
+
+ leftmost_relation_ = relations.back();
+ if (!found_relative_relation) {
+ // Be defensive: if the parser didn't convert the leftmost combinator
+ // to a relative relation, map it here so :has() matching can proceed.
+ leftmost_relation_ = ConvertRelationToRelative(leftmost_relation_);
+ }
+ sibling_combinator_at_rightmost_ = IsAdjacentRelation(relations.front());
+
+ // Determine if an adjacent combinator appears between child/descendant combinators.
+ bool seen_child_or_descendant_to_right = false;
+ for (const auto& relation : relations) {
+ if (IsChildOrDescendantRelation(relation)) {
+ seen_child_or_descendant_to_right = true;
+ continue;
+ }
+ if (IsAdjacentRelation(relation) && seen_child_or_descendant_to_right) {
+ sibling_combinator_between_child_or_descendant_ = true;
+ }
+ }
+
+ // Compute adjacency and depth constraints from leftmost (anchor side) to rightmost.
+ bool in_sibling_phase = IsRelativeAdjacent(leftmost_relation_);
+ for (auto it = relations.rbegin(); it != relations.rend(); ++it) {
+ CSSSelector::RelationType relation = *it;
+ if (IsChildOrDescendantRelation(relation)) {
+ depth_limit_++;
+ if (IsDescendantRelation(relation)) {
+ depth_fixed_ = false;
+ }
+ in_sibling_phase = false;
+ continue;
+ }
+
+ if (IsAdjacentRelation(relation) && in_sibling_phase) {
+ if (IsIndirectAdjacent(relation)) {
+ // Indirect adjacency is unbounded.
+ adjacent_distance_fixed_ = false;
+ adjacent_distance_limit_ = 0;
+ } else if (adjacent_distance_fixed_) {
+ adjacent_distance_limit_++;
+ }
+ }
+ }
+
+ allow_siblings_affected_by_has_ = IsRelativeAdjacent(leftmost_relation_);
+ if (allow_siblings_affected_by_has_) {
+ siblings_affected_by_has_flags_ =
+ depth_limit_ > 0 ? kFlagForSiblingDescendantRelationship : kFlagForSiblingRelationship;
+ }
+ }
+
+ void AddHash(unsigned hash) {
+ if (hash) {
+ pseudo_has_argument_hashes_.push_back(hash);
+ }
+ }
+
+ void AddHashesForSelectorList(const CSSSelector* selector_list_first) {
+ if (!selector_list_first) {
+ return;
+ }
+ for (const CSSSelector* complex = selector_list_first; complex; complex = CSSSelectorList::Next(*complex)) {
+ for (const CSSSelector* simple = complex; simple; simple = simple->NextSimpleSelector()) {
+ AddHashesForSimpleSelector(*simple);
+ }
+ }
+ }
+
+ void AddHashesForSimpleSelector(const CSSSelector& selector) {
+ switch (selector.Match()) {
+ case CSSSelector::kTag:
+ if (selector.TagQName().LocalName() != CSSSelector::UniversalSelectorAtom()) {
+ AddHash(selector.TagQName().LocalName().Hash() * kTagSalt);
+ }
+ break;
+ case CSSSelector::kId:
+ AddHash(selector.Value().Hash() * kIdSalt);
+ break;
+ case CSSSelector::kClass:
+ AddHash(selector.Value().Hash() * kClassSalt);
+ break;
+ case CSSSelector::kAttributeExact:
+ case CSSSelector::kAttributeSet:
+ case CSSSelector::kAttributeList:
+ case CSSSelector::kAttributeHyphen:
+ case CSSSelector::kAttributeContain:
+ case CSSSelector::kAttributeBegin:
+ case CSSSelector::kAttributeEnd:
+ AddHash(selector.Attribute().LocalName().Hash() * kAttributeSalt);
+ break;
+ case CSSSelector::kPseudoClass: {
+ CSSSelector::PseudoType pseudo_type = selector.GetPseudoType();
+ switch (pseudo_type) {
+ case CSSSelector::kPseudoNot:
+ case CSSSelector::kPseudoIs:
+ case CSSSelector::kPseudoWhere:
+ case CSSSelector::kPseudoParent:
+ AddHashesForSelectorList(selector.SelectorListOrParent());
+ break;
+ case CSSSelector::kPseudoVisited:
+ case CSSSelector::kPseudoRelativeAnchor:
+ break;
+ default:
+ AddHash(static_cast(pseudo_type) * kPseudoSalt);
+ if (const CSSSelectorList* list = selector.SelectorList()) {
+ AddHashesForSelectorList(list->First());
+ }
+ break;
+ }
+ break;
+ }
+ default:
+ break;
+ }
+ }
+
+ static constexpr unsigned kClassSalt = 13;
+ static constexpr unsigned kIdSalt = 29;
+ static constexpr unsigned kTagSalt = 7;
+ static constexpr unsigned kAttributeSalt = 19;
+ static constexpr unsigned kPseudoSalt = 23;
+
const CSSSelector* selector_;
+
+ CSSSelector::RelationType leftmost_relation_;
+ int adjacent_distance_limit_;
+ bool adjacent_distance_fixed_;
+ int depth_limit_;
+ bool depth_fixed_;
+ bool allow_siblings_affected_by_has_;
+ unsigned siblings_affected_by_has_flags_;
+ bool sibling_combinator_at_rightmost_;
+ bool sibling_combinator_between_child_or_descendant_;
+ Vector pseudo_has_argument_hashes_;
};
} // namespace webf
-#endif // WEBF_CORE_CSS_CHECK_PSEUDO_HAS_ARGUMENT_CONTEXT_H_
\ No newline at end of file
+#endif // WEBF_CORE_CSS_CHECK_PSEUDO_HAS_ARGUMENT_CONTEXT_H_
diff --git a/bridge/core/css/check_pseudo_has_traversal_iterator.h b/bridge/core/css/check_pseudo_has_traversal_iterator.h
index efe0e40fc1..9fe5d544f8 100644
--- a/bridge/core/css/check_pseudo_has_traversal_iterator.h
+++ b/bridge/core/css/check_pseudo_has_traversal_iterator.h
@@ -6,8 +6,10 @@
#define WEBF_CORE_CSS_CHECK_PSEUDO_HAS_TRAVERSAL_ITERATOR_H_
#include "core/dom/element.h"
+#include "core/dom/element_traversal.h"
#include "core/css/check_pseudo_has_argument_context.h"
#include "foundation/macros.h"
+#include
#include
namespace webf {
@@ -22,16 +24,90 @@ class CheckPseudoHasArgumentTraversalIterator {
public:
CheckPseudoHasArgumentTraversalIterator(Element& element,
const CheckPseudoHasArgumentContext& context)
- : current_(&element), depth_(0) {}
+ : context_(context) {
+ Initialize(element);
+ }
bool AtEnd() const { return current_ == nullptr; }
- void operator++() { current_ = nullptr; } // Stub - stop after first element
+ void operator++() { Advance(); }
Element* CurrentElement() const { return current_; }
int CurrentDepth() const { return depth_; }
private:
- Element* current_;
- int depth_;
+ struct TraversalEntry {
+ Element* element;
+ int depth;
+ };
+
+ void Initialize(Element& anchor) {
+ stack_.clear();
+
+ const CSSSelector::RelationType leftmost_relation = context_.LeftmostRelation();
+ if (leftmost_relation == CSSSelector::kRelativeDirectAdjacent ||
+ leftmost_relation == CSSSelector::kRelativeIndirectAdjacent) {
+ int distance = 0;
+ for (Element* sibling = ElementTraversal::NextSibling(anchor); sibling;
+ sibling = ElementTraversal::NextSibling(*sibling)) {
+ distance++;
+ if (context_.AdjacentDistanceFixed() && context_.AdjacentDistanceLimit() > 0 &&
+ distance > context_.AdjacentDistanceLimit()) {
+ break;
+ }
+ stack_.push_back({sibling, 0});
+ if (context_.AdjacentDistanceFixed() && context_.AdjacentDistanceLimit() > 0 &&
+ distance == context_.AdjacentDistanceLimit()) {
+ break;
+ }
+ }
+ } else {
+ for (Element* child = ElementTraversal::FirstChild(anchor); child;
+ child = ElementTraversal::NextSibling(*child)) {
+ stack_.push_back({child, 1});
+ }
+ }
+
+ std::reverse(stack_.begin(), stack_.end());
+ Advance();
+ }
+
+ bool ShouldDescendFrom(int depth) const {
+ if (context_.DepthLimit() == 0) {
+ return false;
+ }
+ if (context_.DepthFixed() && depth >= context_.DepthLimit()) {
+ return false;
+ }
+ return true;
+ }
+
+ void PushChildren(Element& element, int depth) {
+ if (!ShouldDescendFrom(depth)) {
+ return;
+ }
+ for (Element* child = ElementTraversal::LastChild(element); child;
+ child = ElementTraversal::PreviousSibling(*child)) {
+ stack_.push_back({child, depth + 1});
+ }
+ }
+
+ void Advance() {
+ if (stack_.empty()) {
+ current_ = nullptr;
+ return;
+ }
+ TraversalEntry entry = stack_.back();
+ stack_.pop_back();
+ current_ = entry.element;
+ depth_ = entry.depth;
+ if (current_) {
+ PushChildren(*current_, depth_);
+ }
+ }
+
+ const CheckPseudoHasArgumentContext& context_;
+ Vector stack_;
+ Element* current_{nullptr};
+ int depth_{0};
};
// Stub implementation of CheckPseudoHasFastRejectFilter
@@ -50,4 +126,4 @@ class CheckPseudoHasFastRejectFilter {
} // namespace webf
-#endif // WEBF_CORE_CSS_CHECK_PSEUDO_HAS_TRAVERSAL_ITERATOR_H_
\ No newline at end of file
+#endif // WEBF_CORE_CSS_CHECK_PSEUDO_HAS_TRAVERSAL_ITERATOR_H_
diff --git a/bridge/core/css/resolver/style_cascade.cc b/bridge/core/css/resolver/style_cascade.cc
index 085d1d2a27..5c70ac05b1 100644
--- a/bridge/core/css/resolver/style_cascade.cc
+++ b/bridge/core/css/resolver/style_cascade.cc
@@ -401,7 +401,7 @@ std::shared_ptr StyleCascade::BuildWinningPropertySe
CSSPropertyName name;
std::shared_ptr value;
bool important;
- uint32_t position;
+ CascadePriority priority;
};
std::vector exported_properties;
@@ -461,17 +461,16 @@ std::shared_ptr StyleCascade::BuildWinningPropertySe
// so the UICommand receives the original logical names unchanged.
CSSPropertyName declared_name = prop_ref.Name();
exported_properties.push_back(
- ExportedProperty{id, declared_name, to_set, important, pos});
+ ExportedProperty{id, declared_name, to_set, important, *prio});
}
- // Sort by encoded position so that later declarations (including shorthands)
- // are emitted after earlier ones, matching cascade/source order. This is
- // important for consumers like the Dart style engine that replay SetStyle
- // commands sequentially.
+ // Sort by cascade priority so higher-priority declarations are emitted last.
+ // This keeps shorthand/longhand interactions correct when Dart replays
+ // SetStyle commands sequentially without full cascade context.
std::sort(exported_properties.begin(), exported_properties.end(),
[](const ExportedProperty& a, const ExportedProperty& b) {
- if (a.position != b.position) {
- return a.position < b.position;
+ if (a.priority != b.priority) {
+ return a.priority < b.priority;
}
// Tie-breaker: keep deterministic order by property id.
return static_cast(a.id) < static_cast(b.id);
diff --git a/bridge/core/dom/element.cc b/bridge/core/dom/element.cc
index e5843f263d..c2dfd79338 100644
--- a/bridge/core/dom/element.cc
+++ b/bridge/core/dom/element.cc
@@ -18,6 +18,7 @@
#include "child_list_mutation_scope.h"
#include "comment.h"
#include "core/css/css_identifier_value.h"
+#include "core/css/css_selector_list.h"
#include "core/css/css_property_value_set.h"
#include "core/css/css_selector_list.h"
#include "core/css/css_style_sheet.h"
@@ -27,12 +28,14 @@
#include "core/css/parser/css_parser.h"
#include "core/css/parser/css_parser_context.h"
#include "core/css/selector_checker.h"
+#include "core/css/parser/css_parser_context.h"
#include "core/css/style_recalc_change.h"
#include "core/css/style_recalc_context.h"
#include "core/css/style_scope_frame.h"
#include "core/css/style_scope_data.h"
#include "core/css/style_engine.h"
#include "core/css/style_sheet_contents.h"
+#include "core/css/selector_checker.h"
#include "core/css/white_space.h"
#include "core/dom/document_fragment.h"
#include "core/dom/element_rare_data_vector.h"
@@ -158,6 +161,43 @@ bool IsPotentiallyDisableableFormControl(const Element& element) {
tag_name == "option";
}
+std::shared_ptr ParseSelectorListOrThrow(const AtomicString& selectors,
+ ExceptionState& exception_state,
+ JSContext* ctx) {
+ auto parser_context = std::make_shared(kHTMLStandardMode);
+ auto sheet = std::make_shared(parser_context);
+
+ std::vector arena;
+ tcb::span vector =
+ CSSParser::ParseSelector(parser_context, CSSNestingType::kNone, /*parent_rule_for_nesting=*/nullptr, sheet,
+ selectors.GetString(), arena);
+
+ auto selector_list = CSSSelectorList::AdoptSelectorVector(vector);
+ if (!selector_list->IsValid()) {
+ exception_state.ThrowException(ctx, ErrorType::SyntaxError,
+ "'" + selectors.ToUTF8String() + "' is not a valid selector.");
+ return nullptr;
+ }
+ return selector_list;
+}
+
+bool MatchesAnySelectorInList(Element& element,
+ const CSSSelectorList& selector_list,
+ const ContainerNode* scope) {
+ SelectorChecker checker(SelectorChecker::kQueryingRules);
+ SelectorChecker::SelectorCheckingContext context(&element);
+ context.scope = scope;
+
+ for (const CSSSelector* selector = selector_list.First(); selector; selector = CSSSelectorList::Next(*selector)) {
+ context.selector = selector;
+ SelectorChecker::MatchResult result;
+ if (checker.Match(context, result)) {
+ return true;
+ }
+ }
+ return false;
+}
+
} // namespace
AttributeCollection Element::Attributes() const {
@@ -570,7 +610,7 @@ bool Element::matches(const AtomicString& selectors, ExceptionState& exception_s
if (!selector_list) {
return false;
}
- return MatchesAnySelectorInList(*this, *selector_list, *this);
+ return MatchesAnySelectorInList(*this, *selector_list, this);
}
NativeValue arguments[] = {NativeValueConverter::ToNativeValue(ctx(), selectors)};
@@ -588,9 +628,9 @@ Element* Element::closest(const AtomicString& selectors, ExceptionState& excepti
if (!selector_list) {
return nullptr;
}
-
+ ContainerNode* scope = this;
for (Element* current = this; current; current = current->parentElement()) {
- if (MatchesAnySelectorInList(*current, *selector_list, *this)) {
+ if (MatchesAnySelectorInList(*current, *selector_list, scope)) {
return current;
}
}
diff --git a/docs/CSS_SELECTORS_L4_HAS_PLAN.md b/docs/CSS_SELECTORS_L4_HAS_PLAN.md
new file mode 100644
index 0000000000..2b1a9d943b
--- /dev/null
+++ b/docs/CSS_SELECTORS_L4_HAS_PLAN.md
@@ -0,0 +1,117 @@
+# CSS Selectors Level 4 — :has() Support Plan (C++ Blink/bridge)
+
+Goal: implement correct `:has()` matching + invalidation in the Blink/bridge C++ selector engine.
+
+Status (Jan 28, 2026):
+- Parsing + selector wiring exists (`CSSSelector::kPseudoHas`, `SelectorChecker::CheckPseudoHas`).
+- Traversal, argument context, cache, and fast‑reject are **stubbed**:
+ - `bridge/core/css/check_pseudo_has_argument_context.h`
+ - `bridge/core/css/check_pseudo_has_traversal_iterator.h`
+ - `bridge/core/css/check_pseudo_has_cache_scope.h`
+- Element invalidation flags are declared but **no-op** in `bridge/core/dom/element.h`.
+- Style invalidation hooks for `:has()` are **not implemented** in `StyleEngine`.
+
+---
+
+## Phase 1 — Argument context (correctness baseline)
+Implement `CheckPseudoHasArgumentContext` to compute:
+- Leftmost relation (`kRelativeDescendant`, `kRelativeChild`, `kRelativeDirectAdjacent`, `kRelativeIndirectAdjacent`).
+- Fixed vs unbounded depth and adjacency limits.
+- Whether siblings may be affected and which flags apply:
+ - `AllowSiblingsAffectedByHas()`
+ - `GetSiblingsAffectedByHasFlags()`
+- Hashes for fast‑reject: class/id/tag/attr/pseudo in the argument list.
+
+Files:
+- `bridge/core/css/check_pseudo_has_argument_context.h` (and a new .cc if needed)
+- `bridge/core/css/css_selector.h/.cc` (if new helper APIs are required)
+
+---
+
+## Phase 2 — Argument traversal iterator
+Replace stub traversal with real iteration:
+- Descendant/child traversal with depth tracking.
+- Sibling / adjacent traversal (`+`, `~`) with optional subtree descent.
+- Ensure traversal respects “relative selector” semantics (leftmost combinator).
+- Avoid duplicated traversal when multiple anchors exist (use the “reversed DOM order” guidance in `has_invalidation_flags.h`).
+
+Files:
+- `bridge/core/css/check_pseudo_has_traversal_iterator.h` (consider splitting into .cc)
+- `bridge/core/dom/element_traversal.h` or existing traversal helpers if needed
+
+---
+
+## Phase 3 — Cache + fast reject (perf‑safe)
+Implement `CheckPseudoHasCacheScope` to avoid repeated checks:
+- Per‑document cache keyed by (anchor element, selector hash).
+- Track checked/matched to short‑circuit repeated argument checks.
+Implement `CheckPseudoHasFastRejectFilter`:
+- Conservative bloom filter (or “always false” for FastReject as a correctness fallback).
+
+Files:
+- `bridge/core/css/check_pseudo_has_cache_scope.h`
+- `bridge/core/css/check_pseudo_has_traversal_iterator.h`
+
+---
+
+## Phase 4 — Element invalidation flags
+Wire `HasInvalidationFlags` storage into `Element`:
+- Add a struct field in `ElementRareDataVector` or `NodeRareData`.
+- Implement non‑no‑op setters/getters in `bridge/core/dom/element.h/.cc`:
+ - `SetAffectedBySubjectHas()`
+ - `SetAffectedByNonSubjectHas()`
+ - `SetAffectedByPseudoInHas()`
+ - `SetAffectedByLogicalCombinationsInHas()`
+ - `SetAncestorsOrAncestorSiblingsAffectedByHas()`
+ - `SetSiblingsAffectedByHasFlags()`
+ - `AffectedByMultipleHas()` / `SetAffectedByMultipleHas()`
+
+Files:
+- `bridge/core/dom/element.h/.cc`
+- `bridge/core/dom/element_rare_data_vector.h` or `bridge/core/dom/node_rare_data.h`
+
+---
+
+## Phase 5 — Style invalidation wiring
+Add `:has()` invalidation entry points to `StyleEngine` and DOM mutation paths.
+
+New `StyleEngine` APIs:
+- `InvalidateElementAffectedByHas(Element& changed)`
+- `InvalidateAncestorsOrSiblingsAffectedByHas(Element& changed)`
+
+Call sites:
+- `StyleEngine::IdChangedForElement`
+- `StyleEngine::ClassAttributeChangedForElement`
+- `StyleEngine::AttributeChangedForElement`
+- Insert/remove paths in `bridge/core/dom/container_node.cc`
+
+Rules:
+- Use `RuleFeatureSet::NeedsHasInvalidationFor*` to short‑circuit.
+- For subject `:has()`: mark anchor element `SetNeedsStyleRecalc(kLocalStyleChange, kAffectedByHas)`.
+- For non‑subject `:has()`: schedule descendant/sibling invalidation sets on the anchor element.
+
+Files:
+- `bridge/core/css/style_engine.h/.cc`
+- `bridge/core/dom/container_node.cc`
+
+---
+
+## Phase 6 — Tests
+Unit tests:
+- Selector parsing & matching in `bridge/core/css/css_selector_test.cc`:
+ - Descendant/child/sibling forms (`:has(.b)`, `:has(> .b)`, `:has(+ .b)`, `:has(~ .b)`).
+ - Nested logical combos: `:has(:is(.a .b))`, `:has(:where(.a .b))`.
+
+Integration tests:
+- Mutation invalidation scenarios:
+ - class add/remove on descendants/siblings
+ - sibling insertion/removal
+ - multiple anchors with overlapping scopes
+
+---
+
+## Build/Test checklist (C++)
+- Build: `npm run build:bridge:macos:arm64`
+- Unit: `node scripts/run_bridge_unit_test.js`
+- Integration: `cd integration_tests && npm run integration -- specs/css/css-selectors/has-*.ts`
+
diff --git a/docs/TAILWINDCSS_SUPPORT_PLAN.md b/docs/TAILWINDCSS_SUPPORT_PLAN.md
index e7d72b0096..c4586a934a 100644
--- a/docs/TAILWINDCSS_SUPPORT_PLAN.md
+++ b/docs/TAILWINDCSS_SUPPORT_PLAN.md
@@ -26,7 +26,7 @@ Target: **Tailwind CSS v3.4.x** (core preflight + core utilities + core variants
| `@supports` | `supports-*` variants | ❌ | `webf/lib/src/css/parser/parser.dart` returns `null` for `DIRECTIVE_SUPPORTS` | Implement `@supports` parsing + evaluation (at least allow/deny blocks, and “declare support” checks used by Tailwind). |
| Media queries (MQ) | Responsive (`sm/md/...`), `dark`, `motion-*`, `orientation-*`, `print`, `forced-colors`, `prefers-contrast` | ⚠️ | `webf/lib/src/css/css_rule.dart` only evaluates `min/max-width`, `min/max-aspect-ratio`, `prefers-color-scheme`; rejects `print` type | Expand media query parsing/evaluation for Tailwind variants: `prefers-reduced-motion`, `orientation`, `forced-colors`, `prefers-contrast`, `hover`, `pointer`, `print`. |
| Selector pseudo `:where()` | Preflight selectors (`abbr:where([title])`, `[hidden]:where(:not(...))`), direction variants (`rtl:`/`ltr:`) | ⚠️ | Blink/bridge (C++) supports `:where()` parsing/matching and 0-specificity (`bridge/core/css/css_selector.cc` handles `CSSSelector::kPseudoWhere`), but the legacy Dart selector engine still lacks selector-list parsing/matching (`webf/lib/src/css/parser/parser.dart`, `webf/lib/src/css/query_selector.dart`). | Implement Selectors L4 `:where()`/`:is()` selector-list parsing + matching + specificity in the Dart engine; keep coverage via `integration_tests/specs/css/css-selectors/is-where-*.ts`. |
-| Selector pseudo `:has()` | `has-*`, `group-has-*`, `peer-has-*` variants | ❌ | No `:has()` parsing/matching in Dart selector engine | Implement `:has()` parsing/matching + invalidation strategy (at least a correct baseline, then optimize). |
+| Selector pseudo `:has()` | `has-*`, `group-has-*`, `peer-has-*` variants | ⚠️ | Blink/bridge parses `:has()` and has selector-checking/invalidation hooks, but traversal/caching are stubbed (`bridge/core/css/check_pseudo_has_*`); Dart selector engine still lacks `:has()`. | Implement full :has traversal/caching/invalidation in C++; add Dart parsing/matching; cover with integration tests. |
| Pseudo-classes (state) | `hover:`, `focus:`, `focus-visible:`, `focus-within:`, `active:`, `enabled:`, `disabled:` | ❌ | `webf/lib/src/css/query_selector.dart` only implements a small set (e.g., `:root`, `:empty`, `:first-child`, `:nth-*`); does not implement hover/focus/active/etc | Add element state model + selector matching for Tailwind pseudo-class variants; hook to Flutter pointer/focus events. |
| Pseudo-classes (forms) | `checked:`, `indeterminate:`, `placeholder-shown:`, `required:`, `valid:`/`invalid:`… | ❌ | Not implemented in selector evaluator | Implement form state pseudos based on element type/attributes/value validity model (as supported by WebF’s form elements). |
| Pseudo-elements used by Tailwind variants | `::before/::after`, `::placeholder`, `::selection`, `::marker`, `::file-selector-button`, `::backdrop`, `::first-letter/line` | ⚠️ | WebF has real `::before/::after` elements and first-line/first-letter plumbing, but selector matcher treats only a “legacy” subset as matchable; others return false | Extend pseudo-element matching + rendering support where meaningful; at minimum `::placeholder` for preflight + utilities. |
@@ -49,7 +49,7 @@ Tailwind v3.4.18 core variants are exposed by `variantPlugins` (15 groups).
| `orientationVariants` | `@media (orientation: portrait/landscape)` | ❌ | Media feature not evaluated today. |
| `printVariant` | `@media print` | ❌ | WebF currently rejects non-`screen` media types. |
| `supportsVariants` | `@supports (...)` | ❌ | `@supports` not parsed/evaluated today. |
-| `hasVariants` | `:has(...)` | ❌ | Not parsed/evaluated today. |
+| `hasVariants` | `:has(...)` | ⚠️ | Blink/bridge has parser + checker wiring, but :has traversal/caching are stubbed; Dart selector engine lacks `:has()`. |
| `ariaVariants` | Attribute selectors like `&[aria-checked=\"true\"]` | ✅ | Attribute selectors are supported; needs Tailwind fixture tests. |
| `dataVariants` | Attribute selectors like `&[data-state=\"open\"]` | ✅ | Attribute selectors are supported; needs Tailwind fixture tests. |
| `childVariant` | `& > *` | ✅ | Combinators supported. |
@@ -66,7 +66,7 @@ Tailwind v3.4.18 core utilities are exposed by `corePlugins` (179 keys). Below i
**P0 blockers (must fix first)**
- Selector parsing/matching: `:where()` (Tailwind preflight + direction variants).
-- `:has()` correctness + performance + invalidation strategy.
+- `:has()` correctness + performance + invalidation strategy (C++ has stubbed traversal; Dart missing).
- Selector matching: interactive pseudo-classes (hover/focus/active/disabled/checked/…).
- https://github.com/openwebf/webf/issues/659
- Media query evaluation: `orientation`, `prefers-contrast`.
diff --git a/integration_tests/snapshots/css/css-selectors/has-basic.ts.0939efdd1.png b/integration_tests/snapshots/css/css-selectors/has-basic.ts.0939efdd1.png
new file mode 100644
index 0000000000000000000000000000000000000000..9d2b2359f11be5e33ce0bb54426521506826264b
GIT binary patch
literal 2412
zcmeAS@N?(olHy`uVBq!ia0y~yV9a1(U~1rC1Bz^vdbAu!F%}28J29*~C-V}>VJUX<
z4B-HR8jh3>1_n+sPZ!6KiaBrZTJs$;5O8qpDJrnoxw7fVdY?~CX{$8yVrI*BwkGx3xW3_u^&fFHfese-QQ-P6MMuSEpTZa(&)QZH_<_?oNr4+I%|8LCu
z{%`i$dvY8HJZCOoT*M-!;6(wI^Odc`$dTFQ<||%-)Fz4?3G(BntI`UmSa=1#ZoK+C
zvnN)6lO;dvgA|_&41r9%0;&{H`^$xHJ!CJ?5H@j`;=t^}IZBNN!DuQN%?P7q!Dw+f
gS|iZEHoU?dcS_~!)VG(Sfh{HmPgg&ebxsLQ0AROKpa1{>
literal 0
HcmV?d00001
diff --git a/integration_tests/snapshots/css/css-selectors/has-basic.ts.18ff8aed1.png b/integration_tests/snapshots/css/css-selectors/has-basic.ts.18ff8aed1.png
new file mode 100644
index 0000000000000000000000000000000000000000..bb8d2d1f1f72952d5b8bf3ca2049c0dc77baafb4
GIT binary patch
literal 2415
zcmeAS@N?(olHy`uVBq!ia0y~yV9a1(U~1rC1Bz^vdbAu!F%}28J29*~C-V}>VJUX<
z4B-HR8jh3>1_n+^PZ!6KiaBrZTJkkJ2rxLVKYIA%>FbO_jr{_bBSJadw*+RJ%`$vG
z$DE<)UAjC&!})1{b9>%y%$r|q``UxOFpxp-7z?j}>IH^CCh{rMoUd#dMvlxby6f$a
zCEtGc*F1V}tUyCINQl$KVTuF!)Xi7C0;x@G9hsXDnb`#3H5OMFF+%y~>uu{0~CZG8!}***b(qsnH-9O$DPFVYDn5Ee=O(
f1p3#8E7YS7seIj5FLNH)Xkzem^>bP0l+XkKHwfp=
literal 0
HcmV?d00001
diff --git a/integration_tests/snapshots/css/css-selectors/has-basic.ts.2f79ef791.png b/integration_tests/snapshots/css/css-selectors/has-basic.ts.2f79ef791.png
new file mode 100644
index 0000000000000000000000000000000000000000..67de1fc16e75f8e152b0092a1ae794728f9468e8
GIT binary patch
literal 2421
zcmeAS@N?(olHy`uVBq!ia0y~yV9a1(U~1rC1Bz^vdbAu!F%}28J29*~C-V}>VJUX<
z4B-HR8jh3>1_n-fPZ!6KiaBrZZsa=TAmZv6{^{Ybr`d{H61s=y9q>^Ni!
i^-$2TpiTz5!dzY>^S|}1VI{EX#Ng@b=d#Wzp$Py7cT*Dp
literal 0
HcmV?d00001
diff --git a/integration_tests/snapshots/css/css-selectors/has-basic.ts.30aca6431.png b/integration_tests/snapshots/css/css-selectors/has-basic.ts.30aca6431.png
new file mode 100644
index 0000000000000000000000000000000000000000..b0b1dd361d9b31e27a0e146c92797f3757026667
GIT binary patch
literal 2408
zcmeAS@N?(olHy`uVBq!ia0y~yV9a1(U~1rC1Bz^vdbAu!F%}28J29*~C-V}>VJUX<
z4B-HR8jh3>1_n+cPZ!6KiaBrZTJjxo5Mgy>`}ttsPXP%J9>rN^tGptV+}A#T(Qo;D
z595U5_`eJcHQV;??=7r(bNu|y^;JvcKQ3a3>0#v+P`$tq$V5J68uOJcW77i0McXdh
zpMQO?{xCOFL736>9~K#JKhabDEIr<
zzhFmtM^YAq6vn`)c1qLmXb6smAo(TWXzCeFJ)^0If+hs@G7x{Eqs;$k2P;)zYl*?r
L)z4*}Q$iB};8_OA
literal 0
HcmV?d00001
diff --git a/integration_tests/snapshots/css/css-selectors/has-basic.ts.3eb0b88a1.png b/integration_tests/snapshots/css/css-selectors/has-basic.ts.3eb0b88a1.png
new file mode 100644
index 0000000000000000000000000000000000000000..264d262b4ece5af5f412641980ab61de9c7b6a85
GIT binary patch
literal 2406
zcmeAS@N?(olHy`uVBq!ia0y~yV9a1(U~1rC1Bz^vdbAu!F%}28J29*~C-V}>VJUX<
z4B-HR8jh3>1_n+6PZ!6KiaBrZ8VWTzh_E_d`}E*WO>fTuf${~roENLkta7VAWBHuT
z;av7tW(EuMZMD|17mEM?PhMvt_s#ae=g12TflRytsuWO%L-QNdxTO?c)&5BBQRUy+
zW6yEGmkQ<{pWTx8gHa^{=(KfJ%q~AvhX>n|>kr!k+e!?cu6{1-
HoD!MVJUX<
z4B-HR8jh3>1_n+UPZ!6KiaBrZ8uB$ch%f|RFDS728I|B85?$bNFI(Y4j_J?nzlP7}
zm@^c;`)|j@bU*9Op9&u&dAt7wyFvWq{g_C^h%7&}b3r@1|3T%C|
zuO|2VJ{xhC2a9|zFa$F33aC;*{hr%W_JdJn#sbDgIbYd2j2tO)CCHOEU-1g0HnDZw
z^1q<}cw_GOd9P<4x12DWvA|F*qd}vQtwV@>YK7tQ)LI5t$_=8>9i!SQjmgmv91TJ8
q%Yo6VJUX<
z4B-HR8jh3>1_n+ZPZ!6KiaBrZD)KQo3b-DWJhZoQvN;!X#HN1VsV-+{P49nYYrL7^
z#7>#N3=I2jZ@#u>>$QFIf2Z7^HMw5Cz)sA>VTuE@3n%&1m9neS3r;q%b?n;re}nw*
z8Porqxbw%6;ooFdUIEn$41rAKQ%_f!J4^u@1EZ;A+}FaZzmJyYHzY?=<~)#d)>SdP
zP{9}q0*S&v8P!f{Djf~M(GVoRj2lfoqp4>!^-!n&^S>vh`g&EhR{*e`#Ng@b=d#Wz
Gp$PzK90$4p
literal 0
HcmV?d00001
diff --git a/integration_tests/snapshots/css/css-selectors/has-basic.ts.5ce78b901.png b/integration_tests/snapshots/css/css-selectors/has-basic.ts.5ce78b901.png
new file mode 100644
index 0000000000000000000000000000000000000000..3917faa14cb2ffb0c99c7b088752948e70a63c3c
GIT binary patch
literal 2403
zcmeAS@N?(olHy`uVBq!ia0y~yV9a1(U~1rC1Bz^vdbAu!F%}28J29*~C-V}>VJUX<
z4B-HR8jh3>1_n-EPZ!6KiaBrZ8VWTzh_D`vJ>C5HFa$F33aC;*T@KA}P~(bsuV{!w7CKh?z6A(YXe(MUd3bX8gbWDJa^l5thLUe!L|Wqx3z4rR^*IVXHC<02{;
zLqQ->7$~FKDNUuLAvhX>VJUX<
z4B-HR8jh3>1_n+UPZ!6KiaBrZ8uA@75MeziQTU;tC|H3@XUD8pU-Ohy|mx*@{X7PnckglV(SpfXwYaRpORWv#cUFJfg$kxVf|#C
z$`hG23Jl_@j?6BcCJs{^$fsoUg?2q;pO64FNmlGjMeVw(^R-8d{T_5P?9kwrQt(>9
zxQK;(D(lS440(r%th@rHJ6`>Ly371Plo)011$lS%J%&Ik7(+p*Q5fi>+9^%8qaipN
qg5(!^qp4>!^^B$-s?>j1qY=5&*WF7(8A5T-G@yGywoOu1!S%
literal 0
HcmV?d00001
diff --git a/integration_tests/snapshots/css/css-selectors/has-basic.ts.79d56e171.png b/integration_tests/snapshots/css/css-selectors/has-basic.ts.79d56e171.png
new file mode 100644
index 0000000000000000000000000000000000000000..3bd7eae89bf28cb5ed8292a81f8f98634ef62f79
GIT binary patch
literal 4298
zcmeI0Sx{418h|gmfROHH8We#zdIgmw&`}^wOmJ(tpg}EV5i}qQI4l8TLxKsQfI_>_
zbrB^XLG8h%p$Ur!5fa>xxnc+{Dndd6&26Fr37tR?0%WMEshXFm>VBB6dGWlQI{$h2
ztN!}_@2_(T_fmFQS=d_u0AK|L?AQ+g2Jrx3n6u1uDN=9$%k8DjfWCj%HlViK;r-IV
zn7$1nV)>LI2?)*($VeS4#8Nai4!1id+#=GXLEw&wv
zvAp4S_oC%@E1I0w++6d_vCduJZ)0QcEc=NWFZ+5h9y`~vXc-&b#}-n+FU@I-aM
z4cDX-ylkn;QS6}du9)cU9xK`1eT~kY=mu3fG$TvF3{ks=TWt=(Y2*eq{ol4=fSilw
zoBTJNHeKJO@t0SkBT(|-qXvxI7JVP+ql^;hIczr`CdGKbucsPHE(;Knk#n&0E7c$u
z6+w&aa(5YwehJk^Feb`C)!?I_SuAa;oTPUEFn+Bw^An>WEm-}&YMoOF+Bp+_p2|hX
z!P^dXF~+`YelnR8g0w(&2PlTr-`Wh|y6c_@eLavFr^_)WzBg-3=GahjP#OelY-L-L
z7v1Jqa5FgjmPp1UJ0VJiqS=MXhBd|4T06Mp5|maf^%P^t?SBCRv=_E!$8xu1nI%M)
zqe@U5sGc&Ph_I8jlaXw3|4eK$Mg4%_M}a4Azbu
zAQfzCGfc^gu$+SM>1{Ty5~93N;J)4ml9S9dz)ZJ8K|+9(f%Z7`38fRkw?#{T+cf1}G^#OLfcwAveZ-5~JaIIZge
zs*L~lmrV_2HBQRhs7F8Qt)njD|C9Eh$($p!m}S;ASc#s_S6~Cx3coPb8;};1rpa@s_Mfk?^O
z9v$kOVh1u0!R--;hKe;ZO7~t&eo3LG*LvnO;Bc(fuJT$LdfVlt>p4~F{pq+?su%2xeT_GM?{KP
z743Os1zHA@H@AC|$Rp2u+}$+8pigprWot2U19A|`azj+2mgy>q{uJ=)Z9>7w9gs&!
zi-uj{`FEzSQqO%^sjxhpv9Lj-_zomDcVu35p3QZ)e)kbnRbF>S!Xe>WWLGh3+B*Br
zQeAi}Ettb?+O^Oniu$Z$ve8hAF1vT!BS1Y~IBw@1HY~Tj5-}yxe8iQH$Fl`-Npyel
z;Q~6$njp%WM49JF4Tu*h8++d%r6tK1GgpEaD4V?*G`96`g{#^!KG<%@jEz;aA-ZzM
zo?Oly0qI2p>%whPL6U)k$11IAERHeqr6yW(lULd)@d;BY-Pg3d&NLe^x9n_ucYk{w
zEyw71ugxnexAQ7akTpjX#=V~orm(P$h}Jto73E8J;zeXGIG&4=17^>aA{pZ(Of5f=
zdcK>3`J76?4zNcq9He@yvh~iAh)C5_Fqq3r%wCNII}>8bSp_@})tXj;*@&a4Z-V5@
zXtN@Y?~PPzW3a%vf!f4W`-PW#&blqe-x3N=-gD+@%yR#ZezNW3)XK-c^yo*s
zXm#oh8ip$Ioe3l(@*K%U<%6KsUn*ENG@KX;6O)t8rt`s2(F!%r-TfoqDnuCLR76qN
z7HW9Te46q)TaxFL!*1>?95csHO}+yAaPEw0myB@4r6jEBy7ct
zp)jl~$qxyFBk$iE(i9WdARU_dk1I`?KNHDGXMK*8pk>U7$6zSPKbh;eG;EUG?4v~7
z@@F!y?O&`Ktk6wy8d=BTh`9dVQA_78CFeb(XrR-Njo;m9^lUCA^^#=R!bMyuawtZ<
zblH)>F5+4XpekJz#c4|*`5O=Q^y5wUn8)-y^n7aYUlji;x%UC^H6iy8;P)pCgx^K}
zQ_eqU5`QZE9}(^+`aaS3NsRw`V15$D|5q5V^oIM=)2NsFXueDT5dgH4vZHoe)R}(*
Dq6wiV
literal 0
HcmV?d00001
diff --git a/integration_tests/snapshots/css/css-selectors/has-basic.ts.85a326141.png b/integration_tests/snapshots/css/css-selectors/has-basic.ts.85a326141.png
new file mode 100644
index 0000000000000000000000000000000000000000..bb8d2d1f1f72952d5b8bf3ca2049c0dc77baafb4
GIT binary patch
literal 2415
zcmeAS@N?(olHy`uVBq!ia0y~yV9a1(U~1rC1Bz^vdbAu!F%}28J29*~C-V}>VJUX<
z4B-HR8jh3>1_n+^PZ!6KiaBrZTJkkJ2rxLVKYIA%>FbO_jr{_bBSJadw*+RJ%`$vG
z$DE<)UAjC&!})1{b9>%y%$r|q``UxOFpxp-7z?j}>IH^CCh{rMoUd#dMvlxby6f$a
zCEtGc*F1V}tUyCINQl$KVTuF!)Xi7C0;x@G9hsXDnb`#3H5OMFF+%y~>uu{0~CZG8!}***b(qsnH-9O$DPFVYDn5Ee=O(
f1p3#8E7YS7seIj5FLNH)Xkzem^>bP0l+XkKHwfp=
literal 0
HcmV?d00001
diff --git a/integration_tests/snapshots/css/css-selectors/has-basic.ts.91127d8c1.png b/integration_tests/snapshots/css/css-selectors/has-basic.ts.91127d8c1.png
new file mode 100644
index 0000000000000000000000000000000000000000..3f5766e686df7130a94d32eb61a11d395510fc4a
GIT binary patch
literal 2428
zcmeAS@N?(olHy`uVBq!ia0y~yV9a1(U~1rC1Bz^vdbAu!F%}28J29*~C-V}>VJUX<
z4B-HR8jh3>1_n+wPZ!6KiaBrZ8u}e|;Bh!8QTUb8yV0Pgo
zpSrThG(R9Oh5ZrrePf`f*e{
zrKw~z1V=-V{4!=V^^B&T(bPjhB~6_SbVYrcoI=_3Bj&q+Ehq+0S3j3^P6VJUX<
z4B-HR8jh3>1_n+sPZ!6KiaBrZTJs$;5O8qpDJrnoxw7fVdY?~CX{$8yVrI*BwkGx3xW3_u^&fFHfese-QQ-P6MMuSEpTZa(&)QZH_<_?oNr4+I%|8LCu
z{%`i$dvY8HJZCOoT*M-!;6(wI^Odc`$dTFQ<||%-)Fz4?3G(BntI`UmSa=1#ZoK+C
zvnN)6lO;dvgA|_&41r9%0;&{H`^$xHJ!CJ?5H@j`;=t^}IZBNN!DuQN%?P7q!Dw+f
gS|iZEHoU?dcS_~!)VG(Sfh{HmPgg&ebxsLQ0AROKpa1{>
literal 0
HcmV?d00001
diff --git a/integration_tests/snapshots/css/css-selectors/has-basic.ts.c713df221.png b/integration_tests/snapshots/css/css-selectors/has-basic.ts.c713df221.png
new file mode 100644
index 0000000000000000000000000000000000000000..17e5858c4228b71b7a5e356c2e04b3a86b225216
GIT binary patch
literal 2761
zcmeAS@N?(olHy`uVBq!ia0y~yV9a1(U~1rC1Bz^vdbAu!F%}28J29*~C-V}>VJUX<
z4B-HR8jh3>1_rKTPZ!6KiaBrZ8fJ%vGaUFhS*WR-L$8tNt`EC#`f|Uti4BXr%1vTyjPw1??c`Gw}m^O%kB8^yrAO!
zo~<7*&)_vmv#*jpAbvmQ3k$;!t46jCp^OF%2({wt%I^Dr|NhZ^!
zJU^4!wng$m{X-qTh7~6rfIi&tfq9xC(Dk!;zMNjG&dvJm$ALPAb9qJlzusOI|HsSv
zpy@_4axj5HD{6MH;qDin1&7YOX4to(30-=YP50xU*S0_Yxb^kpg5>0N2iMEU_Oidf
z*R%I~{W)vL@F9W
z)A*$nycRGnVj+sk`ed?O+ua>x<(+%GjlVOmlaW1JtA9Xic8sHa#rL#@2jt{$@E^Fj
z_Uy44v0qdy5}%ba#AET?_qB`-YTEF00ZksG6fC2RhQw${jOGqR#W9*6M)SjHei*X(
cVa55p{nI9~R?U+FHb)seUHx3vIVCg!0HvkKE&u=k
literal 0
HcmV?d00001
diff --git a/integration_tests/snapshots/css/css-selectors/has-basic.ts.d395e40c1.png b/integration_tests/snapshots/css/css-selectors/has-basic.ts.d395e40c1.png
new file mode 100644
index 0000000000000000000000000000000000000000..9d99792abcb442b37f1182d81f733402a0067dfc
GIT binary patch
literal 3399
zcmeH~>rYd67{?EnAt*#0nvKf9co}Ras6%90py&{W;9bK+1Q87oJuOgd!9!c*64WuO
zi9m!xnHv|K1GR7pr57c}4v|(T2>L6O%Vt456k2=Z&~rxi+WiINdGpEhynOO}zu!Dp
z4(^ZgSV~z6L6ArE_YsF7$RQ1a9P?dW7AE&8V~q=NU>=Uz4K=I0Cl?xL=I-dDt_zjt
zdgd|&xz9#N>^hnwz=lpVHRtY9&T7;4pY^G`eczqx_H}GuagOKe#2;4mU#p@2D!J~z
zbIWnBBYm+>thMe16YD5<-#;mEFSzCJf6vvir!MyCt;sspy{c~}w{=#N75%5x?J2Kn
z?LlF_gCXZaODwtCw$V-kC&ScS{a|6|c0wyL=PMGe2TiTO?hLhWz2tKF)3kj5kD+OK
z=VYjFc2xY+Lbhf)71ErJd=4FQ^jNX+;{Yh_wRssJrdYI<`ODi2!WA6Q!oYuDa~`i0
zVW9_zZQ{Oau`CNHhNk@-XB^^IO!y9&ufoDMOuQM!Uy86w;e0w#*`hJO+r*8{uh=9Q
zYe#^XexSRgNWlc!kWX|D?VRQ<&uOg~Tb2FDGdCO9L825ZgVn%X1?v^})JZU|f`ysM
z!e9no0$a`_Q(om-5jMLojqFWR_#T>)9z^GRlOgi-ib=XTUc8y+pp3a<498hHK53_5pX9=8Fn``y9+zDJq
zK48`>16BRL3|SD(CEss!siQroa0W;1f1nCkE8zj;lL>EGFHDClxT>5o(6}x!JD?XC
z>5{kX7zorNkv)7mULl&l1k;$w+R7yB+;Ctp@vvi629Tcud@vh$Z_jnDUS
z?^xQFVJUX<
z4B-HR8jh3>1_n++PZ!6KiaBrZ8uA@75Mgzce)^!GD0nH?l!@k-d|Fq;tc_b`e?2Oj
zVZ!HIe;F9+tk21RHa;$qyypA4=XXxDGkq`+GjW*W!0f_FK6Rz!s`P@BO>7-jdjHSt
ze5$zpVfR~QgWC-L7SU0V<>dRsCG)@aWn)+Ly+RaU^EepCZf?qL_uvzc?NpL-RLOu
VU-`(5!@$N8gQu&X%Q~loCIAZwc)$Pv
literal 0
HcmV?d00001
diff --git a/integration_tests/snapshots/css/css-selectors/has-basic.ts.f2c499201.png b/integration_tests/snapshots/css/css-selectors/has-basic.ts.f2c499201.png
new file mode 100644
index 0000000000000000000000000000000000000000..05b2a151b5c7a26092020737919a492bf5e6591f
GIT binary patch
literal 2406
zcmeAS@N?(olHy`uVBq!ia0y~yV9a1(U~1rC1Bz^vdbAu!F%}28J29*~C-V}>VJUX<
z4B-HR8jh3>1_n+6PZ!6KiaBrZ8uA@75MeziQTXA`oe+@|0d8@NW==Yl8P@-*-}1Q)
z!->i_bqoymdmr1_iG8v8Zu`vZ@v{4@cc!!Q3aDOS2xKCkGL8Alma%C8%k0jcL
QY%4K%y85}Sb4q9e02SX1od5s;
literal 0
HcmV?d00001
diff --git a/integration_tests/snapshots/css/css-selectors/has-basic.ts.f70261ee1.png b/integration_tests/snapshots/css/css-selectors/has-basic.ts.f70261ee1.png
new file mode 100644
index 0000000000000000000000000000000000000000..8ba9db8bb2374b48804550a6ba1d1888e3748afa
GIT binary patch
literal 2422
zcmeAS@N?(olHy`uVBq!ia0y~yV9a1(U~1rC1Bz^vdbAu!F%}28J29*~C-V}>VJUX<
z4B-HR8jh3>1_n+APZ!6KiaBrZTJs$;5O8qpv9OqPM?_GB<fuce2<$L=1k#~Qt(>9
zxQK;(Dr^7b3w-tsEW#!ZSL&MTHr|y^&;+ulI54|#l22WEKewgq2cr%ZJWgS_jcTVf
y<&B2mXb6&DZjGj%(bO}VdMK#)sgr@Kn6mCD{JJw$)fL!wV(@hJb6Mw<&;$VQKlG;n
literal 0
HcmV?d00001
diff --git a/integration_tests/snapshots/css/css-selectors/has-basic.ts.f89e13111.png b/integration_tests/snapshots/css/css-selectors/has-basic.ts.f89e13111.png
new file mode 100644
index 0000000000000000000000000000000000000000..93395959c91813525cfaa39c8c6de868d18a0dcf
GIT binary patch
literal 2402
zcmeAS@N?(olHy`uVBq!ia0y~yV9a1(U~1rC1Bz^vdbAu!F%}28J29*~C-V}>VJUX<
z4B-HR8jh3>1_n+ZPZ!6KiaBrZ8uB$8h_D{Cc=Rv4Y?Vsu!k#@Y#-fWBMb)k{e>Uec
zqr
z+X3I53m6x%NGW(xKwa~7lCERm1i5haJ%&KaT?#X1#n+~~&F^?OOy#D`fk6Mgs+xSE
zP5wX_6&y!dEKnGUquMFW2BRT38iM2(qNAy2H1&+89tv6^&VJUX<
z4B-HR8jh3>1_n-1PZ!6KiaBrZ8uA@75MgzcF8okXbUDyLLwH)ovL!}mUoHJryml)a
zgXQ*kdxnPE;+*7i$MtXj9GA(Ad-I>+db%UC3#W<06bJGt-|)SRvve{VR{Z|W|Jm^P
zj!U8W@*D@|Z(6{(h($`livlXqs8H9jX?j}@GHyYgDQomH+*IVHkcSZ
MUHx3vIVCg!0IpvZ`2YX_
literal 0
HcmV?d00001
diff --git a/integration_tests/snapshots/css/css-selectors/has-basic.ts.fe50857e1.png b/integration_tests/snapshots/css/css-selectors/has-basic.ts.fe50857e1.png
new file mode 100644
index 0000000000000000000000000000000000000000..2b23744683d470e1be5e565e762f624586f253d9
GIT binary patch
literal 2424
zcmeAS@N?(olHy`uVBq!ia0y~yV9a1(U~1rC1Bz^vdbAu!F%}28J29*~C-V}>VJUX<
z4B-HR8jh3>1_n+gPZ!6KiaBrZ8uA@75MXfZ`BYG`Q**)sjwuJFO@dulu8W$xTDtgL
zC8NUgT~!PWcDLi=lXYaD6v%NMIC*0M<02L*1uqJyd*+;X-!OY@bYynP-EIHB?C$N;
z3>KQwCJs{^m|Zx@r>^`{TPR(}z-gAzu;MP8?Yr}|t5uG>5rDkU>4QnXN-8qd}vQ
zd`ha=>K1!I3ecciv0v*XA1`l~Ip@r1AxQ;?f1llw_k&S~3XY`EkE7ZtO(mltI2wZF
qmocNMXEgPUrXC6^Y3gL4Du&$SGXHO|@#h0uPYj-}elF{r5}E+MgY$_1
literal 0
HcmV?d00001
diff --git a/integration_tests/specs/css/css-selectors/has-basic.ts b/integration_tests/specs/css/css-selectors/has-basic.ts
new file mode 100644
index 0000000000..9a11009b69
--- /dev/null
+++ b/integration_tests/specs/css/css-selectors/has-basic.ts
@@ -0,0 +1,463 @@
+/**
+ * CSS Selectors: Basic :has() matching behavior
+ * Based on WPT: css/selectors/has-basic.html
+ *
+ * Key behaviors:
+ * - :has() matches elements that have descendants/siblings matching the argument
+ * - Works with descendant, child, and sibling combinators
+ * - Works with querySelectorAll, querySelector, closest, matches APIs
+ */
+describe('CSS Selectors: :has() basic matching', () => {
+ const green = 'rgb(0, 128, 0)';
+ const red = 'rgb(255, 0, 0)';
+
+ function appendStyle(cssText: string) {
+ const style = document.createElement('style');
+ style.textContent = cssText;
+ document.head.appendChild(style);
+ return style;
+ }
+
+ function createTestDOM() {
+ const main = document.createElement('main');
+ main.id = 'main';
+ main.innerHTML = `
+
+ `;
+ document.body.appendChild(main);
+ return main;
+ }
+
+ function formatElements(elements: Element[]) {
+ return elements.map(e => e.id).sort().join(',');
+ }
+
+ // ========== Basic :has() with descendant selector ==========
+
+ it('A1 :has(#a) matches nothing (no element has #a as descendant)', async () => {
+ const main = createTestDOM();
+ const style = appendStyle(`
+ main div { background-color: green; width: 50px; height: 20px; margin: 2px; }
+ :has(#a) { background-color: red; }
+ `);
+
+ const actual = Array.from(main.querySelectorAll(':has(#a)'));
+ expect(formatElements(actual)).toBe('');
+
+ await snapshot();
+
+ style.remove();
+ main.remove();
+ });
+
+ it('A2 :has(.ancestor) matches element a', async () => {
+ const main = createTestDOM();
+ const style = appendStyle(`
+ main div { background-color: red; width: 50px; height: 20px; margin: 2px; }
+ :has(.ancestor) { background-color: green; }
+ `);
+
+ const actual = Array.from(main.querySelectorAll(':has(.ancestor)'));
+ expect(formatElements(actual)).toBe('a');
+ expect(getComputedStyle(main.querySelector('#a')!).backgroundColor).toBe(green);
+
+ await snapshot();
+
+ style.remove();
+ main.remove();
+ });
+
+ it('A3 :has(.target) matches ancestors of .target elements', async () => {
+ const main = createTestDOM();
+ const style = appendStyle(`
+ main div { background-color: red; width: 50px; height: 20px; margin: 2px; }
+ :has(.target) { background-color: green; }
+ `);
+
+ const actual = Array.from(main.querySelectorAll(':has(.target)'));
+ expect(formatElements(actual)).toBe('a,b,f,h');
+
+ await snapshot();
+
+ style.remove();
+ main.remove();
+ });
+
+ it('A4 :has(.descendant) matches all ancestors of .descendant', async () => {
+ const main = createTestDOM();
+ const style = appendStyle(`
+ main div { background-color: red; width: 50px; height: 20px; margin: 2px; }
+ :has(.descendant) { background-color: green; }
+ `);
+
+ const actual = Array.from(main.querySelectorAll(':has(.descendant)'));
+ expect(formatElements(actual)).toBe('a,b,c,f,h,j');
+
+ await snapshot();
+
+ style.remove();
+ main.remove();
+ });
+
+ // ========== :has() with class filter ==========
+
+ it('A5 .parent:has(.target) filters by class', async () => {
+ const main = createTestDOM();
+ const style = appendStyle(`
+ main div { background-color: red; width: 50px; height: 20px; margin: 2px; }
+ .parent:has(.target) { background-color: green; }
+ `);
+
+ const actual = Array.from(main.querySelectorAll('.parent:has(.target)'));
+ expect(formatElements(actual)).toBe('b,f,h');
+
+ await snapshot();
+
+ style.remove();
+ main.remove();
+ });
+
+ // ========== :has() with sibling combinator ==========
+
+ it('A6 :has(.sibling ~ .target) matches elements with sibling-then-target pattern', async () => {
+ const main = createTestDOM();
+ const style = appendStyle(`
+ main div { background-color: red; width: 50px; height: 20px; margin: 2px; }
+ :has(.sibling ~ .target) { background-color: green; }
+ `);
+
+ const actual = Array.from(main.querySelectorAll(':has(.sibling ~ .target)'));
+ expect(formatElements(actual)).toBe('a,b');
+
+ await snapshot();
+
+ style.remove();
+ main.remove();
+ });
+
+ it('A7 .parent:has(.sibling ~ .target) with class filter', async () => {
+ const main = createTestDOM();
+ const style = appendStyle(`
+ main div { background-color: red; width: 50px; height: 20px; margin: 2px; }
+ .parent:has(.sibling ~ .target) { background-color: green; }
+ `);
+
+ const actual = Array.from(main.querySelectorAll('.parent:has(.sibling ~ .target)'));
+ expect(formatElements(actual)).toBe('b');
+
+ await snapshot();
+
+ style.remove();
+ main.remove();
+ });
+
+ // ========== :has() with :is() nested ==========
+
+ it('A8 :has(:is(.target ~ .sibling .descendant)) complex nesting', async () => {
+ const main = createTestDOM();
+ const style = appendStyle(`
+ main div { background-color: red; width: 50px; height: 20px; margin: 2px; }
+ :has(:is(.target ~ .sibling .descendant)) { background-color: green; }
+ `);
+
+ const actual = Array.from(main.querySelectorAll(':has(:is(.target ~ .sibling .descendant))'));
+ expect(formatElements(actual)).toBe('a,h,j');
+
+ await snapshot();
+
+ style.remove();
+ main.remove();
+ });
+
+ it('A9 .parent:has(:is(.target ~ .sibling .descendant)) with class filter', async () => {
+ const main = createTestDOM();
+ const style = appendStyle(`
+ main div { background-color: red; width: 50px; height: 20px; margin: 2px; }
+ .parent:has(:is(.target ~ .sibling .descendant)) { background-color: green; }
+ `);
+
+ const actual = Array.from(main.querySelectorAll('.parent:has(:is(.target ~ .sibling .descendant))'));
+ expect(formatElements(actual)).toBe('h');
+
+ await snapshot();
+
+ style.remove();
+ main.remove();
+ });
+
+ // ========== :has() in sibling selector context ==========
+
+ it('A10 .sibling:has(.descendant) ~ .target matches target after sibling with descendant', async () => {
+ const main = createTestDOM();
+ const style = appendStyle(`
+ main div { background-color: red; width: 50px; height: 20px; margin: 2px; }
+ .sibling:has(.descendant) ~ .target { background-color: green; }
+ `);
+
+ const actual = Array.from(main.querySelectorAll('.sibling:has(.descendant) ~ .target'));
+ expect(formatElements(actual)).toBe('e');
+
+ await snapshot();
+
+ style.remove();
+ main.remove();
+ });
+
+ // ========== :has() with child combinator ==========
+
+ it('B1 :has(> .parent) matches only direct parent of .parent', async () => {
+ const main = createTestDOM();
+ const style = appendStyle(`
+ main div { background-color: red; width: 50px; height: 20px; margin: 2px; }
+ :has(> .parent) { background-color: green; }
+ `);
+
+ const actual = Array.from(main.querySelectorAll(':has(> .parent)'));
+ expect(formatElements(actual)).toBe('a');
+
+ await snapshot();
+
+ style.remove();
+ main.remove();
+ });
+
+ it('B2 :has(> .target) matches direct parents of .target', async () => {
+ const main = createTestDOM();
+ const style = appendStyle(`
+ main div { background-color: red; width: 50px; height: 20px; margin: 2px; }
+ :has(> .target) { background-color: green; }
+ `);
+
+ const actual = Array.from(main.querySelectorAll(':has(> .target)'));
+ expect(formatElements(actual)).toBe('b,f,h');
+
+ await snapshot();
+
+ style.remove();
+ main.remove();
+ });
+
+ it('B3 :has(> .parent, > .target) matches with OR logic', async () => {
+ const main = createTestDOM();
+ const style = appendStyle(`
+ main div { background-color: red; width: 50px; height: 20px; margin: 2px; }
+ :has(> .parent, > .target) { background-color: green; }
+ `);
+
+ const actual = Array.from(main.querySelectorAll(':has(> .parent, > .target)'));
+ expect(formatElements(actual)).toBe('a,b,f,h');
+
+ await snapshot();
+
+ style.remove();
+ main.remove();
+ });
+
+ // ========== :has() with adjacent sibling ==========
+
+ it('B4 :has(+ #h) matches element immediately before #h', async () => {
+ const main = createTestDOM();
+ const style = appendStyle(`
+ main div { background-color: red; width: 50px; height: 20px; margin: 2px; }
+ :has(+ #h) { background-color: green; }
+ `);
+
+ const actual = Array.from(main.querySelectorAll(':has(+ #h)'));
+ expect(formatElements(actual)).toBe('f');
+
+ await snapshot();
+
+ style.remove();
+ main.remove();
+ });
+
+ // ========== :has() with general sibling ==========
+
+ it('B5 .parent:has(~ #h) matches .parent elements before #h', async () => {
+ const main = createTestDOM();
+ const style = appendStyle(`
+ main div { background-color: red; width: 50px; height: 20px; margin: 2px; }
+ .parent:has(~ #h) { background-color: green; }
+ `);
+
+ const actual = Array.from(main.querySelectorAll('.parent:has(~ #h)'));
+ expect(formatElements(actual)).toBe('b,f');
+
+ await snapshot();
+
+ style.remove();
+ main.remove();
+ });
+
+ // ========== querySelector API ==========
+
+ it('C1 querySelector returns first matching element', async () => {
+ const main = createTestDOM();
+ const style = appendStyle(`
+ main div { background-color: red; width: 50px; height: 20px; margin: 2px; }
+ .sibling:has(.descendant) { background-color: green; }
+ `);
+
+ const result = main.querySelector('.sibling:has(.descendant)');
+ expect(result).toBe(main.querySelector('#c'));
+
+ await snapshot();
+
+ style.remove();
+ main.remove();
+ });
+
+ // ========== closest API ==========
+
+ it('C2 closest finds nearest ancestor matching :has()', async () => {
+ const main = createTestDOM();
+ const style = appendStyle(`
+ main div { background-color: red; width: 50px; height: 20px; margin: 2px; }
+ .ancestor:has(.descendant) { background-color: green; }
+ `);
+
+ const k = main.querySelector('#k')!;
+ const result = k.closest('.ancestor:has(.descendant)');
+ expect(result).toBe(main.querySelector('#h'));
+
+ await snapshot();
+
+ style.remove();
+ main.remove();
+ });
+
+ // ========== matches API ==========
+
+ it('C3 matches returns true for matching :has()', async () => {
+ const main = createTestDOM();
+ const style = appendStyle(`
+ main div { background-color: red; width: 50px; height: 20px; margin: 2px; }
+ :has(.target ~ .sibling .descendant) { background-color: green; }
+ `);
+
+ const h = main.querySelector('#h')!;
+ expect(h.matches(':has(.target ~ .sibling .descendant)')).toBe(true);
+
+ await snapshot();
+
+ style.remove();
+ main.remove();
+ });
+
+ it('C4 matches returns false for non-matching :has()', async () => {
+ const main = createTestDOM();
+
+ const b = main.querySelector('#b')!;
+ // b has .target but not .target ~ .sibling .descendant pattern
+ expect(b.matches(':has(.target ~ .sibling .descendant)')).toBe(false);
+
+ main.remove();
+ });
+});
+
+/**
+ * CSS Selectors: :has() visual styling tests
+ * Tests that :has() correctly applies styles visually
+ */
+describe('CSS Selectors: :has() visual styling', () => {
+ const green = 'rgb(0, 128, 0)';
+
+ function appendStyle(cssText: string) {
+ const style = document.createElement('style');
+ style.textContent = cssText;
+ document.head.appendChild(style);
+ return style;
+ }
+
+ it('D1 Parent styled green when child exists', async () => {
+ const container = document.createElement('div');
+ container.innerHTML = `
+
+ Child
+
+
+ No child span
+
+ `;
+ document.body.appendChild(container);
+
+ const style = appendStyle(`
+ .parent:has(.child) { background-color: green !important; }
+ `);
+
+ const parents = container.querySelectorAll('.parent');
+ expect(getComputedStyle(parents[0]).backgroundColor).toBe(green);
+ // Second parent has no .child, should remain red
+ expect(getComputedStyle(parents[1]).backgroundColor).toBe('rgb(255, 0, 0)');
+
+ await snapshot();
+
+ style.remove();
+ container.remove();
+ });
+
+ it('D2 Multiple levels of :has() nesting', async () => {
+ const container = document.createElement('div');
+ container.innerHTML = `
+
+ `;
+ document.body.appendChild(container);
+
+ const style = appendStyle(`
+ .grandparent:has(.child) { background-color: green !important; }
+ `);
+
+ const grandparent = container.querySelector('.grandparent')!;
+ expect(getComputedStyle(grandparent).backgroundColor).toBe(green);
+
+ await snapshot();
+
+ style.remove();
+ container.remove();
+ });
+
+ it('D3 :has() with attribute selector', async () => {
+ const container = document.createElement('div');
+ container.innerHTML = `
+
+
+
+
+
+
+ `;
+ document.body.appendChild(container);
+
+ const style = appendStyle(`
+ .item:has(input[checked]) { background-color: green !important; }
+ `);
+
+ const items = container.querySelectorAll('.item');
+ expect(getComputedStyle(items[0]).backgroundColor).toBe(green);
+
+ await snapshot();
+
+ style.remove();
+ container.remove();
+ });
+});
From 074f2d04dc78fa05e81076294d0ca1f9c33136e0 Mon Sep 17 00:00:00 2001
From: jwxbond
Date: Wed, 28 Jan 2026 22:13:44 +0800
Subject: [PATCH 02/16] fix(css-selector): support kPseudoChecked, add
`has-specificity` tests and snapshots
---
bridge/core/css/selector_checker.cc | 25 +-
.../has-specificity.ts.08664f2e1.png | Bin 0 -> 2972 bytes
.../has-specificity.ts.0f44a1611.png | Bin 0 -> 3210 bytes
.../has-specificity.ts.1cd508271.png | Bin 0 -> 2369 bytes
.../has-specificity.ts.2a31f0e21.png | Bin 0 -> 2369 bytes
.../has-specificity.ts.349ee1d41.png | Bin 0 -> 2374 bytes
.../has-specificity.ts.4a91931a1.png | Bin 0 -> 3097 bytes
.../has-specificity.ts.4c1451771.png | Bin 0 -> 2369 bytes
.../has-specificity.ts.4eb2a4ac1.png | Bin 0 -> 2972 bytes
.../has-specificity.ts.52bee3611.png | Bin 0 -> 2369 bytes
.../has-specificity.ts.71df37f01.png | Bin 0 -> 2408 bytes
.../has-specificity.ts.7b83fd0e1.png | Bin 0 -> 3210 bytes
.../has-specificity.ts.9fe4eb3c1.png | Bin 0 -> 2408 bytes
.../has-specificity.ts.a610a60b1.png | Bin 0 -> 2369 bytes
.../has-specificity.ts.d4a25c9a1.png | Bin 0 -> 2374 bytes
.../has-specificity.ts.d52b56311.png | Bin 0 -> 2369 bytes
.../has-specificity.ts.e60faaa81.png | Bin 0 -> 3203 bytes
.../has-specificity.ts.f2b936581.png | Bin 0 -> 2633 bytes
.../css/css-selectors/has-specificity.ts | 454 ++++++++++++++++++
19 files changed, 469 insertions(+), 10 deletions(-)
create mode 100644 integration_tests/snapshots/css/css-selectors/has-specificity.ts.08664f2e1.png
create mode 100644 integration_tests/snapshots/css/css-selectors/has-specificity.ts.0f44a1611.png
create mode 100644 integration_tests/snapshots/css/css-selectors/has-specificity.ts.1cd508271.png
create mode 100644 integration_tests/snapshots/css/css-selectors/has-specificity.ts.2a31f0e21.png
create mode 100644 integration_tests/snapshots/css/css-selectors/has-specificity.ts.349ee1d41.png
create mode 100644 integration_tests/snapshots/css/css-selectors/has-specificity.ts.4a91931a1.png
create mode 100644 integration_tests/snapshots/css/css-selectors/has-specificity.ts.4c1451771.png
create mode 100644 integration_tests/snapshots/css/css-selectors/has-specificity.ts.4eb2a4ac1.png
create mode 100644 integration_tests/snapshots/css/css-selectors/has-specificity.ts.52bee3611.png
create mode 100644 integration_tests/snapshots/css/css-selectors/has-specificity.ts.71df37f01.png
create mode 100644 integration_tests/snapshots/css/css-selectors/has-specificity.ts.7b83fd0e1.png
create mode 100644 integration_tests/snapshots/css/css-selectors/has-specificity.ts.9fe4eb3c1.png
create mode 100644 integration_tests/snapshots/css/css-selectors/has-specificity.ts.a610a60b1.png
create mode 100644 integration_tests/snapshots/css/css-selectors/has-specificity.ts.d4a25c9a1.png
create mode 100644 integration_tests/snapshots/css/css-selectors/has-specificity.ts.d52b56311.png
create mode 100644 integration_tests/snapshots/css/css-selectors/has-specificity.ts.e60faaa81.png
create mode 100644 integration_tests/snapshots/css/css-selectors/has-specificity.ts.f2b936581.png
create mode 100644 integration_tests/specs/css/css-selectors/has-specificity.ts
diff --git a/bridge/core/css/selector_checker.cc b/bridge/core/css/selector_checker.cc
index 7a204fee96..cfc409773d 100644
--- a/bridge/core/css/selector_checker.cc
+++ b/bridge/core/css/selector_checker.cc
@@ -1805,16 +1805,21 @@ bool SelectorChecker::CheckPseudoClass(const SelectorCheckingContext& context, M
case CSSSelector::kPseudoInvalid:
return element.MatchesValidityPseudoClasses() && !element.IsValidElement();
case CSSSelector::kPseudoChecked: {
- // TODO: Implement form control checked state
- // if (auto* input_element = DynamicTo(element)) {
- // if (input_element->ShouldAppearChecked() && !input_element->ShouldAppearIndeterminate()) {
- // return true;
- // }
- // } else if (auto* option_element = DynamicTo(element)) {
- // if (option_element->Selected()) {
- // return true;
- // }
- // }
+ // Minimal :checked support for WebF's DOM model.
+ // Match checkable inputs with [checked] and
`;
+ document.body.appendChild(div);
+ return div;
+ }
+
+ // ========== ID vs Class specificity ==========
+
+ it('A1 :has(#foo) wins over :has(.foo) - ID higher than class', async () => {
+ const div = createTestDOM();
+ const style = appendStyle(`
+ #div { width: 100px; height: 50px; }
+ :has(#foo) { background-color: green; }
+ :has(.foo) { background-color: red; }
+ `);
+
+ // :has(#foo) = 1,0,0 in the argument
+ // :has(.foo) = 0,1,0 in the argument
+ // :has(#foo) wins
+ expect(getComputedStyle(div).backgroundColor).toBe(green);
+
+ await snapshot();
+
+ style.remove();
+ div.remove();
+ });
+
+ it('A2 :has(span#foo) wins over :has(#foo) - element + ID higher', async () => {
+ const div = createTestDOM();
+ const style = appendStyle(`
+ #div { width: 100px; height: 50px; }
+ :has(span#foo) { background-color: green; }
+ :has(#foo) { background-color: red; }
+ `);
+
+ // :has(span#foo) = 1,0,1 in the argument
+ // :has(#foo) = 1,0,0 in the argument
+ // :has(span#foo) wins
+ expect(getComputedStyle(div).backgroundColor).toBe(green);
+
+ await snapshot();
+
+ style.remove();
+ div.remove();
+ });
+
+ // ========== Comma-separated arguments ==========
+
+ it('A3 :has(.bar, #foo) has same specificity as :has(#foo, .bar) - order does not matter', async () => {
+ const div = createTestDOM();
+ const style = appendStyle(`
+ #div { width: 100px; height: 50px; }
+ :has(.bar, #foo) { background-color: red; }
+ :has(#foo, .bar) { background-color: green; }
+ `);
+
+ // Both have same specificity (1,0,0 from #foo), latter wins
+ expect(getComputedStyle(div).backgroundColor).toBe(green);
+
+ await snapshot();
+
+ style.remove();
+ div.remove();
+ });
+
+ it('A4 :has(.bar, #foo) wins over :has(.foo, .bar) - ID in list wins', async () => {
+ const div = createTestDOM();
+ const style = appendStyle(`
+ #div { width: 100px; height: 50px; }
+ :has(.bar, #foo) { background-color: green; }
+ :has(.foo, .bar) { background-color: red; }
+ `);
+
+ // :has(.bar, #foo) takes specificity of #foo = 1,0,0
+ // :has(.foo, .bar) = 0,1,0
+ // :has(.bar, #foo) wins
+ expect(getComputedStyle(div).backgroundColor).toBe(green);
+
+ await snapshot();
+
+ style.remove();
+ div.remove();
+ });
+
+ // ========== Combinator specificity ==========
+
+ it('A5 :has(span + span) wins over :has(span) - more elements', async () => {
+ const div = createTestDOM();
+ const style = appendStyle(`
+ #div { width: 100px; height: 50px; }
+ :has(span + span) { background-color: green; }
+ :has(span) { background-color: red; }
+ `);
+
+ // :has(span + span) = 0,0,2 in the argument
+ // :has(span) = 0,0,1 in the argument
+ // :has(span + span) wins
+ expect(getComputedStyle(div).backgroundColor).toBe(green);
+
+ await snapshot();
+
+ style.remove();
+ div.remove();
+ });
+
+ // ========== Mixed selector types ==========
+
+ it('A6 :has(span, li, #foo) wins over :has(span, li, p) - ID in list', async () => {
+ const div = createTestDOM();
+ const style = appendStyle(`
+ #div { width: 100px; height: 50px; }
+ :has(span, li, #foo) { background-color: green; }
+ :has(span, li, p) { background-color: red; }
+ `);
+
+ // :has(span, li, #foo) takes #foo specificity = 1,0,0
+ // :has(span, li, p) = 0,0,1
+ // :has(span, li, #foo) wins
+ expect(getComputedStyle(div).backgroundColor).toBe(green);
+
+ await snapshot();
+
+ style.remove();
+ div.remove();
+ });
+
+ // ========== :has() vs class on element ==========
+
+ it('A7 div.baz wins over div:has(.foo) when latter comes first', async () => {
+ const div = createTestDOM();
+ const style = appendStyle(`
+ #div { width: 100px; height: 50px; }
+ div:has(.foo) { background-color: red; }
+ div.baz { background-color: green; }
+ `);
+
+ // div:has(.foo) = 0,1,1 (div=0,0,1 + .foo=0,1,0)
+ // div.baz = 0,1,1 (div=0,0,1 + .baz=0,1,0)
+ // Same specificity, latter wins
+ expect(getComputedStyle(div).backgroundColor).toBe(green);
+
+ await snapshot();
+
+ style.remove();
+ div.remove();
+ });
+
+ it('A8 div:has(.foo) wins over div.baz when latter comes first', async () => {
+ const div = createTestDOM();
+ const style = appendStyle(`
+ #div { width: 100px; height: 50px; }
+ div.baz { background-color: red; }
+ div:has(.foo) { background-color: green; }
+ `);
+
+ // Same specificity (0,1,1), latter wins
+ expect(getComputedStyle(div).backgroundColor).toBe(green);
+
+ await snapshot();
+
+ style.remove();
+ div.remove();
+ });
+});
+
+describe('CSS Selectors: :has() specificity with multiple selectors', () => {
+ const green = 'rgb(0, 128, 0)';
+
+ function appendStyle(cssText: string) {
+ const style = document.createElement('style');
+ style.textContent = cssText;
+ document.head.appendChild(style);
+ return style;
+ }
+
+ it('B1 :has() with nested ID wins over :has() with class chain', async () => {
+ const container = document.createElement('div');
+ container.innerHTML = `
+
+ `;
+ document.body.appendChild(container);
+
+ const style = appendStyle(`
+ .parent { width: 100px; height: 50px; }
+ .parent:has(.child .deep) { background-color: red; }
+ .parent:has(#deep) { background-color: green; }
+ `);
+
+ const parent = container.querySelector('.parent')!;
+ // :has(.child .deep) = 0,2,0
+ // :has(#deep) = 1,0,0
+ // ID wins
+ expect(getComputedStyle(parent).backgroundColor).toBe(green);
+
+ await snapshot();
+
+ style.remove();
+ container.remove();
+ });
+
+ it('B2 Multiple classes in :has() vs single ID', async () => {
+ const container = document.createElement('div');
+ container.innerHTML = `
+
+ `;
+ document.body.appendChild(container);
+
+ const style = appendStyle(`
+ .parent { width: 100px; height: 50px; }
+ .parent:has(.a.b.c) { background-color: red; }
+ .parent:has(#target) { background-color: green; }
+ `);
+
+ const parent = container.querySelector('.parent')!;
+ // :has(.a.b.c) = 0,3,0
+ // :has(#target) = 1,0,0
+ // ID (1,0,0) > classes (0,3,0)
+ expect(getComputedStyle(parent).backgroundColor).toBe(green);
+
+ await snapshot();
+
+ style.remove();
+ container.remove();
+ });
+
+ it('B3 :has() specificity combines with outer selector', async () => {
+ const container = document.createElement('div');
+ container.innerHTML = `
+
+ `;
+ document.body.appendChild(container);
+
+ const style = appendStyle(`
+ .outer { width: 100px; height: 50px; }
+ div:has(.target) { background-color: red; }
+ .outer:has(.target) { background-color: green; }
+ `);
+
+ const outer = container.querySelector('.outer')!;
+ // div:has(.target) = 0,1,1
+ // .outer:has(.target) = 0,2,0
+ // .outer wins (0,2,0 > 0,1,1)
+ expect(getComputedStyle(outer).backgroundColor).toBe(green);
+
+ await snapshot();
+
+ style.remove();
+ container.remove();
+ });
+
+ it('B4 :has() with attribute selector specificity', async () => {
+ const container = document.createElement('div');
+ container.innerHTML = `
+
+
+
+ `;
+ document.body.appendChild(container);
+
+ const style = appendStyle(`
+ .parent { width: 100px; height: 50px; }
+ .parent:has(.input) { background-color: red; }
+ .parent:has([data-valid]) { background-color: green; }
+ `);
+
+ const parent = container.querySelector('.parent')!;
+ // Both have same specificity (0,1,0 from attribute or class)
+ // Latter wins
+ expect(getComputedStyle(parent).backgroundColor).toBe(green);
+
+ await snapshot();
+
+ style.remove();
+ container.remove();
+ });
+
+ it('B5 Attribute selector in :has() has class-level specificity', async () => {
+ const container = document.createElement('div');
+ container.innerHTML = `
+
+
+
+ `;
+ document.body.appendChild(container);
+
+ const style = appendStyle(`
+ .parent { width: 100px; height: 50px; }
+ .parent:has([data-valid]) { background-color: red; }
+ .parent:has(#input1) { background-color: green; }
+ `);
+
+ const parent = container.querySelector('.parent')!;
+ // :has([data-valid]) = 0,1,0
+ // :has(#input1) = 1,0,0
+ // ID wins
+ expect(getComputedStyle(parent).backgroundColor).toBe(green);
+
+ await snapshot();
+
+ style.remove();
+ container.remove();
+ });
+});
+
+describe('CSS Selectors: :has() specificity edge cases', () => {
+ const green = 'rgb(0, 128, 0)';
+
+ function appendStyle(cssText: string) {
+ const style = document.createElement('style');
+ style.textContent = cssText;
+ document.head.appendChild(style);
+ return style;
+ }
+
+ it('C1 :has(*) has zero specificity from universal selector', async () => {
+ const container = document.createElement('div');
+ container.innerHTML = `
+
+ Child
+
+ `;
+ document.body.appendChild(container);
+
+ const style = appendStyle(`
+ .parent { width: 100px; height: 50px; }
+ .parent:has(*) { background-color: red; }
+ .parent:has(span) { background-color: green; }
+ `);
+
+ const parent = container.querySelector('.parent')!;
+ // :has(*) = 0,0,0 (universal adds nothing)
+ // :has(span) = 0,0,1
+ // span wins
+ expect(getComputedStyle(parent).backgroundColor).toBe(green);
+
+ await snapshot();
+
+ style.remove();
+ container.remove();
+ });
+
+ it('C2 Empty :has() list with valid selector', async () => {
+ const container = document.createElement('div');
+ container.innerHTML = `
+
+ Child
+
+ `;
+ document.body.appendChild(container);
+
+ const style = appendStyle(`
+ .parent { width: 100px; height: 50px; }
+ .parent:has(.child) { background-color: green; }
+ `);
+
+ const parent = container.querySelector('.parent')!;
+ expect(getComputedStyle(parent).backgroundColor).toBe(green);
+
+ await snapshot();
+
+ style.remove();
+ container.remove();
+ });
+
+ it('C3 :has() with pseudo-class adds to specificity', async () => {
+ const container = document.createElement('div');
+ container.innerHTML = `
+
+
+
+ `;
+ document.body.appendChild(container);
+
+ const style = appendStyle(`
+ .parent { width: 100px; height: 50px; }
+ .parent:has(input) { background-color: red; }
+ .parent:has(input:checked) { background-color: green; }
+ `);
+
+ const parent = container.querySelector('.parent')!;
+ // :has(input) = 0,0,1
+ // :has(input:checked) = 0,1,1 (:checked is pseudo-class = 0,1,0)
+ // input:checked wins
+ expect(getComputedStyle(parent).backgroundColor).toBe(green);
+
+ await snapshot();
+
+ style.remove();
+ container.remove();
+ });
+
+ it('C4 Deeply nested :has() with :is()', async () => {
+ const container = document.createElement('div');
+ container.innerHTML = `
+
+ `;
+ document.body.appendChild(container);
+
+ const style = appendStyle(`
+ .parent { width: 100px; height: 50px; }
+ .parent:has(:is(.a span)) { background-color: red; }
+ .parent:has(:is(.a #target)) { background-color: green; }
+ `);
+
+ const parent = container.querySelector('.parent')!;
+ // :has(:is(.a span)) = 0,1,1
+ // :has(:is(.a #target)) = 1,1,0
+ // #target in :is() wins
+ expect(getComputedStyle(parent).backgroundColor).toBe(green);
+
+ await snapshot();
+
+ style.remove();
+ container.remove();
+ });
+});
From ec1ea5bbbfd5555f68e6a85af51f146a06e5b456 Mon Sep 17 00:00:00 2001
From: jwxbond
Date: Thu, 29 Jan 2026 17:06:26 +0800
Subject: [PATCH 03/16] fix(css-selector): add `has-relative-argument.ts` tests
and snapshots
---
.../has-relative-argument.ts.0fba2b1f1.png | Bin 0 -> 2424 bytes
.../has-relative-argument.ts.162690801.png | Bin 0 -> 2427 bytes
.../has-relative-argument.ts.16d0aa021.png | Bin 0 -> 2407 bytes
.../has-relative-argument.ts.1c6db6781.png | Bin 0 -> 2486 bytes
.../has-relative-argument.ts.32bf641d1.png | Bin 0 -> 2516 bytes
.../has-relative-argument.ts.338cb0651.png | Bin 0 -> 2403 bytes
.../has-relative-argument.ts.4692e7641.png | Bin 0 -> 2417 bytes
.../has-relative-argument.ts.4e20adc51.png | Bin 0 -> 2402 bytes
.../has-relative-argument.ts.5705f07d1.png | Bin 0 -> 2473 bytes
.../has-relative-argument.ts.6370d1911.png | Bin 0 -> 2465 bytes
.../has-relative-argument.ts.68a887521.png | Bin 0 -> 2426 bytes
.../has-relative-argument.ts.6ecb1e0a1.png | Bin 0 -> 2427 bytes
.../has-relative-argument.ts.730f6b041.png | Bin 0 -> 2433 bytes
.../has-relative-argument.ts.7af821131.png | Bin 0 -> 2495 bytes
.../has-relative-argument.ts.89871b821.png | Bin 0 -> 2407 bytes
.../has-relative-argument.ts.8998866e1.png | Bin 0 -> 2407 bytes
.../has-relative-argument.ts.8b675fea1.png | Bin 0 -> 2465 bytes
.../has-relative-argument.ts.959a0dfc1.png | Bin 0 -> 2424 bytes
.../has-relative-argument.ts.96d9aab41.png | Bin 0 -> 2473 bytes
.../has-relative-argument.ts.9ee57c921.png | Bin 0 -> 2433 bytes
.../has-relative-argument.ts.a37372181.png | Bin 0 -> 2411 bytes
.../has-relative-argument.ts.ad0a670d1.png | Bin 0 -> 2405 bytes
.../has-relative-argument.ts.bc1750b31.png | Bin 0 -> 2432 bytes
.../has-relative-argument.ts.ccaeb3a21.png | Bin 0 -> 2411 bytes
.../has-relative-argument.ts.da69d1bc1.png | Bin 0 -> 2510 bytes
.../has-relative-argument.ts.e82c3cde1.png | Bin 0 -> 2475 bytes
.../has-relative-argument.ts.f308aac01.png | Bin 0 -> 2410 bytes
.../has-relative-argument.ts.f598325a1.png | Bin 0 -> 2499 bytes
.../has-relative-argument.ts.f6f315711.png | Bin 0 -> 2427 bytes
.../has-relative-argument.ts.f86a4b731.png | Bin 0 -> 2485 bytes
.../has-relative-argument.ts.fa9cf25c1.png | Bin 0 -> 2432 bytes
.../css-selectors/has-relative-argument.ts | 823 ++++++++++++++++++
32 files changed, 823 insertions(+)
create mode 100644 integration_tests/snapshots/css/css-selectors/has-relative-argument.ts.0fba2b1f1.png
create mode 100644 integration_tests/snapshots/css/css-selectors/has-relative-argument.ts.162690801.png
create mode 100644 integration_tests/snapshots/css/css-selectors/has-relative-argument.ts.16d0aa021.png
create mode 100644 integration_tests/snapshots/css/css-selectors/has-relative-argument.ts.1c6db6781.png
create mode 100644 integration_tests/snapshots/css/css-selectors/has-relative-argument.ts.32bf641d1.png
create mode 100644 integration_tests/snapshots/css/css-selectors/has-relative-argument.ts.338cb0651.png
create mode 100644 integration_tests/snapshots/css/css-selectors/has-relative-argument.ts.4692e7641.png
create mode 100644 integration_tests/snapshots/css/css-selectors/has-relative-argument.ts.4e20adc51.png
create mode 100644 integration_tests/snapshots/css/css-selectors/has-relative-argument.ts.5705f07d1.png
create mode 100644 integration_tests/snapshots/css/css-selectors/has-relative-argument.ts.6370d1911.png
create mode 100644 integration_tests/snapshots/css/css-selectors/has-relative-argument.ts.68a887521.png
create mode 100644 integration_tests/snapshots/css/css-selectors/has-relative-argument.ts.6ecb1e0a1.png
create mode 100644 integration_tests/snapshots/css/css-selectors/has-relative-argument.ts.730f6b041.png
create mode 100644 integration_tests/snapshots/css/css-selectors/has-relative-argument.ts.7af821131.png
create mode 100644 integration_tests/snapshots/css/css-selectors/has-relative-argument.ts.89871b821.png
create mode 100644 integration_tests/snapshots/css/css-selectors/has-relative-argument.ts.8998866e1.png
create mode 100644 integration_tests/snapshots/css/css-selectors/has-relative-argument.ts.8b675fea1.png
create mode 100644 integration_tests/snapshots/css/css-selectors/has-relative-argument.ts.959a0dfc1.png
create mode 100644 integration_tests/snapshots/css/css-selectors/has-relative-argument.ts.96d9aab41.png
create mode 100644 integration_tests/snapshots/css/css-selectors/has-relative-argument.ts.9ee57c921.png
create mode 100644 integration_tests/snapshots/css/css-selectors/has-relative-argument.ts.a37372181.png
create mode 100644 integration_tests/snapshots/css/css-selectors/has-relative-argument.ts.ad0a670d1.png
create mode 100644 integration_tests/snapshots/css/css-selectors/has-relative-argument.ts.bc1750b31.png
create mode 100644 integration_tests/snapshots/css/css-selectors/has-relative-argument.ts.ccaeb3a21.png
create mode 100644 integration_tests/snapshots/css/css-selectors/has-relative-argument.ts.da69d1bc1.png
create mode 100644 integration_tests/snapshots/css/css-selectors/has-relative-argument.ts.e82c3cde1.png
create mode 100644 integration_tests/snapshots/css/css-selectors/has-relative-argument.ts.f308aac01.png
create mode 100644 integration_tests/snapshots/css/css-selectors/has-relative-argument.ts.f598325a1.png
create mode 100644 integration_tests/snapshots/css/css-selectors/has-relative-argument.ts.f6f315711.png
create mode 100644 integration_tests/snapshots/css/css-selectors/has-relative-argument.ts.f86a4b731.png
create mode 100644 integration_tests/snapshots/css/css-selectors/has-relative-argument.ts.fa9cf25c1.png
create mode 100644 integration_tests/specs/css/css-selectors/has-relative-argument.ts
diff --git a/integration_tests/snapshots/css/css-selectors/has-relative-argument.ts.0fba2b1f1.png b/integration_tests/snapshots/css/css-selectors/has-relative-argument.ts.0fba2b1f1.png
new file mode 100644
index 0000000000000000000000000000000000000000..c88c9d0eeb862f3a7005d1bd13dca8054c8191e2
GIT binary patch
literal 2424
zcmeAS@N?(olHy`uVBq!ia0y~yV9a1(U~1rC1Bz^vdbAu!F%}28J29*~C-V}>VJUX<
z4B-HR8jh3>1_n+gPZ!6KiaBrZ8ulG>5Mgy>D=e`1DWITW(DH4|(p4E-FS@LK|HSaw
zoX<=S=SsgaGyHh_?Pqq+QvRF0{45Wa#9UwqWa1T2C7Sv=SK9P6=Y&U1Y#qCD|0VB`
zS2)N@ihhtyw|53^p2NVYm(ifn{FfmRL^rZ^2oX&g-QX8^3Ut7&m6BeM&qiNh2JVyP?tcLo~IW$@m)
zfN>Fvl!DhNH5vq?sbDlCjFttX#o=g;Frc;JE6#i)MaHn+xpRT7Ck9VfKbLh*2~7a>
CcZdD}
literal 0
HcmV?d00001
diff --git a/integration_tests/snapshots/css/css-selectors/has-relative-argument.ts.162690801.png b/integration_tests/snapshots/css/css-selectors/has-relative-argument.ts.162690801.png
new file mode 100644
index 0000000000000000000000000000000000000000..328dfa4abddfc0c0cf5bf87b87ec0ec4418ef343
GIT binary patch
literal 2427
zcmeAS@N?(olHy`uVBq!ia0y~yV9a1(U~1rC1Bz^vdbAu!F%}28J29*~C-V}>VJUX<
z4B-HR8jh3>1_n-5PZ!6KiaBrZ81fx55Mg!fDJrnosVT6KV~V1*VX&fB`0Dx3dM%&t
zVVF>C`-_pGX4{(m$1J`m?*7=wFlP$4l!Dg+#zibdQ}>D&r&%%v#av(rJpD}m@LvW_
zy^IEpMz#(iqN!WwdS^Xl@hEU)cFEOyQCQFS?fh-Q^EW5zGe1ZHnajj0ph`4l`fST#
z&VVOPY#qPmAN(gS&vU?Y2T2|Rdi_*_y#pvDvW#!+mwLRM|7I^g%Y!8(JLPL_-W4D9
z0}DykNW5=HRg#c2Mgwa!ut+INM$^k^dKpbGB-Oq{CChweuntxE<1ePi1Z+Stc)I$z
JtaD0e0suNv8MXib
literal 0
HcmV?d00001
diff --git a/integration_tests/snapshots/css/css-selectors/has-relative-argument.ts.16d0aa021.png b/integration_tests/snapshots/css/css-selectors/has-relative-argument.ts.16d0aa021.png
new file mode 100644
index 0000000000000000000000000000000000000000..c8bc7e97f6e2511df9ff9229e192e8edb3837dbe
GIT binary patch
literal 2407
zcmeAS@N?(olHy`uVBq!ia0y~yV9a1(U~1rC1Bz^vdbAu!F%}28J29*~C-V}>VJUX<
z4B-HR8jh3>1_n++PZ!6KiaBp@8+IKI5MVth`M6r&>Sy_ryDmGWwsG-r$!TB$YU}pMGrY4X
zWN_g$ahT%3jGdZU#q0vYmPDxg>$f+3{>-ZW@5~2u)%mcy0P3t2`&AdZ$~$zeGVJUX<
z4B-HR8jh3>1_sUqPZ!6KiaBrZ8fG~gim(Q1R@^_P9p06|-u%n_sAZ^E_~H)_K7F@*
zuES6ym%VoDw|T!=@3A+mp8ryw;lNx6W*1Hqhbh>oEB7Z|n8mN~{NvB;taon<8Fmys
zV(1XcXwYb6!%qE*7mE7AxMW5Zvx{XR15R^6mdymJ0byd)o%P>4{W#-;W1Y+jUJDo(
zu}ER3PF;~!$f|qwHj%JHjlQyV%$i@dzfyr=p9K*vt!3I9Zyfyo^A`h#JA~A~Eza4)
z*!67xtGBg;9r7tOzhTACmES*E5H=VTTr_vcY<8vx-NNkH69g=ktTcDHqJH7Gsqt4k
ze}3izy#&->u{(ZDdO^m&)%SqOPFtN1cLD+>u6SVFfZ~TxVgsoeD0QTy9boFUVJUX<
z4B-HR8jh3>1_sVLPZ!6KiaBrZ8s^Cu3NSccy0Aa|`=`Q$JDf%{HaD;GcAz<_Fj0^lw+%pRubfv~*lX7y
z626yl5h1f}4=PO=;r>o2zrV#Me6ksR>eqUczEzi(Ll&|)`+H1qdpzbEa5Xi(U
zpn3rZ1&o13c8&MeTd%(Nxk7@$kC55ldSvq(7X1$2yO*EAosi+{s+e8w#(%B*?7;9R
z=?EikR{&j@3JkSZ!Y}^r+WgA4zn$^Hkq&0uX#$vDvd-W5$ff~|yxYGS3Q`Cd4i4ea
z{00p|aSRIO`3~=|&kcQFX~RP}_^OZfnLA7b=^`d9B8f;nn}7+TwEpVvv&@Vix(Ou;
zpqEzoFZllY+^X+3a%=>X@Cw`HVJUX<
z4B-HR8jh3>1_n-EPZ!6KiaBp@Z{%xo5OF;id$|4c^K8b6ZBl-Zd=_t9WHn>@ot@XB
zm>cHg_81-KO&q2;j8daP
sFq#TRGs0+DFj^dr)(C^OHoPMK;GHIGx_t3JU_*())78&qol`;+02ob4b^rhX
literal 0
HcmV?d00001
diff --git a/integration_tests/snapshots/css/css-selectors/has-relative-argument.ts.4692e7641.png b/integration_tests/snapshots/css/css-selectors/has-relative-argument.ts.4692e7641.png
new file mode 100644
index 0000000000000000000000000000000000000000..797e2a187a891c0d2c55dbea1badd2c794126b39
GIT binary patch
literal 2417
zcmeAS@N?(olHy`uVBq!ia0y~yV9a1(U~1rC1Bz^vdbAu!F%}28J29*~C-V}>VJUX<
z4B-HR8jh3>1_n-PPZ!6KiaBp@Z{%V!6mdQ1@@@Z7{r=VsV$04mrfZ#E>-Xf~-0Zcc
z3@3ITlSn>O`OCVl(!9a`yTxBdhBurh4pSVMU9eKM(^|r+87!~=eSPK5?Z*roo*rQo
zP`$tq$i$1C`Wh<~^@VZCj4Eaq%R&b1WC5gN{!Mi^|76GEcYNV8Du+G2jE2i-xQu2e
nT&3w~?ikG-qq&2^+>tNhsLkO1Im8CoY+~?q^>bP0l+XkKVd@rM
literal 0
HcmV?d00001
diff --git a/integration_tests/snapshots/css/css-selectors/has-relative-argument.ts.4e20adc51.png b/integration_tests/snapshots/css/css-selectors/has-relative-argument.ts.4e20adc51.png
new file mode 100644
index 0000000000000000000000000000000000000000..c05c2b3ffc42fc81b5d4297becd967f622991c0f
GIT binary patch
literal 2402
zcmeAS@N?(olHy`uVBq!ia0y~yV9a1(U~1rC1Bz^vdbAu!F%}28J29*~C-V}>VJUX<
z4B-HR8jh3>1_n+ZPZ!6KiaBp@Z{#}UAmDmY_GJ6>^ViRrY%~?OSuW)0QL4D@u)-pKTA0!nJU)dgg(quif^*6AY#K7R`>gTe~DWM4f6TS+i
literal 0
HcmV?d00001
diff --git a/integration_tests/snapshots/css/css-selectors/has-relative-argument.ts.5705f07d1.png b/integration_tests/snapshots/css/css-selectors/has-relative-argument.ts.5705f07d1.png
new file mode 100644
index 0000000000000000000000000000000000000000..07349a09d835e79ac841dc8fdd3e0e2463f57edd
GIT binary patch
literal 2473
zcmeAS@N?(olHy`uVBq!ia0y~yV9a1(U~1rC1Bz^vdbAu!F%}28J29*~C-V}>VJUX<
z4B-HR8jh3>1_sVxPZ!6KiaBrZZuHAG6mdP6Jmo*zzB`%$4vz2kcFlY>b>+2Mt4qhS
z-|uB_m~;HhnKh=bU!UW@F@GOFgS!0odIpAcCSC#63k-o+DSPijdymiKU-0zfuj9Ys
z!s{AVnje@c%eLl!1u!N;9ni??8W{$^ITj2!%>&uxvzKwvJO914
z5)6IBCVJUX<
z4B-HR8jh3>1_n-VPZ!6KiaBrZ8u~dKim(Psr{3?FYR&2uFyXiPQA<(Ps}i;^KYh1+
zuES6ym%VoDw|T#L_wX}>$6wyhz);S_E1-ITArLFI-`gqMu3^sc-^*9-+=<@Zr$#t#Px
zr2SJ@q!qI2UcF6Z*g+`Gf#MN_iBb1S97l%0msv*Tuoohu;W8R7quB{p6+4mdKI;Vst0DxbMRsaA1
literal 0
HcmV?d00001
diff --git a/integration_tests/snapshots/css/css-selectors/has-relative-argument.ts.68a887521.png b/integration_tests/snapshots/css/css-selectors/has-relative-argument.ts.68a887521.png
new file mode 100644
index 0000000000000000000000000000000000000000..40970d4ab9f07a49ea9ccc7b8f26905986de2d38
GIT binary patch
literal 2426
zcmeAS@N?(olHy`uVBq!ia0y~yV9a1(U~1rC1Bz^vdbAu!F%}28J29*~C-V}>VJUX<
z4B-HR8jh3>1_n+QPZ!6KiaBrZXbT;35OH;EJ9Q}WX|qQor-g$0h1?%2Rs`!m>$7}r
z!#JV%-fu>RioD7i>EypV*4RH!UGH9Htj}@4apwZYMJ!SZUPMz_5^vZuiX53;bZ6J|
zd}mhKL9|Y&9j6lP9j0(gDP$SnuwVB&;s1G_ulFkh8TgK|@(QS4Uo167J$qo-W5QVU#s%2
z_^2OPN3urJ{7aIXMvW#hHI0TYX^DO`fsH0GqKoR$%r%<1NUT(;Q@)fkFE`Wqr1qnj
R1=xOK@O1TaS?83{1ORa{6afGL
literal 0
HcmV?d00001
diff --git a/integration_tests/snapshots/css/css-selectors/has-relative-argument.ts.6ecb1e0a1.png b/integration_tests/snapshots/css/css-selectors/has-relative-argument.ts.6ecb1e0a1.png
new file mode 100644
index 0000000000000000000000000000000000000000..71b25cd2c76fac779bb3d32b5f368afb6f7d5d3c
GIT binary patch
literal 2427
zcmeAS@N?(olHy`uVBq!ia0y~yV9a1(U~1rC1Bz^vdbAu!F%}28J29*~C-V}>VJUX<
z4B-HR8jh3>1_n-5PZ!6KiaBrZSn@F$3NSeKR9Nh}!@{A`cedee>#CIg6M@-t&dex2
zC(B^@dn-S~f$a9%wrUvQFzSD>`w
zkD=e=cefwaHI&X+z_^G-O2Lb0>eQjS1`S~ohbxZ`{hJ?orT*z6hMXQwQcM8ZrSpcp
z1LT3f=MVkU7H3OP_PM|i$iyq4N;H+4XzwruXz#1c#Q$d{;%!eH-=V-Do!Z3KA(YXe
z(MT+{;%VvPYb8u;P65M(c*{psjt16fdLb#lj%JzBEHj#AMzahFwS4+&JDpGSO}JJ7
P8&C|Mu6{1-oD!M<=PDkO
literal 0
HcmV?d00001
diff --git a/integration_tests/snapshots/css/css-selectors/has-relative-argument.ts.730f6b041.png b/integration_tests/snapshots/css/css-selectors/has-relative-argument.ts.730f6b041.png
new file mode 100644
index 0000000000000000000000000000000000000000..272d831bf96fae8d88dfacf3aa389123d3dac061
GIT binary patch
literal 2433
zcmeAS@N?(olHy`uVBq!ia0y~yV9a1(U~1rC1Bz^vdbAu!F%}28J29*~C-V}>VJUX<
z4B-HR8jh3>1_n-TPZ!6KiaBrZ8ul?6im*ENSXfjS2`eak5NfIp2;Fk)MBr=xPZGuF
zDw!0X@A=Kh@aO%fKiNHN`F~2ta~yEoxqxvIivY3P)La1xh3TCGU_|
zI0$5_USJ4hBAR-AXGNr61BdocSpV@LgG>uIDV_p(|Ju&L;yDbQV1t_KeoTE>7tcaY`24-IVy0ii+8B~T
zg!r%w>18y%4A+{glsQ{Kkum(APaLor#o+1c=d#Wzp$P!u
C*BrV4
literal 0
HcmV?d00001
diff --git a/integration_tests/snapshots/css/css-selectors/has-relative-argument.ts.7af821131.png b/integration_tests/snapshots/css/css-selectors/has-relative-argument.ts.7af821131.png
new file mode 100644
index 0000000000000000000000000000000000000000..f1f6f25a3f1bb70a494372b1ef24c63d569cdba1
GIT binary patch
literal 2495
zcmeAS@N?(olHy`uVBq!ia0y~yV9a1(U~1rC1Bz^vdbAu!F%}28J29*~C-V}>VJUX<
z4B-HR8jh3>1_sVdPZ!6KiaBrZ8u~dK3OG1Q7rvh`c{Y9xCkp%XU2Ix^4m{_D7*OjH_zpM
zGOWJGQ0^wf5Xi(Upn3rZbvrb_VcGXp-)|YOzIXaJLqX~hM%;>l27TS_nX#B(;V4j>
zhB9A+Mk8B?PzDYvd@tjoXX~r<_cJpUBoi?A=_+%FDL@AY5_0gmDrOhcbwEQ|nLf-H
zWOtb2!0f_lf`bAX2+|-~$befB(14wZg!>z{Dh(c6cFzn-akLH>U(8{1Vdhr1q1d(
z1Wj4rEX(#Vb^#-=TApDYA;mF3Z*BsHAfdFAdR1EC6eykvMH?t_5tA0efC+335zz~Z
zhgtDo>pnU#{7EE`5J8U2s=Im{7+qBL&woce=@efY9+ktJ#Ycl@GVJUX<
z4B-HR8jh3>1_n++PZ!6KiaBp@Z{%V!6mdQ1@@@Z7{r=VsV$04mrfZ#E>-Xf~-0Zcc
z3@3ITlSn>O`OCVl(!9a`yTxBdhBurh4pSVMU9eKM(^|r+87!~=eSPK5?Z*roo*rQo
zP`$tq$i$1C`Wh<~^@VZCj4Eaq%R&b1WVJUX<
z4B-HR8jh3>1_n++PZ!6KiaBp@Z{%V!6mdQ1@@@Z7{r=VsV$04mrfZ#E>-Xf~-0Zcc
z3@3ITlSn>O`OCVl(!9a`yTxBdhBurh4pSVMU9eKM(^|r+87!~=eSPK5?Z*roo*rQo
zP`$tq$i$1C`Wh<~^@VZCj4Eaq%R&b1WVJUX<
z4B-HR8jh3>1_n-VPZ!6KiaBrZ8fGy&3b-DOTwKjPzeZ-p)QA~-;sZo2L%tj{sd;&;
z`aP?|x$L!Dua%yyWqQwi;Ntl&>PHz#bWzbx9k0H=ch9QuNS3vaw4hp1ZzC-+ezFma>b{Kt
z13w{SL9YHBoX^jEV4^TP?r;Ew(Mod&CPLknKUb3s404>}6c^Z23Jo
zwgzQ&KHMGxc{Vs7f{9SK;^)fmXPFs4JUGILJGy{Tnfi)Xz|{ULFtP}ygPB##E+9-O
zy$l6)XZ`o)%W*WY6Bm-(+Zi96=w!y8pP-rS)D>w3FeXM_J+2IdFLRB`;VpYcgJ(2&
ia8yg9Nn=DOjr#1q6lL*SKK#ID7K5j&pUXO@geCxm2vvLl
literal 0
HcmV?d00001
diff --git a/integration_tests/snapshots/css/css-selectors/has-relative-argument.ts.959a0dfc1.png b/integration_tests/snapshots/css/css-selectors/has-relative-argument.ts.959a0dfc1.png
new file mode 100644
index 0000000000000000000000000000000000000000..c88c9d0eeb862f3a7005d1bd13dca8054c8191e2
GIT binary patch
literal 2424
zcmeAS@N?(olHy`uVBq!ia0y~yV9a1(U~1rC1Bz^vdbAu!F%}28J29*~C-V}>VJUX<
z4B-HR8jh3>1_n+gPZ!6KiaBrZ8ulG>5Mgy>D=e`1DWITW(DH4|(p4E-FS@LK|HSaw
zoX<=S=SsgaGyHh_?Pqq+QvRF0{45Wa#9UwqWa1T2C7Sv=SK9P6=Y&U1Y#qCD|0VB`
zS2)N@ihhtyw|53^p2NVYm(ifn{FfmRL^rZ^2oX&g-QX8^3Ut7&m6BeM&qiNh2JVyP?tcLo~IW$@m)
zfN>Fvl!DhNH5vq?sbDlCjFttX#o=g;Frc;JE6#i)MaHn+xpRT7Ck9VfKbLh*2~7a>
CcZdD}
literal 0
HcmV?d00001
diff --git a/integration_tests/snapshots/css/css-selectors/has-relative-argument.ts.96d9aab41.png b/integration_tests/snapshots/css/css-selectors/has-relative-argument.ts.96d9aab41.png
new file mode 100644
index 0000000000000000000000000000000000000000..d203da72fd425f341c6d1b6e06f9166b72886f70
GIT binary patch
literal 2473
zcmeAS@N?(olHy`uVBq!ia0y~yV9a1(U~1rC1Bz^vdbAu!F%}28J29*~C-V}>VJUX<
z4B-HR8jh3>1_sVxPZ!6KiaBrZZuCnr6mWHvK3mTmpU3pYLD}-XaEPn4U&)@Vm!G~_
zKG$I=ipyTR^;_KU>}TwA{NFPlID7mHKf?pR1&oVWq!hfcQLoB9E6#0t&YV$c`~SxM
z>L=_os+jLs7BaYSnm9~xV8%}6#(!nY_%*v~y{+-rxO?mk+Uk4_8jWlnLK)bp75`Ni
zy2?9rt*>HsA!PQ?Rpt&?{)F$HzMq+?;PDYg0o4l(flR#Esi&*&F$8`N&gW-7Fjbfx
zw}U~RSqU=t%I}Ab3>AcoO?|~HU~2#Mn*l=%A?MBndI^MyQFqpV@ATu04~}&*V~-1H
z1f9Aft&mms>TM#!j-p2lI0F?F;6`5|m>6}l=2zL<2r%T&v0%WN8bB`a*~_@-+46gG
zYy_iKBRIbSj0r^lP*Atx=gRMgnHfI}P~?2F$B}jLWvfv+?8VJ!xQvF&Xm-L?8IR_U
ch86PnzGcepzdua{*xX|9boFyt=akR{0P#~45&!@I
literal 0
HcmV?d00001
diff --git a/integration_tests/snapshots/css/css-selectors/has-relative-argument.ts.9ee57c921.png b/integration_tests/snapshots/css/css-selectors/has-relative-argument.ts.9ee57c921.png
new file mode 100644
index 0000000000000000000000000000000000000000..272d831bf96fae8d88dfacf3aa389123d3dac061
GIT binary patch
literal 2433
zcmeAS@N?(olHy`uVBq!ia0y~yV9a1(U~1rC1Bz^vdbAu!F%}28J29*~C-V}>VJUX<
z4B-HR8jh3>1_n-TPZ!6KiaBrZ8ul?6im*ENSXfjS2`eak5NfIp2;Fk)MBr=xPZGuF
zDw!0X@A=Kh@aO%fKiNHN`F~2ta~yEoxqxvIivY3P)La1xh3TCGU_|
zI0$5_USJ4hBAR-AXGNr61BdocSpV@LgG>uIDV_p(|Ju&L;yDbQV1t_KeoTE>7tcaY`24-IVy0ii+8B~T
zg!r%w>18y%4A+{glsQ{Kkum(APaLor#o+1c=d#Wzp$P!u
C*BrV4
literal 0
HcmV?d00001
diff --git a/integration_tests/snapshots/css/css-selectors/has-relative-argument.ts.a37372181.png b/integration_tests/snapshots/css/css-selectors/has-relative-argument.ts.a37372181.png
new file mode 100644
index 0000000000000000000000000000000000000000..99502dcfd0e08b73a43594c44d19b9dad16292e3
GIT binary patch
literal 2411
zcmeAS@N?(olHy`uVBq!ia0y~yV9a1(U~1rC1Bz^vdbAu!F%}28J29*~C-V}>VJUX<
z4B-HR8jh3>1_n-1PZ!6KiaBrZY~(uRAmVy3_GJC*`RiL3h#Ag()?hrf)pgZkn;kQ=
z*P1e%xOq$>`OMFc+@HT4US7F*U7dNu|1!(pj0`u_FE9i$@d~J7qrOH6$*tM@_9w%+
zjkfl(XZG$twc3H9%(9Tdh10}giUTutYVWibv04VrW%n2Y{bX<(4zdxXX0WUK=ZM3f
z_`+aR4tv@d4VTey8O=_(O3u;TF`7F@a|dO)VJUX<
z4B-HR8jh3>1_n-kPZ!6KiaBp@Z{#}UAmVy3_GJ6>^Vbgv1P0A!K4;Pux=OgR;CWOw
zFGJBb!($S~b$=|rUw?k9xCkq?Y^Axw<+8fpv#S61
zd}ltOs?OJ-(a6>zl!2XE@qN;TMf?g!uShGL>SV@kEYPk~AT@(sojneJ;tPXOIqYd;
vG+ai*Wi&hCDmh1U$7t>t%^eixjw+@L?=)G%o0qWwn@S9xu6{1-oD!MVJUX<
z4B-HR8jh3>1_n+oPZ!6KiaBrZ8VWTz2rxLF|Mc+3Cu0ZS3qGkEb_pMRv1sMq6v^Uq
zl}rlH_k3q$sCfISMk@Jl1cZ_6HZkH~;R>Y(-V*%qLoj2?qMvlxboF)!a9Ehe`|1t!sWi+ff%VN9k
z^`U<|-xSV~FK}kyJ4CWmlKp<0Gj{m^1L3xRoNdp?%gr^t+)M=lK+`oE**b)XrmFVm
zU76+A5IYm-DHbUOuTg3=2u4%EXhs+<3r35>(HdbuYr|J;%8Dv~7B>gR0b5ZFp00i_
I>zopr01RM?h5!Hn
literal 0
HcmV?d00001
diff --git a/integration_tests/snapshots/css/css-selectors/has-relative-argument.ts.ccaeb3a21.png b/integration_tests/snapshots/css/css-selectors/has-relative-argument.ts.ccaeb3a21.png
new file mode 100644
index 0000000000000000000000000000000000000000..99502dcfd0e08b73a43594c44d19b9dad16292e3
GIT binary patch
literal 2411
zcmeAS@N?(olHy`uVBq!ia0y~yV9a1(U~1rC1Bz^vdbAu!F%}28J29*~C-V}>VJUX<
z4B-HR8jh3>1_n-1PZ!6KiaBrZY~(uRAmVy3_GJC*`RiL3h#Ag()?hrf)pgZkn;kQ=
z*P1e%xOq$>`OMFc+@HT4US7F*U7dNu|1!(pj0`u_FE9i$@d~J7qrOH6$*tM@_9w%+
zjkfl(XZG$twc3H9%(9Tdh10}giUTutYVWibv04VrW%n2Y{bX<(4zdxXX0WUK=ZM3f
z_`+aR4tv@d4VTey8O=_(O3u;TF`7F@a|dO)VJUX<
z4B-HR8jh3>1_sUwPZ!6KiaBrZ8s;fG@-Q5n+x&m;jp>&g7n;`0sIgknC8G68kRu?*
z*jSvwGX8pL?DeSU`x?J9CcM7?QlH_#JO^eMP7{YI*r+S#)fVR3HOyImzWwW*$nW!R
zKW5mFe1uUz^#Vg66EAjZYs^=+jH>Xx{juM^*)ilu7Bb*89%Sd=JfT}Jm^})wN-Lb|
zU{>&2z_^G-3OjWwG{0fR=kUGy|CyNxI^*dobB8Ib?=b|r$>4S?$RF#fm|aZQRmK0~
zW_eIgjN+ZIcm+}k1OmwRD?eA?yT{(3tIXG+(a6>zl!1c^-^;k@-1@3`IrauwLTW)C
z%3pB3IPGe69zXMeK0$Wu5eJRUE3-B}<_#$Mw`w~ux=#}oysPiY%W*W=_gFCCj0aF0
ztpSGG-p07|Geh6+wB=#2CKPbl$NJ12E|L%;APWekoyf0j9aYSG=NYfQS6nT@P)5kn
z*Lr008rJBA!!$$@bNsRo~m$nI7;DbmVF5$CG*RWvo#->;=wfxQvDi
jj=E?xX^i-!v5)b%cqK#5mz^fSHW`DbtDnm{r-UW|6~M+4
literal 0
HcmV?d00001
diff --git a/integration_tests/snapshots/css/css-selectors/has-relative-argument.ts.e82c3cde1.png b/integration_tests/snapshots/css/css-selectors/has-relative-argument.ts.e82c3cde1.png
new file mode 100644
index 0000000000000000000000000000000000000000..7a8b001162dacbac01a20f7fa93272d9bec2c920
GIT binary patch
literal 2475
zcmeAS@N?(olHy`uVBq!ia0y~yV9a1(U~1rC1Bz^vdbAu!F%}28J29*~C-V}>VJUX<
z4B-HR8jh3>1_sVhPZ!6KiaBrZZuHAG6mdP6Jmo*zzB`%$4vz2kcFlY>b>+2Mt4qhS
z-|uB_m~;HhnKh=bU!UW@F@GOFgS!0odIpAcCSC#63k-o+DSPijdymiKU-0zfuj9Ys
z!s{AVnje@c%&B;@xhT!W(BVWjEh*L
zuv4e5NGrVhcjb0FmdKI;Vst06^FV;s5{u
literal 0
HcmV?d00001
diff --git a/integration_tests/snapshots/css/css-selectors/has-relative-argument.ts.f308aac01.png b/integration_tests/snapshots/css/css-selectors/has-relative-argument.ts.f308aac01.png
new file mode 100644
index 0000000000000000000000000000000000000000..0eed3d81e806f7b284186164aa9e1aa538772195
GIT binary patch
literal 2410
zcmeAS@N?(olHy`uVBq!ia0y~yV9a1(U~1rC1Bz^vdbAu!F%}28J29*~C-V}>VJUX<
z4B-HR8jh3>1_n+MPZ!6KiaBrZZsa=TAmDm2@??E?{64WoV$066o?odGeD}lwtNV{FfUt(zs37*#G#W0W;ew-}8BH3a
nNnVJUX<
z4B-HR8jh3>1_sVtPZ!6KiaBrZ8ul?63NSeSKC(ajcaiX%8^Y?(-!0HmQps98v2AAd
zT2qD-mA!_?W_-S5{=tqR=kJ_ujKBF!QHJ~%Mc7!nAzO|N(bzOq!^OAp(7{b?e@4MOT?W#9bB
zrU8t++rJq$q#j|!oqT{v>**?Uhbh1i)F2ecWCe#3u}KZ&M?y|b1tx}9e^z~OXJ>lQ
zO(3a*ytLx;>U+S5(H#FtY?VJUX<
z4B-HR8jh3>1_n-5PZ!6KiaBrZ81fx55Mg!fDJrnosVT6KV~V1*VX&fB`0Dx3dM%&t
zVVF>C`-_pGX4{(m$1J`m?*7=wFlP$4l!Dg+#zibdQ}>D&r&%%v#av(rJpD}m@LvW_
zy^IEpMz#(iqN!WwdS^Xl@hEU)cFEOyQCQFS?fh-Q^EW5zGe1ZHnajj0ph`4l`fST#
z&VVOPY#qPmAN(gS&vU?Y2T2|Rdi_*_y#pvDvW#!+mwLRM|7I^g%Y!8(JLPL_-W4D9
z0}DykNW5=HRg#c2Mgwa!ut+INM$^k^dKpbGB-Oq{CChweuntxE<1ePi1Z+Stc)I$z
JtaD0e0suNv8MXib
literal 0
HcmV?d00001
diff --git a/integration_tests/snapshots/css/css-selectors/has-relative-argument.ts.f86a4b731.png b/integration_tests/snapshots/css/css-selectors/has-relative-argument.ts.f86a4b731.png
new file mode 100644
index 0000000000000000000000000000000000000000..3ec495c674b952484833bd079981de5ac0221a8c
GIT binary patch
literal 2485
zcmeAS@N?(olHy`uVBq!ia0y~yV9a1(U~1rC1Bz^vdbAu!F%}28J29*~C-V}>VJUX<
z4B-HR8jh3>1_sV}PZ!6KiaBrZ8u~dK3OF1zJ++rv-JI1+;Kbf|!{ESwuO!!Qxw-ZG
zJ!Xe<$!TfVW__)E&iw82Z-#U*2tt*Pq&&iLR&2eX3L0>(uwQrM|i<&!VC
z$vbqdzQ+*gCc_ZO#4DhB0SC3NirMAv{#Ukj>?{wC5HR;tXnunR&@GF4EEsTF4suw`
zSGJC*{q5qCTQ<0$p2w8E=@tG2fjj^R_Q%pInHVuOHVH9&zIMnsGOlc7r_FxHHS2rZC1
zPZ3Hgq&V<<_}+LM0fu@B0{H;sz+L-ay-j4WpsG9nqMN7Xc%
fG)8>V*vDvbzKHMj-bpOLrWb>!tDnm{r-UW|x0A~t
literal 0
HcmV?d00001
diff --git a/integration_tests/snapshots/css/css-selectors/has-relative-argument.ts.fa9cf25c1.png b/integration_tests/snapshots/css/css-selectors/has-relative-argument.ts.fa9cf25c1.png
new file mode 100644
index 0000000000000000000000000000000000000000..d7bd5fd85aed2a800afa118f116a53b6edf75feb
GIT binary patch
literal 2432
zcmeAS@N?(olHy`uVBq!ia0y~yV9a1(U~1rC1Bz^vdbAu!F%}28J29*~C-V}>VJUX<
z4B-HR8jh3>1_n+oPZ!6KiaBrZ8VWTz2rxLF|Mc+3Cu0ZS3qGkEb_pMRv1sMq6v^Uq
zl}rlH_k3q$sCfISMk@Jl1cZ_6HZkH~;R>Y(-V*%qLoj2?qMvlxboF)!a9Ehe`|1t!sWi+ff%VN9k
z^`U<|-xSV~FK}kyJ4CWmlKp<0Gj{m^1L3xRoNdp?%gr^t+)M=lK+`oE**b)XrmFVm
zU76+A5IYm-DHbUOuTg3=2u4%EXhs+<3r35>(HdbuYr|J;%8Dv~7B>gR0b5ZFp00i_
I>zopr01RM?h5!Hn
literal 0
HcmV?d00001
diff --git a/integration_tests/specs/css/css-selectors/has-relative-argument.ts b/integration_tests/specs/css/css-selectors/has-relative-argument.ts
new file mode 100644
index 0000000000..187396c1df
--- /dev/null
+++ b/integration_tests/specs/css/css-selectors/has-relative-argument.ts
@@ -0,0 +1,823 @@
+/**
+ * CSS Selectors: :has() with relative selectors
+ * Based on WPT: css/selectors/has-relative-argument.html
+ *
+ * Key behaviors:
+ * - :has() can use child combinator (>)
+ * - :has() can use adjacent sibling combinator (+)
+ * - :has() can use general sibling combinator (~)
+ * - These can be combined with descendant selectors
+ */
+describe('CSS Selectors: :has() with descendant combinators', () => {
+ const green = 'rgb(0, 128, 0)';
+
+ function appendStyle(cssText: string) {
+ const style = document.createElement('style');
+ style.textContent = cssText;
+ document.head.appendChild(style);
+ return style;
+ }
+
+ function createTestDOM() {
+ const main = document.createElement('main');
+ main.id = 'main';
+ main.innerHTML = `
+
+ `;
+ document.body.appendChild(main);
+ return main;
+ }
+
+ function formatElements(elements: Element[]) {
+ return elements.map(e => e.id).sort().join(',');
+ }
+
+ // ========== Basic descendant with :has() ==========
+
+ it('A1 .x:has(.a) matches .x elements containing .a anywhere', async () => {
+ const main = createTestDOM();
+ const style = appendStyle(`
+ main div { background-color: red; width: 50px; height: 15px; margin: 2px; }
+ .x:has(.a) { background-color: green; }
+ `);
+
+ const actual = Array.from(main.querySelectorAll('.x:has(.a)'));
+ expect(formatElements(actual)).toBe('d02,d06,d07,d09,d12');
+
+ await snapshot();
+
+ style.remove();
+ main.remove();
+ });
+
+ it('A2 .x:has(.a > .b) matches .x with .a > .b pattern', async () => {
+ const main = createTestDOM();
+ const style = appendStyle(`
+ main div { background-color: red; width: 50px; height: 15px; margin: 2px; }
+ .x:has(.a > .b) { background-color: green; }
+ `);
+
+ const actual = Array.from(main.querySelectorAll('.x:has(.a > .b)'));
+ expect(formatElements(actual)).toBe('d09');
+
+ await snapshot();
+
+ style.remove();
+ main.remove();
+ });
+
+ it('A3 .x:has(.a .b) matches .x with .a descendant .b', async () => {
+ const main = createTestDOM();
+ const style = appendStyle(`
+ main div { background-color: red; width: 50px; height: 15px; margin: 2px; }
+ .x:has(.a .b) { background-color: green; }
+ `);
+
+ const actual = Array.from(main.querySelectorAll('.x:has(.a .b)'));
+ expect(formatElements(actual)).toBe('d09,d12');
+
+ await snapshot();
+
+ style.remove();
+ main.remove();
+ });
+
+ it('A4 .x:has(.a + .b) matches .x with adjacent .a + .b', async () => {
+ const main = createTestDOM();
+ const style = appendStyle(`
+ main div { background-color: red; width: 50px; height: 15px; margin: 2px; }
+ .x:has(.a + .b) { background-color: green; }
+ `);
+
+ const actual = Array.from(main.querySelectorAll('.x:has(.a + .b)'));
+ expect(formatElements(actual)).toBe('d12');
+
+ await snapshot();
+
+ style.remove();
+ main.remove();
+ });
+
+ it('A5 .x:has(.a ~ .b) matches .x with general sibling .a ~ .b', async () => {
+ const main = createTestDOM();
+ const style = appendStyle(`
+ main div { background-color: red; width: 50px; height: 15px; margin: 2px; }
+ .x:has(.a ~ .b) { background-color: green; }
+ `);
+
+ const actual = Array.from(main.querySelectorAll('.x:has(.a ~ .b)'));
+ expect(formatElements(actual)).toBe('d02,d12');
+
+ await snapshot();
+
+ style.remove();
+ main.remove();
+ });
+});
+
+describe('CSS Selectors: :has() with child combinator (>)', () => {
+ const green = 'rgb(0, 128, 0)';
+
+ function appendStyle(cssText: string) {
+ const style = document.createElement('style');
+ style.textContent = cssText;
+ document.head.appendChild(style);
+ return style;
+ }
+
+ function createTestDOM() {
+ const main = document.createElement('main');
+ main.id = 'main';
+ main.innerHTML = `
+
+ `;
+ document.body.appendChild(main);
+ return main;
+ }
+
+ function formatElements(elements: Element[]) {
+ return elements.map(e => e.id).sort().join(',');
+ }
+
+ it('B1 .x:has(> .a) matches .x with direct child .a', async () => {
+ const main = createTestDOM();
+ const style = appendStyle(`
+ main div { background-color: red; width: 50px; height: 15px; margin: 2px; }
+ .x:has(> .a) { background-color: green; }
+ `);
+
+ const actual = Array.from(main.querySelectorAll('.x:has(> .a)'));
+ expect(formatElements(actual)).toBe('d02,d07,d09,d12');
+
+ await snapshot();
+
+ style.remove();
+ main.remove();
+ });
+
+ it('B2 .x:has(> .a > .b) matches .x with direct > .a > .b', async () => {
+ const main = createTestDOM();
+ const style = appendStyle(`
+ main div { background-color: red; width: 50px; height: 15px; margin: 2px; }
+ .x:has(> .a > .b) { background-color: green; }
+ `);
+
+ const actual = Array.from(main.querySelectorAll('.x:has(> .a > .b)'));
+ expect(formatElements(actual)).toBe('d09');
+
+ await snapshot();
+
+ style.remove();
+ main.remove();
+ });
+
+ it('B3 .x:has(> .a .b) matches .x with > .a then descendant .b', async () => {
+ const main = createTestDOM();
+ const style = appendStyle(`
+ main div { background-color: red; width: 50px; height: 15px; margin: 2px; }
+ .x:has(> .a .b) { background-color: green; }
+ `);
+
+ const actual = Array.from(main.querySelectorAll('.x:has(> .a .b)'));
+ expect(formatElements(actual)).toBe('d09,d12');
+
+ await snapshot();
+
+ style.remove();
+ main.remove();
+ });
+
+ it('B4 .x:has(> .a + .b) matches .x with direct child then adjacent', async () => {
+ const main = createTestDOM();
+ const style = appendStyle(`
+ main div { background-color: red; width: 50px; height: 15px; margin: 2px; }
+ .x:has(> .a + .b) { background-color: green; }
+ `);
+
+ const actual = Array.from(main.querySelectorAll('.x:has(> .a + .b)'));
+ expect(formatElements(actual)).toBe('d12');
+
+ await snapshot();
+
+ style.remove();
+ main.remove();
+ });
+
+ it('B5 .x:has(> .a ~ .b) matches .x with direct > then general sibling', async () => {
+ const main = createTestDOM();
+ const style = appendStyle(`
+ main div { background-color: red; width: 50px; height: 15px; margin: 2px; }
+ .x:has(> .a ~ .b) { background-color: green; }
+ `);
+
+ const actual = Array.from(main.querySelectorAll('.x:has(> .a ~ .b)'));
+ expect(formatElements(actual)).toBe('d02,d12');
+
+ await snapshot();
+
+ style.remove();
+ main.remove();
+ });
+});
+
+describe('CSS Selectors: :has() with adjacent sibling combinator (+)', () => {
+ const green = 'rgb(0, 128, 0)';
+
+ function appendStyle(cssText: string) {
+ const style = document.createElement('style');
+ style.textContent = cssText;
+ document.head.appendChild(style);
+ return style;
+ }
+
+ function createSiblingTestDOM() {
+ const main = document.createElement('main');
+ main.id = 'main';
+ main.innerHTML = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `;
+ document.body.appendChild(main);
+ return main;
+ }
+
+ function formatElements(elements: Element[]) {
+ return elements.map(e => e.id).sort().join(',');
+ }
+
+ it('C1 .x:has(+ .a) matches .x immediately before .a', async () => {
+ const main = createSiblingTestDOM();
+ const style = appendStyle(`
+ main div { background-color: red; width: 30px; height: 10px; margin: 1px; }
+ .x:has(+ .a) { background-color: green; }
+ `);
+
+ const actual = Array.from(main.querySelectorAll('.x:has(+ .a)'));
+ expect(formatElements(actual)).toBe('d19,d21,d24,d28,d32,d37,d40,d46');
+
+ await snapshot();
+
+ style.remove();
+ main.remove();
+ });
+
+ it('C2 .x:has(+ .a > .b) matches .x before .a with direct child .b', async () => {
+ const main = createSiblingTestDOM();
+ const style = appendStyle(`
+ main div { background-color: red; width: 30px; height: 10px; margin: 1px; }
+ .x:has(+ .a > .b) { background-color: green; }
+ `);
+
+ const actual = Array.from(main.querySelectorAll('.x:has(+ .a > .b)'));
+ expect(formatElements(actual)).toBe('d21');
+
+ await snapshot();
+
+ style.remove();
+ main.remove();
+ });
+
+ it('C3 .x:has(+ .a .b) matches .x before .a with descendant .b', async () => {
+ const main = createSiblingTestDOM();
+ const style = appendStyle(`
+ main div { background-color: red; width: 30px; height: 10px; margin: 1px; }
+ .x:has(+ .a .b) { background-color: green; }
+ `);
+
+ const actual = Array.from(main.querySelectorAll('.x:has(+ .a .b)'));
+ expect(formatElements(actual)).toBe('d21,d24');
+
+ await snapshot();
+
+ style.remove();
+ main.remove();
+ });
+
+ it('C4 .x:has(+ .a + .b) matches .x before .a + .b sequence', async () => {
+ const main = createSiblingTestDOM();
+ const style = appendStyle(`
+ main div { background-color: red; width: 30px; height: 10px; margin: 1px; }
+ .x:has(+ .a + .b) { background-color: green; }
+ `);
+
+ const actual = Array.from(main.querySelectorAll('.x:has(+ .a + .b)'));
+ expect(formatElements(actual)).toBe('d28,d32,d37');
+
+ await snapshot();
+
+ style.remove();
+ main.remove();
+ });
+
+ it('C5 .x:has(+ .a ~ .b) matches .x before .a with later sibling .b', async () => {
+ const main = createSiblingTestDOM();
+ const style = appendStyle(`
+ main div { background-color: red; width: 30px; height: 10px; margin: 1px; }
+ .x:has(+ .a ~ .b) { background-color: green; }
+ `);
+
+ const actual = Array.from(main.querySelectorAll('.x:has(+ .a ~ .b)'));
+ expect(formatElements(actual)).toBe('d19,d21,d24,d28,d32,d37,d40');
+
+ await snapshot();
+
+ style.remove();
+ main.remove();
+ });
+});
+
+describe('CSS Selectors: :has() with general sibling combinator (~)', () => {
+ const green = 'rgb(0, 128, 0)';
+
+ function appendStyle(cssText: string) {
+ const style = document.createElement('style');
+ style.textContent = cssText;
+ document.head.appendChild(style);
+ return style;
+ }
+
+ function createSiblingTestDOM() {
+ const main = document.createElement('main');
+ main.id = 'main';
+ main.innerHTML = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `;
+ document.body.appendChild(main);
+ return main;
+ }
+
+ function formatElements(elements: Element[]) {
+ return elements.map(e => e.id).sort().join(',');
+ }
+
+ it('D1 .x:has(~ .a) matches .x with any later sibling .a', async () => {
+ const main = createSiblingTestDOM();
+ const style = appendStyle(`
+ main div { background-color: red; width: 30px; height: 10px; margin: 1px; }
+ .x:has(~ .a) { background-color: green; }
+ `);
+
+ const actual = Array.from(main.querySelectorAll('.x:has(~ .a)'));
+ expect(formatElements(actual)).toBe('d18,d19,d21,d24,d28,d32,d37,d40,d46');
+
+ await snapshot();
+
+ style.remove();
+ main.remove();
+ });
+
+ it('D2 .x:has(~ .a > .b) matches .x with later sibling .a > .b', async () => {
+ const main = createSiblingTestDOM();
+ const style = appendStyle(`
+ main div { background-color: red; width: 30px; height: 10px; margin: 1px; }
+ .x:has(~ .a > .b) { background-color: green; }
+ `);
+
+ const actual = Array.from(main.querySelectorAll('.x:has(~ .a > .b)'));
+ expect(formatElements(actual)).toBe('d18,d19,d21');
+
+ await snapshot();
+
+ style.remove();
+ main.remove();
+ });
+
+ it('D3 .x:has(~ .a .b) matches .x with later sibling .a containing .b', async () => {
+ const main = createSiblingTestDOM();
+ const style = appendStyle(`
+ main div { background-color: red; width: 30px; height: 10px; margin: 1px; }
+ .x:has(~ .a .b) { background-color: green; }
+ `);
+
+ const actual = Array.from(main.querySelectorAll('.x:has(~ .a .b)'));
+ expect(formatElements(actual)).toBe('d18,d19,d21,d24');
+
+ await snapshot();
+
+ style.remove();
+ main.remove();
+ });
+
+ it('D4 .x:has(~ .a + .b) matches .x with later .a + .b sequence', async () => {
+ const main = createSiblingTestDOM();
+ const style = appendStyle(`
+ main div { background-color: red; width: 30px; height: 10px; margin: 1px; }
+ .x:has(~ .a + .b) { background-color: green; }
+ `);
+
+ const actual = Array.from(main.querySelectorAll('.x:has(~ .a + .b)'));
+ expect(formatElements(actual)).toBe('d18,d19,d21,d24,d28,d32,d37');
+
+ await snapshot();
+
+ style.remove();
+ main.remove();
+ });
+
+ it('D5 .x:has(~ .a + .b > .c) matches complex chain', async () => {
+ const main = createSiblingTestDOM();
+ const style = appendStyle(`
+ main div { background-color: red; width: 30px; height: 10px; margin: 1px; }
+ .x:has(~ .a + .b > .c) { background-color: green; }
+ `);
+
+ const actual = Array.from(main.querySelectorAll('.x:has(~ .a + .b > .c)'));
+ expect(formatElements(actual)).toBe('d18,d19,d21,d24,d28');
+
+ await snapshot();
+
+ style.remove();
+ main.remove();
+ });
+
+ it('D6 .x:has(~ .a + .b .c) matches with descendant .c', async () => {
+ const main = createSiblingTestDOM();
+ const style = appendStyle(`
+ main div { background-color: red; width: 30px; height: 10px; margin: 1px; }
+ .x:has(~ .a + .b .c) { background-color: green; }
+ `);
+
+ const actual = Array.from(main.querySelectorAll('.x:has(~ .a + .b .c)'));
+ expect(formatElements(actual)).toBe('d18,d19,d21,d24,d28,d32');
+
+ await snapshot();
+
+ style.remove();
+ main.remove();
+ });
+});
+
+describe('CSS Selectors: :has() complex nested patterns', () => {
+ const green = 'rgb(0, 128, 0)';
+
+ function appendStyle(cssText: string) {
+ const style = document.createElement('style');
+ style.textContent = cssText;
+ document.head.appendChild(style);
+ return style;
+ }
+
+ function createNestedTestDOM() {
+ const main = document.createElement('main');
+ main.id = 'main';
+ main.innerHTML = `
+
+ `;
+ document.body.appendChild(main);
+ return main;
+ }
+
+ function formatElements(elements: Element[]) {
+ return elements.map(e => e.id).sort().join(',');
+ }
+
+ it('E1 .x:has(.d .e) matches .x with .d descendant containing .e', async () => {
+ const main = createNestedTestDOM();
+ const style = appendStyle(`
+ main div { background-color: red; width: 30px; height: 10px; margin: 1px; }
+ .x:has(.d .e) { background-color: green; }
+ `);
+
+ const actual = Array.from(main.querySelectorAll('.x:has(.d .e)'));
+ expect(formatElements(actual)).toBe('d48,d49,d50');
+
+ await snapshot();
+
+ style.remove();
+ main.remove();
+ });
+
+ it('E2 .x:has(.d .e) .f selects .f inside matching elements', async () => {
+ const main = createNestedTestDOM();
+ const style = appendStyle(`
+ main div { background-color: red; width: 30px; height: 10px; margin: 1px; }
+ .x:has(.d .e) .f { background-color: green; }
+ `);
+
+ const actual = Array.from(main.querySelectorAll('.x:has(.d .e) .f'));
+ expect(formatElements(actual)).toBe('d54');
+
+ await snapshot();
+
+ style.remove();
+ main.remove();
+ });
+
+ it('E3 .x:has(> .d) matches .x with direct child .d', async () => {
+ const main = createNestedTestDOM();
+ const style = appendStyle(`
+ main div { background-color: red; width: 30px; height: 10px; margin: 1px; }
+ .x:has(> .d) { background-color: green; }
+ `);
+
+ const actual = Array.from(main.querySelectorAll('.x:has(> .d)'));
+ expect(formatElements(actual)).toBe('d49,d50');
+
+ await snapshot();
+
+ style.remove();
+ main.remove();
+ });
+
+ it('E4 .x:has(> .d) .f selects .f inside :has(> .d) matches', async () => {
+ const main = createNestedTestDOM();
+ const style = appendStyle(`
+ main div { background-color: red; width: 30px; height: 10px; margin: 1px; }
+ .x:has(> .d) .f { background-color: green; }
+ `);
+
+ const actual = Array.from(main.querySelectorAll('.x:has(> .d) .f'));
+ expect(formatElements(actual)).toBe('d54');
+
+ await snapshot();
+
+ style.remove();
+ main.remove();
+ });
+
+ it('E5 .x:has(~ .d ~ .e) matches .x with .d then .e siblings', async () => {
+ const main = createNestedTestDOM();
+ const style = appendStyle(`
+ main div { background-color: red; width: 30px; height: 10px; margin: 1px; }
+ .x:has(~ .d ~ .e) { background-color: green; }
+ `);
+
+ const actual = Array.from(main.querySelectorAll('.x:has(~ .d ~ .e)'));
+ expect(formatElements(actual)).toBe('d48,d55,d56');
+
+ await snapshot();
+
+ style.remove();
+ main.remove();
+ });
+
+ it('E6 .x:has(~ .d ~ .e) ~ .f selects .f after :has() matches', async () => {
+ const main = createNestedTestDOM();
+ const style = appendStyle(`
+ main div { background-color: red; width: 30px; height: 10px; margin: 1px; }
+ .x:has(~ .d ~ .e) ~ .f { background-color: green; }
+ `);
+
+ const actual = Array.from(main.querySelectorAll('.x:has(~ .d ~ .e) ~ .f'));
+ expect(formatElements(actual)).toBe('d60');
+
+ await snapshot();
+
+ style.remove();
+ main.remove();
+ });
+
+ it('E7 .x:has(+ .d ~ .e) matches .x immediately before .d with later .e', async () => {
+ const main = createNestedTestDOM();
+ const style = appendStyle(`
+ main div { background-color: red; width: 30px; height: 10px; margin: 1px; }
+ .x:has(+ .d ~ .e) { background-color: green; }
+ `);
+
+ const actual = Array.from(main.querySelectorAll('.x:has(+ .d ~ .e)'));
+ expect(formatElements(actual)).toBe('d55,d56');
+
+ await snapshot();
+
+ style.remove();
+ main.remove();
+ });
+
+ it('E8 .x:has(+ .d ~ .e) ~ .f selects sibling .f', async () => {
+ const main = createNestedTestDOM();
+ const style = appendStyle(`
+ main div { background-color: red; width: 30px; height: 10px; margin: 1px; }
+ .x:has(+ .d ~ .e) ~ .f { background-color: green; }
+ `);
+
+ const actual = Array.from(main.querySelectorAll('.x:has(+ .d ~ .e) ~ .f'));
+ expect(formatElements(actual)).toBe('d60');
+
+ await snapshot();
+
+ style.remove();
+ main.remove();
+ });
+});
+
+describe('CSS Selectors: :has() in compound selectors', () => {
+ const green = 'rgb(0, 128, 0)';
+
+ function appendStyle(cssText: string) {
+ const style = document.createElement('style');
+ style.textContent = cssText;
+ document.head.appendChild(style);
+ return style;
+ }
+
+ function createCompoundTestDOM() {
+ const main = document.createElement('main');
+ main.id = 'main';
+ main.innerHTML = `
+
+ `;
+ document.body.appendChild(main);
+ return main;
+ }
+
+ function formatElements(elements: Element[]) {
+ return elements.map(e => e.id).sort().join(',');
+ }
+
+ it('F1 .d .x:has(.e) matches .x inside .d that has .e', async () => {
+ const main = createCompoundTestDOM();
+ const style = appendStyle(`
+ main div { background-color: red; width: 30px; height: 10px; margin: 1px; }
+ .d .x:has(.e) { background-color: green; }
+ `);
+
+ const actual = Array.from(main.querySelectorAll('.d .x:has(.e)'));
+ expect(formatElements(actual)).toBe('d51,d52');
+
+ await snapshot();
+
+ style.remove();
+ main.remove();
+ });
+
+ it('F2 .d ~ .x:has(~ .e) matches .x sibling of .d with later .e sibling', async () => {
+ const main = createCompoundTestDOM();
+ const style = appendStyle(`
+ main div { background-color: red; width: 30px; height: 10px; margin: 1px; }
+ .d ~ .x:has(~ .e) { background-color: green; }
+ `);
+
+ const actual = Array.from(main.querySelectorAll('.d ~ .x:has(~ .e)'));
+ expect(formatElements(actual)).toBe('d57,d58');
+
+ await snapshot();
+
+ style.remove();
+ main.remove();
+ });
+});
From 9cc1b417a763c8c6d81613d1ed2ead507a687cc1 Mon Sep 17 00:00:00 2001
From: jwxbond
Date: Thu, 29 Jan 2026 23:11:02 +0800
Subject: [PATCH 04/16] fix(css-selector): add `has-with-pseudo-classes.ts`
tests and snapshots
---
.../has-with-pseudo-classes.ts.038a2ff21.png | Bin 0 -> 2882 bytes
.../has-with-pseudo-classes.ts.13b1f9951.png | Bin 0 -> 2790 bytes
.../has-with-pseudo-classes.ts.16d516f41.png | Bin 0 -> 4397 bytes
.../has-with-pseudo-classes.ts.255491e21.png | Bin 0 -> 5283 bytes
.../has-with-pseudo-classes.ts.3d57c24b1.png | Bin 0 -> 4746 bytes
.../has-with-pseudo-classes.ts.405155561.png | Bin 0 -> 3360 bytes
.../has-with-pseudo-classes.ts.4b071f3d1.png | Bin 0 -> 3112 bytes
.../has-with-pseudo-classes.ts.57ae31111.png | Bin 0 -> 6611 bytes
.../has-with-pseudo-classes.ts.685a66571.png | Bin 0 -> 4308 bytes
.../has-with-pseudo-classes.ts.6c1b8bf51.png | Bin 0 -> 5155 bytes
.../has-with-pseudo-classes.ts.7980e8dc1.png | Bin 0 -> 6439 bytes
.../has-with-pseudo-classes.ts.7b0a89981.png | Bin 0 -> 4441 bytes
.../has-with-pseudo-classes.ts.7d88c1dd1.png | Bin 0 -> 3584 bytes
.../has-with-pseudo-classes.ts.b082184b1.png | Bin 0 -> 2469 bytes
.../has-with-pseudo-classes.ts.bc346ec51.png | Bin 0 -> 3133 bytes
.../has-with-pseudo-classes.ts.c89929d91.png | Bin 0 -> 3133 bytes
.../has-with-pseudo-classes.ts.d081f9d11.png | Bin 0 -> 3705 bytes
.../has-with-pseudo-classes.ts.d22741ab1.png | Bin 0 -> 2645 bytes
.../has-with-pseudo-classes.ts.dd116fee1.png | Bin 0 -> 3998 bytes
.../has-with-pseudo-classes.ts.f55a0da01.png | Bin 0 -> 3885 bytes
.../has-with-pseudo-classes.ts.f64d66ab1.png | Bin 0 -> 2469 bytes
.../css-selectors/has-with-pseudo-classes.ts | 711 ++++++++++++++++++
22 files changed, 711 insertions(+)
create mode 100644 integration_tests/snapshots/css/css-selectors/has-with-pseudo-classes.ts.038a2ff21.png
create mode 100644 integration_tests/snapshots/css/css-selectors/has-with-pseudo-classes.ts.13b1f9951.png
create mode 100644 integration_tests/snapshots/css/css-selectors/has-with-pseudo-classes.ts.16d516f41.png
create mode 100644 integration_tests/snapshots/css/css-selectors/has-with-pseudo-classes.ts.255491e21.png
create mode 100644 integration_tests/snapshots/css/css-selectors/has-with-pseudo-classes.ts.3d57c24b1.png
create mode 100644 integration_tests/snapshots/css/css-selectors/has-with-pseudo-classes.ts.405155561.png
create mode 100644 integration_tests/snapshots/css/css-selectors/has-with-pseudo-classes.ts.4b071f3d1.png
create mode 100644 integration_tests/snapshots/css/css-selectors/has-with-pseudo-classes.ts.57ae31111.png
create mode 100644 integration_tests/snapshots/css/css-selectors/has-with-pseudo-classes.ts.685a66571.png
create mode 100644 integration_tests/snapshots/css/css-selectors/has-with-pseudo-classes.ts.6c1b8bf51.png
create mode 100644 integration_tests/snapshots/css/css-selectors/has-with-pseudo-classes.ts.7980e8dc1.png
create mode 100644 integration_tests/snapshots/css/css-selectors/has-with-pseudo-classes.ts.7b0a89981.png
create mode 100644 integration_tests/snapshots/css/css-selectors/has-with-pseudo-classes.ts.7d88c1dd1.png
create mode 100644 integration_tests/snapshots/css/css-selectors/has-with-pseudo-classes.ts.b082184b1.png
create mode 100644 integration_tests/snapshots/css/css-selectors/has-with-pseudo-classes.ts.bc346ec51.png
create mode 100644 integration_tests/snapshots/css/css-selectors/has-with-pseudo-classes.ts.c89929d91.png
create mode 100644 integration_tests/snapshots/css/css-selectors/has-with-pseudo-classes.ts.d081f9d11.png
create mode 100644 integration_tests/snapshots/css/css-selectors/has-with-pseudo-classes.ts.d22741ab1.png
create mode 100644 integration_tests/snapshots/css/css-selectors/has-with-pseudo-classes.ts.dd116fee1.png
create mode 100644 integration_tests/snapshots/css/css-selectors/has-with-pseudo-classes.ts.f55a0da01.png
create mode 100644 integration_tests/snapshots/css/css-selectors/has-with-pseudo-classes.ts.f64d66ab1.png
create mode 100644 integration_tests/specs/css/css-selectors/has-with-pseudo-classes.ts
diff --git a/integration_tests/snapshots/css/css-selectors/has-with-pseudo-classes.ts.038a2ff21.png b/integration_tests/snapshots/css/css-selectors/has-with-pseudo-classes.ts.038a2ff21.png
new file mode 100644
index 0000000000000000000000000000000000000000..8df414453358a4da2cbfc3cca90295375b015ca4
GIT binary patch
literal 2882
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
QUmh^renMdF-so@t32?6Eq5uE@
literal 0
HcmV?d00001
diff --git a/integration_tests/snapshots/css/css-selectors/has-with-pseudo-classes.ts.255491e21.png b/integration_tests/snapshots/css/css-selectors/has-with-pseudo-classes.ts.255491e21.png
new file mode 100644
index 0000000000000000000000000000000000000000..b536e5b0279e822d1bc95b02794a343a4115ce55
GIT binary patch
literal 5283
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
literal 0
HcmV?d00001
diff --git a/integration_tests/snapshots/css/css-selectors/has-with-pseudo-classes.ts.3d57c24b1.png b/integration_tests/snapshots/css/css-selectors/has-with-pseudo-classes.ts.3d57c24b1.png
new file mode 100644
index 0000000000000000000000000000000000000000..b6e884b8ca6e47dbcbca61d2e47b0f38fdf43391
GIT binary patch
literal 4746
zcmeI0YgAKL7RM7Ff~ClaOF;r;Di?fIpcE8CFg.#*X=qJW@5P@W|S8s3PCwMnQq
z6iEq>4i%>(f)N3c7^t;LuObgEnnFm>T!JFukwg-bkW5^&X4Xts=j+TD_LsBHKKq=r
z)?WL!{^#s3_l1X8f8g)|3Wc%;ckeuaLZMHhP$tEe%Zwhj!>jAY0iAUqBpB7)=lI4r
zG0O@DKesemzGcEC6w1~I+!^%wS(a=xje70GG2UFt<;|h0&o8NK(fUddsB!zVu~K4c4Q
z+;`(=yVt?!D}}R!uJZNXkJp~5>P%EGLJX#YHPP$eh1KWH6ijPb*DSF4vp=HtO+;L|U7Nt3(F$>IYh*8BF!y%Av+#U`{d$@2n6Q3NW_dxxvi7;?IoLiI(b%)FA7=itqv*%AsQ%E}t
zrxRqqWVlkqLIsz=)&Hh0`!iSzAsLYHYlI4P`@S$62Hmhy8grCF%}v5rcepOR
z5mH7VU7t`f?Rc!lV@liBIhe(>)GU`w82YR9cawCz_^9kQRQ2$y67Ck5&u7iH!*kzM
zO2fp%r1|3_@?&KVb*x{=L_$ChrsK#~>22)&*&&nE+^;M&kAN~a%4p06Ph3w{V&XCG
z876$gw#5uxKaus15qm6aTPbu
z@;Yje3fj~UfqrK%LIPRD2KC$Efb|Py@Z4xtN=^S>c{vFbJ(E7)Z<>k@Z7t^;CoJk6XsAwIyxsnkJe5C2`K%
zOOp+}U6pTnbK=;Sx}Tyf6}=xZ+Mc4Kdo^@?i$ir-VotdVh0Bd?F^NA&6bBkW$c{>L
z$77G5{&3c^gIhvKm=0s2h*u&$M{jT)KwUF)sILe#t+4$aFBz3)fp+g9(stx}F2wv$
z`YK>ukAwHQW&gfuSheou5IFBV_AtqnqSVB!LOshK=qDZ-R;?^~yf7&&OV0TIK3K~{
zXyDPdK|62KYKvREd0M?^1X--skAj3sWEd!;k@#u7@7&+&0Wm0-(71mpE*s?HUP~A(
zpY#wqDZ*GA1bR(H!YN@b3)wCyy_e*%N6rE2FcYu1d4f4ZshUhcVDE&x<+3%^z+3K+<0$=nE(PZ9QE
z>Q+_*GZExMs1?fp1derS5=W39ArnO(!&waff`>)Xkc}&j=T
zS7)JF06k)4SX4ZNc&L6RTuuW5s`jQ#?IHIIDP0gJm0XkoMKd_ZY$pKRv2-O1@TJmXE0Ew9
zc{0ZT@OSJ>R=nYHJGX@|pwK9RT0ZHOMt)9oC6B9fMV7hHl>Ia3y0tIJi<_EF<6DVm
zFH3ZQuN!6jZuiG9p1Td6liJr2>?^)|d0#@l41j%6{1*DB7%QmzX-RPjLK^>wR
zwMd2y8ksEkLT6(}B<=c7`m{#@jvp*CWvQEh@}qSrCh?k!i$<6pWm{~Uy8+g=7QV<1
z%wza_!x#o9xxq7$w`oN@O@;}IBM@Bipr{5Rz1JAQwj3cZMtplZLS6#|h+bQv&)UnX
zO9=!Rl;j}W<||;fOdTGasQf!1iOR37K!iXB_9cPDvbr_YMTC-V1bDE)_Pxod+$tSA)Ok8}D--7jCv^m~(9X-=`kmwY
z!ZH%+@v6wGcEKI0F|NJ2MD*0mfk-0kH~#}kdQZ}Oy_(N>*FI=ce|e=(;6Gh|C}B&B
z#&_>h5|)zij~~!dB`j6KQYHMWA+UtQ{~Cu$1KPL!=|=Mh#%_|)z|X>WHV4O_`#b*8
BuS5U<
literal 0
HcmV?d00001
diff --git a/integration_tests/snapshots/css/css-selectors/has-with-pseudo-classes.ts.405155561.png b/integration_tests/snapshots/css/css-selectors/has-with-pseudo-classes.ts.405155561.png
new file mode 100644
index 0000000000000000000000000000000000000000..c9798a51fa8d195f913d8517858d469c5f0223f6
GIT binary patch
literal 3360
zcmeHKYfO`86#fb*C>p%Pm=U#`X2Nh#FWXqPw7^gbPO3&F8z4lS#kr4)h^6+XbzAU4
z>XtbtmzHTJ%n1!R3N5|JDs-Z@AZm3lZLx)xOE0vQuaJ+n>)-v+?Z^K0{qy8~bI!>*
zPo5`l&iUOrnTr?3E`%UxaTYmaF9cyqASmoi)coK}Ypkg;2$=G{ndwlAX4QD`5>cL>
zwJ$1oSW$*2E`de@aduxA83N)3e*w
zl6n3GE6Cf|e|9AunZ+_LXk~8sV|5uP!l$DjaP8LQ$PHj}|+Y^d_q+;^f;OTd;h-?YQv3cD;I*>Z=
zpYrR(9i?b39QfUC7RyUOH0hNtaw_l)t!^+%?e79b5zv(~pNeS)q{2HE<}U(Nfm)u+
zIRUIgDo^-Ov~EW~A4Vr&eknQ&7q;6Oz|d~TIS1wur1qDvQ<7;lin#`4rFUu$^8V4j
z^pzGyyY8@`iYWlu1c(f9^&sBya?34F5oq2L=5K)mCq{qbK1X;ksntGe?aTv;LqI4f
z=GX`tO_XmQ;CNX+2P{fiXUqe{qrh9rH1!qBUe-CUYCgjsS&ajAlISt4pP|(0dZ5eF{5dqHhDvuX%Xdq9s{GO-7*DKc5FHzcLYby@}Y$XC!AXc
zr#Y@)MCdk%>2)6~PXT^DUbh@@<}OtU8D9iwH0HR(=Z1w+JD<{^gh63_s?u@}U5z0m
zw(KgCdA>$+lB_3`wqC!80F;W>z>4HGBZFGxyxyRqq9&NzWv6J!vuZHba
z1FJZYr$M?KD=}W7ohh<0hrcqG9k@*JWI4RugkG*6s|}y}`6PV7oY&!dBF1j@C}QgI
ze=wik(%1$BK%+sPVOsC`IUkP$@P2O)Vg0PVqm84vn1~~)@vi$
zoIiyVwd|{&$0X(fI%}Yyig;IkDzV=yOg0-j?JG6e)jNe;u@-;qin1K&3TOR0H4~Rm
zy54SSz;)n;kVL>4okM$IBZucpp*DN=i@IFXYJ(+`*?R>Tj|=h-vK1U4*2*9Gpag@r
z@SuL?uK(g4%S;Lou!(&0O|@o|R{lvmX*XZEKUOPs-F9KEyHwOViM5sjQF?ArGyFED
z>&W#OTco26%&%!AH=ONysICsnSnxsW52@j=1-Y~QJnP4Z)C)`B9Q!ZIg+gA-c8H!3
uJ+Jp;NFgDGgcS0IPb*}S|7MaW+;I1%u4Tu>n?Db3IZ)R2oQ#(ABHrK32dI$%
literal 0
HcmV?d00001
diff --git a/integration_tests/snapshots/css/css-selectors/has-with-pseudo-classes.ts.4b071f3d1.png b/integration_tests/snapshots/css/css-selectors/has-with-pseudo-classes.ts.4b071f3d1.png
new file mode 100644
index 0000000000000000000000000000000000000000..8e4e8fbe6bdc4726c4bafad9d9597451f93638ce
GIT binary patch
literal 3112
zcmeH}`%hDM6u@tRl?pb*xkM}vNtkho2I7OKeGFD6U;-O6!NsK`T%jz`A}wO2y`|lz
ztHXgH!X8l2nK8#w>@>YFT0l|mSh01mt+Fq)2uNe)g5^>A@V4t;;QVz-&o3wEk1cht1GZhs;P3yx$$Dns;Be-R?g%sEE!
zhlFxc0YTvtIqZyMqIP^n-^;f`nBDR;>7U%(tnF-%N-P&dAPAzeJf!&L}RD9in8-+<=Q>rWjM*IN!*$?qznKv?8Wj6URtHjJG;B
z5mWyu``rEd;5h5~X9x*uU;g1}euBh#cSG0gtJ=X@h}br3JMbA<@p_r;Ejd|{F7EQ3
zPB4*%OZc_-{_aH?)xW=wRt|_kB8pUl6R6G>ty<&)e%fe|lePKV+jZpt%}=wv2F%Fk
zZZj{xuK9Z}3^0wZ2I%vbeA89SoAj|YR+E}k%uheP;zP%Ic5NJ5SPb497@O3B-Pc)
zV8W%>SiF&PslB8!+I*HSC7j3244_=S-n+r_ipCW_L9rNk&r(OpedqHc)=(KH
zgy^8X95BU8;wcTvXb>{Kt>k$o>@QmPMhiJ`@JFzp+TsVB$_F~k_zsZX#Cx!UvF(0L
ztcFy}0N$}Dh?vzhwy!;cOm|#$e|c^I4nnVcF47*lj5xPiT_bj55HFwco$UT#cd%C08j@%~#n9yr)&l8Pk<=C5;O%E!*|JO;HH8PRkq1xuHA
zM#U{O$)!J^$ox34QjP|7-QW81saIGO
w7{|x{bxUG?QcKdUf=V+
z@As~?fBD`QZSt3Ge=#sHFu@!Yvsb>1S%TjaKMys|Y92-x)mVv7FQo
z))2nK{IE`crL7D5n}NYr66Wx?KU^-6j!>!t!b`<7+}nq|k9`LKW*Z!??{I4SInK=M
zU*Jt+SxzCVHbobfMm|55@UAre>+@a-KZJD7cQl-(XXY3l`M56n??dO;p7{Di+}-sJ
zS9h9?ef9bG)?I7XeYtw^%8m%#>pqOYS8P0ZQ@;7>nfFyPchzKtDeX5ED!FHp9U3^0
zxT?IwTr#=cAf(|l7BbX4TRMbV)CDA6q5RF@-uSw0<-4yK?LJ<<>PAZJYL+X}&|0Gu+Q@I72f!)W($@H~G78>8LLfk{GS
zoz!JAI53C9<_WI@x*Z%X4k`lR^$%#apk51@zT!qa9HCztv-
zCN#%y^z#el<)y|;kLn#rp_#K7Y1^<044?`Fal$=IaY#}`>S!J
zRF;o)<{?0%JGHRa&a;65lS26vMi%oNRW0r7brnb9x}+F3Zzm-_bIBg3rAd_B4IdCS
zibE>>x7d@OkK7d211&zaNuZn|rQ`{ZSPwIqST{ev_ykD#JCJH~t{)!jlc|V?%n00v
z0GzQ!UMlRbr%?E!W8h0_+J$TC4Ng0m3?q`T_3iW=Q4JB*--BDNa|pZ>M6hf7pnF5x
zOe7Ok{c0(g2Oh)g`swOi0P)zd2jlzUui=MDbv3s@K;j3*&a5SVo1-B1_rK
zGH#!{<09#HwzwW+IQa4?p>QOtzN@D4-kax@Hb#Qa7N)LrupZpHZ3xM?Uv{e+d4h2c
zKUUIrm&LsaA9c$Uo?#B+e?o=qN5AQ)qb39&j}O2eJqzXU+8F)T8)Wj9X1V}l+jDn>
z8TQjEWoe8AaW^nAE|7UO>^QSTF1iwDT(_xE5Y@6~z>%Z8s@W=QE%_KykKM-X1zWu_Y2CF
z7^(*t)_oXK^lOv_Z`MHRdE!$WwT6J=
ziI&7>U2CVAJYwO<;8}9M@XC(#3pR@b)+x`^sm%_Y(|Wez!DdHJ5zt5OXk-htyB7x_
zm>I>t1Xy!4W>^3$Cx9*6ohR;Zwl}zg^wdR4xJKH+d7(I#Vi0-#$+s}~>BLJAISO3F
zeO)e|0c-&T6Gh(331*11=}$vJx5o7C3qOMQpe1}uA6Npf#zldfpp)%&Kql1NUj6_R
zBxui~Yt*vVf?!*OHAmtq%BAiV%y2*ZfPl}fOF{E{`_s6-v-f~a)PMq^2%y#NqrC5@
zalPYX!E6lqW*FM8u9CXftM+_|n#)^8CDqfX8ZizRk>vL`H|2OlkL4Vn6@ZdHa?RdI
z0dgm233%#V`&;(`B*%x;5@xVx%eD!f+oecN1tzF8OaPpzGax(b<*4x>XDud3k7E0x
z2;0WAHYBfD-VS8ct23!xUbVIOVO=BxZd3SHRtAP$0BtbtCBjPko-@0Wqb(FPdy$UN
zrZ$=OHZhq?GP2S^Z1sBHiP;SQtX%I|t&;VnR2db#s>VVVxzD{058You<6S)DM_n!k
zimAgR`My>@&Ij~LSis~-EsTQ3xNElU4vQ?u!wzB&PLQNGa?2RjfACKpmM?uY&n9~)
zLO{cen6d-Y3vj9V5uNf8MmdG`qBM-i7@3U*%KH^`qY*_gBy9#@QMS-7TSPf7SiM0s
z{Qh*C3(-j)YM2{gQwR#}NVc0)+qU#EgHklDU+zgKtdd~-)TP(pyB3+m)w1={$azm`
zD$DvEHfbMP$JC~v1*R&T+6VcKTvbbSj+<{)P8nOl@`dcpAI(jEA%K>>u`M3en)W~m
zyp<3&VK!HBeQ#XJNPnWBNH_@S9-$V8iFYrZ2gj((C$O9Nj+{!+?Y6K3x7-Hk_HcBV
zcu(Ze<-^6gWk8Ojb!%ul)OEj>3bBI@EfA6;hTa1jZEaO{OHDU}lDXuAaep(K<_{+A
zO2&e^JHl5|=jN8J-P|ui_BY+RudcMWGx2O8-%gN6s%W_6P{M%C2=9I|HGx2taqeRa
z6DmxTwMDMlObnck?{S#qBb}607*+dy^)vRsBq%#Y;`;QK-b$d+)zRv9lEdT$lP-kq
zq)cw1kk(hJH1|N+lvE2h3QA(I4fM>3h|B7wN>A3C*G*-qY1_yU|3fdudFB@&7bs+B
za^DROJ5&~%VNMWCX{UJo@uzHCc^#=7rELbtALySZ~gc4$e-@6
z`LA~TPlolR;a|C+!R}XPdQHLRmUlSJMiE;wWvg|oR~_wKnf!O5nIB%0f&(cRDaO%X
z^fMN%U~kh}eB*V06Y+dH^X)rL;KNR}i?T-Xoq$v+eW)&~FE)!$f0{OK*EscrdDPi_
zx+}|c&XarcSDDxBDC2Rc)gsmvnrdGxKo`Ld#U5nlz7EJpZn`7nZJ*&d})g((34svZ5BP|phz=O{j
zrr9_Rji(d4`8yol_=0|LH&Cn?3rx5a70c{+T~Mp>sik?c9?ikVZ;RvXp`K5ol
z|GL608)h^eGsLpV@uW6CaBQ&P`Oo92l2V}>rw#^^->y^1WShBt*e|HU+t{lG;iR$t
zx`QpQ9GDU(c=ugO{ED5>YLbhsF&pvdmoJwEMEWtV1VGapeQ`Rl0l_)Xc|{8_7mv_!{k;BpV)1=<_LbI-g*Np#_tZL1DWEzU%K9Rz<+M=F4%S?qb?)JI40j
zU_E)xHO7a>HhjEZa(VO@40t4S^Y}_dzC06vdbeDu5PDF3CH1WeTT5t?go5Fpg2P^Pf!)2{XnyDMPZp3QvM~$QO;w`?m
zDp~~_`n<0y&KE31o{P|Sx1P4t7&9_}?^vQB)4LRfvH766;
z>JfR4g{qdu+Pgw8S=i|OK(L2wW=Do42qfz@iO{;rIL2HIBu{z;7Xgjq`PghUAUJs2%_v^zdtRT-0dw=5C;|vI@qH@iNx$vZ~i=`D|)73SC#}@Fe=oP46h0
z!X)TU(jjEtVyV*WLwWrM#Uw%`H2aS_&;p6)^6jztk4`bFVD|oXyS?)Fa$fwrp1@BQ
zhs#;29cEN=o9|w>D1P&0JyXV=Ti#uQk{p%^zpPKKPVVD^h(g
z=bJrc3vIT}sY=O;s5h$ksRZq!*rQ6PX?=D-I{ti9^PWJ^UghXbBk;yfsp|s^9Plxa
zV1Oa5OBiC{KH~s)DF-pMT_+c&fs8Itw4^~0LL$dEU(}V*0_qRkCasKru5!(AZ;6Q?utg=B6@y1U0EA9*4}jo
zg>+2&Sg-r($y9C1{2$(B?kK%Zj%Uf+BFm9A+t2Gh_>ZS7n_YNyDIShSA
zFQ1S7>3gWQemz}F*?t4&o20^n2EVHKy^fx`y1{l?cKjPU3547A5GGqEEI1^
zzX5wU`llV#KQUE*ivRTVe=(q+YV{w7WuKt(2`c}2a{37jpTO`54FA_tjQ^J=vP8K-
Y?Y3E%4Q1(n%ot#f_#S@pUC7mc1#^xgH2?qr
literal 0
HcmV?d00001
diff --git a/integration_tests/snapshots/css/css-selectors/has-with-pseudo-classes.ts.685a66571.png b/integration_tests/snapshots/css/css-selectors/has-with-pseudo-classes.ts.685a66571.png
new file mode 100644
index 0000000000000000000000000000000000000000..c34e4183938ae2e1d86c87b6a648b7bd4ef45212
GIT binary patch
literal 4308
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
ChRRm}
literal 0
HcmV?d00001
diff --git a/integration_tests/snapshots/css/css-selectors/has-with-pseudo-classes.ts.7980e8dc1.png b/integration_tests/snapshots/css/css-selectors/has-with-pseudo-classes.ts.7980e8dc1.png
new file mode 100644
index 0000000000000000000000000000000000000000..8dd894b2be884a893925143169800301806419ca
GIT binary patch
literal 6439
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
literal 0
HcmV?d00001
diff --git a/integration_tests/snapshots/css/css-selectors/has-with-pseudo-classes.ts.bc346ec51.png b/integration_tests/snapshots/css/css-selectors/has-with-pseudo-classes.ts.bc346ec51.png
new file mode 100644
index 0000000000000000000000000000000000000000..7285d2d66871001827d640a14eded40816d33761
GIT binary patch
literal 3133
zcmeH~?N3uz9LEoj1t-EZh75UdQ-%qfCIi+=c_@rUnM*6i1s0&)3j-wfwjCY9xRhI9
zn*swy(2)&Yna<(HkdW%F46)!O3!BpF0BsL+h07?lT#MLV3YB&jvM2Y3f1u~hH|KZG
zNlrfbet$Wq_AvKA(5A3W0Dz#(gBeEv@G1hpTOQy~S9-#xJ81Ee9yyQ>E{}wp>4~p2
zJ@aS)Jt_i@odV!FQ)Wim(XzATB(D=GmHqr!b2sFbthYnHH*%1y?|Ss-jTvdp+*?&G
zZ!x%M_v66>sXhvM`}R!{AIx3ieO>cXSi|%28HYL#AH3LQSC7Z<=Z@PAEvV?cWYYAi
z(_Vc?kBuAIQ#3OffcvO%yrrI(KQX@>Gww~Dst=H0z19Awwie0v
zRISS;#-ruo-(Y1A`3~fNc<&2vt*~98qdq2|>S6Jfz+QmEl2ZUal3
zZ3K+eQB{PUxi-J=zk;b&op`AbwLf;~8*ckc#vg65F&l~8CPEO&+^oj#^@0(@7)Ky-
zKE&^fQAe5eF5$Q}T{4cQW=?ab9Dw+
zd?kEct~|lk3Vx@w@7F9Os&1}!p%q&OWF=@VabaKatU~CVv7+HX)YBHhInD9O2#nuQ
z^7Ht7zQGm(ChlyH=ZuQ+9sQ2cp*!3~#-qh;H%@`2*4Tgeu3;`W{?h5zQ!T{u&|~v1
z$gkX`^g4UBv&yxIcjrmR*(%w>F&cDsyLcHQ8pLVFF&*^)PlZ%Rj3U@5%vbxb)*;;!
zlpe7r8=Q)k5kCpVfBN!NSAzK@W>o^oNlPm$5>jqhd4=qR{C?6NqX;tokRLUN8K`+&
z5z!r`qi*92UgC~{cl%PbQK^j@1TBW4s(v9n$z7|WQwejeLe!QsUA98sW+8WScLn8;
z_N68cCxP(Gg6XoSIR?icct_*axl%>N8rB4@3jfH}K{$*wS{XEyEb73BevE?|%YcNK
zFJOd3YeANG(1x<^S!S{?D3$sJg_NLuckAmzlbS=<{Hpk$UyoyB&R&EkbQ*B07%hTO
zwYqFJ`LJ?J{Z?ub+CLXf{qAoO-Z{};xw14`OH&*~QuWj_!MNcrgTX49X|}+Zbtv0=
zwHI;skWnhxOyiP3ngm$qVwr*J;&AtDL4lWX3%y|!S4~((UIZ~E(OP#e+1%FVCuipc
z+AMgoOuB|WwyKiAF-Q?@Z4CKOXE)kRpp7CgSbG1IvzCDgznT1$WCZD
zPuN@ORpsQK4ed!gc$8E`U1{~@)IDp&e`Ehw`aSY@AM}v$ka(tPd$RJMSAr)Uo^*K9
fu|Z3?B=FGd!;2a&+~
literal 0
HcmV?d00001
diff --git a/integration_tests/snapshots/css/css-selectors/has-with-pseudo-classes.ts.c89929d91.png b/integration_tests/snapshots/css/css-selectors/has-with-pseudo-classes.ts.c89929d91.png
new file mode 100644
index 0000000000000000000000000000000000000000..7285d2d66871001827d640a14eded40816d33761
GIT binary patch
literal 3133
zcmeH~?N3uz9LEoj1t-EZh75UdQ-%qfCIi+=c_@rUnM*6i1s0&)3j-wfwjCY9xRhI9
zn*swy(2)&Yna<(HkdW%F46)!O3!BpF0BsL+h07?lT#MLV3YB&jvM2Y3f1u~hH|KZG
zNlrfbet$Wq_AvKA(5A3W0Dz#(gBeEv@G1hpTOQy~S9-#xJ81Ee9yyQ>E{}wp>4~p2
zJ@aS)Jt_i@odV!FQ)Wim(XzATB(D=GmHqr!b2sFbthYnHH*%1y?|Ss-jTvdp+*?&G
zZ!x%M_v66>sXhvM`}R!{AIx3ieO>cXSi|%28HYL#AH3LQSC7Z<=Z@PAEvV?cWYYAi
z(_Vc?kBuAIQ#3OffcvO%yrrI(KQX@>Gww~Dst=H0z19Awwie0v
zRISS;#-ruo-(Y1A`3~fNc<&2vt*~98qdq2|>S6Jfz+QmEl2ZUal3
zZ3K+eQB{PUxi-J=zk;b&op`AbwLf;~8*ckc#vg65F&l~8CPEO&+^oj#^@0(@7)Ky-
zKE&^fQAe5eF5$Q}T{4cQW=?ab9Dw+
zd?kEct~|lk3Vx@w@7F9Os&1}!p%q&OWF=@VabaKatU~CVv7+HX)YBHhInD9O2#nuQ
z^7Ht7zQGm(ChlyH=ZuQ+9sQ2cp*!3~#-qh;H%@`2*4Tgeu3;`W{?h5zQ!T{u&|~v1
z$gkX`^g4UBv&yxIcjrmR*(%w>F&cDsyLcHQ8pLVFF&*^)PlZ%Rj3U@5%vbxb)*;;!
zlpe7r8=Q)k5kCpVfBN!NSAzK@W>o^oNlPm$5>jqhd4=qR{C?6NqX;tokRLUN8K`+&
z5z!r`qi*92UgC~{cl%PbQK^j@1TBW4s(v9n$z7|WQwejeLe!QsUA98sW+8WScLn8;
z_N68cCxP(Gg6XoSIR?icct_*axl%>N8rB4@3jfH}K{$*wS{XEyEb73BevE?|%YcNK
zFJOd3YeANG(1x<^S!S{?D3$sJg_NLuckAmzlbS=<{Hpk$UyoyB&R&EkbQ*B07%hTO
zwYqFJ`LJ?J{Z?ub+CLXf{qAoO-Z{};xw14`OH&*~QuWj_!MNcrgTX49X|}+Zbtv0=
zwHI;skWnhxOyiP3ngm$qVwr*J;&AtDL4lWX3%y|!S4~((UIZ~E(OP#e+1%FVCuipc
z+AMgoOuB|WwyKiAF-Q?@Z4CKOXE)kRpp7CgSbG1IvzCDgznT1$WCZD
zPuN@ORpsQK4ed!gc$8E`U1{~@)IDp&e`Ehw`aSY@AM}v$ka(tPd$RJMSAr)Uo^*K9
fu|Z3?B=FGd!;2a&+~
literal 0
HcmV?d00001
diff --git a/integration_tests/snapshots/css/css-selectors/has-with-pseudo-classes.ts.d081f9d11.png b/integration_tests/snapshots/css/css-selectors/has-with-pseudo-classes.ts.d081f9d11.png
new file mode 100644
index 0000000000000000000000000000000000000000..86ab945aadfe38241a8d66d0569b91d297371879
GIT binary patch
literal 3705
zcmeHK|5s9H7{7`+%cFMMnoTYH&Q57=!;P2*<}5SM(dC+7`U1^1Xw#^9LnRbq&DG{n
z*W4sj#J1yD>M&*LOcdKvNiX0sC4(fj0KW{Pn+O83ORd`W7wiY`FVA_O_qoq~p3mp=
zyyv}NzMBy1vm{^%002I5yJC_6!0iYCxS#U!L_CcFV*r7c%O0uJw*ipHLULE#7g?t9IAgHA};Ge(mAa;EDR8{H`Zgbpc--
z9h(QWu(Z9{mc*qnM5V>b8r<;W1Jg|bPmQQ
z)A`(CfK*o0b$V0ZDv=8A>C^p~Lait)COIzh8MK}OnB?#533Bx5l0iX6l_M=kgi1ad
z&&EHHUueB=mGF;NsC1T;kxKMzur&f3tG`fV(69@8?NNLFdhII+zi?ys4Zp9w@OEW@t6S`L+;F=B7C
z9v{DF*L_E@Mz;;qsp(iB>FPjl83gT{kICQRj6`5C6jrW0&u9&n
zePmoUC#8^XS*3P+r$T%BC|Y|V-pF$}9a!)WQ=P6e;tXEN4;;b;9^#eV-cCY5#$e!k
zRWBqMiHet`TyEdUwr~w&J{BX$79|?!ch_ZP4!%u6)ej`GJ07%erjWIM5y3p%jGzFs
zC~LGS$B#E(`ukXUSHS+u@?l`=m%Y{glp=rh#w~fq20UEeMR{2nGIXofRXaLynR`s=
za{V|3NkALQC2OU0i4DqlnV)t*lzO=CNl{B4xoGBc8zhWz2og7qU!;GaL-W7N{Xn-4
z?kyJJvt;Q-Kkw6Jo}%sB)GyJ@)N#3wxjG8kAjs9Vd_`~9o)+9pzw~nKF{|`xWjAqp
zon%fbzA`@X7(F5DSwI4=0Yw2k>zVZsc^a&ArWv#nU{p~VT``rRZ@CHO3`GzQX58^9FFLwLh
zBVhIJ>Src_KL%8H`39_>RD{LfKJGi`Z7TG-l%CxWATKQ4@Z7{Z`sy(BTw@1a|69EF
z;z3dcR*uUdlg(AREFsQ3ls7BwgLzou{`>i4GAb`^-d^4NP0uv54OFW02Whay+D4cG
zl@f8t)|I*K<5UI%q){xTSx>;LAGeBvOS#bnVdNhw%FJ)d1ZiYH@#QF
zOkYhQHy@+QOZX>GJkEk-Ga%bxCrYO(eJ*W@e^RXp&QUBvkMJY>2+fbia#k@jw`?`d
zM>6LPE}Rs!)2`qIB$(q*d;^1VH6$<)2~?X_tDYNkIw*ghN%*<({ijh6gbgwX!j@1S
z-Ys2kCi3<@*WXyZY3$mypjjQVq+wV3E*VCDLM4{7V^QnjV)xd7RzX6
z2Bd!~dsFbY+yyo6`S3Xof@X(1S#wN0uD4<(jM+a>egz*MhhHM<&H01
zlqyyp!SAeQFNfE!iTXAQ6|*?}skxaZU}Y_FTkX>BKlJ=>buX;`Gwut3ECBNC>|YSW
pf*2OW@cb^cFc1H?d6;CmVOWdY<0hNRkl#ljZbw3lF#6DEe*tkZn%)2a
literal 0
HcmV?d00001
diff --git a/integration_tests/snapshots/css/css-selectors/has-with-pseudo-classes.ts.d22741ab1.png b/integration_tests/snapshots/css/css-selectors/has-with-pseudo-classes.ts.d22741ab1.png
new file mode 100644
index 0000000000000000000000000000000000000000..f5a2201f8555ae2c5604cc78b19f53ef32cb7628
GIT binary patch
literal 2645
zcmeAS@N?(olHy`uVBq!ia0y~yV9a1(U~1rC1Bz^vdbAu!F%}28J29*~C-V}>VJUX<
z4B-HR8jh3>1_sXmo-U3d6?5L+vF%Dpm1ujoywpH2(9qzDp`nD}QPmleGj}kAFH(DV
z@C5HqV=m*#9J4wnDlD>4N)7V0w(mU|QR^KqF0(JV)bE3vdHh_hS8oa|WY`?e1&7-*
zF!Yx{{`2I`Ueywur~Gf+}!l>PEk@8`2^&;J$Pxl>ku
zyFA=T`M}najm`g(Z@Jew}=tqd+(I&4TxG*UjaNuNxmPxPSZG
z>yv5BJFe+5)mGo$d|aPH{<+Paa{Zlr2ZT#Mc04{@zw`6fT>1akA2Sr3+kcbMh10}g
ziUTtt>dMWB1r{-11sH5j@kuFoEnr;4GD?jG!DuQN%?P7q!Dw+fS|g0shUCbP0l+XkK%qbHO
literal 0
HcmV?d00001
diff --git a/integration_tests/snapshots/css/css-selectors/has-with-pseudo-classes.ts.dd116fee1.png b/integration_tests/snapshots/css/css-selectors/has-with-pseudo-classes.ts.dd116fee1.png
new file mode 100644
index 0000000000000000000000000000000000000000..234ee4dd6f114328aab598e3a2e6d1ced3449184
GIT binary patch
literal 3998
zcmeH~YgAL&6~`}#1hK$?b%`MmJ6y4wrhFGG4Ac;I8Bnqt+
zGzsIqFo;BmWocHc5D*C{ASg<_1`2!-89lax!Ip!5!TjKFRxe`Ct?Bt?8ch)Y-
zDy)A{h6@X8s67F3{S#iwijvivlT%7Pd`lqiGlx4&ZPs9PYG0~h
zs85UQmJ0%!^51c0Rk3F7EU){x
zA3C+$((cPhp+)41%{%_GEYnFn0#+oT)nqDT{7-~cfN9mZf;87%J~j6NlpcNPo6syn
z+R3O98DTa#SdU)J+SE^tWISj!X~2+3y#NgCk#v(^&&KA2!PCe`v&L%lB6Cwe`9gY7
zdO^^ulQXm8T)w|Nk+~CI@T~NzT|5CZdn5z!6h+^w8(9l+1Xw$mkBnqGJ(osvdIH84
zKITx`2vyi+nw9Aqm?Zdko#=UMKa8uN)GkjR=zC-Mt0s!k;d?eHEi^9m?PvJt*-;u@a8m?As%zda4js@9R6NK)fa*aqWfGM-=9d%h^Uik
zErpMsFJo&_V<3t31+1c222Hm*KCa9xb(}+yX7^l*o`35ZJGX%Se71BQlu?0sLGlb}%^mzVqkAZL!GpU%wf8+_@n(&px>-zQBqt0#z=PwGPB@
zjA}gR@f|QJO0NTRo%gtl#0@GPZM`z0XLm<;58;DYy8CvwbQdQ8rNRV1C1pFqPHNe(
ze52bqd(5s~!I#P)HyERDn96@>JM8A)GZ{jldnvc0%fh+2l$pS-Xf0CUfr>nb4f48d
zP9E&T^*+8<%pV7IZ)o7MatH)Env_Dm(7DcyHFZ(~hhFzOzcw+QR3$K)vLw