Browse Source

VisualTests: Enable filtering tests. Hover to view links, click to copy them to clipboard.

Michael Ragazzon 5 years ago
parent
commit
b629f9ea3b

+ 43 - 9
Tests/Data/description.rml

@@ -17,8 +17,9 @@
 		color: #ccc;
 		color: #ccc;
 		padding: 20px 20px;
 		padding: 20px 20px;
 		z-index: 100;
 		z-index: 100;
+		tab-index: auto;
 	}
 	}
-	#content, #content > * { padding-top: 0.8em; }
+	#content > * { padding-top: 0.8em; }
 	code {
 	code {
 		display: block;
 		display: block;
 		white-space: pre-wrap;
 		white-space: pre-wrap;
@@ -29,27 +30,60 @@
 	h3   { color: white; font-size: 1.15em; }
 	h3   { color: white; font-size: 1.15em; }
 	
 	
 	p.links a { margin: 0 0.7em; }
 	p.links a { margin: 0 0.7em; }
-	#test_suite {
+	#header {
+		color: #ddb;
+	}
+	#filter {
 		position: absolute;
 		position: absolute;
-		text-align: center;
-		top: 15px;
-		left: 20px;
 		right: 20px;
 		right: 20px;
-		color: #ddb;
+		top: 20px;
+		width: 50%;
+		text-align: right;
+	}
+	input.text {
+		font-size: 0.85em;
+		padding: 3px 4px;
+		width: 80%;
+		background-color: #444;
+		color: #ffe;
+		border: 1px #555;
+		line-height: 1.2;
+		height: 1.8em;
+		cursor: text;
+		tab-index: auto;
+	}
+	input.text:focus {
+		border-color: #dda;
+	}
+	#filter_text {
+		color: #ffc;
 	}
 	}
 	#goto {
 	#goto {
 		position: absolute;
 		position: absolute;
 		left: 20px;
 		left: 20px;
-		bottom: 20px;
+		bottom: 30px;
 		width: 200px;
 		width: 200px;
 		color: #ddb;
 		color: #ddb;
 	}
 	}
-	
+	#hovertext {
+		position: absolute;
+		left: 20px;
+		bottom: 10px;
+		right: 20px;
+		font-size: 0.9em;
+		text-align: left;
+		color: #aaa;
+	}
+	#hovertext.confirmation {
+		color: #afa;
+	}
 </style>
 </style>
 </head>
 </head>
 <body>
 <body>
-<div id="test_suite"/>
+<div id="header"/>
+<div id="filter"><input id="filterinput" type="text" value=""/><br/><span id="filter_text">Filtered 2 of 63.</span></div>
 <div id="content"/>
 <div id="content"/>
 <div id="goto"/>
 <div id="goto"/>
+<div id="hovertext"/>
 </body>
 </body>
 </rml>
 </rml>

+ 3 - 0
Tests/Data/style.rcss

@@ -47,6 +47,9 @@ a {
 a:hover {
 a:hover {
 	color: #5285e6;
 	color: #5285e6;
 }
 }
+a:active {
+	color: #4275e0;
+}
 scrollbarvertical
 scrollbarvertical
 {
 {
 	width: 16dp;
 	width: 16dp;

+ 39 - 11
Tests/Source/VisualTests/TestNavigator.cpp

@@ -47,6 +47,7 @@ TestNavigator::TestNavigator(ShellRenderInterfaceOpenGL* shell_renderer, Rml::Co
 	RMLUI_ASSERTMSG(!this->test_suites.empty(), "At least one test suite is required.");
 	RMLUI_ASSERTMSG(!this->test_suites.empty(), "At least one test suite is required.");
 	context->GetRootElement()->AddEventListener(Rml::EventId::Keydown, this);
 	context->GetRootElement()->AddEventListener(Rml::EventId::Keydown, this);
 	context->GetRootElement()->AddEventListener(Rml::EventId::Textinput, this);
 	context->GetRootElement()->AddEventListener(Rml::EventId::Textinput, this);
+	context->GetRootElement()->AddEventListener(Rml::EventId::Change, this);
 	LoadActiveTest();
 	LoadActiveTest();
 }
 }
 
 
@@ -54,6 +55,7 @@ TestNavigator::~TestNavigator()
 {
 {
 	context->GetRootElement()->RemoveEventListener(Rml::EventId::Keydown, this);
 	context->GetRootElement()->RemoveEventListener(Rml::EventId::Keydown, this);
 	context->GetRootElement()->RemoveEventListener(Rml::EventId::Textinput, this);
 	context->GetRootElement()->RemoveEventListener(Rml::EventId::Textinput, this);
+	context->GetRootElement()->RemoveEventListener(Rml::EventId::Change, this);
 }
 }
 
 
 void TestNavigator::Update()
 void TestNavigator::Update()
@@ -88,9 +90,8 @@ void TestNavigator::Update()
 			iteration_index += 1;
 			iteration_index += 1;
 
 
 			TestSuite& suite = CurrentSuite();
 			TestSuite& suite = CurrentSuite();
-			if (iteration_index < suite.GetNumTests())
+			if (suite.Next())
 			{
 			{
-				suite.SetIndex(iteration_index);
 				LoadActiveTest();
 				LoadActiveTest();
 			}
 			}
 			else
 			else
@@ -111,33 +112,37 @@ void TestNavigator::ProcessEvent(Rml::Event& event)
 
 
 		if (key_identifier == Rml::Input::KI_LEFT)
 		if (key_identifier == Rml::Input::KI_LEFT)
 		{
 		{
-			if (CurrentSuite().SetIndex(CurrentSuite().GetIndex() - 1))
+			if (CurrentSuite().Previous())
 			{
 			{
 				LoadActiveTest();
 				LoadActiveTest();
 			}
 			}
 		}
 		}
 		else if (key_identifier == Rml::Input::KI_RIGHT)
 		else if (key_identifier == Rml::Input::KI_RIGHT)
 		{
 		{
-			if (CurrentSuite().SetIndex(CurrentSuite().GetIndex() + 1))
+			if (CurrentSuite().Next())
 			{
 			{
 				LoadActiveTest();
 				LoadActiveTest();
 			}
 			}
 		}
 		}
 		else if (key_identifier == Rml::Input::KI_UP)
 		else if (key_identifier == Rml::Input::KI_UP)
 		{
 		{
+			const Rml::String& filter = CurrentSuite().GetFilter();
 			int new_index = std::max(0, index - 1);
 			int new_index = std::max(0, index - 1);
 			if (new_index != index)
 			if (new_index != index)
 			{
 			{
 				index = new_index;
 				index = new_index;
+				CurrentSuite().SetFilter(filter);
 				LoadActiveTest();
 				LoadActiveTest();
 			}
 			}
 		}
 		}
 		else if (key_identifier == Rml::Input::KI_DOWN)
 		else if (key_identifier == Rml::Input::KI_DOWN)
 		{
 		{
+			const Rml::String& filter = CurrentSuite().GetFilter();
 			int new_index = std::min((int)test_suites.size() - 1, index + 1);
 			int new_index = std::min((int)test_suites.size() - 1, index + 1);
 			if (new_index != index)
 			if (new_index != index)
 			{
 			{
 				index = new_index;
 				index = new_index;
+				CurrentSuite().SetFilter(filter);
 				LoadActiveTest();
 				LoadActiveTest();
 			}
 			}
 		}
 		}
@@ -204,9 +209,10 @@ void TestNavigator::ProcessEvent(Rml::Event& event)
 				source_state = SourceType::None;
 				source_state = SourceType::None;
 				viewer->ShowSource(source_state);
 				viewer->ShowSource(source_state);
 			}
 			}
-			else
+			else if (goto_index >= 0)
 			{
 			{
-				Shell::RequestExit();
+				goto_index = -1;
+				viewer->SetGoToText("");
 			}
 			}
 		}
 		}
 		else if (key_identifier == Rml::Input::KI_C && key_ctrl)
 		else if (key_identifier == Rml::Input::KI_C && key_ctrl)
@@ -218,12 +224,12 @@ void TestNavigator::ProcessEvent(Rml::Event& event)
 		}
 		}
 		else if (key_identifier == Rml::Input::KI_HOME)
 		else if (key_identifier == Rml::Input::KI_HOME)
 		{
 		{
-			CurrentSuite().SetIndex(0);
+			CurrentSuite().SetIndex(0, TestSuite::Direction::Forward);
 			LoadActiveTest();
 			LoadActiveTest();
 		}
 		}
 		else if (key_identifier == Rml::Input::KI_END)
 		else if (key_identifier == Rml::Input::KI_END)
 		{
 		{
-			CurrentSuite().SetIndex(CurrentSuite().GetNumTests() - 1);
+			CurrentSuite().SetIndex(CurrentSuite().GetNumTests() - 1, TestSuite::Direction::Backward);
 			LoadActiveTest();
 			LoadActiveTest();
 		}
 		}
 		else if (goto_index >= 0 && key_identifier == Rml::Input::KI_BACK)
 		else if (goto_index >= 0 && key_identifier == Rml::Input::KI_BACK)
@@ -275,12 +281,22 @@ void TestNavigator::ProcessEvent(Rml::Event& event)
 			}
 			}
 		}
 		}
 	}
 	}
+
+	if (event == Rml::EventId::Change)
+	{
+		Rml::Element* element = event.GetTargetElement();
+		if (element->GetId() == "filterinput")
+		{
+			CurrentSuite().SetFilter(event.GetParameter< Rml::String >("value", ""));
+			LoadActiveTest();
+		}
+	}
 }
 }
 
 
 void TestNavigator::LoadActiveTest()
 void TestNavigator::LoadActiveTest()
 {
 {
 	const TestSuite& suite = CurrentSuite();
 	const TestSuite& suite = CurrentSuite();
-	viewer->LoadTest(suite.GetDirectory(), suite.GetFilename(), suite.GetIndex(), suite.GetNumTests(), index, (int)test_suites.size());
+	viewer->LoadTest(suite.GetDirectory(), suite.GetFilename(), suite.GetIndex(), suite.GetNumTests(), suite.GetFilterIndex(), suite.GetNumFilteredTests(), index, (int)test_suites.size());
 	viewer->ShowSource(source_state);
 	viewer->ShowSource(source_state);
 }
 }
 
 
@@ -325,7 +341,7 @@ void TestNavigator::StartTestSuiteIteration(IterationState new_iteration_state)
 
 
 	iteration_state = new_iteration_state;
 	iteration_state = new_iteration_state;
 	iteration_index = 0;
 	iteration_index = 0;
-	suite.SetIndex(iteration_index);
+	suite.SetIndex(iteration_index, TestSuite::Direction::Forward);
 	LoadActiveTest();
 	LoadActiveTest();
 }
 }
 
 
@@ -349,6 +365,7 @@ void TestNavigator::StopTestSuiteIteration()
 	const Rml::String output_directory = GetOutputDirectory();
 	const Rml::String output_directory = GetOutputDirectory();
 	TestSuite& suite = CurrentSuite();
 	TestSuite& suite = CurrentSuite();
 	const int num_tests = suite.GetNumTests();
 	const int num_tests = suite.GetNumTests();
+	const int num_filtered_tests = suite.GetNumFilteredTests();
 
 
 	if (iteration_state == IterationState::Capture)
 	if (iteration_state == IterationState::Capture)
 	{
 	{
@@ -356,6 +373,10 @@ void TestNavigator::StopTestSuiteIteration()
 		{
 		{
 			Rml::Log::Message(Rml::Log::LT_INFO, "Successfully captured %d document screenshots to directory: %s", iteration_index, output_directory.c_str());
 			Rml::Log::Message(Rml::Log::LT_INFO, "Successfully captured %d document screenshots to directory: %s", iteration_index, output_directory.c_str());
 		}
 		}
+		else if (iteration_index == num_filtered_tests)
+		{
+			Rml::Log::Message(Rml::Log::LT_INFO, "Successfully captured %d document screenshots (filtered out of %d total tests) to directory: %s", iteration_index, num_tests, output_directory.c_str());
+		}
 		else
 		else
 		{
 		{
 			Rml::Log::Message(Rml::Log::LT_ERROR, "Test suite capture aborted after %d of %d test(s). Output directory: %s", iteration_index, num_tests, output_directory.c_str());
 			Rml::Log::Message(Rml::Log::LT_ERROR, "Test suite capture aborted after %d of %d test(s). Output directory: %s", iteration_index, num_tests, output_directory.c_str());
@@ -385,13 +406,20 @@ void TestNavigator::StopTestSuiteIteration()
 		for (int i = (int)comparison_results.size(); i < num_tests; i++)
 		for (int i = (int)comparison_results.size(); i < num_tests; i++)
 			skipped.push_back(i);
 			skipped.push_back(i);
 
 
-		const Rml::String summary = Rml::CreateString(256, "  Total tests: %d\n  Equal: %d\n  Not equal: %d\n  Failed: %d\n  Skipped: %d",
+		Rml::String summary = Rml::CreateString(256, "  Total tests: %d\n  Equal: %d\n  Not equal: %d\n  Failed: %d\n  Skipped: %d",
 			num_tests, (int)equal.size(), (int)not_equal.size(), (int)failed.size(), (int)skipped.size());
 			num_tests, (int)equal.size(), (int)not_equal.size(), (int)failed.size(), (int)skipped.size());
 
 
+		if (!suite.GetFilter().empty())
+			summary += "\n  Filter applied: " + suite.GetFilter();
+
 		if (iteration_index == num_tests)
 		if (iteration_index == num_tests)
 		{
 		{
 			Rml::Log::Message(Rml::Log::LT_INFO, "Compared all test documents to their screenshot captures.\n%s", summary.c_str());
 			Rml::Log::Message(Rml::Log::LT_INFO, "Compared all test documents to their screenshot captures.\n%s", summary.c_str());
 		}
 		}
+		else if (iteration_index == num_filtered_tests)
+		{
+			Rml::Log::Message(Rml::Log::LT_INFO, "Compared all filtered test documents to their screenshot captures.\n%s", summary.c_str());
+		}
 		else
 		else
 		{
 		{
 			Rml::Log::Message(Rml::Log::LT_ERROR, "Test suite comparison aborted after %d of %d test(s).\n%s", iteration_index, num_tests, summary.c_str());
 			Rml::Log::Message(Rml::Log::LT_ERROR, "Test suite comparison aborted after %d of %d test(s).\n%s", iteration_index, num_tests, summary.c_str());

+ 2 - 0
Tests/Source/VisualTests/TestNavigator.h

@@ -67,6 +67,8 @@ private:
 	TestViewer* viewer;
 	TestViewer* viewer;
 	TestSuiteList test_suites;
 	TestSuiteList test_suites;
 
 
+	Rml::String test_filter;
+
 	int index = 0;
 	int index = 0;
 	int goto_index = -1;
 	int goto_index = -1;
 	SourceType source_state = SourceType::None;
 	SourceType source_state = SourceType::None;

+ 88 - 1
Tests/Source/VisualTests/TestSuite.h

@@ -34,6 +34,8 @@
 
 
 class TestSuite {
 class TestSuite {
 public:
 public:
+	enum class Direction { None, Forward, Backward, Any };
+
 	TestSuite(Rml::String directory, Rml::StringList files) : directory(std::move(directory)), files(std::move(files))
 	TestSuite(Rml::String directory, Rml::StringList files) : directory(std::move(directory)), files(std::move(files))
 	{
 	{
 		RMLUI_ASSERTMSG(!this->files.empty(), "At least one file in the test suite is required.");
 		RMLUI_ASSERTMSG(!this->files.empty(), "At least one file in the test suite is required.");
@@ -52,29 +54,114 @@ public:
 		return directory + '/' + files[index];
 		return directory + '/' + files[index];
 	}
 	}
 
 
-	bool SetIndex(int new_index)
+	const Rml::String& GetFilter() const
+	{
+		return filter;
+	}
+	void SetFilter(Rml::String new_filter)
+	{
+		filter = new_filter;
+		UpdateFilteredTests();
+		SetIndex(index, Direction::Any);
+	}
+
+	bool SetIndex(int new_index, Direction filter_direction = Direction::None)
 	{
 	{
+		new_index = GetIndexFiltered(new_index, filter_direction);
 		if (new_index < 0 || new_index >= (int)files.size())
 		if (new_index < 0 || new_index >= (int)files.size())
 			return false;
 			return false;
 		index = new_index;
 		index = new_index;
 		return true;
 		return true;
 	}
 	}
 
 
+	bool Next()
+	{
+		return SetIndex(index + 1, Direction::Forward);
+	}
+	bool Previous()
+	{
+		return SetIndex(index - 1, Direction::Backward);
+	}
+
 	int GetIndex() const
 	int GetIndex() const
 	{
 	{
 		return index;
 		return index;
 	}
 	}
 
 
+	int GetFilterIndex() const
+	{
+		auto it = std::lower_bound(filtered_tests_indices.begin(), filtered_tests_indices.end(), index);
+		if (it == filtered_tests_indices.end() || *it != index)
+			return -1;
+		return int(it - filtered_tests_indices.begin());
+	}
+
 	int GetNumTests() const
 	int GetNumTests() const
 	{
 	{
 		return (int)files.size();
 		return (int)files.size();
 	}
 	}
 
 
+	int GetNumFilteredTests() const
+	{
+		return filter.empty() ? GetNumTests() : (int)filtered_tests_indices.size();
+	}
+
+
 private:
 private:
+	int GetIndexFiltered(int new_index, Direction filter_direction) const
+	{
+		if (filter_direction == Direction::None || filtered_tests_indices.empty())
+			return new_index;
+
+		auto it = std::lower_bound(filtered_tests_indices.begin(), filtered_tests_indices.end(), new_index);
+		
+		// Exact match
+		if (it != filtered_tests_indices.end() && *it == new_index)
+			return *it;
+
+		if (filter_direction == Direction::Forward)
+		{
+			if (it != filtered_tests_indices.end())
+				return *it;
+		}
+		else if (filter_direction == Direction::Backward)
+		{
+			if (it != filtered_tests_indices.begin())
+				return *(it - 1);
+		}
+		else if (filter_direction == Direction::Any)
+		{
+			// Like forward but will go back if not possible.
+			return it == filtered_tests_indices.end() ? *(it - 1) : *it;
+		}
+
+		return -1;
+	}
+
+	bool MatchesFilter(int id) const
+	{
+		RMLUI_ASSERT(id >= 0 && id < (int)files.size());
+		return (files[id].find(filter) != Rml::String::npos);
+	}
+
+	void UpdateFilteredTests()
+	{
+		filtered_tests_indices.clear();
+		for (int i = 0; i < (int)files.size(); i++)
+		{
+			if (MatchesFilter(i))
+				filtered_tests_indices.push_back(i);
+		}
+	}
+
 	Rml::String directory;
 	Rml::String directory;
 	Rml::StringList files;
 	Rml::StringList files;
 
 
 	int index = 0;
 	int index = 0;
+
+	Rml::String filter;
+
+	Rml::Vector<int> filtered_tests_indices;
 };
 };
 
 
 using TestSuiteList = Rml::Vector<TestSuite>;
 using TestSuiteList = Rml::Vector<TestSuite>;

+ 136 - 51
Tests/Source/VisualTests/TestViewer.cpp

@@ -53,6 +53,46 @@ static void InitializeXmlNodeHandlers()
 }
 }
 
 
 
 
+class EventListenerLinks : public Rml::EventListener {
+public:
+
+	void ProcessEvent(Rml::Event& event) override
+	{
+		Rml::Element* element = event.GetCurrentElement();
+		Rml::String href = element->GetAttribute<Rml::String>("href", "");
+
+		if (href.empty() || !hover_text)
+			return;
+		
+		if (event == Rml::EventId::Click)
+		{
+			Shell::SetClipboardText(href);
+			hover_text->SetInnerRML("Copied to clipboard");
+			hover_text->SetClass("confirmation", true);
+		}
+		else if (event == Rml::EventId::Mouseover)
+		{
+			hover_text->SetInnerRML(Rml::StringUtilities::EncodeRml(href));
+		}
+		else if (event == Rml::EventId::Mouseout)
+		{
+			hover_text->SetInnerRML("");
+			hover_text->SetClass("confirmation", false);
+		}
+	}
+
+	void SetHoverTextElement(Element* element)
+	{
+		hover_text = element;
+	}
+
+private:
+
+	Element* hover_text = nullptr;
+};
+static EventListenerLinks event_listener_links;
+
+
 TestViewer::TestViewer(Rml::Context* context) : context(context)
 TestViewer::TestViewer(Rml::Context* context) : context(context)
 {
 {
 	InitializeXmlNodeHandlers();
 	InitializeXmlNodeHandlers();
@@ -61,6 +101,7 @@ TestViewer::TestViewer(Rml::Context* context) : context(context)
 
 
 	document_description = context->LoadDocument(local_data_path_prefix + "description.rml");
 	document_description = context->LoadDocument(local_data_path_prefix + "description.rml");
 	RMLUI_ASSERT(document_description);
 	RMLUI_ASSERT(document_description);
+	event_listener_links.SetHoverTextElement(document_description->GetElementById("hovertext"));
 	document_description->Show();
 	document_description->Show();
 
 
 	document_source = context->LoadDocument(local_data_path_prefix + "view_source.rml");
 	document_source = context->LoadDocument(local_data_path_prefix + "view_source.rml");
@@ -69,6 +110,8 @@ TestViewer::TestViewer(Rml::Context* context) : context(context)
 
 
 TestViewer::~TestViewer()
 TestViewer::~TestViewer()
 {
 {
+	event_listener_links.SetHoverTextElement(nullptr);
+
 	for (ElementDocument* doc : { document_test, document_description, document_source, document_reference })
 	for (ElementDocument* doc : { document_test, document_description, document_source, document_reference })
 	{
 	{
 		if (doc)
 		if (doc)
@@ -119,14 +162,14 @@ void TestViewer::ShowSource(SourceType type)
 			RMLUI_ASSERT(element);
 			RMLUI_ASSERT(element);
 			element->SetInnerRML(rml_source);
 			element->SetInnerRML(rml_source);
 
 
-			document_source->Show();
+			document_source->Show(ModalFlag::None, FocusFlag::None);
 		}
 		}
 	}
 	}
 }
 }
 
 
 
 
 
 
-bool TestViewer::LoadTest(const Rml::String& directory, const Rml::String& filename, int test_index, int number_of_tests, int suite_index, int number_of_suites)
+bool TestViewer::LoadTest(const Rml::String& directory, const Rml::String& filename, int test_index, int number_of_tests, int filtered_test_index, int filtered_number_of_tests, int suite_index, int number_of_suites)
 {
 {
 	if (document_test)
 	if (document_test)
 	{
 	{
@@ -146,79 +189,121 @@ bool TestViewer::LoadTest(const Rml::String& directory, const Rml::String& filen
 	meta_handler->ClearMetaList();
 	meta_handler->ClearMetaList();
 	link_handler->ClearLinkList();
 	link_handler->ClearLinkList();
 
 
-	Element* description_test_suite = document_description->GetElementById("test_suite");
-	RMLUI_ASSERT(description_test_suite);
-	description_test_suite->SetInnerRML(CreateString(64, "Test suite %d of %d", suite_index + 1, number_of_suites));
+	const Rml::String test_path = directory + '/' + filename;
+	Rml::String reference_path;
 
 
-	SetGoToText("");
-
-	source_test = LoadFile(directory + '/' + filename);
-	if (source_test.empty())
-		return false;
+	// Load test document, and reference document if it exists.
+	{
+		source_test = LoadFile(test_path);
+		if (source_test.empty())
+			return false;
 
 
-	document_test = context->LoadDocumentFromMemory(source_test);
-	if (!document_test)
-		return false;
+		document_test = context->LoadDocumentFromMemory(source_test);
+		if (!document_test)
+			return false;
 
 
-	document_test->Show();
+		document_test->Show(ModalFlag::None, FocusFlag::None);
 
 
-	for (const LinkItem& item : link_handler->GetLinkList())
-	{
-		if (item.rel == "match")
+		for (const LinkItem& item : link_handler->GetLinkList())
 		{
 		{
-			reference_filename = item.href;
-			break;
+			if (item.rel == "match")
+			{
+				reference_filename = item.href;
+				break;
+			}
 		}
 		}
-	}
 
 
-	if (!reference_filename.empty())
-	{
-		source_reference = LoadFile(directory + '/' + reference_filename);
+		reference_path = directory + '/' + reference_filename;
 
 
-		if (!source_reference.empty())
+		if (!reference_filename.empty())
 		{
 		{
-			document_reference = context->LoadDocumentFromMemory(source_reference);
-			if (document_reference)
+			source_reference = LoadFile(reference_path);
+
+			if (!source_reference.empty())
 			{
 			{
-				document_reference->SetProperty(PropertyId::Left, Property(510.f, Property::PX));
-				document_reference->Show();
+				document_reference = context->LoadDocumentFromMemory(source_reference);
+				if (document_reference)
+				{
+					document_reference->SetProperty(PropertyId::Left, Property(510.f, Property::PX));
+					document_reference->Show(ModalFlag::None, FocusFlag::None);
+				}
 			}
 			}
 		}
 		}
 	}
 	}
 
 
-	String rml_description = Rml::CreateString(512, "<h1>%s</h1><p>Test %d of %d.<br/>%s", document_test->GetTitle().c_str(), test_index + 1, number_of_tests, filename.c_str());
-	if (!reference_filename.empty())
+	SetGoToText("");
+
+	// Description Header
+	{
+		Element* description_header = document_description->GetElementById("header");
+		RMLUI_ASSERT(description_header);
+
+		description_header->SetInnerRML(CreateString(512, "Test suite %d of %d<br/>Test %d of %d<br/>",
+			suite_index + 1, number_of_suites, test_index + 1, number_of_tests));
+	}
+
+	// Description Filter
 	{
 	{
-		if (document_reference)
-			rml_description += "<br/>" + reference_filename;
+		Element* description_filter_text = document_description->GetElementById("filter_text");
+		RMLUI_ASSERT(description_filter_text);
+		if (filtered_number_of_tests == 0)
+			description_filter_text->SetInnerRML("No matches");
+		else if (filtered_number_of_tests < number_of_tests && filtered_test_index >= 0)
+			description_filter_text->SetInnerRML(CreateString(128, "Filtered %d of %d", filtered_test_index + 1, filtered_number_of_tests));
 		else
 		else
-			rml_description += "<br/>(X " + reference_filename + ")";
+			description_filter_text->SetInnerRML("");
 	}
 	}
-	rml_description += "</p>";
 
 
-	const LinkList& link_list = link_handler->GetLinkList();
-	if(!link_list.empty())
+
+	// Description Content
 	{
 	{
-		rml_description += "<p class=\"links\">";
-		for (const LinkItem& item : link_list)
-		{
-			if (item.rel == "match")
-				continue;
+		String rml_description = Rml::CreateString(512, "<h1>%s</h1><p><a href=\"%s\">%s</a>",
+			document_test->GetTitle().c_str(), test_path.c_str(), filename.c_str());
 
 
-			rml_description += "<a href=\"" + item.href + "\">" + item.rel + "</a> ";
+		if (!reference_filename.empty())
+		{
+			if (document_reference)
+				rml_description += "<br/><a href=\"" + reference_path + "\">" + reference_filename + "</a>";
+			else
+				rml_description += "<br/>(missing)&nbsp;" + reference_filename + "";
 		}
 		}
 		rml_description += "</p>";
 		rml_description += "</p>";
-	}
 
 
-	for (const MetaItem& item : meta_handler->GetMetaList())
-	{
-		rml_description += "<h3>" + item.name + "</h3>";
-		rml_description += "<p style=\"min-height: 120px;\">" + item.content + "</p>";
-	}
 
 
-	Element* description_content = document_description->GetElementById("content");
-	RMLUI_ASSERT(description_content);
-	description_content->SetInnerRML(rml_description);
+		const LinkList& link_list = link_handler->GetLinkList();
+		if(!link_list.empty())
+		{
+			rml_description += "<p class=\"links\">";
+			for (const LinkItem& item : link_list)
+			{
+				if (item.rel == "match")
+					continue;
+
+				rml_description += "<a href=\"" + item.href + "\">" + item.rel + "</a> ";
+			}
+			rml_description += "</p>";
+		}
+
+		for (const MetaItem& item : meta_handler->GetMetaList())
+		{
+			rml_description += "<h3>" + item.name + "</h3>";
+			rml_description += "<p style=\"min-height: 120px;\">" + item.content + "</p>";
+		}
+
+		Element* description_content = document_description->GetElementById("content");
+		RMLUI_ASSERT(description_content);
+		description_content->SetInnerRML(rml_description);
+
+		// Add link hover and click handler.
+		Rml::ElementList link_elements;
+		description_content->GetElementsByTagName(link_elements, "a");
+
+		for (Rml::Element* element : link_elements) {
+			element->AddEventListener(Rml::EventId::Click, &event_listener_links);
+			element->AddEventListener(Rml::EventId::Mouseover, &event_listener_links);
+			element->AddEventListener(Rml::EventId::Mouseout, &event_listener_links);
+		}
+	}
 
 
 	return true;
 	return true;
 }
 }

+ 1 - 1
Tests/Source/VisualTests/TestViewer.h

@@ -45,7 +45,7 @@ public:
 	
 	
 	void ShowSource(SourceType type);
 	void ShowSource(SourceType type);
 
 
-	bool LoadTest(const Rml::String& directory, const Rml::String& filename, int test_index, int number_of_tests, int suite_index, int number_of_suites);
+	bool LoadTest(const Rml::String& directory, const Rml::String& filename, int test_index, int number_of_tests, int filtered_test_index, int filtered_number_of_tests, int suite_index, int number_of_suites);
 
 
 	void SetGoToText(const Rml::String& rml);
 	void SetGoToText(const Rml::String& rml);
 
 

+ 1 - 0
Tests/Source/VisualTests/main.cpp

@@ -34,6 +34,7 @@
 #include <RmlUi/Core/Core.h>
 #include <RmlUi/Core/Core.h>
 #include <RmlUi/Core/Element.h>
 #include <RmlUi/Core/Element.h>
 #include <RmlUi/Core/ElementDocument.h>
 #include <RmlUi/Core/ElementDocument.h>
+#include <RmlUi/Core/EventListenerInstancer.h>
 #include <RmlUi/Core/ID.h>
 #include <RmlUi/Core/ID.h>
 #include <RmlUi/Core/StringUtilities.h>
 #include <RmlUi/Core/StringUtilities.h>
 #include <RmlUi/Debugger.h>
 #include <RmlUi/Debugger.h>