Project

General

Profile

Download (6.39 KB) Statistics
| Branch: | Tag: | Revision:

haketilo / test / haketilo_test / conftest.py @ 241f58b6

1
# SPDX-License-Identifier: GPL-3.0-or-later
2

    
3
"""
4
Common fixtures for Haketilo unit tests
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 of this code in a
26
# proprietary program, I am not going to enforce this in court.
27

    
28
import pytest
29
from pathlib import Path
30
from tempfile import TemporaryDirectory
31
from selenium.webdriver.common.by import By
32
from selenium.webdriver.support.ui import WebDriverWait
33
from selenium.webdriver.support import expected_conditions as EC
34

    
35
from .profiles           import firefox_safe_mode
36
from .server             import do_an_internet
37
from .extension_crafting import make_extension
38
from .world_wide_library import start_serving_script, dump_scripts
39
from .misc_constants     import proj_root
40

    
41
@pytest.fixture(scope="session")
42
def proxy():
43
    httpd = do_an_internet()
44
    yield httpd
45
    httpd.shutdown()
46

    
47
@pytest.fixture(scope="session")
48
def _driver(proxy):
49
    with firefox_safe_mode(proxy.server_port) as driver:
50
        yield driver
51
        driver.quit()
52

    
53
def close_all_but_one_window(driver):
54
    while len(driver.window_handles) > 1:
55
        driver.switch_to.window(driver.window_handles[-1])
56
        driver.close()
57
    driver.switch_to.window(driver.window_handles[0])
58

    
59
@pytest.fixture()
60
def driver(proxy, _driver, request):
61
    nav_target = request.node.get_closest_marker('get_page')
62
    nav_target = nav_target.args[0] if nav_target else 'about:blank'
63

    
64
    second_driver = request.node.get_closest_marker('second_driver')
65

    
66
    if second_driver:
67
        with firefox_safe_mode(proxy.server_port) as _driver:
68
            _driver.get(nav_target)
69
            yield _driver
70
            _driver.quit()
71
    else:
72
        close_all_but_one_window(_driver)
73
        _driver.get(nav_target)
74
        _driver.implicitly_wait(0)
75
        yield _driver
76

    
77
def safe_uninstall_addon(driver, addon_id):
78
    """
79
    Unloading an extension might cause its windows to vanish. Make sure there's
80
    at least one window navigated to some other page before uninstalling the
81
    addon. Otherwise, we could be left with a windowless browser :c
82
    """
83
    driver.switch_to.window(driver.window_handles[-1])
84
    driver.get('https://gotmyowndoma.in/')
85
    driver.uninstall_addon(addon_id)
86

    
87
@pytest.fixture()
88
def webextension(driver, request):
89
    ext_data = request.node.get_closest_marker('ext_data')
90
    if ext_data is None:
91
        raise Exception('"webextension" fixture requires "ext_data" marker to be set')
92
    ext_data = ext_data.args[0].copy()
93

    
94
    navigate_to = ext_data.get('navigate_to')
95
    if navigate_to is not None:
96
        del ext_data['navigate_to']
97

    
98
    driver.get('https://gotmyowndoma.in/')
99
    ext_path = make_extension(Path(driver.firefox_profile.path), **ext_data)
100
    addon_id = driver.install_addon(str(ext_path), temporary=True)
101
    get_url = lambda d: d.execute_script('return window.ext_page_url;')
102
    ext_page_url = WebDriverWait(driver, 10).until(get_url)
103
    driver.get(ext_page_url)
104

    
105
    if navigate_to is not None:
106
        driver.get(driver.current_url.replace('testpage.html', navigate_to))
107

    
108
    yield
109

    
110
    safe_uninstall_addon(driver, addon_id)
111
    ext_path.unlink()
112

    
113
@pytest.fixture()
114
def haketilo(driver):
115
    addon_id = driver.install_addon(str(Path.cwd() / 'mozilla-build.zip'),
116
                                    temporary=True)
117

    
118
    yield
119

    
120
    safe_uninstall_addon(driver, addon_id)
121

    
122
script_injector_script = '''\
123
/*
124
 * Selenium by default executes scripts in some weird one-time context. We want
125
 * separately-loaded scripts to be able to access global variables defined
126
 * before, including those declared with `const` or `let`. To achieve that, we
127
 * run our scripts by injecting them into the page with a <script> tag that runs
128
 * javascript served by our proxy. We use custom properties of the `window`
129
 * object to communicate with injected code.
130
 */
131
const inject = async () => {
132
    delete window.haketilo_selenium_return_value;
133
    delete window.haketilo_selenium_exception;
134
    window.returnval = val => window.haketilo_selenium_return_value = val;
135

    
136
    const injectee = document.createElement('script');
137
    injectee.src = arguments[0];
138
    injectee.type = "application/javascript";
139
    injectee.async = true;
140
    const prom = new Promise(cb => injectee.onload = cb);
141

    
142
    window.arguments = arguments[1];
143
    document.body.append(injectee);
144

    
145
    await prom;
146

    
147
    /*
148
     * To ease debugging, we want this script to signal all exceptions from the
149
     * injectee.
150
     */
151
    if (window.haketilo_selenium_exception !== false)
152
        throw ['haketilo_selenium_error',
153
               'Error in injected script! Check your geckodriver.log and ./injected_scripts/!'];
154

    
155
    return window.haketilo_selenium_return_value;
156
}
157
return inject();
158
'''
159

    
160
def _execute_in_page_context(driver, script, args):
161
    script = script + '\n;\nwindow.haketilo_selenium_exception = false;'
162
    script_url = start_serving_script(script)
163

    
164
    try:
165
        result = driver.execute_script(script_injector_script, script_url, args)
166
        if type(result) is list and len(result) == 2 and \
167
           result[0] == 'haketilo_selenium_error':
168
            raise Exception(result[1])
169
        return result
170
    except Exception as e:
171
        dump_scripts()
172
        raise e from None
173

    
174
# Some fixtures here just define functions that operate on driver. We should
175
# consider making them into webdriver wrapper class methods.
176

    
177
@pytest.fixture()
178
def execute_in_page(driver):
179
    def do_execute(script, *args):
180
        return _execute_in_page_context(driver, script, args)
181

    
182
    yield do_execute
183

    
184
@pytest.fixture()
185
def wait_elem_text(driver):
186
    def do_wait(id, text):
187
        WebDriverWait(driver, 10).until(
188
            EC.text_to_be_present_in_element((By.ID, id), text)
189
        )
190

    
191
    yield do_wait
(3-3/11)