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;