The geckodriver process is reused across tests within the same module when command line arguments, environment variables, and profile match. A custom profile is used to avoid conflicts with the harness Firefox instance. Differential Revision: https://phabricator.services.mozilla.com/D308763
433 lines
14 KiB
Python
433 lines
14 KiB
Python
import os
|
|
from copy import deepcopy
|
|
|
|
import pytest
|
|
import pytest_asyncio
|
|
import tests.support.fixtures as global_fixtures
|
|
from tests.support import defaults
|
|
|
|
from .chrome_handler import using_chrome_handler
|
|
from .context import using_context
|
|
from .helpers import (
|
|
Browser,
|
|
Geckodriver,
|
|
create_custom_profile,
|
|
get_profile_folder,
|
|
read_user_preferences,
|
|
)
|
|
|
|
_geckodriver_state = {"current": None}
|
|
|
|
|
|
def pytest_configure(config):
|
|
config.addinivalue_line(
|
|
"markers",
|
|
"geckodriver(allow_system_access=False, force_new=False): "
|
|
"Configure the geckodriver instance for the test",
|
|
)
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def browser(configuration, firefox_options):
|
|
"""Start a Firefox instance without using geckodriver.
|
|
|
|
geckodriver will automatically use the --remote-allow-hosts and
|
|
--remote.allow.origins command line arguments.
|
|
|
|
Starting Firefox without geckodriver allows to set those command line arguments
|
|
as needed. The fixture method returns the browser instance that should be used
|
|
to connect to a RemoteAgent supported protocol (WebDriver BiDi).
|
|
"""
|
|
current_browser = None
|
|
|
|
def _browser(
|
|
clone_profile=True,
|
|
extra_args=None,
|
|
extra_prefs=None,
|
|
use_bidi=False,
|
|
use_marionette=False,
|
|
):
|
|
nonlocal current_browser
|
|
|
|
webdriver_args = configuration["webdriver"]["args"]
|
|
log_level = None
|
|
truncate_enabled = True
|
|
|
|
if "-vvv" in webdriver_args:
|
|
log_level = "Trace"
|
|
truncate_enabled = False
|
|
elif "-vv" in webdriver_args:
|
|
log_level = "Trace"
|
|
elif "-v" in webdriver_args:
|
|
log_level = "Debug"
|
|
|
|
# If the requested preferences and arguments match the ones for the
|
|
# already started firefox, we can reuse the current firefox instance,
|
|
# return the instance immediately.
|
|
if current_browser:
|
|
if (
|
|
current_browser.extra_args == extra_args
|
|
and current_browser.extra_prefs == extra_prefs
|
|
and current_browser.is_running
|
|
and current_browser.use_bidi == use_bidi
|
|
and current_browser.use_marionette == use_marionette
|
|
and current_browser.log_level == log_level
|
|
and current_browser.truncate_enabled == truncate_enabled
|
|
):
|
|
return current_browser
|
|
|
|
# Otherwise, if firefox is already started, terminate it because we need
|
|
# to create a new instance for the provided preferences.
|
|
current_browser.quit()
|
|
|
|
binary = configuration["browser"]["binary"]
|
|
env = configuration["browser"]["env"]
|
|
|
|
profile_path = get_profile_folder(firefox_options)
|
|
default_prefs = read_user_preferences(profile_path)
|
|
profile = create_custom_profile(
|
|
profile_path, default_prefs, clone=clone_profile
|
|
)
|
|
|
|
current_browser = Browser(
|
|
binary,
|
|
profile,
|
|
extra_args=extra_args,
|
|
extra_prefs=extra_prefs,
|
|
env=env,
|
|
log_level=log_level,
|
|
truncate_enabled=truncate_enabled,
|
|
use_bidi=use_bidi,
|
|
use_marionette=use_marionette,
|
|
)
|
|
current_browser.start()
|
|
return current_browser
|
|
|
|
yield _browser
|
|
|
|
# Stop firefox at the end of the test module.
|
|
if current_browser is not None:
|
|
current_browser.quit()
|
|
current_browser = None
|
|
|
|
|
|
@pytest.fixture
|
|
def default_chrome_handler(current_session):
|
|
manifest_path = os.path.join(
|
|
os.path.abspath(os.path.dirname(__file__)), "chrome-assets", "chrome.manifest"
|
|
)
|
|
entries = [["content", "marionette-chrome", "chrome/"]]
|
|
|
|
with using_chrome_handler(current_session, manifest_path, entries):
|
|
yield "chrome://marionette-chrome/content/"
|
|
|
|
|
|
@pytest.fixture
|
|
def default_preferences(profile_folder):
|
|
return read_user_preferences(profile_folder)
|
|
|
|
|
|
@pytest.fixture
|
|
def new_chrome_window(current_session):
|
|
opened_chrome_windows = []
|
|
|
|
def _new_chrome_window(url, focus=True):
|
|
# Bug 1944570: Replace with BiDi once scripts can be evaluated
|
|
# in the parent process.
|
|
with using_context(current_session, "chrome"):
|
|
new_window = current_session.execute_async_script(
|
|
"""
|
|
const { NavigableManager } = ChromeUtils.importESModule(
|
|
"chrome://remote/content/shared/NavigableManager.sys.mjs"
|
|
);
|
|
|
|
let [url, focus, resolve] = arguments;
|
|
|
|
function waitForEvent(target, type, args) {
|
|
return new Promise(resolve => {
|
|
let params = Object.assign({once: true}, args);
|
|
target.addEventListener(type, event => {
|
|
dump(`** Received DOM event ${event.type} for ${event.target}\n`);
|
|
resolve();
|
|
}, params);
|
|
});
|
|
}
|
|
|
|
function waitForFocus(win) {
|
|
return Promise.all([
|
|
waitForEvent(win, "activate"),
|
|
waitForEvent(win, "focus", {capture: true}),
|
|
]);
|
|
}
|
|
|
|
const isLoaded = window =>
|
|
window?.document.readyState === "complete" &&
|
|
!window?.document.isUncommittedInitialDocument;
|
|
|
|
(async function() {
|
|
// Open a window, wait for it to receive focus
|
|
let newWindow = window.openDialog(url, null, "chrome,centerscreen");
|
|
let focused = waitForFocus(newWindow);
|
|
|
|
newWindow.focus();
|
|
await focused;
|
|
|
|
// The new window shouldn't get focused. As such set the
|
|
// focus back to the opening window.
|
|
if (!focus && Services.focus.activeWindow != window) {
|
|
let focused = waitForFocus(window);
|
|
window.focus();
|
|
await focused;
|
|
}
|
|
|
|
// Wait for the new window to be finished loading
|
|
if (isLoaded(newWindow)) {
|
|
resolve(newWindow);
|
|
} else {
|
|
const onLoad = () => {
|
|
if (isLoaded(newWindow)) {
|
|
newWindow.removeEventListener("load", onLoad);
|
|
resolve(newWindow);
|
|
} else {
|
|
dump(`** Target window not loaded yet. Waiting for the next "load" event\n`);
|
|
}
|
|
};
|
|
newWindow.addEventListener("load", onLoad);
|
|
}
|
|
})();
|
|
""",
|
|
args=[url, focus],
|
|
)
|
|
|
|
# Append opened chrome window to automatic closing on teardown
|
|
opened_chrome_windows.append(new_window)
|
|
return new_window
|
|
|
|
yield _new_chrome_window
|
|
|
|
with using_context(current_session, "chrome"):
|
|
for win in opened_chrome_windows:
|
|
try:
|
|
current_session.window_handle = win.id
|
|
current_session.execute_script("arguments[0].close()", args=[win])
|
|
except Exception:
|
|
pass
|
|
|
|
current_session.window_handle = current_session.handles[0]
|
|
|
|
|
|
@pytest.fixture(name="create_custom_profile")
|
|
def fixture_create_custom_profile(default_preferences, profile_folder):
|
|
profile = None
|
|
|
|
def _create_custom_profile(clone=True):
|
|
profile = create_custom_profile(
|
|
profile_folder, default_preferences, clone=clone
|
|
)
|
|
|
|
return profile
|
|
|
|
yield _create_custom_profile
|
|
|
|
# if profile is not None:
|
|
if profile:
|
|
profile.cleanup()
|
|
|
|
|
|
@pytest.fixture(scope="session")
|
|
def firefox_options(configuration):
|
|
return configuration["capabilities"]["moz:firefoxOptions"]
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def geckodriver(configuration, firefox_options):
|
|
"""Start a geckodriver instance directly."""
|
|
|
|
custom_profile = None
|
|
|
|
def _geckodriver(
|
|
config=None,
|
|
hostname=None,
|
|
extra_args=None,
|
|
extra_env=None,
|
|
popen_kwargs=None,
|
|
profile=None,
|
|
force_new=False,
|
|
):
|
|
nonlocal custom_profile
|
|
|
|
if config is None:
|
|
config = deepcopy(configuration)
|
|
|
|
if extra_args is None:
|
|
extra_args = []
|
|
|
|
if extra_env is None:
|
|
extra_env = {}
|
|
|
|
# When --profile-root is used, geckodriver manages its own profile.
|
|
uses_profile_root = "--profile-root" in extra_args
|
|
|
|
# Use a cloned profile to avoid conflicts with the harness Firefox.
|
|
if not uses_profile_root and profile is None:
|
|
if custom_profile is None:
|
|
profile_path = get_profile_folder(firefox_options)
|
|
default_prefs = read_user_preferences(profile_path)
|
|
custom_profile = create_custom_profile(
|
|
profile_path, default_prefs, clone=True
|
|
)
|
|
profile = custom_profile
|
|
|
|
# Reuse the running geckodriver if the configuration matches.
|
|
current = _geckodriver_state["current"]
|
|
if current is not None:
|
|
if (
|
|
not force_new
|
|
and current.extra_args == extra_args
|
|
and current.extra_env == extra_env
|
|
and current.hostname == (hostname or config["host"])
|
|
and current.proc
|
|
and current.proc.poll() is None
|
|
and (
|
|
uses_profile_root
|
|
or (
|
|
custom_profile is not None
|
|
and profile.profile == custom_profile.profile
|
|
)
|
|
)
|
|
):
|
|
return current
|
|
|
|
current.kill()
|
|
|
|
if not uses_profile_root:
|
|
fx_options = config["capabilities"].setdefault("moz:firefoxOptions", {})
|
|
fx_args = fx_options.setdefault("args", [])
|
|
if "--profile" in fx_args:
|
|
index = fx_args.index("--profile")
|
|
fx_args[index + 1] = profile.profile
|
|
else:
|
|
fx_args.extend(["--profile", profile.profile])
|
|
|
|
# End any active harness session to free up ports and resources
|
|
# before starting the custom geckodriver instance.
|
|
harness_session = global_fixtures.get_current_session()
|
|
if harness_session is not None:
|
|
harness_session.end()
|
|
global_fixtures.set_current_session(None)
|
|
|
|
current = Geckodriver(config, hostname, extra_args, extra_env, popen_kwargs)
|
|
current.start()
|
|
_geckodriver_state["current"] = current
|
|
|
|
return current
|
|
|
|
yield _geckodriver
|
|
|
|
if _geckodriver_state["current"] is not None:
|
|
_geckodriver_state["current"].kill()
|
|
_geckodriver_state["current"] = None
|
|
|
|
if custom_profile is not None:
|
|
custom_profile.cleanup()
|
|
|
|
|
|
@pytest.fixture
|
|
def profile_folder(firefox_options):
|
|
return get_profile_folder(firefox_options)
|
|
|
|
|
|
@pytest_asyncio.fixture
|
|
async def bidi_session(request, geckodriver):
|
|
"""Override bidi_session to use the geckodriver fixture.
|
|
|
|
Reads the ``geckodriver`` marker for extra arguments and the
|
|
``capabilities`` marker for custom session capabilities.
|
|
"""
|
|
extra_args, force_new = _get_geckodriver_marker_args(request)
|
|
capabilities = _get_capabilities_marker_args(request)
|
|
|
|
driver = geckodriver(extra_args=extra_args, force_new=force_new)
|
|
|
|
bidi_capabilities = {"webSocketUrl": True}
|
|
if capabilities is not None:
|
|
bidi_capabilities.update(capabilities)
|
|
driver.new_session(capabilities=bidi_capabilities)
|
|
|
|
bidi_session = driver.session.bidi_session
|
|
|
|
# Clear any stale transport from a previous event loop (e.g. from a test
|
|
# that used the geckodriver fixture directly to start a bidi session).
|
|
bidi_session.transport = None
|
|
await bidi_session.start()
|
|
|
|
if driver.session.capabilities.get("setWindowRect"):
|
|
driver.session.window.size = defaults.WINDOW_SIZE
|
|
driver.session.window.position = defaults.WINDOW_POSITION
|
|
|
|
yield bidi_session
|
|
|
|
if bidi_session.transport is not None:
|
|
await bidi_session.transport.end()
|
|
bidi_session.transport = None
|
|
|
|
|
|
@pytest.fixture
|
|
def session(request, configuration, geckodriver):
|
|
"""Override session to use the geckodriver fixture."""
|
|
extra_args, force_new = _get_geckodriver_marker_args(request)
|
|
capabilities = _get_capabilities_marker_args(request)
|
|
|
|
driver = geckodriver(extra_args=extra_args, force_new=force_new)
|
|
|
|
session_capabilities = {"webSocketUrl": False}
|
|
if capabilities is not None:
|
|
session_capabilities.update(capabilities)
|
|
driver.new_session(capabilities=session_capabilities)
|
|
|
|
session = driver.session
|
|
|
|
if session.capabilities.get("setWindowRect"):
|
|
session.window.size = defaults.WINDOW_SIZE
|
|
session.window.position = defaults.WINDOW_POSITION
|
|
|
|
multiplier = configuration["timeout_multiplier"]
|
|
session.timeouts.implicit = defaults.IMPLICIT_WAIT_TIMEOUT * multiplier
|
|
session.timeouts.page_load = defaults.PAGE_LOAD_TIMEOUT * multiplier
|
|
session.timeouts.script = defaults.SCRIPT_TIMEOUT * multiplier
|
|
|
|
yield session
|
|
|
|
|
|
@pytest.fixture
|
|
def current_session():
|
|
"""Override current_session to return the active geckodriver session."""
|
|
assert _geckodriver_state["current"] is not None, (
|
|
"current_session requires an active geckodriver session. "
|
|
"Use the bidi_session or session fixture first."
|
|
)
|
|
return _geckodriver_state["current"].session
|
|
|
|
|
|
def _get_capabilities_marker_args(request):
|
|
"""Read the capabilities marker and return extra capabilities, or None."""
|
|
capabilities_marker = request.node.get_closest_marker("capabilities")
|
|
if capabilities_marker and capabilities_marker.args:
|
|
return capabilities_marker.args[0]
|
|
|
|
return None
|
|
|
|
|
|
def _get_geckodriver_marker_args(request):
|
|
"""Read the geckodriver marker for extra args and force_new."""
|
|
extra_args = []
|
|
force_new = False
|
|
|
|
geckodriver_marker = request.node.get_closest_marker("geckodriver")
|
|
if geckodriver_marker:
|
|
if geckodriver_marker.kwargs.get("allow_system_access"):
|
|
extra_args.append("--allow-system-access")
|
|
force_new = geckodriver_marker.kwargs.get("force_new", False)
|
|
|
|
return extra_args, force_new
|