Skip to content

Commit 40a80d5

Browse files
authored
fix: Table hooks (#599)
1 parent 079681d commit 40a80d5

File tree

5 files changed

+108
-29
lines changed

5 files changed

+108
-29
lines changed

docs/user_guide.rst

+4-7
Original file line numberDiff line numberDiff line change
@@ -197,20 +197,17 @@ adds a sortable time column, and removes the links column:
197197
.. code-block:: python
198198
199199
from datetime import datetime
200-
from py.xml import html
201200
import pytest
202201
203202
204203
def pytest_html_results_table_header(cells):
205-
cells.insert(2, html.th("Description"))
206-
cells.insert(1, html.th("Time", class_="sortable time", col="time"))
207-
cells.pop()
204+
cells.insert(2, "<th>Description</th>")
205+
cells.insert(1, '<th class="sortable time" data-column-type="time">Time</th>')
208206
209207
210208
def pytest_html_results_table_row(report, cells):
211-
cells.insert(2, html.td(report.description))
212-
cells.insert(1, html.td(datetime.utcnow(), class_="col-time"))
213-
cells.pop()
209+
cells.insert(2, "<td>A description</td>")
210+
cells.insert(1, '<td class="col-time">A time</td>')
214211
215212
216213
@pytest.hookimpl(hookwrapper=True)

src/pytest_html/nextgen.py

+31-11
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,25 @@ class Cells:
3737
def __init__(self):
3838
self._html = {}
3939

40+
def __delitem__(self, key):
41+
# This means the item should be removed
42+
self._html = None
43+
4044
@property
4145
def html(self):
4246
return self._html
4347

4448
def insert(self, index, html):
49+
# backwards-compat
50+
if not isinstance(html, str):
51+
if html.__module__.startswith("py."):
52+
warnings.warn(
53+
"The 'py' module is deprecated and support "
54+
"will be removed in a future release.",
55+
DeprecationWarning,
56+
)
57+
html = str(html)
58+
html = html.replace("col", "data-column-type")
4559
self._html[index] = html
4660

4761
class Report:
@@ -219,6 +233,7 @@ def pytest_sessionstart(self, session):
219233

220234
header_cells = self.Cells()
221235
session.config.hook.pytest_html_results_table_header(cells=header_cells)
236+
222237
self._report.set_data("resultsTableHeader", header_cells.html)
223238

224239
self._report.set_data("runningState", "Started")
@@ -258,25 +273,30 @@ def pytest_runtest_logreport(self, report):
258273
}
259274

260275
test_id = report.nodeid
261-
if report.when != "call":
276+
if report.when == "call":
277+
row_cells = self.Cells()
278+
self._config.hook.pytest_html_results_table_row(
279+
report=report, cells=row_cells
280+
)
281+
if row_cells.html is None:
282+
return
283+
data["resultsTableRow"] = row_cells.html
284+
285+
table_html = []
286+
self._config.hook.pytest_html_results_table_html(
287+
report=report, data=table_html
288+
)
289+
data["tableHtml"] = table_html
290+
else:
262291
test_id += f"::{report.when}"
263292
data["testId"] = test_id
264293

265294
# Order here matters!
266295
log = report.longreprtext or report.capstdout or "No log output captured."
267296
data["log"] = _handle_ansi(log)
268-
269297
data["result"] = _process_outcome(report)
270-
271-
row_cells = self.Cells()
272-
self._config.hook.pytest_html_results_table_row(report=report, cells=row_cells)
273-
data["resultsTableRow"] = row_cells.html
274-
275-
table_html = []
276-
self._config.hook.pytest_html_results_table_html(report=report, data=table_html)
277-
data["tableHtml"] = table_html
278-
279298
data["extras"] = self._process_extras(report, test_id)
299+
280300
self._report.add_test(data)
281301
self._generate_report()
282302

src/pytest_html/scripts/dom.js

+15-4
Original file line numberDiff line numberDiff line change
@@ -52,17 +52,28 @@ const dom = {
5252
const header = listHeader.content.cloneNode(true)
5353
const sortAttr = storageModule.getSort()
5454
const sortAsc = JSON.parse(storageModule.getSortDirection())
55-
const sortables = ['result', 'testId', 'duration']
55+
56+
const regex = /data-column-type="(\w+)/
57+
const cols = Object.values(resultsTableHeader).reduce((result, value) => {
58+
if (value.includes("sortable")) {
59+
const matches = regex.exec(value)
60+
if (matches) {
61+
result.push(matches[1])
62+
}
63+
}
64+
return result
65+
}, [])
66+
const sortables = ['result', 'testId', 'duration', ...cols]
67+
68+
// Add custom html from the pytest_html_results_table_header hook
69+
insertAdditionalHTML(resultsTableHeader, header, 'th')
5670

5771
sortables.forEach((sortCol) => {
5872
if (sortCol === sortAttr) {
5973
header.querySelector(`[data-column-type="${sortCol}"]`).classList.add(sortAsc ? 'desc' : 'asc')
6074
}
6175
})
6276

63-
// Add custom html from the pytest_html_results_table_header hook
64-
insertAdditionalHTML(resultsTableHeader, header, 'th')
65-
6677
return header
6778
},
6879
getListHeaderEmpty: () => listHeaderEmpty.content.cloneNode(true),

src/pytest_html/scripts/storage.js

+6-6
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
1-
const possibleFiltes = ['passed', 'skipped', 'failed', 'error', 'xfailed', 'xpassed', 'rerun']
1+
const possibleFilters = ['passed', 'skipped', 'failed', 'error', 'xfailed', 'xpassed', 'rerun']
22

33
const getVisible = () => {
44
const url = new URL(window.location.href)
55
const settings = new URLSearchParams(url.search).get('visible') || ''
66
return settings ?
7-
[...new Set(settings.split(',').filter((filter) => possibleFiltes.includes(filter)))] : possibleFiltes
7+
[...new Set(settings.split(',').filter((filter) => possibleFilters.includes(filter)))] : possibleFilters
88
}
99
const hideCategory = (categoryToHide) => {
1010
const url = new URL(window.location.href)
1111
const visibleParams = new URLSearchParams(url.search).get('visible')
12-
const currentVisible = visibleParams ? visibleParams.split(',') : [...possibleFiltes]
12+
const currentVisible = visibleParams ? visibleParams.split(',') : [...possibleFilters]
1313
const settings = [...new Set(currentVisible)].filter((f) => f !== categoryToHide).join(',')
1414

1515
url.searchParams.set('visible', settings)
@@ -21,15 +21,15 @@ const showCategory = (categoryToShow) => {
2121
return
2222
}
2323
const url = new URL(window.location.href)
24-
const currentVisible = new URLSearchParams(url.search).get('visible')?.split(',') || [...possibleFiltes]
24+
const currentVisible = new URLSearchParams(url.search).get('visible')?.split(',') || [...possibleFilters]
2525
const settings = [...new Set([categoryToShow, ...currentVisible])]
26-
const noFilter = possibleFiltes.length === settings.length || !settings.length
26+
const noFilter = possibleFilters.length === settings.length || !settings.length
2727

2828
noFilter ? url.searchParams.delete('visible') : url.searchParams.set('visible', settings.join(','))
2929
history.pushState({}, null, unescape(url.href))
3030
}
3131
const setFilter = (currentFilter) => {
32-
if (!possibleFiltes.includes(currentFilter)) {
32+
if (!possibleFilters.includes(currentFilter)) {
3333
return
3434
}
3535
const url = new URL(window.location.href)

testing/test_integration.py

+52-1
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@ def run(pytester, path="report.html", *args):
3333
chrome_options = webdriver.ChromeOptions()
3434
chrome_options.add_argument("--headless")
3535
chrome_options.add_argument("--window-size=1920x1080")
36-
# chrome_options.add_argument("--allow-file-access-from-files")
3736
driver = webdriver.Remote(
3837
command_executor="http://127.0.0.1:4444", options=chrome_options
3938
)
@@ -476,3 +475,55 @@ def test_xdist(self, pytester):
476475
pytester.makepyfile("def test_xdist(): pass")
477476
page = run(pytester, "report.html", "-n1")
478477
assert_results(page, passed=1)
478+
479+
def test_results_table_hook_insert(self, pytester):
480+
header_selector = (
481+
".summary #results-table-head tr:nth-child(1) th:nth-child({})"
482+
)
483+
row_selector = ".summary #results-table tr:nth-child(1) td:nth-child({})"
484+
485+
pytester.makeconftest(
486+
"""
487+
def pytest_html_results_table_header(cells):
488+
cells.insert(2, "<th>Description</th>")
489+
cells.insert(
490+
1,
491+
'<th class="sortable time" data-column-type="time">Time</th>'
492+
)
493+
494+
def pytest_html_results_table_row(report, cells):
495+
cells.insert(2, "<td>A description</td>")
496+
cells.insert(1, '<td class="col-time">A time</td>')
497+
"""
498+
)
499+
pytester.makepyfile("def test_pass(): pass")
500+
page = run(pytester)
501+
502+
assert_that(get_text(page, header_selector.format(2))).is_equal_to("Time")
503+
assert_that(get_text(page, header_selector.format(3))).is_equal_to(
504+
"Description"
505+
)
506+
507+
assert_that(get_text(page, row_selector.format(2))).is_equal_to("A time")
508+
assert_that(get_text(page, row_selector.format(3))).is_equal_to("A description")
509+
510+
def test_results_table_hook_delete(self, pytester):
511+
pytester.makeconftest(
512+
"""
513+
def pytest_html_results_table_row(report, cells):
514+
if report.skipped:
515+
del cells[:]
516+
"""
517+
)
518+
pytester.makepyfile(
519+
"""
520+
import pytest
521+
def test_skip():
522+
pytest.skip('reason')
523+
524+
def test_pass(): pass
525+
526+
"""
527+
)
528+
page = run(pytester)
529+
assert_results(page, passed=1)

0 commit comments

Comments
 (0)