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