Putting APIs behind a pref in Gecko

Putting WebIDL APIs behind a flag in Gecko is surprisingly easy. Gecko supports an extended WebIDL attribute called “Pref”. You can use it on either interfaces or individual attributes, like so:

[NoInterfaceObject, Exposed=(Window)]
interface AppInstallEventHandlersMixin {
  [Pref="dom.manifest.oninstall"]
  attribute EventHandler oninstall;
};

You can see other examples in this dxr search for Pref= throughout Gecko’s tree.

Where it gets a little more tricky is testing, which I cover below.

Setting the preference

Normally, if you want something behind a preference, you want to have it disabled. The place to declare the preferences is in the modules/libpref/init/all.js file.

For example:

// Whether window.oninstall from "W3C Web Manifest" is enabled
pref("dom.manifest.oninstall", false);

Testing

Once you’ve added Pref= extended attribute, and disabled your feature, you will naturally want to test it using Mochitests.

Browser test

Creating a browser test for something like the above is fairly straight forward. Just Cu.import Services and set the pref type. In my case, I just had a boolean, so we can create a simple promise wrapper for SpecialPowers.pushPrefEnv():


// Enable window.oninstall, so we can fire events at it.
function enableOnInstallPref() {
  const prefs = new Map([
    ["dom.manifest.oninstall", true]
  ]);
  const ops = {
    "set": [...prefs.entries()],
  };
  return new Promise(resolve => SpecialPowers.pushPrefEnv(ops, resolve));
}

add_task(function*(){
  yield enableOnInstallPref();
  // do the rest of the test.
});

You could synchronously set the preference here using Services.prefs. API, but Boris Zbarsky warns: “Yes, but what unsets it so you don’t pollute the environment for later tests? pushPrefEnv handles this for you.”

Content tests

Content tests are a little more tricky, because you need to asynchronously set the preference, and then create an iframe. As Boris explains:

WebIDL conditional annotations for an interface are evaluated once per global: when the interface prototype object is created. The right way to write tests that toggle such prefs is to set the pref (via pushPrefEnv, please, not setBoolPref!) then once the pref is set create a new global to test in: either window.open or creating an iframe.

So, let’s now let’s pretend you have a file called “test_window_oninstall_event.html”. We can create a enableOnInstallPref() function that returns a Promise. Once the promise resolves, we create the iframe and again resolve with Window object. We can then run:


"use strict";
SimpleTest.waitForExplicitFinish();
function enableOnInstallPref() {
  const prefs = new Map([
    ["dom.manifest.oninstall", true]
  ]);
  const ops = {
    "set": [...prefs.entries()],
  };
  return new Promise(resolve => SpecialPowers.pushPrefEnv(ops, resolve));
}

function createIframe() {
  return new Promise((resolve) => {
    const iframe = document.createElement("iframe");
    iframe.src = "about:blank";
    iframe.onload = () => resolve(iframe.contentWindow);
    document.body.appendChild(iframe);
  });
}

function firstTest(ifrWindow){
  return new Promise(()=>{
    const hasOnInstallProp = ifrWindow.hasOwnProperty("oninstall");
    ok(hasOnInstallProp, "window has own oninstall property");

    // no point in continuing
    if (!hasOnInstallProp) {
      const err = new Error("No 'oninstall' IDL attribute. Aborting early.");
      return reject(err);
    }
    // do other tests
  }); 
}

function secondTest(ifrWindow){/* other test */}

// We can now do everything in order
const finish = SimpleTest.finish.bind(SimpleTest);
enableOnInstallPref()
  .then(createIframe)
  .then(firstTest)
  .then(secondTest)
  .then(finish)
  .catch(err => {
    dump(`${err}: ${err.stack}`);
    finish();
  });

If you would like to see the full code that this example is based on, please take a look at the attachments in Gecko Bug 1280777 – put window.oninstall behind a pref.

Leave a Reply

Your email address will not be published. Required fields are marked *