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)