Source code

Revision control

Copy as Markdown

Other Tools

Test Info: Warnings

/* Any copyright is dedicated to the Public Domain.
// Tests UrlbarResult.toWire()/fromWire() and the UrlbarQueryContext prototype
// restoration that the Urlbar actor pair relies on to carry results across the
// process boundary. Actor messages structured-clone their payload, which only
// copies own enumerable data properties: UrlbarResult keeps its data in
// private fields behind getters, so a bare structuredClone loses all of it.
"use strict";
function makeUrlResult({ heuristic = false } = {}) {
return new UrlbarResult({
type: UrlbarShared.RESULT_TYPE.URL,
source: UrlbarShared.RESULT_SOURCE.HISTORY,
heuristic,
payload: {
title: "Example page",
},
});
}
function makeSearchResult() {
return new UrlbarResult(
{
type: UrlbarShared.RESULT_TYPE.SEARCH,
source: UrlbarShared.RESULT_SOURCE.SEARCH,
payload: {
engine: "Test",
suggestion: "foo bar",
query: "foo",
lowerCaseSuggestion: "foo bar",
},
},
{ suggestion: [[0, 3]] }
);
}
// Sending a result through the structured-clone algorithm the way an actor
// message would: the wire form survives, the bare instance does not.
function roundTrip(result) {
return UrlbarResult.fromWire(structuredClone(result.toWire()));
}
add_task(function test_bare_structuredClone_loses_data() {
let result = makeUrlResult();
let cloned = structuredClone(result);
Assert.equal(
cloned.type,
undefined,
"A bare structuredClone of a UrlbarResult loses the private-field data"
);
Assert.equal(cloned.payload, undefined, "payload is lost too");
});
add_task(function test_url_result_roundtrip() {
let result = makeUrlResult({ heuristic: true });
result.rowIndex = 3;
let restored = roundTrip(result);
Assert.ok(restored instanceof UrlbarResult, "Reconstructed a UrlbarResult");
Assert.equal(restored.type, result.type, "type preserved");
Assert.equal(restored.source, result.source, "source preserved");
Assert.equal(restored.heuristic, true, "heuristic preserved");
Assert.deepEqual(restored.payload, result.payload, "payload preserved");
Assert.equal(restored.rowIndex, 3, "ad-hoc public rowIndex preserved");
// Derived getter works, which means the prototype is intact.
Assert.equal(restored.icon, result.icon, "derived icon getter works");
});
add_task(function test_search_result_with_highlights_roundtrip() {
let result = makeSearchResult();
let restored = roundTrip(result);
Assert.deepEqual(
restored.testHighlights,
result.testHighlights,
"highlights preserved"
);
// A method that reads the payload + highlights still works.
let { value } = restored.getDisplayableValueAndHighlights("suggestion");
Assert.equal(value, "foo bar", "getDisplayableValueAndHighlights works");
});
add_task(function test_providerType_roundtrip() {
let result = makeSearchResult();
// providerType is set on the result after construction (by the providers
// manager), via a setter rather than a constructor parameter.
result.providerType = UrlbarShared.PROVIDER_TYPE.PROFILE;
let restored = roundTrip(result);
Assert.equal(
restored.providerType,
UrlbarShared.PROVIDER_TYPE.PROFILE,
"set-only providerType preserved"
);
});
add_task(function test_fromWire_skips_payload_validation() {
let result = makeUrlResult();
// Simulate an internal field added to the payload after the result was
// validated at creation time (e.g. QuickSuggest's suggestionObject, kept on
// the payload for dismissal). Such a field isn't in the payload schema, so
// re-validating the payload would reject it.
let wire = result.toWire();
wire.payload = { ...wire.payload, internalField: { some: "object" } };
// A validating construction rejects the unknown field...
Assert.throws(
() => new UrlbarResult(wire),
/./,
"constructing with validation rejects the internal payload field"
);
// ...but fromWire() carries it through, since the wire payload was already
// validated before serialization.
let restored = UrlbarResult.fromWire(structuredClone(wire));
Assert.deepEqual(
restored.payload.internalField,
{ some: "object" },
"fromWire preserves the internal payload field without re-validating"
);
});
add_task(function test_fromWire_resolves_to_live_result() {
/**
* Stands in for a Rust suggestion's UniFFI class, whose prototype structured
* clone drops although dismissal checks for it with instanceof.
*/
class Suggestion {}
let live = makeUrlResult();
live.id = 7;
live.payload.suggestionObject = new Suggestion();
let wire = structuredClone(live.toWire());
wire.rowIndex = 2;
Assert.ok(
!(wire.payload.suggestionObject instanceof Suggestion),
"the clone dropped the suggestion's class"
);
let restored = UrlbarResult.fromWire(wire, [makeSearchResult(), live]);
Assert.strictEqual(restored, live, "the live result with the same id");
Assert.ok(
restored.payload.suggestionObject instanceof Suggestion,
"the live payload data the wire dropped is intact"
);
Assert.equal(restored.rowIndex, 2, "the wire's rowIndex carried over");
let other = makeSearchResult();
other.id = 8;
let unresolved = UrlbarResult.fromWire(wire, [other]);
Assert.notStrictEqual(
unresolved,
live,
"no result with the id: reconstructed instead"
);
Assert.equal(unresolved.id, 7, "the reconstruction keeps the wire's id");
});
add_task(function test_queryContext_roundtrip() {
let context = createContext("foo bar", { providers: ["test"] });
let heuristic = makeUrlResult({ heuristic: true });
context.results = [heuristic, makeSearchResult()];
context.heuristicResult = heuristic;
// Simulate crossing the actor boundary: structured-clone the context's data
// (with results in wire form), then restore the class on the far side.
let wire = structuredClone({
...context,
results: context.results.map(r => r.toWire()),
heuristicResult: context.heuristicResult.toWire(),
});
Object.setPrototypeOf(wire, UrlbarQueryContext.prototype);
wire.results = wire.results.map(r => UrlbarResult.fromWire(r));
wire.heuristicResult = UrlbarResult.fromWire(wire.heuristicResult);
Assert.equal(wire.searchString, "foo bar", "searchString preserved");
Assert.deepEqual(wire.tokens, context.tokens, "tokens preserved");
Assert.ok(
wire.results.every(r => r instanceof UrlbarResult),
"nested results reconstructed as UrlbarResult instances"
);
// heuristicResult starts out as the same instance as results[0], but
// serializing each result on its own doesn't preserve that shared identity
// (the actor wiring will need to dedup, e.g. by index), so we compare data
// rather than identity. deepEqual on the instances wouldn't work: a result's
// data lives in private fields, which deepEqual can't see, so we compare the
// toWire() forms, where that data is exposed as plain properties.
Assert.deepEqual(
wire.heuristicResult.toWire(),
wire.results[0].toWire(),
"heuristicResult round-trips to the same data as the first result"
);
// A context method works after prototype restoration.
Assert.equal(
typeof wire.allowRemoteResults,
"function",
"allowRemoteResults is callable after prototype restoration"
);
Assert.equal(
typeof wire.allowRemoteResults(),
"boolean",
"allowRemoteResults returns a result"
);
});
add_task(function test_queryContext_isSearchbarSAP_roundtrip() {
for (let [sapName, expected] of [
["newtab_searchbar", true],
["urlbar", false],
]) {
let context = createContext("foo", { sapName });
let wire = structuredClone(context.toWire());
Assert.equal(
wire.isSearchbarSAP,
undefined,
`isSearchbarSAP is a getter, so ${sapName}'s wire form omits it`
);
let restored = UrlbarQueryContext.fromWire(wire);
Assert.equal(restored.sapName, sapName, `${sapName} preserved`);
Assert.equal(
restored.isSearchbarSAP,
expected,
`isSearchbarSAP is recomputed from ${sapName} after the round trip`
);
}
});