blob: 57841bd06068c98b6f3ce87e08c0a4961a4d9596 [file]
// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "third_party/blink/renderer/core/lcp_critical_path_predictor/element_locator.h"
#include <string_view>
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/renderer/core/dom/document.h"
#include "third_party/blink/renderer/core/dom/element_traversal.h"
#include "third_party/blink/renderer/core/editing/testing/editing_test_base.h"
#include "third_party/blink/renderer/core/html/parser/html_token.h"
#include "third_party/blink/renderer/core/lcp_critical_path_predictor/element_locator.pb.h"
#include "third_party/blink/renderer/platform/wtf/text/atomic_string.h"
#include "third_party/googletest/src/googletest/include/gtest/gtest.h"
namespace blink {
using ElementLocatorTest = EditingTestBase;
bool HasDataLocateMe(Element& element) {
DEFINE_STATIC_LOCAL(const AtomicString, kDataLocateMe, ("data-locate-me"));
return element.hasAttribute(kDataLocateMe);
}
TEST_F(ElementLocatorTest, OfElement) {
struct TestCase {
const char* body_html;
const char* expected_locator_string;
};
constexpr TestCase test_cases[] = {
// Single element with an id.
{"<div id='a' data-locate-me></div>", "/#a"},
// No id on the element, so use relative position.
{"<div id='container'><div data-locate-me></div></div>",
"/div[0]/#container"},
// No id on the document, so stop at BODY.
{"<div data-locate-me></div>", "/div[0]/body[0]"},
// Siblings
{"<div id='container'><p><p><p><p data-locate-me><p></div>",
"/p[3]/#container"},
// Siblings with different tag names
{"<div id='container'><h1></h1><p><p data-locate-me><p><a></a></div>",
"/p[1]/#container"},
// Misc complicated cases
{"<section id='container'>"
"<article></article>"
"<article></article>"
"<article><h2>Title</h2>"
" <img src=logo.svg>"
" <img src=photo.jpg data-locate-me>asdffdsa"
"</article>"
"<article></article>"
"</section>",
"/img[1]/article[2]/#container"},
};
for (const auto& test_case : test_cases) {
SCOPED_TRACE(testing::Message() << std::endl
<< "body_html = " << test_case.body_html);
SetBodyContent(test_case.body_html);
Element* target =
Traversal<Element>::FirstWithin(GetDocument(), HasDataLocateMe);
ASSERT_TRUE(target);
auto locator = element_locator::OfElement(*target);
if (test_case.expected_locator_string) {
String locator_string = element_locator::ToStringForTesting(locator);
EXPECT_EQ(String(test_case.expected_locator_string), locator_string);
}
}
}
class TokenStreamMatcherTest : public ::testing::Test {
public:
struct Expectation {
enum class Type { kStartTag, kEndTag } type = Type::kStartTag;
const std::string_view tag_name;
const std::string_view id_attr;
bool should_match = false;
};
static const auto kEndTag = Expectation::Type::kEndTag;
void TestMatch(element_locator::TokenStreamMatcher& matcher,
const Vector<Expectation>& exps) {
size_t i = 0;
for (const Expectation& exp : exps) {
SCOPED_TRACE(testing::Message() << "expectation index = " << i);
AtomicString tag_name = AtomicString::FromUtf8(exp.tag_name);
EXPECT_TRUE(tag_name.Impl()->IsStatic());
switch (exp.type) {
case Expectation::Type::kStartTag: {
HTMLToken token;
{
auto it = exp.tag_name.begin();
token.BeginStartTag(static_cast<LChar>(*it++));
for (; it != exp.tag_name.end(); ++it) {
token.AppendToName(static_cast<UChar>(*it));
}
}
if (!exp.id_attr.empty()) {
token.AddNewAttribute('i');
token.AppendToAttributeName('d');
for (char it : exp.id_attr) {
token.AppendToAttributeValue(static_cast<LChar>(it));
}
}
bool matched =
matcher.ObserveStartTagAndReportMatch(tag_name.Impl(), token);
EXPECT_EQ(matched, exp.should_match);
} break;
case Expectation::Type::kEndTag:
matcher.ObserveEndTag(tag_name.Impl());
break;
}
++i;
}
}
};
TEST_F(TokenStreamMatcherTest, SingleId) {
ElementLocator locator;
auto* c = locator.add_components()->mutable_id();
c->set_id_attr("target");
element_locator::TokenStreamMatcher matcher({locator});
Vector<Expectation> exps = {
{.tag_name = "h1"},
{.type = kEndTag, .tag_name = "h1"},
{.tag_name = "p"},
{.tag_name = "input"},
{.tag_name = "img", .id_attr = "target", .should_match = true},
{.type = kEndTag, .tag_name = "div"},
};
TestMatch(matcher, exps);
}
TEST_F(TokenStreamMatcherTest, SingleNth) {
ElementLocator locator;
auto* c = locator.add_components()->mutable_nth();
c->set_tag_name("img");
c->set_index(2);
element_locator::TokenStreamMatcher matcher({locator});
Vector<Expectation> exps = {
{.tag_name = "div"}, {.tag_name = "img"},
{.tag_name = "span"}, {.type = kEndTag, .tag_name = "span"},
{.tag_name = "img"}, {.tag_name = "img", .should_match = true},
{.tag_name = "img"}, {.type = kEndTag, .tag_name = "div"},
};
TestMatch(matcher, exps);
}
TEST_F(TokenStreamMatcherTest, CloseAPElement) {
ElementLocator locator;
auto* c0 = locator.add_components()->mutable_nth();
c0->set_tag_name("img");
c0->set_index(0);
auto* c1 = locator.add_components()->mutable_nth();
c1->set_tag_name("p");
c1->set_index(2);
auto* c2 = locator.add_components()->mutable_id();
c2->set_id_attr("container");
EXPECT_EQ(String("/img[0]/p[2]/#container"),
element_locator::ToStringForTesting(locator));
element_locator::TokenStreamMatcher matcher({locator});
Vector<Expectation> exps = {
{.tag_name = "div", .id_attr = "container"},
{.tag_name = "p"},
{.tag_name = "img"},
{.tag_name = "p"},
{.tag_name = "p"},
{.tag_name = "img", .should_match = true},
{.type = kEndTag, .tag_name = "div"}};
TestMatch(matcher, exps);
}
TEST_F(TokenStreamMatcherTest, Complicated) {
ElementLocator locator;
auto* c0 = locator.add_components()->mutable_nth();
c0->set_tag_name("img");
c0->set_index(1);
auto* c1 = locator.add_components()->mutable_nth();
c1->set_tag_name("article");
c1->set_index(2);
auto* c2 = locator.add_components()->mutable_id();
c2->set_id_attr("container");
EXPECT_EQ(String("/img[1]/article[2]/#container"),
element_locator::ToStringForTesting(locator));
element_locator::TokenStreamMatcher matcher({locator});
Vector<Expectation> exps = {
{.tag_name = "section", .id_attr = "container"},
{.tag_name = "article"},
{.type = kEndTag, .tag_name = "article"},
{.tag_name = "article"},
{.type = kEndTag, .tag_name = "article"},
{.tag_name = "article"},
{.tag_name = "h2"},
{.type = kEndTag, .tag_name = "h2"},
{.tag_name = "img"},
{.tag_name = "img", .should_match = true},
{.type = kEndTag, .tag_name = "article"},
{.tag_name = "article"},
{.type = kEndTag, .tag_name = "article"},
{.type = kEndTag, .tag_name = "section"},
};
TestMatch(matcher, exps);
}
TEST_F(TokenStreamMatcherTest, DontMatchNonImg) {
ElementLocator locator;
auto* c0 = locator.add_components()->mutable_nth();
c0->set_tag_name("p");
c0->set_index(2);
auto* c1 = locator.add_components()->mutable_id();
c1->set_id_attr("container");
EXPECT_EQ(String("/p[2]/#container"),
element_locator::ToStringForTesting(locator));
element_locator::TokenStreamMatcher matcher({locator});
Vector<Expectation> exps = {
{.tag_name = "div", .id_attr = "container"},
{.tag_name = "p"},
{.tag_name = "img"},
{.tag_name = "p"},
{.tag_name = "p", .should_match = false},
{.type = kEndTag, .tag_name = "div"},
};
TestMatch(matcher, exps);
}
} // namespace blink