Skip to content

Commit

Permalink
end2end tests: add addon helper from grasp, start migrating/unifying
Browse files Browse the repository at this point in the history
  • Loading branch information
karlicoss committed May 18, 2024
1 parent 813c4a6 commit a3043c0
Show file tree
Hide file tree
Showing 2 changed files with 169 additions and 8 deletions.
119 changes: 119 additions & 0 deletions tests/addon_helper.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
from dataclasses import dataclass
from functools import cached_property
import json
from pathlib import Path
import subprocess
import re


import psutil
from selenium import webdriver


@dataclass
class AddonHelper:
driver: webdriver.Remote

@cached_property
def addon_id(self) -> str:
return get_addon_id(driver=self.driver)

@property
def extension_prefix(self) -> str:
protocol = {
'chrome': 'chrome-extension',
'firefox': 'moz-extension',
}[self.driver.name]
return f'{protocol}://{self.addon_id}'

def open_page(self, path: str) -> None:
self.driver.get(self.extension_prefix + '/' + path)


# NOTE looks like it used to be posssible in webdriver api?
# at least as of 2011 https://github.com/gavinp/chromium/blob/681563ea0f892a051f4ef3d5e53438e0bb7d2261/chrome/test/webdriver/test/chromedriver.py#L35-L40
# but here https://github.com/SeleniumHQ/selenium/blob/master/cpp/webdriver-server/command_types.h there are no Extension commands
# also see driver.command_executor._commands
def _get_chrome_addon_id(driver: webdriver.Remote) -> str:
"""
For a temporary addon extension id is autogenerated, so we need to extract it every time
"""
user_data_dir = Path(driver.capabilities['chrome']['userDataDir'])
prefs_file = user_data_dir / 'Default/Preferences'
assert prefs_file.exists(), prefs_file

# for some idiotic reason, after chrome launches, extension settings aren't immediately available
# this can take up to 30 secons in this loop until they are populated
while True:
prefs = json.loads(prefs_file.read_text())
extension_settings = prefs.get('extensions', {}).get('settings', None)
if extension_settings is not None:
# there are some other weird builtin extension as well
# this seems like the easiest way to filter them out extracing by extension name or path
[addon_id] = [k for k, v in extension_settings.items() if v['creation_flags'] != 1]
return addon_id


def _get_firefox_addon_id(driver: webdriver.Remote) -> str:
moz_profile = Path(driver.capabilities['moz:profile'])
prefs_file = moz_profile / 'prefs.js'
assert prefs_file.exists(), prefs_file

while True:
for line in prefs_file.read_text().splitlines():
m = re.fullmatch(r'user_pref\("extensions.webextensions.uuids", "(.*)"\);', line)
if m is None:
continue
# this contains a json with escaped quotes
user_prefs_s = m.group(1).replace('\\', '')
user_prefs = json.loads(user_prefs_s)
addon_ids = [v for k, v in user_prefs.items() if 'mozilla.' not in k]
if len(addon_ids) == 0:
# for some stupid reason it doesn't appear immediately in the file
continue
[addon_id] = addon_ids
return addon_id


def get_addon_id(driver: webdriver.Remote) -> str:
extractor = {
'firefox': _get_firefox_addon_id,
'chrome': _get_chrome_addon_id,
}[driver.name]
return extractor(driver)


def get_window_id(driver: webdriver.Remote) -> str:
if driver.name == 'firefox':
pid = str(driver.capabilities['moz:processID'])
elif driver.name == 'chrome':
# ugh no pid in capabilities...
driver_pid = driver.service.process.pid # type: ignore[attr-defined]
process = psutil.Process(driver_pid)
[chrome_process] = process.children()
cmdline = chrome_process.cmdline()
assert '--enable-automation' in cmdline, cmdline
pid = str(chrome_process.pid)
else:
raise RuntimeError(driver.name)
return get_wid_by_pid(pid)


def get_wid_by_pid(pid: str) -> str:
# https://askubuntu.com/a/385037/427470
wids = subprocess.check_output(['xdotool', 'search', '--pid', pid]).decode('utf8').splitlines()
wids = [w.strip() for w in wids if len(w.strip()) > 0]

def has_wm_desktop(wid: str) -> bool:
# TODO no idea why is that important. found out experimentally
out = subprocess.check_output(['xprop', '-id', wid, '_NET_WM_DESKTOP']).decode('utf8')
return 'not found' not in out

[wid] = filter(has_wm_desktop, wids)
return wid


def focus_browser_window(driver: webdriver.Remote) -> None:
# FIXME assert not is_headless(driver) # just in case
wid = get_window_id(driver)
subprocess.check_call(['xdotool', 'windowactivate', '--sync', wid])
58 changes: 50 additions & 8 deletions tests/end2end_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from contextlib import contextmanager, ExitStack
import json
from pathlib import Path
from dataclasses import dataclass
from datetime import datetime
from tempfile import TemporaryDirectory
import os
Expand Down Expand Up @@ -38,6 +39,7 @@
from common import under_ci, has_x, local_http_server, notnone
from browser_helper import open_extension_page, get_cmd_hotkey
from webdriver_utils import frame_context, window_context, is_visible
from addon_helper import AddonHelper


logger = LazyLogger('promnesia-tests', level='debug')
Expand Down Expand Up @@ -158,6 +160,7 @@ def _get_webdriver(tdir: Path, browser: Browser, extension: bool=True) -> Driver
from selenium.webdriver.chrome.service import Service
service = Service(**mexepath)
driver = webdriver.Chrome(service=service, options=cr_options)
# TODO ad this to common helper
logger.info(f"using webdriver: {driver.capabilities['browserVersion']} {driver.capabilities['chrome']['chromedriverVersion']}")
else:
raise RuntimeError(f'Unexpected browser {browser}')
Expand Down Expand Up @@ -231,8 +234,8 @@ def set_checkbox(cid: str, value: bool) -> None:
# TODO log properly
print(f"Setting: port {port}, show_dots {show_dots}")

helper = TestHelper(driver)
page = helper.options_page
addon = Addon(helper=AddonHelper(driver=driver))
page = addon.options_page
page.open()

set_host(driver=driver, host=host, port=port)
Expand Down Expand Up @@ -497,6 +500,38 @@ def save(self) -> None:
_switch_to_alert(self.driver).accept()


@dataclass
class OptionsPage2:
helper: AddonHelper

def open(self) -> None:
# TODO extract from manifest -> options_id -> options.html
# seems like addon just links to the actual manifest on filesystem, so will need to read from that
page_name = 'options_page.html'
self.helper.open_page(page_name)

# make sure settings are loaded first -- otherwise we might get race conditions when we try to set them in tests
Wait(self.helper.driver, timeout=5).until(
EC.presence_of_element_located((By.ID, 'promnesia-settings-loaded'))
)

def save(self) -> None:
OptionsPage(driver=self.helper.driver, helper=None).save() # type: ignore[arg-type]

def set_position(self, settings: str):
OptionsPage(driver=self.helper.driver, helper=None).set_position(settings=settings) # type: ignore[arg-type]


# TODO gradually replace TestHelper and other older stuff
@dataclass
class Addon:
helper: AddonHelper

@property
def options_page(self) -> OptionsPage2:
return OptionsPage2(helper=self.helper)


class TestHelper(NamedTuple):
driver: Driver

Expand Down Expand Up @@ -678,28 +713,35 @@ def driver(browser: Browser) -> Iterator[Driver]:
yield d


@pytest.fixture
def addon(browser: Browser) -> Iterator[Addon]:
with get_webdriver(browser=browser) as driver:
helper = AddonHelper(driver)
yield Addon(helper=helper)


@browsers()
def test_installs(tmp_path: Path, driver: Driver) -> None:
def test_installs(addon: Addon) -> None:
"""
Even loading the extension into webdriver is pretty elaborate, so the test just checks it works
"""
pass
assert addon.helper.addon_id is not None


@browsers()
def test_settings(tmp_path: Path, driver: Driver) -> None:
def test_settings(addon: Addon) -> None:
"""
Just a basic test for opening options page and making sure it loads options
"""
helper = TestHelper(driver)
helper.open_options_page()
driver = addon.helper.driver
addon.options_page.open()
hh = driver.find_element(By.ID, 'host_id')
assert hh.get_attribute('value') == 'http://localhost:13131' # default

configure_extension(driver, port='12345', show_dots=False)
driver.get('about:blank')

helper.open_options_page()
addon.options_page.open()
hh = driver.find_element(By.ID, 'host_id')
assert hh.get_attribute('value') == 'http://localhost:12345'

Expand Down

0 comments on commit a3043c0

Please sign in to comment.