Skip to content

Changelog

All notable changes to this project are documented here. The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.

0.4.1 - 2026-10-04

Fixed

  • The HTML report's phase hover outline in a parameter table now follows an attachment payload opened or closed under the pointer.

0.4.0 - 2026-10-04

Added

  • Python 3.15 is supported.
  • Hovering a phase in the HTML report's narration, or a column of its parameter table, highlights that phase in both.
  • Scenarios that fail as expected (xfail) get their own xfailed status, with their reason, steps and error, in every report format.

Changed

  • The parameter table orders its columns the way the narration first shows them, and shows its status column only when the cases differ in status.
  • The HTML report's Stories view shows each scenario as a full card that expands in place, and a tag clicked there opens the Scenarios view filtered by it.
  • The HTML report is visually tidied: term refs, the Glossary view, story coverage, narration spacing, hover states and the header row are restyled, and a parametrized scenario is marked by a second status bar.
  • The bundled pytest-given-authoring skill asks for one scenario per rule and covers parametrized scenarios as decision tables, and the pytest-given-reviewing skill catches more ways scenarios, glossary rows, pins, tags and parameter tables can disagree.

Fixed

  • A @given fixture parametrized with an unhashable value, such as a list, no longer errors every test that requests it.
  • Editor source links build {path} from pytest's rootdir instead of the working directory, so they no longer point at a doubled path when pytest runs from a subdirectory.
  • A scenario deep link stays working when the test's file or directory name contains +, &, = or #.
  • A parametrize value of infinity or NaN reaches the JSON report as a string ("inf", "nan"), so the report stays valid JSON.
  • A @given generator fixture that yields twice errors with pytest's own "more than one 'yield'" message, as it does without pytest-given, instead of passing silently.
  • An Annotated[..., given(Template(...))] label whose placeholder is not a bare parametrize column name now fails its scenario with the fix, instead of crashing the HTML report or silently dropping the step.
  • A scenario failing on a pytest-given refusal raised from its test body points at the test's own line, not at pytest-given's.
  • Error messages are clearer: a FileGlossary table error names the file, so does a glossary file that is a directory or not UTF-8, and a step nested across phases gets a concrete fix suggested.
  • The Markdown report's note below a parameter table names the row by its short parametrize values or its row number, instead of repeating multiline values.
  • A failure inside the narration lint is summarized under pytest-given: narration lint failed, no longer as report not written beside a report that was kept.
  • The tag-shadows-term lint finding counts a scenario once when it carries two spellings of the tag, such as Guest and guest.
  • pytest-given report -o report.markdown writes Markdown, as -o report.md does, instead of refusing the path as an HTML one.
  • pytest-given report --help mentions Markdown output and says where each format goes without -o.
  • The HTML report's links keep a tag filter whose tag contains a comma, by repeating the parameter per tag or term (#tag=a&tag=b), and copying a link no longer replaces the current page's entry in the browser history.
  • The HTML report no longer says "All Scenarios" when every status is filtered out, disables a status filter no scenario has, and the Glossary view says no terms match when every kind is unchecked.
  • The HTML report's view tabs and search boxes show a visible keyboard focus ring, and a turned-off status filter keeps readable contrast.
  • The HTML report's status filters wrap onto a second row instead of being cut off in a narrow sidebar.
  • A parametrized scenario shows a skip reason only when every case was skipped, no longer its first case's reason beside cases that ran.

0.3.0 - 2026-09-27

Added

  • A documentation site at https://nwilbert.github.io/pytest-given/ has a user guide, configuration and CLI reference, and the example reports. The bundled skills link to it, and the CLI help names the page on source-link templates.
  • The HTML report has a dark theme, with a Light / Dark / System control in its header that each browser remembers.
  • --given-theme / given_theme (and --theme on pytest-given report) set whether the HTML report opens light, dark, or following the viewer's system.
  • Tags can be organized hierarchically: a / in a tag nests it in the HTML report's Tags sidebar (ticket/ABC-123 goes under a ticket heading), and selecting the heading filters to every tag beneath it.
  • Projects can also install the bundled skills with library-skills (uvx library-skills install --claude), alongside the skills of their other dependencies.
  • One scenario can document a flow that spans several stories: @scenario(stories=[a, b]) lists it under each story in the Stories tab, and the Scenarios view's sentence filter names the story when a report has several.
  • A sentence can be named (sentence(..., name='checkout')), so a pin can refer to it by name and survives reordering the story. The Stories timeline shows the name beside the sentence's coverage.

Changed

  • Breaking. Story vocabulary follows Domain Storytelling: a story is made of sentences, and activity is the verb kind. activity() is now sentence(), path() is now clause(), and Glossary.verb() is now Glossary.activity(). A file glossary's kind column says activity instead of verb, the report's #activity-filter= link parameter is now #sentence-filter=, and the HTML report says Sentence and Activities where it said Activity and Verbs.
  • Breaking. @scenario(story=) is now stories=.
  • Breaking. Pins refer to sentences by handle under pins= instead of by number: given(..., activity=3) becomes given(..., pins=the_story[3]), and @scenario(activities=[2, 3]) becomes pins=[the_story[2], the_story[3]]. activity_id= is gone, because sentences are numbered by position; to pin a sentence independently of its position, name it and pin the_story['name']. A pin no longer requires the scenario to bind the pinned sentence's story.
  • Breaking. A scenario's pins= now sets its coverage outright: it covers exactly those sentences plus its steps' pins, with no narration matching. activities= only narrowed which sentences narration could cover. Pass pins=[] to a step or scenario to turn off narration matching without pinning anything.
  • The HTML report is restyled. It embeds Source Sans 3 and Source Code Pro, so it looks the same on every machine and offline. Lists share one surface instead of a card per row, Given/When/Then sit in a gutter beside the steps, and sidebar labels and counts use sentence case. Step narration and story sentences are set larger and darker than the surrounding labels and counts, at the same line spacing, so a scenario takes no more room.
  • The HTML report strips source comments from its inlined stylesheet and script, which saves about 29 KB per report and offsets part of the size the embedded fonts add.
  • Grouping errors for varying attachment labels and varying str narration name the parametrize case that differs. The varying-str error also suggests group_parametrized=False as a way out.
  • A story that no scenario covers now shows up in the report as uncovered, where before it was missing. The one exception is a story declared before the session started, such as one in a module still imported from an earlier pytest.main() in the same process: it is left out with a warning.
  • Story coverage ignores instances: a step narrating guest, or any guest instance, now covers a sentence naming guest('Alice'). Sentences that differ only by instance can be told apart only with a pin.
  • The authoring skill advises writing generic verbs (searches for, adds) as plain strings in sentences rather than glossary terms, keeping the glossary to domain vocabulary. The hotel-booking example follows it.
  • The authoring skill explains how coverage matching works (instances are ignored, and two sentences whose terms nest always cover together) and how to check coverage in the JSON report. The reviewing skill checks coverage from the report instead of re-deriving it.
  • The reviewing skill can list each scenario's narration beside its test's source for side-by-side review, and checks that a scenario demonstrates every rule the project's changelog announces. Both skills flag match= pins that use alternation, and a when that narrates setup while its body acts.
  • The authoring, reviewing and navigating skills cover sentence handles, pins= and stories=. The reviewing skill also checks that the test body exercises each sentence its scenario pins.

Fixed

  • When rendering a report fails unexpectedly, the previous run's report is still discarded instead of staying on disk looking current.
  • A #scenario= deep link now opens the right scenario when two node ids differ only in a character the slug folds, and no longer breaks the URL.
  • pytest-given report reports a non-UTF-8 input file as an error instead of crashing with a traceback.
  • An unknown --given-source-link preset is reported under the flag the user typed, not under the given_source_link ini name.
  • A refused scenario on a run with no --given-* sink no longer reports itself under a "report not written" heading.
  • when_then(...) rejects a Template narration in a test body, as given/when/then already do.
  • The Glossary view no longer lists a term's own name in another case (guest.low) under Instances.
  • On native Windows, stories and glossary terms now record where they are declared, so the lint rules that depend on it run there too.
  • tag-shadows-term now catches a tag that collides with a term id through a non-ASCII character that lowercases into ASCII.
  • A dead-term finding's message states the rule's actual criterion, including that a term ref in a @scenario name keeps a term alive.
  • The navigating skill shows the failure messages of a parametrized scenario's cases, where it printed an empty message. It also starts from a committed or CI-published report when one exists, instead of always rerunning the suite.
  • The reviewing skill lints the whole suite, where a selection failed on the project's ignore entries as stale, keeps the project's own given_lint_rules when enabling dead-term, and ranks findings in one explicit order.
  • A pin on an Annotated given(...) label now takes effect instead of being silently dropped, replacing the pins of the fixture step it relabels (pins=[] clears them). A pin on a step fixture's own @given(...) label also takes effect now.
  • A pin on a @given fixture scoped wider than function now counts in every scenario the fixture reaches, even when a test without @scenario set it up first. The run used to fail with a bare AssertionError.

0.2.0 - 2026-09-04

Added

  • pytest_given.PytestGivenWarning is a top-level export, and a step or attach() recorded in a test without @scenario now warns with it instead of pytest.PytestWarning.
  • --given-title=TEXT (or the given_title ini) names the report, replacing the rootdir name.
  • A parametrized scenario's parameter table now carries a typed column per varying value — param, derived, or attachment for a varying attachment payload — rather than one column per parametrize name.
  • @scenario(group_parametrized=False) declines the grouping and emits each case as its own scenario, titled by its parametrize id.
  • The HTML report's sidebar gains Terms as a third browse axis, and all three axes — Tags, Terms, Modules — now filter the Scenarios view the same way, with each active filter carried in the URL.
  • The sidebar can be ordered by group size as well as by name, and resized by dragging its seam or with the arrow keys.
  • A selected activity in the Stories view offers Open in Scenarios, filtering the Scenarios view down to the scenarios covering it.

Changed

CLI

  • Breaking. --given-lint is a plain boolean flag: write --given-lint and --no-given-lint instead of --given-lint=true / --given-lint=false. Either form still overrides the given_lint ini for one run.

Authoring API

  • Breaking. Step narration must now be uniform across parametrize cases; these fail the run with PytestGivenError, writing no report, instead of quietly reporting case 1:

  • a plain str (usually an f-string) that renders differently per case;

  • a varying interpolation that is not a bare name (t"{cup_size * 0.01}", t"{m.balance}");
  • a t-string narrating a parametrize name that no longer holds the case's value — either a local rebound it, or the body mutated it in place;
  • a step whose set of attach labels differs between cases;
  • a glossary term ref that names a different term or reads differently between cases, including one bound to a parametrize column;
  • passed cases that narrate different templates altogether.

Every one but the last is fixed by binding the varying part to a local and narrating it with a t-string, keeping labels and term refs constant; varying content belongs in the new attachment column. The last needs @scenario(..., group_parametrized=False), giving each case its own scenario. - Breaking. attach() now takes a plain str label; a t-string label raises PytestGivenError — use an f-string. - Breaking. attach() called with no step open now raises PytestGivenError instead of silently discarding the payload; move the call inside the step it belongs to. - Breaking. activity(..., id=N) is now activity(..., activity_id=N); the Activity.id field itself is unchanged. - Breaking. FileGlossary is now a Glossary subclass rather than a wrapper around one, so its .glossary attribute is gone — use the FileGlossary itself wherever that attribute was passed. - A glossary term placed in an activity slot its declared kind forbids now raises PytestGivenError when activity(...) is built rather than at session finish. - A glossary file whose table has a header and separator but no data rows now says so, instead of reporting that no table was found. - A FileGlossary whose columns are all named now skips a Markdown table that carries none of those names, so a glossary file may hold prose tables beside the glossary; a table carrying some of them still raises, as does any table under an index-based column spec. - @scenario(activities=...) now rejects a str and non-int members with a TypeError. - @scenario now returns the test function itself rather than a wrapper, so the test keeps its own signature.

Plugin and run behavior

  • An unknown given_source_link preset is now a UsageError raised before the suite runs.
  • The collection-time @scenario checks now report as a UsageError instead of an INTERNALERROR traceback.
  • The narration lint summary prints each finding's location in its own column rather than appended to the message.
  • pytest-given with no subcommand, and pytest-given skills with no subcommand, now print that parser's usage and exit 2 instead of the root help and exit 1.

Report content (all formats)

  • Breaking (JSON report). parameters.names becomes parameters.columns ({id, name, kind}), cells may hold an attachment object, placeholder parts gain column_id, term-ref parts lose param_column, and a grouped step's narration.text is the template rather than case 1's rendering.
  • Breaking (JSON report). A step no longer carries status or error; failure lives on the scenario and on the parameter table's cases. A consumer reading step.status should read scenario.status instead.
  • The Markdown report now shows a scenario's failure reason — the message and the failing frame — under the scenario, and under the parameter table for each failed case.

HTML report

  • The browse sidebar leads with Modules and renders them as a collapsible package tree whose nodes filter by path prefix; it no longer lists individual scenarios under each group.
  • The report's colors are retuned into one system — a term ref in a step or a scenario title reads as a word under a light wash rather than a bordered pill (the Glossary view keeps its pills), and column colors are generated per column — and the sidebar, its filter chips and the attachment badges are tidied along with it.
  • The report opens and filters substantially faster on large suites, and its file is smaller — a term reference now points at its glossary entry instead of repeating the entry's definition, which takes about 18% off a term-heavy report.
  • A glossary term referenced only in a @scenario title now contributes an instance to the Glossary view, where it previously counted toward the term's scenario tally while showing no instance.

Bundled skills

  • The authoring and reviewing skills gain the report mechanics their rules depend on, a symptom index, a completeness audit, the full lint rule catalog, and guidance for sparser tagging.

Removed

  • Breaking. The divergent-case-structure lint rule; delete any given_lint_rules or given_lint_ignore entry naming it, which would otherwise fail config parsing.

Fixed

Authoring API

  • Parametrize cases that claim different step activities now say so, instead of reporting the more drastic "a different step structure".

  • A @given fixture scoped wider than function no longer loses its step when the first test to use it has no @scenario.

  • @given/@when/@then are signature-preserving, so a decorated helper stays callable to a type checker (was: "StepDecorated" not callable).
  • @given(...) above @pytest.fixture now raises and names the fix, instead of surfacing as fixture '<name>' not found.
  • @scenario(activities=...) is now typed int | Sequence[int] | None, so a bare activities=2 type-checks.
  • @scenario(activities=...) with an unknown id, or without story=, is rejected at the decorator rather than at collection.
  • A -k- or --deselect-narrowed run no longer fails on an authoring error in a scenario it did not select.
  • Glossary term handles are now hashable, and equal for the same term whichever accessor produced them.
  • A when_then step in a test without @scenario now points its warning at the test rather than at pytest-given's own module.

Narration lint

  • A given_lint_ignore entry beginning with a Windows drive letter (c:/repo/tests/t.py::test_x) is no longer rejected as an unknown rule prefix.

  • A rule configured off no longer runs; levels were applied only after every rule had already produced its findings.

  • A scenario tag with no ASCII alphanumerics (tags=['日本語']) no longer takes the run down with a traceback from tag-shadows-term.

Report

  • pytest-given report reports an unreadable input as an error instead of a traceback (a directory raised IsADirectoryError through the console script).
  • A JSON report with an out-of-range scenario status or term kind is rejected by name, instead of crashing a renderer with a bare KeyError.
  • A <br> inside an inline code span in a glossary definition renders as text rather than as a line break.
  • The Terms browse axis no longer lists a term the selected glossary does not hold.

Plugin and run behavior

  • A --given-json/--given-html/--given-md path that could not be a report file is now refused before the suite runs, instead of a bare flag swallowing a following test path and overwriting — or, on a failed run, deleting — it.
  • pytest-given report now discards a stale report when the render fails, not only when the write does.
  • pytest-given report --source-link is now validated on a --format md run instead of being accepted and ignored, and an unknown preset is reported under the name the user typed.
  • A git on PATH that cannot be executed no longer fails the run.
  • A nested in-process pytest run that dies while parsing its arguments no longer strands the outer session's captured rootdir, which silently dropped every later step's source anchor.
  • @given/@when/@then on an async def helper now records around the awaited body, and async generator fixtures are handled too.
  • The narration lint now inspects async def step helpers, whose bodies were invisible to every AST rule.
  • An explicit --given-source-link= now disables source links instead of falling through to the given_source_link ini.
  • A finished scenario no longer leaves its collector — and every scenario and step it recorded — reachable from a process-global for the rest of the process.
  • An error-level lint finding no longer overwrites a more specific exit code, so an interrupted or nothing-collected run keeps reporting as one.
  • A report that cannot be written into an unwritable directory now reports through the terminal summary instead of escaping as a bare traceback.
  • pytest-given report and pytest-given skills install now report a failed write as a CLI error, and a failed report write discards the previous run's report rather than leaving it to read as current.
  • pytest-given report now reports a bad input file — missing, unparsable, or JSON that is not a pytest-given report — as a CLI error instead of a traceback, as does an unknown --source-link preset.
  • A fixture that raises after its yield now fails the scenario it tore down, instead of leaving it green in a report pytest counted as an error.
  • An error-level narration-lint finding now counts as an error in the run's summary line.
  • Every failure building or writing a report — a suite reaching two glossaries, a term used in incompatible slots, an unusable source-link template, an unwritable output path — now surfaces as a terminal summary and a failing exit code, where only grouping errors did.
  • The sinks are now rendered in full before any is written, and a failure on either side discards all of them.

Report content (all formats)

  • A parametrized scenario whose cases all skipped now keeps its skip reason instead of reporting skipped with none.
  • A parameter row whose cell count disagreed with the table's columns silently truncated in the HTML report; it now fails the same way it already did in Markdown.

  • A parametrized scenario now keeps its place in source order instead of moving below every unparametrized one.

  • A glossary term written as a code span keeps the markup inside it, so `a*b*c` canonicalizes to a*b*c.
  • A parameter-table cell now reads the way the step pointing at it read, carrying the interpolation's own format spec and, under indirect=True, the bound test argument.
  • A Template narration's text is now what its parts render, so the report's search box and jq queries match what the page displays.
  • The grouped step tree now comes from the first case that passed, where a skipped case 1 used to render an empty tree.
  • A parametrize value that is a glossary term instance now narrates as its display rather than the whole Glossary dataclass repr — in a step's Template slot, in a scenario name, and in an Annotated[..., given(Template(...))] parameter label.

HTML report

  • Two test files sharing a basename across directories no longer abort the HTML report; the scenarios' #scenario= slugs gain directory components instead.
  • A #view=stories, #view=glossary or #term= link opened against a report that has no such tab now falls back to the Scenarios view.
  • The Glossary view's kind headings and their term counts now follow the search and definition filters, and a filter matching nothing says so.
  • Content reaching past a scenario card's right edge is no longer clipped: a wide parameter table and an attachment payload scroll, a source path and a traceback's frame location wrap, and the source link no longer overlaps the card's last element.
  • Jumping to a scenario from a story activity, or to a term's scenarios from the Glossary tab, now clears filters that would hide the target; the filters in a #scenario= deep link still win.
  • Accent-colored text and the parametrize column colors now meet WCAG AA, and term kinds stay distinguishable for red-green color blindness.
  • The report is operable from the keyboard: status pills, browse-axis and browse-tree rows, tag pills, story sidebar entries, activity and attachment badges, and every expand/collapse chevron are now real buttons, and the view tabs report which one is selected.
  • A step pinned with given(..., activity=N) now covers an activity regardless of its term count; an under-anchored activity previously still rendered as "not coverage-tracked".

Bundled skills

  • The bundled skills are corrected against the shipped behavior.

0.1.0 - 2026-08-08

First public release.

Added

  • @scenario decorator plus given / when / then step blocks, usable as both context managers and decorators, including on fixtures.
  • Self-contained interactive HTML report (--given-html), Markdown report (--given-md), and JSON report (--given-json). The HTML bundles Alpine.js and needs no server or external assets.
  • Structured step text: plain strings, Template objects, and t-strings (PEP 750), with parameter interpolation rendered as highlighted values.
  • attach() for text and JSON attachments on a step.
  • Domain Storytelling support: ubiquitous-language glossaries (inline or Markdown-backed via FileGlossary), Domain Stories, and story coverage.
  • Narration lint (--given-lint) with a configurable rule catalog via given_lint_rules and given_lint_ignore.
  • --given-source-link with vscode, cursor, zed, pycharm, and github presets for jumping from a report step to its source.
  • pytest-given console script: report to re-render a saved JSON report, and skills install to mirror the bundled agent skills into a project's .claude/skills/.
  • Bundled authoring, navigating, and reviewing skills for AI agents, shipped in the wheel and version-matched to the plugin.