2023-01-27 14:13:12 +00:00
|
|
|
from __future__ import annotations
|
|
|
|
|
2020-01-09 14:25:45 +00:00
|
|
|
import shutil
|
2022-08-15 16:30:45 +00:00
|
|
|
from collections import defaultdict
|
2020-01-09 14:25:45 +00:00
|
|
|
from datetime import datetime
|
|
|
|
from pathlib import Path
|
2020-01-07 14:42:55 +00:00
|
|
|
|
|
|
|
import dominate
|
2021-06-22 09:01:29 +00:00
|
|
|
import dominate.tags as t
|
2022-08-15 16:30:45 +00:00
|
|
|
from dominate.tags import a, div, h1, h2, hr, p, span, strong, table, th, tr
|
2020-02-18 09:20:02 +00:00
|
|
|
from dominate.util import text
|
2020-01-07 14:42:55 +00:00
|
|
|
|
2023-01-27 14:13:12 +00:00
|
|
|
from ..common import UI_TESTS_DIR, TestCase, TestResult
|
2020-03-03 14:50:57 +00:00
|
|
|
from . import download, html
|
2020-01-07 14:42:55 +00:00
|
|
|
|
2022-01-28 15:02:17 +00:00
|
|
|
HERE = Path(__file__).resolve().parent
|
2023-01-27 14:13:12 +00:00
|
|
|
REPORTS_PATH = UI_TESTS_DIR / "reports"
|
|
|
|
TESTREPORT_PATH = REPORTS_PATH / "test"
|
|
|
|
IMAGES_PATH = TESTREPORT_PATH / "images"
|
2021-06-22 09:01:29 +00:00
|
|
|
|
|
|
|
STYLE = (HERE / "testreport.css").read_text()
|
|
|
|
SCRIPT = (HERE / "testreport.js").read_text()
|
|
|
|
|
2022-08-15 16:30:45 +00:00
|
|
|
# These two html files are referencing each other
|
|
|
|
ALL_SCREENS = "all_screens.html"
|
|
|
|
ALL_UNIQUE_SCREENS = "all_unique_screens.html"
|
|
|
|
|
|
|
|
|
2022-01-28 18:26:03 +00:00
|
|
|
def document(
|
2023-01-27 14:13:12 +00:00
|
|
|
title: str, actual_hash: str | None = None, index: bool = False
|
2022-01-28 18:26:03 +00:00
|
|
|
) -> dominate.document:
|
2021-06-22 09:01:29 +00:00
|
|
|
doc = dominate.document(title=title)
|
|
|
|
style = t.style()
|
|
|
|
style.add_raw_string(STYLE)
|
|
|
|
script = t.script()
|
|
|
|
script.add_raw_string(SCRIPT)
|
|
|
|
doc.head.add(style, script)
|
|
|
|
|
|
|
|
if actual_hash is not None:
|
|
|
|
doc.body["data-actual-hash"] = actual_hash
|
|
|
|
|
|
|
|
if index:
|
|
|
|
doc.body["data-index"] = True
|
|
|
|
|
|
|
|
return doc
|
2020-01-07 14:42:55 +00:00
|
|
|
|
|
|
|
|
2023-01-27 14:13:12 +00:00
|
|
|
def _header(test_name: str, expected_hash: str | None, actual_hash: str) -> None:
|
2020-01-07 14:42:55 +00:00
|
|
|
h1(test_name)
|
|
|
|
with div():
|
|
|
|
if actual_hash == expected_hash:
|
|
|
|
p(
|
|
|
|
"This test succeeded on UI comparison.",
|
|
|
|
style="color: green; font-weight: bold;",
|
|
|
|
)
|
2023-01-27 14:13:12 +00:00
|
|
|
elif expected_hash is None:
|
|
|
|
p(
|
|
|
|
"This test is new and has no expected hash.",
|
|
|
|
style="color: blue; font-weight: bold;",
|
|
|
|
)
|
2020-01-07 14:42:55 +00:00
|
|
|
else:
|
|
|
|
p(
|
|
|
|
"This test failed on UI comparison.",
|
|
|
|
style="color: red; font-weight: bold;",
|
|
|
|
)
|
2023-01-27 14:13:12 +00:00
|
|
|
p("Expected: ", expected_hash or "(new test case)")
|
2020-01-07 14:42:55 +00:00
|
|
|
p("Actual: ", actual_hash)
|
|
|
|
hr()
|
|
|
|
|
|
|
|
|
2023-01-27 14:13:12 +00:00
|
|
|
def setup(main_runner: bool) -> None:
|
2022-08-15 16:30:45 +00:00
|
|
|
"""Delete and create the reports dir to clear previous entries."""
|
2023-01-27 14:13:12 +00:00
|
|
|
if main_runner:
|
|
|
|
shutil.rmtree(TESTREPORT_PATH, ignore_errors=True)
|
|
|
|
TESTREPORT_PATH.mkdir()
|
|
|
|
(TESTREPORT_PATH / "failed").mkdir()
|
|
|
|
(TESTREPORT_PATH / "passed").mkdir()
|
|
|
|
(TESTREPORT_PATH / "new").mkdir()
|
|
|
|
IMAGES_PATH.mkdir(parents=True)
|
|
|
|
|
|
|
|
html.set_image_dir(IMAGES_PATH)
|
2020-01-09 14:25:45 +00:00
|
|
|
|
|
|
|
|
2022-01-28 18:26:03 +00:00
|
|
|
def index() -> Path:
|
2022-08-15 16:30:45 +00:00
|
|
|
"""Generate index.html with all the test results - lists of failed and passed tests."""
|
2023-01-27 14:13:12 +00:00
|
|
|
passed_tests = list((TESTREPORT_PATH / "passed").iterdir())
|
|
|
|
failed_tests = list((TESTREPORT_PATH / "failed").iterdir())
|
|
|
|
new_tests = list((TESTREPORT_PATH / "new").iterdir())
|
|
|
|
|
|
|
|
actual_hashes = {
|
|
|
|
result.test.id: result.actual_hash for result in TestResult.recent_tests()
|
|
|
|
}
|
2020-01-10 08:32:04 +00:00
|
|
|
|
2020-01-09 14:25:45 +00:00
|
|
|
title = "UI Test report " + datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
2021-06-22 09:01:29 +00:00
|
|
|
doc = document(title=title, index=True)
|
2020-01-09 14:25:45 +00:00
|
|
|
|
|
|
|
with doc:
|
|
|
|
h1("UI Test report")
|
2020-01-09 14:37:15 +00:00
|
|
|
if not failed_tests:
|
2020-01-09 14:25:45 +00:00
|
|
|
p("All tests succeeded!", style="color: green; font-weight: bold;")
|
|
|
|
else:
|
|
|
|
p("Some tests failed!", style="color: red; font-weight: bold;")
|
|
|
|
hr()
|
|
|
|
|
|
|
|
h2("Failed", style="color: red;")
|
2021-06-22 09:01:29 +00:00
|
|
|
with p(id="file-hint"):
|
|
|
|
strong("Tip:")
|
|
|
|
text(" use ")
|
|
|
|
t.span("./tests/show_results.sh", style="font-family: monospace")
|
|
|
|
text(" to enable smart features.")
|
|
|
|
|
|
|
|
with div("Test colors", _class="script-hidden"):
|
|
|
|
with t.ul():
|
|
|
|
with t.li():
|
|
|
|
t.span("new", style="color: blue")
|
|
|
|
t.button("clear all", onclick="resetState('all')")
|
|
|
|
with t.li():
|
|
|
|
t.span("marked OK", style="color: grey")
|
|
|
|
t.button("clear", onclick="resetState('ok')")
|
|
|
|
with t.li():
|
|
|
|
t.span("marked BAD", style="color: darkred")
|
|
|
|
t.button("clear", onclick="resetState('bad')")
|
|
|
|
|
2023-01-27 14:13:12 +00:00
|
|
|
html.report_links(failed_tests, TESTREPORT_PATH, actual_hashes)
|
|
|
|
|
|
|
|
h2("New tests", style="color: blue;")
|
|
|
|
html.report_links(new_tests, TESTREPORT_PATH)
|
2020-01-09 14:25:45 +00:00
|
|
|
|
|
|
|
h2("Passed", style="color: green;")
|
2023-01-27 14:13:12 +00:00
|
|
|
html.report_links(passed_tests, TESTREPORT_PATH)
|
2020-01-09 14:25:45 +00:00
|
|
|
|
2023-01-27 14:13:12 +00:00
|
|
|
return html.write(TESTREPORT_PATH, doc, "index.html")
|
2020-01-09 14:25:45 +00:00
|
|
|
|
|
|
|
|
2023-01-27 14:13:12 +00:00
|
|
|
def all_screens() -> Path:
|
2022-08-15 16:30:45 +00:00
|
|
|
"""Generate an HTML file for all the screens from the current test run.
|
|
|
|
|
|
|
|
Shows all test-cases at one place.
|
|
|
|
"""
|
|
|
|
title = "All test cases"
|
|
|
|
doc = dominate.document(title=title)
|
|
|
|
|
|
|
|
with doc:
|
|
|
|
h1("All test cases")
|
|
|
|
hr()
|
|
|
|
|
|
|
|
count = 0
|
2023-01-27 14:13:12 +00:00
|
|
|
result_count = 0
|
|
|
|
for result in TestResult.recent_tests():
|
|
|
|
result_count += 1
|
|
|
|
h2(result.test.id, id=result.test.id)
|
|
|
|
for image in result.images:
|
2022-08-15 16:30:45 +00:00
|
|
|
# Including link to each image to see where else it occurs.
|
2023-01-27 14:13:12 +00:00
|
|
|
with a(href=f"{ALL_UNIQUE_SCREENS}#{image}"):
|
|
|
|
html.image_link(image, TESTREPORT_PATH)
|
2022-08-15 16:30:45 +00:00
|
|
|
count += 1
|
|
|
|
|
2023-01-27 14:13:12 +00:00
|
|
|
h2(f"{count} screens from {result_count} testcases.")
|
2022-08-15 16:30:45 +00:00
|
|
|
|
2023-01-27 14:13:12 +00:00
|
|
|
return html.write(TESTREPORT_PATH, doc, ALL_SCREENS)
|
2022-08-15 16:30:45 +00:00
|
|
|
|
|
|
|
|
2023-01-27 14:13:12 +00:00
|
|
|
def all_unique_screens() -> Path:
|
2022-08-15 16:30:45 +00:00
|
|
|
"""Generate an HTML file with all the unique screens from the current test run."""
|
2023-01-27 14:13:12 +00:00
|
|
|
results = TestResult.recent_tests()
|
|
|
|
result_count = 0
|
|
|
|
test_cases = defaultdict(list)
|
|
|
|
for result in results:
|
|
|
|
result_count += 1
|
|
|
|
for image in result.images:
|
|
|
|
test_cases[image].append(result.test.id)
|
|
|
|
|
|
|
|
test_case_pairs = sorted(test_cases.items(), key=lambda x: len(x[1]), reverse=True)
|
|
|
|
|
2022-08-15 16:30:45 +00:00
|
|
|
title = "All unique screens"
|
|
|
|
doc = dominate.document(title=title)
|
|
|
|
|
|
|
|
with doc:
|
|
|
|
h1("All unique screens")
|
|
|
|
hr()
|
|
|
|
|
2023-01-27 14:13:12 +00:00
|
|
|
for hash, tests in test_case_pairs:
|
|
|
|
# Adding link to the appropriate hash, where other testcases
|
|
|
|
# with the same hash (screen) are listed.
|
|
|
|
with a(href=f"#{hash}"):
|
|
|
|
with span(id="l-" + hash):
|
|
|
|
html.image_link(
|
|
|
|
hash, TESTREPORT_PATH, title=f"{len(tests)} testcases)"
|
|
|
|
)
|
2022-08-15 16:30:45 +00:00
|
|
|
|
|
|
|
# Adding all screen hashes together with links to testcases having these screens.
|
2023-01-27 14:13:12 +00:00
|
|
|
for hash, tests in test_case_pairs:
|
|
|
|
h2(hash)
|
|
|
|
with div(id=hash):
|
|
|
|
with a(href=f"#l-{hash}"):
|
|
|
|
html.image_link(hash, TESTREPORT_PATH)
|
|
|
|
for case in tests:
|
2022-08-15 16:30:45 +00:00
|
|
|
# Adding link to each test-case
|
2023-01-27 14:13:12 +00:00
|
|
|
with a(href=f"{ALL_SCREENS}#{case}"):
|
|
|
|
p(case)
|
2022-08-15 16:30:45 +00:00
|
|
|
|
2023-01-27 14:13:12 +00:00
|
|
|
h2(f"{len(test_case_pairs)} unique screens from {result_count} testcases.")
|
2022-08-15 16:30:45 +00:00
|
|
|
|
2023-01-27 14:13:12 +00:00
|
|
|
return html.write(TESTREPORT_PATH, doc, ALL_UNIQUE_SCREENS)
|
2022-08-15 16:30:45 +00:00
|
|
|
|
|
|
|
|
|
|
|
def generate_reports() -> None:
|
|
|
|
"""Generate HTML reports for the test."""
|
2023-01-27 14:13:12 +00:00
|
|
|
html.set_image_dir(IMAGES_PATH)
|
2022-08-15 16:30:45 +00:00
|
|
|
index()
|
2023-01-27 14:13:12 +00:00
|
|
|
all_screens()
|
|
|
|
all_unique_screens()
|
2022-08-15 16:30:45 +00:00
|
|
|
|
|
|
|
|
2023-01-27 14:13:12 +00:00
|
|
|
def _copy_deduplicated(test: TestCase) -> None:
|
|
|
|
"""Copy the actual screenshots to the deduplicated dir."""
|
|
|
|
html.store_images(*test.actual_screens)
|
|
|
|
html.store_images(*test.recorded_screens)
|
2022-08-15 16:30:45 +00:00
|
|
|
|
|
|
|
|
2023-01-27 14:13:12 +00:00
|
|
|
def failed(result: TestResult) -> Path:
|
2022-08-15 16:30:45 +00:00
|
|
|
"""Generate an HTML file for a failed test-case.
|
|
|
|
|
|
|
|
Compares the actual screenshots to the expected ones.
|
|
|
|
"""
|
2020-02-18 09:20:02 +00:00
|
|
|
download_failed = False
|
|
|
|
|
2023-01-27 14:13:12 +00:00
|
|
|
if not result.test.recorded_dir.exists():
|
|
|
|
result.test.recorded_dir.mkdir()
|
|
|
|
|
|
|
|
if result.expected_hash:
|
|
|
|
try:
|
|
|
|
download.fetch_recorded(result.expected_hash, result.test.recorded_dir)
|
|
|
|
except Exception:
|
|
|
|
download_failed = True
|
2020-01-07 14:42:55 +00:00
|
|
|
|
2023-01-27 14:13:12 +00:00
|
|
|
_copy_deduplicated(result.test)
|
2020-01-07 14:42:55 +00:00
|
|
|
|
2023-01-27 14:13:12 +00:00
|
|
|
doc = document(title=result.test.id, actual_hash=result.actual_hash)
|
2020-01-07 14:42:55 +00:00
|
|
|
with doc:
|
2023-01-27 14:13:12 +00:00
|
|
|
_header(result.test.id, result.expected_hash, result.actual_hash)
|
2020-01-07 14:42:55 +00:00
|
|
|
|
2021-06-22 09:01:29 +00:00
|
|
|
with div(id="markbox", _class="script-hidden"):
|
|
|
|
p("Click a button to mark the test result as:")
|
|
|
|
with div(id="buttons"):
|
|
|
|
t.button("OK", id="mark-ok", onclick="markState('ok')")
|
2022-12-12 15:05:47 +00:00
|
|
|
t.button("OK & UPDATE", id="mark-update", onclick="markState('update')")
|
2021-06-22 09:01:29 +00:00
|
|
|
t.button("BAD", id="mark-bad", onclick="markState('bad')")
|
|
|
|
|
2020-02-18 09:20:02 +00:00
|
|
|
if download_failed:
|
|
|
|
with p():
|
|
|
|
strong("WARNING:")
|
|
|
|
text(" failed to download recorded fixtures. Is this a new test case?")
|
|
|
|
|
2020-01-07 14:42:55 +00:00
|
|
|
with table(border=1, width=600):
|
|
|
|
with tr():
|
|
|
|
th("Expected")
|
|
|
|
th("Actual")
|
|
|
|
|
2023-01-27 14:13:12 +00:00
|
|
|
html.diff_table(result.diff_lines(), TESTREPORT_PATH / "failed")
|
2020-01-07 14:42:55 +00:00
|
|
|
|
2023-01-27 14:13:12 +00:00
|
|
|
return html.write(TESTREPORT_PATH / "failed", doc, result.test.id + ".html")
|
2020-01-07 14:42:55 +00:00
|
|
|
|
|
|
|
|
2023-01-27 14:13:12 +00:00
|
|
|
def passed(result: TestResult) -> Path:
|
2022-08-15 16:30:45 +00:00
|
|
|
"""Generate an HTML file for a passed test-case."""
|
2023-01-27 14:13:12 +00:00
|
|
|
return recorded(result, header="Passed")
|
|
|
|
|
2020-01-07 14:42:55 +00:00
|
|
|
|
2023-01-27 14:13:12 +00:00
|
|
|
def missing(result: TestResult) -> Path:
|
|
|
|
"""Generate an HTML file for a newly seen test-case."""
|
|
|
|
return recorded(result, header="New testcase")
|
2022-05-25 12:54:03 +00:00
|
|
|
|
|
|
|
|
2023-01-27 14:13:12 +00:00
|
|
|
def recorded(result: TestResult, header: str = "Recorded") -> Path:
|
2022-08-15 16:30:45 +00:00
|
|
|
"""Generate an HTML file for a passed test-case.
|
|
|
|
|
|
|
|
Shows all the screens from it in exact order.
|
|
|
|
"""
|
2023-01-27 14:13:12 +00:00
|
|
|
_copy_deduplicated(result.test)
|
|
|
|
|
|
|
|
doc = document(title=result.test.id)
|
2020-01-07 14:42:55 +00:00
|
|
|
|
|
|
|
with doc:
|
2023-01-27 14:13:12 +00:00
|
|
|
_header(result.test.id, result.actual_hash, result.actual_hash)
|
2020-01-07 14:42:55 +00:00
|
|
|
|
|
|
|
with table(border=1):
|
|
|
|
with tr():
|
2023-01-27 14:13:12 +00:00
|
|
|
th(header)
|
2020-01-07 14:42:55 +00:00
|
|
|
|
2023-01-27 14:13:12 +00:00
|
|
|
for screen in result.images:
|
2020-01-07 14:42:55 +00:00
|
|
|
with tr():
|
2023-01-27 14:13:12 +00:00
|
|
|
html.image_column(screen, TESTREPORT_PATH / "new")
|
2020-01-07 14:42:55 +00:00
|
|
|
|
2023-01-27 14:13:12 +00:00
|
|
|
return html.write(TESTREPORT_PATH / "new", doc, result.test.id + ".html")
|