Selkie.git | docs/api/ | Selkie--Test--Snapshot--Harness.md
NAME
====
Selkie::Test::Snapshot::Harness - Fork-per-scenario snapshot runner
SYNOPSIS
========
In your app's `xt/02-snapshots.rakutest`:
```raku
use Test;
use lib 'lib';
use Selkie::Test::Snapshot::Harness;
run-snapshots;
```
That's the entire harness. Drop scenario scripts into `xt/snapshots/`, each one self-contained:
```raku
# xt/snapshots/01-my-widget.raku
use lib 'lib';
use Selkie::Test::Snapshot;
use My::App::Widget;
my $w = My::App::Widget.new(...);
print render-to-string($w, rows => 10, cols => 40);
```
DESCRIPTION
===========
Fork-per-scenario harness for widget snapshot testing. Each `.raku` file in the scenarios directory runs in its own subprocess. The harness captures stdout, normalises it (trim trailing whitespace, drop trailing blank rows), and compares against a golden file under `golden/`.
**Subprocess isolation matters.** There's a MoarVM specializer bug that intermittently crashes ("Spesh: releasing temp not in use") during rendering patterns involving NativeCall + nested widget planes. Per- scenario subprocesses give each render a cold spesh state. The harness additionally sets `MVM_SPESH_DISABLE=1` in the subprocess environment to eliminate the risk entirely — marginally slower, fully reliable.
When upstream MoarVM fixes the spesh bug we can drop `MVM_SPESH_DISABLE` but keep the subprocess isolation, since it's also useful for widget- test independence in general.
**Scenarios never touch the real terminal.** Each scenario process detaches from its controlling terminal before notcurses init (see [Selkie::Test::Snapshot](Selkie--Test--Snapshot.md)), so a run from an interactive shell — or under a release tool like `mi6 release` — cannot hang on terminal query replies or churn the terminal's state 60+ times in a row.
WORKFLOW
========
* First run / missing `.snap`: golden file is created, test passes.
* Matching output: test passes silently.
* Mismatch: test fails with a unified-ish diff in TAP diagnostics.
* `SELKIE_UPDATE_SNAPSHOTS=1`: overwrite every golden file.
ARGUMENTS
=========
`run-snapshots` accepts named args:
* `:snap-dir('xt/snapshots')` — directory containing scenario scripts
* `:golden-subdir('golden')` — subdirectory of `snap-dir` for goldens
* `:styled-golden-subdir('golden-styled')` — subdirectory for styled goldens; the harness routes scenarios that emit the `=== styled-snapshot v1 ===` marker (via [Selkie::Test::Snapshot](Selkie--Test--Snapshot.md)'s `:capture-styles` mode) here automatically
* `:raku-args` — extra `-I` flags for the subprocess (defaults to `-I lib`)
* `:disable-spesh(True)` — set `MVM_SPESH_DISABLE=1`. Set to False if you want to test with spesh enabled, but expect flakes.
STYLED SCENARIOS
================
Plain and styled scenarios can live in the same `xt/snapshots/` dir. The harness reads each subprocess's stdout, peeks at the first line, and routes to `golden/` or `golden-styled/` accordingly. No configuration is needed in scenario scripts beyond passing `:capture-styles` to `render-to-string`:
```raku
# xt/snapshots/24-heatmap-styled.raku
use lib 'lib';
use Selkie::Test::Snapshot;
use My::Heatmap;
print render-to-string(My::Heatmap.new(...), :rows(8), :cols(20), :capture-styles);
```
### sub run-snapshots
```raku
sub run-snapshots(
IO(Any) :$snap-dir = "xt/snapshots",
Str :$golden-subdir = "golden",
Str :$styled-golden-subdir = "golden-styled",
:@raku-args = Code.new,
Bool :$disable-spesh = Bool::True
) returns Mu
```
Run every `*.raku` file in `$snap-dir` as an isolated subprocess, capture stdout, and snapshot-test it against `{$snap-dir}/{$golden-subdir}/{name}.snap` (plain) or `{$snap-dir}/{$styled-golden-subdir}/{name}.snap` (styled, when the scenario emits the `=== styled-snapshot v1 ===` marker). Emits one TAP assertion per scenario. Call this from an xt/ rakutest file — nothing else needed.