• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright 2022 The Chromium Authors
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5from __future__ import annotations
6
7import abc
8import contextlib
9import copy
10import dataclasses
11import pathlib
12from typing import (TYPE_CHECKING, Any, Iterator, List, Optional, Tuple, Type,
13                    Union, cast)
14
15from crossbench import plt
16from crossbench.browsers.all import Chrome, Chromium, Edge, Firefox, Safari
17from crossbench.browsers.attributes import BrowserAttributes
18from crossbench.browsers.browser import Browser
19from crossbench.browsers.settings import Settings
20from crossbench.flags.chrome import ChromeFeatures, ChromeFlags
21from crossbench.flags.js_flags import JSFlags
22from crossbench.network.base import Network
23from crossbench.plt.android_adb import AndroidAdbPlatform
24
25if TYPE_CHECKING:
26  import datetime as dt
27  import re
28
29  from crossbench.cli.config.secrets import Secret
30  from crossbench.flags.base import FlagsData
31  from crossbench.runner.groups.session import BrowserSessionRunGroup
32
33
34@dataclasses.dataclass(frozen=True)
35class JsInvocation:
36  result: Any
37  script: Optional[Union[str, re.Pattern]] = None
38  arguments: Optional[List[Any]] = None
39  timeout: Optional[dt.timedelta] = None
40
41
42class MockNetwork(Network):
43
44  @contextlib.contextmanager
45  def open(self, session: BrowserSessionRunGroup) -> Iterator[Network]:
46    with super().open(session):
47      assert session.browser.network is self
48      yield self
49      assert self.is_running
50
51
52class MockBrowser(Browser, metaclass=abc.ABCMeta):
53  MACOS_BIN_NAME: str = ""
54  VERSION: str = "100.22.33.44"
55
56  @classmethod
57  @abc.abstractmethod
58  def mock_app_path(cls, platform: plt.Platform) -> pathlib.Path:
59    pass
60
61  @classmethod
62  def setup_fs(cls, fs, platform: plt.Platform = plt.PLATFORM) -> None:
63    app_path = cls.mock_app_path(platform)
64    macos_bin_name = app_path.stem
65    if cls.MACOS_BIN_NAME:
66      macos_bin_name = cls.MACOS_BIN_NAME
67    cls.setup_bin(fs, app_path, macos_bin_name, platform)
68
69  @classmethod
70  def setup_bin(cls,
71                fs,
72                bin_path: pathlib.Path,
73                macos_bin_name: str,
74                platform: plt.Platform = plt.PLATFORM) -> None:
75    if platform.is_macos:
76      assert bin_path.suffix == ".app"
77      bin_path = bin_path / "Contents" / "MacOS" / macos_bin_name
78    elif platform.is_win:
79      assert bin_path.suffix == ".exe"
80    if not bin_path.exists():
81      fs.create_file(bin_path)
82
83  @classmethod
84  def default_flags(cls, initial_data: FlagsData = None) -> ChromeFlags:
85    return ChromeFlags(initial_data)
86
87  def __init__(self,
88               label: str,
89               path: Optional[pathlib.Path] = None,
90               settings: Optional[Settings] = None):
91    settings = settings or Settings()
92    platform = settings.platform
93    path = path or self.mock_app_path(platform)
94    self.app_path = path
95    if maybe_driver := settings.driver_path:
96      assert isinstance(maybe_driver, pathlib.Path) and maybe_driver.exists()
97    super().__init__(label, path, settings=settings)
98    self.url_list: List[str] = []
99    self.expected_js: List[JsInvocation] = []
100    self.expected_is_logged_in: List[Secret] = []
101    self.invoked_js: List[JsInvocation] = []
102    self.did_run: bool = False
103    self.clear_cache_dir: bool = False
104    self.tab_handler_generator = self._tab_handler_generator()
105    self.tab_list: List[int] = [next(self.tab_handler_generator)]
106
107  def expect_js(
108      self,
109      expected_js: Optional[JsInvocation] = None,
110      result: Any = None,
111  ) -> None:
112    if not expected_js:
113      self.expected_js.append(JsInvocation(result=result))
114      return
115    self.expected_js.append(expected_js)
116    return
117
118  def was_js_invoked(self, script: str) -> bool:
119    return any(script is invoked_js.script for invoked_js in self.invoked_js)
120
121  def expect_is_logged_in(self, secret: Secret) -> None:
122    self.expected_is_logged_in.append(secret)
123
124  def clear_cache(self) -> None:
125    pass
126
127  def start(self, session: BrowserSessionRunGroup) -> None:
128    assert not self._is_running
129    self._is_running = True
130    self.did_run = True
131
132  def force_quit(self) -> None:
133    if not self._is_running:
134      return
135    self._is_running = False
136
137  def _extract_version(self) -> str:
138    return self.VERSION
139
140  def user_agent(self) -> str:
141    return f"Mock Browser {self.type_name}, {self.VERSION}"
142
143  def show_url(self, url, target: Optional[str] = None) -> None:
144    self.url_list.append(url)
145
146  def current_window_id(self) -> str:
147    return str(self.tab_list[-1])
148
149  def _tab_handler_generator(self):
150    tab_handler = 0
151    while True:
152      yield tab_handler
153      tab_handler += 1
154
155  def switch_to_new_tab(self) -> None:
156    self.tab_list.append(next(self.tab_handler_generator))
157
158  def js(self, script, timeout: Optional[dt.timedelta] = None, arguments=()):
159    self.invoked_js.append(
160        JsInvocation(
161            result=None, script=script, arguments=arguments, timeout=timeout))
162
163    if self.expected_js is None:
164      return None
165
166    assert self.expected_js, ("Not enough expected_js available. "
167                              "Please add another expected_js entry for "
168                              f"arguments={arguments} \n"
169                              f"Script: {script}")
170    expectation = self.expected_js.pop(0)
171
172    if expectation.timeout:
173      assert expectation.timeout == timeout, (
174          f"JS timeout does not match. "
175          f"Expected: {expectation.timeout} Got: {timeout}")
176
177    if expected_script := expectation.script:
178      if isinstance(expected_script, str):
179        result = expected_script == script
180      else:
181        result = expected_script.fullmatch(script)
182      assert result, (f"JS script does not match expectation. "
183                      f"Expected: {expected_script} Got: {script}")
184
185    if expectation.arguments:
186      assert len(expectation.arguments) == len(arguments), (
187          f"Number of JS arguments does not match. "
188          f"Expected: {len(expectation.arguments)} Got: {len(arguments)}")
189
190      for expected_argument, argument in zip(expectation.arguments, arguments):
191        assert expected_argument == argument, (
192            f"Arguments do not match. "
193            f"Expected: {expected_argument} Got: {argument}")
194
195    # Return copies to avoid leaking data between repetitions.
196    return copy.deepcopy(expectation.result)
197
198  def is_logged_in(self, secret: Secret, strict: bool = False) -> bool:
199    for login in self.expected_is_logged_in:
200      if login.type == secret.type:
201        if login.username == secret.username:
202          return True
203        if strict:
204          raise RuntimeError("Secret mismatch")
205    return False
206
207def app_root(platform: plt.Platform) -> pathlib.Path:
208  if platform.is_macos:
209    return pathlib.Path("/Applications")
210  if platform.is_win:
211    return pathlib.Path("C:/Program Files")
212  return pathlib.Path("/usr/bin")
213
214
215class MockChromiumBrowser(MockBrowser, metaclass=abc.ABCMeta):
216
217  def _setup_flags(self, settings: Settings) -> ChromeFlags:
218    flags = ChromeFlags(settings.flags)
219    flags.js_flags.update(settings.js_flags)
220    return flags
221
222  @property
223  def chrome_flags(self) -> ChromeFlags:
224    chrome_flags = cast(ChromeFlags, self.flags)
225    assert isinstance(chrome_flags, ChromeFlags)
226    return chrome_flags
227
228  @property
229  def js_flags(self) -> JSFlags:
230    return self.chrome_flags.js_flags
231
232  @property
233  def features(self) -> ChromeFeatures:
234    return self.chrome_flags.features
235
236  @property
237  def attributes(self) -> BrowserAttributes:
238    return BrowserAttributes.CHROMIUM | BrowserAttributes.CHROMIUM_BASED
239
240
241# Inject MockBrowser into the browser hierarchy for easier testing.
242Chromium.register(MockChromiumBrowser)
243
244
245class MockChromeBrowser(MockChromiumBrowser, metaclass=abc.ABCMeta):
246
247  @property
248  def type_name(self) -> str:
249    return "chrome"
250
251  @property
252  def attributes(self) -> BrowserAttributes:
253    return BrowserAttributes.CHROME | BrowserAttributes.CHROMIUM_BASED
254
255
256Chrome.register(MockChromeBrowser)
257if not TYPE_CHECKING:
258  assert issubclass(MockChromeBrowser, Chrome)
259
260
261class MockChromeStable(MockChromeBrowser):
262
263  @classmethod
264  def mock_app_path(cls, platform: plt.Platform = plt.PLATFORM) -> pathlib.Path:
265    if platform.is_macos:
266      return app_root(platform) / "Google Chrome.app"
267    if platform.is_win:
268      return app_root(platform) / "Google/Chrome/Application/chrome.exe"
269    return app_root(platform) / "google-chrome"
270
271
272if not TYPE_CHECKING:
273  assert issubclass(MockChromeStable, Chromium)
274  assert issubclass(MockChromeStable, Chrome)
275
276
277class MockChromeAndroidStable(MockChromeStable):
278
279  @property
280  def platform(self) -> AndroidAdbPlatform:
281    assert isinstance(
282        self._platform,
283        AndroidAdbPlatform), (f"Invalid platform: {self._platform}")
284    return cast(AndroidAdbPlatform, self._platform)
285
286  def _resolve_binary(self, path: pathlib.Path) -> pathlib.Path:
287    return path
288
289  @property
290  def attributes(self) -> BrowserAttributes:
291    return (BrowserAttributes.CHROME | BrowserAttributes.CHROMIUM_BASED
292            | BrowserAttributes.MOBILE)
293
294
295class MockChromeBeta(MockChromeBrowser):
296  VERSION = "101.22.33.44"
297
298  @classmethod
299  def mock_app_path(cls, platform: plt.Platform = plt.PLATFORM) -> pathlib.Path:
300    if platform.is_macos:
301      return app_root(platform) / "Google Chrome Beta.app"
302    if platform.is_win:
303      return app_root(platform) / "Google/Chrome Beta/Application/chrome.exe"
304    return app_root(platform) / "google-chrome-beta"
305
306
307class MockChromeDev(MockChromeBrowser):
308  VERSION = "102.22.33.44"
309
310  @classmethod
311  def mock_app_path(cls, platform: plt.Platform = plt.PLATFORM) -> pathlib.Path:
312    if platform.is_macos:
313      return app_root(platform) / "Google Chrome Dev.app"
314    if platform.is_win:
315      return app_root(platform) / "Google/Chrome Dev/Application/chrome.exe"
316    return app_root(platform) / "google-chrome-unstable"
317
318
319class MockChromeCanary(MockChromeBrowser):
320  VERSION = "103.22.33.44"
321
322  @classmethod
323  def mock_app_path(cls, platform: plt.Platform = plt.PLATFORM) -> pathlib.Path:
324    if platform.is_macos:
325      return app_root(platform) / "Google Chrome Canary.app"
326    if platform.is_win:
327      return app_root(platform) / "Google/Chrome SxS/Application/chrome.exe"
328    return app_root(platform) / "google-chrome-canary"
329
330
331class MockEdgeBrowser(MockChromiumBrowser, metaclass=abc.ABCMeta):
332
333  @property
334  def type_name(self) -> str:
335    return "edge"
336
337  @property
338  def attributes(self) -> BrowserAttributes:
339    return BrowserAttributes.EDGE | BrowserAttributes.CHROMIUM_BASED
340
341
342Edge.register(MockEdgeBrowser)
343if not TYPE_CHECKING:
344  assert issubclass(MockEdgeBrowser, Chromium)
345  assert issubclass(MockEdgeBrowser, Edge)
346
347
348class MockEdgeStable(MockEdgeBrowser):
349
350  @classmethod
351  def mock_app_path(cls, platform: plt.Platform = plt.PLATFORM) -> pathlib.Path:
352    if platform.is_macos:
353      return app_root(platform) / "Microsoft Edge.app"
354    if platform.is_win:
355      return app_root(platform) / "Microsoft/Edge/Application/msedge.exe"
356    return app_root(platform) / "microsoft-edge"
357
358
359class MockEdgeBeta(MockEdgeBrowser):
360  VERSION = "101.22.33.44"
361
362  @classmethod
363  def mock_app_path(cls, platform: plt.Platform = plt.PLATFORM) -> pathlib.Path:
364    if platform.is_macos:
365      return app_root(platform) / "Microsoft Edge Beta.app"
366    if platform.is_win:
367      return app_root(platform) / "Microsoft/Edge Beta/Application/msedge.exe"
368    return app_root(platform) / "microsoft-edge-beta"
369
370
371class MockEdgeDev(MockEdgeBrowser):
372  VERSION = "102.22.33.44"
373
374  @classmethod
375  def mock_app_path(cls, platform: plt.Platform = plt.PLATFORM) -> pathlib.Path:
376    if platform.is_macos:
377      return app_root(platform) / "Microsoft Edge Dev.app"
378    if platform.is_win:
379      return app_root(platform) / "Microsoft/Edge Dev/Application/msedge.exe"
380    return app_root(platform) / "microsoft-edge-dev"
381
382
383class MockEdgeCanary(MockEdgeBrowser):
384  VERSION = "103.22.33.44"
385
386  @classmethod
387  def mock_app_path(cls, platform: plt.Platform = plt.PLATFORM) -> pathlib.Path:
388    if platform.is_macos:
389      return app_root(platform) / "Microsoft Edge Canary.app"
390    if platform.is_win:
391      return app_root(platform) / "Microsoft/Edge SxS/Application/msedge.exe"
392    return app_root(platform) / "unsupported/msedge-canary"
393
394
395class MockSafariBrowser(MockBrowser, metaclass=abc.ABCMeta):
396
397  @property
398  def type_name(self) -> str:
399    return "safari"
400
401  @property
402  def attributes(self) -> BrowserAttributes:
403    return BrowserAttributes.SAFARI
404
405
406Safari.register(MockSafariBrowser)
407if not TYPE_CHECKING:
408  assert issubclass(MockSafariBrowser, Safari)
409
410
411class MockSafari(MockSafariBrowser):
412
413  @classmethod
414  def mock_app_path(cls, platform: plt.Platform = plt.PLATFORM) -> pathlib.Path:
415    if platform.is_macos:
416      return app_root(platform) / "Safari.app"
417    if platform.is_win:
418      return app_root(platform) / "Unsupported/Safari.exe"
419    return pathlib.Path("/unsupported-platform/Safari")
420
421
422class MockSafariTechnologyPreview(MockSafariBrowser):
423
424  @classmethod
425  def mock_app_path(cls, platform: plt.Platform = plt.PLATFORM) -> pathlib.Path:
426    if platform.is_macos:
427      return app_root(platform) / "Safari Technology Preview.app"
428    if platform.is_win:
429      return app_root(platform) / "Unsupported/Safari Technology Preview.exe"
430    return pathlib.Path("/unsupported-platform/Safari Technology Preview")
431
432
433class MockFirefoxBrowser(MockBrowser, metaclass=abc.ABCMeta):
434
435  @property
436  def type_name(self) -> str:
437    return "firefox"
438
439  @property
440  def attributes(self) -> BrowserAttributes:
441    return BrowserAttributes.FIREFOX
442
443
444Firefox.register(MockFirefoxBrowser)
445if not TYPE_CHECKING:
446  assert issubclass(MockFirefoxBrowser, Firefox)
447
448
449class MockFirefox(MockFirefoxBrowser):
450
451  @classmethod
452  def mock_app_path(cls, platform: plt.Platform = plt.PLATFORM) -> pathlib.Path:
453    if platform.is_macos:
454      return app_root(platform) / "Firefox.app"
455    if platform.is_win:
456      return app_root(platform) / "Mozilla Firefox/firefox.exe"
457    return app_root(platform) / "firefox"
458
459
460class MockFirefoxDeveloperEdition(MockFirefoxBrowser):
461
462  @classmethod
463  def mock_app_path(cls, platform: plt.Platform = plt.PLATFORM) -> pathlib.Path:
464    if platform.is_macos:
465      return app_root(platform) / "Firefox Developer Edition.app"
466    if platform.is_win:
467      return app_root(platform) / "Firefox Developer Edition/firefox.exe"
468    return app_root(platform) / "firefox-developer-edition"
469
470
471class MockFirefoxNightly(MockFirefoxBrowser):
472
473  @classmethod
474  def mock_app_path(cls, platform: plt.Platform = plt.PLATFORM) -> pathlib.Path:
475    if platform.is_macos:
476      return app_root(platform) / "Firefox Nightly.app"
477    if platform.is_win:
478      return app_root(platform) / "Firefox Nightly/firefox.exe"
479    return app_root(platform) / "firefox-trunk"
480
481
482ALL: Tuple[Type[MockBrowser], ...] = (
483    MockChromeCanary,
484    MockChromeDev,
485    MockChromeBeta,
486    MockChromeStable,
487    MockEdgeCanary,
488    MockEdgeDev,
489    MockEdgeBeta,
490    MockEdgeStable,
491    MockSafari,
492    MockSafariTechnologyPreview,
493    MockFirefox,
494    MockFirefoxDeveloperEdition,
495    MockFirefoxNightly,
496)
497