summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorWojtek Kosior <koszko@koszko.org>2021-12-13 17:58:29 +0100
committerWojtek Kosior <koszko@koszko.org>2021-12-13 17:58:29 +0100
commitc699b6409e98fe64a70417a18b6e335b4c60f86d (patch)
treed01108437eaaa89fb167b35a7e4439e69b5d38d2
parent4c933941e64ba2b4b5da3c19e5ed5924a30e0752 (diff)
downloadbrowser-extension-c699b6409e98fe64a70417a18b6e335b4c60f86d.tar.gz
browser-extension-c699b6409e98fe64a70417a18b6e335b4c60f86d.zip
facilitate creating and installing WebExtensions during tests
It is now possible to more conveniently test WebExtension APIs code by wrapping it into a test WebExtension and temporarily installing in the driven browser.
-rw-r--r--copyright1
-rw-r--r--test/extension_crafting.py121
-rw-r--r--test/misc_constants.py3
-rwxr-xr-xtest/profiles.py26
-rw-r--r--test/unit/conftest.py31
-rw-r--r--test/unit/test_basic.py17
6 files changed, 190 insertions, 9 deletions
diff --git a/copyright b/copyright
index dace232..9c39134 100644
--- a/copyright
+++ b/copyright
@@ -97,6 +97,7 @@ Copyright: 2021 Wojtek Kosior <koszko@koszko.org>
License: CC0
Files: test/profiles.py test/script_loader.py test/unit/conftest.py
+ test/extension_crafting.py
Copyright: 2021 Wojtek Kosior <koszko@koszko.org>
License: GPL-3+
Comment: Wojtek Kosior promises not to sue even in case of violations
diff --git a/test/extension_crafting.py b/test/extension_crafting.py
new file mode 100644
index 0000000..6f1800b
--- /dev/null
+++ b/test/extension_crafting.py
@@ -0,0 +1,121 @@
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+"""
+Making temporary WebExtensions for use in the test suite
+"""
+
+# This file is part of Haketilo.
+#
+# Copyright (C) 2021 Wojtek Kosior <koszko@koszko.org>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+#
+# I, Wojtek Kosior, thereby promise not to sue for violation of this file's
+# license. Although I request that you do not make use this code in a
+# proprietary program, I am not going to enforce this in court.
+
+import json
+import zipfile
+from pathlib import Path
+from uuid import uuid4
+
+from .misc_constants import *
+
+class ManifestTemplateValueToFill:
+ pass
+
+def manifest_template():
+ return {
+ 'manifest_version': 2,
+ 'name': 'Haketilo test extension',
+ 'version': '1.0',
+ 'applications': {
+ 'gecko': {
+ 'id': ManifestTemplateValueToFill(),
+ 'strict_min_version': '60.0'
+ }
+ },
+ 'permissions': [
+ 'contextMenus',
+ 'webRequest',
+ 'webRequestBlocking',
+ 'activeTab',
+ 'notifications',
+ 'sessions',
+ 'storage',
+ 'tabs',
+ '<all_urls>',
+ 'unlimitedStorage'
+ ],
+ 'web_accessible_resources': ['testpage.html'],
+ 'background': {
+ 'persistent': True,
+ 'scripts': ['__open_test_page.js', 'background.js']
+ },
+ 'content_scripts': [
+ {
+ 'run_at': 'document_start',
+ 'matches': ['<all_urls>'],
+ 'match_about_blank': True,
+ 'all_frames': True,
+ 'js': ['content.js']
+ }
+ ]
+ }
+
+default_background_script = ''
+default_content_script = ''
+default_test_page = '''
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>Extension's options page for testing</title>
+ </head>
+ <body>
+ <h1>Extension's options page for testing</h1>
+ </body>
+</html>
+'''
+
+open_test_page_script = '''(() => {
+const page_url = browser.runtime.getURL("testpage.html");
+const execute_details = {
+ code: `window.location.href=${JSON.stringify(page_url)};`
+};
+browser.tabs.query({currentWindow: true, active: true})
+ .then(t => browser.tabs.executeScript(t.id, execute_details));
+})();'''
+
+def make_extension(destination_dir,
+ background_script=default_background_script,
+ content_script=default_content_script,
+ test_page=default_test_page,
+ extra_files={}):
+ manifest = manifest_template()
+ extension_id = '{%s}' % uuid4()
+ manifest['applications']['gecko']['id'] = extension_id
+ files = {
+ 'manifest.json' : json.dumps(manifest),
+ '__open_test_page.js': open_test_page_script,
+ 'background.js' : background_script,
+ 'content.js' : content_script,
+ 'testpage.html' : test_page,
+ **extra_files
+ }
+ destination_path = destination_dir / f'{extension_id}.xpi'
+ with zipfile.ZipFile(destination_path, 'x') as xpi:
+ for filename, contents in files.items():
+ xpi.writestr(filename, contents)
+
+ return destination_path
diff --git a/test/misc_constants.py b/test/misc_constants.py
index 22432a6..b3e9e32 100644
--- a/test/misc_constants.py
+++ b/test/misc_constants.py
@@ -41,6 +41,9 @@ default_proxy_port = 1337
default_cert_dir = here / 'certs'
+default_extension_uuid = 'a1291446-be95-48ad-a4c6-a475e389399b'
+default_haketilo_id = '{6fe13369-88e9-440f-b837-5012fb3bedec}'
+
mime_types = {
"7z": "application/x-7z-compressed", "oga": "audio/ogg",
"abw": "application/x-abiword", "ogv": "video/ogg",
diff --git a/test/profiles.py b/test/profiles.py
index 1530aea..795a0db 100755
--- a/test/profiles.py
+++ b/test/profiles.py
@@ -27,7 +27,8 @@ Browser profiles and Selenium driver initialization
from selenium import webdriver
from selenium.webdriver.firefox.options import Options
-import time
+import json
+from shutil import rmtree
from .misc_constants import *
@@ -35,7 +36,8 @@ class HaketiloFirefox(webdriver.Firefox):
"""
This wrapper class around selenium.webdriver.Firefox adds a `loaded_scripts`
instance property that gets resetted to an empty array every time the
- `get()` method is called.
+ `get()` method is called and also facilitates removing the temporary
+ profile directory after Firefox quits.
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@@ -48,6 +50,11 @@ class HaketiloFirefox(webdriver.Firefox):
self.reset_loaded_scripts()
super().get(*args, **kwargs)
+ def quit(self, *args, **kwargs):
+ profile_path = self.firefox_profile.path
+ super().quit(*args, **kwargs)
+ rmtree(profile_path, ignore_errors=True)
+
def set_profile_proxy(profile, proxy_host, proxy_port):
"""
Create a Firefox profile that uses the specified HTTP proxy for all
@@ -67,6 +74,20 @@ def set_profile_proxy(profile, proxy_host, proxy_port):
def set_profile_console_logging(profile):
profile.set_preference('devtools.console.stdout.content', True)
+# The function below seems not to work for extensions that are
+# temporarily-installed in Firefox safe mode. Testing is needed to see if it
+# works with non-temporary extensions (without safe mode).
+def set_webextension_uuid(profile, extension_id, uuid=default_extension_uuid):
+ """
+ Firefox would normally assign a unique, random UUID to installed extension.
+ This UUID is needed to easily navigate to extension's settings page (and
+ other extension's pages). Since there's no way to learn such UUID with
+ current WebDriver implementation, this function works around this by telling
+ Firefox to use a predefined UUID for a certain extension.
+ """
+ profile.set_preference('extensions.webextensions.uuids',
+ json.dumps({extension_id: uuid}))
+
def firefox_safe_mode(firefox_binary=default_firefox_binary,
proxy_host=default_proxy_host,
proxy_port=default_proxy_port):
@@ -97,6 +118,7 @@ def firefox_with_profile(firefox_binary=default_firefox_binary,
profile = webdriver.FirefoxProfile(profile_dir)
set_profile_proxy(profile, proxy_host, proxy_port)
set_profile_console_logging(profile)
+ set_webextension_uuid(profile, default_haketilo_id)
return HaketiloFirefox(firefox_profile=profile,
firefox_binary=firefox_binary)
diff --git a/test/unit/conftest.py b/test/unit/conftest.py
index 1500006..e1c98a1 100644
--- a/test/unit/conftest.py
+++ b/test/unit/conftest.py
@@ -26,10 +26,14 @@ Common fixtures for Haketilo unit tests
# proprietary program, I am not going to enforce this in court.
import pytest
+from pathlib import Path
+from selenium.webdriver.support.ui import WebDriverWait
+from selenium.webdriver.support import expected_conditions as EC
-from ..profiles import firefox_safe_mode
-from ..server import do_an_internet
-from ..script_loader import load_script
+from ..profiles import firefox_safe_mode
+from ..server import do_an_internet
+from ..script_loader import load_script
+from ..extension_crafting import make_extension
@pytest.fixture(scope="package")
def proxy():
@@ -43,6 +47,23 @@ def driver(proxy):
yield driver
driver.quit()
+@pytest.fixture()
+def webextension(driver, request):
+ ext_data = request.node.get_closest_marker('ext_data')
+ if ext_data is None:
+ raise Exception('"webextension" fixture requires "ext_data" marker to be set')
+
+ ext_path = make_extension(Path(driver.firefox_profile.path),
+ **ext_data.args[0])
+ driver.get('https://gotmyowndoma.in/')
+ addon_id = driver.install_addon(str(ext_path), temporary=True)
+ WebDriverWait(driver, 10).until(
+ EC.title_contains("Extension's options page for testing")
+ )
+ yield
+ driver.uninstall_addon(addon_id)
+ ext_path.unlink()
+
script_injecting_script = '''\
/*
* Selenium by default executes scripts in some weird one-time context. We want
@@ -63,8 +84,8 @@ window.arguments = arguments[1];
document.body.append(script_elem);
/*
- * To ease debugging, we want this script to forward signal all exceptions from
- * the injectee.
+ * To ease debugging, we want this script to signal all exceptions from the
+ * injectee.
*/
try {
if (window.haketilo_selenium_exception !== false)
diff --git a/test/unit/test_basic.py b/test/unit/test_basic.py
index cbe5c8c..3b09cb6 100644
--- a/test/unit/test_basic.py
+++ b/test/unit/test_basic.py
@@ -26,8 +26,9 @@ def test_driver(driver):
"""
for proto in ['http://', 'https://']:
driver.get(proto + 'gotmyowndoma.in')
- element = driver.find_element_by_tag_name('title')
- title = driver.execute_script('return arguments[0].innerText;', element)
+ title = driver.execute_script(
+ 'return document.getElementsByTagName("title")[0].innerText;'
+ )
assert "Schrodinger's Document" in title
def test_script_loader(execute_in_page, load_into_page):
@@ -39,3 +40,15 @@ def test_script_loader(execute_in_page, load_into_page):
page='https://gotmyowndoma.in')
assert execute_in_page('returnval(TYPE_PREFIX.VAR);') == '_'
+
+@pytest.mark.ext_data({})
+def test_webextension(driver, webextension):
+ """
+ A trivial test case that verifies a test WebExtension created and installed
+ by the `webextension` fixture works and redirects specially-constructed URLs
+ to its test page.
+ """
+ heading = driver.execute_script(
+ 'return document.getElementsByTagName("h1")[0].innerText;'
+ )
+ assert "Extension's options page for testing" in heading