VS Code extension
The extension reads the same output.json the plugin already writes and renders it beside your code: failures grouped by suite, a readable error on every card, and one click from a failure to the def that produced it.
What it is
Pytest HTML Reporter for VS Code is a read-only viewer for reports produced by the
pytest-html-reporter pytest plugin. Its single data source is the plugin's
output.json, plus the rotated copies the plugin keeps in archive/. It never runs
pytest, and it never scrapes the generated HTML for data.
The problem it solves is the walk between the report and the code. A generated report lives in a
browser tab: you read a failure there, remember the suite and the test name, switch windows, open the
file, and search for the function. The extension collapses that into a click. The same run summary,
the same failures, the same error text — in the activity bar, one click from the def.
- Failures at a glanceEvery failing test grouped under its suite, with a truncated error line on each card.
-
Jump to the testClick a failure to open the suite file with the cursor on the matching
def. - Copy the real errorThe card shows the ANSI-stripped text; the clipboard gets the original, terminal colouring intact.
-
Flaky versus brokenPast builds in
archive/separate a test that sometimes fails from one that has never passed. - Live refreshThe sidebar reloads when a run finishes and rewrites the report file.
- The full report, in the editorOpen the generated HTML — Steps, Attachments, Analytics, Coverage, screenshots — in a tab instead of a browser.
What it deliberately does not do
It does not run your tests. The Python extension's Test Explorer already does that, and
this extension never shells out to pytest. It does not reproduce Steps, Attachments, logs, BDD
scenarios or per-file coverage either — those are the HTML report's job, and the sidebar links out to
it rather than rebuilding it. Every number in the sidebar comes from output.json; the
HTML panel is display only and feeds nothing back.
output.json has been written — run pytest again.Installing
The extension is published as prashanth-sams.pytest-html-reporter-vscode — publisher
prashanth-sams, display name Pytest HTML Reporter, category Testing. It
requires VS Code ^1.90.0 and activates on onStartupFinished, so nothing else
is needed after install: open a folder that contains a report and the view populates itself.
From the Extensions view
-
Open the Extensions view
Ctrl+Shift+Xon Windows and Linux,Cmd+Shift+Xon macOS. -
Search for
Pytest HTML ReporterInstall the entry published by prashanth-sams. The icon is the reporter's own bar-chart mark.
-
Open the view
Click the Pytest HTML Reporter icon in the activity bar. The view inside it is titled Test Results.
From the command line, Quick Open, or a VSIX
$ code --install-extension prashanth-sams.pytest-html-reporter-vscode
ext install prashanth-sams.pytest-html-reporter-vscode
$ code --install-extension pytest-html-reporter-vscode-0.1.2.vsix
$ npm install
$ npm run build
The shipped artifact is a single esbuild bundle (dist/extension.js) plus the icons and
docs; src/, tests/, tools/ and .preview/ are kept
out of the package.
Requirements: a report to read
The extension displays what the plugin writes, so the plugin has to have run. Install
pytest-html-reporter into the environment your tests run in and pass
--html-report. The full path from nothing to a report is on
Getting started, and every flag is on the
CLI reference.
$ pip install pytest-html-reporter
$ pytest --html-report=./report
Around the output.json it resolves, the extension expects — and reads — exactly this
layout. <report dir> is simply the directory containing that file; nothing about the
location is hard-coded beyond the discovery order below.
<report dir>/output.json the current run
<report dir>/archive/output_<timestamp>.json past runs, the history window
<report dir>/pytest_html_report.html the full HTML report
<report dir>/pytest_screenshots/ screenshots referenced by the HTML
output.json is required. A missing archive/ just means the history window is one build long, and a missing HTML report only removes the footer button.The sidebar, top to bottom
The view is a webview called Test Results, inside the Pytest HTML Reporter activity bar container. It keeps its context when hidden, so switching away and back does not re-parse the report.
results state, dark theme — open .preview/preview-results-dark.html from the extension repo and capture it at sidebar width (roughly 340px). It must show the summary panel with the 98% pass-rate ring, the proportional distribution bar, the tinted count chips (664 passed, 14 failed, 1 skipped, 1 xpassed, 1 xfailed), the "14 failing tests" heading, and at least the first two suite groups with their failure cards and error snippets.
1. Report switcher
A <select> at the very top, rendered only when two or more reports are
available. Each option is labelled with the last two path segments — report/output.json —
so report/ and reports/ stay distinguishable; the value is the full path.
Choosing one sets it as the active report and reloads.
2. Summary panel
A bordered card holding five things.
- Header. The label "Test Summary", and on the right the run's date exactly as the
plugin wrote it —
September 02, 2026. - Pass-rate ring. A 64px SVG donut. The rate is
(passed + xpassed + xfailed) / (passed + xpassed + xfailed + failed + error)— skips are excluded from both sides, because a skipped test never earned a verdict. The tone is green whenfailed + erroris zero, yellow at a rate of 0.9 or better, red otherwise. Itsaria-labelreads "N percent of tests passed". - Hero facts. The total test count in large type, then a muted sub-line carrying the summed duration (only when at least one test in the build was timed) and "N builds tracked" (only when more than one build of history loaded).
- Distribution bar. One 6px proportional bar; segment order pass, xpass, xfail,
skip, error, fail, with zero-count statuses omitted. Any segment under 2.5% is lifted to that
minimum and the whole row rescaled to 100%, so a single failure in seven hundred tests stays visible
instead of rendering at 0.14% of the width. Each segment's tooltip reads like
14 failed. - Count chips. One tinted pill per status — passed, failed, error, skipped,
xpassed, xfailed, reruns.
passedandfailedare always drawn even at zero; the other five are omitted when zero. Each chip carries its own background, border and text colour mixed from the matching theme variable, so the status reads from the colour and not only from the word.
The ring rounds plainly, so a run one test short of perfect reads 100% on the dial, with the tone
and the count chips carrying the rest of the truth beside it. The percentage that is guarded in
both directions is the pass rate on a flaky · P% badge: it renders
99% rather than a 100% the badge itself contradicts, and 1% rather than
a 0% a test that has passed never earned.
3. Failing tests, grouped by suite
A section title reads N failing tests, with a muted N builds of history on
the right when more than one build loaded. Below it, only suites containing at least one FAIL or ERROR
are rendered, in report order. Each group header is clickable, shows the suite path and a red pill with
the count of failing tests in it, and toggles the group body. Long suite paths elide at the
front, because the filename is what identifies a suite.
4. Test cards
One card per failing test — FAIL and ERROR only; an expected failure is not a failure. The whole
card is the jump target. It carries the test name as pytest wrote it, parametrization suffix included
(test_heading[chromium]), a status badge, a meta row of badges, and the error snippet on a
neutral plate with a red left edge.
| Badge | When it appears | What it says |
|---|---|---|
| Duration | Always | not measured when the record carries no duration, <1ms under a millisecond, 10ms under a second, 3.42s under a minute, 1m 0s above. |
| Retries | Rerun count above zero | N retries on an orange plate. |
| broken | Failures and zero passes across at least 2 builds | broken · 0/N on a red plate; tooltip "Never passed in N builds". |
| flaky | The test both passed and failed in the window, or was retried within a build while also passing | flaky · P% on a yellow plate; tooltip "F flips across N builds". |
| Trend strip | At least 2 recorded outcomes, and not broken | The last 12 outcomes as coloured ticks, oldest to newest. |
| copy error | Always | A text button; puts the raw message on the clipboard without triggering the card's jump. |
not measured is not zero. Archives written before plugin 0.3.7 carry no duration at all, and defaulting those to zero would report a suite of instantaneous tests. A measured zero is real and renders <1ms.5. Footer
An Open full HTML report button, rendered only when an HTML report was actually
found next to output.json.
Theming
Every colour in the panel is a VS Code theme variable with a hard-coded fallback, so it follows
light, dark and high-contrast themes rather than carrying its own palette. Status colours come from the
--vscode-testing-* family — the same one the built-in Test Explorer uses — plus the chart
colours for xPASS, xFAIL, reruns and warnings. prefers-reduced-motion disables the loading
spinner's animation and the donut transition.
results state in the Light+ theme — .preview/preview-results-light.html at the same sidebar width and the same scroll position as the dark capture, so the two can sit side by side and show the panel following the editor theme.
The six sidebar states
Every load picks exactly one of these and renders it whole.
| State | When it is shown | What the user sees |
|---|---|---|
loading | At the start of every load, auto-refresh included | A spinner and "Loading test results…". |
no-config | No configured path exists and detection found nothing | Clipboard icon, "No report found", the hint pytest --html-report=./report in a code chip, and the buttons Configure report path and Refresh. |
error | Reading or parsing the resolved output.json threw | Red alert icon, "Unable to load report", the exception message, and the buttons Retry and Configure report path. |
empty | The report parsed but records zero tests | Report switcher, clipboard icon, "No tests in this report", and a Refresh button. |
all-passed | Parsed, has tests, no FAIL or ERROR | Report switcher, the full summary panel, a green check, "All tests passed", "N tests completed with no failures.", and the footer. |
results | Parsed, has tests, at least one FAIL or ERROR | Report switcher, summary panel, failing-test count, collapsible suite groups of failure cards, and the footer. |
all-passed state, dark theme, at sidebar width: the summary panel above a green check icon, "All tests passed" and "N tests completed with no failures.", with the "Open full HTML report" footer button visible. Regenerate it first with PREVIEW_REPORT pointed at a genuinely green run — the checked-in .preview/preview-all-passed-dark.html renders the green panel over the failure fixture's summary, which reads 14 failed.
Jump to the test
This is the one feature the report format makes hard, and it is worth knowing why.
output.json records no line numbers and no nodeid. Each result carries
only suite_name — a workspace-relative .py path — and test_name,
which is pytest's item.name: the function name plus any [param] suffix, with
the class segment of the nodeid already discarded. So the file comes from the suite name, and the line
is recovered by scanning that file.
-
Click anywhere on a failure card
The card posts its index back to the extension host, which runs the jump command with the suite name and test name.
-
The suite path resolves against each workspace folder
Absolute paths and Windows drive paths are used as-is; a leading
./is stripped. If none of them stat, the basename is looked up with a bounded workspace search and the first hit is used. If nothing is found:Could not find <suite> in this workspace. -
The file is scanned for the matching def
The test name is cut at the first
[, because a parametrized id can itself contain brackets —test_a_status_is_grouped_by_its_class[200-2xx]. Everydef name(andasync def name(at any indentation is collected in source order. -
One match jumps; several offer a pick list
Two or more definitions open a Quick Pick titled
<name> is defined N times in this file, placeholder "The report does not record which one ran — pick a definition", each entry labelledLine Nwith the trimmed source line beside it. Cancelling cancels the jump. -
The editor opens and the line flashes
Non-preview, cursor at column 0, revealed in the centre if it was outside the viewport, and the whole line highlighted with the find-match colour for 1800 ms.
Matching is exact on the name, so test_heading never picks up
test_heading_mismatch. Decorators are handled for free: only a line whose first non-whitespace
token is def or async can match, so a @pytest.mark.parametrize(...)
above the function is skipped and the def is what comes back.
def written inside a triple-quoted string can match. That is an accepted trade against shipping a Python parser: a mis-jump into a string literal costs one keystroke.If the test has been renamed or deleted since the run, you get
Could not find <test> in <suite> — it may have been renamed since this run. and
the file still opens at line 1, because the file is still the right place to land.
Copy the real error
The plugin stores message exactly as pytest rendered it for a terminal, ANSI escape
codes included — a real record contains runs like [91m and [39;49;00m in the
middle of an assertion diff. So each parsed result keeps both forms: the raw message and a stripped
one.
The card shows the stripped text, because a sidebar full of literal escape codes is unreadable. The copy error button puts the original on the clipboard, so pasting it into a terminal reproduces pytest's own red-and-green colouring. A toast confirms with "Error copied to clipboard." A test with no message copies nothing.
The snippet on the card is chosen, not truncated blindly: whitespace is condensed to single spaces,
then the first SomeError: / SomeException: / SomeFailure: line is
preferred, falling back to the head of the message for a bare assert False or a collection
error. Anything longer than maxErrorLength is hard-cut with a trailing ellipsis.
Build history, flaky and broken
This is the part no single report can give, and the reason the extension reads archive/
at all. Every *.json in <report dir>/archive is read, plus the current
output.json, through a cheap parse that keeps only counters and per-test outcomes —
messages and snippets, by far the biggest part of a record, are dropped so a window of builds is cheap
to hold. Files that cannot be read, or are not reports, are counted and skipped; they never fail the
load. A missing archive/ directory is fine: a first run has a history of exactly itself.
Ordering is by each file's inner start_time, never by its filename. The
plugin names an archived file for the run that displaced it, not the run inside it, so sorting
on the name shifts the whole history by one build and scrambles any trend read off it. The extension
has a dedicated regression test for exactly this.
After sorting oldest-first, the newest history.maxBuilds builds are kept and the rest
are reported as omitted rather than silently dropped. Cross-build identity is
suite_name::test_name — the only identity the archives carry.
The verdict rules
They are ported from the plugin's own analytics, so a verdict in the sidebar and a verdict on the Analytics tab agree.
- Only
FAILandERRORcount as failures.xFAILandxPASSsit on the pass side — both were declared in advance, and counting an expected failure against a test's pass rate would make every xfail-marked test look like the least reliable thing in the suite. - A test that alternates is flaky. A test that has never passed is broken, not flaky, because there is nothing intermittent about it.
- A retry inside a single build is enough to be flaky on its own, even if every build ended green.
- Skips neither flip an outcome nor enter the pass rate — a pass, a skip and a fail is 50%, not 33%. A test that was only ever skipped has no decided outcome at all, so it is neither flaky nor broken and carries no badge.
A flake badge is drawn only when history actually says something: the test appears in at least 2
builds and is either broken or flaky. A stable test gets no badge, so the ones that do mean something.
The trend strip needs at least 2 recorded outcomes and is skipped entirely for a broken test, whose
0/N badge and row of identical red ticks would state the same fact twice.
npm run preview output and not in the sidebar itself. Badges and trend strips are unaffected.The status bar item
One left-aligned status bar item mirrors the headline count and updates on every load.
- Hidden when no report is loaded, or when the report records zero tests.
- With failures:
$(error) N failing, where N isfailed + error, on the theme's error background. - Without failures:
$(pass) N passing. - Tooltip in both cases:
pytest-html-reporter — P passed, B failed of T. - Clicking it focuses the Test Results view.
The full HTML report, in an editor tab
Pytest HTML Reporter: Open Full HTML Report — the browser icon in the view title bar, or the footer button — opens the plugin's generated report inside VS Code. It exists because the generated report has tabs the sidebar deliberately does not reproduce: Steps, Attachments, Analytics, Coverage and screenshots. See the report tour for what is on each of them.
- One panel is created and then reused — titled pytest HTML Report. A second invocation reveals the existing panel instead of opening another.
- The report links screenshots as
pytest_screenshots/<file>, which resolves to nothing inside a webview. Thosesrcandhrefreferences — and only those — are rewritten to webview URIs, so captured screenshots load in the tab. Everything else in the file is already inlined by the plugin. - With no HTML report beside
output.jsonyou get a warning instead: "No HTML report found next to output.json. Run pytest with --html-report to generate one."
Nothing in this panel feeds the sidebar. It is display only.
Command reference
Five commands, all under the palette category Pytest HTML Reporter — so in the
Command Palette they read as Pytest HTML Reporter: <title>. Three of them are also
pinned to the view's title bar, in this navigation order: refresh, open HTML report, configure path.
| Command id | Palette title | What it does |
|---|---|---|
pytestHtmlReporter.refresh |
Refresh Test Results | Re-resolves the report path and reloads the sidebar. Title-bar icon $(refresh). Also reachable from the Refresh buttons in the no-config and empty states, and from Retry in the error state. |
pytestHtmlReporter.openHtmlReport |
Open Full HTML Report | Opens the generated HTML report in a reusable editor-tab webview. Warns when no HTML report sits next to output.json. Title-bar icon $(browser). |
pytestHtmlReporter.configureReportPath |
Configure Report Path | Opens a Quick Pick of detected reports plus Browse… and, when paths are already configured, Clear configured paths. Title-bar icon $(gear). |
pytestHtmlReporter.jumpToTest |
Jump To Test hidden | Takes a suite name and a test name, opens the suite file and moves the cursor to the matching def. Invoked by clicking a failure card; hidden from the palette because it needs arguments. |
pytestHtmlReporter.focusSidebar |
Focus Test Results View hidden | Focuses the Test Results view. Hidden from the palette; it exists so the status bar item has something to click. |
The three visible commands are gated on there being at least one workspace folder open.
Configuration reference
Six settings, all under the pytestHtmlReporter section and all scoped to a resource, so
they can be set per workspace folder. The Settings UI groups them under Pytest HTML
Reporter. Changing any of them reloads the sidebar.
| Setting | Type | Default | What it does |
|---|---|---|---|
pytestHtmlReporter.reportJsonPaths |
array of string | [] |
Paths to output.json files written by pytest-html-reporter. Empty means detect them automatically; non-empty makes this list the only source of reports. |
pytestHtmlReporter.autoRefresh |
boolean | true |
Reload the sidebar when the report file changes on disk. When false no watcher is installed at all; the Refresh command still works. |
pytestHtmlReporter.showErrorSnippets |
boolean | true |
Show a truncated error message on each failing test. False suppresses both the computation of the snippet during parsing and the block on the card. |
pytestHtmlReporter.maxErrorLength |
number, 50–500 | 150 |
Maximum length of the error snippet on a failing test. Clamped both by the manifest and again at read time, so a hand-edited settings.json cannot push it out of range. |
pytestHtmlReporter.history.enabled |
boolean | true |
Read past builds from archive/*.json to flag flaky and consistently broken tests. False means no flake badges, no trend strips and no build-count annotations. |
pytestHtmlReporter.history.maxBuilds |
number, 2–200 | 25 |
How many recent builds to read from archive/ when working out flakiness. The window keeps the newest N after sorting by inner start_time. |
{
"pytestHtmlReporter.reportJsonPaths": ["/Users/you/project/report/output.json"],
"pytestHtmlReporter.autoRefresh": true,
"pytestHtmlReporter.showErrorSnippets": true,
"pytestHtmlReporter.maxErrorLength": 200,
"pytestHtmlReporter.history.enabled": true,
"pytestHtmlReporter.history.maxBuilds": 50
}
How it finds output.json
Per workspace folder, best candidate first:
<workspace>/output.json— the plugin's default, because it writes relative to pytest's working directory<workspace>/report/output.json<workspace>/reports/output.json<workspace>/test-reports/output.json<workspace>/.reports/output.json<workspace>/test-results/output.json- A bounded search for
**/output.json, excludingnode_modules,.venv,venvandsite-packages, capped at 20 results
Duplicates are removed by absolute path and the order above is kept, so conventional locations always come before search hits.
Every candidate is shape-checked before it is offered. The file is read, parsed, and
required to have a content.suites object. output.json is a common filename and
a Python repo may hold several with nothing to do with pytest; rendering an unrelated one as test
results would be worse than finding nothing. A file that fails the check, or fails to parse, is silently
not offered.
Which report is active is the currently active one if it is still available, otherwise the
first available one. When two or more are available the switcher dropdown appears at the top of the
sidebar. When there are none at all, the sidebar shows the no-config state.
no-config state, dark theme, at sidebar width — .preview/preview-no-config-dark.html. It must show the clipboard icon, the "No report found" heading, the pytest --html-report=./report code chip, and both buttons: Configure report path and Refresh.
Overriding detection
When this list is non-empty, only these paths are considered — detection is skipped entirely.
Each entry is checked for existence, and any that no longer exist are pruned back into workspace
settings, so a report deleted between runs stops haunting the switcher. If every configured path is
dead, the extension falls back to auto-detection rather than showing nothing. Entries are used exactly
as written: nothing expands ${workspaceFolder} and nothing resolves a relative path
against the workspace folder, so write each one in full — Configure Report Path
writes them that way itself.
{
"pytestHtmlReporter.reportJsonPaths": [
"/Users/you/project/report/output.json",
"/Users/you/project/e2e/report/output.json"
]
}
The Configure Report Path flow
Pytest HTML Reporter: Configure Report Path — also the gear icon in the view title bar, and the primary button on the no-report and error states — opens a Quick Pick titled pytest-html-reporter: report path with:
- every auto-detected report, labelled with its last two path segments, described by its full path, and marked Currently configured when it is already in the setting;
- Browse…, which opens a file dialog filtered to
jsonwith the open-label "Use this report"; - Clear configured paths, offered only when paths are currently configured, which goes back to automatic detection.
Whichever branch runs, the setting is written as a single-element array — or an empty one for Clear — targeting workspace settings when a folder is open and user settings otherwise, and the sidebar refreshes. Dismissing the pick changes nothing.
Finding the HTML report beside it
The HTML report is located by looking, never by predicting:
pytest_html_report.html in the same directory if it exists, otherwise the single
*.html file there if there is exactly one. Zero or two-plus candidates mean no HTML report
and the footer button is not rendered.
The reason is that --html-report can rename the file, and a path holding
strftime placeholders is expanded inside the pytest process against that process's clock —
so recomputing the name here would disagree with disk whenever a run crosses a minute boundary.
Auto-refresh and file watching
The sidebar reloads when the view is first resolved, when the Refresh command runs, when the active
report file changes on disk, when you switch reports in the dropdown, when workspace folders change,
and when any pytestHtmlReporter setting changes.
The watch. Once a report path resolves, a watcher is installed on that one file,
replacing any previous one. It is skipped entirely when autoRefresh is false. Each change
event resets a 300 ms debounce; when the timer fires, the sidebar reloads. Watcher errors stop the
watch silently — watching is a convenience, and the manual refresh still works.
The settle poll. Before every read, the extension waits for the file to stop growing:
it stats the file every 120 ms, up to 40 times, and reads as soon as two consecutive stats report
the same non-zero size. The plugin writes output.json in one pass at the very end of a run
and a real suite's file runs to megabytes, so a watcher that read on the first change event would
routinely catch a half-written file and report a parse error for a run that was perfectly fine.
Re-entrancy. A load already in flight makes the next one return immediately, so a burst of change events cannot stack up parses of the same file.
| Constant | Value | What it controls |
|---|---|---|
WATCH_DEBOUNCE_MS | 300 | Delay after the last file event before reloading. |
SETTLE_POLL_MS | 120 | Interval between size checks while waiting for the file to stop growing. |
SETTLE_MAX_POLLS | 40 | Maximum size checks — about 4.8 s — before reading anyway. |
What the extension reads, and what it recounts
Tolerance is a stated design goal. The plugin has been through releases that added fields
(duration in 0.3.7, coverage in 0.3.6), so an archive directory holds records
of mixed vintage, and a run interrupted partway leaves a file that is valid JSON but internally
inconsistent. A file that cannot be made sense of is skipped rather than allowed to take the sidebar
down.
- Required:
content.suites. Its absence is what makes a file "not a pytest-html-reporter report". - Read per suite:
suite_name,tests, and the per-suite counter block. - Read per test:
test_name,status,message,rerun,duration. - Deliberately not trusted: the file's own
status_list,total_testsandtotal_suite. The first two are serialized as strings, and all three are written from separate accumulators, so an interrupted or xdist run can leave them disagreeing with the records underneath. Summary counters are recounted from the records, which are the ground truth you are about to be shown.
Two coercions worth knowing: suite and test maps are JSON objects keyed by stringified integers, so
keys are sorted numerically to restore the run's real collection order; and an unrecognised
status reads as PASS, on the grounds that inventing a failure the run never reported is the
worse error.
Report text is never executed
Every string that came out of a report — suite name, test name, error text, file path — is HTML-escaped
before it reaches the markup, in both quote forms, so it is equally safe inside an attribute. No handler
is ever written with report data interpolated into it: rows carry a numeric index and listeners are
attached by delegation, so nothing from the report is ever parsed as code. The webview's content policy
is default-src 'none' plus inline style and script, which are generated in the
same string and shipped with it, so no report content can pull in a remote resource.
Rendering the sidebar without VS Code
The extension repo ships a preview harness that renders the exact markup the webview gets into
standalone HTML under .preview/ — one dark file and one light file per scenario. It is how
the screenshots on this page are produced, and how a style change is checked without launching an
Extension Development Host.
$ npm run preview # results -> .preview/preview-results-{dark,light}.html
$ npm run preview -- all-passed # all-passed -> .preview/preview-all-passed-{dark,light}.html
$ npm run preview -- no-config # no-config -> .preview/preview-no-config-{dark,light}.html
$ npm run preview -- error # error -> .preview/preview-error-{dark,light}.html
The real Dark+ and Light+ theme variables are stubbed in, so what the browser shows is what the
webview renders. Point the harness at one of your own runs to preview that run's real
archive/ too — which is how you get captures that include flake badges, trend strips and the
failures-per-build chart.
$ PREVIEW_REPORT=/path/to/report/output.json npm run preview
Troubleshooting
error state, dark theme, at sidebar width — .preview/preview-error-dark.html. It must show the red alert icon, the "Unable to load report" heading, the message "Invalid report format: Unexpected token", and both buttons: Retry and Configure report path.
The sidebar is empty — it says "No report found"
That state means no configured path exists and detection found nothing, so start by
confirming a report exists at all: is there an output.json anywhere under the folder you
opened? If not, run pytest --html-report=./report — see
Getting started.
If the file does exist, there are three usual reasons detection missed it. It lives outside the
six conventional locations and outside the bounded search, which skips
node_modules, .venv, venv and site-packages and
stops at 20 hits. Or your report directory sits outside the workspace folder entirely — pytest was run
from elsewhere. Or the file is not a pytest-html-reporter report: every candidate is required to have a
content.suites object, and anything else is silently not offered.
In all three cases the fix is the same: run Pytest HTML Reporter: Configure Report Path and browse to the file.
The sidebar shows "No tests in this report"
The file parsed, but the recounted total is zero — the run collected nothing. That is a pytest collection problem rather than an extension one: check the paths and markers you passed to pytest, and that the run you are looking at is the one you think it is (the summary panel prints the run's date, and the switcher shows which report is active).
"Unable to load report" with a parse error
Reading or parsing the resolved output.json threw, and the message under the heading is
the exception. If it appeared during a run, press Retry: the plugin writes the file in
one pass at the end of a run, and although the extension waits for the size to stabilise before reading,
a file still being written is the most common cause.
If it persists, the file itself is truncated or corrupt — an interrupted run, or a partial copy. Re-run pytest to write a fresh one.
The sidebar does not update when a run finishes
Check pytestHtmlReporter.autoRefresh — when it is false, no watcher is installed at
all. Check next that the run wrote to the report the sidebar has active: the watch follows one
file, so a second suite writing to a different directory will not trigger it; pick it in the dropdown or
with Configure Report Path. Some network and container filesystems do not deliver change events either.
In every case Pytest HTML Reporter: Refresh Test Results, or the refresh icon in the
view title bar, reloads on demand.
Clicking a failure opens the wrong function, or a pick list
Expected in one case: when a single file defines the same test name twice — two classes with a method
of the same name — the record has nothing to tell them apart, so you get a pick list rather than a silent
guess at the wrong one. The scan is also a line scan, not a Python parse, so a def written
inside a triple-quoted string can match.
"Could not find … it may have been renamed since this run" means the report is older than the code. Re-run pytest.
No flaky or broken badges anywhere
Badges need history, and history needs an archive/ directory beside
output.json with past runs in it — a first run has a history of exactly itself. A test also
has to appear in at least 2 builds before it gets a verdict, and a stable test never gets a badge by
design. Finally, check pytestHtmlReporter.history.enabled is not false, and that
history.maxBuilds is wide enough to include the runs you care about.
"No HTML report found next to output.json"
The extension looks for pytest_html_report.html in the report directory, and failing that
accepts a single *.html file there. Two or more HTML files in the same directory are
ambiguous, so it offers none. Either clear the extras out, or point
reportJsonPaths at the output.json that sits beside the report you want.
The pasted error has strange bracket sequences in it
That is working as intended. The card shows the message with ANSI escape codes stripped for reading; copy error deliberately copies the original, colouring intact, so pasting it into a terminal reproduces pytest's own red-and-green assertion diff. Paste it into a terminal rather than a plain-text field.