Source code
Revision control
Copy as Markdown
Other Tools
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
import asyncio
import json
import os
import re
import subprocess
import sys
from datetime import datetime
import pytest
import webdriver
from client import Client
try:
import pathlib
except ImportError:
import pathlib2 as pathlib
APS_PREF = "privacy.partition.always_partition_third_party_non_cookie_storage"
CB_PBM_PREF = "network.cookie.cookieBehavior.pbmode"
CB_PREF = "network.cookie.cookieBehavior"
INJECTIONS_PREF = "extensions.webcompat.perform_injections"
NOTIFICATIONS_PERMISSIONS_PREF = "permissions.default.desktop-notification"
PBM_PREF = "browser.privatebrowsing.autostart"
PIP_OVERRIDES_PREF = "extensions.webcompat.enable_picture_in_picture_overrides"
SHIMS_PREF = "extensions.webcompat.enable_shims"
STRICT_ETP_PREF = "privacy.trackingprotection.enabled"
UA_OVERRIDES_PREF = "extensions.webcompat.perform_ua_overrides"
SYSTEM_ADDON_UPDATES_PREF = "extensions.systemAddon.update.enabled"
class WebDriver:
def __init__(self, config):
self.browser_binary = config.getoption("browser_binary")
self.device_serial = config.getoption("device_serial")
self.package_name = config.getoption("package_name")
self.addon = config.getoption("addon")
self.webdriver_binary = config.getoption("webdriver_binary")
self.port = config.getoption("webdriver_port")
self.ws_port = config.getoption("webdriver_ws_port")
self.log_level = config.getoption("webdriver_log_level")
self.headless = config.getoption("headless")
self.debug = config.getoption("debug")
self.proc = None
def command_line_driver(self):
raise NotImplementedError
def capabilities(self, test_config):
raise NotImplementedError
def __enter__(self):
assert self.proc is None
self.proc = subprocess.Popen(self.command_line_driver())
return self
def __exit__(self, *args, **kwargs):
self.proc.kill()
class FirefoxWebDriver(WebDriver):
def command_line_driver(self):
rv = [
self.webdriver_binary,
"--port",
str(self.port),
"--websocket-port",
str(self.ws_port),
]
if self.debug:
rv.append("-vv")
elif self.log_level == "DEBUG":
rv.append("-v")
return rv
def capabilities(self, test_config):
prefs = {}
if "aps" in test_config:
prefs[APS_PREF] = test_config["aps"]
if "use_interventions" in test_config:
value = test_config["use_interventions"]
prefs[INJECTIONS_PREF] = value
prefs[UA_OVERRIDES_PREF] = value
prefs[PIP_OVERRIDES_PREF] = value
if "use_pbm" in test_config:
prefs[PBM_PREF] = test_config["use_pbm"]
if "use_shims" in test_config:
prefs[SHIMS_PREF] = test_config["use_shims"]
if "use_strict_etp" in test_config:
prefs[STRICT_ETP_PREF] = test_config["use_strict_etp"]
if "no_overlay_scrollbars" in test_config:
prefs["widget.gtk.overlay-scrollbars.enabled"] = False
prefs[SYSTEM_ADDON_UPDATES_PREF] = False
# remote/cdp/CDP.sys.mjs sets cookieBehavior to 0,
# which we definitely do not want, so set it back to 5.
cookieBehavior = 4 if test_config.get("without_tcp") else 5
prefs[CB_PREF] = cookieBehavior
prefs[CB_PBM_PREF] = cookieBehavior
# prevent "allow notifications for?" popups by setting the
# default permission for notificaitons to PERM_DENY_ACTION.
prefs[NOTIFICATIONS_PERMISSIONS_PREF] = 2
fx_options = {"prefs": prefs}
if self.browser_binary:
fx_options["binary"] = self.browser_binary
if self.headless:
fx_options["args"] = ["--headless"]
if self.device_serial:
fx_options["androidDeviceSerial"] = self.device_serial
fx_options["androidPackage"] = self.package_name
if self.addon:
prefs["xpinstall.signatures.required"] = False
prefs["extensions.experiments.enabled"] = True
return {
"pageLoadStrategy": "normal",
"moz:firefoxOptions": fx_options,
}
@pytest.fixture(scope="session")
def should_do_2fa(request):
return request.config.getoption("do2fa", False)
@pytest.fixture(scope="session")
def config_file(request):
path = request.config.getoption("config")
if not path:
return None
with open(path) as f:
return json.load(f)
@pytest.fixture
def bug_number(request):
return re.findall(r"\d+", str(request.fspath.basename))[0]
@pytest.fixture
def in_headless_mode(request):
return request.config.getoption("headless")
@pytest.fixture
def credentials(bug_number, config_file):
if not config_file:
pytest.skip(f"login info required for bug #{bug_number}")
return None
try:
credentials = config_file[bug_number]
except KeyError:
pytest.skip(f"no login for bug #{bug_number} found")
return
return {"username": credentials["username"], "password": credentials["password"]}
@pytest.fixture(scope="session")
def driver(pytestconfig):
if pytestconfig.getoption("browser") == "firefox":
cls = FirefoxWebDriver
else:
assert False
with cls(pytestconfig) as driver_instance:
yield driver_instance
@pytest.hookimpl(tryfirst=True, hookwrapper=True)
def pytest_runtest_makereport(item, call):
outcome = yield
rep = outcome.get_result()
setattr(item, "rep_" + rep.when, rep)
@pytest.fixture(scope="function", autouse=True)
async def test_failed_check(request):
yield
if (
not request.config.getoption("no_failure_screenshots")
and request.node.rep_setup.passed
and request.node.rep_call.failed
):
session = request.node.funcargs["session"]
try:
file_name = f'{request.node.nodeid}_failure_{datetime.today().strftime("%Y-%m-%d_%H:%M")}.png'.replace(
"/", "_"
).replace(
"::", "__"
)
await take_screenshot(session, file_name)
print("Saved failure screenshot to: ", file_name)
except Exception as e:
print("Error saving screenshot: ", e)
async def take_screenshot(session, file_name):
cwd = pathlib.Path(os.getcwd())
path = cwd / file_name
top = await session.bidi_session.browsing_context.get_tree()
screenshot = await session.bidi_session.browsing_context.capture_screenshot(
context=top[0]["context"]
)
with path.open("wb") as strm:
strm.write(screenshot)
return file_name
@pytest.fixture(scope="session")
def event_loop():
return asyncio.get_event_loop_policy().new_event_loop()
@pytest.fixture(scope="function")
async def client(request, session, event_loop):
return Client(request, session, event_loop)
def install_addon(session, addon_file_path):
context = session.send_session_command("GET", "moz/context")
session.send_session_command("POST", "moz/context", {"context": "chrome"})
session.execute_async_script(
"""
async function installAsBuiltinExtension(xpi) {
// The built-in location requires a resource: URL that maps to a
// jar: or file: URL. This would typically be something bundled
// into omni.ja but we use a temp file.
let base = Services.io.newURI(`jar:file:${xpi.path}!/`);
let resProto = Services.io
.getProtocolHandler("resource")
.QueryInterface(Ci.nsIResProtocolHandler);
resProto.setSubstitution("ext-test", base);
}
const addon_file_path = arguments[0];
const cb = arguments[1];
const { AddonManager } = ChromeUtils.importESModule(
"resource://gre/modules/AddonManager.sys.mjs"
);
const { ExtensionPermissions } = ChromeUtils.importESModule(
"resource://gre/modules/ExtensionPermissions.sys.mjs"
);
const { FileUtils } = ChromeUtils.importESModule(
"resource://gre/modules/FileUtils.sys.mjs"
);
const file = new FileUtils.File(arguments[0]);
installAsBuiltinExtension(file).then(addon => {
// also make sure the addon works in private browsing mode
const incognitoPermission = {
permissions: ["internal:privateBrowsingAllowed"],
origins: [],
};
ExtensionPermissions.add(addon.id, incognitoPermission).then(() => {
addon.reload().then(cb);
});
});
""",
[addon_file_path],
)
session.send_session_command("POST", "moz/context", {"context": context})
@pytest.fixture(scope="function")
async def session(driver, test_config):
caps = driver.capabilities(test_config)
caps.update(
{
"acceptInsecureCerts": True,
"webSocketUrl": True,
}
)
caps = {"alwaysMatch": caps}
print(caps)
session = None
for i in range(0, 15):
try:
if not session:
session = webdriver.Session(
"localhost", driver.port, capabilities=caps, enable_bidi=True
)
session.test_config = test_config
session.start()
break
except (ConnectionRefusedError, webdriver.error.TimeoutException):
await asyncio.sleep(0.5)
try:
await session.bidi_session.start()
except AttributeError:
sys.exit("Could not start a WebDriver session; please try again")
if driver.addon:
install_addon(session, driver.addon)
yield session
await session.bidi_session.end()
session.end()
@pytest.fixture(autouse=True)
def platform(session):
return session.capabilities["platformName"]
@pytest.fixture(autouse=True)
def check_visible_scrollbars(session):
plat = session.capabilities["platformName"]
if plat == "android":
return "Android does not have visible scrollbars"
elif plat == "mac":
cmd = ["defaults", "read", "-g", "AppleShowScrollBars"]
p = subprocess.Popen(cmd, stdout=subprocess.PIPE)
p.wait()
if "Always" in str(p.stdout.readline()):
return None
return "scrollbars are not set to always be visible in MacOS system preferences"
return None
@pytest.fixture(autouse=True)
def need_visible_scrollbars(bug_number, check_visible_scrollbars, request, session):
if request.node.get_closest_marker("need_visible_scrollbars"):
if (
request.node.get_closest_marker("need_visible_scrollbars")
and check_visible_scrollbars
):
pytest.skip(f"Bug #{bug_number} skipped: {check_visible_scrollbars}")
@pytest.fixture(autouse=True)
def only_platforms(bug_number, platform, request, session):
if request.node.get_closest_marker("only_platforms"):
plats = request.node.get_closest_marker("only_platforms").args
for only in plats:
if only == platform:
return
pytest.skip(
f"Bug #{bug_number} skipped on platform ({platform}, test only for {' or '.join(plats)})"
)
@pytest.fixture(autouse=True)
def skip_platforms(bug_number, platform, request, session):
if request.node.get_closest_marker("skip_platforms"):
plats = request.node.get_closest_marker("skip_platforms").args
for skipped in plats:
if skipped == platform:
pytest.skip(
f"Bug #{bug_number} skipped on platform ({platform}, test skipped for {' and '.join(plats)})"
)