import os
from time import sleep
from typing import Literal
from urllib.parse import unquote
import urllib3
from fortrace.core.qemu_monitor import QEMUMonitorSession
from fortrace.utility.applications.application import (
ApplicationEvent,
ApplicationType,
FileDialogue,
GenericApplication,
ParentNotifier,
PopupType,
)
from fortrace.utility.image_processing.image_similarity import nrmse
from fortrace.utility.logger_helper import setup_logger
logger = setup_logger(__name__)
[docs]
class GenericWebBrowser(GenericApplication):
"""Representation of a generic Web browser, providing basic functionality."""
_tabs = list[str]
def __init__(
self, name: str, qs: QEMUMonitorSession, parent_notifier: ParentNotifier
):
super().__init__(name, ApplicationType.WEB_BROWSER, qs, parent_notifier)
# TODO: think about tab identifier, instead of empty string/ tab dataclass?
self._tabs = [""] # contains tabs in correct order, start with one opened tab
self._active_tab = 0
def _new_tab(self):
self._qs.send_key_combination("ctrl-t")
self._tabs.append("")
self._active_tab = len(self._tabs) - 1
sleep(1)
[docs]
def close_tab(self):
"""Close focused tab"""
self._qs.send_key_combination("ctrl-w")
self._tabs.remove(self._tabs[self._active_tab])
# deleted tab was rightmost tab
if self._active_tab == len(self._tabs):
self._active_tab -= 1
# if last tab is closed application will close and must notify desktop_env
if len(self._tabs) == 0:
self._parent_notifier(ApplicationEvent.CLOSED, application_reference=self)
def _focus_address_bar(self):
self._qs.send_key_combination("ctrl-l")
sleep(0.5)
[docs]
def focus_tab(self, url: str):
"""Set focus on tab.
Args:
url: the url of the tab to be focused (the parameter is matched against all members of _active_tabs. The
first substring match is selected)
"""
new_active_tab_idx = 0
for idx, tab_url in enumerate(self._tabs):
if url in tab_url:
new_active_tab_idx = idx
self._goto_tab(new_active_tab_idx)
[docs]
def go_back(self):
"""Go back one page."""
self._qs.send_key_combination("alt-left")
[docs]
def go_forward(self):
"""Go forward one page."""
self._qs.send_key_combination("alt-right")
def _goto_tab(self, idx: int):
"""Go to the opened tab with the specified index.
Args:
idx: index of the tab (starting at 1)
"""
if 1 <= idx <= 8:
self._qs.send_key_combination(f"alt-{idx}")
else:
raise NotImplementedError(f"Can't switch to tab with index {idx}")
self._active_tab = idx
[docs]
def browse_to_url(
self,
url: str | urllib3.util.Url,
new_tab: bool = False,
):
"""Browse to the provided URL or download file behind URL
If you provide the URL pointing directly to the file, most browsers will download it.
Args:
url: to destination url #TODO: check whether str is needed
new_tab: open it in a new tab. If yes, the focus will be on the new tab
"""
if new_tab:
self._new_tab()
before = self.take_screenshot()
self._focus_address_bar()
self._qs.send_text(
unquote(url) if isinstance(url, str) else unquote(url.url), True
)
for _ in range(10):
sleep(5) # wait for page to load
score = nrmse(before, self.take_screenshot())
if score >= 0.25:
logger.debug("Consider side as loaded, since screenshots differ enough")
break
else:
logger.warning(
"After more than 50 seconds the screenshots still look very similar"
" (NRMSE: %s). This might be due to the page %s failed to load.",
score,
str(url),
)
[docs]
def save_to_favorites(self):
"""Bookmarks/'saves to favourites' current page of active tab."""
self._qs.send_key_combination("ctrl-d")
self._qs.send_key_combination("ret")
[docs]
def save(self, destination: os.PathLike | None = None, name: str | None = None):
"""Save the current page.
Args:
destination: path to save file to (defaults to downloads directory)
name: can be provided if the file should be renamed
"""
before = self._qs.take_screenshot()
self._qs.send_key_combination("ctrl-s")
saving_dialogue = self.open_popup(
before, PopupType.FILE_DIALOGUE
) # type:FileDialogue
saving_dialogue.save_to_directory(destination, name)
[docs]
def remove_from_favorite(self):
"""Removes current page from bookmarks/favourites."""
self._qs.send_key_combination("ctrl-d")
self._qs.send_key_combination("tab", 3)
self._qs.send_key_combination("ret")
[docs]
def start_web_search(
self,
search_terms: str,
search_engine: Literal["Google", "Bing", "DuckDuckGo"] | None = None,
):
"""Start a web search from the address bar (as search engine can be specified there).
Args:
search_terms: the terms to search
search_engine: which search engine to use (None for browser default)
"""
self._focus_address_bar()
if search_engine is not None:
self._qs.send_text("@" + search_engine + " ")
self._qs.send_text(search_terms, True)
@property
def active_tab(self):
return self._tabs[self._active_tab]