fix: Collapsed should support All and none (#605)
This commit is contained in:
parent
f381d082bc
commit
3306c1c09f
|
@ -248,15 +248,19 @@ Auto Collapsing Table Rows
|
|||
|
||||
By default, all rows in the **Results** table will be expanded except those that have :code:`Passed`.
|
||||
|
||||
This behavior can be customized either with a query parameter: :code:`?collapsed=Passed,XFailed,Skipped`
|
||||
or by setting the :code:`render_collapsed` in a configuration file (pytest.ini, setup.cfg, etc).
|
||||
This behavior can be customized with a query parameter: :code:`?collapsed=Passed,XFailed,Skipped`.
|
||||
If you want all rows to be collapsed you can pass :code:`?collapsed=All`.
|
||||
By setting the query parameter to empty string :code:`?collapsed=""` **none** of the rows will be collapsed.
|
||||
|
||||
Note that the query parameter is case insensitive, so passing :code:`PASSED` and :code:`passed` has the same effect.
|
||||
|
||||
You can also set the collapsed behaviour by setting the :code:`render_collapsed` in a configuration file (pytest.ini, setup.cfg, etc).
|
||||
Note that the query parameter takes precedence.
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
[pytest]
|
||||
render_collapsed = True
|
||||
|
||||
**NOTE:** Setting :code:`render_collapsed` will, unlike the query parameter, affect all statuses.
|
||||
render_collapsed = failed,error
|
||||
|
||||
Controlling Test Result Visibility Via Query Params
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
|
|
@ -71,6 +71,10 @@ class BaseReport:
|
|||
"additionalSummary": defaultdict(list),
|
||||
}
|
||||
|
||||
collapsed = config.getini("render_collapsed")
|
||||
if collapsed:
|
||||
self.set_data("collapsed", collapsed.split(","))
|
||||
|
||||
@property
|
||||
def title(self):
|
||||
return self._data["title"]
|
||||
|
|
|
@ -44,8 +44,8 @@ def pytest_addoption(parser):
|
|||
)
|
||||
parser.addini(
|
||||
"render_collapsed",
|
||||
type="bool",
|
||||
default=False,
|
||||
type="string",
|
||||
default="",
|
||||
help="Open the report with all rows collapsed. Useful for very large reports",
|
||||
)
|
||||
parser.addini(
|
||||
|
|
|
@ -2,7 +2,7 @@ const { getCollapsedCategory } = require('./storage.js')
|
|||
|
||||
class DataManager {
|
||||
setManager(data) {
|
||||
const collapsedCategories = [...getCollapsedCategory(), 'passed']
|
||||
const collapsedCategories = [...getCollapsedCategory(data.collapsed)]
|
||||
const dataBlob = { ...data, tests: Object.values(data.tests).flat().map((test, index) => ({
|
||||
...test,
|
||||
id: `test_${index}`,
|
||||
|
|
|
@ -3,7 +3,7 @@ const { dom, findAll } = require('./dom.js')
|
|||
const { manager } = require('./datamanager.js')
|
||||
const { doSort } = require('./sort.js')
|
||||
const { doFilter } = require('./filter.js')
|
||||
const { getVisible } = require('./storage.js')
|
||||
const { getVisible, possibleResults } = require('./storage.js')
|
||||
|
||||
const removeChildren = (node) => {
|
||||
while (node.firstChild) {
|
||||
|
@ -61,16 +61,6 @@ const renderContent = (tests) => {
|
|||
}
|
||||
|
||||
const renderDerived = (tests, collectedItems, isFinished) => {
|
||||
const possibleResults = [
|
||||
{ result: 'passed', label: 'Passed' },
|
||||
{ result: 'skipped', label: 'Skipped' },
|
||||
{ result: 'failed', label: 'Failed' },
|
||||
{ result: 'error', label: 'Errors' },
|
||||
{ result: 'xfailed', label: 'Unexpected failures' },
|
||||
{ result: 'xpassed', label: 'Unexpected passes' },
|
||||
{ result: 'rerun', label: 'Reruns' },
|
||||
]
|
||||
|
||||
const currentFilter = getVisible()
|
||||
possibleResults.forEach(({ result, label }) => {
|
||||
const count = tests.filter((test) => test.result.toLowerCase() === result).length
|
||||
|
|
|
@ -1,4 +1,13 @@
|
|||
const possibleFilters = ['passed', 'skipped', 'failed', 'error', 'xfailed', 'xpassed', 'rerun']
|
||||
const possibleResults = [
|
||||
{ result: 'passed', label: 'Passed' },
|
||||
{ result: 'skipped', label: 'Skipped' },
|
||||
{ result: 'failed', label: 'Failed' },
|
||||
{ result: 'error', label: 'Errors' },
|
||||
{ result: 'xfailed', label: 'Unexpected failures' },
|
||||
{ result: 'xpassed', label: 'Unexpected passes' },
|
||||
{ result: 'rerun', label: 'Reruns' },
|
||||
]
|
||||
const possibleFilters = possibleResults.map((item) => item.result)
|
||||
|
||||
const getVisible = () => {
|
||||
const url = new URL(window.location.href)
|
||||
|
@ -49,16 +58,29 @@ const setSort = (type) => {
|
|||
history.pushState({}, null, unescape(url.href))
|
||||
}
|
||||
|
||||
const getCollapsedCategory = () => {
|
||||
let categotries
|
||||
const getCollapsedCategory = (config) => {
|
||||
let categories
|
||||
if (typeof window !== 'undefined') {
|
||||
const url = new URL(window.location.href)
|
||||
const collapsedItems = new URLSearchParams(url.search).get('collapsed')
|
||||
categotries = collapsedItems?.split(',') || []
|
||||
switch (true) {
|
||||
case collapsedItems === null:
|
||||
categories = config || ['passed'];
|
||||
break;
|
||||
case collapsedItems?.length === 0 || /^["']{2}$/.test(collapsedItems):
|
||||
categories = [];
|
||||
break;
|
||||
case /^all$/.test(collapsedItems):
|
||||
categories = [...possibleFilters];
|
||||
break;
|
||||
default:
|
||||
categories = collapsedItems?.split(',').map(item => item.toLowerCase()) || [];
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
categotries = []
|
||||
categories = []
|
||||
}
|
||||
return categotries
|
||||
return categories
|
||||
}
|
||||
|
||||
const getSortDirection = () => JSON.parse(sessionStorage.getItem('sortAsc'))
|
||||
|
@ -75,4 +97,6 @@ module.exports = {
|
|||
setSort,
|
||||
setSortDirection,
|
||||
getCollapsedCategory,
|
||||
possibleFilters,
|
||||
possibleResults,
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import json
|
|||
import os
|
||||
import random
|
||||
import re
|
||||
import urllib.parse
|
||||
from base64 import b64encode
|
||||
from pathlib import Path
|
||||
|
||||
|
@ -26,9 +27,16 @@ OUTCOMES = {
|
|||
}
|
||||
|
||||
|
||||
def run(pytester, path="report.html", *args):
|
||||
def run(pytester, path="report.html", cmd_flags=None, query_params=None):
|
||||
if cmd_flags is None:
|
||||
cmd_flags = []
|
||||
|
||||
if query_params is None:
|
||||
query_params = {}
|
||||
query_params = urllib.parse.urlencode(query_params)
|
||||
|
||||
path = pytester.path.joinpath(path)
|
||||
pytester.runpytest("--html", path, *args)
|
||||
pytester.runpytest("--html", path, *cmd_flags)
|
||||
|
||||
chrome_options = webdriver.ChromeOptions()
|
||||
if os.environ.get("CI", False):
|
||||
|
@ -48,7 +56,7 @@ def run(pytester, path="report.html", *args):
|
|||
continue
|
||||
# End workaround
|
||||
|
||||
driver.get(f"file:///reports{path}")
|
||||
driver.get(f"file:///reports{path}?{query_params}")
|
||||
return BeautifulSoup(driver.page_source, "html.parser")
|
||||
finally:
|
||||
driver.quit()
|
||||
|
@ -91,6 +99,10 @@ def get_text(page, selector):
|
|||
return get_element(page, selector).string
|
||||
|
||||
|
||||
def is_collapsed(page, test_name):
|
||||
return get_element(page, f".summary tbody[id$='{test_name}'] .expander")
|
||||
|
||||
|
||||
def get_log(page, test_id=None):
|
||||
# TODO(jim) move to get_text (use .contents)
|
||||
if test_id:
|
||||
|
@ -267,7 +279,7 @@ class TestHTML:
|
|||
|
||||
def test_resources_inline_css(self, pytester):
|
||||
pytester.makepyfile("def test_pass(): pass")
|
||||
page = run(pytester, "report.html", "--self-contained-html")
|
||||
page = run(pytester, cmd_flags=["--self-contained-html"])
|
||||
|
||||
content = file_content()
|
||||
|
||||
|
@ -349,7 +361,7 @@ class TestHTML:
|
|||
)
|
||||
|
||||
pytester.makepyfile("def test_pass(): pass")
|
||||
page = run(pytester, "report.html", "--self-contained-html")
|
||||
page = run(pytester, cmd_flags=["--self-contained-html"])
|
||||
|
||||
element = page.select_one(".summary a[class='col-links__extra text']")
|
||||
assert_that(element.string).is_equal_to("Text")
|
||||
|
@ -374,7 +386,7 @@ class TestHTML:
|
|||
)
|
||||
|
||||
pytester.makepyfile("def test_pass(): pass")
|
||||
page = run(pytester, "report.html", "--self-contained-html")
|
||||
page = run(pytester, cmd_flags=["--self-contained-html"])
|
||||
|
||||
content_str = json.dumps(content)
|
||||
data = b64encode(content_str.encode("utf-8")).decode("ascii")
|
||||
|
@ -435,7 +447,7 @@ class TestHTML:
|
|||
"""
|
||||
)
|
||||
pytester.makepyfile("def test_pass(): pass")
|
||||
page = run(pytester, "report.html", "--self-contained-html")
|
||||
page = run(pytester, cmd_flags=["--self-contained-html"])
|
||||
|
||||
# element = page.select_one(".summary a[class='col-links__extra image']")
|
||||
src = f"data:{mime_type};base64,{data}"
|
||||
|
@ -463,7 +475,7 @@ class TestHTML:
|
|||
"""
|
||||
)
|
||||
pytester.makepyfile("def test_pass(): pass")
|
||||
page = run(pytester, "report.html", "--self-contained-html")
|
||||
page = run(pytester, cmd_flags=["--self-contained-html"])
|
||||
|
||||
# element = page.select_one(".summary a[class='col-links__extra video']")
|
||||
src = f"data:{mime_type};base64,{data}"
|
||||
|
@ -477,7 +489,7 @@ class TestHTML:
|
|||
|
||||
def test_xdist(self, pytester):
|
||||
pytester.makepyfile("def test_xdist(): pass")
|
||||
page = run(pytester, "report.html", "-n1")
|
||||
page = run(pytester, cmd_flags=["-n1"])
|
||||
assert_results(page, passed=1)
|
||||
|
||||
def test_results_table_hook_insert(self, pytester):
|
||||
|
@ -552,7 +564,7 @@ class TestHTML:
|
|||
assert True
|
||||
"""
|
||||
)
|
||||
page = run(pytester, "report.html", no_capture)
|
||||
page = run(pytester, "report.html", cmd_flags=[no_capture])
|
||||
assert_results(page, passed=1)
|
||||
|
||||
log = get_log(page)
|
||||
|
@ -657,3 +669,95 @@ class TestLogCapturing:
|
|||
assert_that(log).contains("No log output captured.")
|
||||
for when in ["setup", "test", "teardown"]:
|
||||
assert_that(log).does_not_match(self.LOG_LINE_REGEX.format(when))
|
||||
|
||||
|
||||
class TestCollapsedQueryParam:
|
||||
@pytest.fixture
|
||||
def test_file(self):
|
||||
return """
|
||||
import pytest
|
||||
@pytest.fixture
|
||||
def setup():
|
||||
error
|
||||
|
||||
def test_error(setup):
|
||||
assert True
|
||||
|
||||
def test_pass():
|
||||
assert True
|
||||
|
||||
def test_fail():
|
||||
assert False
|
||||
"""
|
||||
|
||||
def test_default(self, pytester, test_file):
|
||||
pytester.makepyfile(test_file)
|
||||
page = run(pytester)
|
||||
assert_results(page, passed=1, failed=1, error=1)
|
||||
|
||||
assert_that(is_collapsed(page, "test_pass")).is_true()
|
||||
assert_that(is_collapsed(page, "test_fail")).is_false()
|
||||
assert_that(is_collapsed(page, "test_error::setup")).is_false()
|
||||
|
||||
@pytest.mark.parametrize("param", ["failed,error", "FAILED,eRRoR"])
|
||||
def test_specified(self, pytester, test_file, param):
|
||||
pytester.makepyfile(test_file)
|
||||
page = run(pytester, query_params={"collapsed": param})
|
||||
assert_results(page, passed=1, failed=1, error=1)
|
||||
|
||||
assert_that(is_collapsed(page, "test_pass")).is_false()
|
||||
assert_that(is_collapsed(page, "test_fail")).is_true()
|
||||
assert_that(is_collapsed(page, "test_error::setup")).is_true()
|
||||
|
||||
def test_all(self, pytester, test_file):
|
||||
pytester.makepyfile(test_file)
|
||||
page = run(pytester, query_params={"collapsed": "all"})
|
||||
assert_results(page, passed=1, failed=1, error=1)
|
||||
|
||||
for test_name in ["test_pass", "test_fail", "test_error::setup"]:
|
||||
assert_that(is_collapsed(page, test_name)).is_true()
|
||||
|
||||
@pytest.mark.parametrize("param", ["", 'collapsed=""', "collapsed=''"])
|
||||
def test_falsy(self, pytester, test_file, param):
|
||||
pytester.makepyfile(test_file)
|
||||
page = run(pytester, query_params={"collapsed": param})
|
||||
assert_results(page, passed=1, failed=1, error=1)
|
||||
|
||||
assert_that(is_collapsed(page, "test_pass")).is_false()
|
||||
assert_that(is_collapsed(page, "test_fail")).is_false()
|
||||
assert_that(is_collapsed(page, "test_error::setup")).is_false()
|
||||
|
||||
def test_render_collapsed(self, pytester, test_file):
|
||||
pytester.makeini(
|
||||
"""
|
||||
[pytest]
|
||||
render_collapsed = failed,error
|
||||
"""
|
||||
)
|
||||
pytester.makepyfile(test_file)
|
||||
page = run(pytester)
|
||||
assert_results(page, passed=1, failed=1, error=1)
|
||||
|
||||
assert_that(is_collapsed(page, "test_pass")).is_false()
|
||||
assert_that(is_collapsed(page, "test_fail")).is_true()
|
||||
assert_that(is_collapsed(page, "test_error::setup")).is_true()
|
||||
|
||||
def test_render_collapsed_precedence(self, pytester, test_file):
|
||||
pytester.makeini(
|
||||
"""
|
||||
[pytest]
|
||||
render_collapsed = failed,error
|
||||
"""
|
||||
)
|
||||
test_file += """
|
||||
def test_skip():
|
||||
pytest.skip('meh')
|
||||
"""
|
||||
pytester.makepyfile(test_file)
|
||||
page = run(pytester, query_params={"collapsed": "skipped"})
|
||||
assert_results(page, passed=1, failed=1, error=1, skipped=1)
|
||||
|
||||
assert_that(is_collapsed(page, "test_pass")).is_false()
|
||||
assert_that(is_collapsed(page, "test_fail")).is_false()
|
||||
assert_that(is_collapsed(page, "test_error::setup")).is_false()
|
||||
assert_that(is_collapsed(page, "test_skip")).is_true()
|
||||
|
|
|
@ -156,3 +156,69 @@ describe('utils tests', () => {
|
|||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Storage tests', () => {
|
||||
describe('getCollapsedCategory', () => {
|
||||
let originalWindow
|
||||
const mockWindow = (queryParam) => {
|
||||
const mock = {
|
||||
location: {
|
||||
href: `https://example.com/page?${queryParam}`
|
||||
}
|
||||
}
|
||||
originalWindow = global.window
|
||||
global.window = mock
|
||||
}
|
||||
after(() => global.window = originalWindow)
|
||||
|
||||
it('collapses passed by default', () => {
|
||||
mockWindow()
|
||||
const collapsedItems = storageModule.getCollapsedCategory()
|
||||
expect(collapsedItems).to.eql(['passed'])
|
||||
})
|
||||
|
||||
it('collapses specified outcomes', () => {
|
||||
mockWindow('collapsed=failed,error')
|
||||
const collapsedItems = storageModule.getCollapsedCategory()
|
||||
expect(collapsedItems).to.eql(['failed', 'error'])
|
||||
})
|
||||
|
||||
it('collapses all', () => {
|
||||
mockWindow('collapsed=all')
|
||||
const collapsedItems = storageModule.getCollapsedCategory()
|
||||
expect(collapsedItems).to.eql(storageModule.possibleFilters)
|
||||
})
|
||||
|
||||
it('handles case insensitive params', () => {
|
||||
mockWindow('collapsed=fAiLeD,ERROR,passed')
|
||||
const collapsedItems = storageModule.getCollapsedCategory()
|
||||
expect(collapsedItems).to.eql(['failed', 'error', 'passed'])
|
||||
})
|
||||
|
||||
it('handles python config', () => {
|
||||
mockWindow()
|
||||
const collapsedItems = storageModule.getCollapsedCategory(['failed', 'error'])
|
||||
expect(collapsedItems).to.eql(['failed', 'error'])
|
||||
})
|
||||
|
||||
it('handles python config precedence', () => {
|
||||
mockWindow('collapsed=xpassed,xfailed')
|
||||
const collapsedItems = storageModule.getCollapsedCategory(['failed', 'error'])
|
||||
expect(collapsedItems).to.eql(['xpassed', 'xfailed'])
|
||||
})
|
||||
|
||||
const falsy = [
|
||||
{ param: 'collapsed' },
|
||||
{ param: 'collapsed=' },
|
||||
{ param: 'collapsed=""' },
|
||||
{ param: 'collapsed=\'\'' }
|
||||
]
|
||||
falsy.forEach(({param}) => {
|
||||
it(`collapses none with ${param}`, () => {
|
||||
mockWindow(param)
|
||||
const collapsedItems = storageModule.getCollapsedCategory()
|
||||
expect(collapsedItems).to.be.empty
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
Loading…
Reference in New Issue