Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package python-pytest-html for openSUSE:Factory checked in at 2023-09-04 22:53:31 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-pytest-html (Old) and /work/SRC/openSUSE:Factory/.python-pytest-html.new.1766 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-pytest-html" Mon Sep 4 22:53:31 2023 rev:15 rq:1108827 version:4.0.0 Changes: -------- --- /work/SRC/openSUSE:Factory/python-pytest-html/python-pytest-html.changes 2023-08-08 15:54:56.400995389 +0200 +++ /work/SRC/openSUSE:Factory/.python-pytest-html.new.1766/python-pytest-html.changes 2023-09-04 22:54:08.344948559 +0200 @@ -1,0 +2,83 @@ +Mon Sep 4 09:56:29 UTC 2023 - Daniel Garcia <[email protected]> + +- Refresh patches and node_modules.tar.gz +- Update to 4.0.0: + * Feat: Add duration format hook (#724) @BeyondEvil + * Chore: Drop support for python 3.7 (#723) @BeyondEvil + * Add expander to log output (#721) @drRedflint + * Fix: Broken sorting for custom columns (#715) @BeyondEvil + * Chore: Stop running scheduled tests on forks (#720) @BeyondEvil + * Chore: Fix tox (#718) @BeyondEvil + * use max height instead of fixed height (#706) @drRedflint + * if only one item in gallery, remove navigation (#705) @drRedflint + * Chore: Support legacy pytest-metadata (#714) @BeyondEvil + * Feature: Untemplate table header (#713) @BeyondEvil + * Fix: Borken HTML in jinja template (#712) @BeyondEvil + * Feature: Update json-data-blob (#704) @BeyondEvil + * Fix: Collapsed state between redraws (#703) @BeyondEvil + * Feature: Only one collapsed state (#701) @BeyondEvil + * Chore: General JS cleanup (#700) @BeyondEvil + * Feature: Template test and duration summary (#698) @BeyondEvil + * Feature: Template result filters (#697) @BeyondEvil + * Feature: Template table header (#696) @BeyondEvil + * Fix: visible query param (#695) @BeyondEvil + * Fix: Handle legacy py html (#694) @BeyondEvil + * Fix: Environment table toggle bug (#693) @BeyondEvil + * Feature: Add initial sort column as ini (#692) @BeyondEvil + * Fix: Duration sorting (#691) @BeyondEvil + * Fix: Logging issues with teardown (#690) @BeyondEvil + * Chore: Simplify results table hooks (#688) @BeyondEvil + * Enable variable expansion for CSS addons. (#676) @BeyondEvil + * Fix: results table html hook (#669) @BeyondEvil + * fix for #671 - Sort icons inverted in next-gen branch (#672) @harmin-parra + * Docs: Update ReadTheDocs to v2 (#673) @BeyondEvil + * Feature: Add 'session' to results summary hook (#660) @BeyondEvil + * Chore: Fix npm building (#658) @BeyondEvil + * Feature: Add hide-able Environment Table (#638) @BeyondEvil + * Feature: Make entire row collapsible (#656) @BeyondEvil + * Chore: Disambiguate collapsed (#657) @BeyondEvil + * Chore: Assorted fixes around pytest entry points (#655) @BeyondEvil + * Chore: Add eslint (#651) @BeyondEvil + * Chore: Decouple ReportData (#650) @BeyondEvil + * Chore: Add npm build hooks (#649) @BeyondEvil + * Docs: Fix deprecations page title [skip ci] (#645) @BeyondEvil + * Fix: Renamed report-data class to avoid confusion (#642) @BeyondEvil + * Chore: Temporary imports for backwards compat (#643) @BeyondEvil + * Docs: Add Deprecations docs (#640) @BeyondEvil + * Fix: Support cells.pop() (#641) @BeyondEvil + * Fix: Order and layout of outcome summary (#629) @BeyondEvil + * Fix: Sorting of custom table columns (#634) @BeyondEvil + * Chore: Allow concurrency on default branch (#639) @BeyondEvil + * Fix: Initial sort and query param (#637) @BeyondEvil + * Fix: Add skip marker results to report (#636) @BeyondEvil + * Fix: Deprecate use of 'True' in render_collapsed (#635) @BeyondEvil + * Fix: Color E(xecption) lines in the log red (#631) @BeyondEvil + * Fix: Handle appends on table hooks (#630) @BeyondEvil + * Fix: Handle assignment on table hooks (#628) @BeyondEvil + * Docs: Update contrib docs (#627) @BeyondEvil + * Fix issue with report.extra attribute (#626) @BeyondEvil + * chore: It's , 120 is fine (#625) @BeyondEvil + * Next gen (#621) @BeyondEvil + * chore: Migrate from Poetry to Hatch (#617) @BeyondEvil + * docs: Update to current (#616) @BeyondEvil + * fix: Broken sorting due to typo in jinja template (#614) @BeyondEvil + * fix: Use the same duration formatting as for the tests (#613) @BeyondEvil + * fix: Replacing log HTML (#611) @BeyondEvil + * fix: Incorrect precedence render collapsed (#610) @BeyondEvil + * chore: Better directory and class structure (#609) @BeyondEvil + * fix: Deprecate the Cells.pop function (#608) @BeyondEvil + * fix: Collapsed should support All and none (#605) @BeyondEvil + * tests: Add tests for stdout and sterr capture (#604) @BeyondEvil + * fix: Missing logging in report (#603) @BeyondEvil + * chore: Add code coverage for JS (#600) @BeyondEvil + * Fix: Table row hook (#599) @BeyondEvil + * fix: Report fails to render with pytest-xdist (#598) @BeyondEvil + * fix: Add config to report object (#588) @BeyondEvil + * update: duration_format renders deprecation warning (#589) @BeyondEvil + * chore: Add unit test file (#590) @BeyondEvil + * refactor: stop overwriting pytest data (#597) @BeyondEvil + * Combined fe and be (#479) @BeyondEvil + * Revert "Rename master branch to main" (#562) @BeyondEvil + * Switch to setuptools-scm >= 7.0.0 (#567) @dvzrv + +------------------------------------------------------------------- Old: ---- pytest_html-4.0.0rc5.tar.gz New: ---- pytest_html-4.0.0.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-pytest-html.spec ++++++ --- /var/tmp/diff_new_pack.8Y1ldV/_old 2023-09-04 22:54:09.833001159 +0200 +++ /var/tmp/diff_new_pack.8Y1ldV/_new 2023-09-04 22:54:09.833001159 +0200 @@ -18,7 +18,7 @@ %{?sle15_python_module_pythons} Name: python-pytest-html -Version: 4.0.0rc5 +Version: 4.0.0 Release: 0 Summary: Pytest plugin for generating HTML reports License: MPL-2.0 ++++++ drop-assertpy-dep.patch ++++++ --- /var/tmp/diff_new_pack.8Y1ldV/_old 2023-09-04 22:54:09.861002149 +0200 +++ /var/tmp/diff_new_pack.8Y1ldV/_new 2023-09-04 22:54:09.861002149 +0200 @@ -1,7 +1,7 @@ -Index: pytest_html-4.0.0rc5/testing/test_e2e.py +Index: pytest_html-4.0.0/testing/test_e2e.py =================================================================== ---- pytest_html-4.0.0rc5.orig/testing/test_e2e.py -+++ pytest_html-4.0.0rc5/testing/test_e2e.py +--- pytest_html-4.0.0.orig/testing/test_e2e.py ++++ pytest_html-4.0.0/testing/test_e2e.py @@ -5,7 +5,6 @@ import urllib.parse import pytest @@ -10,7 +10,7 @@ from selenium import webdriver from selenium.webdriver.common.by import By from selenium.webdriver.support.wait import WebDriverWait -@@ -67,7 +66,7 @@ def test_visible(pytester, path, driver) +@@ -84,7 +83,7 @@ def test_visible(pytester, path, driver) ec.visibility_of_all_elements_located((By.CSS_SELECTOR, "#results-table")) ) result = driver.find_elements(By.CSS_SELECTOR, "tr.collapsible") @@ -19,16 +19,47 @@ query_params = _encode_query_params({"visible": ""}) driver.get(f"file:///reports{path()}?{query_params}") -@@ -75,4 +74,4 @@ def test_visible(pytester, path, driver) +@@ -92,7 +91,7 @@ def test_visible(pytester, path, driver) ec.visibility_of_all_elements_located((By.CSS_SELECTOR, "#results-table")) ) result = driver.find_elements(By.CSS_SELECTOR, "tr.collapsible") - assert_that(result).is_length(0) + assert len(result) == 0 -Index: pytest_html-4.0.0rc5/testing/test_integration.py + + + def test_custom_sorting(pytester, path, driver): +@@ -121,17 +120,17 @@ def test_custom_sorting(pytester, path, + ) + + rows = _parse_result_table(driver) +- assert_that(rows).is_length(2) +- assert_that(rows[0]["test"]).contains("AAA") +- assert_that(rows[0]["alpha"]).is_equal_to("AAA") +- assert_that(rows[1]["test"]).contains("BBB") +- assert_that(rows[1]["alpha"]).is_equal_to("BBB") ++ assert len(rows) == 2 ++ assert "AAA" in rows[0]["test"] ++ assert "AAA" == rows[0]["alpha"] ++ assert "BBB" in rows[1]["test"] ++ assert "BBB" == rows[1]["alpha"] + + driver.find_element(By.CSS_SELECTOR, "th[data-column-type='alpha']").click() + # we might need some wait here to ensure sorting happened + rows = _parse_result_table(driver) +- assert_that(rows).is_length(2) +- assert_that(rows[0]["test"]).contains("BBB") +- assert_that(rows[0]["alpha"]).is_equal_to("BBB") +- assert_that(rows[1]["test"]).contains("AAA") +- assert_that(rows[1]["alpha"]).is_equal_to("AAA") ++ assert len(rows) == 2 ++ assert "BBB" in rows[0]["test"] ++ assert "BBB" == rows[0]["alpha"] ++ assert "AAA" in rows[1]["test"] ++ assert "AAA" == rows[1]["alpha"] +Index: pytest_html-4.0.0/testing/test_integration.py =================================================================== ---- pytest_html-4.0.0rc5.orig/testing/test_integration.py -+++ pytest_html-4.0.0rc5/testing/test_integration.py +--- pytest_html-4.0.0.orig/testing/test_integration.py ++++ pytest_html-4.0.0/testing/test_integration.py @@ -9,7 +9,6 @@ from base64 import b64encode from pathlib import Path @@ -37,7 +68,7 @@ from bs4 import BeautifulSoup from selenium import webdriver -@@ -76,7 +75,7 @@ def assert_results( +@@ -82,7 +81,7 @@ def assert_results( if isinstance(number, int): number_of_tests += number result = get_text(page, f"span[class={outcome}]") @@ -46,7 +77,7 @@ def get_element(page, selector): -@@ -142,20 +141,18 @@ class TestHTML: +@@ -148,13 +147,11 @@ class TestHTML: duration = get_text(page, "#results-table td[class='col-duration']") total_duration = get_text(page, "p[class='run-count']") if pause < 1: @@ -62,6 +93,15 @@ + assert re.match(expectation, duration) + assert re.match(r"\d{2}:\d{2}:\d{2}", total_duration) + def test_duration_format_hook(self, pytester): + pytester.makeconftest( +@@ -169,14 +166,14 @@ class TestHTML: + assert_results(page, passed=1) + + duration = get_text(page, "#results-table td[class='col-duration']") +- assert_that(duration).contains("seconds") ++ assert "seconds" in duration + def test_total_number_of_tests_zero(self, pytester): page = run(pytester) assert_results(page) @@ -72,7 +112,7 @@ def test_total_number_of_tests_singular(self, pytester): pytester.makepyfile("def test_pass(): pass") -@@ -163,7 +160,7 @@ class TestHTML: +@@ -184,7 +181,7 @@ class TestHTML: assert_results(page, passed=1) total = get_text(page, "p[class='run-count']") @@ -81,7 +121,7 @@ def test_total_number_of_tests_plural(self, pytester): pytester.makepyfile( -@@ -176,7 +173,7 @@ class TestHTML: +@@ -197,7 +194,7 @@ class TestHTML: assert_results(page, passed=2) total = get_text(page, "p[class='run-count']") @@ -90,28 +130,28 @@ def test_pass(self, pytester): pytester.makepyfile("def test_pass(): pass") -@@ -196,7 +193,7 @@ class TestHTML: +@@ -217,7 +214,7 @@ class TestHTML: assert_results(page, skipped=1, total_tests=0) - log = get_text(page, ".summary div[class='log']") + log = get_text(page, "div[class='log']") - assert_that(log).contains(reason) + assert reason in log def test_skip_function_marker(self, pytester): reason = str(random.random()) -@@ -212,7 +209,7 @@ class TestHTML: +@@ -233,7 +230,7 @@ class TestHTML: assert_results(page, skipped=1, total_tests=0) - log = get_text(page, ".summary div[class='log']") + log = get_text(page, "div[class='log']") - assert_that(log).contains(reason) + assert reason in log def test_skip_class_marker(self, pytester): reason = str(random.random()) -@@ -229,16 +226,14 @@ class TestHTML: +@@ -250,16 +247,14 @@ class TestHTML: assert_results(page, skipped=1, total_tests=0) - log = get_text(page, ".summary div[class='log']") + log = get_text(page, "div[class='log']") - assert_that(log).contains(reason) + assert reason in log @@ -120,15 +160,15 @@ page = run(pytester) assert_results(page, failed=1) - assert_that(get_log(page)).contains("AssertionError") -- assert_that(get_text(page, ".summary div[class='log'] span.error")).matches( +- assert_that(get_text(page, "div[class='log'] span.error")).matches( - r"^E\s+assert False$" - ) + assert "AssertionError" in get_log(page) -+ assert re.match(r"^E\s+assert False$", get_text(page, ".summary div[class='log'] span.error")) ++ assert re.match(r"^E\s+assert False$", get_text(page, "div[class='log'] span.error")) def test_xfail(self, pytester): reason = str(random.random()) -@@ -251,7 +246,7 @@ class TestHTML: +@@ -272,7 +267,7 @@ class TestHTML: ) page = run(pytester) assert_results(page, xfailed=1) @@ -137,7 +177,7 @@ def test_xfail_function_marker(self, pytester): reason = str(random.random()) -@@ -265,7 +260,7 @@ class TestHTML: +@@ -286,7 +281,7 @@ class TestHTML: ) page = run(pytester) assert_results(page, xfailed=1) @@ -146,18 +186,18 @@ def test_xfail_class_marker(self, pytester): pytester.makepyfile( -@@ -353,8 +348,8 @@ class TestHTML: +@@ -374,8 +369,8 @@ class TestHTML: assert_results(page, error=1, total_tests=0) - col_name = get_text(page, ".summary td[class='col-name']") + col_name = get_text(page, "td[class='col-testId']") - assert_that(col_name).contains("::setup") - assert_that(get_log(page)).contains("ValueError") -+ assert "::setup" in col_name ++ asswert "::setup" in col_name + assert "ValueError" in get_log(page) @pytest.mark.parametrize("title", ["", "Special Report"]) def test_report_title(self, pytester, title): -@@ -371,8 +366,8 @@ class TestHTML: +@@ -392,8 +387,8 @@ class TestHTML: expected_title = title if title else "report.html" page = run(pytester) @@ -168,7 +208,7 @@ def test_resources_inline_css(self, pytester): pytester.makepyfile("def test_pass(): pass") -@@ -380,15 +375,13 @@ class TestHTML: +@@ -401,15 +396,13 @@ class TestHTML: content = file_content() @@ -186,10 +226,10 @@ def test_custom_content_in_summary(self, pytester): content = { -@@ -412,11 +405,11 @@ class TestHTML: - page = run(pytester) - - elements = page.select(".summary__data p:not(.run-count):not(.filter)") +@@ -435,11 +428,11 @@ class TestHTML: + elements = page.select( + ".additional-summary p" + ) # ".summary__data p:not(.run-count):not(.filter)") - assert_that(elements).is_length(3) + assert len(elements) == 3 for element in elements: @@ -200,73 +240,73 @@ def test_extra_html(self, pytester): content = str(random.random()) -@@ -437,7 +430,7 @@ class TestHTML: +@@ -460,7 +453,7 @@ class TestHTML: pytester.makepyfile("def test_pass(): pass") page = run(pytester) -- assert_that(page.select_one(".summary .extraHTML").string).is_equal_to(content) -+ assert content == page.select_one(".summary .extraHTML").string +- assert_that(page.select_one(".extraHTML").string).is_equal_to(content) ++ assert content == page.select_one(".extraHTML").string @pytest.mark.parametrize( "content, encoded", -@@ -461,10 +454,8 @@ class TestHTML: +@@ -484,10 +477,8 @@ class TestHTML: page = run(pytester, cmd_flags=["--self-contained-html"]) - element = page.select_one(".summary a[class='col-links__extra text']") + element = page.select_one("a[class='col-links__extra text']") - assert_that(element.string).is_equal_to("Text") - assert_that(element["href"]).is_equal_to( - f"data:text/plain;charset=utf-8;base64,{encoded}" - ) + assert "Text" == element.string -+ assert element["href"] == f"data:text/plain;charset=utf-8;base64,{encoded}" ++ assert f"data:text/plain;charset=utf-8;base64,{encoded}" == element["href"] def test_extra_json(self, pytester): content = {str(random.random()): str(random.random())} -@@ -489,10 +480,8 @@ class TestHTML: +@@ -512,10 +503,8 @@ class TestHTML: data = b64encode(content_str.encode("utf-8")).decode("ascii") - element = page.select_one(".summary a[class='col-links__extra json']") + element = page.select_one("a[class='col-links__extra json']") - assert_that(element.string).is_equal_to("JSON") - assert_that(element["href"]).is_equal_to( - f"data:application/json;charset=utf-8;base64,{data}" - ) + assert "JSON" == element.string -+ assert element["href"] == f"data:application/json;charset=utf-8;base64,{data}" ++ assert f"data:application/json;charset=utf-8;base64,{data}" == element["href"] def test_extra_url(self, pytester): content = str(random.random()) -@@ -513,8 +502,8 @@ class TestHTML: +@@ -536,8 +525,8 @@ class TestHTML: page = run(pytester) - element = page.select_one(".summary a[class='col-links__extra url']") + element = page.select_one("a[class='col-links__extra url']") - assert_that(element.string).is_equal_to("URL") - assert_that(element["href"]).is_equal_to(content) + assert "URL" == element.string -+ assert element["href"] == content ++ assert content == element["href"] @pytest.mark.parametrize( "mime_type, extension", -@@ -552,7 +541,7 @@ class TestHTML: +@@ -575,7 +564,7 @@ class TestHTML: # assert_that(element["href"]).is_equal_to(src) - element = page.select_one(".summary .media img") + element = page.select_one(".media img") - assert_that(str(element)).is_equal_to(f'<img src="{src}"/>') -+ assert str(element) == f'<img src="{src}"/>' ++ assert f'<img src="{src}"/>' == str(element) @pytest.mark.parametrize("mime_type, extension", [("video/mp4", "mp4")]) def test_extra_video(self, pytester, mime_type, extension): -@@ -580,9 +569,7 @@ class TestHTML: +@@ -603,9 +592,7 @@ class TestHTML: # assert_that(element["href"]).is_equal_to(src) - element = page.select_one(".summary .media video") + element = page.select_one(".media video") - assert_that(str(element)).is_equal_to( - f'<video controls="">\n<source src="{src}" type="{mime_type}"/>\n</video>' - ) -+ assert str(element) == f'<video controls="">\n<source src="{src}" type="{mime_type}"/>\n</video>' ++ assert f'<video controls="">\n<source src="{src}" type="{mime_type}"/>\n</video>' == str(element) def test_xdist(self, pytester): pytester.makepyfile("def test_xdist(): pass") -@@ -613,19 +600,10 @@ class TestHTML: +@@ -634,19 +621,10 @@ class TestHTML: description_index = 5 time_index = 6 @@ -289,8 +329,8 @@ + assert "A description" == get_text(page, row_selector.format(description_index)) def test_results_table_hook_insert(self, pytester): - header_selector = ( -@@ -652,19 +630,10 @@ class TestHTML: + header_selector = "#results-table-head tr:nth-child(1) th:nth-child({})" +@@ -671,19 +649,10 @@ class TestHTML: description_index = 4 time_index = 2 @@ -314,22 +354,20 @@ def test_results_table_hook_delete(self, pytester): pytester.makeconftest( -@@ -701,12 +670,12 @@ class TestHTML: +@@ -720,10 +689,10 @@ class TestHTML: page = run(pytester) - header_columns = page.select(".summary #results-table-head th") + header_columns = page.select("#results-table-head th") - assert_that(header_columns).is_length(3) + assert len(header_columns) == 3 - row_columns = page.select_one(".summary .results-table-row").select( - "td:not(.extra)" - ) + row_columns = page.select_one(".results-table-row").select("td:not(.extra)") - assert_that(row_columns).is_length(3) + assert len(row_columns) == 3 @pytest.mark.parametrize("no_capture", ["", "-s"]) def test_standard_streams(self, pytester, no_capture): -@@ -735,11 +704,11 @@ class TestHTML: +@@ -752,11 +721,11 @@ class TestHTML: for when in ["setup", "call", "teardown"]: for stream in ["stdout", "stderr"]: if no_capture: @@ -345,7 +383,7 @@ class TestLogCapturing: -@@ -787,7 +756,7 @@ class TestLogCapturing: +@@ -804,7 +773,7 @@ class TestLogCapturing: log = get_log(page) for when in ["setup", "test", "teardown"]: @@ -354,7 +392,7 @@ @pytest.mark.usefixtures("log_cli") def test_setup_error(self, test_file, pytester): -@@ -796,9 +765,9 @@ class TestLogCapturing: +@@ -813,9 +782,9 @@ class TestLogCapturing: assert_results(page, error=1) log = get_log(page) @@ -367,7 +405,7 @@ @pytest.mark.usefixtures("log_cli") def test_test_fails(self, test_file, pytester): -@@ -808,7 +777,7 @@ class TestLogCapturing: +@@ -825,7 +794,7 @@ class TestLogCapturing: log = get_log(page) for when in ["setup", "test", "teardown"]: @@ -376,7 +414,7 @@ @pytest.mark.usefixtures("log_cli") @pytest.mark.parametrize( -@@ -822,7 +791,7 @@ class TestLogCapturing: +@@ -839,7 +808,7 @@ class TestLogCapturing: for test_name in ["test_logging", "test_logging::teardown"]: log = get_log(page, test_name) for when in ["setup", "test", "teardown"]: @@ -385,7 +423,7 @@ def test_no_log(self, test_file, pytester): pytester.makepyfile(test_file(assertion=True)) -@@ -830,9 +799,9 @@ class TestLogCapturing: +@@ -847,9 +816,9 @@ class TestLogCapturing: assert_results(page, passed=1) log = get_log(page, "test_logging") @@ -397,7 +435,7 @@ @pytest.mark.usefixtures("log_cli") def test_rerun(self, test_file, pytester): -@@ -843,8 +812,8 @@ class TestLogCapturing: +@@ -860,8 +829,8 @@ class TestLogCapturing: assert_results(page, failed=1, rerun=2) log = get_log(page) @@ -408,7 +446,7 @@ class TestCollapsedQueryParam: -@@ -871,9 +840,9 @@ class TestCollapsedQueryParam: +@@ -888,9 +857,9 @@ class TestCollapsedQueryParam: page = run(pytester) assert_results(page, passed=1, failed=1, error=1) @@ -421,7 +459,7 @@ @pytest.mark.parametrize("param", ["failed,error", "FAILED,eRRoR"]) def test_specified(self, pytester, test_file, param): -@@ -881,9 +850,9 @@ class TestCollapsedQueryParam: +@@ -898,9 +867,9 @@ class TestCollapsedQueryParam: page = run(pytester, query_params={"collapsed": param}) assert_results(page, passed=1, failed=1, error=1) @@ -434,7 +472,7 @@ def test_all(self, pytester, test_file): pytester.makepyfile(test_file) -@@ -891,7 +860,7 @@ class TestCollapsedQueryParam: +@@ -908,7 +877,7 @@ class TestCollapsedQueryParam: assert_results(page, passed=1, failed=1, error=1) for test_name in ["test_pass", "test_fail", "test_error::setup"]: @@ -443,7 +481,7 @@ @pytest.mark.parametrize("param", ["", 'collapsed=""', "collapsed=''"]) def test_falsy(self, pytester, test_file, param): -@@ -899,9 +868,9 @@ class TestCollapsedQueryParam: +@@ -916,9 +885,9 @@ class TestCollapsedQueryParam: page = run(pytester, query_params={"collapsed": param}) assert_results(page, passed=1, failed=1, error=1) @@ -456,7 +494,7 @@ @pytest.mark.parametrize("param", ["failed,error", "FAILED,eRRoR"]) def test_render_collapsed(self, pytester, test_file, param): -@@ -915,9 +884,9 @@ class TestCollapsedQueryParam: +@@ -932,9 +901,9 @@ class TestCollapsedQueryParam: page = run(pytester) assert_results(page, passed=1, failed=1, error=1) @@ -469,7 +507,7 @@ def test_render_collapsed_precedence(self, pytester, test_file): pytester.makeini( -@@ -934,7 +903,7 @@ class TestCollapsedQueryParam: +@@ -951,7 +920,7 @@ class TestCollapsedQueryParam: page = run(pytester, query_params={"collapsed": "skipped"}) assert_results(page, passed=1, failed=1, error=1, skipped=1) @@ -481,10 +519,10 @@ + assert not is_collapsed(page, "test_fail") + assert not is_collapsed(page, "test_error::setup") + assert is_collapsed(page, "test_skip") -Index: pytest_html-4.0.0rc5/testing/test_unit.py +Index: pytest_html-4.0.0/testing/test_unit.py =================================================================== ---- pytest_html-4.0.0rc5.orig/testing/test_unit.py -+++ pytest_html-4.0.0rc5/testing/test_unit.py +--- pytest_html-4.0.0.orig/testing/test_unit.py ++++ pytest_html-4.0.0/testing/test_unit.py @@ -4,7 +4,6 @@ import sys import pkg_resources ++++++ node_modules.tar.gz ++++++ /work/SRC/openSUSE:Factory/python-pytest-html/node_modules.tar.gz /work/SRC/openSUSE:Factory/.python-pytest-html.new.1766/node_modules.tar.gz differ: char 12, line 1 ++++++ pytest_html-4.0.0rc5.tar.gz -> pytest_html-4.0.0.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pytest_html-4.0.0rc5/PKG-INFO new/pytest_html-4.0.0/PKG-INFO --- old/pytest_html-4.0.0rc5/PKG-INFO 2023-07-28 17:28:57.000000000 +0200 +++ new/pytest_html-4.0.0/PKG-INFO 2023-09-01 20:48:44.000000000 +0200 @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: pytest-html -Version: 4.0.0rc5 +Version: 4.0.0 Summary: pytest plugin for generating HTML reports Project-URL: Homepage, https://github.com/pytest-dev/pytest-html Project-URL: Tracker, https://github.com/pytest-dev/pytest-html/issues @@ -17,7 +17,6 @@ Classifier: Operating System :: MacOS :: MacOS X Classifier: Operating System :: Microsoft :: Windows Classifier: Operating System :: POSIX -Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 @@ -27,9 +26,9 @@ Classifier: Topic :: Software Development :: Quality Assurance Classifier: Topic :: Software Development :: Testing Classifier: Topic :: Utilities -Requires-Python: >=3.7 +Requires-Python: >=3.8 Requires-Dist: jinja2>=3.0.0 -Requires-Dist: pytest-metadata>=3.0.0 +Requires-Dist: pytest-metadata>=2.0.0 Requires-Dist: pytest>=7.0.0 Provides-Extra: docs Requires-Dist: pip-tools>=6.13.0; extra == 'docs' diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pytest_html-4.0.0rc5/docs/changelog.rst new/pytest_html-4.0.0/docs/changelog.rst --- old/pytest_html-4.0.0rc5/docs/changelog.rst 2023-07-28 17:28:57.000000000 +0200 +++ new/pytest_html-4.0.0/docs/changelog.rst 2023-09-01 20:48:44.000000000 +0200 @@ -6,6 +6,22 @@ Version History --------------- +4.0.0 (2023-09-01) +~~~~~~~~~~~~~~~~~~ + +This release is the result of more than two years of rewrites. + +We've tried our best to keep this release backwards-compatible with v3. + +If you find something that seems to be a regression, please consult the documentation first, +before filing an issue. + +Thanks to all the users who have contributed with ideas, solutions and beta-testing. +You're too many to name, but you know who you are. + +A special thanks to `@drRedflint <https://github.com/drRedflint>`_ and `@jeffwright13 <https://github.com/jeffwright13>`_ +for all the javascript and testing respectively. + 3.2.0 (2022-10-25) ~~~~~~~~~~~~~~~~~~ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pytest_html-4.0.0rc5/docs/user_guide.rst new/pytest_html-4.0.0/docs/user_guide.rst --- old/pytest_html-4.0.0rc5/docs/user_guide.rst 2023-07-28 17:28:57.000000000 +0200 +++ new/pytest_html-4.0.0/docs/user_guide.rst 2023-09-01 20:48:44.000000000 +0200 @@ -304,6 +304,28 @@ If tests are run in parallel (with `pytest-xdist`_ for example), then the order may not be in the correct order. +Formatting the Duration Column +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The formatting of the timestamp used in the :code:`Durations` column can be modified by using the +:code:`pytest_html_duration_format` hook. The default timestamp will be `nnn ms` for durations +less than one second and `hh:mm:ss` for durations equal to or greater than one second. + +Below is an example of a :code:`conftest.py` file setting :code:`pytest_html_duration_format`: + +.. code-block:: python + + import datetime + + + def pytest_html_duration_format(duration): + duration_timedelta = datetime.timedelta(seconds=duration) + time = datetime.datetime(1, 1, 1) + duration_timedelta + return time.strftime("%H:%M:%S") + +**NOTE**: The behavior of sorting the duration column is not guaranteed when providing a custom format. + +**NOTE**: The formatting of the total duration is not affected by this hook. .. [email protected](tryfirst=True): https://docs.pytest.org/en/stable/writing_plugins.html#hook-function-ordering-call-example .. _ansi2html: https://pypi.python.org/pypi/ansi2html/ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pytest_html-4.0.0rc5/pyproject.toml new/pytest_html-4.0.0/pyproject.toml --- old/pytest_html-4.0.0rc5/pyproject.toml 2023-07-28 17:28:57.000000000 +0200 +++ new/pytest_html-4.0.0/pyproject.toml 2023-09-01 20:48:44.000000000 +0200 @@ -10,7 +10,7 @@ description = "pytest plugin for generating HTML reports" readme = "README.rst" license = "MPL-2.0" -requires-python = ">=3.7" +requires-python = ">=3.8" keywords = [ "pytest", "html", @@ -29,7 +29,6 @@ "Operating System :: POSIX", "Operating System :: Microsoft :: Windows", "Operating System :: MacOS :: MacOS X", - "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", @@ -42,7 +41,7 @@ ] dependencies = [ "pytest>=7.0.0", - "pytest-metadata>=3.0.0", + "pytest-metadata>=2.0.0", "Jinja2>=3.0.0", ] dynamic = [ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pytest_html-4.0.0rc5/src/layout/css/style.scss new/pytest_html-4.0.0/src/layout/css/style.scss --- old/pytest_html-4.0.0rc5/src/layout/css/style.scss 2023-07-28 17:28:57.000000000 +0200 +++ new/pytest_html-4.0.0/src/layout/css/style.scss 2023-09-01 20:48:44.000000000 +0200 @@ -123,22 +123,51 @@ $extra-height: 240px; $extra-media-width: 320px; -.log { - background-color: #e6e6e6; - border: $border-width solid #e6e6e6; - color: black; - display: block; - font-family: 'Courier New', Courier, monospace; - height: $extra-height - 2 * $spacing; - overflow-y: scroll; - padding: $spacing; - white-space: pre-wrap; - - &:only-child { - height: inherit; +.logwrapper { + max-height: $extra-height - 2 * $spacing; + overflow-y: scroll; + background-color: #e6e6e6; + &.expanded { + max-height: none; + .logexpander { + &:after { + content: 'collapse [-]'; + } + } + } + .logexpander { + z-index: 1; + position: sticky; + top: 10px; + width: max-content; + border: 1px solid; + border-radius: 3px; + padding: 5px 7px; + margin: 10px 0 10px calc(100% - 80px); + cursor: pointer; + background-color: #e6e6e6; + &:after { + content: 'expand [+]'; + } + &:hover { + color: #000; + border-color: #000; + } + } + .log { + min-height: 40px; + position: relative; + top: -50px; + height: calc(100% + 50px); + border: $border-width solid #e6e6e6; + color: black; + display: block; + font-family: 'Courier New', Courier, monospace; + padding: $spacing; + padding-right: 80px; + white-space: pre-wrap; } } - div.media { border: $border-width solid #e6e6e6; float: right; @@ -156,6 +185,9 @@ overflow: hidden; height: 200px; } +.media-container--fullscreen { + grid-template-columns: 0px auto 0px; +} .media-container__nav--right, .media-container__nav--left { text-align: center; @@ -197,6 +229,7 @@ } .col-result { + width: 130px; &:hover::after { content: ' (hide details)'; } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pytest_html-4.0.0rc5/src/pytest_html/__version.py new/pytest_html-4.0.0/src/pytest_html/__version.py --- old/pytest_html-4.0.0rc5/src/pytest_html/__version.py 2023-07-28 17:28:57.000000000 +0200 +++ new/pytest_html-4.0.0/src/pytest_html/__version.py 2023-09-01 20:48:44.000000000 +0200 @@ -1,4 +1,4 @@ # file generated by setuptools_scm # don't change, don't track in version control -__version__ = version = '4.0.0rc5' +__version__ = version = '4.0.0' __version_tuple__ = version_tuple = (4, 0, 0) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pytest_html-4.0.0rc5/src/pytest_html/basereport.py new/pytest_html-4.0.0/src/pytest_html/basereport.py --- old/pytest_html-4.0.0rc5/src/pytest_html/basereport.py 2023-07-28 17:28:57.000000000 +0200 +++ new/pytest_html-4.0.0/src/pytest_html/basereport.py 2023-09-01 20:48:44.000000000 +0200 @@ -10,7 +10,6 @@ from pathlib import Path import pytest -from pytest_metadata.plugin import metadata_key from pytest_html import __version__ from pytest_html import extras @@ -66,7 +65,18 @@ self._write_report(rendered_report) def _generate_environment(self): - metadata = self._config.stash[metadata_key] + try: + from pytest_metadata.plugin import metadata_key + + metadata = self._config.stash[metadata_key] + except ImportError: + # old version of pytest-metadata + metadata = self._config._metadata + warnings.warn( + "'pytest-metadata < 3.0.0' is deprecated and support will be dropped in next major version", + DeprecationWarning, + ) + for key in metadata.keys(): value = metadata[key] if self._is_redactable_environment_variable(key): @@ -139,6 +149,15 @@ return f"{counts}/{self._report.collected_items} {'tests' if plural else 'test'} done." + def _hydrate_data(self, data, cells): + for index, cell in enumerate(cells): + # extract column name and data if column is sortable + if "sortable" in self._report.table_header[index]: + name_match = re.search(r"col-(\w+)", cell) + data_match = re.search(r"<td.*?>(.*?)</td>", cell) + if name_match and data_match: + data[name_match.group(1)] = data_match.group(1) + @pytest.hookimpl(trylast=True) def pytest_sessionstart(self, session): self._report.set_data("environment", self._generate_environment()) @@ -178,40 +197,45 @@ def pytest_runtest_logreport(self, report): if hasattr(report, "duration_formatter"): warnings.warn( - "'duration_formatter' has been removed and no longer has any effect!", + "'duration_formatter' has been removed and no longer has any effect!" + "Please use the 'pytest_html_duration_format' hook instead.", DeprecationWarning, ) outcome = _process_outcome(report) - data = { - "result": outcome, - "duration": _format_duration(report.duration), - } + try: + # hook returns as list for some reason + duration = self._config.hook.pytest_html_duration_format( + duration=report.duration + )[0] + except IndexError: + duration = _format_duration(report.duration) self._report.total_duration += report.duration test_id = report.nodeid if report.when != "call": test_id += f"::{report.when}" - data["testId"] = test_id - data["extras"] = self._process_extras(report, test_id) + data = { + "extras": self._process_extras(report, test_id), + } links = [ extra for extra in data["extras"] if extra["format_type"] in ["json", "text", "url"] ] cells = [ - f'<td class="col-result">{data["result"]}</td>', - f'<td class="col-name">{data["testId"]}</td>', - f'<td class="col-duration">{data["duration"]}</td>', + f'<td class="col-result">{outcome}</td>', + f'<td class="col-testId">{test_id}</td>', + f'<td class="col-duration">{duration}</td>', f'<td class="col-links">{_process_links(links)}</td>', ] - self._config.hook.pytest_html_results_table_row(report=report, cells=cells) if not cells: return cells = _fix_py(cells) + self._hydrate_data(data, cells) data["resultsTableRow"] = cells # don't count passed setups and teardowns diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pytest_html-4.0.0rc5/src/pytest_html/hooks.py new/pytest_html-4.0.0/src/pytest_html/hooks.py --- old/pytest_html-4.0.0rc5/src/pytest_html/hooks.py 2023-07-28 17:28:57.000000000 +0200 +++ new/pytest_html-4.0.0/src/pytest_html/hooks.py 2023-09-01 20:48:44.000000000 +0200 @@ -21,3 +21,7 @@ def pytest_html_results_table_html(report, data): """Called after building results table additional HTML.""" + + +def pytest_html_duration_format(duration): + """Called before using the default duration formatting.""" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pytest_html-4.0.0rc5/src/pytest_html/report_data.py new/pytest_html-4.0.0/src/pytest_html/report_data.py --- old/pytest_html-4.0.0rc5/src/pytest_html/report_data.py 2023-07-28 17:28:57.000000000 +0200 +++ new/pytest_html-4.0.0/src/pytest_html/report_data.py 2023-09-01 20:48:44.000000000 +0200 @@ -41,7 +41,6 @@ self._data = { "environment": {}, "tests": defaultdict(list), - "resultsTableRow": None, } collapsed = config.getini("render_collapsed") diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pytest_html-4.0.0rc5/src/pytest_html/resources/app.js new/pytest_html-4.0.0/src/pytest_html/resources/app.js --- old/pytest_html-4.0.0rc5/src/pytest_html/resources/app.js 2023-07-28 17:28:57.000000000 +0200 +++ new/pytest_html-4.0.0/src/pytest_html/resources/app.js 2023-09-01 20:48:44.000000000 +0200 @@ -68,7 +68,6 @@ const mediaViewer = require('./mediaviewer.js') const templateEnvRow = document.getElementById('template_environment_row') const templateResult = document.getElementById('template_results-table__tbody') -const listHeaderEmpty = document.getElementById('template_results-table__head--empty') function htmlToElements(html) { const temp = document.createElement('template') @@ -104,11 +103,9 @@ return envRow }, - getListHeaderEmpty: () => listHeaderEmpty.content.cloneNode(true), - getResultTBody: ({ testId, id, log, duration, extras, resultsTableRow, tableHtml, result, collapsed }) => { - const resultLower = result.toLowerCase() + getResultTBody: ({ testId, id, log, extras, resultsTableRow, tableHtml, result, collapsed }) => { const resultBody = templateResult.content.cloneNode(true) - resultBody.querySelector('tbody').classList.add(resultLower) + resultBody.querySelector('tbody').classList.add(result.toLowerCase()) resultBody.querySelector('tbody').id = testId resultBody.querySelector('.collapsible').dataset.id = id @@ -214,7 +211,7 @@ init() },{"./datamanager.js":1,"./filter.js":3,"./main.js":5,"./sort.js":7}],5:[function(require,module,exports){ -const { dom, findAll } = require('./dom.js') +const { dom, find, findAll } = require('./dom.js') const { manager } = require('./datamanager.js') const { doSort } = require('./sort.js') const { doFilter } = require('./filter.js') @@ -244,52 +241,54 @@ renderEnvironmentTable() } +const addItemToggleListener = (elem) => { + elem.addEventListener('click', ({ target }) => { + const id = target.parentElement.dataset.id + manager.toggleCollapsedItem(id) + + const collapsedIds = getCollapsedIds() + if (collapsedIds.includes(id)) { + const updated = collapsedIds.filter((item) => item !== id) + setCollapsedIds(updated) + } else { + collapsedIds.push(id) + setCollapsedIds(collapsedIds) + } + redraw() + }) +} + const renderContent = (tests) => { const sortAttr = getSort(manager.initialSort) const sortAsc = JSON.parse(getSortDirection()) const rows = tests.map(dom.getResultTBody) const table = document.getElementById('results-table') - const tableHeader = document.getElementById('template_results-table__head').content.cloneNode(true) - - removeChildren(table) + const tableHeader = document.getElementById('results-table-head') - tableHeader.querySelector(`.sortable[data-column-type="${sortAttr}"]`)?.classList.add(sortAsc ? 'desc' : 'asc') - if (!rows.length) { - tableHeader.appendChild(dom.getListHeaderEmpty()) - } - table.appendChild(tableHeader) + const newTable = document.createElement('table') + newTable.id = 'results-table' - rows.forEach((row) => !!row && table.appendChild(row)) + // remove all sorting classes and set the relevant + findAll('.sortable', tableHeader).forEach((elem) => elem.classList.remove('asc', 'desc')) + tableHeader.querySelector(`.sortable[data-column-type="${sortAttr}"]`).classList.add(sortAsc ? 'desc' : 'asc') + newTable.appendChild(tableHeader) - table.querySelectorAll('.extra').forEach((item) => { - item.colSpan = document.querySelectorAll('th').length - }) - - findAll('.sortable').forEach((elem) => { - elem.addEventListener('click', (evt) => { - const { target: element } = evt - const { columnType } = element.dataset - doSort(columnType) - redraw() - }) - }) - - findAll('.collapsible td:not(.col-links').forEach((elem) => { - elem.addEventListener('click', ({ target }) => { - const id = target.parentElement.dataset.id - manager.toggleCollapsedItem(id) - - const collapsedIds = getCollapsedIds() - if (collapsedIds.includes(id)) { - const updated = collapsedIds.filter((item) => item !== id) - setCollapsedIds(updated) - } else { - collapsedIds.push(id) - setCollapsedIds(collapsedIds) + if (!rows.length) { + const emptyTable = document.getElementById('template_results-table__body--empty').content.cloneNode(true) + newTable.appendChild(emptyTable) + } else { + rows.forEach((row) => { + if (!!row) { + findAll('.collapsible td:not(.col-links', row).forEach(addItemToggleListener) + find('.logexpander', row).addEventListener('click', + (evt) => evt.target.parentNode.classList.toggle('expanded'), + ) + newTable.appendChild(row) } - redraw() }) - }) + } + + table.replaceWith(newTable) } const renderDerived = () => { @@ -327,6 +326,16 @@ findAll('input[name="filter_checkbox"]').forEach((elem) => { elem.addEventListener('click', filterColumn) }) + + findAll('.sortable').forEach((elem) => { + elem.addEventListener('click', (evt) => { + const { target: element } = evt + const { columnType } = element.dataset + doSort(columnType) + redraw() + }) + }) + document.getElementById('show_all_details').addEventListener('click', () => { manager.allCollapsed = false setCollapsedIds([]) @@ -387,6 +396,7 @@ } const mediaViewer = new MediaViewer(assets) + const container = resultBody.querySelector('.media-container') const leftArrow = resultBody.querySelector('.media-container__nav--left') const rightArrow = resultBody.querySelector('.media-container__nav--right') const mediaName = resultBody.querySelector('.media__name') @@ -424,9 +434,12 @@ const openImg = () => { window.open(mediaViewer.activeFile.path, '_blank') } - - leftArrow.addEventListener('click', moveLeft) - rightArrow.addEventListener('click', doRight) + if (assets.length === 1) { + container.classList.add('media-container--fullscreen') + } else { + leftArrow.addEventListener('click', moveLeft) + rightArrow.addEventListener('click', doRight) + } imageEl.addEventListener('click', openImg) } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pytest_html-4.0.0rc5/src/pytest_html/resources/index.jinja2 new/pytest_html-4.0.0/src/pytest_html/resources/index.jinja2 --- old/pytest_html-4.0.0rc5/src/pytest_html/resources/index.jinja2 2023-07-28 17:28:57.000000000 +0200 +++ new/pytest_html-4.0.0/src/pytest_html/resources/index.jinja2 2023-09-01 20:48:44.000000000 +0200 @@ -26,12 +26,18 @@ <td></td> </tr> </template> + <template id="template_results-table__body--empty"> + <tbody class="results-table-row"> + <tr id="not-found-message"> + <td colspan="{{ table_head|length }}">No results found. Check the filters.</th> + </tr> + </template> <template id="template_results-table__tbody"> <tbody class="results-table-row"> <tr class="collapsible"> </tr> <tr class="extras-row"> - <td class="extra" colspan="4"> + <td class="extra" colspan="{{ table_head|length }}"> <div class="extraHTML"></div> <div class="media"> <div class="media-container"> @@ -47,32 +53,23 @@ <div class="media__name"></div> <div class="media__counter"></div> </div> - <div class="log"></div> + <div class="logwrapper"> + <div class="logexpander"></div> + <div class="log"></div> + </div> </td> </tr> </tbody> </template> - <template id="template_results-table__head"> - <thead id="results-table-head"> - <tr> - {%- for th in table_head %} - {{ th|safe }} - {%- endfor %} - </tr> - </thead> - </template> - <template id="template_results-table__head--empty"> - <tr id="not-found-message"> - <th colspan="4">No results found. Check the filters.</th> - </tr> - </template> <!-- END TEMPLATES --> <div class="summary"> <div class="summary__data"> <h2>Summary</h2> - {%- for p in additional_summary['prefix'] %} - {{ p|safe }} - {%- endfor %} + <div class="additional-summary prefix"> + {%- for p in additional_summary['prefix'] %} + {{ p|safe }} + {%- endfor %} + </div> <p class="run-count">{{ run_count }}</p> <p class="filter">(Un)check the boxes to filter the results.</p> <div class="summary__reload"> @@ -81,26 +78,38 @@ </div> </div> <div class="summary__spacer"></div> - <div class="controls"> - <div class="filters"> - {%- for result, values in outcomes.items() %} - <input checked="true" class="filter" name="filter_checkbox" type="checkbox" data-test-result="{{ result }}" {{ "disabled" if values["value"] == 0 }}/> - <span class="{{ result }}">{{ values["value"] }} {{ values["label"] }}{{ "," if result != "rerun" }}</span> - {%- endfor %} - </div> - <div class="collapse"> - <button id="show_all_details">Show all details</button> / <button id="hide_all_details">Hide all details</button> - <div> + <div class="controls"> + <div class="filters"> + {%- for result, values in outcomes.items() %} + <input checked="true" class="filter" name="filter_checkbox" type="checkbox" data-test-result="{{ result }}" {{ "disabled" if values["value"] == 0 }}/> + <span class="{{ result }}">{{ values["value"] }} {{ values["label"] }}{{ "," if result != "rerun" }}</span> + {%- endfor %} + </div> + <div class="collapse"> + <button id="show_all_details">Show all details</button> / <button id="hide_all_details">Hide all details</button> </div> </div> </div> - {%- for s in additional_summary['summary'] %} - {{ s|safe }} - {%- endfor %} - {%- for p in additional_summary['postfix'] %} - {{ p|safe }} - {%- endfor %} - <table id="results-table"></table> + <div class="additional-summary summary"> + {%- for s in additional_summary['summary'] %} + {{ s|safe }} + {%- endfor %} + </div> + <div class="additional-summary postfix"> + {%- for p in additional_summary['postfix'] %} + {{ p|safe }} + {%- endfor %} + </div> + </div> + <table id="results-table"> + <thead id="results-table-head"> + <tr> + {%- for th in table_head %} + {{ th|safe }} + {%- endfor %} + </tr> + </thead> + </table> </body> <footer> <div id="data-container" data-jsonblob="{{ test_data }}"></div> diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pytest_html-4.0.0rc5/src/pytest_html/resources/style.css new/pytest_html-4.0.0/src/pytest_html/resources/style.css --- old/pytest_html-4.0.0rc5/src/pytest_html/resources/style.css 2023-07-28 17:28:57.000000000 +0200 +++ new/pytest_html-4.0.0/src/pytest_html/resources/style.css 2023-09-01 20:48:44.000000000 +0200 @@ -104,20 +104,49 @@ /*------------------ * 2. Extra *------------------*/ -.log { +.logwrapper { + max-height: 230px; + overflow-y: scroll; background-color: #e6e6e6; +} +.logwrapper.expanded { + max-height: none; +} +.logwrapper.expanded .logexpander:after { + content: "collapse [-]"; +} +.logwrapper .logexpander { + z-index: 1; + position: sticky; + top: 10px; + width: max-content; + border: 1px solid; + border-radius: 3px; + padding: 5px 7px; + margin: 10px 0 10px calc(100% - 80px); + cursor: pointer; + background-color: #e6e6e6; +} +.logwrapper .logexpander:after { + content: "expand [+]"; +} +.logwrapper .logexpander:hover { + color: #000; + border-color: #000; +} +.logwrapper .log { + min-height: 40px; + position: relative; + top: -50px; + height: calc(100% + 50px); border: 1px solid #e6e6e6; color: black; display: block; font-family: "Courier New", Courier, monospace; - height: 230px; - overflow-y: scroll; padding: 5px; + padding-right: 80px; white-space: pre-wrap; } -.log:only-child { - height: inherit; -} div.media { border: 1px solid #e6e6e6; @@ -137,6 +166,10 @@ height: 200px; } +.media-container--fullscreen { + grid-template-columns: 0px auto 0px; +} + .media-container__nav--right, .media-container__nav--left { text-align: center; @@ -173,6 +206,9 @@ cursor: pointer; } +.col-result { + width: 130px; +} .col-result:hover::after { content: " (hide details)"; } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pytest_html-4.0.0rc5/src/pytest_html/scripts/dom.js new/pytest_html-4.0.0/src/pytest_html/scripts/dom.js --- old/pytest_html-4.0.0rc5/src/pytest_html/scripts/dom.js 2023-07-28 17:28:57.000000000 +0200 +++ new/pytest_html-4.0.0/src/pytest_html/scripts/dom.js 2023-09-01 20:48:44.000000000 +0200 @@ -1,7 +1,6 @@ const mediaViewer = require('./mediaviewer.js') const templateEnvRow = document.getElementById('template_environment_row') const templateResult = document.getElementById('template_results-table__tbody') -const listHeaderEmpty = document.getElementById('template_results-table__head--empty') function htmlToElements(html) { const temp = document.createElement('template') @@ -37,11 +36,9 @@ return envRow }, - getListHeaderEmpty: () => listHeaderEmpty.content.cloneNode(true), - getResultTBody: ({ testId, id, log, duration, extras, resultsTableRow, tableHtml, result, collapsed }) => { - const resultLower = result.toLowerCase() + getResultTBody: ({ testId, id, log, extras, resultsTableRow, tableHtml, result, collapsed }) => { const resultBody = templateResult.content.cloneNode(true) - resultBody.querySelector('tbody').classList.add(resultLower) + resultBody.querySelector('tbody').classList.add(result.toLowerCase()) resultBody.querySelector('tbody').id = testId resultBody.querySelector('.collapsible').dataset.id = id diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pytest_html-4.0.0rc5/src/pytest_html/scripts/main.js new/pytest_html-4.0.0/src/pytest_html/scripts/main.js --- old/pytest_html-4.0.0rc5/src/pytest_html/scripts/main.js 2023-07-28 17:28:57.000000000 +0200 +++ new/pytest_html-4.0.0/src/pytest_html/scripts/main.js 2023-09-01 20:48:44.000000000 +0200 @@ -1,4 +1,4 @@ -const { dom, findAll } = require('./dom.js') +const { dom, find, findAll } = require('./dom.js') const { manager } = require('./datamanager.js') const { doSort } = require('./sort.js') const { doFilter } = require('./filter.js') @@ -28,52 +28,54 @@ renderEnvironmentTable() } +const addItemToggleListener = (elem) => { + elem.addEventListener('click', ({ target }) => { + const id = target.parentElement.dataset.id + manager.toggleCollapsedItem(id) + + const collapsedIds = getCollapsedIds() + if (collapsedIds.includes(id)) { + const updated = collapsedIds.filter((item) => item !== id) + setCollapsedIds(updated) + } else { + collapsedIds.push(id) + setCollapsedIds(collapsedIds) + } + redraw() + }) +} + const renderContent = (tests) => { const sortAttr = getSort(manager.initialSort) const sortAsc = JSON.parse(getSortDirection()) const rows = tests.map(dom.getResultTBody) const table = document.getElementById('results-table') - const tableHeader = document.getElementById('template_results-table__head').content.cloneNode(true) - - removeChildren(table) - - tableHeader.querySelector(`.sortable[data-column-type="${sortAttr}"]`)?.classList.add(sortAsc ? 'desc' : 'asc') - if (!rows.length) { - tableHeader.appendChild(dom.getListHeaderEmpty()) - } - table.appendChild(tableHeader) + const tableHeader = document.getElementById('results-table-head') - rows.forEach((row) => !!row && table.appendChild(row)) + const newTable = document.createElement('table') + newTable.id = 'results-table' - table.querySelectorAll('.extra').forEach((item) => { - item.colSpan = document.querySelectorAll('th').length - }) + // remove all sorting classes and set the relevant + findAll('.sortable', tableHeader).forEach((elem) => elem.classList.remove('asc', 'desc')) + tableHeader.querySelector(`.sortable[data-column-type="${sortAttr}"]`).classList.add(sortAsc ? 'desc' : 'asc') + newTable.appendChild(tableHeader) - findAll('.sortable').forEach((elem) => { - elem.addEventListener('click', (evt) => { - const { target: element } = evt - const { columnType } = element.dataset - doSort(columnType) - redraw() - }) - }) - - findAll('.collapsible td:not(.col-links').forEach((elem) => { - elem.addEventListener('click', ({ target }) => { - const id = target.parentElement.dataset.id - manager.toggleCollapsedItem(id) - - const collapsedIds = getCollapsedIds() - if (collapsedIds.includes(id)) { - const updated = collapsedIds.filter((item) => item !== id) - setCollapsedIds(updated) - } else { - collapsedIds.push(id) - setCollapsedIds(collapsedIds) + if (!rows.length) { + const emptyTable = document.getElementById('template_results-table__body--empty').content.cloneNode(true) + newTable.appendChild(emptyTable) + } else { + rows.forEach((row) => { + if (!!row) { + findAll('.collapsible td:not(.col-links', row).forEach(addItemToggleListener) + find('.logexpander', row).addEventListener('click', + (evt) => evt.target.parentNode.classList.toggle('expanded'), + ) + newTable.appendChild(row) } - redraw() }) - }) + } + + table.replaceWith(newTable) } const renderDerived = () => { @@ -111,6 +113,16 @@ findAll('input[name="filter_checkbox"]').forEach((elem) => { elem.addEventListener('click', filterColumn) }) + + findAll('.sortable').forEach((elem) => { + elem.addEventListener('click', (evt) => { + const { target: element } = evt + const { columnType } = element.dataset + doSort(columnType) + redraw() + }) + }) + document.getElementById('show_all_details').addEventListener('click', () => { manager.allCollapsed = false setCollapsedIds([]) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pytest_html-4.0.0rc5/src/pytest_html/scripts/mediaviewer.js new/pytest_html-4.0.0/src/pytest_html/scripts/mediaviewer.js --- old/pytest_html-4.0.0rc5/src/pytest_html/scripts/mediaviewer.js 2023-07-28 17:28:57.000000000 +0200 +++ new/pytest_html-4.0.0/src/pytest_html/scripts/mediaviewer.js 2023-09-01 20:48:44.000000000 +0200 @@ -31,6 +31,7 @@ } const mediaViewer = new MediaViewer(assets) + const container = resultBody.querySelector('.media-container') const leftArrow = resultBody.querySelector('.media-container__nav--left') const rightArrow = resultBody.querySelector('.media-container__nav--right') const mediaName = resultBody.querySelector('.media__name') @@ -68,9 +69,12 @@ const openImg = () => { window.open(mediaViewer.activeFile.path, '_blank') } - - leftArrow.addEventListener('click', moveLeft) - rightArrow.addEventListener('click', doRight) + if (assets.length === 1) { + container.classList.add('media-container--fullscreen') + } else { + leftArrow.addEventListener('click', moveLeft) + rightArrow.addEventListener('click', doRight) + } imageEl.addEventListener('click', openImg) } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pytest_html-4.0.0rc5/testing/test_e2e.py new/pytest_html-4.0.0/testing/test_e2e.py --- old/pytest_html-4.0.0rc5/testing/test_e2e.py 2023-07-28 17:28:57.000000000 +0200 +++ new/pytest_html-4.0.0/testing/test_e2e.py 2023-09-01 20:48:44.000000000 +0200 @@ -54,6 +54,23 @@ return urllib.parse.urlencode(params) +def _parse_result_table(driver): + table = driver.find_element(By.ID, "results-table") + headers = table.find_elements(By.CSS_SELECTOR, "thead th") + rows = table.find_elements(By.CSS_SELECTOR, "tbody tr.collapsible") + table_data = [] + for row in rows: + data_dict = {} + + cells = row.find_elements(By.TAG_NAME, "td") + for header, cell in zip(headers, cells): + data_dict[header.text.lower()] = cell.text + + table_data.append(data_dict) + + return table_data + + def test_visible(pytester, path, driver): pytester.makepyfile( """ @@ -76,3 +93,45 @@ ) result = driver.find_elements(By.CSS_SELECTOR, "tr.collapsible") assert_that(result).is_length(0) + + +def test_custom_sorting(pytester, path, driver): + pytester.makeconftest( + """ + def pytest_html_results_table_header(cells): + cells.append( + '<th class="sortable alpha" data-column-type="alpha">Alpha</th>' + ) + + def pytest_html_results_table_row(report, cells): + data = report.nodeid.split("_")[-1] + cells.append(f'<td class="col-alpha">{data}</td>') + """ + ) + pytester.makepyfile( + """ + def test_AAA(): pass + def test_BBB(): pass + """ + ) + query_params = _encode_query_params({"sort": "alpha"}) + driver.get(f"file:///reports{path()}?{query_params}") + WebDriverWait(driver, 5).until( + ec.visibility_of_all_elements_located((By.CSS_SELECTOR, "#results-table")) + ) + + rows = _parse_result_table(driver) + assert_that(rows).is_length(2) + assert_that(rows[0]["test"]).contains("AAA") + assert_that(rows[0]["alpha"]).is_equal_to("AAA") + assert_that(rows[1]["test"]).contains("BBB") + assert_that(rows[1]["alpha"]).is_equal_to("BBB") + + driver.find_element(By.CSS_SELECTOR, "th[data-column-type='alpha']").click() + # we might need some wait here to ensure sorting happened + rows = _parse_result_table(driver) + assert_that(rows).is_length(2) + assert_that(rows[0]["test"]).contains("BBB") + assert_that(rows[0]["alpha"]).is_equal_to("BBB") + assert_that(rows[1]["test"]).contains("AAA") + assert_that(rows[1]["alpha"]).is_equal_to("AAA") diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pytest_html-4.0.0rc5/testing/test_integration.py new/pytest_html-4.0.0/testing/test_integration.py --- old/pytest_html-4.0.0rc5/testing/test_integration.py 2023-07-28 17:28:57.000000000 +0200 +++ new/pytest_html-4.0.0/testing/test_integration.py 2023-09-01 20:48:44.000000000 +0200 @@ -52,7 +52,13 @@ # End workaround driver.get(f"file:///reports{path}?{query_params}") - return BeautifulSoup(driver.page_source, "html.parser") + soup = BeautifulSoup(driver.page_source, "html.parser") + + # remove all templates as they bork the BS parsing + for template in soup("template"): + template.decompose() + + return soup finally: driver.quit() @@ -88,15 +94,15 @@ def is_collapsed(page, test_name): - return get_element(page, f".summary tbody[id$='{test_name}'] .collapsed") + return get_element(page, f"tbody[id$='{test_name}'] .collapsed") def get_log(page, test_id=None): # TODO(jim) move to get_text (use .contents) if test_id: - log = get_element(page, f".summary tbody[id$='{test_id}'] div[class='log']") + log = get_element(page, f"tbody[id$='{test_id}'] div[class='log']") else: - log = get_element(page, ".summary div[class='log']") + log = get_element(page, "div[class='log']") all_text = "" for text in log.strings: all_text += text @@ -150,6 +156,21 @@ assert_that(duration).matches(expectation) assert_that(total_duration).matches(r"\d{2}:\d{2}:\d{2}") + def test_duration_format_hook(self, pytester): + pytester.makeconftest( + """ + def pytest_html_duration_format(duration): + return str(round(duration * 1000)) + " seconds" + """ + ) + + pytester.makepyfile("def test_pass(): pass") + page = run(pytester) + assert_results(page, passed=1) + + duration = get_text(page, "#results-table td[class='col-duration']") + assert_that(duration).contains("seconds") + def test_total_number_of_tests_zero(self, pytester): page = run(pytester) assert_results(page) @@ -195,7 +216,7 @@ page = run(pytester) assert_results(page, skipped=1, total_tests=0) - log = get_text(page, ".summary div[class='log']") + log = get_text(page, "div[class='log']") assert_that(log).contains(reason) def test_skip_function_marker(self, pytester): @@ -211,7 +232,7 @@ page = run(pytester) assert_results(page, skipped=1, total_tests=0) - log = get_text(page, ".summary div[class='log']") + log = get_text(page, "div[class='log']") assert_that(log).contains(reason) def test_skip_class_marker(self, pytester): @@ -228,7 +249,7 @@ page = run(pytester) assert_results(page, skipped=1, total_tests=0) - log = get_text(page, ".summary div[class='log']") + log = get_text(page, "div[class='log']") assert_that(log).contains(reason) def test_fail(self, pytester): @@ -236,7 +257,7 @@ page = run(pytester) assert_results(page, failed=1) assert_that(get_log(page)).contains("AssertionError") - assert_that(get_text(page, ".summary div[class='log'] span.error")).matches( + assert_that(get_text(page, "div[class='log'] span.error")).matches( r"^E\s+assert False$" ) @@ -352,7 +373,7 @@ page = run(pytester) assert_results(page, error=1, total_tests=0) - col_name = get_text(page, ".summary td[class='col-name']") + col_name = get_text(page, "td[class='col-testId']") assert_that(col_name).contains("::setup") assert_that(get_log(page)).contains("ValueError") @@ -411,7 +432,9 @@ pytester.makepyfile("def test_pass(): pass") page = run(pytester) - elements = page.select(".summary__data p:not(.run-count):not(.filter)") + elements = page.select( + ".additional-summary p" + ) # ".summary__data p:not(.run-count):not(.filter)") assert_that(elements).is_length(3) for element in elements: key = re.search(r"(\w+).*", element.string).group(1) @@ -437,7 +460,7 @@ pytester.makepyfile("def test_pass(): pass") page = run(pytester) - assert_that(page.select_one(".summary .extraHTML").string).is_equal_to(content) + assert_that(page.select_one(".extraHTML").string).is_equal_to(content) @pytest.mark.parametrize( "content, encoded", @@ -460,7 +483,7 @@ pytester.makepyfile("def test_pass(): pass") page = run(pytester, cmd_flags=["--self-contained-html"]) - element = page.select_one(".summary a[class='col-links__extra text']") + element = page.select_one("a[class='col-links__extra text']") assert_that(element.string).is_equal_to("Text") assert_that(element["href"]).is_equal_to( f"data:text/plain;charset=utf-8;base64,{encoded}" @@ -488,7 +511,7 @@ content_str = json.dumps(content) data = b64encode(content_str.encode("utf-8")).decode("ascii") - element = page.select_one(".summary a[class='col-links__extra json']") + element = page.select_one("a[class='col-links__extra json']") assert_that(element.string).is_equal_to("JSON") assert_that(element["href"]).is_equal_to( f"data:application/json;charset=utf-8;base64,{data}" @@ -512,7 +535,7 @@ pytester.makepyfile("def test_pass(): pass") page = run(pytester) - element = page.select_one(".summary a[class='col-links__extra url']") + element = page.select_one("a[class='col-links__extra url']") assert_that(element.string).is_equal_to("URL") assert_that(element["href"]).is_equal_to(content) @@ -551,7 +574,7 @@ # assert_that(element.string).is_equal_to("Image") # assert_that(element["href"]).is_equal_to(src) - element = page.select_one(".summary .media img") + element = page.select_one(".media img") assert_that(str(element)).is_equal_to(f'<img src="{src}"/>') @pytest.mark.parametrize("mime_type, extension", [("video/mp4", "mp4")]) @@ -579,7 +602,7 @@ # assert_that(element.string).is_equal_to("Video") # assert_that(element["href"]).is_equal_to(src) - element = page.select_one(".summary .media video") + element = page.select_one(".media video") assert_that(str(element)).is_equal_to( f'<video controls="">\n<source src="{src}" type="{mime_type}"/>\n</video>' ) @@ -590,10 +613,8 @@ assert_results(page, passed=1) def test_results_table_hook_append(self, pytester): - header_selector = ( - ".summary #results-table-head tr:nth-child(1) th:nth-child({})" - ) - row_selector = ".summary #results-table tr:nth-child(1) td:nth-child({})" + header_selector = "#results-table-head tr:nth-child(1) th:nth-child({})" + row_selector = "#results-table tr:nth-child(1) td:nth-child({})" pytester.makeconftest( """ @@ -628,10 +649,8 @@ ) def test_results_table_hook_insert(self, pytester): - header_selector = ( - ".summary #results-table-head tr:nth-child(1) th:nth-child({})" - ) - row_selector = ".summary #results-table tr:nth-child(1) td:nth-child({})" + header_selector = "#results-table-head tr:nth-child(1) th:nth-child({})" + row_selector = "#results-table tr:nth-child(1) td:nth-child({})" pytester.makeconftest( """ @@ -700,12 +719,10 @@ pytester.makepyfile("def test_pass(): pass") page = run(pytester) - header_columns = page.select(".summary #results-table-head th") + header_columns = page.select("#results-table-head th") assert_that(header_columns).is_length(3) - row_columns = page.select_one(".summary .results-table-row").select( - "td:not(.extra)" - ) + row_columns = page.select_one(".results-table-row").select("td:not(.extra)") assert_that(row_columns).is_length(3) @pytest.mark.parametrize("no_capture", ["", "-s"]) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pytest_html-4.0.0rc5/tox.ini new/pytest_html-4.0.0/tox.ini --- old/pytest_html-4.0.0rc5/tox.ini 2023-07-28 17:28:57.000000000 +0200 +++ new/pytest_html-4.0.0/tox.ini 2023-09-01 20:48:44.000000000 +0200 @@ -4,7 +4,7 @@ # and then run "tox" from this directory. [tox] -envlist = py{3.7, 3.8, 3.9, 3.10, py3.9}, docs, linting +envlist = {3.8, 3.9, 3.10, 3.10-cov, pypy3.9}, docs, linting isolated_build = True [testenv]
