blob: 95cca9e41b30e98ba73c7eaf612b663868247102 [file] [log] [blame]
# Copyright 2022 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
from __future__ import annotations
import abc
import contextlib
import copy
import dataclasses
import pathlib
from typing import (TYPE_CHECKING, Any, Iterator, List, Optional, Tuple, Type,
cast)
from crossbench import plt
from crossbench.browsers.all import Chrome, Chromium, Edge, Firefox, Safari
from crossbench.browsers.attributes import BrowserAttributes
from crossbench.browsers.browser import Browser
from crossbench.browsers.settings import Settings
from crossbench.flags.chrome import ChromeFeatures, ChromeFlags
from crossbench.flags.js_flags import JSFlags
from crossbench.network.base import Network
from crossbench.plt.android_adb import AndroidAdbPlatform
if TYPE_CHECKING:
import datetime as dt
from crossbench.flags.base import Flags
from crossbench.runner.groups import BrowserSessionRunGroup
from crossbench.runner.runner import Runner
@dataclasses.dataclass(frozen=True)
class JsInvocation:
result: Any
script: Optional[str] = None
arguments: Optional[List[Any]] = None
timeout: Optional[dt.timedelta] = None
class MockNetwork(Network):
@contextlib.contextmanager
def open(self, session: BrowserSessionRunGroup) -> Iterator[Network]:
with super().open(session):
assert session.browser.network is self
yield self
assert self.is_running
class MockBrowser(Browser, metaclass=abc.ABCMeta):
MACOS_BIN_NAME: str = ""
VERSION: str = "100.22.33.44"
@classmethod
@abc.abstractmethod
def mock_app_path(cls, platform: plt.Platform) -> pathlib.Path:
pass
@classmethod
def setup_fs(cls, fs, platform: plt.Platform = plt.PLATFORM) -> None:
app_path = cls.mock_app_path(platform)
macos_bin_name = app_path.stem
if cls.MACOS_BIN_NAME:
macos_bin_name = cls.MACOS_BIN_NAME
cls.setup_bin(fs, app_path, macos_bin_name, platform)
@classmethod
def setup_bin(cls,
fs,
bin_path: pathlib.Path,
macos_bin_name: str,
platform: plt.Platform = plt.PLATFORM) -> None:
if platform.is_macos:
assert bin_path.suffix == ".app"
bin_path = bin_path / "Contents" / "MacOS" / macos_bin_name
elif platform.is_win:
assert bin_path.suffix == ".exe"
if not bin_path.exists():
fs.create_file(bin_path)
@classmethod
def default_flags(cls,
initial_data: Flags.InitialDataType = None) -> ChromeFlags:
return ChromeFlags(initial_data)
def __init__(self,
label: str,
path: Optional[pathlib.Path] = None,
settings: Optional[Settings] = None):
settings = settings or Settings()
platform = settings.platform
path = path or self.mock_app_path(platform)
self.app_path = path
if maybe_driver := settings.driver_path:
assert isinstance(maybe_driver, pathlib.Path) and maybe_driver.exists()
super().__init__(label, path, settings=settings)
self.url_list: List[str] = []
self.expected_js: List[JsInvocation] = []
self.invoked_js: List[JsInvocation] = []
self.did_run: bool = False
self.clear_cache_dir: bool = False
def expect_js(
self,
expected_js: Optional[JsInvocation] = None,
result: Any = None,
) -> None:
if not expected_js:
self.expected_js.append(JsInvocation(result=result))
return
self.expected_js.append(expected_js)
return
def was_js_invoked(self, expected_script: str) -> bool:
return expected_script in [
invoked_js.script for invoked_js in self.invoked_js
]
def clear_cache(self, runner: Runner) -> None:
pass
def start(self, session: BrowserSessionRunGroup) -> None:
assert not self._is_running
self._is_running = True
self.did_run = True
def force_quit(self) -> None:
if not self._is_running:
return
self._is_running = False
def _extract_version(self) -> str:
return self.VERSION
def user_agent(self, runner: Runner) -> str:
return f"Mock Browser {self.type_name}, {self.VERSION}"
def show_url(self, runner: Runner, url, target: Optional[str] = None) -> None:
self.url_list.append(url)
def js(self,
runner: Runner,
script,
timeout: Optional[dt.timedelta] = None,
arguments=()):
self.invoked_js.append(
JsInvocation(
result=None, script=script, arguments=arguments, timeout=timeout))
if self.expected_js is None:
return None
assert self.expected_js, ("Not enough expected_js available. "
"Please add another expected_js entry for "
f"arguments={arguments} \n"
f"Script: {script}")
expectation = self.expected_js.pop(0)
if expectation.timeout:
assert expectation.timeout == timeout, (
f"JS timeout does not match. "
f"Expected: {expectation.timeout} Got: {timeout}")
if expectation.script:
assert expectation.script == script, (
f"JS script does not match expectation. "
f"Expected: {expectation.script} Got: {script}")
if expectation.arguments:
assert len(expectation.arguments) == len(arguments), (
f"Number of JS arguments does not match. "
f"Expected: {len(expectation.arguments)} Got: {len(arguments)}")
for expected_argument, argument in zip(expectation.arguments, arguments):
assert expected_argument == argument, (
f"Arguments do not match. "
f"Expected: {expected_argument} Got: {argument}")
# Return copies to avoid leaking data between repetitions.
return copy.deepcopy(expectation.result)
def app_root(platform: plt.Platform) -> pathlib.Path:
if platform.is_macos:
return pathlib.Path("/Applications")
if platform.is_win:
return pathlib.Path("C:/Program Files")
return pathlib.Path("/usr/bin")
class MockChromiumBrowser(MockBrowser, metaclass=abc.ABCMeta):
def _setup_flags(self, settings: Settings) -> ChromeFlags:
flags = ChromeFlags(settings.flags)
flags.js_flags.update(settings.js_flags)
return flags
@property
def chrome_flags(self) -> ChromeFlags:
chrome_flags = cast(ChromeFlags, self.flags)
assert isinstance(chrome_flags, ChromeFlags)
return chrome_flags
@property
def js_flags(self) -> JSFlags:
return self.chrome_flags.js_flags
@property
def features(self) -> ChromeFeatures:
return self.chrome_flags.features
@property
def attributes(self) -> BrowserAttributes:
return BrowserAttributes.CHROMIUM | BrowserAttributes.CHROMIUM_BASED
# Inject MockBrowser into the browser hierarchy for easier testing.
Chromium.register(MockChromiumBrowser)
class MockChromeBrowser(MockChromiumBrowser, metaclass=abc.ABCMeta):
@property
def type_name(self) -> str:
return "chrome"
@property
def attributes(self) -> BrowserAttributes:
return BrowserAttributes.CHROME | BrowserAttributes.CHROMIUM_BASED
Chrome.register(MockChromeBrowser)
if not TYPE_CHECKING:
assert issubclass(MockChromeBrowser, Chrome)
class MockChromeStable(MockChromeBrowser):
@classmethod
def mock_app_path(cls, platform: plt.Platform = plt.PLATFORM) -> pathlib.Path:
if platform.is_macos:
return app_root(platform) / "Google Chrome.app"
if platform.is_win:
return app_root(platform) / "Google/Chrome/Application/chrome.exe"
return app_root(platform) / "google-chrome"
if not TYPE_CHECKING:
assert issubclass(MockChromeStable, Chromium)
assert issubclass(MockChromeStable, Chrome)
class MockChromeAndroidStable(MockChromeStable):
@property
def platform(self) -> AndroidAdbPlatform:
assert isinstance(
self._platform,
AndroidAdbPlatform), (f"Invalid platform: {self._platform}")
return cast(AndroidAdbPlatform, self._platform)
def _resolve_binary(self, path: pathlib.Path) -> pathlib.Path:
return path
@property
def attributes(self) -> BrowserAttributes:
return (BrowserAttributes.CHROME | BrowserAttributes.CHROMIUM_BASED
| BrowserAttributes.MOBILE)
class MockChromeBeta(MockChromeBrowser):
VERSION = "101.22.33.44"
@classmethod
def mock_app_path(cls, platform: plt.Platform = plt.PLATFORM) -> pathlib.Path:
if platform.is_macos:
return app_root(platform) / "Google Chrome Beta.app"
if platform.is_win:
return app_root(platform) / "Google/Chrome Beta/Application/chrome.exe"
return app_root(platform) / "google-chrome-beta"
class MockChromeDev(MockChromeBrowser):
VERSION = "102.22.33.44"
@classmethod
def mock_app_path(cls, platform: plt.Platform = plt.PLATFORM) -> pathlib.Path:
if platform.is_macos:
return app_root(platform) / "Google Chrome Dev.app"
if platform.is_win:
return app_root(platform) / "Google/Chrome Dev/Application/chrome.exe"
return app_root(platform) / "google-chrome-unstable"
class MockChromeCanary(MockChromeBrowser):
VERSION = "103.22.33.44"
@classmethod
def mock_app_path(cls, platform: plt.Platform = plt.PLATFORM) -> pathlib.Path:
if platform.is_macos:
return app_root(platform) / "Google Chrome Canary.app"
if platform.is_win:
return app_root(platform) / "Google/Chrome SxS/Application/chrome.exe"
return app_root(platform) / "google-chrome-canary"
class MockEdgeBrowser(MockChromiumBrowser, metaclass=abc.ABCMeta):
@property
def type_name(self) -> str:
return "edge"
@property
def attributes(self) -> BrowserAttributes:
return BrowserAttributes.EDGE | BrowserAttributes.CHROMIUM_BASED
Edge.register(MockEdgeBrowser)
if not TYPE_CHECKING:
assert issubclass(MockEdgeBrowser, Chromium)
assert issubclass(MockEdgeBrowser, Edge)
class MockEdgeStable(MockEdgeBrowser):
@classmethod
def mock_app_path(cls, platform: plt.Platform = plt.PLATFORM) -> pathlib.Path:
if platform.is_macos:
return app_root(platform) / "Microsoft Edge.app"
if platform.is_win:
return app_root(platform) / "Microsoft/Edge/Application/msedge.exe"
return app_root(platform) / "microsoft-edge"
class MockEdgeBeta(MockEdgeBrowser):
VERSION = "101.22.33.44"
@classmethod
def mock_app_path(cls, platform: plt.Platform = plt.PLATFORM) -> pathlib.Path:
if platform.is_macos:
return app_root(platform) / "Microsoft Edge Beta.app"
if platform.is_win:
return app_root(platform) / "Microsoft/Edge Beta/Application/msedge.exe"
return app_root(platform) / "microsoft-edge-beta"
class MockEdgeDev(MockEdgeBrowser):
VERSION = "102.22.33.44"
@classmethod
def mock_app_path(cls, platform: plt.Platform = plt.PLATFORM) -> pathlib.Path:
if platform.is_macos:
return app_root(platform) / "Microsoft Edge Dev.app"
if platform.is_win:
return app_root(platform) / "Microsoft/Edge Dev/Application/msedge.exe"
return app_root(platform) / "microsoft-edge-dev"
class MockEdgeCanary(MockEdgeBrowser):
VERSION = "103.22.33.44"
@classmethod
def mock_app_path(cls, platform: plt.Platform = plt.PLATFORM) -> pathlib.Path:
if platform.is_macos:
return app_root(platform) / "Microsoft Edge Canary.app"
if platform.is_win:
return app_root(platform) / "Microsoft/Edge SxS/Application/msedge.exe"
return app_root(platform) / "unsupported/msedge-canary"
class MockSafariBrowser(MockBrowser, metaclass=abc.ABCMeta):
@property
def type_name(self) -> str:
return "safari"
@property
def attributes(self) -> BrowserAttributes:
return BrowserAttributes.SAFARI
Safari.register(MockSafariBrowser)
if not TYPE_CHECKING:
assert issubclass(MockSafariBrowser, Safari)
class MockSafari(MockSafariBrowser):
@classmethod
def mock_app_path(cls, platform: plt.Platform = plt.PLATFORM) -> pathlib.Path:
if platform.is_macos:
return app_root(platform) / "Safari.app"
if platform.is_win:
return app_root(platform) / "Unsupported/Safari.exe"
return pathlib.Path("/unsupported-platform/Safari")
class MockSafariTechnologyPreview(MockSafariBrowser):
@classmethod
def mock_app_path(cls, platform: plt.Platform = plt.PLATFORM) -> pathlib.Path:
if platform.is_macos:
return app_root(platform) / "Safari Technology Preview.app"
if platform.is_win:
return app_root(platform) / "Unsupported/Safari Technology Preview.exe"
return pathlib.Path("/unsupported-platform/Safari Technology Preview")
class MockFirefoxBrowser(MockBrowser, metaclass=abc.ABCMeta):
@property
def type_name(self) -> str:
return "firefox"
@property
def attributes(self) -> BrowserAttributes:
return BrowserAttributes.FIREFOX
Firefox.register(MockFirefoxBrowser)
if not TYPE_CHECKING:
assert issubclass(MockFirefoxBrowser, Firefox)
class MockFirefox(MockFirefoxBrowser):
@classmethod
def mock_app_path(cls, platform: plt.Platform = plt.PLATFORM) -> pathlib.Path:
if platform.is_macos:
return app_root(platform) / "Firefox.app"
if platform.is_win:
return app_root(platform) / "Mozilla Firefox/firefox.exe"
return app_root(platform) / "firefox"
class MockFirefoxDeveloperEdition(MockFirefoxBrowser):
@classmethod
def mock_app_path(cls, platform: plt.Platform = plt.PLATFORM) -> pathlib.Path:
if platform.is_macos:
return app_root(platform) / "Firefox Developer Edition.app"
if platform.is_win:
return app_root(platform) / "Firefox Developer Edition/firefox.exe"
return app_root(platform) / "firefox-developer-edition"
class MockFirefoxNightly(MockFirefoxBrowser):
@classmethod
def mock_app_path(cls, platform: plt.Platform = plt.PLATFORM) -> pathlib.Path:
if platform.is_macos:
return app_root(platform) / "Firefox Nightly.app"
if platform.is_win:
return app_root(platform) / "Firefox Nightly/firefox.exe"
return app_root(platform) / "firefox-trunk"
ALL: Tuple[Type[MockBrowser], ...] = (
MockChromeCanary,
MockChromeDev,
MockChromeBeta,
MockChromeStable,
MockEdgeCanary,
MockEdgeDev,
MockEdgeBeta,
MockEdgeStable,
MockSafari,
MockSafariTechnologyPreview,
MockFirefox,
MockFirefoxDeveloperEdition,
MockFirefoxNightly,
)