Configure a Firefox web extension from Selenium

I’ve been wanting to add automated tests for Referer Modifier for a while, and now I finally got around to implementing some using Selenium (which lets you remote-control a browser). One tricky question to solve was: How do I automatically configure the freshly installed Firefox add-on?

Selenium has interfaces to open a page in the browser, find elements, click on them, and so on. It also has a way to install an add-on, in my Python unittest code it looks like this:

    def setUp(self):
        self.browser = webdriver.Firefox(options=self.options)
        self.browser.install_addon(str(self.addon_path), temporary=True)

The self.addon_path is the path to the ZIP archive containing the add-on, setting temporary=True is necessary because Firefox refuses to install unsigned add-ons permanently. But how do I configure the settings I want to test?

Semi-obvious answer: also automate the options page. Which leads to the next question: How to I open the options page? If you use Firefox with add-ons you may have noticed that add-on configuration pages have URLs like this:

moz-extension://[some UUID goes here]/page.html

Hm, okay, but what’s the right UUID? It’s definitely not the add-on ID set in the web extension manifest. As it turns out, the UUIDs are randomly generated on each computer to make them hard to guess, and a mapping from add-on IDs to the local UUIDs is stored in the extensions.webextensions.uuids preference. Next question: How do I get that mapping?

Answer: Not at all, it seems Selenium has no way to access Firefox preferences once the browser is running. But it has something else: It lets you set up a Firefox profile (including preferences) before starting the browser… If I can’t read the mapping, maybe I can just provide my own? 🤔

    @classmethod
    def setUpClass(cls):
        cls.ext_dir = Path(sys.argv[0]).parent
        with open(cls.ext_dir / 'manifest.json') as fh:
            manifest = json.load(fh)

        cls.addon_path = (cls.ext_dir /
                          f'referer-mod-{manifest["version"]}.zip').resolve()
        addon_id = manifest["browser_specific_settings"]["gecko"]["id"]
        addon_dyn_id = str(uuid.uuid4())
        cls.config_url = f'moz-extension://{addon_dyn_id}/options.html'
        print(f'Dynamic ID: {addon_dyn_id}')

        profile = webdriver.FirefoxProfile()
        # Pre-seed the dynamic addon ID so we can find the options page
        profile.set_preference('extensions.webextensions.uuids',
                               json.dumps({addon_id: addon_dyn_id}))
        # [...]
        cls.options = FirefoxOptions()
        cls.options.profile = profile

And it works! What this code does is:

  1. Read the manifest file of the add-on.
  2. Retrieve the static add-on ID from the manifest.
  3. Create a random UUID.
  4. Create a new Firefox profile and store the mapping from the add-on ID to the UUID as JSON in the extensions.webextensions.uuids preference.

The resulting cls.options is what’s used as the options parameter in the first snippet above. The pre-configured mapping is used when the add-on is installed, and because my test knows the UUID it generated (still randomly!) it can later load the config_url generated above, and import the configuration I want to test:

        test_config = (self.ext_dir / 'test_config.json').resolve()
        self.browser.get(self.config_url)
        import_file = self.browser.find_element_by_id('import_file')
        import_file.send_keys(str(test_config))
        import_button = self.browser.find_element_by_id('import_button')
        import_button.click()

And done! Of course if your add-on doesn’t support configuration import, or you actually want to test the add-on UI (good idea!) a few more actions will be necessary. If you want to see the full test, look at test.py in the Referer Modifier repository.

Leave a Comment

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: