diff --git a/docs/user_guide.rst b/docs/user_guide.rst index bf0c0f78..ca0fb7e9 100644 --- a/docs/user_guide.rst +++ b/docs/user_guide.rst @@ -197,20 +197,17 @@ adds a sortable time column, and removes the links column: .. code-block:: python from datetime import datetime - from py.xml import html import pytest def pytest_html_results_table_header(cells): - cells.insert(2, html.th("Description")) - cells.insert(1, html.th("Time", class_="sortable time", col="time")) - cells.pop() + cells.insert(2, "Description") + cells.insert(1, 'Time') def pytest_html_results_table_row(report, cells): - cells.insert(2, html.td(report.description)) - cells.insert(1, html.td(datetime.utcnow(), class_="col-time")) - cells.pop() + cells.insert(2, "A description") + cells.insert(1, 'A time') @pytest.hookimpl(hookwrapper=True) diff --git a/src/pytest_html/nextgen.py b/src/pytest_html/nextgen.py index 219d2d79..0ad6f7d4 100644 --- a/src/pytest_html/nextgen.py +++ b/src/pytest_html/nextgen.py @@ -37,11 +37,25 @@ class Cells: def __init__(self): self._html = {} + def __delitem__(self, key): + # This means the item should be removed + self._html = None + @property def html(self): return self._html def insert(self, index, html): + # backwards-compat + if not isinstance(html, str): + if html.__module__.startswith("py."): + warnings.warn( + "The 'py' module is deprecated and support " + "will be removed in a future release.", + DeprecationWarning, + ) + html = str(html) + html = html.replace("col", "data-column-type") self._html[index] = html class Report: @@ -219,6 +233,7 @@ def pytest_sessionstart(self, session): header_cells = self.Cells() session.config.hook.pytest_html_results_table_header(cells=header_cells) + self._report.set_data("resultsTableHeader", header_cells.html) self._report.set_data("runningState", "Started") @@ -258,25 +273,30 @@ def pytest_runtest_logreport(self, report): } test_id = report.nodeid - if report.when != "call": + if report.when == "call": + row_cells = self.Cells() + self._config.hook.pytest_html_results_table_row( + report=report, cells=row_cells + ) + if row_cells.html is None: + return + data["resultsTableRow"] = row_cells.html + + table_html = [] + self._config.hook.pytest_html_results_table_html( + report=report, data=table_html + ) + data["tableHtml"] = table_html + else: test_id += f"::{report.when}" data["testId"] = test_id # Order here matters! log = report.longreprtext or report.capstdout or "No log output captured." data["log"] = _handle_ansi(log) - data["result"] = _process_outcome(report) - - row_cells = self.Cells() - self._config.hook.pytest_html_results_table_row(report=report, cells=row_cells) - data["resultsTableRow"] = row_cells.html - - table_html = [] - self._config.hook.pytest_html_results_table_html(report=report, data=table_html) - data["tableHtml"] = table_html - data["extras"] = self._process_extras(report, test_id) + self._report.add_test(data) self._generate_report() diff --git a/src/pytest_html/scripts/dom.js b/src/pytest_html/scripts/dom.js index b3d07c80..eb68d986 100644 --- a/src/pytest_html/scripts/dom.js +++ b/src/pytest_html/scripts/dom.js @@ -52,7 +52,21 @@ const dom = { const header = listHeader.content.cloneNode(true) const sortAttr = storageModule.getSort() const sortAsc = JSON.parse(storageModule.getSortDirection()) - const sortables = ['result', 'testId', 'duration'] + + const regex = /data-column-type="(\w+)/ + const cols = Object.values(resultsTableHeader).reduce((result, value) => { + if (value.includes("sortable")) { + const matches = regex.exec(value) + if (matches) { + result.push(matches[1]) + } + } + return result + }, []) + const sortables = ['result', 'testId', 'duration', ...cols] + + // Add custom html from the pytest_html_results_table_header hook + insertAdditionalHTML(resultsTableHeader, header, 'th') sortables.forEach((sortCol) => { if (sortCol === sortAttr) { @@ -60,9 +74,6 @@ const dom = { } }) - // Add custom html from the pytest_html_results_table_header hook - insertAdditionalHTML(resultsTableHeader, header, 'th') - return header }, getListHeaderEmpty: () => listHeaderEmpty.content.cloneNode(true), diff --git a/src/pytest_html/scripts/storage.js b/src/pytest_html/scripts/storage.js index 4a87216b..5703b013 100644 --- a/src/pytest_html/scripts/storage.js +++ b/src/pytest_html/scripts/storage.js @@ -1,15 +1,15 @@ -const possibleFiltes = ['passed', 'skipped', 'failed', 'error', 'xfailed', 'xpassed', 'rerun'] +const possibleFilters = ['passed', 'skipped', 'failed', 'error', 'xfailed', 'xpassed', 'rerun'] const getVisible = () => { const url = new URL(window.location.href) const settings = new URLSearchParams(url.search).get('visible') || '' return settings ? - [...new Set(settings.split(',').filter((filter) => possibleFiltes.includes(filter)))] : possibleFiltes + [...new Set(settings.split(',').filter((filter) => possibleFilters.includes(filter)))] : possibleFilters } const hideCategory = (categoryToHide) => { const url = new URL(window.location.href) const visibleParams = new URLSearchParams(url.search).get('visible') - const currentVisible = visibleParams ? visibleParams.split(',') : [...possibleFiltes] + const currentVisible = visibleParams ? visibleParams.split(',') : [...possibleFilters] const settings = [...new Set(currentVisible)].filter((f) => f !== categoryToHide).join(',') url.searchParams.set('visible', settings) @@ -21,15 +21,15 @@ const showCategory = (categoryToShow) => { return } const url = new URL(window.location.href) - const currentVisible = new URLSearchParams(url.search).get('visible')?.split(',') || [...possibleFiltes] + const currentVisible = new URLSearchParams(url.search).get('visible')?.split(',') || [...possibleFilters] const settings = [...new Set([categoryToShow, ...currentVisible])] - const noFilter = possibleFiltes.length === settings.length || !settings.length + const noFilter = possibleFilters.length === settings.length || !settings.length noFilter ? url.searchParams.delete('visible') : url.searchParams.set('visible', settings.join(',')) history.pushState({}, null, unescape(url.href)) } const setFilter = (currentFilter) => { - if (!possibleFiltes.includes(currentFilter)) { + if (!possibleFilters.includes(currentFilter)) { return } const url = new URL(window.location.href) diff --git a/testing/test_integration.py b/testing/test_integration.py index cf8b038a..587a2785 100644 --- a/testing/test_integration.py +++ b/testing/test_integration.py @@ -33,7 +33,6 @@ def run(pytester, path="report.html", *args): chrome_options = webdriver.ChromeOptions() chrome_options.add_argument("--headless") chrome_options.add_argument("--window-size=1920x1080") - # chrome_options.add_argument("--allow-file-access-from-files") driver = webdriver.Remote( command_executor="http://127.0.0.1:4444", options=chrome_options ) @@ -476,3 +475,55 @@ def test_xdist(self, pytester): pytester.makepyfile("def test_xdist(): pass") page = run(pytester, "report.html", "-n1") assert_results(page, passed=1) + + 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({})" + + pytester.makeconftest( + """ + def pytest_html_results_table_header(cells): + cells.insert(2, "Description") + cells.insert( + 1, + 'Time' + ) + + def pytest_html_results_table_row(report, cells): + cells.insert(2, "A description") + cells.insert(1, 'A time') + """ + ) + pytester.makepyfile("def test_pass(): pass") + page = run(pytester) + + assert_that(get_text(page, header_selector.format(2))).is_equal_to("Time") + assert_that(get_text(page, header_selector.format(3))).is_equal_to( + "Description" + ) + + assert_that(get_text(page, row_selector.format(2))).is_equal_to("A time") + assert_that(get_text(page, row_selector.format(3))).is_equal_to("A description") + + def test_results_table_hook_delete(self, pytester): + pytester.makeconftest( + """ + def pytest_html_results_table_row(report, cells): + if report.skipped: + del cells[:] + """ + ) + pytester.makepyfile( + """ + import pytest + def test_skip(): + pytest.skip('reason') + + def test_pass(): pass + + """ + ) + page = run(pytester) + assert_results(page, passed=1)