chrome_driver_wrapper.py (4200B)
1 # Copyright 2024 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 """ Starts a web engine shell on an existing fuchsia device, and returns a 5 ChromeDriver instance to control it.""" 6 7 import logging 8 import os 9 import subprocess 10 11 from contextlib import AbstractContextManager 12 from typing import List 13 14 # From vpython wheel. 15 # pylint: disable=import-error 16 from selenium import webdriver 17 from selenium.webdriver import ChromeOptions 18 from selenium.webdriver.chrome.service import Service 19 from selenium.webdriver.common.by import By 20 # pylint: enable=import-error 21 22 from common import get_ffx_isolate_dir, get_free_local_port 23 from isolate_daemon import IsolateDaemon 24 from run_webpage_test import capture_devtools_addr 25 26 LOG_DIR = os.environ.get('ISOLATED_OUTDIR', '/tmp') 27 28 29 class ChromeDriverWrapper(AbstractContextManager): 30 """Manages the web engine shell on the device and the chromedriver 31 communicating with it. This class expects the chromedriver exists at 32 clang_x64/stripped/chromedriver in output dir.""" 33 34 def __init__(self, extra_args: List[str] = None): 35 # The reference of the webdriver.Chrome instance. 36 self._driver = None 37 38 # Creates the isolate dir for daemon to ensure it can be shared across 39 # the processes. Note, it has no effect if isolate_dir has already been 40 # set. 41 self._isolate_dir = IsolateDaemon.IsolateDir() 42 43 # The process of the run_test.py webpage. 44 self._proc: subprocess.Popen = None 45 46 # Extra arguments sent to run_test.py webpage process. 47 self._extra_args = extra_args or [] 48 49 def __enter__(self): 50 """Starts the run_test.py and the chromedriver connecting to it, must be 51 executed before other commands.""" 52 self._isolate_dir.__enter__() 53 logging.warning('ffx daemon is running in %s', get_ffx_isolate_dir()) 54 55 self._proc = subprocess.Popen([ 56 os.path.join(os.path.dirname(os.path.abspath(__file__)), 57 'run_test.py'), 'webpage', '--out-dir=.', 58 '--browser=web-engine-shell', '--device', f'--logs-dir={LOG_DIR}' 59 ] + self._extra_args, 60 env={ 61 **os.environ, 'CHROME_HEADLESS': '1' 62 }) 63 address, port = capture_devtools_addr(self._proc, LOG_DIR) 64 logging.warning('DevTools is now running on %s:%s', address, port) 65 66 options = ChromeOptions() 67 options.debugger_address = f'{address}:{str(port)}' 68 # The port webdriver running on is not very interesting, the _driver 69 # instance will be used directly. So a random free local port is used. 70 self._driver = webdriver.Chrome(options=options, 71 service=Service( 72 os.path.join( 73 'clang_x64', 'stripped', 74 'chromedriver'), 75 get_free_local_port())) 76 self._driver.__enter__() 77 return self 78 79 def __exit__(self, exc_type, exc_val, exc_tb) -> bool: 80 """Stops the run_test.py and the chromedriver, cannot perform other 81 commands afterward.""" 82 try: 83 return self._driver.__exit__(exc_type, exc_val, exc_tb) 84 finally: 85 self._proc.terminate() 86 self._proc.wait() 87 self._isolate_dir.__exit__(exc_type, exc_val, exc_tb) 88 89 def __getattr__(self, name): 90 """Forwards function calls to the underlying |_driver| instance.""" 91 return getattr(self._driver, name) 92 93 # Explicitly override find_element_by_id to avoid importing selenium 94 # packages in the caller files. 95 # The find_element_by_id in webdriver.Chrome is deprecated. 96 # DeprecationWarning: find_element_by_* commands are deprecated. Please 97 # use find_element() instead 98 def find_element_by_id(self, id_str): 99 """Returns the element in the page with id |id_str|.""" 100 return self._driver.find_element(By.ID, id_str)