From daa243f74a358ed5e9b0e2e1c6dad20ec10eb131 Mon Sep 17 00:00:00 2001 From: Tobias Deiminger Date: Fri, 18 Oct 2024 23:22:47 +0200 Subject: [PATCH] Fix generated search URLs for project statistics Search URLs generated for the project statistic page were caught by Jinja2 autoescaping, which makes them invalid. A good place to mark them as safe is the view object: It's the immediate layer below Jinja templates and specifically made to provide strings included by a Jinja2 template. Relates to #1920. --- .../document_screen_view_object.py | 2 +- .../project_statistics_view_object.py | 16 +++++---- .../components/table_key_value/index.jinja | 6 +++- .../project_statistics/project_statistics.py | 8 +++++ .../end2end/helpers/screens/search/search.py | 13 +++++++ .../view_project_statistics/input.sdoc | 35 +++++++++++++++++++ .../view_project_statistics/strictdoc.toml | 3 +- .../view_project_statistics/test_case.py | 33 +++++++++++++++++ 8 files changed, 106 insertions(+), 10 deletions(-) create mode 100644 tests/end2end/screens/project_statistics/view_project_statistics/input.sdoc diff --git a/strictdoc/export/html/generators/view_objects/document_screen_view_object.py b/strictdoc/export/html/generators/view_objects/document_screen_view_object.py index 2548f4917..0775bc9ed 100644 --- a/strictdoc/export/html/generators/view_objects/document_screen_view_object.py +++ b/strictdoc/export/html/generators/view_objects/document_screen_view_object.py @@ -246,7 +246,7 @@ def render_standalone_document_link( else "/".join((root_prefix, document_link)) ) - def render_static_url(self, url: str) -> str: + def render_static_url(self, url: str) -> Markup: return Markup(self.link_renderer.render_static_url(url)) def render_local_anchor(self, node) -> str: diff --git a/strictdoc/export/html/generators/view_objects/project_statistics_view_object.py b/strictdoc/export/html/generators/view_objects/project_statistics_view_object.py index e2306eb83..ed9b3c4aa 100644 --- a/strictdoc/export/html/generators/view_objects/project_statistics_view_object.py +++ b/strictdoc/export/html/generators/view_objects/project_statistics_view_object.py @@ -2,6 +2,8 @@ from dataclasses import dataclass from datetime import datetime +from markupsafe import Markup + from strictdoc import __version__ from strictdoc.core.document_tree_iterator import DocumentTreeIterator from strictdoc.core.project_config import ProjectConfig @@ -34,19 +36,19 @@ def __init__( self.is_running_on_server: bool = project_config.is_running_on_server self.strictdoc_version = __version__ - def render_screen(self, jinja_environment: JinjaEnvironment): + def render_screen(self, jinja_environment: JinjaEnvironment) -> Markup: return jinja_environment.render_template_as_markup( "screens/project_statistics/index.jinja", view_object=self ) - def render_static_url(self, url: str): - return self.link_renderer.render_static_url(url) + def render_static_url(self, url: str) -> Markup: + return Markup(self.link_renderer.render_static_url(url)) - def render_url(self, url: str): - return self.link_renderer.render_url(url) + def render_url(self, url: str) -> Markup: + return Markup(self.link_renderer.render_url(url)) - def render_static_url_with_prefix(self, url: str): - return self.link_renderer.render_static_url_with_prefix(url) + def render_static_url_with_prefix(self, url: str) -> Markup: + return Markup(self.link_renderer.render_static_url_with_prefix(url)) def is_empty_tree(self) -> bool: return self.document_tree_iterator.is_empty_tree() diff --git a/strictdoc/export/html/templates/components/table_key_value/index.jinja b/strictdoc/export/html/templates/components/table_key_value/index.jinja index cb6b4f9ff..498cd12af 100644 --- a/strictdoc/export/html/templates/components/table_key_value/index.jinja +++ b/strictdoc/export/html/templates/components/table_key_value/index.jinja @@ -7,7 +7,11 @@
{{ obj["Section"] }}
{% else %} {% if view_object.project_config.is_activated_search() and obj["Link"] is defined %} - {{ obj["Key"] }} + {{ obj["Key"] }} + {% else %}
{{ obj["Key"] }}
{% endif %} diff --git a/tests/end2end/helpers/screens/project_statistics/project_statistics.py b/tests/end2end/helpers/screens/project_statistics/project_statistics.py index d36b68e72..aa1469512 100644 --- a/tests/end2end/helpers/screens/project_statistics/project_statistics.py +++ b/tests/end2end/helpers/screens/project_statistics/project_statistics.py @@ -1,12 +1,20 @@ from selenium.webdriver.common.by import By from seleniumbase import BaseCase +from tests.end2end.helpers.screens.search.search import Screen_SearchResults + class Screen_ProjectStatistics: # pylint: disable=invalid-name def __init__(self, test_case: BaseCase) -> None: assert isinstance(test_case, BaseCase) self.test_case: BaseCase = test_case + def do_click_on_search_link(self, test_id: str) -> Screen_SearchResults: + self.test_case.click_xpath( + f'//a[@data-testid="{test_id}"]', + ) + return Screen_SearchResults(self.test_case) + def assert_on_screen(self) -> None: self.test_case.assert_element( '//body[@data-viewtype="project-statistics"]', diff --git a/tests/end2end/helpers/screens/search/search.py b/tests/end2end/helpers/screens/search/search.py index f8e96b580..924ba0dae 100644 --- a/tests/end2end/helpers/screens/search/search.py +++ b/tests/end2end/helpers/screens/search/search.py @@ -1,3 +1,5 @@ +from selenium.webdriver.common.by import By + from tests.end2end.helpers.screens.screen import Screen @@ -7,6 +9,17 @@ def do_click_on_search_requirements(self): '//a[@data-testid="node.is_requirement"]', ) + def assert_nr_results(self, nr_results: int): + content = ( + f"Found {nr_results} results." + if nr_results > 0 + else "Nothing matching the query was found." + ) + self.test_case.assert_element( + "//div[@class='sdoc-form-success']" f"[contains(., '{content}')]", + by=By.XPATH, + ) + class Screen_Search(Screen): # pylint: disable=invalid-name def do_click_on_search_requirements(self) -> Screen_SearchResults: diff --git a/tests/end2end/screens/project_statistics/view_project_statistics/input.sdoc b/tests/end2end/screens/project_statistics/view_project_statistics/input.sdoc new file mode 100644 index 000000000..70b1837cb --- /dev/null +++ b/tests/end2end/screens/project_statistics/view_project_statistics/input.sdoc @@ -0,0 +1,35 @@ +[DOCUMENT] +TITLE: Test document + +[SECTION] +TITLE: Section title + +[REQUIREMENT] +UID: REQ-1 +STATUS: Active +TITLE: Requirement title +STATEMENT: Requirement statement. +RATIONALE: Rationale. TBD + +[REQUIREMENT] +UID: REQ-2 +STATUS: Draft +TITLE: Requirement title +STATEMENT: Requirement statement. + +[REQUIREMENT] +UID: REQ-3 +STATUS: Other +TITLE: Requirement title +STATEMENT: Requirement statement. + +[REQUIREMENT] +STATUS: Backlog +TITLE: Requirement title +STATEMENT: Requirement statement. TBD + +[REQUIREMENT] +TITLE: Requirement title +STATEMENT: Requirement statement. TBC + +[/SECTION] diff --git a/tests/end2end/screens/project_statistics/view_project_statistics/strictdoc.toml b/tests/end2end/screens/project_statistics/view_project_statistics/strictdoc.toml index 4241d0c27..5868b4638 100644 --- a/tests/end2end/screens/project_statistics/view_project_statistics/strictdoc.toml +++ b/tests/end2end/screens/project_statistics/view_project_statistics/strictdoc.toml @@ -1,5 +1,6 @@ [project] features = [ - "PROJECT_STATISTICS_SCREEN" + "PROJECT_STATISTICS_SCREEN", + "SEARCH" ] diff --git a/tests/end2end/screens/project_statistics/view_project_statistics/test_case.py b/tests/end2end/screens/project_statistics/view_project_statistics/test_case.py index 3756f4fe7..42d84f828 100644 --- a/tests/end2end/screens/project_statistics/view_project_statistics/test_case.py +++ b/tests/end2end/screens/project_statistics/view_project_statistics/test_case.py @@ -7,12 +7,36 @@ from tests.end2end.helpers.screens.project_statistics.project_statistics import ( Screen_ProjectStatistics, ) +from tests.end2end.helpers.screens.search.search import Screen_SearchResults from tests.end2end.server import SDocTestServer path_to_this_test_file_folder = os.path.dirname(os.path.abspath(__file__)) class Test(E2ECase): + expected_search_results = [ + ("search-total-sections", 1), + ("search-sections-without-any-text", 1), + ("search-total-requirements", 5), + ("search-requirements-with-no-uid", 2), + ( + "search-root-level-requirements-not-connected-to-by-any-requirement", + 0, + ), + ( + "search-non-root-level-requirements-not-connected-to-any-parent-requirement", + 4, + ), + ("search-requirements-with-no-rationale", 4), + ("search-requirements-with-no-status", 1), + ("search-requirements-with-status-active", 1), + ("search-requirements-with-status-draft", 1), + ("search-requirements-with-status-backlog", 1), + ("search-requirements-with-all-other-statuses", 1), + ("search-total-tbd", 2), + ("search-total-tbc", 1), + ] + def test(self): with SDocTestServer( input_path=path_to_this_test_file_folder @@ -27,3 +51,12 @@ def test(self): screen_project_index.do_click_on_project_statistics_link() ) screen_requirements_coverage.assert_on_screen() + + for test_id, nr_results in self.expected_search_results: + screen_search_results: Screen_SearchResults = ( + screen_requirements_coverage.do_click_on_search_link( + test_id + ) + ) + screen_search_results.assert_nr_results(nr_results) + self.go_back()