Flip the default: background steps render inline with the scenario's regular steps in one
Steps list. Separating them into the collapsible Background Steps section becomes opt-in
configuration. When combined, repeated primary keywords collapse to And — so
Given / Given / When reads Given / And / When, not Given / Given / When.
| Source | Path | Keyword provenance |
|---|---|---|
Gherkin Background: via ReqNRoll |
ScenarioInfoEnumerableExtensions.cs:72 | StepInstance.StepDefinitionKeyword — a ReqNRoll enum, so always English (ReqNRollTrackingHooks.cs:74,128) |
| Cucumber messages ingest | CucumberFeatureSynthesizer.cs:397,497 | The literal Gherkin keyword, trimmed — localised for non-English dialects |
Generic ingest (background: true records) |
FeatureSynthesizer.cs:81,147 | Producer-supplied |
| Heuristic common-prefix detection | FeatureSynthesizer.cs:233-241 → BackgroundStepsDetector.cs | Whatever the framework recorded |
All four land in Scenario.BackgroundSteps (Scenario.cs:24).
Shared instances. BackgroundStepsDetector assigns the same array instance
(members[0].Steps.Take(n).ToArray()) to every scenario in a Rule group
(BackgroundStepsDetector.cs:45-50) — the
ScenarioStep records are shared.
Concurrent readers. RunOutputs is Parallel.Invoke
(ReportGenerator.cs:340-354): the Specifications
HTML, the TestRunReport HTML, and the JSON/XML/YAML data writers all run at the same time over
the same Scenario and ScenarioStep instances.
Together these make keyword rewriting-by-mutation a data race that would leak And into the data
files. The keyword change must be a render-time projection. (StepText.ApplyToFeatures
(StepText.cs:137-148) does mutate shared background steps,
but it runs before RunOutputs and capitalisation is idempotent — it is not a precedent for this.)
Two duplicated blocks, both emitting <details class="scenario-background"><summary class="h4">Background Steps</summary>:
- ReportGenerator.cs:1213-1226 — normal scenario
- ReportGenerator.cs:1993-2006 — parameterized-group detail panels
Each is followed by a near-identical scenario-steps block
(1228-1247,
2008-2026) doing three things the background block
does not: ShouldRenderCombinedTable, the afterThen tracker (drives skipTabularInline), and
RenderCombinedTabularParameters.
CSS: stylesheets.css:598-627.
Both HTML reports (Specifications and TestRunReport) go through the same GenerateHtmlReport, so
they change together.
ReportConfigurationOptions.InlineBackgroundSteps
(ReportConfigurationOptions.cs:242-243) is
declared, documented in the wiki and CHANGELOG, and never read anywhere. GenerateHtmlReport has
no corresponding parameter. Its "test"
(StepRenderingReportTests.cs:836-849)
calls GenerateReportWithInlineBackground, a helper byte-identical to GenerateReport, and asserts
only that both step texts appear — it passes vacuously. Line 839 admits it:
// inlineBackgroundSteps option not yet implemented.
No user can currently be depending on it doing anything.
OrderedStepPaths (ReportGenerator.cs:3008-3015)
enumerates background steps first with path b0, b1, … then regular steps 0, 1, …, and
AttributeInteractionsToSteps' doc comment states attribution is positional "in document order
(background steps first)" (2928-2938).
REPORT_QUERY_PLAN.md §276 depends on that backgroundSteps/steps index path.
Two consequences: combined rendering matches the interaction-attribution model exactly, and the data outputs must keep the two collections separate.
toggle_expand_collapse is only wired to details.feature and details.scenario
(ReportGenerator.cs:889). The background <details>
is in neither selector, so "Expand All Scenarios" never opens the Background section — every
E2E test has to click its summary explicitly. Combining removes that dead end. It also means a
misfiring common-prefix heuristic no longer hides real steps behind a collapsed disclosure.
All are pre-existing, all get fixed here under TDD (per CLAUDE.md), all are made more visible by the default change.
CollectStepText is called on scenario.Steps at four sites
(1155,
1732,
1830,
1916) and never on BackgroundSteps. The
search box filters purely on the resulting data-search / data-row-search attributes
(report-search-function.js), so text that lives
only in a background step is unfindable.
With showStepNumbers, the background list numbers 1..n and the steps list restarts at 1
(1219 vs
1240). Two steps in one scenario are both "1.".
var hasAnyDetail = scenarios.Any(s => s.Steps is { Length: > 0 } || s.Result == ExecutionResult.Failed);BackgroundSteps is not consulted, so a passing scenario whose entire step list was extracted as
background renders no detail panel at all — the steps vanish from the report.
This is reachable, not theoretical: BackgroundStepsDetector deliberately permits the whole list to
become background ("or when all steps are common (no remaining steps)",
BackgroundStepsDetector.cs:36-42), leaving
Steps as an empty array. A scenario outline whose example rows share identical step text — no
<param> interpolation in the step prose — hits exactly this.
ReportGenerator.cs:656 and
697-709 build hasAnySteps and allSteps from
s.Steps only. The summary table therefore under-counts, and a feature whose steps all became
background loses the four step columns entirely. In combined mode the mismatch becomes plainly
visible: the table says 2, the scenario below lists 4.
Four writers emit Steps and no background:
GenerateYamlSpecs— 1511-1516 (public API)GenerateSpecificationsYaml— 3772-3777GenerateSpecificationsJson— 3804GenerateSpecificationsXml— 3838
The Specifications HTML shows them; Specifications.yml/.json/.xml — the living-documentation
artefact — silently omits them. The TestRunReport writers get this right
(JSON 2999,
XML 3299,
YAML 3426), so there is an established shape to copy.
SpecificationsDataTests has no background case at all, which is why this survived.
Fix by emitting a sibling BackgroundSteps node, matching the TestRunReport writers — not by
merging into Steps, which would make the data lossy and break the b{i} path convention of §1.5.
There is no Specifications schema (only GenerateTestRunReportSchema,
280), so no schema change is needed.
Recommendation: add SeparateBackgroundSteps (bool, default false), mirroring SeparateSetup
(ReportConfigurationOptions.cs:53), and mark
InlineBackgroundSteps [Obsolete].
Why a new name rather than flipping InlineBackgroundSteps to default true:
InlineBackgroundSteps = falsewould then mean "separate", which reads backwards.- A
boolcannot distinguish "left at default" from "explicitlyfalse", so honouring the old property would silently give anyone who copied the wiki'sInlineBackgroundSteps = falseexample (Step-Tracking.md:488) the old behaviour — from a property that has never had an effect. Separate*matches the naming already in the record.
[Obsolete("Background steps are inlined by default. Set SeparateBackgroundSteps = true for the old separate section.")],
kept one minor version. Ignoring it changes nothing for anyone, since it was already a no-op.
Alternative if you'd rather not grow the surface: delete InlineBackgroundSteps outright. Binary-breaking
on a property that has never done anything. Say the word.
One option or two? showStepNumbers is split per report
(SpecificationsShowStepNumbers / TestRunReportShowStepNumbers), but GroupParameterizedTests,
TitleizeParameterNames and MaxParameterColumns are shared. Background-step layout is a
presentation-consistency concern across both reports, so: one shared option. Easy to split later
if asked.
New helper src/Kronikol/Reports/StepKeywordCollapser.cs:
/// Returns the keyword to *display* for each step, collapsing a repeat of the
/// current primary keyword to "And". Never mutates the steps.
public static string?[] DisplayKeywords(IReadOnlyList<ScenarioStep> steps)Walking the list, tracking current = the last primary keyword seen:
| Step keyword | Displayed as | Effect on current |
|---|---|---|
Given / When / Then (case-insensitive, trimmed), ≠ current |
unchanged | becomes current |
Given / When / Then, == current |
And |
unchanged |
And / But / * |
unchanged | unchanged (conjunctions inherit) |
null / empty |
unchanged | unchanged |
| anything else (localised or free-form) | unchanged | current reset to null |
Reuse the exact vocabulary already established by
IngestAttribution.PhaseForStep (IngestAttribution.cs:172-182) —
given/context, when/then/action/outcome/butwhen, and/but/conjunction/* — rather than inventing
a second keyword table. Note butwhen is in that list and must be treated as a primary, not a
conjunction.
Casing of the emitted And follows the keyword it replaces (GIVEN → AND).
Localisation is a deliberate non-goal. We cannot synthesise a German Und from Angenommen, so
unrecognised keywords pass through untouched and disable collapsing until the next recognised
primary. This costs nothing on the main path: ReqNRoll supplies an English enum (§1.1), so only the
Cucumber-messages ingest of a non-English feature file is affected, and it degrades to today's
rendering. See §6 for the optional fix.
RenderStep (2290) gains an optional
string? displayKeyword = null, used in place of step.Keyword at
2352. No model mutation — this is what keeps §1.2 safe.
Scope of collapsing — its own option. Collapsing only at the background/steps seam would be
inconsistent (Given / Given inside a plain scenario still renders doubled). Recommendation: apply
to the whole rendered list, gated by CollapseRepeatedStepKeywords (bool, default true), so
literal keywords are one flag away.
With SeparateBackgroundSteps = true the two lists collapse independently, so the separated
Steps section still opens with its own Given.
Background steps keep a marker: RenderStep emits class="step step-background" (and
step step-collapsible step-background) when the step came from the background. CSS gives it a muted
left accent consistent with the existing .scenario-background border. This also gives the E2E tests
a stable hook.
BackgroundStepsDetectorand every ingestion path — detection is unchanged; only presentation moves.- The
backgroundSteps/stepssplit in all data outputs, the merge reader (MergeableReportReader.cs:92), attachment copying (3717) and theb{i}step-path convention. - Diagram generation.
hasStepDelimitersis computed from<<stepDelimiter>>markers in the PlantUML (559-562), not from the HTML step list, andBuildStepWindowsskipsrecord.Background == true(IngestAttribution.cs:196-205). Explicit background steps therefore have no delimiter bar while heuristic ones do — unchanged by this work, but slightly more noticeable once the two kinds of step sit in one list. Worth a wiki sentence, not a code change. CiSummaryGenerator— renders no steps at all. Unaffected.ScenarioStableId— hashes names and example values, not steps. Unaffected.
New tests/Kronikol.Tests/Reports/StepKeywordCollapserTests.cs. Red first:
Given / Given / When→Given / And / WhenGiven / When / When / Then / Then→Given / When / And / Then / AndGiven / And / Given→Given / And / And(a conjunction does not resetcurrent)Given / Then / Given→ unchanged (not a repeat of the current keyword)But,*,null, empty pass through and do not resetcurrentButWhentreated as a primary- unrecognised keyword (
Angenommen) passes through and resetscurrent, so a followingGivenis not collapsed GIVEN / GIVEN→GIVEN / AND(casing follows source)- keywords with surrounding whitespace (
"Given ") are matched trimmed - input steps are not mutated
Then implement.
private static void RenderScenarioStepSections(
StringBuilder body, Scenario scenario, bool showStepNumbers,
bool separateBackgroundSteps, bool collapseRepeatedStepKeywords)- Combined (default): concatenate
BackgroundSteps ?? []+Steps ?? []; render nothing if empty. One<details class="scenario-steps" open><summary class="h4">Steps</summary>.ShouldRenderCombinedTable, theafterThentracker andRenderCombinedTabularParametersall take the combined array. Numbering1..Nacross the whole list. Indices< BackgroundSteps.Lengthgetstep-background. - Separated: current markup, plus the Bug 2 numbering fix (steps continue at
BackgroundSteps.Length + 1) and independent per-section collapsing.
Replace both call sites (1213-1247, 1993-2026). This removes ~35 lines of existing duplication and guarantees the two surfaces stay in step.
ReportConfigurationOptions:SeparateBackgroundSteps,CollapseRepeatedStepKeywords,[Obsolete]onInlineBackgroundSteps.GenerateHtmlReport(404-436): two new optional params at the end (source-compatible — every existing call uses named arguments).RenderParameterizedGroup(1682-1708): same two, threaded from 1110-1127.- Both
Add(...)sites — 250 (Specifications), 255 (TestRunReport).
Add CollectStepText(s.BackgroundSteps, …) beside each of the four existing calls. Red test: a
report whose only occurrence of a phrase is in a background step, asserting the phrase lands in
data-search; a parameterized-group variant asserting data-row-search. Applies in both modes.
Include s.BackgroundSteps is { Length: > 0 } in the predicate at
1980. Red test: a passing parameterized group whose
scenarios have only background steps currently renders no .param-detail-panel.
hasAnySteps and allSteps (656,
699-704) concatenate background steps. Red test:
feature with 2 background + 2 regular steps reports 4, not 2.
Add a BackgroundSteps node to all four writers, shaped like the TestRunReport equivalents. Red
tests in SpecificationsDataTests for YAML, JSON and XML, plus one for the public GenerateYamlSpecs.
.step-background in stylesheets.css. Keep the .scenario-background rules — separated mode still
uses them.
Fix the broken helper first: replace GenerateReportWithInlineBackground
(StepRenderingReportTests.cs:26-34)
with GenerateReportSeparateBackground that actually passes the flag. Rewrite
822-877:
Report_combines_background_steps_with_regular_steps_by_defaultReport_separates_background_steps_when_option_enabledReport_renders_and_keyword_for_background_given_followed_by_scenario_givenReport_separated_mode_does_not_collapse_across_sectionsReport_numbers_combined_steps_continuously/…_separated_steps_continuouslyReport_renders_combined_list_when_only_background_steps_exist(Steps = null)Report_renders_nothing_when_both_lists_emptyReport_combined_tabular_parameters_span_background_and_regular_steps— background step carrying a tabular parameter, confirmingShouldRenderCombinedTablesees the combined array- Parameterized-group equivalents for both modes — that second surface has no background coverage today
- A regression test for §1.2: generate the HTML and then assert the JSON data still says
Given, proving no keyword mutation leaked into the model
ReportTestHelper.GenerateReportWithBackground
(ReportTestHelper.cs:1551) uses
Given / And background and When / Then steps — it never exercises the Given / Given seam. Add a
fourth scenario with background [Given X] and steps starting Given Y, plus a
GenerateReportWithSeparatedBackground(...) and its PlaywrightTestBase wrapper
(PlaywrightTestBase.cs:65).
BackgroundRenderingTests.cs — all eight tests assert details.scenario-background:
| Existing | New form |
|---|---|
…render_background_section |
0 .scenario-background, 2 .step-background inside .scenario-steps |
Background_section_has_correct_summary_text |
the single summary reads Steps |
Background_section_is_collapsed_by_default |
replaced: the combined list is open by default (and needs no extra click — §1.6) |
…contains_correct_number_of_steps |
.scenario-steps .step count is 4 |
Background_steps_display_correct_text |
same texts, no second <details> to open |
…renders_before_steps_section |
background steps are the first .step children |
Scenario_without_background_has_no_background_section |
third scenario has zero .step-background |
Multiple_scenarios_with_same_background… |
both scenarios show their background steps inline |
New E2E:
Repeated_given_keyword_renders_as_andagainst the new fourth scenarioSeparatedBackgroundRenderingTests.csre-asserting the original eight against the separated fixtureSearch_matches_text_that_only_appears_in_a_background_step—FillSearchBar(), assert the scenario stays visible
Per the repo's Playwright rules: PollingInterval = 200, no Force = true, no network mocking,
.First/.Nth(n) on multi-match selectors.
../Kronikol.wiki/Report-Configuration.md:153— replace theInlineBackgroundStepsrow with the two new options; note the deprecation.../Kronikol.wiki/Step-Tracking.md:253-286, 486-498— rewrite "Background Steps": combined by default,Andcollapsing, the localisation caveat, the delimiter note from §3.4, how to opt into the separate section.../Kronikol.wiki/Generated-Reports.md:178— still describes the collapsible section as the behaviour.../Kronikol.wiki/API-Reference.md:55—BackgroundStepsDetectorentry implies a separate rendered section.CHANGELOG.md— behaviour change, two new options, one deprecation, five bug fixes.
Full suite green, then patch-bump all packages to one version, changelog, commit, tag v{version},
push commit and tag.
- Visible default change. Every report gains a longer
Stepslist and loses theBackground Stepsdisclosure.SeparateBackgroundSteps = truerestores it in one line. CollapseRepeatedStepKeywordsdefaults on and touches scenarios with no background — anyGiven / Givenanywhere becomesGiven / And. Wider blast radius than the background change itself. Defaulting itfalseand collapsing only across the seam is a one-flag adjustment; I'd still recommend on, since the alternative renders the same duplication it exists to fix.- Localised Gherkin passes through uncollapsed (§3.2). Affects only the Cucumber-messages ingest of non-English features; degrades to today's output.
- Kronikol4J output parity. The Java port's report output byte-matches .NET today; this diverges
it for any feature with background steps. There is no
java/tree in this repo, so it is a follow-up on that side, out of scope here. - Step numbers shift relative to external notes citing "step 3". Diagram delimiters key off keyword+text rather than index (§3.4), so diagrams are unaffected.
- The shared-instance +
Parallel.Invokeconstraint (§1.2) is the one way this change could silently corrupt data. Step 9's data-output regression test exists specifically to pin it. - Bugs 3–5 are behaviour changes to existing output in their own right. If you want them split into a separate release from the default flip, Steps 4–7 are independent of Steps 1–3 and can ship first.
ScenarioStep has no KeywordType, though the Cucumber pipeline knows it
(CucumberMessages.cs:201,
TestRunRecord.cs:65-72) and
CucumberStepWindow already carries it
(CucumberFeatureSynthesizer.cs:47).
Carrying it onto ScenarioStep would let the collapser (a) recognise a localised primary keyword by
its meaning and (b) recover the localised word for "And" — the literal keyword of any step in the
same feature whose KeywordType is Conjunction. That closes the §3.2 gap properly.
Cost: ScenarioStep + the three TestRunReport data writers + the JSON schema + the merge reader +
round-trip tests. Genuinely useful but clearly a separate piece of work; excluded from this plan
unless you want it folded in.
Source
src/Kronikol/ReportConfigurationOptions.cs— 2 new options, 1 obsoletedsrc/Kronikol/Reports/StepKeywordCollapser.cs— newsrc/Kronikol/Reports/ReportGenerator.cs— signature, 2 call sites, extracted renderer,RenderStep, 4 search sites,hasAnyDetail, Features-Summary counts, 4 specs data writerssrc/Kronikol/Reports/stylesheets.css—.step-background
Tests
tests/Kronikol.Tests/Reports/StepKeywordCollapserTests.cs— newtests/Kronikol.Tests/Reports/StepRenderingReportTests.cs— helper fix + rewritten background teststests/Kronikol.Tests/Reports/SpecificationsDataTests.cs— background coverage for YAML/JSON/XMLtests/Kronikol.Tests.EndToEnd/ReportTestHelper.cs,PlaywrightTestBase.cs— fixturestests/Kronikol.Tests.EndToEnd/BackgroundRenderingTests.cs— rewrittentests/Kronikol.Tests.EndToEnd/SeparatedBackgroundRenderingTests.cs— new
Docs
CHANGELOG.md,../Kronikol.wiki/{Report-Configuration,Step-Tracking,Generated-Reports,API-Reference}.md
Untouched by design: BackgroundStepsDetector, all ingestion paths, TestRunReport JSON/XML/YAML,
the JSON schema, the merge reader, CiSummaryGenerator, ScenarioStableId, diagram generation.