pytest-given Self-Report
pytest 9.0.3 pytest-given 0.2.0
Module:
A test without `@scenario` stays out of the report
✓ passed 38ms
Given
a suite whose only test is undecorated
When
the suite runs with --given-json
Then
the test itself passes
the report holds no scenario
A step fixture is grafted in as a given step
✓ passed 40ms
Given
a scenario consuming a step fixture
import pytest
from pytest_given import scenario, given, then

@pytest.fixture
@given("a prepared value")
def value():
    return 42

@scenario("Fixture test")
def test_fixture(value):
    with then(f"value is {value}"):
        assert value == 42
When
the suite runs with --given-json
Then
the test passes
the step from the fixture leads the recorded steps
The cases of a parametrized scenario become one scenario with a parameter table
✓ passed 44ms
Given
a parametrized scenario over two cases
import pytest
from pytest_given import scenario, given, when, then

@scenario("Param test", tags=["math"])
@pytest.mark.parametrize("a,b,expected", [(1, 2, 3), (2, 3, 5)])
def test_add(a, b, expected):
    with given(t"a={a} and b={b}"):
        pass
    with then(t"sum is {expected}"):
        assert a + b == expected
When
the suite runs with --given-json
Then
both cases pass
the two runs collapse into one scenario
the parameter table holds a param column per argument
it holds one row per case, with that row's values
the grouped steps carry a placeholder per matching name
A refusal on a run with no sink does not claim a report was skipped
✓ passed 25ms
Given
a suite whose narration varies across parametrize cases
When
the suite runs with no --given-* sink
Then
the refusal is reported without claiming a report was skipped
A refused run discards the previous run's report
✓ passed 24ms
Given
a suite whose narration varies across parametrize cases
import pytest
from pytest_given import scenario, when

@scenario("Brew")
@pytest.mark.parametrize("cup_size", [200, 350])
def test_brew(cup_size):
    with when(f"it brews {cup_size} ml"):
        assert cup_size > 0
a report on disk from a previous run
When
the suite runs with those sinks configured
Then
the run says no report was written, naming the sink
the stale files are gone rather than left reading as current
An unknown source link preset stops the run before it collects
✓ passed 11ms
Given
a suite that would otherwise pass
from pytest_given import scenario, then

@scenario("Brew")
def test_brew():
    with then("it brews"):
        assert True
When
the suite runs with a misspelled source link preset
Then
the run ends as a usage error, naming the flag the user typed
no test ran: the run stopped at configure, before collection
An unknown source link preset in an ini reports the ini name
✓ passed 12ms
Given
a suite configured through the ini rather than the flag
When
the suite runs with an HTML sink
Then
the error names the ini setting, not a flag the user never typed
A report that fails to render discards the previous one too
✓ passed 39ms
Given
a suite with one scenario
a report pair on disk from a previous run
When
the run trips a renderer failure
Then
the run fails, saying no report was written
neither the stale pair nor a half-written new one survives
A bare run writes no report at all
✓ passed 25ms
Given
a suite with one scenario
    import pytest
    from pytest_given import scenario, given, when, then

    @scenario('Buy coffee')
    def test_buy():
        with given('a machine'):
            pass
        with when('I insert money'):
            pass
        with then('I get coffee'):
            assert True
When
the suite runs with no output flag
Then
the run passes
nothing is written to disk
A bare `--given-md` prints the narration to stdout
✓ passed 36ms
Given
a suite with one scenario
When
the suite runs with a bare --given-md
Then
the narration is printed between the fence markers
Each sink flag writes only its own report file
✓ passed 72ms
Given
a suite with one scenario
When
the suite runs with --given-html alone
Then
the HTML rendering is written
no JSON lands beside it
A sink flag pointed at a source file is refused before the suite runs
✓ passed 12ms
Given
a suite with one scenario
When
a bare --given-html swallows the test path that follows it
Then
the run is refused, naming the path and the flag-order fix
the source file is left exactly as it was, not overwritten
A rejected authoring form fails the run and writes no report
✓ passed 24ms
Given
a suite whose narration varies across parametrize cases
import pytest
from pytest_given import scenario, when

@pytest.mark.parametrize('cup_size', [200, 350])
@scenario('Brew')
def test_brew(cup_size):
    with when(f'the machine brews {cup_size} ml'):
        pass
When
the suite runs with all three sinks configured
Then
the run fails, naming the offending form
not one sink is written, and no traceback escapes
`--given-title` names the report instead of the rootdir
✓ passed 40ms
Given
a suite with one scenario
When
the suite runs with --given-title
Then
the test passes
the title reaches the JSON metadata
the title also heads the Markdown rendering
`--given-theme` sets the theme the HTML report opens in
✓ passed 149ms
Given
a suite with one scenario
When
the suite runs with --given-theme=dark
Then
the test passes
the page declares dark as its default theme
An unknown theme stops the run before it collects
✓ passed 11ms
Given
a suite that would otherwise pass
When
the suite runs with a misspelled theme, and no HTML sink
Then
the run ends as a usage error, naming the flag the user typed
no test ran: the run stopped at configure, before collection
A run with no sink still enforces the grouping rules
✓ passed 24ms
Given
a suite whose f-string narration records no parts
import pytest
from pytest_given import scenario, then

@scenario("Brew")
@pytest.mark.parametrize('cup_size', [200, 300])
def test_brew(cup_size):
    with then(f'it brews {cup_size} ml'):
        assert cup_size
When
the suite runs with no sink configured
Then
the run still fails, naming the offending form
Narration lint is off unless it is asked for
✓ passed 26ms
Given
a suite with one flawed step
from pytest_given import scenario, given, when, then

@scenario("Empty given")
def test_empty_given():
    with given("a value"):
        pass
    with when("computing"):
        x = 2
    with then("it is two"):
        assert x == 2
When
the suite runs without the lint flag
Then
the run passes and says nothing about the lint
no step source is recorded, so the AST surface costs nothing
An error-severity finding fails the run
✓ passed 29ms
Given
a suite whose given step has an empty body
from pytest_given import scenario, given, when, then

@scenario("Empty given")
def test_empty_given():
    with given("a value"):
        pass
    with when("computing"):
        x = 2
    with then("it is two"):
        assert x == 2
When
the suite runs with the lint enabled
Then
the run exits failed, naming the lint rule and the step
A lint rule downgraded to warn reports without failing the run
✓ passed 26ms
Given
a suite whose given step has an empty body
from pytest_given import scenario, given, when, then

@scenario("Empty given")
def test_empty_given():
    with given("a value"):
        pass
    with when("computing"):
        x = 2
    with then("it is two"):
        assert x == 2
When
the suite runs with that lint rule set to warn
Then
the run still passes
the finding is printed anyway
Either narration lint flag overrides the ini for one run
✓ passed 50ms
Given
a suite with one flawed step
from pytest_given import scenario, given, when, then

@scenario("Empty given")
def test_empty_given():
    with given("a value"):
        pass
    with when("computing"):
        x = 2
    with then("it is two"):
        assert x == 2
When
the suite runs with the lint enabled by ini but off by flag
Then
the lint does not run
When
the suite runs with the lint disabled by ini but on by flag
Then
the lint runs and its error finding fails the run
An error finding leaves a more specific exit code alone
✓ passed 24ms
Given
a suite whose lint would fail, under a stale ignore entry
from pytest_given import scenario, given, when, then

@scenario("Empty given")
def test_empty_given():
    with given("a value"):
        pass
    with when("computing"):
        x = 2
    with then("it is two"):
        assert x == 2
When
the suite runs deselected, so nothing is collected
Then
the run keeps NO_TESTS_COLLECTED rather than reporting a test failure
A failure inside the lint keeps the report it was handed
✓ passed 84ms
Given
a clean suite and a lint pass that raises
When
the suite runs with an HTML sink
Then
the failure is summarized rather than raised as a traceback
the report that was already written is still there
A scenario records under its node ID
✓ passed 0ms
Given
a fresh Collector
When
a Scenario starts under its Node ID and finishes
Then
it carries its Node ID, name, status and Tag
A scenario is timed from past its step fixture setup
✓ passed 0ms
Given
a Collector whose clock reads 100.3s once setup is done
When
the clock is started past setup and the body runs 0.2s
Then
the recorded duration is the body alone, not the setup before it
Steps record with their phases
✓ passed 0ms
Given
an Active scenario in a fresh Collector
When
a given and a when Step are pushed
Then
each Step carries its Phase
Steps pushed during fixture setup record into the fixture recording
✓ passed 0ms
Given
a Fixture recording under setup
When
a Step is pushed inside the fixture body
Then
it is recorded as a child of the recording root
An attachment lands on the step being recorded
✓ passed 0ms
Given
a Fixture recording under setup
When
an Attachment is attached inside the fixture body
Then
the Attachment lands on the recording root
Fixture-body steps do not leak into the active scenario
✓ passed 0ms
Given
an Active scenario with a Fixture recording
When
a Step is pushed inside the fixture body
Then
the step lives only in the recording, not the scenario
An attachment outside every step is refused
✓ passed 0ms
Given
an Active scenario with no Step open
When
an attachment is made from the test body
Then
it is refused rather than dropped
A fixture recording is deep-copied when grafted
✓ passed 0ms
Given
a Fixture recording with a nested child Step
When
a Graft copies it into the Active scenario
Then
the scenario gains a deep copy of the recorded steps
A step fixture failing in teardown fails its finished scenario
✓ passed 0ms
Given
a Scenario that already finished as passed
When
a fixture raises past its yield, after the scenario finished
Then
the recorded scenario carries the failure
A teardown failure keeps the error the scenario already carries
✓ passed 0ms
Given
a Scenario that already failed in its body
When
its fixture then also fails in teardown
Then
the body failure is what the report shows
A Collector reports which node ids it recorded
✓ passed 0ms
Given
a Collector that recorded one scenario
When
the recorded and an unrecorded node id are both asked about
Then
only the recorded node id is claimed
A leaf given is grafted as a childless given step
✓ passed 0ms
Given
an Active scenario is being recorded
When
a leaf Graft appends a childless Step
Then
the step is a given with no children
Grafting with an override replaces the root label but keeps children
✓ passed 0ms
Given
a Fixture recording whose root has a label and a child
When
a Graft supplies an override Narration
Then
the grafted root shows the override text and keeps its children
Grafting with no active scenario is refused
✓ passed 0ms
Given
a collector with no Active scenario
When
a leaf Graft runs
Then
the invariant is asserted rather than silently dropping the step
FileGlossary lookup is case-insensitive
✓ passed 1ms
Given
a File glossary loaded from a Markdown file
# Glossary

| Term | Meaning |
|------|---------|
| Guest  | A person booking. |
| Room   | A bookable room. |
| search | Look up options. |
When
the same Term is looked up in three different cases
Then
every lookup resolves to one handle type and the same id
Repeated lookups return the same handle
✓ passed 0ms
Given
a File glossary loaded from a Markdown file
# Glossary

| Term | Meaning |
|------|---------|
| Guest  | A person booking. |
| Room   | A bookable room. |
| search | Look up options. |
When
the same Term is looked up twice
Then
both lookups return the one memoized handle
File-loaded terms start kindless
✓ passed 0ms
Given
a Markdown glossary file with no kind column
# Glossary

| Term | Meaning |
|------|---------|
| Guest  | A person booking. |
| Room   | A bookable room. |
| search | Look up options. |
When
a File glossary loads it
Then
each Term is Kindless until Kind inference runs
An unknown name raises with a suggestion
✓ passed 1ms
Given
a File glossary loaded from a Markdown file
# Glossary

| Term | Meaning |
|------|---------|
| Guest  | A person booking. |
| Room   | A bookable room. |
| search | Look up options. |
When
a misspelt Term is looked up
Then
a PytestGivenError is raised with a spelling hint
Handles are usable inline in an activity
✓ passed 1ms
Given
a File glossary loaded from a Markdown file
# Glossary

| Term | Meaning |
|------|---------|
| Guest  | A person booking. |
| Room   | A bookable room. |
| search | Look up options. |
When
its handles build an Activity
Then
each slot becomes a Term ref
Calling a handle overrides its display
✓ passed 1ms
Given
a File glossary loaded from a Markdown file
# Glossary

| Term | Meaning |
|------|---------|
| Guest  | A person booking. |
| Room   | A bookable room. |
| search | Look up options. |
When
a handle is called to name an Instance
Then
the Term ref carries the overridden display
An explicit kind column sets term kinds
✓ passed 1ms
Given
a Markdown glossary with an explicit Kind column
| Term | Meaning | Kind |
|---|---|---|
| Guest | x | Actor |
| Room | y | Work Object |
When
the File glossary reads the Kind column
Then
kinds come straight from the file, not Kindless inference
A kind column can be selected by integer index
✓ passed 1ms
Given
a Markdown glossary with the kind in the third column
| Term | Meaning | Kind |
|---|---|---|
| Guest | x | Actor |
| Room | y | Work Object |
When
the File glossary selects the kind column by index
Then
the kinds are read from that column
A work_object kind alias maps to the object kind
✓ passed 4ms
Given
a glossary whose Kind cell says work_object
| Term | Meaning | Kind |
|---|---|---|
| Room | y | work_object |
When
the File glossary parses the kind
Then
it normalizes to the Work Object kind
An unrecognized kind value is rejected
✓ passed 1ms
Given
a glossary whose Kind cell holds an unknown value
| Term | Meaning | Kind |
|---|---|---|
| Guest | x | Wizard |
When
the File glossary loads the file
Then
a PytestGivenError names the unrecognized kind
A missing glossary file is reported clearly
✓ passed 0ms
Given
a path to a file that does not exist
When
a File glossary is opened on that path
Then
a PytestGivenError reports the file is not found
A term cell with no alphanumeric characters is rejected
✓ passed 1ms
Given
a row whose Term cell has no id-able characters
| Term | Meaning |
|---|---|
| @#$ | some definition |
When
the File glossary loads the file
Then
a PytestGivenError is raised with file:line context
Conflicting duplicate rows are rejected
✓ passed 1ms
Given
two rows for one Term with different definitions
| Term | Meaning |
|---|---|
| Guest | First definition. |
| Guest | Second definition. |
When
the File glossary loads the file
Then
a PytestGivenError reports the conflicting rows
A blank description normalizes to undefined
✓ passed 1ms
Given
a row whose description cell is blank
| Term | Meaning |
|---|---|
| Guest |   |
When
the File glossary parses it
Then
the Term definition is None, i.e. Undefined
Identical duplicate rows collapse to one term
✓ passed 1ms
Given
two identical rows for the same Term
| Term | Meaning |
|---|---|
| Guest | A person booking. |
| Guest | A person booking. |
When
the File glossary parses them
Then
they collapse to a single Term
Calling FileGlossary looks up a known term
✓ passed 0ms
Given
a File glossary loaded from a Markdown file
# Glossary

| Term | Meaning |
|------|---------|
| Guest  | A person booking. |
| Room   | A bookable room. |
| search | Look up options. |
When
a known Term is looked up by call
Then
a Deferred term is returned
FileGlossary is a closed vocabulary
✓ passed 1ms
Given
a File glossary loaded from a Markdown file
# Glossary

| Term | Meaning |
|------|---------|
| Guest  | A person booking. |
| Room   | A bookable room. |
| search | Look up options. |
When
an unknown name is called
Then
a PytestGivenError is raised
no new Term was created
Term ids are derived as URL-safe slugs
✓ passed 0ms
Given
the name {text}
When
it is slugified into a Term id
Then
the id is the expected slug {expected}
text expected status
Guest 'guest'
Order received 'order-received'
Work Object 'work-object'
do_the_thing 'do-the-thing'
Buy / sell 'buy-sell'
Guest #1 'guest-1'
café 'caf'
booking system 'booking-system'
A name with no id-able characters is rejected
✓ passed 0ms
Given
the name {text}
When
it is slugified into a Term id
Then
a PytestGivenError reports the derived id is empty
text status
---
###
Calling an actor names a distinct instance
✓ passed 0ms
Given
an Actor handle for Guest
When
the Actor is called with a name
Then
an Instance with a distinct display is returned
Calling a verb records an inflection of the same term
✓ passed 0ms
Given
a Verb handle for confirm
When
the Verb is called with a surface form
Then
an Inflection sharing the verb identity is returned
Registering an actor returns a typed handle
✓ passed 0ms
Given
an empty glossary
When
an Actor is registered with a definition
Then
a handle carrying the Actor kind is returned
Re-registering a term with matching fields is idempotent
✓ passed 0ms
Given
an Actor already registered with a definition
When
the same name and definition are registered again
Then
both handles share the one Term
Re-registering a term with a different definition is rejected
✓ passed 0ms
Given
an Actor already registered with one definition
When
the name is registered again with a different definition
Then
a PytestGivenError reports the conflict with the prior registration
The same name cannot be two different kinds
✓ passed 0ms
Given
a name already registered as an Actor
When
the same name is registered as a Verb
Then
a PytestGivenError reports the conflict with the prior registration
Registering an actor captures its definition site
✓ passed 0ms
Given
a rootdir-aware glossary
When
an Actor is registered
Then
the Term records a Source link to this file
Calling the glossary declares a kindless term
✓ passed 0ms
Given
an empty glossary
When
a Term is declared by call, without a kind
Then
the Term is registered as Kindless
Subscript looks up an already-declared term
✓ passed 0ms
Given
a glossary with one declared Term
When
the name is looked up by subscript
Then
the returned Term is the declared one
Subscripting an unknown name raises with a hint
✓ passed 0ms
Given
a glossary with one declared Term
When
a near-miss name is subscripted
Then
a PytestGivenError is raised with a spelling hint
Term kinds are inferred from activity-slot positions
✓ passed 0ms
Given
a glossary of three Kindless Term entries
When
Kind inference runs over a Story
Then
they are inferred as Actor, Verb, Work Object by slot
An actor slot anywhere wins over a noun slot elsewhere
✓ passed 0ms
Given
a glossary of Kindless Term entries
one Story putting a Term in a noun slot and another putting it in an Actor slot
When
Kind inference runs over both stories
Then
its inferred kind is Actor
A term used in no story stays kindless
✓ passed 0ms
Given
a Term referenced by no Story
When
Kind inference runs with no stories
Then
the Term remains Kindless
A term in both a verb and a noun slot is a conflict
✓ passed 0ms
Given
a Kindless Term used in a verb slot and a noun slot
When
kind resolution runs over both stories
Then
a PytestGivenError names the conflicting term
A declared kind consistent with its slot is kept
✓ passed 0ms
Given
a glossary with explicitly declared Term kinds
When
Kind inference runs over a matching Story
Then
the declared kinds are verified and preserved
A declared verb in an actor slot is rejected
✓ passed 0ms
Given
a Term declared as a Verb
When
kind resolution places it in the Actor slot
Then
a PytestGivenError names the misplaced term
A term used as both verb and actor is a conflict
✓ passed 0ms
Given
a Kindless Term used in a verb slot and an actor slot
When
kind resolution runs over both stories
Then
a PytestGivenError names the conflicting term
A declared work object in an actor slot is rejected
✓ passed 0ms
Given
a Term declared as a Work Object
When
kind resolution places it in the Actor slot
Then
a PytestGivenError names the misplaced term
A declared actor in a verb slot is rejected
✓ passed 0ms
Given
a Term declared as an Actor
When
kind resolution places it at position 1 (the verb slot)
Then
a PytestGivenError says an actor cannot fill the verb slot
A conflict error names only the offending stories
✓ passed 0ms
Given
an Actor Term that also appears in a verb slot
When
kind resolution raises
Then
only the offending story is named in the message
A conflict message excludes stories with an unrelated slot
✓ passed 0ms
Given
a Kindless Term used in verb, actor and noun slots
When
the verb-vs-actor conflict is raised
Then
only the verb and actor stories are named, not the noun one
A declared verb in a noun slot is rejected
✓ passed 0ms
Given
a Term declared as a Verb
When
kind resolution places it at position ≥2 (a noun slot)
Then
a PytestGivenError says a verb cannot fill the noun slot
Slot positions alternate verb/noun after the actor
✓ passed 0ms
Given
the five positions of a short activity path
When
the Slot rule is applied to each position
Then
position 0 is the actor Slot, then verb and noun alternate
A pipe table parses into term and definition rows
✓ passed 0ms
Given
a Markdown document with one pipe table
# Glossary

| Term | Meaning |
|------|---------|
| Guest | A person booking. |
| Room  | A bookable room. |
When
the parser reads it into rows for a File glossary
Then
each row carries a Term, definition and source line
Multiple tables in one file are merged
✓ passed 0ms
Given
a document containing two separate pipe tables
# Glossary

| Term | Meaning |
|------|---------|
| Guest | A person booking. |
| Room  | A bookable room. |

## More

| Term | Meaning |
|---|---|
| Search | Look up. |
When
the parser reads the whole document
Then
every table contributes its Term rows
Columns can be selected by header name
✓ passed 0ms
Given
a table with custom, differently-cased header names
| Word | Note | Role |
|---|---|---|
| Guest | x | Actor |
When
the parser selects columns by header name
Then
the named columns are matched case-insensitively
Escaped pipes are preserved in cells
✓ passed 0ms
Given
cells containing escaped pipe characters (\|)
| Term | Meaning |
|---|---|
| A\|B | pipe\|here |
When
the parser splits the row
Then
the escaped pipe survives as a literal pipe
Tables inside fenced code blocks are skipped
✓ passed 0ms
Given
a fenced code block that contains a look-alike table
```
| Term | Meaning |
|---|---|
| Fake | nope |
```

| Term | Meaning |
|---|---|
| Real | yes |
When
the parser reads the document
Then
only the real table outside the fence contributes rows
A file with no pipe table is rejected
✓ passed 0ms
Given
a document with no pipe table
# Just a heading

No tables here.
When
the parser reads it for a File glossary
Then
a PytestGivenError reports that the file has no pipe table
A missing named column is rejected
✓ passed 0ms
Given
a Markdown document with one pipe table
# Glossary

| Term | Meaning |
|------|---------|
| Guest | A person booking. |
| Room  | A bookable room. |
When
the parser selects a header name that is absent
Then
a PytestGivenError names the missing column
A column index out of range is rejected
✓ passed 0ms
Given
a Markdown document with one pipe table
# Glossary

| Term | Meaning |
|------|---------|
| Guest | A person booking. |
| Room  | A bookable room. |
When
the parser selects a column index past the table width
Then
a PytestGivenError names the out-of-range column
A data row with too few columns is rejected
✓ passed 0ms
Given
a table with a data row narrower than its header
| Term | Meaning | Type |
|---|---|---|
| Guest | A person |
| Room | A bookable room | place |
When
the parser reads the short row
Then
a PytestGivenError points at the short row
Bold term cells render as clean terms
✓ passed 0ms
Given
a Term cell written with **bold** emphasis
| Term | Meaning |
|---|---|
| **Scenario** | A decorated test. |
When
the parser reads the term cell
Then
the emphasis is unwrapped to the plain canonical
Italic and inline-code term cells are unwrapped
✓ passed 0ms
Given
Term cells using *italic* and `code` emphasis
| Term | Meaning |
|---|---|
| *Step* | one. |
| `given` | two. |
When
the parser reads the term cells
Then
each unwraps to its plain text
Underscores inside an identifier survive
✓ passed 0ms
Given
a Term literally named work_object
| Term | Meaning |
|---|---|
| work_object | a thing. |
When
the parser reads the term cell
Then
the single underscores are not treated as emphasis
Emphasis is stripped from kind cells too
✓ passed 0ms
Given
a Kind cell written with bold emphasis
| Term | Meaning | Kind |
|---|---|---|
| Guest | x | **Actor** |
When
the parser reads the kind cell
Then
the kind is unwrapped to plain text
Definition markdown is left intact
✓ passed 0ms
Given
a definition cell rich with inline code
| Term | Meaning |
|---|---|
| Scenario | A test decorated with `@scenario(...)`. |
When
the parser reads the row
Then
the definition keeps its markup for the tooltip
A pipe line without a separator is not a table
✓ passed 0ms
Given
prose containing a stray pipe, then a real table
This line has a | in it but no separator follows.
Next line is not a separator.

| Term | Meaning |
|---|---|
| Real | yes |
When
the parser reads the document
Then
only the real pipe table produces rows
A code-span term cell keeps the markup inside it
✓ passed 0ms
Given
a Term cell written as a code span around an asterisk pair
| Term | Meaning |
|---|---|
| `a*b*c` | a literal. |
When
the parser reads the term cell
Then
the span unwraps once and its contents stay literal
A step pairs its narration with a phase
✓ passed 0ms
When
a given Step descriptor is created
Then
it carries the given Phase and its Narration
A step opened outside a scenario warns rather than raising
✓ passed 0ms
Given
a collector recording inside an undecorated test
When
a given step is opened against it
Then
a `PytestGivenWarning` is raised, not an error
it names the missing `@scenario`, so a suite can filter it
when_then records the action and its outcome as siblings
✓ passed 0ms
Given
an Active scenario in a local Collector
When
a when_then block exits cleanly
Then
a when and a sibling then Step are recorded
when_then pairs with an inner pytest.raises
✓ passed 0ms
Given
an Active scenario in a local Collector
When
the when_then body raises and an inner pytest.raises swallows it
Then
both sibling steps are still recorded
when_then omits the then when the body raises uncaught
✓ passed 0ms
Given
an Active scenario in a local Collector
When
the when_then body raises with nothing catching inside
Then
only the when step is recorded — the outcome never held
A cross-phase step cannot open inside a when_then body
✓ passed 0ms
Given
an Active scenario in a local Collector
When
a given or then opens inside the when_then body
Then
a PytestGivenError reports the cross-phase nesting
the Step stack is left balanced
phase_name status
given
then
A nested when becomes a child of the when_then action
✓ passed 0ms
Given
an Active scenario in a local Collector
When
a when opens inside the when_then body
Then
the sub-action is a child of the action and the then still follows
`@scenario` marks the test function without wrapping it
✓ passed 0ms
Given
a test function taking one fixture
When
the function is decorated
Then
the very same function comes back, keeping its signature
it carries the scenario marker, and a plain one does not
An attachment label must be plain text
✓ passed 0ms
Given
a non-str Attachment label of kind {label_kind}
When
it is attached
Then
a PytestGivenError says Attachment labels are plain text
label_kind status
deferred-template
t-string
not-a-string
A `Template` narration is refused in a test body
✓ passed 0ms
Given
an Active scenario in a local Collector
When
a {phase_name} step opens on a `Template`
Then
a PytestGivenError says a template is not supported in a test body
phase_name status
given
when
then
A string `activities=` argument is refused by `@scenario`
✓ passed 0ms
Given
a string where a sequence of activity ids goes
When
the scenario is declared
Then
a `TypeError` naming the argument is raised
An actor handle in a path becomes a term ref
✓ passed 0ms
Given
a Guest actor
a search verb
a Room work object
When
a Path is built from three glossary handles
Then
the Actor slot becomes a Term ref
An inflected verb keeps its term identity but shows the inflection
✓ passed 0ms
Given
a Guest actor
a search verb
a Room work object
a Verb handle called with an Inflection
When
it takes the verb slot of a Path
Then
the Term ref shows the inflection over the same Verb
A bare string in a path becomes a connective word
✓ passed 0ms
Given
a Guest actor
a search verb
a Room work object
When
a Path is built with a bare word between term nodes
Then
the bare word becomes an Activity Part word, not a Term ref
A path needs at least an actor, a verb and a node
✓ passed 0ms
Given
a Guest actor
a search verb
When
a Path of only two parts is built
Then
a PytestGivenError rejects it as too short, counting the parts
Position 0 of a path must be an actor
✓ passed 0ms
Given
a search verb
a Room work object
When
a Path is built with a Work Object in position 0
Then
a PytestGivenError says position 0 is the Actor slot
A verb cannot open a path
✓ passed 0ms
Given
a Guest actor
a search verb
a Room work object
When
a Verb is placed in position 0 of a Path
Then
a PytestGivenError says position 0 is the Actor slot
A bare string may stand in for the actor slot
✓ passed 0ms
Given
a search verb
a Room work object
When
a bare string takes position 0 of a Path
Then
it is accepted as an Activity Part word
Position 1 of a path must be a verb
✓ passed 0ms
Given
a Guest actor
a Room work object
When
an Actor is placed in position 1 of a Path
Then
a PytestGivenError says position 1 is the Verb slot
A work object cannot fill the verb slot
✓ passed 0ms
Given
a Guest actor
a Room work object
When
a Work Object is placed in position 1 of a Path
Then
a PytestGivenError says position 1 is the Verb slot
Position 2 of a path must be a noun
✓ passed 0ms
Given
a Guest actor
a search verb
When
a Verb is placed in position 2 of a Path
Then
a PytestGivenError says position 2 is the noun slot
A bare verb may sit between two real entity nodes
✓ passed 0ms
Given
a Guest actor
a Room work object
When
a bare verb sits between an Actor and a Work Object
Then
the entities are term refs and the verb stays a bare word
A path may be fully bare words
✓ passed 0ms
Given
three plain words with no glossary handles
When
a Path is built from them
Then
every part is an Activity Part word
Node/edge alternation allows a trailing connective node
✓ passed 0ms
Given
an Actor, a Verb, a Work Object and a second actor
When
they form a five-part Path joined by a connective
Then
even positions are term-ref nodes and the connective stays a word
A path may not end on a dangling edge
✓ passed 0ms
Given
an Actor, Verb and Work Object plus a connective
When
a path ending on a connective edge is built
Then
a PytestGivenError names the trailing arrow with no target
A single-path activity synthesizes one path
✓ passed 0ms
Given
a Guest actor
a search verb
a Room work object
When
an Activity is built from handles directly
Then
it wraps a single Path
An activity may branch into multiple paths
✓ passed 0ms
Given
a Guest actor
a search verb
a Room work object
two alternate Path branches
When
they are combined into one Activity
Then
the activity carries both paths
Mixing loose parts and prebuilt paths is rejected
✓ passed 0ms
Given
a Guest actor
a search verb
a Room work object
a prebuilt Path
When
it is combined with loose handles in one Activity
Then
a PytestGivenError rejects the mix
Activity id 0 is reserved
✓ passed 0ms
Given
a Guest actor
a search verb
a Room work object
When
an Activity is built with explicit activity_id=0
Then
a PytestGivenError says activity_id=0 is reserved
A story auto-numbers its activities from one
✓ passed 0ms
Given
a Guest actor
a search verb
a Room work object
When
a Story is built from two Activity rows
Then
the activities are numbered 1 and 2
Auto-numbering skips ids already taken explicitly
✓ passed 0ms
Given
a Guest actor
a search verb
a Room work object
a mix of explicit and auto Activity ids
When
they are assembled into a Story
Then
auto picks skip the ids already used explicitly
Duplicate activity ids in a story are rejected
✓ passed 0ms
Given
a Guest actor
a search verb
a Room work object
two Activity rows sharing an explicit id
When
they are assembled into a Story
Then
a PytestGivenError reports the duplicate activity id
A story derives its id from its title
✓ passed 0ms
Given
a human-readable story title
When
a Story is built from it
Then
its id is the slugified title
A story may span only one glossary
✓ passed 0ms
Given
a Guest actor
a search verb
a Room work object
two activities that reach two different glossaries
When
a Story is built spanning both glossaries
Then
a PytestGivenError says a story spans multiple glossaries
Two stories with the same id collide
✓ passed 0ms
Given
a Story already declared under an id
When
a second story is declared with the same slug
Then
a PytestGivenError reports the id was already declared
A path may chain a second verb-object pair
✓ passed 0ms
Given
an Actor, two Verb and two Work Object handles
When
they form a five-node Path (actor verb object verb object)
Then
every slot is a Term ref, with no bare words
A declared work object in a verb slot is rejected at construction
✓ passed 1ms
Given
a File glossary declaring Room a work object
When
Room is placed in the verb slot
Then
a PytestGivenError names the term and its declared kind
A slot error names the term, not its repr
✓ passed 0ms
Given
a Guest actor
a Room work object
a search verb
When
a work object is placed in the verb slot
Then
the message names the term without dumping the glossary
the message is short and free of dataclass reprs
A kindless term stays valid in any slot
✓ passed 0ms
Given
a Kindless Term declared with g(...)
When
it is placed in a node slot and a verb slot
Then
both paths construct, leaving the kind to inference
A non-handle activity part names its type
✓ passed 0ms
Given
a Guest actor
a Room work object
When
an int is passed where a verb handle belongs
Then
a PytestGivenError names the offending type and the path
A Template parses a bare placeholder
✓ passed 0ms
Given
a deferred Templatize template with one placeholder
When
the template is parsed
Then
it splits into literal and placeholder Narration parts
A Template substitutes parametrize values
✓ passed 0ms
Given
a Templatize template referencing a Case column
When
a Parameter table value is substituted in
Then
the placeholder is filled with that value
A Template accepts bare identifiers only
✓ passed 0ms
Given
the placeholder {text}
When
a Templatize template is built from it
Then
a PytestGivenError says bare identifiers only
text status
count={obj.attr}
{d[key]}
{x + 1}
A t-string interpolation becomes a value part
✓ passed 0ms
Given
a t-string step with one interpolated value
When
the t-string is parsed at runtime
Then
the interpolation becomes a Narration value part
A t-string can interpolate an arbitrary expression
✓ passed 0ms
Given
a t-string step interpolating a computed expression
When
the t-string is parsed
Then
the Value highlight part records the full expression
A glossary handle in a t-string emits a term ref
✓ passed 0ms
Given
an Actor handle from the glossary
When
the handle is interpolated into a t-string step
Then
the step carries a Term ref for that Actor
A work object handle in a t-string emits a term ref
✓ passed 0ms
Given
a Work Object handle from the glossary
When
it is interpolated into a t-string step
Then
the step carries a Term ref for that Work Object
A bare verb handle keeps its canonical display
✓ passed 0ms
Given
a Verb handle used without an Inflection
When
it is interpolated into a t-string step
Then
the Term ref shows the canonical verb
An inflected verb in a t-string shows the inflection
✓ passed 0ms
Given
a Verb handle called with an Inflection
When
it is interpolated into a t-string step
Then
the Term ref shows the inflection but keeps the verb id
A term ref may not carry a format spec
✓ passed 0ms
Given
an Actor handle interpolated with a format spec
When
the t-string is parsed
Then
a PytestGivenError says a Term ref takes no format spec
A FileGlossary handle works in a t-string step
✓ passed 0ms
Given
a Deferred term from a File glossary
When
it is interpolated into a t-string step
Then
the step carries a single Term ref
Narration lint flags a step whose body does nothing
✓ passed 1ms
Given
a given step whose body is only `pass`
def test_a():
    with given('a value'):
        pass
When
the AST rules parse that source
Then
an empty-step finding points at the step line
its severity is error
Narration lint flags a then step that checks nothing
✓ passed 1ms
Given
a then step whose body only calls
def test_a():
    with then('it is one'):
        x = compute()
        handlers[0](x)
When
the AST rules parse that source
Then
a then-without-check finding reports the unchecked then
Narration lint flags an assert outside a then step
✓ passed 2ms
Given
a {phase} step whose body asserts
step body
When
the AST rules parse that source
Then
a warn finding names the {phase} step holding the assert
phase step body status
given
step body
def test_a():
    with given('a stocked machine'):
        machine = stock()
        assert machine['coffees'] > 0
when
step body
def test_a():
    with when('a stocked machine'):
        machine = stock()
        assert machine['coffees'] > 0
Narration lint flags a then step that folds in the action
✓ passed 1ms
Given
a scenario with no when, acting inside its then
def test_a():
    with given('a machine'):
        machine = stock()
    with then('it brews'):
        assert brew(machine) == 'coffee'
When
the AST rules parse that source
Then
a warn finding points at the then and says no when acts
Narration lint flags a narration interpolating a name the body never uses
✓ passed 1ms
Given
a given step whose body never loads the name
def test_a():
    with given(t'a {size} ml cup'):
        cup = make_cup()
When
the AST rules parse that source
Then
a warn finding names the interpolation the body ignores
Narration lint flags a passed scenario that skips a phase
✓ passed 0ms
Given
a passed scenario narrating only given and then
When
the runtime rules run
Then
one missing-phase finding names the absent when and the scenario source
its severity is the catalog default, warn
Narration lint flags a tag that duplicates a term
✓ passed 0ms
Given
a glossary defining one term
two scenarios carrying that word as a tag
When
the runtime rules run
Then
a single warn finding names the tag, the term it shadows, and both scenarios
Narration lint flags a term referenced by no scenario name, step or story
✓ passed 0ms
Given
a glossary holding one unreferenced term
When
the runtime rules run over no scenarios and no stories
Then
the finding names the unreferenced term
its severity is off — the rule is opt-in
An activity is referenced by its terms, whatever their surface form
✓ passed 0ms
Given
an Activity written with an Instance and an Inflection
When
Coverage collects the Activity references
Then
they are the Term ids alone; words contribute nothing
A branching activity unions references across its paths
✓ passed 0ms
Given
an Activity that branches into two Path alternatives
When
Coverage collects the Activity references
Then
the terms of both branches are present
A step is referenced by its terms, whatever their surface form
✓ passed 0ms
Given
a Step naming an Instance and an Inflection
When
Coverage collects the Step references
Then
they are the Term ids alone
An instance and its bare term cover each other
✓ passed 0ms
Given
an Activity naming a bare Actor
the same Activity naming an Instance of that actor
a Step naming the Instance, and one naming the bare actor
When
Coverage is computed for each pairing
Then
the Instance Step covers the bare Activity
the bare Step covers the Instance Activity
Promoting a bare word to a verb ref drops coverage from a step that matched
✓ passed 0ms
Given
a Step naming two term refs
the same Activity with that middle slot a bare word, then a Verb ref
When
Coverage is computed against each Story
Then
the two-ref Activity is covered
the widened Activity is no longer covered
A scenario activity binding constrains coverage
✓ passed 0ms
Given
a Story with two matching activities
a Scenario bound only to activity 1
When
Coverage is computed against the Story
Then
Coverage considers only the bound Activity
An activity with two distinct terms is coverage-eligible
✓ passed 0ms
Given
an Activity anchored by two distinct Term refs
When
its Coverage eligibility is checked
Then
it is eligible for Coverage tracking
An under-anchored activity is not coverage-eligible
✓ passed 0ms
Given
an Activity that mentions only one distinct Term
When
its Coverage eligibility is checked
Then
it is ineligible — Coverage needs at least two anchors
An under-anchored activity is never covered by narration matching
✓ passed 0ms
Given
a Story whose Activity is all bare words
a Scenario narrating one Term ref
When
Coverage is computed against the Story
Then
Coverage excludes the under-anchored Activity
Nested steps are walked for coverage
✓ passed 0ms
Given
a Story with one canonical Activity
the covering term refs in a nested child Step
When
Coverage is computed against the Story
Then
the nested Step still counts and the Activity is covered
An explicit step binding covers an eligible activity
✓ passed 0ms
Given
a Story with a coverage-eligible Activity
a Step bound to it explicitly by id
When
Coverage is computed against the Story
Then
Coverage counts it directly, without narration matching
An explicit binding covers an under-anchored activity
✓ passed 0ms
Given
a Story whose Activity is under-anchored
a Step bound to it explicitly by id
When
Coverage is computed against the Story
Then
Coverage counts it, despite the missing anchors
The glossary view aggregates instances and verb forms
✓ passed 0ms
Given
a Report whose Story and Scenario reference entity Instances and an Inflection
{
  "metadata": {
    "project": "p",
    "timestamp": "t",
    "pytest_version": "8",
    "plugin_version": "0",
    "commit_sha": null,
    "title": null
  },
  "scenarios": [
    {
      "id": "t",
      "narration": {
        "text": "s",
        "parts": []
      },
      "module": "m",
      "tags": [],
      "status": "passed",
      "duration_ms": 0,
      "steps": [
        {
          "phase": "when",
          "narration": {
            "text": "x",
            "parts": [
              {
                "term_id": "guest",
                "display": "Alice",
                "expression": ""
              },
              {
                "term_id": "search",
                "display": "searches",
                "expression": ""
              },
              {
                "term_id": "room",
                "display": "Deluxe Suite",
                "expression": ""
              }
            ]
          },
          "children": [],
          "attachments": [],
          "activity_ids": [],
          "fixture_name": null
        }
      ],
      "parameters": null,
      "error": null,
      "skip_reason": null,
      "source": null,
      "story_id": "book",
      "activity_ids": []
    }
  ],
  "glossary": {
    "terms": [
      {
        "id": "guest",
        "kind": "actor",
        "canonical": "Guest",
        "definition": null,
        "source": null
      },
      {
        "id": "room",
        "kind": "object",
        "canonical": "Room",
        "definition": null,
        "source": null
      },
      {
        "id": "search",
        "kind": "verb",
        "canonical": "search",
        "definition": null,
        "source": null
      }
    ]
  },
  "stories": [
    {
      "id": "book",
      "title": "Book",
      "activities": [
        {
          "id": 1,
          "paths": [
            {
              "parts": [
                {
                  "term_id": "guest",
                  "display": "Alice"
                },
                {
                  "term_id": "search",
                  "display": "searches for"
                },
                {
                  "term_id": "room",
                  "display": "Deluxe Suite"
                }
              ]
            }
          ]
        }
      ],
      "source": null
    }
  ]
}
When
the Glossary aggregations are built
Then
the entity terms collect their Instances
the verb collects its Inflection but not its canonical form
Terms referenced by an activity record the story
✓ passed 0ms
Given
a Story whose Activity references an actor and a verb
When
the Glossary aggregations are built
Then
the actor and the verb each list that Story
A story referencing a term twice lists it once
✓ passed 0ms
Given
a Story whose two activities repeat the same Term and the same Inflection
When
the Glossary aggregations are built
Then
the Story and the Inflection appear once each
A canonical entity reference is not an instance, whatever its case
✓ passed 0ms
Given
a Story activity referencing entities by canonical name, and a Step referencing one in lowercase
When
the Glossary aggregations are built
Then
neither entity term records an Instance
A kindless term records only its story ref
✓ passed 0ms
Given
a Kindless Term referenced by a Story activity
When
the Glossary aggregations are built
Then
the Term lists the Story but no Instance and no Inflection
An instance seen in a fixture step records its fixture provenance
✓ passed 0ms
Given
a Scenario whose fixture-sourced Step names an Instance
When
the Glossary aggregations are built
Then
the Instance carries the fixture name
The term index maps each term to its scenarios once
✓ passed 0ms
Given
a Scenario referencing one Term in two steps and another in its name
When
the term-scenario index is built
Then
each Term maps to the scenario exactly once
Parameter coloring marks placeholders and table headers
✓ passed 145ms
Given
a Report holding a Parametrized scenario with a Parameter table
When
the Renderer renders the HTML page
Then
Parameter coloring classes mark the grouped placeholder and the table headers
the page carries one generated color rule per column, after the stylesheet so a term ref bound to a column takes the column ink
each column ink is a token set once per theme, so the dark theme only redefines the token
A passed scenario renders as a checked heading with step bullets
✓ passed 0ms
Given
a Report holding a passed Scenario with three steps
When
the Markdown Report is rendered
Then
the heading is checked and each Step is a phase bullet
# pytest-given — proj

## ✓ Buy coffee
`tests/t.py::test_buy` · billing, happy-path

- **given** a machine
- **when** I insert $2
- **then** I get a coffee
Nested steps indent under their parent
✓ passed 0ms
Given
a Scenario whose when Step has a nested child
When
the Markdown Report is rendered
Then
the child bullet indents under its parent
# pytest-given — proj

## ✓ Nest
`tests/t.py::test_nest`

- **when** outer
  - **when** inner
Structured narration renders terms, values and placeholders
✓ passed 0ms
Given
a Step whose Narration carries a Term ref, a value and a placeholder
When
the Markdown Report is rendered
Then
the Term ref renders in guillemets, the value verbatim and the placeholder in braces
# pytest-given — proj

## ✓ ignored
`tests/t.py::test_parts`

- **when** a «Guest»42{amount}
A parametrized scenario renders its parameter table
✓ passed 0ms
Given
a Parametrized scenario with a two-Case Parameter table
When
the Markdown Report is rendered
Then
the heading counts the cases and the Parameter table lists each row
# pytest-given — proj

## ✓ Pricing · 2 cases
`tests/t.py::test_price`

- **when** insert

| euros | expect | |
|---|---|---|
| 1 | False | ✓ |
| 2 | True | ✓ |
A failing step is marked with a minimal error digest
✓ passed 0ms
Given
a failed Scenario carrying a two-line error and an internal frame
{
  "message": "ValueError: not sold out\nassert 1 == 0",
  "frames": [
    {
      "path": "/x/_pytest/runner.py",
      "lineno": 1,
      "func": "run",
      "code": "",
      "is_internal": true
    },
    {
      "path": "/x/tests/test_shop.py",
      "lineno": 88,
      "func": "test_sold_out",
      "code": "buy(m)",
      "is_internal": false
    }
  ],
  "error_tail": null
}
When
the Markdown Report is rendered
Then
the heading is crossed and the error follows the steps
# pytest-given — proj

## ✗ Sold out
`tests/t.py::test_sold_out`

- **then** reports sold out

> ValueError: not sold out
> test_shop.py:88 in test_sold_out
only the first message line and the non-internal frame are quoted
A multi-line attachment renders as a fenced block
✓ passed 0ms
Given
a Step carrying a multi-line Attachment
When
the Markdown Report is rendered
Then
the Attachment content sits in an indented fence, not inline
# pytest-given — proj

## ✓ Multi
`tests/t.py::test_multiline`

- **then** result
  - 📎 Doc:
    ```
    line1
    line2
    ```
A skipped scenario shows its skip reason
✓ passed 0ms
Given
a skipped Scenario with a reason
When
the Markdown Report is rendered
Then
the heading is marked skipped and the reason follows the node id
# pytest-given — proj

## ○ Later · skipped
`tests/t.py::test_skip` — reason: needs fixture data

- **when** act
The JSON report carries each activity's coverage
✓ passed 1ms
Given
a Story with a covered, an uncovered, an untracked Activity
When
the JSON sink is rendered
Then
a top-level `coverage` lists every activity once
the rest of the report is the input dict, unchanged
A re-rendered report recomputes coverage rather than carrying it
✓ passed 0ms
Given
a saved report dict whose `coverage` no longer matches its steps
When
`pytest-given report` re-renders it to JSON
Then
the coverage is the one the steps actually earn
The literal `none` disables the source link
✓ passed 0ms
Given
the source link config set to `none`
When
the config value is resolved
Then
no template comes back, so no link is rendered
A named editor preset becomes that editor's source link template
✓ passed 0ms
Given
the config set to the {preset} preset
When
the config value is resolved
Then
the template is that editor's URL scheme
preset url_scheme status
vscode vscode://file/{path}:{line}
cursor cursor://file/{path}:{line}
zed zed://file/{path}:{line}
pycharm pycharm://open?file={path}&line={line}
A raw URL template is used as the source link verbatim
✓ passed 0ms
Given
a raw blob-URL template rather than a preset name
When
the config value is resolved
Then
it comes back unchanged
An unknown preset name is refused, with the valid ones listed
✓ passed 0ms
Given
a bareword that is neither a known preset nor a template
When
the config value is resolved
Then
the value is refused
the error names the offender and lists every valid preset
The github preset prefers GITHUB_REPOSITORY over the git remote
✓ passed 0ms
Given
GITHUB_REPOSITORY naming one repository
an origin remote naming a different one
When
the github preset is resolved
Then
the template points at the environment's repository
The github preset derives org and repo from the git origin remote
✓ passed 0ms
Given
no GITHUB_REPOSITORY, and an https origin remote
When
the github preset is resolved
Then
the blob-URL template names the remote's org and repo
The github preset refuses a remote that is not on GitHub
✓ passed 0ms
Given
no GITHUB_REPOSITORY, and an origin remote on another host
When
the github preset is resolved
Then
the preset is refused
the error points at the env var and the raw-template escape hatch
An under-anchored activity is flagged ineligible in rollups
✓ passed 0ms
Given
a Story with an anchored and an under-anchored Activity
When
the story rollups are built
Then
only the anchored Activity is Coverage-eligible
A pinned under-anchored activity stops reading as untracked
✓ passed 0ms
Given
a Story whose only Activity is under-anchored
a Scenario whose step pins it by id
When
the story rollups are built
Then
it stays narration-ineligible but is no longer untracked
An Activity is labeled by the prose of its paths
✓ passed 0ms
Given
a Story with a two-path activity
When
the activity labels are built
Then
the label reads as prose under a story-scoped key, with the path texts joined
Grouping collapses parametrize cases into one scenario
✓ passed 0ms
Given
three Case records of one Parametrized scenario
When
the grouping pass collapses them
Then
one scenario remains and any failed Case fails it
A parametrized scenario keeps its place among the scenarios around it
✓ passed 0ms
Given
a plain scenario between two parametrized ones
When
the grouping pass runs
Then
the report lists them in the order the file declares
The grouped tree comes from the first passed case
✓ passed 0ms
Given
a skipped first Case and a second one that ran
When
the cases are grouped
Then
the tree is the one the passed Case recorded
A plain-str narration that varies across cases is refused
✓ passed 0ms
Given
two cases whose text differs but records no parts
When
the cases are grouped
Then
the grouping is refused
the error names the test, the missing parts and the t-string fix
it names the Case whose values were baked in, and the per-case opt-out
A narrated value that varies becomes a derived parameter table column
✓ passed 0ms
Given
two cases narrating a value that differs
When
templatizing walks the cases
Then
the value becomes a derived column beside the parametrize one
the Step keeps a placeholder pointing at that column
the placeholder keeps the format spec and conversion it narrated
A varying interpolation that is not a bare name is refused
✓ passed 0ms
Given
two cases narrating a computed expression
When
the cases are grouped
Then
the grouping is refused
the error quotes the expression and shows the bind-a-local fix
A parameter table cell reads the way the scenario name formats it
✓ passed 0ms
Given
a Template scenario name formatting its parameter
When
the cases are grouped
Then
the cells carry the formatting the name declared
A scenario name formatting a parameter a step reads plainly gets its own column
✓ passed 0ms
Given
a name formatting the parameter and a step reading it plainly
When
the cases are grouped
Then
the name points at a column holding what it renders
the name renders the disambiguated token, text and parts agreeing
A step formatting a parameter the scenario name reads plainly gets its own column
✓ passed 0ms
Given
a step formatting the parameter and a name reading it plainly
When
the cases are grouped
Then
the step points at a column holding what it renders
the step renders the disambiguated token, text and parts agreeing
A step narrating a parameter its column no longer holds is refused
✓ passed 0ms
Given
two cases narrating a value their column lacks
When
the cases are grouped
Then
the grouping is refused
the error names the column and what the case actually narrated
A term ref whose display differs between cases is refused
✓ passed 0ms
Given
two cases whose Term ref reads differently
When
the cases are grouped
Then
the grouping is refused
the error names the Term ref and the split-it-out fix
A term ref that *is* the parametrize value is refused too
✓ passed 0ms
Given
two cases whose Term ref is the parameter itself
When
the cases are grouped
Then
the grouping is refused
the error points at the per-case scenario opt-out
An attachment whose payload varies becomes an attachment column
✓ passed 0ms
Given
two cases attaching a label with differing payloads
When
templatizing walks the cases
Then
the payload moves into an attachment column
the Step keeps a content-less badge pointing at it
A step whose set of attachment labels differs between cases is refused
✓ passed 0ms
Given
an Attachment label only one Case attaches
When
the cases are grouped
Then
the grouping is refused
the error names the label, the case, and asks for a constant one
A parameter table cell reads the way the step that points at it read
✓ passed 0ms
Given
two cases narrating a parameter with a format spec
When
grouping builds the parameter table
Then
each cell carries the formatted text, under one column
the step keeps its placeholder, which that cell substitutes into
Cases that narrate different steps are refused rather than grouped
✓ passed 0ms
Given
two cases whose step trees differ
When
the cases are grouped
Then
the grouping is refused
the error names the divergence and the opt-out that answers it
A step narrating a glossary term parameter keeps pointing at its parameter table column
✓ passed 0ms
Given
a step narrating a parameter bound to a glossary term instance
When
the cases are grouped
Then
the parameter table holds the term displays alone
the step still points at that column
A parametrized scenario can decline the grouping and keep one scenario per case
✓ passed 0ms
Given
two cases of a scenario that opted out
When
the grouping pass runs
Then
each case stands alone, with no parameter table
Adopt pytest-given
12 activities37 scenarios
Domain ExperttellsStoryto theDeveloper
no coverage
DevelopercapturesStoryasActivity
3 scenarios
DeveloperbuildsGlossarywith theDomain Expert
3 scenarios
AgentwritesScenariowithTagagainst theGlossary
1 scenario
AgentnarratesStepwith aPhase
3 scenarios
AgentattachesAttachmentto aStep
1 scenario
CollectorrecordsStepon theStep stack
3 scenarios
CollectorgraftsFixture recordingfrom aStep fixture
4 scenarios
CollectorgroupsParametrized scenariointo aParameter table
4 scenarios
RendererrendersReportwithParameter coloring
1 scenario
Narration lintflagsScenarioagainst aLint rule
11 scenarios
Domain ExpertreviewsScenarioin theReport
no coverage
Scenarios
✓ passed
Covers:
✓ passed
Covers:
✓ passed
Covers:
✓ passed
Covers:
✓ passed
✓ passed
Covers:
✓ passed
Covers:
✓ passed
Covers:
✓ passed
Covers:
✓ passed
Covers:
✓ passed
Covers:
✓ passed
Covers:
✓ passed
Covers:
✓ passed
Covers:
✓ passed
Covers:
✓ passed
Covers:
✓ passed
Covers:
✓ passed
Covers:
✓ passed
Covers:
✓ passed
Covers:
✓ passed
Covers:
✓ passed
Covers:
✓ passed
Covers:
✓ passed
Covers:
✓ passed
Covers:
✓ passed
Covers:
✓ passed
Covers:
✓ passed
Covers:
✓ passed
Covers:
✓ passed
Covers:
✓ passed
Covers:
✓ passed
Covers:
✓ passed
Covers:
✓ passed
Covers:
✓ passed
✓ passed
✓ passed
Covers:
Glossary
54 terms 6 actors 16 work objects 2 verbs 30 uncategorized
Actors
Collector
The per-session object accumulating scenarios, fixture recordings, and parameter info, published through a ContextVar so a nested run can displace and restore it.
1 story11 scenarios
Renderer
Converts a JSON report into a rendering — a self-contained HTML page or a Markdown document.
1 story2 scenarios
Narration lint
The --given-lint pass that checks recorded scenarios against the Lint rule catalog once the Report model is built. It enforces the structural subset of the narration conventions — it cannot check whether a Step's text is semantically true.
1 story10 scenarios
Developer
Person who writes the application code and — together with the Agent — the scenarios; curates the glossary with the Domain Expert.
1 story
Domain Expert
Person who owns the domain knowledge and the ubiquitous language; source of domain stories and reviewer of scenarios and reports. A stakeholder in the broad sense.
1 story
Agent
AI coding agent that authors and maintains scenarios alongside the Developer, guided by the pytest-given skills.
1 story
Work Objects
Scenario
A test function decorated with @scenario(...). Not every pytest test is a scenario — undecorated tests are tolerated but not collected.
1 instance1 story35 scenarios
Step
A unit of narration: a with given(...) / when(...) / then(...) block, or the root recording from a step fixture. Steps nest, and each carries a phase, narration, attachments and children — but no status or error of its own: a failure is recorded on the Scenario, and per Case on the Parameter table, which is where both renderers read it.
2 instances1 story48 scenarios
Phase
The category of a step: given, when, or then. A step has exactly one phase.
1 instance1 story3 scenarios
Tag
Free-form string label attached via @scenario(name, tags=[...]). Used by the report's filter UI, where a / nests it (ticket/ABC-123 under ticket) and selecting a prefix filters to every tag beneath it.
1 story2 scenarios
Attachment
A labeled blob (text or JSON) bound to the currently-active step via attach(label, content).
1 story6 scenarios
Parametrized scenario
A @scenario-decorated test that also carries @pytest.mark.parametrize(...). Produces multiple scenario records during a run; pytest-given groups them.
1 story6 scenarios
Parameter table
The per-scenario grouping of typed columns + cases. A column is a param (a @pytest.mark.parametrize input), a derived (a narrated value that varies across cases) or an attachment (an attachment whose payload varies). Appears in the report below the grouped-template steps.
1 story9 scenarios
Step fixture
A pytest fixture whose function is wrapped with @given(text). Only @given is allowed on fixtures; @when / @then are rejected.
1 story3 scenarios
Fixture recording
A captured subtree of steps + attachments produced while a step fixture is being set up. Stored keyed by fixture-instance identity. A generator fixture's teardown records nothing — it refuses steps and attachments.
1 story5 scenarios
Step stack
The chain of currently-open steps; entered by with given(...), popped on exit. Mirrored inside a fixture recording while a fixture body is running.
1 story1 scenario
Report
The output artifact: a JSON data file and optional self-contained HTML and Markdown renderings derived from it. The JSON is the source of truth.
1 story20 scenarios
Parameter coloring
Each parametrize column gets a stable highlight color; placeholders and matching values share that color wherever they appear in step text and the parameter table.
1 story1 scenario
Lint rule
One named check, carrying a surface and a default Severity. A runtime rule reads the recorded scenarios; an ast rule parses the step bodies' source. The catalog in lint/base.py is data, so severities, config validation and docs stay in sync with one table.
1 instance1 story10 scenarios
Glossary
The Ubiquitous-Language concept — the shared vocabulary a domain speaks in — and the class that realizes it: Glossary(), with .actor(...), .work_object(...), .verb(...) registration methods and g('foo') (declare-or-get a kindless term, optional definition=) / g['foo'] (get-only, raises on unknown) accessor forms.
1 story13 scenarios
Story
A named flow modeled as a sequence of activities. Constructed by story('Title', [activity(...), ...]). Stories are first-class report tabs and the unit of coverage.
1 instance1 story28 scenarios
Activity
One row in a story — typically actor + verb + work_object plus optional connective words. Constructed by activity(...).
1 instance1 story26 scenarios
Verbs
Group
Collapsing the N scenario records of a parametrized scenario into one logical scenario carrying a Parameter table. Cases group when they share both the same name and the same Node ID without its parametrize tail — one test function; two same-named scenarios on different test functions stay separate. @scenario(group_parametrized=False) declines the merge, so each case lives on as its own scenario.
1 story17 scenarios
Graft
Attaching a fixture recording into the active scenario's step tree at the moment its host test starts.
1 story5 scenarios
Uncategorized
Narration
The human-readable text on a step or scenario name. A Narration bundles the flat rendered text with parts — empty for plain-string authoring, and for a t-string or pytest_given.Template a list of NarrationLiteral / NarrationValue / NarrationPlaceholder / NarrationTermRef pieces. The structured form lets the templatizer and renderer treat parametrize-bound values specially without regex tricks.
9 scenarios
when_then
A step-authoring helper that emits a when action and its then outcome as two sibling steps from a single with block. Used mainly to narrate an expected raise (with when_then('the action', 'the error is raised'), pytest.raises(...)), so the action and its outcome stay distinct steps.
5 scenarios
Case
One row of a parametrized scenario — a single tuple of parameter values, its status, and any error.
20 scenarios
Templatize
Derive the grouped-template step text by comparing every comparable case: what all of them share stays inline, and anything that varies becomes a {name} placeholder or attachment badge pointing at a parameter-table column. The baseline tree comes from the first passed case, and every other passed case must narrate that same template.
5 scenarios
Plain fixture
A pytest fixture without a pytest-given decorator. Used by tests but produces no step in the report.
Active scenario
The scenario currently being recorded into; tracked by node ID.
12 scenarios
Node ID
A pytest test identifier (e.g., tests/test_x.py::test_y[a-b]). Used as a key throughout the collector.
2 scenarios
Value highlight
A neutral highlight applied to t-string interpolation values that don't correspond to a parametrize column and are constant across every case (e.g., a computed expression like price * 1.2). One that varies becomes a derived column instead.
1 scenario
Source link
A clickable file:line anchor on a scenario card, a story panel, or an expanded glossary term card, resolved from the given_source_link config — a preset name like vscode / github, or a raw URL template. Captured as a SourceLocation (POSIX relpath + 1-indexed line) from pytest.Item.location for a scenario, from the declaration site for a Story or Term. Disabled by default.
6 scenarios
Theme
The HTML report's colour scheme — light, dark, or auto (follow the viewer's system). The given_theme config sets the default a report opens in; a viewer's own choice from the report's theme control, once made, wins over it in that browser.
2 scenarios
Finding
One Lint rule firing on one subject: the rule id, the Severity it fired at, the subject, a Source link location and a message naming the offender.
11 scenarios
Severity
A Lint rule's level — off, warn or error — defaulted by the catalog and overridable per rule via given_lint_rules. Only error fails the run; warn reports in the terminal summary; an off rule is not evaluated at all.
4 scenarios
File glossary
A glossary loaded from a Markdown file, via the FileGlossary(path) class. It parses all GFM pipe tables in the file into the same inner Glossary model; terms are accessed by name (g['Guest'], case-insensitive). Kind inference fills in term kinds post-collection from activity slot positions when no explicit kind_column is configured.
21 scenarios
Deferred term
A term handed over before its kind is settled — what g('foo') and g['foo'] on a code glossary return, and every file glossary lookup. The handle is the same TermHandle the typed registrations (g.actor(...) and friends) hand back; declared_kind is None is what marks the deferral. The deferral is in the handing over, not the term: a row with an explicit kind_column arrives through the same handle already kinded, while the rest stay None until kind inference runs.
2 scenarios
Term
A registered glossary entry: an Actor, Work Object, Verb, or kindless term. Each carries an id (slug), a canonical name, a kind (None when kindless), and an optional definition (`str
54 scenarios
Handle
The Python object a glossary hands back for a termTermHandle, the same type from every accessor (g.actor(...), g('foo'), g['Guest'], a captured guest = ...). It is what steps, scenario titles and activities interpolate, in one of three surface forms: bare (room — the canonical display), `.low` (room.low — lowercased), or called (room('Deluxe Suite') — an instance on an Actor or Work Object, an inflection on a Verb). Each form renders as a term ref.
Actor
A glossary term for a participant in the domain (e.g., Guest). Carries the actor kind color: a wash in narration, a pill in the Glossary view.
27 scenarios
Work Object
A glossary term for a thing acted on (e.g., Room, Booking). Carries the work-object kind color: a wash in narration, a pill in the Glossary view.
12 scenarios
Verb
A glossary term for an action (e.g., book, confirm). Verbs accept inflections — calling book('books') records books as a surface form of the canonical book.
25 scenarios
Term ref
An occurrence of a term inside narration. Modeled as NarrationTermRef in step text and as ActivityTermRef inside activity prose.
18 scenarios
Instance
A named refinement of an Actor or Work Object (e.g., guest('Alice') is an instance of the Guest actor). Instances aggregate in the Glossary tab's refs block.
9 scenarios
Inflection
A surface form of a Verb other than its canonical name (e.g., searches for as an inflection of search). Reported under "Also used as:" in the Glossary.
9 scenarios
Activity Part
The two-variant union making up an activity's prose (ActivityPart = ActivityTermRef or ActivityWord): a reference to a glossary term, whose kind resolves via the glossary, or a bare path word — a node label or edge connective that carries no kind or id, is never classified by inference, and never reaches the glossary. That last is what separates it from a kindless term, which has an id and is tracked.
4 scenarios
Path
A branching segment inside a story — path(...) lets alternate activity sequences share a prefix.
19 scenarios
Slot
A position role in an activity path, from its node/edge alternation: position 0 is the actor slot, odd positions are verb slots, and even positions ≥ 2 are noun slots. Slots drive both path validation and kind inference.
14 scenarios
Scenario↔activity binding
The link between a scenario (or step) and one or more story activities. Carried by @scenario(story=, activities=) and the activity= kwarg on given/when/then.
3 scenarios
Coverage
The "did this scenario touch that activity" relation. Computed by the A_refs ⊆ S rule: an activity is covered when the set of terms it references is a subset of the terms a single step references — matching is per step, not against the union across steps, and on terms, not surface forms: an instance or inflection counts as its term. A step can also cover an activity explicitly via the activity= pin, regardless of its narration or its term count.
15 scenarios
Kind inference
The post-collection pass (infer_glossary_kinds) that assigns each undeclared term a kind from the slot positions it occupies across all story activities; declared kinds are verified against observed positions instead. A term used in incompatible slots (or conflicting with its declared kind) raises. A term never referenced by any activity stays kindless.
5 scenarios
Kindless
A term with kind=None — left unset when kind inference finds no story slot to infer from (a term used only in t-string steps, never in an activity). Shown in the report's Uncategorized bucket.
11 scenarios
Undefined
A term with definition is None; surfaced by a badge and filter in the Glossary view. Orthogonal to kindless.
1 scenario
No terms match this filter.