Browse Source

Add selector unit tests with dynamic structural operations

Michael Ragazzon 3 years ago
parent
commit
979b75b559
1 changed files with 97 additions and 14 deletions
  1. 97 14
      Tests/Source/UnitTests/Selectors.cpp

+ 97 - 14
Tests/Source/UnitTests/Selectors.cpp

@@ -36,6 +36,7 @@
 
 using namespace Rml;
 
+// clang-format off
 static const String doc_begin = R"(
 <rml>
 <head>
@@ -70,10 +71,23 @@ static const String doc_end = R"(
 </rml>
 )";
 
+enum class SelectorOp { None, RemoveElementsByIds, InsertElementBefore, RemoveClasses, RemoveChecked };
+
 struct QuerySelector {
+	QuerySelector(String selector, String expected_ids) : selector(std::move(selector)), expected_ids(std::move(expected_ids)) {}
+	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;
+
+	// 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;
 };
+
 static const Vector<QuerySelector> query_selectors =
 {
 	{ "span",                        "Y D0 D1 F0" },
@@ -85,26 +99,33 @@ static const Vector<QuerySelector> query_selectors =
 	{ "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" },
+	{ ":checked",                    "I",               SelectorOp::RemoveChecked,        "I", "" },
 	{ ".parent :nth-child(odd)",     "A C D0 E F0 G" },
-	{ ".parent > :nth-child(even)",  "B D F H" },
-	{ ":first-child",                "X A D0 F0" },
-	{ ":last-child",                 "D1 F0 H I" },
-	{ "p:nth-child(2)",              "B" },
-	{ "h1:nth-child(2)",             "" },
-	{ "p:nth-child(3n+1)",           "D 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" },
+	{ ":only-child",                 "F0",              SelectorOp::RemoveElementsByIds,  "D0",    "D1 F0" },
 	{ ":only-of-type",               "Y A E F0 I" },
 	{ "span:empty",                  "Y D0 D1 F0" },
-	{ ".hello.world, #P span, #I",   "Z D0 D1 F0 I" },
+	{ ".hello.world, #P span, #I",   "Z D0 D1 F0 I",    SelectorOp::RemoveClasses,        "world", "D0 D1 F0 I" },
 	{ "body * span",                 "D0 D1 F0" },
 };
+
 struct ClosestSelector {
 	String start_id;
 	String selector;
@@ -123,7 +144,7 @@ static const Vector<ClosestSelector> closest_selectors =
 	{ "D1",  ":nth-child(4)",    "D" },
 	{ "D1",  "div:nth-child(4)", "P" },
 };
-
+// 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.
@@ -132,7 +153,9 @@ static void GetMatchingIds(String& matching_ids, Element* element)
 	String id = element->GetId();
 	if (!id.empty() && element->GetProperty<int>("drag") == (int)Style::Drag::Drag)
 	{
-		matching_ids += id + ' ';
+		if (!matching_ids.empty())
+			matching_ids += ' ';
+		matching_ids += id;
 	}
 
 	for (int i = 0; i < element->GetNumChildren(); i++)
@@ -157,6 +180,36 @@ static String ElementListToIds(const ElementList& elements)
 	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")
 {
 	const Vector2i window_size(1024, 768);
@@ -183,11 +236,41 @@ TEST_CASE("Selectors")
 
 			String matching_ids;
 			GetMatchingIds(matching_ids, document);
+			CHECK_MESSAGE(matching_ids == selector.expected_ids, "Selector: " << selector.selector);
 
-			if (!matching_ids.empty())
-				matching_ids.pop_back();
+			// 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::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);
+			}
 
-			CHECK_MESSAGE(matching_ids == selector.expected_ids, "Selector: " << selector.selector);
 			context->UnloadDocument(document);
 		}
 	}