Skip to content

Commit 0d04204

Browse files
committed
fix: Collapsed should support All and none
1 parent f381d08 commit 0d04204

File tree

8 files changed

+227
-35
lines changed

8 files changed

+227
-35
lines changed

docs/user_guide.rst

+9-5
Original file line numberDiff line numberDiff line change
@@ -248,15 +248,19 @@ Auto Collapsing Table Rows
248248

249249
By default, all rows in the **Results** table will be expanded except those that have :code:`Passed`.
250250

251-
This behavior can be customized either with a query parameter: :code:`?collapsed=Passed,XFailed,Skipped`
252-
or by setting the :code:`render_collapsed` in a configuration file (pytest.ini, setup.cfg, etc).
251+
This behavior can be customized with a query parameter: :code:`?collapsed=Passed,XFailed,Skipped`.
252+
If you want all rows to be collapsed you can pass :code:`?collapsed=All`.
253+
By setting the query parameter to empty string :code:`?collapsed=""` **none** of the rows will be collapsed.
254+
255+
Note that the query parameter is case insensitive, so passing :code:`PASSED` and :code:`passed` has the same effect.
256+
257+
You can also set the collapsed behaviour by setting the :code:`render_collapsed` in a configuration file (pytest.ini, setup.cfg, etc).
258+
Note that the query parameter takes precedence.
253259

254260
.. code-block:: ini
255261
256262
[pytest]
257-
render_collapsed = True
258-
259-
**NOTE:** Setting :code:`render_collapsed` will, unlike the query parameter, affect all statuses.
263+
render_collapsed = failed,error
260264
261265
Controlling Test Result Visibility Via Query Params
262266
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

src/pytest_html/nextgen.py

+4
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,10 @@ def __init__(self, title, config):
7171
"additionalSummary": defaultdict(list),
7272
}
7373

74+
collapsed = config.getini("render_collapsed")
75+
if collapsed:
76+
self.set_data("collapsed", collapsed.split(","))
77+
7478
@property
7579
def title(self):
7680
return self._data["title"]

src/pytest_html/plugin.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,8 @@ def pytest_addoption(parser):
4444
)
4545
parser.addini(
4646
"render_collapsed",
47-
type="bool",
48-
default=False,
47+
type="string",
48+
default="",
4949
help="Open the report with all rows collapsed. Useful for very large reports",
5050
)
5151
parser.addini(

src/pytest_html/scripts/datamanager.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ const { getCollapsedCategory } = require('./storage.js')
22

33
class DataManager {
44
setManager(data) {
5-
const collapsedCategories = [...getCollapsedCategory(), 'passed']
5+
const collapsedCategories = [...getCollapsedCategory(data.collapsed)]
66
const dataBlob = { ...data, tests: Object.values(data.tests).flat().map((test, index) => ({
77
...test,
88
id: `test_${index}`,

src/pytest_html/scripts/main.js

+1-11
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ const { dom, findAll } = require('./dom.js')
33
const { manager } = require('./datamanager.js')
44
const { doSort } = require('./sort.js')
55
const { doFilter } = require('./filter.js')
6-
const { getVisible } = require('./storage.js')
6+
const { getVisible, possibleResults } = require('./storage.js')
77

88
const removeChildren = (node) => {
99
while (node.firstChild) {
@@ -61,16 +61,6 @@ const renderContent = (tests) => {
6161
}
6262

6363
const renderDerived = (tests, collectedItems, isFinished) => {
64-
const possibleResults = [
65-
{ result: 'passed', label: 'Passed' },
66-
{ result: 'skipped', label: 'Skipped' },
67-
{ result: 'failed', label: 'Failed' },
68-
{ result: 'error', label: 'Errors' },
69-
{ result: 'xfailed', label: 'Unexpected failures' },
70-
{ result: 'xpassed', label: 'Unexpected passes' },
71-
{ result: 'rerun', label: 'Reruns' },
72-
]
73-
7464
const currentFilter = getVisible()
7565
possibleResults.forEach(({ result, label }) => {
7666
const count = tests.filter((test) => test.result.toLowerCase() === result).length

src/pytest_html/scripts/storage.js

+30-6
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,13 @@
1-
const possibleFilters = ['passed', 'skipped', 'failed', 'error', 'xfailed', 'xpassed', 'rerun']
1+
const possibleResults = [
2+
{ result: 'passed', label: 'Passed' },
3+
{ result: 'skipped', label: 'Skipped' },
4+
{ result: 'failed', label: 'Failed' },
5+
{ result: 'error', label: 'Errors' },
6+
{ result: 'xfailed', label: 'Unexpected failures' },
7+
{ result: 'xpassed', label: 'Unexpected passes' },
8+
{ result: 'rerun', label: 'Reruns' },
9+
]
10+
const possibleFilters = possibleResults.map((item) => item.result)
211

312
const getVisible = () => {
413
const url = new URL(window.location.href)
@@ -49,16 +58,29 @@ const setSort = (type) => {
4958
history.pushState({}, null, unescape(url.href))
5059
}
5160

52-
const getCollapsedCategory = () => {
53-
let categotries
61+
const getCollapsedCategory = (config) => {
62+
let categories
5463
if (typeof window !== 'undefined') {
5564
const url = new URL(window.location.href)
5665
const collapsedItems = new URLSearchParams(url.search).get('collapsed')
57-
categotries = collapsedItems?.split(',') || []
66+
switch (true) {
67+
case collapsedItems === null:
68+
categories = config || ['passed'];
69+
break;
70+
case collapsedItems?.length === 0 || /^["']{2}$/.test(collapsedItems):
71+
categories = [];
72+
break;
73+
case /^all$/.test(collapsedItems):
74+
categories = [...possibleFilters];
75+
break;
76+
default:
77+
categories = collapsedItems?.split(',').map(item => item.toLowerCase()) || [];
78+
break;
79+
}
5880
} else {
59-
categotries = []
81+
categories = []
6082
}
61-
return categotries
83+
return categories
6284
}
6385

6486
const getSortDirection = () => JSON.parse(sessionStorage.getItem('sortAsc'))
@@ -75,4 +97,6 @@ module.exports = {
7597
setSort,
7698
setSortDirection,
7799
getCollapsedCategory,
100+
possibleFilters,
101+
possibleResults,
78102
}

testing/test_integration.py

+114-10
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import os
55
import random
66
import re
7+
import urllib.parse
78
from base64 import b64encode
89
from pathlib import Path
910

@@ -26,9 +27,16 @@
2627
}
2728

2829

29-
def run(pytester, path="report.html", *args):
30+
def run(pytester, path="report.html", cmd_flags=None, query_params=None):
31+
if cmd_flags is None:
32+
cmd_flags = []
33+
34+
if query_params is None:
35+
query_params = {}
36+
query_params = urllib.parse.urlencode(query_params)
37+
3038
path = pytester.path.joinpath(path)
31-
pytester.runpytest("--html", path, *args)
39+
pytester.runpytest("--html", path, *cmd_flags)
3240

3341
chrome_options = webdriver.ChromeOptions()
3442
if os.environ.get("CI", False):
@@ -48,7 +56,7 @@ def run(pytester, path="report.html", *args):
4856
continue
4957
# End workaround
5058

51-
driver.get(f"file:///reports{path}")
59+
driver.get(f"file:///reports{path}?{query_params}")
5260
return BeautifulSoup(driver.page_source, "html.parser")
5361
finally:
5462
driver.quit()
@@ -91,6 +99,10 @@ def get_text(page, selector):
9199
return get_element(page, selector).string
92100

93101

102+
def is_collapsed(page, test_name):
103+
return get_element(page, f".summary tbody[id$='{test_name}'] .expander")
104+
105+
94106
def get_log(page, test_id=None):
95107
# TODO(jim) move to get_text (use .contents)
96108
if test_id:
@@ -267,7 +279,7 @@ def pytest_html_report_title(report):
267279

268280
def test_resources_inline_css(self, pytester):
269281
pytester.makepyfile("def test_pass(): pass")
270-
page = run(pytester, "report.html", "--self-contained-html")
282+
page = run(pytester, cmd_flags=["--self-contained-html"])
271283

272284
content = file_content()
273285

@@ -349,7 +361,7 @@ def pytest_runtest_makereport(item, call):
349361
)
350362

351363
pytester.makepyfile("def test_pass(): pass")
352-
page = run(pytester, "report.html", "--self-contained-html")
364+
page = run(pytester, cmd_flags=["--self-contained-html"])
353365

354366
element = page.select_one(".summary a[class='col-links__extra text']")
355367
assert_that(element.string).is_equal_to("Text")
@@ -374,7 +386,7 @@ def pytest_runtest_makereport(item, call):
374386
)
375387

376388
pytester.makepyfile("def test_pass(): pass")
377-
page = run(pytester, "report.html", "--self-contained-html")
389+
page = run(pytester, cmd_flags=["--self-contained-html"])
378390

379391
content_str = json.dumps(content)
380392
data = b64encode(content_str.encode("utf-8")).decode("ascii")
@@ -435,7 +447,7 @@ def pytest_runtest_makereport(item, call):
435447
"""
436448
)
437449
pytester.makepyfile("def test_pass(): pass")
438-
page = run(pytester, "report.html", "--self-contained-html")
450+
page = run(pytester, cmd_flags=["--self-contained-html"])
439451

440452
# element = page.select_one(".summary a[class='col-links__extra image']")
441453
src = f"data:{mime_type};base64,{data}"
@@ -463,7 +475,7 @@ def pytest_runtest_makereport(item, call):
463475
"""
464476
)
465477
pytester.makepyfile("def test_pass(): pass")
466-
page = run(pytester, "report.html", "--self-contained-html")
478+
page = run(pytester, cmd_flags=["--self-contained-html"])
467479

468480
# element = page.select_one(".summary a[class='col-links__extra video']")
469481
src = f"data:{mime_type};base64,{data}"
@@ -477,7 +489,7 @@ def pytest_runtest_makereport(item, call):
477489

478490
def test_xdist(self, pytester):
479491
pytester.makepyfile("def test_xdist(): pass")
480-
page = run(pytester, "report.html", "-n1")
492+
page = run(pytester, cmd_flags=["-n1"])
481493
assert_results(page, passed=1)
482494

483495
def test_results_table_hook_insert(self, pytester):
@@ -552,7 +564,7 @@ def test_streams(setup):
552564
assert True
553565
"""
554566
)
555-
page = run(pytester, "report.html", no_capture)
567+
page = run(pytester, "report.html", cmd_flags=[no_capture])
556568
assert_results(page, passed=1)
557569

558570
log = get_log(page)
@@ -657,3 +669,95 @@ def test_no_log(self, test_file, pytester):
657669
assert_that(log).contains("No log output captured.")
658670
for when in ["setup", "test", "teardown"]:
659671
assert_that(log).does_not_match(self.LOG_LINE_REGEX.format(when))
672+
673+
674+
class TestCollapsedQueryParam:
675+
@pytest.fixture
676+
def test_file(self):
677+
return """
678+
import pytest
679+
@pytest.fixture
680+
def setup():
681+
error
682+
683+
def test_error(setup):
684+
assert True
685+
686+
def test_pass():
687+
assert True
688+
689+
def test_fail():
690+
assert False
691+
"""
692+
693+
def test_default(self, pytester, test_file):
694+
pytester.makepyfile(test_file)
695+
page = run(pytester)
696+
assert_results(page, passed=1, failed=1, error=1)
697+
698+
assert_that(is_collapsed(page, "test_pass")).is_true()
699+
assert_that(is_collapsed(page, "test_fail")).is_false()
700+
assert_that(is_collapsed(page, "test_error::setup")).is_false()
701+
702+
@pytest.mark.parametrize("param", ["failed,error", "FAILED,eRRoR"])
703+
def test_specified(self, pytester, test_file, param):
704+
pytester.makepyfile(test_file)
705+
page = run(pytester, query_params={"collapsed": param})
706+
assert_results(page, passed=1, failed=1, error=1)
707+
708+
assert_that(is_collapsed(page, "test_pass")).is_false()
709+
assert_that(is_collapsed(page, "test_fail")).is_true()
710+
assert_that(is_collapsed(page, "test_error::setup")).is_true()
711+
712+
def test_all(self, pytester, test_file):
713+
pytester.makepyfile(test_file)
714+
page = run(pytester, query_params={"collapsed": "all"})
715+
assert_results(page, passed=1, failed=1, error=1)
716+
717+
for test_name in ["test_pass", "test_fail", "test_error::setup"]:
718+
assert_that(is_collapsed(page, test_name)).is_true()
719+
720+
@pytest.mark.parametrize("param", ["", 'collapsed=""', "collapsed=''"])
721+
def test_falsy(self, pytester, test_file, param):
722+
pytester.makepyfile(test_file)
723+
page = run(pytester, query_params={"collapsed": param})
724+
assert_results(page, passed=1, failed=1, error=1)
725+
726+
assert_that(is_collapsed(page, "test_pass")).is_false()
727+
assert_that(is_collapsed(page, "test_fail")).is_false()
728+
assert_that(is_collapsed(page, "test_error::setup")).is_false()
729+
730+
def test_render_collapsed(self, pytester, test_file):
731+
pytester.makeini(
732+
"""
733+
[pytest]
734+
render_collapsed = failed,error
735+
"""
736+
)
737+
pytester.makepyfile(test_file)
738+
page = run(pytester)
739+
assert_results(page, passed=1, failed=1, error=1)
740+
741+
assert_that(is_collapsed(page, "test_pass")).is_false()
742+
assert_that(is_collapsed(page, "test_fail")).is_true()
743+
assert_that(is_collapsed(page, "test_error::setup")).is_true()
744+
745+
def test_render_collapsed_precedence(self, pytester, test_file):
746+
pytester.makeini(
747+
"""
748+
[pytest]
749+
render_collapsed = failed,error
750+
"""
751+
)
752+
test_file += """
753+
def test_skip():
754+
pytest.skip('meh')
755+
"""
756+
pytester.makepyfile(test_file)
757+
page = run(pytester, query_params={"collapsed": "skipped"})
758+
assert_results(page, passed=1, failed=1, error=1, skipped=1)
759+
760+
assert_that(is_collapsed(page, "test_pass")).is_false()
761+
assert_that(is_collapsed(page, "test_fail")).is_false()
762+
assert_that(is_collapsed(page, "test_error::setup")).is_false()
763+
assert_that(is_collapsed(page, "test_skip")).is_true()

0 commit comments

Comments
 (0)