Commit a8c8aab2 authored by Rob Wu's avatar Rob Wu
Browse files

Bug 1758475 - Add StorageArea.onChanged event targets r=rpl

parent f7009a72
Loading
Loading
Loading
Loading
+36 −22
Original line number Diff line number Diff line
@@ -151,6 +151,35 @@ this.storage = class extends ExtensionAPI {
      context
    );

    // onChangedName is "storage.onChanged", "storage.sync.onChanged", etc.
    function makeOnChangedEventTarget(onChangedName) {
      return new EventManager({
        context,
        name: onChangedName,
        register: fire => {
          let onChanged = (data, area) => {
            let changes = new context.cloneScope.Object();
            for (let [key, value] of Object.entries(data)) {
              changes[key] = deserialize(value);
            }
            if (area) {
              // storage.onChanged includes the area.
              fire.raw(changes, area);
            } else {
              // StorageArea.onChanged doesn't include the area.
              fire.raw(changes);
            }
          };

          let parent = context.childManager.getParentEvent(onChangedName);
          parent.addListener(onChanged);
          return () => {
            parent.removeListener(onChanged);
          };
        },
      }).api();
    }

    function sanitize(items) {
      // The schema validator already takes care of arrays (which are only allowed
      // to contain strings). Strings and null are safe values.
@@ -222,7 +251,9 @@ this.storage = class extends ExtensionAPI {
    let promiseStorageLocalBackend;

    // Generate the backend-agnostic local API wrapped methods.
    const local = {};
    const local = {
      onChanged: makeOnChangedEventTarget("storage.local.onChanged"),
    };
    for (let method of ["get", "set", "remove", "clear"]) {
      local[method] = async function(...args) {
        try {
@@ -293,6 +324,7 @@ this.storage = class extends ExtensionAPI {
              [items]
            );
          },
          onChanged: makeOnChangedEventTarget("storage.sync.onChanged"),
        },

        managed: {
@@ -310,29 +342,11 @@ this.storage = class extends ExtensionAPI {
          clear() {
            return Promise.reject({ message: "storage.managed is read-only" });
          },
        },

        onChanged: new EventManager({
          context,
          name: "storage.onChanged",
          register: fire => {
            let onChanged = (data, area) => {
              let changes = new context.cloneScope.Object();
              for (let [key, value] of Object.entries(data)) {
                changes[key] = deserialize(value);
              }
              fire.raw(changes, area);
            };

            let parent = context.childManager.getParentEvent(
              "storage.onChanged"
            );
            parent.addListener(onChanged);
            return () => {
              parent.removeListener(onChanged);
            };
          onChanged: makeOnChangedEventTarget("storage.managed.onChanged"),
        },
        }).api(),

        onChanged: makeOnChangedEventTarget("storage.onChanged"),
      },
    };
  }
+39 −0
Original line number Diff line number Diff line
@@ -12,6 +12,7 @@ XPCOMUtils.defineLazyModuleGetters(this, {
});

var { ExtensionError } = ExtensionUtils;
var { ignoreEvent } = ExtensionCommon;

XPCOMUtils.defineLazyGetter(this, "extensionStorageSync", () => {
  // TODO bug 1637465: Remove Kinto-based implementation.
@@ -85,6 +86,30 @@ this.storage = class extends ExtensionAPIPersistent {
        },
      };
    },
    "local.onChanged"({ fire }) {
      let unregister = this.registerLocalChangedListener(changes => {
        // |changes| is already serialized. Send the raw value, so that it can
        // be deserialized by the onChanged handler in child/ext-storage.js.
        fire.raw(changes);
      });
      return {
        unregister,
        convert(_fire) {
          fire = _fire;
        },
      };
    },
    "sync.onChanged"({ fire }) {
      let unregister = this.registerSyncChangedListener(changes => {
        fire.async(changes);
      });
      return {
        unregister,
        convert(_fire) {
          fire = _fire;
        },
      };
    },
  };

  registerLocalChangedListener(onStorageLocalChanged) {
@@ -213,6 +238,12 @@ this.storage = class extends ExtensionAPIPersistent {
              return ExtensionStorageIDB.selectBackend(context);
            },
          },
          onChanged: new EventManager({
            context,
            module: "storage",
            event: "local.onChanged",
            extensionApi: this,
          }).api(),
        },

        sync: {
@@ -236,6 +267,12 @@ this.storage = class extends ExtensionAPIPersistent {
            enforceNoTemporaryAddon(extension.id);
            return extensionStorageSync.getBytesInUse(extension, keys, context);
          },
          onChanged: new EventManager({
            context,
            module: "storage",
            event: "sync.onChanged",
            extensionApi: this,
          }).api(),
        },

        managed: {
@@ -256,6 +293,8 @@ this.storage = class extends ExtensionAPIPersistent {
            }
            return ExtensionStorage._filterProperties(data, keys);
          },
          // managed storage is currently initialized once.
          onChanged: ignoreEvent(context, "storage.managed.onChanged"),
        },

        onChanged: new EventManager({
+30 −0
Original line number Diff line number Diff line
@@ -154,6 +154,21 @@
              }
            ]
          }
        ],
        "events": [
          {
            "name": "onChanged",
            "type": "function",
            "description": "Fired when one or more items change.",
            "parameters": [
              {
                "name": "changes",
                "type": "object",
                "additionalProperties": { "$ref": "StorageChange" },
                "description": "Object mapping each key that changed to its corresponding $(ref:storage.StorageChange) for that item."
              }
            ]
          }
        ]
      },
      {
@@ -283,6 +298,21 @@
              }
            ]
          }
        ],
        "events": [
          {
            "name": "onChanged",
            "type": "function",
            "description": "Fired when one or more items change.",
            "parameters": [
              {
                "name": "changes",
                "type": "object",
                "additionalProperties": { "$ref": "StorageChange" },
                "description": "Object mapping each key that changed to its corresponding $(ref:storage.StorageChange) for that item."
              }
            ]
          }
        ]
      }
    ],
+33 −5
Original line number Diff line number Diff line
@@ -1242,8 +1242,8 @@ async function test_contentscript_storage(storageType) {
}

async function test_storage_change_event_page(areaName) {
  async function testFn() {
    function background(areaName) {
  async function testOnChanged(targetIsStorageArea) {
    function backgroundTestStorageTopNamespace(areaName) {
      browser.storage.onChanged.addListener((changes, area) => {
        browser.test.assertEq(area, areaName, "Expected areaName");
        browser.test.assertEq(
@@ -1254,6 +1254,26 @@ async function test_storage_change_event_page(areaName) {
        browser.test.sendMessage("onChanged_was_fired");
      });
    }
    function backgroundTestStorageAreaNamespace(areaName) {
      browser.storage[areaName].onChanged.addListener((changes, ...args) => {
        browser.test.assertEq(args.length, 0, "no more args after changes");
        browser.test.assertEq(
          JSON.stringify(changes),
          `{"storageKey":{"newValue":"newStorageValue"}}`,
          `Expected changes via ${areaName}.onChanged event`
        );
        browser.test.sendMessage("onChanged_was_fired");
      });
    }
    let background, onChangedName;
    if (targetIsStorageArea) {
      // Test storage.local.onChanged / storage.sync.onChanged.
      background = backgroundTestStorageAreaNamespace;
      onChangedName = `${areaName}.onChanged`;
    } else {
      background = backgroundTestStorageTopNamespace;
      onChangedName = "onChanged";
    }
    let extension = ExtensionTestUtils.loadExtension({
      manifest: {
        permissions: ["storage"],
@@ -1275,12 +1295,12 @@ async function test_storage_change_event_page(areaName) {
      },
    });
    await extension.startup();
    assertPersistentListeners(extension, "storage", "onChanged", {
    assertPersistentListeners(extension, "storage", onChangedName, {
      primed: false,
    });

    await extension.terminateBackground();
    assertPersistentListeners(extension, "storage", "onChanged", {
    assertPersistentListeners(extension, "storage", onChangedName, {
      primed: true,
    });

@@ -1292,11 +1312,19 @@ async function test_storage_change_event_page(areaName) {
    await contentPage.close();
    await extension.awaitMessage("onChanged_was_fired");

    assertPersistentListeners(extension, "storage", "onChanged", {
    assertPersistentListeners(extension, "storage", onChangedName, {
      primed: false,
    });
    await extension.unload();
  }

  async function testFn() {
    // Test browser.storage.onChanged.addListener
    await testOnChanged(/* targetIsStorageArea */ false);
    // Test browser.storage.local.onChanged.addListener
    // and browser.storage.sync.onChanged.addListener, depending on areaName.
    await testOnChanged(/* targetIsStorageArea */ true);
  }

  return runWithPrefs([["extensions.eventPages.enabled", true]], testFn);
}
+43 −0
Original line number Diff line number Diff line
@@ -168,3 +168,46 @@ add_task(async function test_manifest_not_found() {
  await extension.awaitFinish();
  await extension.unload();
});

add_task(async function test_manifest_not_found() {
  let extension = ExtensionTestUtils.loadExtension({
    manifest: {
      permissions: ["storage"],
    },

    async background() {
      const dummyListener = () => {};
      browser.storage.managed.onChanged.addListener(dummyListener);
      browser.test.assertTrue(
        browser.storage.managed.onChanged.hasListener(dummyListener),
        "addListener works according to hasListener"
      );
      browser.storage.managed.onChanged.removeListener(dummyListener);

      // We should get a warning for each registration.
      browser.storage.managed.onChanged.addListener(() => {});
      browser.storage.managed.onChanged.addListener(() => {});
      browser.storage.managed.onChanged.addListener(() => {});

      // Invoke the storage.managed API to make sure that we have made a
      // round trip to the parent process and back. This is because event
      // registration is async but we cannot await (bug 1300234).
      await browser.test.assertRejects(
        browser.storage.managed.get({ a: 1 }),
        /Managed storage manifest not found/,
        "browser.storage.managed.get() rejects when without manifest"
      );

      browser.test.notifyPass();
    },
  });

  let { messages } = await promiseConsoleOutput(async () => {
    await extension.startup();
    await extension.awaitFinish();
    await extension.unload();
  });
  const UNSUP_EVENT_WARNING = `attempting to use listener "storage.managed.onChanged", which is unimplemented`;
  messages = messages.filter(msg => msg.message.includes(UNSUP_EVENT_WARNING));
  Assert.equal(messages.length, 4, "Expected msg for each addListener call");
});