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>&nbsp;/&nbsp;<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>&nbsp;/&nbsp;<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]

Reply via email to