From ce4f66009841ad48cf43e191a2d763c3e3c9bbf6 Mon Sep 17 00:00:00 2001 From: PJ Date: Sat, 15 Aug 2026 12:45:37 +0530 Subject: [PATCH] test(browser): drive an undefined reading through the whole web path four layers carry it: the page's envelope, the driver's unwrap, the runner's count check and the verifier's decode. each has a unit test and only a run proves they compose. goes red both ways, decoding an absent value as null and dropping the envelope. --- test/browser/browser_test.go | 18 +++++++++++++ .../testdata/undefined-extractor/index.html | 18 +++++++++++++ .../testdata/undefined-extractor/spec.ts | 26 +++++++++++++++++++ 3 files changed, 62 insertions(+) create mode 100644 test/browser/testdata/undefined-extractor/index.html create mode 100644 test/browser/testdata/undefined-extractor/spec.ts diff --git a/test/browser/browser_test.go b/test/browser/browser_test.go index b2b0b56..efc62d9 100644 --- a/test/browser/browser_test.go +++ b/test/browser/browser_test.go @@ -192,3 +192,21 @@ func testdataDir(t *testing.T) string { func specSrcDir(t *testing.T) string { return filepath.Join(repoRoot(t), "pkg", "spec", "src") } + +// TestBrowserUndefinedExtractorStaysUndefined drives the four layers of the +// undefined reading through one run: the page wraps each reading in a {value} +// envelope, the driver unwraps an absent value to an empty payload, the runner +// checks the page reported one reading per extractor, and the verifier decodes +// the empty payload as undefined. Each layer has its own unit test; only a run +// proves they compose. Written straight into the map instead, an undefined +// reading lost its whole index to JSON.stringify and that extractor silently +// kept goja's dump-derived value while its neighbours held the page's. +func TestBrowserUndefinedExtractorStaysUndefined(t *testing.T) { + violations := runFixture(t, "undefined-extractor") + if slices.Contains(violations, "undefinedStaysUndefined") { + t.Error("a reading the page could not take did not reach the spec as undefined") + } + if !slices.Contains(violations, "counterNeverMoves") { + t.Fatalf("nothing was ever tapped, so the property above held vacuously; violations=%v", violations) + } +} diff --git a/test/browser/testdata/undefined-extractor/index.html b/test/browser/testdata/undefined-extractor/index.html new file mode 100644 index 0000000..4df2a39 --- /dev/null +++ b/test/browser/testdata/undefined-extractor/index.html @@ -0,0 +1,18 @@ + + + + +
+ +
0
+
+ + + diff --git a/test/browser/testdata/undefined-extractor/spec.ts b/test/browser/testdata/undefined-extractor/spec.ts new file mode 100644 index 0000000..bf7fb6b --- /dev/null +++ b/test/browser/testdata/undefined-extractor/spec.ts @@ -0,0 +1,26 @@ +import { always, extract, taps } from "@sanderling/spec"; + +// Two extractors that resolve nothing, the shape every route-scoped extractor +// has on the steps it is off its own screen (folio has nine of them). JSON has +// no undefined, so these are what the {value} envelope exists for. +const absent = extract((s) => s.ax.find({ id: "nothing-is-ever-here" })).named("absent"); +const absentText = extract((s) => s.ax.find({ id: "missing" })?.text).named("absentText"); + +const bumps = extract((s) => { + const el = s.ax.find({ id: "count" }); + return el ? parseInt(el.text, 10) || 0 : 0; +}).named("bumps"); + +// A reading the page could not take must arrive as undefined, not as null and +// not as goja's reading of the dump. This property is what a spec sees. +const undefinedStaysUndefined = always( + () => absent.current === undefined && absentText.current === undefined, +); + +// The run has to actually be doing something, or the property above holds for +// the wrong reason. +const counterNeverMoves = always(() => bumps.current === 0); + +export const properties = { undefinedStaysUndefined, counterNeverMoves }; + +export const actionsRoot = taps;