diff --git a/src/pytest_html/nextgen.py b/src/pytest_html/nextgen.py index e471fd1a..ab1d2883 100644 --- a/src/pytest_html/nextgen.py +++ b/src/pytest_html/nextgen.py @@ -36,6 +36,7 @@ def __init__(self, title, duration_format): self._data = { "title": title, "collectedItems": 0, + "runningState": "not_started", "durationFormat": duration_format, "environment": {}, "tests": [], @@ -122,6 +123,27 @@ def _data_content(self, *args, **kwargs): def _media_content(self, *args, **kwargs): pass + def _process_extras(self, report): + test_id = report.nodeid.encode("utf-8").decode("unicode_escape") + test_index = hasattr(report, "rerun") and report.rerun + 1 or 0 + report_extras = getattr(report, "extras", []) + for extra_index, extra in enumerate(report_extras): + content = extra["content"] + asset_name = self._asset_filename(test_id, extra_index, test_index, extra['extension']) + if extra["format_type"] == extras.FORMAT_JSON: + content = json.dumps(content) + extra["content"] = self._data_content(content, asset_name=asset_name, mime_type=extra["mime_type"]) + + if extra["format_type"] == extras.FORMAT_TEXT: + if isinstance(content, bytes): + content = content.decode("utf-8") + extra["content"] = self._data_content(content, asset_name=asset_name, mime_type=extra["mime_type"]) + + if extra["format_type"] == extras.FORMAT_IMAGE or extra["format_type"] == extras.FORMAT_VIDEO: + extra["content"] = self._media_content(content, asset_name=asset_name, mime_type=extra["mime_type"]) + + return report_extras + def _read_template(self, search_paths): env = Environment( loader=FileSystemLoader(search_paths), @@ -165,13 +187,15 @@ def pytest_sessionstart(self, session): if hasattr(config, "_metadata") and config._metadata: self._report.data["environment"] = self._generate_environment() - self._generate_report() session.config.hook.pytest_html_report_title(report=self._report) 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.data["runningState"] = "Started" + self._generate_report() + @pytest.hookimpl(trylast=True) def pytest_sessionfinish(self, session): session.config.hook.pytest_html_results_summary( @@ -179,6 +203,7 @@ def pytest_sessionfinish(self, session): summary=self._report.data["additionalSummary"]["summary"], postfix=self._report.data["additionalSummary"]["postfix"], ) + self._report.data["runningState"] = "Finished" self._generate_report() @pytest.hookimpl(trylast=True) @@ -191,7 +216,7 @@ def pytest_runtest_logreport(self, report): config=self._config, report=report ) - test_id = report.nodeid.encode("utf-8").decode("unicode_escape") + data["outcome"] = _process_outcome(report) row_cells = self.Cells() self._config.hook.pytest_html_results_table_row(report=report, cells=row_cells) @@ -201,25 +226,7 @@ def pytest_runtest_logreport(self, report): self._config.hook.pytest_html_results_table_html(report=report, data=table_html) data.update({"tableHtml": table_html}) - test_index = hasattr(report, "rerun") and report.rerun + 1 or 0 - - report_extras = getattr(report, "extras", []) - for extra_index, extra in enumerate(report_extras): - content = extra["content"] - asset_name = self._asset_filename(test_id, extra_index, test_index, extra['extension']) - if extra["format_type"] == extras.FORMAT_JSON: - content = json.dumps(content) - extra["content"] = self._data_content(content, asset_name=asset_name, mime_type=extra["mime_type"]) - - if extra["format_type"] == extras.FORMAT_TEXT: - if isinstance(content, bytes): - content = content.decode("utf-8") - extra["content"] = self._data_content(content, asset_name=asset_name, mime_type=extra["mime_type"]) - - if extra["format_type"] == extras.FORMAT_IMAGE or extra["format_type"] == extras.FORMAT_VIDEO: - extra["content"] = self._media_content(content, asset_name=asset_name, mime_type=extra["mime_type"]) - - data.update({"extras": report_extras}) + data.update({"extras": self._process_extras(report)}) self._report.data["tests"].append(data) self._generate_report() @@ -287,3 +294,15 @@ def _media_content(self, content, mime_type, *args, **kwargs): def _generate_report(self, *args, **kwargs): super()._generate_report(self_contained=True) + + +def _process_outcome(report): + if report.when in ["setup", "teardown"] and report.outcome == "failed": + return "Error" + if hasattr(report, "wasxfail"): + if report.outcome in ["passed", "failed"]: + return "XPassed" + if report.outcome == "skipped": + return "XFailed" + + return report.outcome.capitalize() diff --git a/src/pytest_html/scripts/datamanager.js b/src/pytest_html/scripts/datamanager.js index a74bae47..287be187 100644 --- a/src/pytest_html/scripts/datamanager.js +++ b/src/pytest_html/scripts/datamanager.js @@ -30,6 +30,9 @@ class DataManager { get durationFormat() { return this.renderData.durationFormat } + get isFinished() { + return this.data.runningState === 'Finished' + } } module.exports = { diff --git a/src/pytest_html/scripts/dom.js b/src/pytest_html/scripts/dom.js index 779f7ec9..afff1b6b 100644 --- a/src/pytest_html/scripts/dom.js +++ b/src/pytest_html/scripts/dom.js @@ -64,14 +64,14 @@ const dom = { return header }, getListHeaderEmpty: () => listHeaderEmpty.content.cloneNode(true), - getResultTBody: ({ nodeid, longrepr, duration, extras, resultsTableRow, tableHtml }, outcome) => { + getResultTBody: ({ nodeid, longrepr, duration, extras, resultsTableRow, tableHtml, outcome}) => { const outcomeLower = outcome.toLowerCase() const resultBody = templateResult.content.cloneNode(true) resultBody.querySelector('tbody').classList.add(outcomeLower) resultBody.querySelector('.col-result').innerText = outcome resultBody.querySelector('.col-name').innerText = nodeid resultBody.querySelector('.col-duration').innerText = `${formatDuration(duration)}s` - if (['failed', 'error', 'skipped', 'xfailed', 'xpassed'].includes(outcomeLower)) { + if (['failed', 'error', 'xfailed', 'xpassed'].includes(outcomeLower)) { resultBody.querySelector('.log').innerText = longrepr ? longrepr.reprtraceback.reprentries[0].data.lines.join('\n') : '' } else { diff --git a/src/pytest_html/scripts/main.js b/src/pytest_html/scripts/main.js index c713bef4..2e14a76d 100644 --- a/src/pytest_html/scripts/main.js +++ b/src/pytest_html/scripts/main.js @@ -11,21 +11,6 @@ const removeChildren = (node) => { } } -const getOutcome = ({ nodeid, wasxfail }, tests) => { - const relatedOutcome = tests - .filter((test) => test.nodeid === nodeid) - .map(({ outcome }) => outcome) - if (relatedOutcome.includes('failed')) { - return typeof wasxfail === 'undefined' ? 'Failed' : 'XPassed' - } else if (relatedOutcome.includes('error')) { - return 'Error' - } else if (relatedOutcome.includes('skipped')) { - return typeof wasxfail === 'undefined' ? 'Skipped' : 'XFailed' - } else { - return typeof wasxfail === 'undefined' ? 'Passed' : 'XPassed' - } -} - const renderStatic = () => { const title = manager.title const environment = manager.environment @@ -41,10 +26,10 @@ const renderStatic = () => { } const renderContent = (tests) => { - const renderSet = tests.filter(({ when }) => when === 'call') + const renderSet = tests.filter(({ when, outcome }) => when === 'call' || outcome === 'Error' ) const rows = renderSet.map((test) => - dom.getResultTBody(test, getOutcome(test, tests)) + dom.getResultTBody(test) ) const table = document.querySelector('#results-table') @@ -63,8 +48,8 @@ const renderContent = (tests) => { }) } -const renderDerived = (tests, collectedItems) => { - const renderSet = tests.filter(({ when }) => when === 'call') +const renderDerived = (tests, collectedItems, isFinished) => { + const renderSet = tests.filter(({ when, outcome }) => when === 'call' || outcome === 'Error') const possibleOutcomes = [ { outcome: 'passed', label: 'Passed' }, @@ -78,15 +63,7 @@ const renderDerived = (tests, collectedItems) => { const currentFilter = getFilter() possibleOutcomes.forEach(({ outcome, label }) => { - const count = renderSet.filter((test) => { - const wasXpassed = outcome === 'xpassed' && ['passed', 'failed'].includes(test.outcome) - const wasXfailed = outcome === 'xfailed' && test.outcome === 'skipped' - if (typeof test.wasxfail !== 'undefined') { - return wasXpassed || wasXfailed - } else { - return test.outcome === outcome - } - }).length + const count = renderSet.filter((test) => test.outcome.toLowerCase() === outcome).length const input = document.querySelector(`input[data-test-result="${outcome}"]`) document.querySelector(`.${outcome}`).innerText = `${count} ${label}` @@ -94,15 +71,18 @@ const renderDerived = (tests, collectedItems) => { input.checked = !currentFilter.includes(outcome) }) - if (collectedItems === renderSet.length) { + const numberOfTests = renderSet.filter(({outcome}) => + ['Passed', 'Failed', 'XPassed', 'XFailed'].includes(outcome) + ).length + if (isFinished) { const accTime = tests.reduce((prev, { duration }) => prev + duration, 0) const formattedAccTime = formatDuration(accTime) - const testWord = renderSet.length > 1 ? 'tests' : 'test' - const innerText = `${renderSet.length} ${testWord} ran in ${formattedAccTime} seconds.` + const testWord = numberOfTests > 1 ? 'tests' : 'test' + const innerText = `${numberOfTests} ${testWord} ran in ${formattedAccTime} seconds.` document.querySelector('.run-count').innerText = innerText document.querySelector('.summary__reload__button').classList.add('hidden') } else { - document.querySelector('.run-count').innerText = `${renderSet.length} / ${collectedItems} tests done` + document.querySelector('.run-count').innerText = `${numberOfTests} / ${collectedItems} tests done` } } @@ -128,13 +108,11 @@ const bindEvents = () => { } const renderPage = () => { - const filteredTests = manager.testSubset - const allTests = manager.allTests - const collectedItems = manager.collectedItems + const { testSubset, allTests, collectedItems, isFinished } = manager renderStatic() - renderContent(filteredTests) - renderDerived(allTests, collectedItems) + renderContent(testSubset) + renderDerived(allTests, collectedItems, isFinished) } const redraw = () => {