/* * This source file is part of RmlUi, the HTML/CSS Interface Middleware * * For the latest information, see http://github.com/mikke89/RmlUi * * Copyright (c) 2008-2010 CodePoint Ltd, Shift Technology Ltd * Copyright (c) 2019-2023 The RmlUi Team, and contributors * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. * */ #include "../Common/TestsShell.h" #include #include #include #include #include #include using namespace Rml; static const String doc_begin = R"( Demo

Some text

Text

)"; enum class SelectorOp { None, RemoveElementsByIds, InsertElementBefore, RemoveClasses, RemoveId, RemoveChecked, RemoveAttributeUnit, SetHover }; struct QuerySelector { QuerySelector(String selector, String expected_ids, int expect_num_warnings = 0, int expect_num_query_warnings = 0) : selector(std::move(selector)), expected_ids(std::move(expected_ids)), expect_num_warnings(expect_num_warnings), expect_num_query_warnings(expect_num_query_warnings) {} QuerySelector(String selector, String expected_ids, SelectorOp operation, String operation_argument, String expected_ids_after_operation) : selector(std::move(selector)), expected_ids(std::move(expected_ids)), operation(operation), operation_argument(std::move(operation_argument)), expected_ids_after_operation(std::move(expected_ids_after_operation)) {} String selector; String expected_ids; // If this rule should fail to parse or otherwise produce warnings, set these to non-zero values. int expect_num_warnings = 0, expect_num_query_warnings = 0; // Optionally also test the selector after dynamically making a structural operation on the document. SelectorOp operation = SelectorOp::None; String operation_argument; String expected_ids_after_operation; }; // clang-format off static const Vector query_selectors = { { "span", "Y D0 D1 F0" }, { ".hello", "X Z H" }, { ".hello.world", "Z H" }, { "div.hello", "X Z" }, { "body .hello", "X Z H" }, { "body>.hello", "X Z" }, { "body > .hello", "X Z" }, { ".parent *", "A B C D D0 D1 E F F0 G H" }, { ".parent > *", "A B C D E F G H" }, { ":checked", "I", SelectorOp::RemoveChecked, "I", "" }, { "*", "X Y Z P A B C D D0 D1 E F F0 G H I" }, { "*span", "Y D0 D1 F0" }, { "*.hello", "X Z H" }, { "*:checked", "I" }, { "p[unit='m']", "B" }, { "p[unit=\"m\"]", "B" }, { "[class]", "X Y Z P F0 G H" }, { "[class=hello]", "X" }, { "[class=]", "G" }, { "[class='']", "G" }, { "[class=\"\"]", "G" }, { "[class~=hello]", "X Z H" }, { "[class~=ello]", "" }, { "[class|=hello]", "X F0" }, { "[class^=hello]", "X Z F0" }, { "[class^=]", "X Y Z P F0 G H" }, { "[class$=hello]", "X H" }, { "[class*=hello]", "X Z F0 H" }, { "[class*=ello]", "X Z F0 H" }, { "[class~=hello].world", "Z H" }, { "*[class~=hello].world", "Z H" }, { ".world[class~=hello]", "Z H" }, { "[class~=hello][class~=world]", "Z H" }, { "[class=hello world]", "Z" }, { "[class='hello world']", "Z" }, { "[class=\"hello world\"]", "Z" }, { "[invalid", "", 1, 4 }, { "[]", "", 1, 4 }, { "[x=Rule{What}]", "", 2, 0 }, { "[x=Hello,world]", "", 1, 2 }, // The next ones are valid in CSS but we currently don't bother handling them, just make sure we don't crash. { "[x='Rule{What}']", "", 2, 0 }, { "[x='Hello,world']", "", 1, 2 }, { "#X[class=hello]", "X" }, { "[class=hello]#X", "X" }, { "#Y[class=hello]", "" }, { "div[class=hello]", "X" }, { "[class=hello]div", "X" }, { "span[class=hello]", "" }, { ".parent :nth-child(odd)", "A C D0 E F0 G" }, { ".parent > :nth-child(even)", "B D F H", SelectorOp::RemoveClasses, "parent", "" }, { ":first-child", "X A D0 F0", SelectorOp::RemoveElementsByIds, "A F0", "X B D0" }, { ":last-child", "D1 F0 H I", SelectorOp::RemoveElementsByIds, "D0 H", "D1 F0 G I" }, { "h1:nth-child(2)", "", SelectorOp::InsertElementBefore, "A", "A" }, { "p:nth-child(2)", "B", SelectorOp::InsertElementBefore, "A", "" }, { "p:nth-child(2)", "B", SelectorOp::RemoveElementsByIds, "A", "C" }, { "p:nth-child(3n+1)", "D G", SelectorOp::RemoveElementsByIds, "B", "H" }, { "p:nth-child(3n + 1)", "D G" }, { "#P > :nth-last-child(2n+1)", "B D F H" }, { "#P p:nth-of-type(odd)", "B D G" }, { "span:first-child", "D0 F0" }, { "body span:first-child", "D0 F0" }, { "body > p span:first-child", "" }, { "body > div span:first-child", "D0 F0" }, { ":nth-child(4) * span:first-child", "D0 F0", SelectorOp::RemoveElementsByIds, "X", "" }, { "p:nth-last-of-type(3n+1)", "D H" }, { ":first-of-type", "X Y A B D0 E F0 I" }, { ":last-of-type", "Y P A D1 E F0 H I" }, { ":only-child", "F0", SelectorOp::RemoveElementsByIds, "D0", "D1 F0" }, { ":only-of-type", "Y A E F0 I" }, { "span:empty", "Y D0 F0" }, { ".hello.world, #P span, #I", "Z D0 D1 F0 H I", SelectorOp::RemoveClasses, "world", "D0 D1 F0 I" }, { "body * span", "D0 D1 F0" }, { "D1 *", "" }, { "#E + #F", "F", SelectorOp::InsertElementBefore, "F", "" }, { "#E+#F", "F" }, { "#E +#F", "F" }, { "#E+ #F", "F" }, { "#F + #E", "" }, { "#A + #B", "B", SelectorOp::RemoveId, "A", "" }, { "* + #A", "" }, { "#H + *", "" }, { "#P + *", "I" }, { "div.parent > #B + p", "C" }, { "div.parent > #B + div", "" }, { "#B ~ #F", "F" }, { "#B~#F", "F" }, { "#B ~#F", "F" }, { "#B~ #F", "F" }, { "#F ~ #B", "" }, { "div.parent > #B ~ p:empty", "C G H", SelectorOp::InsertElementBefore, "H", "C G Inserted H" }, { "div.parent > #B ~ * span", "D0 D1 F0" }, { ":not(*)", "" }, { ":not(span)", "X Z P A B C D E F G H I" }, { "#D :not(#D0)", "D1" }, { "body > :not(:checked)", "X Y Z P", SelectorOp::RemoveChecked, "I", "X Y Z P I" }, { "div.hello:not(.world)", "X" }, { ":not(div,:nth-child(2),p *)", "A C D E F G H I" }, { ".hello + .world", "Y", SelectorOp::RemoveClasses, "hello", "" }, { "#B ~ h3", "E", SelectorOp::RemoveId, "B", "" }, { "#Z + div > :nth-child(2)", "B", SelectorOp::RemoveId, "Z", "" }, { ":hover + #P #D1", "", SelectorOp::SetHover, "Z", "D1" }, { ":not(:hover) + #P #D1", "D1", SelectorOp::SetHover, "Z", "" }, { "#X + #Y", "Y", SelectorOp::RemoveId, "X", "" }, { "p[unit=m]", "B", SelectorOp::RemoveAttributeUnit, "B", "" }, { "p[unit=m] + *", "C", SelectorOp::RemoveAttributeUnit, "B", "" }, { "body > * #D0", "D0" }, { "#E + * ~ *", "G H" }, { "#B + * ~ #G", "G" }, { "body > :nth-child(4) span:first-child", "D0 F0", SelectorOp::RemoveElementsByIds, "X", "" }, }; struct ClosestSelector { String start_id; String selector; String expected_id; }; static const Vector closest_selectors = { { "D1", "#P", "P" }, { "D1", "#P, body", "P" }, { "D1", "#P, #D", "D" }, { "D1", "#Z", "" }, { "D1", "#D1", "" }, { "D1", "#D0", "" }, { "D1", "div", "P" }, { "D1", "p", "D" }, { "D1", ":nth-child(4)", "D" }, { "D1", "div:nth-child(4)", "P" }, }; struct MatchesSelector { String id; String selector; bool expected_result; }; static const Vector matches_selectors = { { "X", ".world", false }, { "X", ".hello", true }, { "X", ".hello, .world", true }, { "E", "h3", true }, { "G", "p#G[class]", true }, { "G", "p#G[missing]", false }, { "B", "[unit='m']", true } }; struct ScopeSelector : public QuerySelector { String scope_selector; ScopeSelector(const String& scope_selector, const String& selector, const String& expected_ids) : QuerySelector(selector, expected_ids), scope_selector(scope_selector) {} }; static const Vector scope_selectors = { { "", ":scope *", "X Y Z P A B C D D0 D1 E F F0 G H I" }, // should be equivalent to just "*" { "", ":scope > *", "X Y Z P I" }, { "", ":scope > *:not(:checked)", "X Y Z P" }, { "#P", ":scope > p", "B C D F G H" }, { "#P", ":scope span", "D0 D1 F0" }, }; struct ContainsSelector { String element_id; String target_id; bool expected_result; }; static const Vector contains_selectors = { { "A", "A", true }, { "P", "A", true }, { "A", "P", false }, { "P", "D0", true }, { "D0", "P", false }, { "X", "P", false }, { "P", "X", false }, }; // clang-format on // Recursively iterate through 'element' and all of its descendants to find all // elements matching a particular property used to tag matching selectors. static void GetMatchingIds(String& matching_ids, Element* element) { String id = element->GetId(); if (!id.empty() && element->GetProperty("drag") == (int)Style::Drag::Drag) { if (!matching_ids.empty()) matching_ids += ' '; matching_ids += id; } for (int i = 0; i < element->GetNumChildren(); i++) { GetMatchingIds(matching_ids, element->GetChild(i)); } } // Return the list of IDs that should match the above 'selectors.expected_ids'. static String ElementListToIds(const ElementList& elements) { String result; for (Element* element : elements) { result += element->GetId() + ' '; } if (!result.empty()) result.pop_back(); return result; } static void RemoveElementsWithIds(ElementDocument* document, const String& remove_ids) { StringList remove_id_list; StringUtilities::ExpandString(remove_id_list, remove_ids, ' '); for (const String& id : remove_id_list) { if (Element* element = document->GetElementById(id)) element->GetParentNode()->RemoveChild(element); } } static void RemoveClassesFromAllElements(ElementDocument* document, const String& remove_classes) { StringList class_list; StringUtilities::ExpandString(class_list, remove_classes, ' '); for (const String& name : class_list) { ElementList element_list; document->GetElementsByClassName(element_list, name); for (Element* element : element_list) element->SetClass(name, false); } } static void InsertElementBefore(ElementDocument* document, const String& before_id) { Element* element = document->GetElementById(before_id); ElementPtr new_element = document->CreateElement("p"); new_element->SetId("Inserted"); element->GetParentNode()->InsertBefore(std::move(new_element), element); } TEST_CASE("Selectors") { Context* context = TestsShell::GetContext(); SUBCASE("RCSS document selectors") { for (const QuerySelector& selector : query_selectors) { TestsShell::SetNumExpectedWarnings(selector.expect_num_warnings); const String selector_css = selector.selector + " { drag: drag; } "; const String document_string = doc_begin + selector_css + doc_end; ElementDocument* document = context->LoadDocumentFromMemory(document_string); // Update the context to settle any dirty state. context->Update(); String matching_ids; GetMatchingIds(matching_ids, document); CHECK_MESSAGE(matching_ids == selector.expected_ids, "Selector: " << selector.selector); // Also check validity of selectors after structural document changes. if (selector.operation != SelectorOp::None) { String operation_str; switch (selector.operation) { case SelectorOp::RemoveElementsByIds: RemoveElementsWithIds(document, selector.operation_argument); operation_str = "RemoveElementsByIds"; break; case SelectorOp::InsertElementBefore: InsertElementBefore(document, selector.operation_argument); operation_str = "InsertElementBefore"; break; case SelectorOp::RemoveClasses: RemoveClassesFromAllElements(document, selector.operation_argument); operation_str = "RemoveClasses"; break; case SelectorOp::RemoveChecked: document->GetElementById(selector.operation_argument)->RemoveAttribute("checked"); operation_str = "RemoveChecked"; break; case SelectorOp::RemoveId: document->GetElementById(selector.operation_argument)->SetId(""); operation_str = "RemoveId"; break; case SelectorOp::RemoveAttributeUnit: document->GetElementById(selector.operation_argument)->RemoveAttribute("unit"); operation_str = "RemoveAttributeUnit"; break; case SelectorOp::SetHover: document->GetElementById(selector.operation_argument)->SetPseudoClass("hover", true); operation_str = "SetHover"; break; case SelectorOp::None: break; } context->Update(); String matching_ids_after_operation; GetMatchingIds(matching_ids_after_operation, document); CHECK_MESSAGE(matching_ids_after_operation == selector.expected_ids_after_operation, "Selector: ", selector.selector, " Operation: ", operation_str, " Argument: ", selector.operation_argument); } context->UnloadDocument(document); } } SUBCASE("QuerySelector(All)") { const String document_string = doc_begin + doc_end; ElementDocument* document = context->LoadDocumentFromMemory(document_string); REQUIRE(document); for (const QuerySelector& selector : query_selectors) { TestsShell::SetNumExpectedWarnings(selector.expect_num_query_warnings); ElementList elements; document->QuerySelectorAll(elements, selector.selector); String matching_ids = ElementListToIds(elements); Element* first_element = document->QuerySelector(selector.selector); if (first_element) { CHECK_MESSAGE(first_element == elements[0], "QuerySelector does not return the first match of QuerySelectorAll."); } else { CHECK_MESSAGE(elements.empty(), "QuerySelector found nothing, while QuerySelectorAll found " << elements.size() << " element(s)."); } CHECK_MESSAGE(matching_ids == selector.expected_ids, "QuerySelector: " << selector.selector); } context->UnloadDocument(document); } SUBCASE("Closest") { const String document_string = doc_begin + doc_end; ElementDocument* document = context->LoadDocumentFromMemory(document_string); REQUIRE(document); for (const ClosestSelector& selector : closest_selectors) { Element* start = document->GetElementById(selector.start_id); REQUIRE(start); Element* match = start->Closest(selector.selector); const String match_id = match ? match->GetId() : ""; CHECK_MESSAGE(match_id == selector.expected_id, "Closest() selector '" << selector.selector << "' from " << selector.start_id); } context->UnloadDocument(document); } SUBCASE("Matches") { const String document_string = doc_begin + doc_end; ElementDocument* document = context->LoadDocumentFromMemory(document_string); REQUIRE(document); for (const MatchesSelector& selector : matches_selectors) { Element* start = document->GetElementById(selector.id); REQUIRE(start); bool matches = start->Matches(selector.selector); CHECK_MESSAGE(matches == selector.expected_result, "Matches() selector '" << selector.selector << "' from " << selector.id); } context->UnloadDocument(document); } SUBCASE("Scope") { const String document_string = doc_begin + doc_end; ElementDocument* document = context->LoadDocumentFromMemory(document_string); REQUIRE(document); for (const ScopeSelector& selector : scope_selectors) { Element* start = (selector.scope_selector.empty() ? document : document->QuerySelector(selector.scope_selector)); REQUIRE(start); ElementList elements; start->QuerySelectorAll(elements, selector.selector); String matching_ids = ElementListToIds(elements); Element* first_element = start->QuerySelector(selector.selector); if (first_element) { CHECK_MESSAGE(first_element == elements[0], "QuerySelector does not return the first match of QuerySelectorAll."); } else { CHECK_MESSAGE(elements.empty(), "QuerySelector found nothing, while QuerySelectorAll found " << elements.size() << " element(s)."); } CHECK_MESSAGE(matching_ids == selector.expected_ids, "QuerySelector: " << selector.selector); } context->UnloadDocument(document); } SUBCASE("Contains") { const String document_string = doc_begin + doc_end; ElementDocument* document = context->LoadDocumentFromMemory(document_string); REQUIRE(document); for (const ContainsSelector& selector : contains_selectors) { Element* element = document->GetElementById(selector.element_id); Element* target = document->GetElementById(selector.target_id); REQUIRE(element); REQUIRE(target); CHECK_MESSAGE(element->Contains(target) == selector.expected_result, "'" << selector.element_id << "' contains '" << selector.target_id << "'"); } context->UnloadDocument(document); } TestsShell::ShutdownShell(); }