diff --git a/dojo/templates/dojo/endpoint_pdf_report.html b/dojo/templates/dojo/endpoint_pdf_report.html
index 96d76d0b71f..84d152f97e5 100644
--- a/dojo/templates/dojo/endpoint_pdf_report.html
+++ b/dojo/templates/dojo/endpoint_pdf_report.html
@@ -285,7 +285,7 @@
Notes
{% endblock %}
diff --git a/dojo/templates/dojo/engagement_pdf_report.html b/dojo/templates/dojo/engagement_pdf_report.html
index 8b0dd906af2..8ae8c2d29a5 100644
--- a/dojo/templates/dojo/engagement_pdf_report.html
+++ b/dojo/templates/dojo/engagement_pdf_report.html
@@ -420,7 +420,7 @@ Notes
{% endblock %}
diff --git a/dojo/templates/dojo/finding_pdf_report.html b/dojo/templates/dojo/finding_pdf_report.html
index 5b1011fd178..c5667c1c37e 100644
--- a/dojo/templates/dojo/finding_pdf_report.html
+++ b/dojo/templates/dojo/finding_pdf_report.html
@@ -270,7 +270,7 @@ Notes
{% endblock %}
diff --git a/dojo/templates/dojo/product_endpoint_pdf_report.html b/dojo/templates/dojo/product_endpoint_pdf_report.html
index b86e2860267..77348e6eb3d 100644
--- a/dojo/templates/dojo/product_endpoint_pdf_report.html
+++ b/dojo/templates/dojo/product_endpoint_pdf_report.html
@@ -338,7 +338,7 @@ Notes
{% endblock metrics %}
{% endblock %}
diff --git a/dojo/templates/dojo/product_pdf_report.html b/dojo/templates/dojo/product_pdf_report.html
index a25e0084192..9501b62afbd 100644
--- a/dojo/templates/dojo/product_pdf_report.html
+++ b/dojo/templates/dojo/product_pdf_report.html
@@ -391,7 +391,7 @@ Notes
{% endblock %}
diff --git a/dojo/templates/dojo/product_type_pdf_report.html b/dojo/templates/dojo/product_type_pdf_report.html
index 11eeb095e48..9848f8bc498 100644
--- a/dojo/templates/dojo/product_type_pdf_report.html
+++ b/dojo/templates/dojo/product_type_pdf_report.html
@@ -320,7 +320,7 @@ Notes
{% endblock %}
diff --git a/dojo/templates/dojo/test_pdf_report.html b/dojo/templates/dojo/test_pdf_report.html
index c86684da0c6..c0001a3c150 100644
--- a/dojo/templates/dojo/test_pdf_report.html
+++ b/dojo/templates/dojo/test_pdf_report.html
@@ -434,7 +434,7 @@ Notes
{% endblock %}
diff --git a/dojo/templates_classic/dojo/endpoint_pdf_report.html b/dojo/templates_classic/dojo/endpoint_pdf_report.html
index a1d6843c144..37076ef3e9c 100644
--- a/dojo/templates_classic/dojo/endpoint_pdf_report.html
+++ b/dojo/templates_classic/dojo/endpoint_pdf_report.html
@@ -287,7 +287,7 @@ Notes
{% endblock %}
diff --git a/dojo/templates_classic/dojo/engagement_pdf_report.html b/dojo/templates_classic/dojo/engagement_pdf_report.html
index 7b90548ae1b..b10b3293ecd 100644
--- a/dojo/templates_classic/dojo/engagement_pdf_report.html
+++ b/dojo/templates_classic/dojo/engagement_pdf_report.html
@@ -422,7 +422,7 @@ Notes
{% endblock %}
diff --git a/dojo/templates_classic/dojo/finding_pdf_report.html b/dojo/templates_classic/dojo/finding_pdf_report.html
index 86820527db2..d44a3b1092c 100644
--- a/dojo/templates_classic/dojo/finding_pdf_report.html
+++ b/dojo/templates_classic/dojo/finding_pdf_report.html
@@ -266,7 +266,7 @@ Notes
{% endblock %}
diff --git a/dojo/templates_classic/dojo/product_endpoint_pdf_report.html b/dojo/templates_classic/dojo/product_endpoint_pdf_report.html
index b5c67cf9b1c..63d91e77624 100644
--- a/dojo/templates_classic/dojo/product_endpoint_pdf_report.html
+++ b/dojo/templates_classic/dojo/product_endpoint_pdf_report.html
@@ -344,7 +344,7 @@ Notes
{% endblock metrics %}
{% endblock %}
diff --git a/dojo/templates_classic/dojo/product_pdf_report.html b/dojo/templates_classic/dojo/product_pdf_report.html
index 347fa6f810e..3580801dfbe 100644
--- a/dojo/templates_classic/dojo/product_pdf_report.html
+++ b/dojo/templates_classic/dojo/product_pdf_report.html
@@ -397,7 +397,7 @@ Notes
{% endif %}
{% endblock %}
diff --git a/dojo/templates_classic/dojo/product_type_pdf_report.html b/dojo/templates_classic/dojo/product_type_pdf_report.html
index 36a2f82a1b7..3c5234e6997 100644
--- a/dojo/templates_classic/dojo/product_type_pdf_report.html
+++ b/dojo/templates_classic/dojo/product_type_pdf_report.html
@@ -322,7 +322,7 @@ Notes
{% endblock %}
diff --git a/dojo/templates_classic/dojo/test_pdf_report.html b/dojo/templates_classic/dojo/test_pdf_report.html
index 7dd1e0f102c..0989b8796c0 100644
--- a/dojo/templates_classic/dojo/test_pdf_report.html
+++ b/dojo/templates_classic/dojo/test_pdf_report.html
@@ -436,7 +436,7 @@ Notes
{% endblock %}
diff --git a/tests/report_builder_test.py b/tests/report_builder_test.py
index 7f11eb93fec..0ce5bb08110 100644
--- a/tests/report_builder_test.py
+++ b/tests/report_builder_test.py
@@ -3,6 +3,7 @@
from base_test_class import BaseTestCase
from product_test import ProductTest
+from selenium.common.exceptions import TimeoutException
from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions
@@ -30,6 +31,40 @@ def enter_values(self, driver):
if "wysiwyg-content" in class_names:
widget.find_element(By.CLASS_NAME, "editor").send_keys("wysiwyg")
+ # Regression guard for the report-chart / table-of-contents race: the
+ # executive-summary charts are rendered from a ready handler, while the
+ # table-of-contents builder runs on window.onload and reassigns
+ # #contents.innerHTML. That reassignment re-parses the subtree and
+ # destroys any chart canvases painted before it (Chart.js in the new UI,
+ # Flot in the classic UI). With executive summary AND table of contents
+ # both enabled, every chart must end up painted once the page settles.
+ def assert_report_charts_painted(self, chart_ids):
+ driver = self.driver
+ charts_painted = (
+ "return arguments[0].map(function (id) {"
+ " var el = document.getElementById(id);"
+ " if (!el) { return false; }"
+ " var canvases = el.querySelectorAll('canvas');"
+ " for (var i = 0; i < canvases.length; i++) {"
+ " if (!canvases[i].width || !canvases[i].height) { continue; }"
+ " var ctx = canvases[i].getContext('2d');"
+ " var data = ctx.getImageData(0, 0, canvases[i].width, canvases[i].height).data;"
+ " for (var j = 3; j < data.length; j += 64) {"
+ " if (data[j] > 0) { return true; }"
+ " }"
+ " }"
+ " return false;"
+ "});"
+ )
+ try:
+ WebDriverWait(driver, 20).until(lambda d: all(d.execute_script(charts_painted, chart_ids)))
+ except TimeoutException:
+ self.fail(
+ "Executive-summary charts were not painted after page load "
+ f"({chart_ids} painted: {driver.execute_script(charts_painted, chart_ids)}); "
+ "the table-of-contents builder likely destroyed them.",
+ )
+
def generate_HTML_report(self):
driver = self.driver
driver.get(self.base_url + "reports/builder")
@@ -59,6 +94,10 @@ def test_product_type_report(self):
driver.find_element(By.NAME, "_generate").click()
+ # opened_per_month_2 is only rendered when the product type has
+ # endpoint-per-month data, so only the unconditional chart is asserted.
+ self.assert_report_charts_painted(["open_findings"])
+
def test_product_report(self):
driver = self.driver
self.goto_product_overview(driver)
@@ -77,6 +116,8 @@ def test_product_report(self):
driver.find_element(By.NAME, "_generate").click()
+ self.assert_report_charts_painted(["open_findings", "finding_age"])
+
def test_engagement_report(self):
driver = self.driver
self.goto_product_overview(driver)
@@ -97,6 +138,8 @@ def test_engagement_report(self):
driver.find_element(By.NAME, "_generate").click()
+ self.assert_report_charts_painted(["open_findings", "finding_age"])
+
def test_test_report(self):
driver = self.driver
self.goto_product_overview(driver)
@@ -118,6 +161,8 @@ def test_test_report(self):
driver.find_element(By.NAME, "_generate").click()
+ self.assert_report_charts_painted(["open_findings", "finding_age"])
+
def test_product_endpoint_report(self):
driver = self.driver
self.goto_product_overview(driver)
@@ -144,7 +189,14 @@ def test_product_endpoint_report(self):
driver.find_element(By.NAME, "_generate").click()
+ self.assert_report_charts_painted(["accepted_findings", "open_findings", "closed_findings", "finding_age"])
+
def test_product_list_report(self):
+ # Unlike the report tests above, this does not call
+ # assert_report_charts_painted(): it generates the Findings Report,
+ # whose view currently errors before the report (and its charts)
+ # render, so there are no chart canvases to assert on. This test only
+ # exercises the generate action until that view is fixed.
driver = self.driver
self.goto_product_overview(driver)
driver.find_element(By.ID, "dropdownMenu1").click()