• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

fiduswriter / fiduswriter / 29761200330

20 Jul 2026 04:48PM UTC coverage: 88.069% (-0.02%) from 88.093%
29761200330

push

github

johanneswilm
remove unused dependencies

10814 of 12279 relevant lines covered (88.07%)

5.46 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

83.54
fiduswriter/testing/selenium_helper.py
1
import re
19✔
2
import os
19✔
3
import time
19✔
4
from urllib3.exceptions import MaxRetryError, ReadTimeoutError
19✔
5
from selenium.common.exceptions import (
19✔
6
    ElementClickInterceptedException,
7
    ElementNotInteractableException,
8
    TimeoutException,
9
    WebDriverException,
10
)
11
from selenium.webdriver.support.wait import WebDriverWait
19✔
12
from selenium.webdriver.support import expected_conditions as EC
19✔
13
from selenium.webdriver.common.by import By
19✔
14
from selenium import webdriver
19✔
15
from selenium.webdriver.chrome.service import Service as ChromiumService
19✔
16
from webdriver_manager.chrome import ChromeDriverManager
19✔
17
from webdriver_manager.core.os_manager import ChromeType
19✔
18

19
from allauth.account.models import EmailAddress
19✔
20
from django.contrib.auth.hashers import make_password
19✔
21
from django.contrib.auth import get_user_model
19✔
22
from django.conf import settings
19✔
23
from django.test import Client
19✔
24
from django.contrib.contenttypes.models import ContentType
19✔
25
import logging
19✔
26

27
logger = logging.getLogger(__name__)
19✔
28

29

30
class SeleniumHelper:
19✔
31
    """
32
    Methods for manipulating django and the browser for testing purposes.
33
    """
34

35
    login_page = "/"
19✔
36

37
    def find_urls(self, string):
19✔
38
        return re.findall(
2✔
39
            (
40
                "http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*(),]|"
41
                "(?:%[0-9a-fA-F][0-9a-fA-F]))+"
42
            ),
43
            string,
44
        )
45

46
    # create django data
47
    def create_user(
19✔
48
        self, username="User", email="test@example.com", passtext="p4ssw0rd"
49
    ):
50
        User = get_user_model()
18✔
51
        user = User.objects.create(
18✔
52
            username=username,
53
            email=email,
54
            password=make_password(passtext),
55
            is_active=True,
56
        )
57
        user.save()
18✔
58

59
        # avoid the unverified-email login trap
60
        EmailAddress.objects.create(
18✔
61
            user=user, email=email, verified=True, primary=True
62
        ).save()
63

64
        return user
18✔
65

66
    # drive browser
67
    def login_user(self, user, driver, client):
19✔
68
        client.force_login(user=user)
13✔
69
        cookie = client.cookies[settings.SESSION_COOKIE_NAME]
13✔
70
        # output the cookie to the console for debugging
71
        logger.debug("cookie: %s" % cookie.value)
13✔
72
        if driver.current_url == "data:,":
13✔
73
            # To set the cookie at the right domain we load the front page.
74
            driver.get(f"{self.live_server_url}{self.login_page}")
13✔
75
            WebDriverWait(driver, self.wait_time).until(
13✔
76
                EC.presence_of_element_located((By.ID, "id-login"))
77
            )
78
        driver.add_cookie(
13✔
79
            {
80
                "name": settings.SESSION_COOKIE_NAME,
81
                "value": cookie.value,
82
                "secure": False,
83
                "path": "/",
84
            }
85
        )
86

87
    def login_user_manually(self, user, driver, passtext="p4ssw0rd"):
19✔
88
        username = user.username
×
89
        driver.delete_cookie(settings.SESSION_COOKIE_NAME)
×
90
        driver.get("{}{}".format(self.live_server_url, "/"))
×
91
        driver.find_element(By.ID, "id-login").send_keys(username)
×
92
        driver.find_element(By.ID, "id-password").send_keys(passtext)
×
93
        driver.find_element(By.ID, "login-submit").click()
×
94
        # Wait until there is an element with the ID user-preferences
95
        # which is only present on the dashboard.
96
        WebDriverWait(driver, self.wait_time).until(
×
97
            EC.presence_of_element_located((By.ID, "user-preferences"))
98
        )
99

100
    def logout_user(self, driver, client):
19✔
101
        client.logout()
2✔
102
        driver.delete_cookie(settings.SESSION_COOKIE_NAME)
2✔
103

104
    def wait_until_file_exists(self, path, wait_time):
19✔
105
        count = 0
4✔
106
        while not os.path.exists(path):
4✔
107
            time.sleep(1)
4✔
108
            count += 1
4✔
109
            if count > wait_time:
4✔
110
                break
×
111

112
    def retry_click(self, driver, selector, retries=5):
19✔
113
        count = 0
2✔
114
        while count < retries:
2✔
115
            try:
2✔
116
                WebDriverWait(driver, self.wait_time).until(
2✔
117
                    EC.element_to_be_clickable(selector)
118
                ).click()
119
                break
2✔
120
            except ElementClickInterceptedException:
1✔
121
                # Alerts may be overlapping the target; wait for them to clear.
122
                try:
1✔
123
                    WebDriverWait(driver, 6).until(
1✔
124
                        EC.invisibility_of_element_located(
125
                            (By.CSS_SELECTOR, "#fw-alerts-outer-wrapper li")
126
                        )
127
                    )
128
                except TimeoutException:
×
129
                    pass
×
130
                count += 1
1✔
131
                time.sleep(1)
1✔
132

133
    def safe_click_element(self, driver, element, retries=3):
19✔
134
        """Click an element, falling back to a JS click if it is not interactable."""
135
        for attempt in range(retries):
2✔
136
            try:
2✔
137
                element.click()
2✔
138
                return
2✔
139
            except (
1✔
140
                ElementClickInterceptedException,
141
                ElementNotInteractableException,
142
            ):
143
                # Scroll the element to the center of the viewport and retry.
144
                driver.execute_script(
1✔
145
                    "arguments[0].scrollIntoView({block: 'center', inline: 'center'});",
146
                    element,
147
                )
148
                time.sleep(0.5)
1✔
149
        # Final fallback: click via JavaScript so tests are not blocked by
150
        # overlapping chrome or elements that Selenium considers not interactable.
151
        driver.execute_script("arguments[0].click();", element)
×
152

153
    def click_new_document_button(self, driver):
19✔
154
        """Click the new document button and handle the encryption choice dialog if present."""
155
        from selenium.common.exceptions import StaleElementReferenceException
6✔
156

157
        for _attempt in range(3):
6✔
158
            try:
6✔
159
                WebDriverWait(driver, self.wait_time).until(
6✔
160
                    EC.element_to_be_clickable(
161
                        (By.CSS_SELECTOR, ".new_document button")
162
                    )
163
                ).click()
164
                break
6✔
165
            except StaleElementReferenceException:
×
166
                time.sleep(0.5)
×
167
        try:
6✔
168
            WebDriverWait(driver, 2).until(
6✔
169
                EC.presence_of_element_located((By.CSS_SELECTOR, ".fw-dialog"))
170
            )
171
            driver.find_element(By.CSS_SELECTOR, ".fw-dialog .fw-dark").click()
×
172
        except TimeoutException:
6✔
173
            pass
6✔
174

175
    @classmethod
19✔
176
    def get_drivers(cls, number, download_dir=False, user_agent=False):
19✔
177
        # django native clients, to be used for faster login.
178
        clients = []
19✔
179
        for i in range(number):
19✔
180
            clients.append(Client())
19✔
181
        drivers = []
19✔
182
        wait_time = 0
19✔
183
        options = webdriver.ChromeOptions()
19✔
184
        options.add_argument("--kiosk-printing")
19✔
185
        options.add_argument("--safebrowsing-disable-download-protection")
19✔
186
        options.add_argument("--safebrowsing-disable-extension-blacklist")
19✔
187
        options.add_argument("--window-size=1920,1080")
19✔
188
        prefs = {
19✔
189
            "profile.password_manager_leak_detection": False,
190
        }
191
        if download_dir:
19✔
192
            prefs["download.default_directory"] = download_dir
5✔
193
            prefs["download.prompt_for_download"] = False
5✔
194
            prefs["download.directory_upgrade"] = True
5✔
195
        options.add_experimental_option("prefs", prefs)
19✔
196
        if user_agent:
19✔
197
            options.add_argument(f"user-agent={user_agent}")
1✔
198
        if os.getenv("CI"):
19✔
199
            options.binary_location = "/usr/bin/google-chrome-stable"
19✔
200
            if os.getenv("DEBUG_MODE") != "1":
19✔
201
                options.add_argument("--headless=new")
19✔
202
            options.add_argument("--disable-gpu")
19✔
203
            wait_time = 20
19✔
204
        else:
205

206
            wait_time = 10
×
207
        for i in range(number):
19✔
208
            driver_env = os.environ.copy()
19✔
209
            if os.getenv("CI") and os.getenv("DEBUG_MODE") == "1" and i < 2:
19✔
210
                driver_env["DISPLAY"] = f":{99 - i}"
×
211
            driver = webdriver.Chrome(
19✔
212
                service=ChromiumService(
213
                    ChromeDriverManager(
214
                        chrome_type=ChromeType.GOOGLE
215
                    ).install(),
216
                    env=driver_env,
217
                ),
218
                options=options,
219
            )
220
            # Set sizes of browsers so that all buttons are visible.
221
            driver.set_window_position(0, 0)
19✔
222
            driver.set_window_size(1920, 1080)
19✔
223
            drivers.append(driver)
19✔
224
        cls.drivers = drivers
19✔
225
        return {"clients": clients, "drivers": drivers, "wait_time": wait_time}
19✔
226

227
    def setUp(self):
19✔
228
        # Clear ContentType cache during testing to prevent FK constraint errors
229
        # Content types get new IDs after each test but cached values don't update
230
        ContentType.objects.clear_cache()
14✔
231
        self.addCleanup(ContentType.objects.clear_cache)
14✔
232
        return super().setUp()
14✔
233

234
    def tearDown(self):
19✔
235
        # Source: https://stackoverflow.com/a/39606065
236
        result = self._outcome.result
19✔
237
        ok = all(
19✔
238
            test != self for test, text in result.errors + result.failures
239
        )
240
        if ok:
19✔
241
            for driver in self.drivers:
19✔
242
                self.leave_site(driver)
19✔
243
        else:
244
            if not os.path.exists("screenshots"):
×
245
                os.makedirs("screenshots")
×
246
            for id, driver in enumerate(self.drivers, start=1):
×
247
                screenshotfile = (
×
248
                    f"screenshots/driver{id}-{self._testMethodName}.png"
249
                )
250
                logger.info(f"Saving {screenshotfile}")
×
251
                driver.save_screenshot(screenshotfile)
×
252
                self.leave_site(driver)
×
253
        return super().tearDown()
19✔
254

255
    def safe_get(self, driver, url, retries=3):
19✔
256
        """Navigate to url, retrying on transient timeout errors."""
257
        for attempt in range(retries):
1✔
258
            try:
1✔
259
                driver.get(url)
1✔
260
                return
1✔
261
            except (
×
262
                MaxRetryError,
263
                ReadTimeoutError,
264
                TimeoutException,
265
                WebDriverException,
266
            ):
267
                if attempt == retries - 1:
×
268
                    raise
×
269
                time.sleep(1)
×
270

271
    def leave_site(self, driver):
19✔
272
        try:
19✔
273
            driver.execute_script(
19✔
274
                "if (window.theApp && window.theApp.page) {"
275
                # Only call close() for non-E2EE pages. The E2EE editor's
276
                # close() schedules an async snapshot + a 300 ms timeout
277
                # before closing the WebSocket, which can race with the
278
                # navigation below and leave the browser in a bad state.
279
                "  if (!window.theApp.page.e2ee && window.theApp.page.close) {"
280
                "    window.theApp.page.close();"
281
                "  }"
282
                "  window.theApp.page = null;"
283
                "}"
284
                # Suppress any 'Leave site?' beforeunload dialog so that
285
                # driver.get() below cannot be blocked by it.
286
                "window.onbeforeunload = null;"
287
            )
288
        except (MaxRetryError, ReadTimeoutError, WebDriverException):
1✔
289
            pass
1✔
290
        try:
19✔
291
            driver.get("data:,")
19✔
292
        except (MaxRetryError, ReadTimeoutError, WebDriverException):
2✔
293
            pass
2✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc