Project

General

Profile

« Previous | Next » 

Revision c699b640

Added by koszko over 1 year ago

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.

View differences:

copyright
97 97
License: CC0
98 98

  
99 99
Files: test/profiles.py test/script_loader.py test/unit/conftest.py
100
       test/extension_crafting.py
100 101
Copyright: 2021 Wojtek Kosior <koszko@koszko.org>
101 102
License: GPL-3+
102 103
Comment: Wojtek Kosior promises not to sue even in case of violations
test/extension_crafting.py
1
# SPDX-License-Identifier: GPL-3.0-or-later
2

  
3
"""
4
Making temporary WebExtensions for use in the test suite
5
"""
6

  
7
# This file is part of Haketilo.
8
#
9
# Copyright (C) 2021 Wojtek Kosior <koszko@koszko.org>
10
#
11
# This program is free software: you can redistribute it and/or modify
12
# it under the terms of the GNU General Public License as published by
13
# the Free Software Foundation, either version 3 of the License, or
14
# (at your option) any later version.
15
#
16
# This program is distributed in the hope that it will be useful,
17
# but WITHOUT ANY WARRANTY; without even the implied warranty of
18
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
19
# GNU General Public License for more details.
20
#
21
# You should have received a copy of the GNU General Public License
22
# along with this program.  If not, see <https://www.gnu.org/licenses/>.
23
#
24
# I, Wojtek Kosior, thereby promise not to sue for violation of this file's
25
# license. Although I request that you do not make use this code in a
26
# proprietary program, I am not going to enforce this in court.
27

  
28
import json
29
import zipfile
30
from pathlib import Path
31
from uuid import uuid4
32

  
33
from .misc_constants import *
34

  
35
class ManifestTemplateValueToFill:
36
    pass
37

  
38
def manifest_template():
39
    return {
40
        'manifest_version': 2,
41
        'name': 'Haketilo test extension',
42
        'version': '1.0',
43
        'applications': {
44
	    'gecko': {
45
	        'id': ManifestTemplateValueToFill(),
46
	        'strict_min_version': '60.0'
47
	    }
48
        },
49
        'permissions': [
50
	    'contextMenus',
51
	    'webRequest',
52
	    'webRequestBlocking',
53
	    'activeTab',
54
	    'notifications',
55
	    'sessions',
56
	    'storage',
57
	    'tabs',
58
	    '<all_urls>',
59
	    'unlimitedStorage'
60
        ],
61
        'web_accessible_resources': ['testpage.html'],
62
        'background': {
63
	    'persistent': True,
64
	    'scripts': ['__open_test_page.js', 'background.js']
65
        },
66
        'content_scripts': [
67
	    {
68
	        'run_at': 'document_start',
69
	        'matches': ['<all_urls>'],
70
	        'match_about_blank': True,
71
	        'all_frames': True,
72
	        'js': ['content.js']
73
	    }
74
        ]
75
    }
76

  
77
default_background_script = ''
78
default_content_script = ''
79
default_test_page = '''
80
<!DOCTYPE html>
81
<html>
82
  <head>
83
    <title>Extension's options page for testing</title>
84
  </head>
85
  <body>
86
    <h1>Extension's options page for testing</h1>
87
  </body>
88
</html>
89
'''
90

  
91
open_test_page_script = '''(() => {
92
const page_url = browser.runtime.getURL("testpage.html");
93
const execute_details = {
94
    code: `window.location.href=${JSON.stringify(page_url)};`
95
};
96
browser.tabs.query({currentWindow: true, active: true})
97
    .then(t => browser.tabs.executeScript(t.id, execute_details));
98
})();'''
99

  
100
def make_extension(destination_dir,
101
                   background_script=default_background_script,
102
                   content_script=default_content_script,
103
                   test_page=default_test_page,
104
                   extra_files={}):
105
    manifest = manifest_template()
106
    extension_id = '{%s}' % uuid4()
107
    manifest['applications']['gecko']['id'] = extension_id
108
    files = {
109
        'manifest.json'      : json.dumps(manifest),
110
        '__open_test_page.js': open_test_page_script,
111
        'background.js'      : background_script,
112
        'content.js'         : content_script,
113
        'testpage.html'      : test_page,
114
        **extra_files
115
    }
116
    destination_path = destination_dir / f'{extension_id}.xpi'
117
    with zipfile.ZipFile(destination_path, 'x') as xpi:
118
        for filename, contents in files.items():
119
            xpi.writestr(filename, contents)
120

  
121
    return destination_path
test/misc_constants.py
41 41

  
42 42
default_cert_dir = here / 'certs'
43 43

  
44
default_extension_uuid = 'a1291446-be95-48ad-a4c6-a475e389399b'
45
default_haketilo_id = '{6fe13369-88e9-440f-b837-5012fb3bedec}'
46

  
44 47
mime_types = {
45 48
	"7z": "application/x-7z-compressed",	"oga": "audio/ogg",
46 49
	"abw": "application/x-abiword",		"ogv": "video/ogg",
test/profiles.py
27 27

  
28 28
from selenium import webdriver
29 29
from selenium.webdriver.firefox.options import Options
30
import time
30
import json
31
from shutil import rmtree
31 32

  
32 33
from .misc_constants import *
33 34

  
......
35 36
    """
36 37
    This wrapper class around selenium.webdriver.Firefox adds a `loaded_scripts`
37 38
    instance property that gets resetted to an empty array every time the
38
    `get()` method is called.
39
    `get()` method is called and also facilitates removing the temporary
40
    profile directory after Firefox quits.
39 41
    """
40 42
    def __init__(self, *args, **kwargs):
41 43
        super().__init__(*args, **kwargs)
......
48 50
        self.reset_loaded_scripts()
49 51
        super().get(*args, **kwargs)
50 52

  
53
    def quit(self, *args, **kwargs):
54
        profile_path = self.firefox_profile.path
55
        super().quit(*args, **kwargs)
56
        rmtree(profile_path, ignore_errors=True)
57

  
51 58
def set_profile_proxy(profile, proxy_host, proxy_port):
52 59
    """
53 60
    Create a Firefox profile that uses the specified HTTP proxy for all
......
67 74
def set_profile_console_logging(profile):
68 75
    profile.set_preference('devtools.console.stdout.content', True)
69 76

  
77
# The function below seems not to work for extensions that are
78
# temporarily-installed in Firefox safe mode. Testing is needed to see if it
79
# works with non-temporary extensions (without safe mode).
80
def set_webextension_uuid(profile, extension_id, uuid=default_extension_uuid):
81
    """
82
    Firefox would normally assign a unique, random UUID to installed extension.
83
    This UUID is needed to easily navigate to extension's settings page (and
84
    other extension's pages). Since there's no way to learn such UUID with
85
    current WebDriver implementation, this function works around this by telling
86
    Firefox to use a predefined UUID for a certain extension.
87
    """
88
    profile.set_preference('extensions.webextensions.uuids',
89
                           json.dumps({extension_id: uuid}))
90

  
70 91
def firefox_safe_mode(firefox_binary=default_firefox_binary,
71 92
                      proxy_host=default_proxy_host,
72 93
                      proxy_port=default_proxy_port):
......
97 118
    profile = webdriver.FirefoxProfile(profile_dir)
98 119
    set_profile_proxy(profile, proxy_host, proxy_port)
99 120
    set_profile_console_logging(profile)
121
    set_webextension_uuid(profile, default_haketilo_id)
100 122

  
101 123
    return HaketiloFirefox(firefox_profile=profile,
102 124
                           firefox_binary=firefox_binary)
test/unit/conftest.py
26 26
# proprietary program, I am not going to enforce this in court.
27 27

  
28 28
import pytest
29
from pathlib import Path
30
from selenium.webdriver.support.ui import WebDriverWait
31
from selenium.webdriver.support import expected_conditions as EC
29 32

  
30
from ..profiles      import firefox_safe_mode
31
from ..server        import do_an_internet
32
from ..script_loader import load_script
33
from ..profiles           import firefox_safe_mode
34
from ..server             import do_an_internet
35
from ..script_loader      import load_script
36
from ..extension_crafting import make_extension
33 37

  
34 38
@pytest.fixture(scope="package")
35 39
def proxy():
......
43 47
        yield driver
44 48
        driver.quit()
45 49

  
50
@pytest.fixture()
51
def webextension(driver, request):
52
    ext_data = request.node.get_closest_marker('ext_data')
53
    if ext_data is None:
54
        raise Exception('"webextension" fixture requires "ext_data" marker to be set')
55

  
56
    ext_path = make_extension(Path(driver.firefox_profile.path),
57
                              **ext_data.args[0])
58
    driver.get('https://gotmyowndoma.in/')
59
    addon_id = driver.install_addon(str(ext_path), temporary=True)
60
    WebDriverWait(driver, 10).until(
61
        EC.title_contains("Extension's options page for testing")
62
    )
63
    yield
64
    driver.uninstall_addon(addon_id)
65
    ext_path.unlink()
66

  
46 67
script_injecting_script = '''\
47 68
/*
48 69
 * Selenium by default executes scripts in some weird one-time context. We want
......
63 84
document.body.append(script_elem);
64 85

  
65 86
/*
66
 * To ease debugging, we want this script to forward signal all exceptions from
67
 * the injectee.
87
 * To ease debugging, we want this script to signal all exceptions from the
88
 * injectee.
68 89
 */
69 90
try {
70 91
    if (window.haketilo_selenium_exception !== false)
test/unit/test_basic.py
26 26
    """
27 27
    for proto in ['http://', 'https://']:
28 28
        driver.get(proto + 'gotmyowndoma.in')
29
        element = driver.find_element_by_tag_name('title')
30
        title = driver.execute_script('return arguments[0].innerText;', element)
29
        title = driver.execute_script(
30
            'return document.getElementsByTagName("title")[0].innerText;'
31
        )
31 32
        assert "Schrodinger's Document" in title
32 33

  
33 34
def test_script_loader(execute_in_page, load_into_page):
......
39 40
                   page='https://gotmyowndoma.in')
40 41

  
41 42
    assert execute_in_page('returnval(TYPE_PREFIX.VAR);') == '_'
43

  
44
@pytest.mark.ext_data({})
45
def test_webextension(driver, webextension):
46
    """
47
    A trivial test case that verifies a test WebExtension created and installed
48
    by the `webextension` fixture works and redirects specially-constructed URLs
49
    to its test page.
50
    """
51
    heading = driver.execute_script(
52
        'return document.getElementsByTagName("h1")[0].innerText;'
53
    )
54
    assert "Extension's options page for testing" in heading

Also available in: Unified diff