294 lines
7.5 KiB
JavaScript
294 lines
7.5 KiB
JavaScript
/* Any copyright is dedicated to the Public Domain.
|
|
http://creativecommons.org/publicdomain/zero/1.0/ */
|
|
|
|
"use strict";
|
|
|
|
const { FileTestUtils } = ChromeUtils.importESModule(
|
|
"resource://testing-common/FileTestUtils.sys.mjs"
|
|
);
|
|
const { SharedDataMap } = ChromeUtils.importESModule(
|
|
"resource://nimbus/lib/SharedDataMap.sys.mjs"
|
|
);
|
|
|
|
const KEY = "browser_shareddatamap.js";
|
|
const PATH = FileTestUtils.getTempFile("shared-data-map").path;
|
|
|
|
add_setup(() => {
|
|
SpecialPowers.addTaskImport(
|
|
"SharedDataMap",
|
|
"resource://nimbus/lib/SharedDataMap.sys.mjs"
|
|
);
|
|
SpecialPowers.addTaskImport(
|
|
"TestUtils",
|
|
"resource://testing-common/TestUtils.sys.mjs"
|
|
);
|
|
SpecialPowers.addTaskImport(
|
|
"sinon",
|
|
"resource://testing-common/Sinon.sys.mjs"
|
|
);
|
|
|
|
Services.ppmm.releaseCachedProcesses();
|
|
});
|
|
|
|
function setupTest({ multiprocess = false } = {}) {
|
|
const sdm = new SharedDataMap(KEY, { path: PATH });
|
|
|
|
return {
|
|
sdm,
|
|
async cleanup() {
|
|
sdm._removeEntriesByKeys(Object.keys(sdm._store.data));
|
|
|
|
// Wait for the store to finish writing to disk, then delete the file on disk.
|
|
await sdm._store.finalize();
|
|
await IOUtils.remove(PATH);
|
|
|
|
if (multiprocess) {
|
|
// Ensure we shut down any cached processes for the next test.
|
|
Services.ppmm.releaseCachedProcesses();
|
|
}
|
|
},
|
|
};
|
|
}
|
|
|
|
add_task(async function testSetSaves() {
|
|
const { sdm, cleanup } = setupTest();
|
|
await sdm.init();
|
|
|
|
sinon.spy(sdm._store, "saveSoon");
|
|
|
|
sdm.set("foo", "bar");
|
|
|
|
Assert.ok(
|
|
sdm._store.saveSoon.calledOnce,
|
|
"Should call saveSoon when setting a value"
|
|
);
|
|
|
|
await cleanup();
|
|
});
|
|
|
|
add_task(async function testUpdate() {
|
|
const { sdm, cleanup } = setupTest();
|
|
await sdm.init();
|
|
|
|
sdm.set("foo", "foo");
|
|
sdm.set("bar", "bar");
|
|
|
|
Assert.equal(sdm.get("foo"), "foo");
|
|
Assert.equal(sdm.get("bar"), "bar");
|
|
|
|
sdm.set("foo", "baz");
|
|
sdm.set("bar", "qux");
|
|
|
|
Assert.equal(sdm.get("foo"), "baz");
|
|
Assert.equal(sdm.get("bar"), "qux");
|
|
|
|
sdm._removeEntriesByKeys(["bar", "qux"]);
|
|
|
|
Assert.equal(sdm.get("foo"), "baz");
|
|
Assert.equal(typeof sdm.get("bar"), "undefined");
|
|
|
|
await cleanup();
|
|
});
|
|
|
|
add_task(async function testInitSafe() {
|
|
const { sdm, cleanup } = setupTest();
|
|
await sdm.init();
|
|
|
|
sinon.stub(sdm._store, "load");
|
|
sinon.replaceGetter(sdm._store, "data", () => {
|
|
throw new Error("uh oh");
|
|
});
|
|
|
|
await sdm.init();
|
|
Assert.ok(sdm._store.load.calledOnce, "should have called load");
|
|
|
|
await cleanup();
|
|
}).skip();
|
|
|
|
add_task(async function testInitMultiple() {
|
|
const { sdm, cleanup } = setupTest();
|
|
|
|
sinon.spy(sdm._store, "load");
|
|
|
|
await sdm.init();
|
|
await sdm.ready();
|
|
|
|
Assert.ok(sdm._store.load.calledOnce, "load called");
|
|
|
|
await sdm.init();
|
|
|
|
Assert.ok(sdm._store.load.calledOnce, "load called only once");
|
|
|
|
await cleanup();
|
|
});
|
|
|
|
add_task(async function testChildInit() {
|
|
const browserWindow = Services.wm.getMostRecentWindow("navigator:browser");
|
|
const tab = await BrowserTestUtils.openNewForegroundTab({
|
|
gBrowser: browserWindow.gBrowser,
|
|
url: "https://example.com",
|
|
forceNewProcess: true,
|
|
});
|
|
|
|
await SpecialPowers.spawn(tab.linkedBrowser, [KEY], async key => {
|
|
const childSdm = new SharedDataMap(key);
|
|
|
|
Assert.ok(
|
|
!Object.hasOwn(childSdm, "_store"),
|
|
"child SharedDataMap does not have a store"
|
|
);
|
|
});
|
|
|
|
Services.ppmm.releaseCachedProcesses();
|
|
|
|
BrowserTestUtils.removeTab(tab);
|
|
});
|
|
|
|
add_task(async function testSetNotifiesParent() {
|
|
const { sdm, cleanup } = setupTest();
|
|
await sdm.init();
|
|
|
|
const onUpdate = sinon.stub();
|
|
sdm.on("parent-store-update:foo", onUpdate);
|
|
sdm.set("foo", "bar");
|
|
|
|
Assert.ok(
|
|
onUpdate.calledOnceWithExactly("parent-store-update:foo", "bar"),
|
|
"update event sent"
|
|
);
|
|
|
|
await cleanup();
|
|
});
|
|
|
|
add_task(async function testSetNotifiesChild() {
|
|
const { sdm, cleanup } = setupTest({ multiprocess: true });
|
|
await sdm.init();
|
|
|
|
const browserWindow = Services.wm.getMostRecentWindow("navigator:browser");
|
|
|
|
// Open a tab so we have a content process.
|
|
const tab = await BrowserTestUtils.openNewForegroundTab({
|
|
gBrowser: browserWindow.gBrowser,
|
|
url: "https://example.com",
|
|
forceNewProcess: true,
|
|
});
|
|
|
|
const messagePrefix = "testSetNotifiesChild";
|
|
|
|
const complete = SpecialPowers.spawn(
|
|
tab.linkedBrowser,
|
|
[KEY, messagePrefix],
|
|
async (key, prefix) => {
|
|
function waitForSharedDataChanged() {
|
|
return new Promise(resolve => {
|
|
const listener = () => {
|
|
resolve();
|
|
Services.cpmm.sharedData.removeEventListener("change", listener);
|
|
};
|
|
|
|
Services.cpmm.sharedData.addEventListener("change", listener);
|
|
});
|
|
}
|
|
|
|
const childSdm = new SharedDataMap(key);
|
|
const onUpdate = sinon.stub();
|
|
childSdm.on("child-store-update:foo", onUpdate);
|
|
await childSdm.ready();
|
|
|
|
Assert.equal(
|
|
typeof childSdm.get("foo"),
|
|
"undefined",
|
|
"child does not see value"
|
|
);
|
|
|
|
// Send the begin message and wait for the SharedData update.
|
|
let sharedDataChanged = waitForSharedDataChanged();
|
|
Services.cpmm.sendAsyncMessage(`${prefix}:begin`);
|
|
await sharedDataChanged;
|
|
|
|
// Wait for the update event to fire.
|
|
await TestUtils.waitForCondition(
|
|
() => onUpdate.callCount,
|
|
"Child received an update event"
|
|
);
|
|
Assert.ok(
|
|
onUpdate.calledOnceWith("child-store-update:foo", { bar: 1 }),
|
|
"child received update event"
|
|
);
|
|
Assert.deepEqual(
|
|
childSdm.get("foo"),
|
|
{ bar: 1 },
|
|
"child sees updated value"
|
|
);
|
|
|
|
onUpdate.resetHistory();
|
|
|
|
// Send the delete message and wait for the SharedData update.
|
|
sharedDataChanged = waitForSharedDataChanged();
|
|
Services.cpmm.sendAsyncMessage(`${prefix}:delete`);
|
|
await sharedDataChanged;
|
|
|
|
await TestUtils.waitForCondition(() => !childSdm.get("foo"));
|
|
Assert.ok(onUpdate.notCalled, "onUpdate not called for deletions");
|
|
Assert.equal(
|
|
typeof childSdm.get("foo"),
|
|
"undefined",
|
|
"After deletion, foo is undefined"
|
|
);
|
|
}
|
|
);
|
|
|
|
// Wait for the child process to register its listeners.
|
|
await waitForChildMessage(`${messagePrefix}:begin`);
|
|
|
|
// Set the value in the parent and wait for the child to finish its assertions.
|
|
const continuePromise = waitForChildMessage(`${messagePrefix}:delete`);
|
|
|
|
// Flush immediately. Do not wait for idle dispatch.
|
|
sdm.set("foo", { bar: 1 });
|
|
Services.ppmm.sharedData.flush();
|
|
await continuePromise;
|
|
|
|
// Delete the entry. _deleteForTests flushes so we don't have to.
|
|
sdm._deleteForTests("foo");
|
|
|
|
// Wait for the child task to complete.
|
|
await complete;
|
|
|
|
BrowserTestUtils.removeTab(tab);
|
|
|
|
await cleanup();
|
|
});
|
|
|
|
add_task(async function testGetFromChildExisting() {
|
|
const { sdm, cleanup } = setupTest({ multiprocess: true });
|
|
await sdm.init();
|
|
|
|
sdm.set("foo", { bar: "baz", qux: ["quux"] });
|
|
|
|
// Ensure that we re-serialize SharedData to make the content immediately
|
|
// available to new processes.
|
|
Services.ppmm.sharedData.flush();
|
|
|
|
const browserWindow = Services.wm.getMostRecentWindow("navigator:browser");
|
|
const tab = await BrowserTestUtils.openNewForegroundTab({
|
|
gBrowser: browserWindow.gBrowser,
|
|
url: "https://example.com",
|
|
forceNewProcess: true,
|
|
});
|
|
|
|
await SpecialPowers.spawn(tab.linkedBrowser, [KEY], async key => {
|
|
const childSdm = new SharedDataMap(key);
|
|
await childSdm.ready();
|
|
|
|
Assert.deepEqual(
|
|
childSdm.get("foo"),
|
|
{ bar: "baz", qux: ["quux"] },
|
|
"child sees correct value"
|
|
);
|
|
});
|
|
|
|
BrowserTestUtils.removeTab(tab);
|
|
|
|
await cleanup();
|
|
});
|