diff --git a/extension/src/background.js b/extension/src/background.js index bfa2a5fc..71b0540a 100644 --- a/extension/src/background.js +++ b/extension/src/background.js @@ -1048,10 +1048,10 @@ function initBackground(): void { chrome.runtime.onMessage.addListener((info: any, _: chrome$MessageSender) => { // see selenium_bridge.js - if (info === 'selenium-bridge-activate') { + if (info === 'selenium-bridge-_execute_browser_action') { handleToggleSidebar() } - if (info === 'selenium-bridge-mark-visited') { + if (info === 'selenium-bridge-mark_visited') { handleToggleMarkVisited() } if (info === 'selenium-bridge-search') { diff --git a/extension/src/selenium_bridge.js b/extension/src/selenium_bridge.js index 7c1b0711..d9f78871 100644 --- a/extension/src/selenium_bridge.js +++ b/extension/src/selenium_bridge.js @@ -2,8 +2,8 @@ // hack to hook into the extension... https://stackoverflow.com/a/38554438/706389 for (const x of [ - 'selenium-bridge-activate', - 'selenium-bridge-mark-visited', + 'selenium-bridge-_execute_browser_action', + 'selenium-bridge-mark_visited', 'selenium-bridge-search', ]) { document.addEventListener(x, () => { diff --git a/setup.py b/setup.py index f2fa6e1e..50496224 100644 --- a/setup.py +++ b/setup.py @@ -60,6 +60,8 @@ def main() -> None: 'mypy', 'lxml', # for coverage reports + + 'loguru', ], 'testing-gui': [ # pyautogui seems problematic, wheels often fail to build under windows diff --git a/tests/addon_helper.py b/tests/addon_helper.py index 82f0e1c0..d10bfadb 100644 --- a/tests/addon_helper.py +++ b/tests/addon_helper.py @@ -2,22 +2,31 @@ from functools import cached_property import json from pathlib import Path -import subprocess import re +import subprocess +from typing import Any - +from loguru import logger import psutil from selenium import webdriver +from webdriver_utils import is_headless + @dataclass class AddonHelper: driver: webdriver.Remote + addon_source: Path @cached_property def addon_id(self) -> str: return get_addon_id(driver=self.driver) + @cached_property + def manifest(self) -> Any: + # ugh. sadly (at least in Firefox) there doesn't seem a way to read actual manifest loaded in browser? + return json.loads((self.addon_source / 'manifest.json').read_text()) + @property def extension_prefix(self) -> str: protocol = { @@ -29,6 +38,42 @@ def extension_prefix(self) -> str: def open_page(self, path: str) -> None: self.driver.get(self.extension_prefix + '/' + path) + @property + def options_page_name(self) -> str: + return self.manifest['options_ui']['page'] + + @property + def headless(self) -> bool: + return is_headless(self.driver) + + def trigger_command(self, command: str) -> None: + commands = self.manifest['commands'] + assert command in commands, (command, commands) + + if self.headless: + # see selenium_bridge.js + ccc = f'selenium-bridge-{command}' + self.driver.execute_script( + f""" + var event = document.createEvent('HTMLEvents'); + event.initEvent('{ccc}', true, true); + document.dispatchEvent(event); + """ + ) + else: + hotkey = commands[command]['suggested_key']['default'] + self.trigger_hotkey(hotkey) + + def trigger_hotkey(self, key: str) -> None: + assert not self.headless # just in case + lkey = key.lower().split('+') + logger.debug(f'sending hotkey {lkey}') + + import pyautogui + + focus_browser_window(self.driver) + pyautogui.hotkey(*lkey) + # 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 @@ -114,6 +159,6 @@ def has_wm_desktop(wid: str) -> bool: def focus_browser_window(driver: webdriver.Remote) -> None: - # FIXME assert not is_headless(driver) # just in case + assert not is_headless(driver) # just in case wid = get_window_id(driver) subprocess.check_call(['xdotool', 'windowactivate', '--sync', wid]) diff --git a/tests/end2end_test.py b/tests/end2end_test.py index 3a835b15..71cbe69e 100755 --- a/tests/end2end_test.py +++ b/tests/end2end_test.py @@ -38,7 +38,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 webdriver_utils import frame_context, is_visible, is_headless from addon_helper import AddonHelper, focus_browser_window, get_window_id @@ -222,7 +222,9 @@ def set_checkbox(cid: str, value: bool) -> None: # TODO log properly print(f"Setting: port {port}, show_dots {show_dots}") - addon = Addon(helper=AddonHelper(driver=driver)) + addon_source = get_addon_path(kind=driver.name) + helper = AddonHelper(driver=driver, addon_source=addon_source) + addon = Addon(helper=helper) page = addon.options_page page.open() @@ -288,16 +290,6 @@ def send_key(key) -> None: pyautogui.hotkey(*key) -def is_headless(driver: Driver) -> bool: - if driver.name == 'firefox': - return driver.capabilities.get('moz:headless', False) - elif driver.name == 'chrome': - # https://antoinevastel.com/bot%20detection/2018/01/17/detect-chrome-headless-v2.html - return driver.execute_script("return navigator.webdriver") is True - else: - raise RuntimeError(driver.name) - - # TODO move to common or something def trigger_hotkey(driver: Driver, hotkey: str) -> None: headless = is_headless(driver) @@ -313,8 +305,8 @@ def trigger_hotkey(driver: Driver, hotkey: str) -> None: def trigger_command(driver: Driver, cmd: str) -> None: if is_headless(driver): ccc = { - Command.ACTIVATE : 'selenium-bridge-activate', - Command.MARK_VISITED: 'selenium-bridge-mark-visited', + Command.ACTIVATE : 'selenium-bridge-_execute_browser_action', + Command.MARK_VISITED: 'selenium-bridge-mark_visited', Command.SEARCH : 'selenium-bridge-search', }[cmd] # see selenium_bridge.js @@ -460,10 +452,7 @@ 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) + self.helper.open_page(self.helper.options_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( @@ -497,8 +486,9 @@ class AddonHelperX: delegate: AddonHelper + # can remove later, this is just hack for Addon.sidebar def activate(self) -> None: - trigger_command(self.delegate.driver, Command.ACTIVATE) + self.delegate.trigger_command(Command.ACTIVATE) def __getattr__(self, name: str) -> Any: return getattr(self.delegate, name) @@ -701,7 +691,8 @@ def driver(browser: Browser) -> Iterator[Driver]: @pytest.fixture def addon(driver: Driver) -> Iterator[Addon]: - helper = AddonHelper(driver) + addon_source = get_addon_path(kind=driver.name) + helper = AddonHelper(driver=driver, addon_source=addon_source) yield Addon(helper=helper) diff --git a/tests/webdriver_utils.py b/tests/webdriver_utils.py index 270113de..905151c2 100644 --- a/tests/webdriver_utils.py +++ b/tests/webdriver_utils.py @@ -44,3 +44,13 @@ def is_visible(driver: Driver, element: WebElement) -> bool: # (returning true for elements that aren't displayed) # it seems to even differ between browsers return driver.execute_script('return arguments[0].checkVisibility()', element) + + +def is_headless(driver: Driver) -> bool: + if driver.name == 'firefox': + return driver.capabilities.get('moz:headless', False) + elif driver.name == 'chrome': + # https://antoinevastel.com/bot%20detection/2018/01/17/detect-chrome-headless-v2.html + return driver.execute_script("return navigator.webdriver") is True + else: + raise RuntimeError(driver.name)