mirror of
https://github.com/priyanshujain/sanderling.git
synced 2026-10-02 11:07:10 +00:00
test: pin that a nested undefined does not survive the wire
This commit is contained in:
1 parent
0cef5bd1d3
commit
56d6aa53f1
2 files changed
+100
No files matched your search
@@ -177,3 +177,79 @@ func compactJSON(t *testing.T, source string) string {
|
|||||||
}
|
}
|
||||||
return compact.String()
|
return compact.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestExtractorEncoding_NestedUndefinedIsNotOnTheWire pins the one reading
|
||||||
|
// shape the two hosts do NOT encode alike, rather than hiding it.
|
||||||
|
//
|
||||||
|
// JSON has no undefined, so the page loses the whole key (asserted in
|
||||||
|
// pkg/spec/test/web-runtime.test.ts) while goja writes null. goja cannot mirror
|
||||||
|
// the drop: Export reports an undefined member and a null member identically as
|
||||||
|
// nil, so dropping those keys here would drop the genuine nulls the page keeps.
|
||||||
|
// Mirroring the other way, by writing null on the page, would break the one
|
||||||
|
// thing that does agree. Carrying the member across takes a wire format that
|
||||||
|
// can express undefined, which is a change to every layer that parses a reading
|
||||||
|
// and to the replay UI that renders one.
|
||||||
|
//
|
||||||
|
// So the guarantee is narrower than "the same object": both hosts answer
|
||||||
|
// undefined when a property READS the member. Key presence (`in`, Object.keys)
|
||||||
|
// is not part of it, and this test says so out loud, so closing the gap has to
|
||||||
|
// be a deliberate change to both hosts at once.
|
||||||
|
func TestExtractorEncoding_NestedUndefinedIsNotOnTheWire(t *testing.T) {
|
||||||
|
const reading = `({ absent: undefined, empty: null, present: 1 })`
|
||||||
|
const fromGoja = `{"absent":null,"empty":null,"present":1}`
|
||||||
|
// What the page sends for the same getter, with the key gone.
|
||||||
|
const fromWeb = `{"empty":null,"present":1}`
|
||||||
|
|
||||||
|
if got := encodeSpecValue(t, reading); got != fromGoja {
|
||||||
|
t.Errorf("goja encoded the reading as %s, want %s", got, fromGoja)
|
||||||
|
}
|
||||||
|
|
||||||
|
native := newVerifier(t)
|
||||||
|
mustLoad(t, native, "__sanderling__.extract(state => "+reading+", \"value\");\nglobalThis.properties = {};")
|
||||||
|
if err := native.PushSnapshot(SnapshotInput{}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
web := newVerifier(t)
|
||||||
|
mustLoad(t, web, "__sanderling__.extract(state => null, \"value\");\nglobalThis.properties = {};")
|
||||||
|
if err := web.PushSnapshot(SnapshotInput{}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, err := web.OverrideExtractorValues(map[int]json.RawMessage{0: json.RawMessage(fromWeb)}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, probe := range []struct {
|
||||||
|
expression string
|
||||||
|
native bool
|
||||||
|
web bool
|
||||||
|
}{
|
||||||
|
{"reading.absent === undefined", true, true},
|
||||||
|
{"reading.empty === null", true, true},
|
||||||
|
{"reading.present === 1", true, true},
|
||||||
|
// The half that does not survive the wire.
|
||||||
|
{`"absent" in reading`, true, false},
|
||||||
|
} {
|
||||||
|
if got := evaluateAgainstReading(t, native, probe.expression); got != probe.native {
|
||||||
|
t.Errorf("goja host: %s is %v, want %v", probe.expression, got, probe.native)
|
||||||
|
}
|
||||||
|
if got := evaluateAgainstReading(t, web, probe.expression); got != probe.web {
|
||||||
|
t.Errorf("web host: %s is %v, want %v", probe.expression, got, probe.web)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// evaluateAgainstReading answers a boolean expression over the value a property
|
||||||
|
// would read out of the first extractor, which is where the two hosts have to
|
||||||
|
// agree.
|
||||||
|
func evaluateAgainstReading(t *testing.T, verifier *Verifier, expression string) bool {
|
||||||
|
t.Helper()
|
||||||
|
if err := verifier.runtime.GlobalObject().Set("reading", verifier.extractors[0].currentValue); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
value, err := verifier.runtime.RunString(expression)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("evaluate %s: %v", expression, err)
|
||||||
|
}
|
||||||
|
return value.ToBoolean()
|
||||||
|
}
|
||||||
@@ -611,3 +611,27 @@ test("ax.find resolves the shadow-hosted match the hierarchy dump reaches first"
|
|||||||
assert.deepEqual(readingOf(values, 1), ["shadow", "light"]);
|
assert.deepEqual(readingOf(values, 1), ["shadow", "light"]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// A nested undefined is the one reading shape the two hosts do NOT encode
|
||||||
|
// alike, and this pins the split instead of hiding it. JSON has no undefined,
|
||||||
|
// so the key goes with the value here; goja marshals the same member as null,
|
||||||
|
// and it cannot do otherwise, because an exported goja object reports undefined
|
||||||
|
// and null identically, so dropping those keys there would drop the genuine
|
||||||
|
// nulls this host keeps. Carrying the member across would take a wire format
|
||||||
|
// that can express undefined.
|
||||||
|
//
|
||||||
|
// What both hosts DO agree on is the member's value: reading it answers
|
||||||
|
// undefined either way, and that is the guarantee a property may rely on. Key
|
||||||
|
// presence (`in`, Object.keys) is not.
|
||||||
|
// TestExtractorEncoding_NestedUndefinedIsNotOnTheWire in
|
||||||
|
// internal/verifier/extractor_encoding_test.go pins the other half.
|
||||||
|
test("a nested undefined leaves the page as a dropped key, a nested null does not", () => {
|
||||||
|
__testing__.extractors.length = 0;
|
||||||
|
__testing__.runtime.extract(() => ({ absent: undefined, empty: null, present: 1 }));
|
||||||
|
let wire = "";
|
||||||
|
withState(() => {
|
||||||
|
// Exactly what extractorScript in internal/driver/chrome/driver.go sends.
|
||||||
|
wire = JSON.stringify(__testing__.evaluateExtractors());
|
||||||
|
});
|
||||||
|
assert.equal(wire, `{"0":{"value":{"empty":null,"present":1}}}`);
|
||||||
|
});
|
||||||
Reference in new issue
Block a user