summaryrefslogtreecommitdiffstats
path: root/dom/tests/browser/page_localStorage.js
diff options
context:
space:
mode:
Diffstat (limited to 'dom/tests/browser/page_localStorage.js')
-rw-r--r--dom/tests/browser/page_localStorage.js127
1 files changed, 127 insertions, 0 deletions
diff --git a/dom/tests/browser/page_localStorage.js b/dom/tests/browser/page_localStorage.js
new file mode 100644
index 0000000000..4fecf10bcf
--- /dev/null
+++ b/dom/tests/browser/page_localStorage.js
@@ -0,0 +1,127 @@
+/**
+ * Helper page used by browser_localStorage_xxx.js.
+ *
+ * We expose methods to be invoked by SpecialPowers.spawn() calls.
+ * SpecialPowers.spawn() uses the message manager and is PContent-based. When
+ * LocalStorage was PContent-managed, ordering was inherently ensured so we
+ * could assume each page had already received all relevant events. Now some
+ * explicit type of coordination is required.
+ *
+ * This gets complicated because:
+ * - LocalStorage is an ugly API that gives us almost unlimited implementation
+ * flexibility in the face of multiple processes. It's also an API that sites
+ * may misuse which may encourage us to leverage that flexibility in the
+ * future to improve performance at the expense of propagation latency, and
+ * possibly involving content-observable coalescing of events.
+ * - The Quantum DOM effort and its event labeling and separate task queues and
+ * green threading and current LocalStorage implementation mean that using
+ * other PBackground-based APIs such as BroadcastChannel may not provide
+ * reliable ordering guarantees. Specifically, it's hard to guarantee that
+ * a BroadcastChannel postMessage() issued after a series of LocalStorage
+ * writes won't be received by the target window before the writes are
+ * perceived. At least not without constraining the implementations of both
+ * APIs.
+ * - Some of our tests explicitly want to verify LocalStorage behavior without
+ * having a "storage" listener, so we can't add a storage listener if the test
+ * didn't already want one.
+ *
+ * We use 2 approaches for coordination:
+ * 1. If we're already listening for events, we listen for the sentinel value to
+ * be written. This is efficient and appropriate in this case.
+ * 2. If we're not listening for events, we use setTimeout(0) to poll the
+ * localStorage key and value until it changes to our expected value.
+ * setTimeout(0) eventually clamps to setTimeout(4), so in the event we are
+ * experiencing delays, we have reasonable, non-CPU-consuming back-off in
+ * place that leaves the CPU free to time out and fail our test if something
+ * broke. This is ugly but makes us less brittle.
+ *
+ * Both of these involve mutateStorage writing the sentinel value at the end of
+ * the batch. All of our result-returning methods accordingly filter out the
+ * sentinel key/value pair.
+ **/
+
+var pageName = document.location.search.substring(1);
+window.addEventListener("load", () => {
+ document.getElementById("pageNameH").textContent = pageName;
+});
+
+// Key that conveys the end of a write batch. Filtered out from state and
+// events.
+const SENTINEL_KEY = "WRITE_BATCH_SENTINEL";
+
+var storageEventsPromise = null;
+function listenForStorageEvents(sentinelValue) {
+ const recordedEvents = [];
+ storageEventsPromise = new Promise(function (resolve, reject) {
+ window.addEventListener("storage", function thisHandler(event) {
+ if (event.key === SENTINEL_KEY) {
+ // There should be no way for this to have the wrong value, but reject
+ // if it is wrong.
+ if (event.newValue === sentinelValue) {
+ window.removeEventListener("storage", thisHandler);
+ resolve(recordedEvents);
+ } else {
+ reject(event.newValue);
+ }
+ } else {
+ recordedEvents.push([event.key, event.newValue, event.oldValue]);
+ }
+ });
+ });
+}
+
+function mutateStorage({ mutations, sentinelValue }) {
+ mutations.forEach(function ([key, value]) {
+ if (key !== null) {
+ if (value === null) {
+ localStorage.removeItem(key);
+ } else {
+ localStorage.setItem(key, value);
+ }
+ } else {
+ localStorage.clear();
+ }
+ });
+ localStorage.setItem(SENTINEL_KEY, sentinelValue);
+}
+
+// Returns a promise that is resolve when the sentinel key has taken on the
+// sentinel value. Oddly structured to make sure promises don't let us
+// accidentally side-step the timeout clamping logic.
+function waitForSentinelValue(sentinelValue) {
+ return new Promise(function (resolve) {
+ function checkFunc() {
+ if (localStorage.getItem(SENTINEL_KEY) === sentinelValue) {
+ resolve();
+ } else {
+ // I believe linters will only yell at us if we use a non-zero constant.
+ // Other forms of back-off were considered, including attempting to
+ // issue a round-trip through PBackground, but that still potentially
+ // runs afoul of labeling while also making us dependent on unrelated
+ // APIs.
+ setTimeout(checkFunc, 0);
+ }
+ }
+ checkFunc();
+ });
+}
+
+async function getStorageState(maybeSentinel) {
+ if (maybeSentinel) {
+ await waitForSentinelValue(maybeSentinel);
+ }
+
+ let numKeys = localStorage.length;
+ let state = {};
+ for (var iKey = 0; iKey < numKeys; iKey++) {
+ let key = localStorage.key(iKey);
+ if (key !== SENTINEL_KEY) {
+ state[key] = localStorage.getItem(key);
+ }
+ }
+ return state;
+}
+
+function returnAndClearStorageEvents() {
+ return storageEventsPromise;
+}