1 // Copyright 2020 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 #include "third_party/blink/renderer/core/css/element_rule_collector.h"
6
7 #include "base/optional.h"
8 #include "testing/gtest/include/gtest/gtest.h"
9 #include "third_party/blink/renderer/core/css/css_test_helpers.h"
10 #include "third_party/blink/renderer/core/css/selector_filter.h"
11 #include "third_party/blink/renderer/core/dom/document.h"
12 #include "third_party/blink/renderer/core/dom/element_traversal.h"
13 #include "third_party/blink/renderer/core/dom/flat_tree_traversal.h"
14 #include "third_party/blink/renderer/core/style/computed_style.h"
15 #include "third_party/blink/renderer/core/testing/page_test_base.h"
16
17 namespace blink {
18
19 class ElementRuleCollectorTest : public PageTestBase {
20 public:
InsideLink(Element * element)21 EInsideLink InsideLink(Element* element) {
22 if (!element)
23 return EInsideLink::kNotInsideLink;
24 if (element->IsLink()) {
25 ElementResolveContext context(*element);
26 return context.ElementLinkState();
27 }
28 return InsideLink(DynamicTo<Element>(FlatTreeTraversal::Parent(*element)));
29 }
30
31 // Matches an element against a selector via ElementRuleCollector.
32 //
33 // Upon successful match, the combined CSSSelector::LinkMatchMask of
34 // of all matched rules is returned, or base::nullopt if no-match.
Match(Element * element,const String & selector,const ContainerNode * scope=nullptr)35 base::Optional<unsigned> Match(Element* element,
36 const String& selector,
37 const ContainerNode* scope = nullptr) {
38 ElementResolveContext context(*element);
39 SelectorFilter filter;
40 MatchResult result;
41 auto style = ComputedStyle::Create();
42 ElementRuleCollector collector(context, filter, result, style.get(),
43 InsideLink(element));
44
45 String rule = selector + " { color: green }";
46 auto* style_rule =
47 DynamicTo<StyleRule>(css_test_helpers::ParseRule(GetDocument(), rule));
48 if (!style_rule)
49 return base::nullopt;
50 RuleSet* rule_set = MakeGarbageCollected<RuleSet>();
51 rule_set->AddStyleRule(style_rule, kRuleHasNoSpecialState);
52
53 MatchRequest request(rule_set, scope);
54
55 collector.CollectMatchingRules(request);
56 collector.SortAndTransferMatchedRules();
57
58 const MatchedPropertiesVector& vector = result.GetMatchedProperties();
59 if (!vector.size())
60 return base::nullopt;
61
62 // Either the normal rules matched, the visited dependent rules matched,
63 // or both. There should be nothing else.
64 DCHECK(vector.size() == 1 || vector.size() == 2);
65
66 unsigned link_match_type = 0;
67 for (const auto& matched_propeties : vector)
68 link_match_type |= matched_propeties.types_.link_match_type;
69 return link_match_type;
70 }
71 };
72
TEST_F(ElementRuleCollectorTest,LinkMatchType)73 TEST_F(ElementRuleCollectorTest, LinkMatchType) {
74 SetBodyInnerHTML(R"HTML(
75 <div id=foo></div>
76 <a id=visited href="">
77 <span id=visited_span></span>
78 </a>
79 <a id=link href="unvisited">
80 <span id=unvisited_span></span>
81 </a>
82 <div id=bar></div>
83 )HTML");
84 Element* foo = GetDocument().getElementById("foo");
85 Element* bar = GetDocument().getElementById("bar");
86 Element* visited = GetDocument().getElementById("visited");
87 Element* link = GetDocument().getElementById("link");
88 Element* unvisited_span = GetDocument().getElementById("unvisited_span");
89 Element* visited_span = GetDocument().getElementById("visited_span");
90 ASSERT_TRUE(foo);
91 ASSERT_TRUE(bar);
92 ASSERT_TRUE(visited);
93 ASSERT_TRUE(link);
94 ASSERT_TRUE(unvisited_span);
95 ASSERT_TRUE(visited_span);
96
97 ASSERT_EQ(EInsideLink::kInsideVisitedLink, InsideLink(visited));
98 ASSERT_EQ(EInsideLink::kInsideVisitedLink, InsideLink(visited_span));
99 ASSERT_EQ(EInsideLink::kNotInsideLink, InsideLink(foo));
100 ASSERT_EQ(EInsideLink::kInsideUnvisitedLink, InsideLink(link));
101 ASSERT_EQ(EInsideLink::kInsideUnvisitedLink, InsideLink(unvisited_span));
102 ASSERT_EQ(EInsideLink::kNotInsideLink, InsideLink(bar));
103
104 const auto kMatchLink = CSSSelector::kMatchLink;
105 const auto kMatchVisited = CSSSelector::kMatchVisited;
106 const auto kMatchAll = CSSSelector::kMatchAll;
107
108 EXPECT_EQ(Match(foo, "#bar"), base::nullopt);
109 EXPECT_EQ(Match(visited, "#foo"), base::nullopt);
110 EXPECT_EQ(Match(link, "#foo"), base::nullopt);
111
112 EXPECT_EQ(Match(foo, "#foo"), kMatchLink);
113 EXPECT_EQ(Match(link, ":visited"), base::nullopt);
114 EXPECT_EQ(Match(link, ":link"), kMatchLink);
115 // Note that for elements that are not inside links at all, we always
116 // expect kMatchLink, since kMatchLink represents the regular (non-visited)
117 // style.
118 EXPECT_EQ(Match(foo, ":not(:visited)"), kMatchLink);
119 EXPECT_EQ(Match(foo, ":not(:link)"), kMatchLink);
120 EXPECT_EQ(Match(foo, ":not(:link):not(:visited)"), kMatchLink);
121
122 EXPECT_EQ(Match(visited, ":link"), kMatchLink);
123 EXPECT_EQ(Match(visited, ":visited"), kMatchVisited);
124 EXPECT_EQ(Match(visited, ":link:visited"), base::nullopt);
125 EXPECT_EQ(Match(visited, ":visited:link"), base::nullopt);
126 EXPECT_EQ(Match(visited, "#visited:visited"), kMatchVisited);
127 EXPECT_EQ(Match(visited, ":visited#visited"), kMatchVisited);
128 EXPECT_EQ(Match(visited, "body :link"), kMatchLink);
129 EXPECT_EQ(Match(visited, "body > :link"), kMatchLink);
130 EXPECT_EQ(Match(visited_span, ":link span"), kMatchLink);
131 EXPECT_EQ(Match(visited_span, ":visited span"), kMatchVisited);
132 EXPECT_EQ(Match(visited, ":not(:visited)"), kMatchLink);
133 EXPECT_EQ(Match(visited, ":not(:link)"), kMatchVisited);
134 EXPECT_EQ(Match(visited, ":not(:link):not(:visited)"), base::nullopt);
135 EXPECT_EQ(Match(visited, ":is(:not(:link))"), kMatchVisited);
136 EXPECT_EQ(Match(visited, ":is(:not(:visited))"), kMatchLink);
137 EXPECT_EQ(Match(visited, ":is(:link, :not(:link))"), kMatchAll);
138 EXPECT_EQ(Match(visited, ":is(:not(:visited), :not(:link))"), kMatchAll);
139 EXPECT_EQ(Match(visited, ":is(:not(:visited):not(:link))"), base::nullopt);
140 EXPECT_EQ(Match(visited, ":is(:not(:visited):link)"), kMatchLink);
141 EXPECT_EQ(Match(visited, ":not(:is(:link))"), kMatchVisited);
142 EXPECT_EQ(Match(visited, ":not(:is(:visited))"), kMatchLink);
143 EXPECT_EQ(Match(visited, ":not(:is(:not(:visited)))"), kMatchVisited);
144 EXPECT_EQ(Match(visited, ":not(:is(:link, :visited))"), base::nullopt);
145 EXPECT_EQ(Match(visited, ":not(:is(:link:visited))"), kMatchAll);
146 EXPECT_EQ(Match(visited, ":not(:is(:not(:link):visited))"), kMatchLink);
147 EXPECT_EQ(Match(visited, ":not(:is(:not(:link):not(:visited)))"), kMatchAll);
148
149 EXPECT_EQ(Match(visited, ":is(#visited)"), kMatchAll);
150 EXPECT_EQ(Match(visited, ":is(#visited, :visited)"), kMatchAll);
151 EXPECT_EQ(Match(visited, ":is(#visited, :link)"), kMatchAll);
152 EXPECT_EQ(Match(visited, ":is(#unrelated, :link)"), kMatchLink);
153 EXPECT_EQ(Match(visited, ":is(:visited, :is(#unrelated))"), kMatchVisited);
154 EXPECT_EQ(Match(visited, ":is(:visited, #visited)"), kMatchAll);
155 EXPECT_EQ(Match(visited, ":is(:link, #visited)"), kMatchAll);
156 EXPECT_EQ(Match(visited, ":is(:visited)"), kMatchVisited);
157 EXPECT_EQ(Match(visited, ":is(:link)"), kMatchLink);
158 EXPECT_EQ(Match(visited, ":is(:link):is(:visited)"), base::nullopt);
159 EXPECT_EQ(Match(visited, ":is(:link:visited)"), base::nullopt);
160 EXPECT_EQ(Match(visited, ":is(:link, :link)"), kMatchLink);
161 EXPECT_EQ(Match(visited, ":is(:is(:link))"), kMatchLink);
162 EXPECT_EQ(Match(visited, ":is(:link, :visited)"), kMatchAll);
163 EXPECT_EQ(Match(visited, ":is(:link, :visited):link"), kMatchLink);
164 EXPECT_EQ(Match(visited, ":is(:link, :visited):visited"), kMatchVisited);
165 EXPECT_EQ(Match(visited, ":link:is(:link, :visited)"), kMatchLink);
166 EXPECT_EQ(Match(visited, ":visited:is(:link, :visited)"), kMatchVisited);
167
168 // When using :link/:visited in a sibling selector, we expect special
169 // behavior for privacy reasons.
170 // https://developer.mozilla.org/en-US/docs/Web/CSS/Privacy_and_the_:visited_selector
171 EXPECT_EQ(Match(bar, ":link + #bar"), kMatchLink);
172 EXPECT_EQ(Match(bar, ":visited + #bar"), base::nullopt);
173 EXPECT_EQ(Match(bar, ":is(:link + #bar)"), kMatchLink);
174 EXPECT_EQ(Match(bar, ":is(:visited ~ #bar)"), base::nullopt);
175 EXPECT_EQ(Match(bar, ":not(:is(:link + #bar))"), base::nullopt);
176 EXPECT_EQ(Match(bar, ":not(:is(:visited ~ #bar))"), kMatchLink);
177 }
178
TEST_F(ElementRuleCollectorTest,LinkMatchTypeHostContext)179 TEST_F(ElementRuleCollectorTest, LinkMatchTypeHostContext) {
180 SetBodyInnerHTML(R"HTML(
181 <a href=""><div id="visited_host"></div></a>
182 <a href="unvisited"><div id="unvisited_host"></div></a>
183 )HTML");
184
185 Element* visited_host = GetDocument().getElementById("visited_host");
186 Element* unvisited_host = GetDocument().getElementById("unvisited_host");
187 ASSERT_TRUE(visited_host);
188 ASSERT_TRUE(unvisited_host);
189
190 ShadowRoot& visited_root =
191 visited_host->AttachShadowRootInternal(ShadowRootType::kOpen);
192 ShadowRoot& unvisited_root =
193 unvisited_host->AttachShadowRootInternal(ShadowRootType::kOpen);
194
195 visited_root.setInnerHTML(R"HTML(
196 <style id=style></style>
197 <div id=div></div>
198 )HTML");
199 unvisited_root.setInnerHTML(R"HTML(
200 <style id=style></style>
201 <div id=div></div>
202 )HTML");
203
204 UpdateAllLifecyclePhasesForTest();
205
206 Element* visited_style = visited_root.getElementById("style");
207 Element* unvisited_style = unvisited_root.getElementById("style");
208 ASSERT_TRUE(visited_style);
209 ASSERT_TRUE(unvisited_style);
210
211 Element* visited_div = visited_root.getElementById("div");
212 Element* unvisited_div = unvisited_root.getElementById("div");
213 ASSERT_TRUE(visited_div);
214 ASSERT_TRUE(unvisited_div);
215
216 const auto kMatchLink = CSSSelector::kMatchLink;
217 const auto kMatchVisited = CSSSelector::kMatchVisited;
218 const auto kMatchAll = CSSSelector::kMatchAll;
219
220 {
221 Element* element = visited_div;
222 const ContainerNode* scope = visited_style;
223
224 EXPECT_EQ(Match(element, ":host-context(a) div", scope), kMatchAll);
225 EXPECT_EQ(Match(element, ":host-context(:link) div", scope), kMatchLink);
226 EXPECT_EQ(Match(element, ":host-context(:visited) div", scope),
227 kMatchVisited);
228 EXPECT_EQ(Match(element, ":host-context(:is(:visited, :link)) div", scope),
229 kMatchAll);
230
231 // :host-context(:not(:visited/link)) matches the host itself.
232 EXPECT_EQ(Match(element, ":host-context(:not(:visited)) div", scope),
233 kMatchAll);
234 EXPECT_EQ(Match(element, ":host-context(:not(:link)) div", scope),
235 kMatchAll);
236 }
237
238 {
239 Element* element = unvisited_div;
240 const ContainerNode* scope = unvisited_style;
241
242 EXPECT_EQ(Match(element, ":host-context(a) div", scope), kMatchAll);
243 EXPECT_EQ(Match(element, ":host-context(:link) div", scope), kMatchLink);
244 EXPECT_EQ(Match(element, ":host-context(:visited) div", scope),
245 base::nullopt);
246 EXPECT_EQ(Match(element, ":host-context(:is(:visited, :link)) div", scope),
247 kMatchLink);
248 }
249 }
250
251 } // namespace blink
252