"""Optional Chrome scenarios exercise the CDP client against a real browser."""
from __future__ import annotations
from functools import partial
from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
from unittest import mock
import contextlib
import os
import shutil
import sys
import tempfile
import threading
import time
import unittest
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT / "priv" / "python"))
from albedo_plugins.browser import spawn, render, one # noqa: E402
from albedo_plugins.browser.errors import NotActionable # noqa: E402
from albedo_plugins.browser import hooks, paths # noqa: E402
from albedo_plugins.browser.launch import OwnedChrome, _command # noqa: E402
class BrowserIntegrationTest(unittest.IsolatedAsyncioTestCase):
"""End-to-end integration tests using local Chromium if present."""
async def asyncSetUp(self):
self.has_chrome = (
shutil.which("chromium") is not None
or shutil.which("google-chrome") is not None
or Path("/Applications/Chromium.app/Contents/MacOS/Chromium").exists()
or Path(
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"
).exists()
)
async def test_spawn_navigate_observe_interact_close(self):
if not self.has_chrome:
self.skipTest("No Chrome or Chromium installed on this system")
b = await spawn(headless=True)
try:
page = await b.new_page()
html = (
"data:text/html,"
""
"
Albedo Test"
""
"
Header
"
""
""
""
""
""
)
await page.goto(html)
# Observe
obs = await page.observe()
self.assertEqual(obs["title"], "Albedo Test")
text = render(obs)
self.assertIn("Header", text)
self.assertIn("Save", text)
# Find and One
btn = one(obs, role="button", name="Save")
self.assertIsNotNone(btn)
field = one(obs, role="textbox", name="Name")
self.assertIsNotNone(field)
# Fill and Click
await page.fill(field, "new value")
self.assertEqual(
await page.evaluate("document.querySelector('#name').value"),
"new value",
)
await page.click(btn)
self.assertTrue(await page.evaluate("window.clicked"))
# Select takes an option's value or its label
picker = one(obs, role="combobox", name="Pick")
self.assertIsNotNone(picker)
await page.select(picker, "b")
self.assertEqual(
await page.evaluate(
"[window.picked, document.querySelector('#pick').value]"
),
["b", "b"],
)
await page.select(picker, "Alpha")
self.assertEqual(
await page.evaluate("document.querySelector('#pick').value"), "a"
)
with self.assertRaises(NotActionable) as refused:
await page.select(picker, "Gamma")
self.assertEqual(refused.exception.details["found"], False)
# A screenshot nobody gave a path goes to the cache, not the working tree
with (
tempfile.TemporaryDirectory() as home,
mock.patch.dict(os.environ, {"ALBEDO_HOME": home}),
):
shot = await page.screenshot()
self.assertEqual(
shot.parent,
(Path(home) / "cache" / "browser-screenshots").resolve(),
)
self.assertTrue(shot.read_bytes().startswith(b"\x89PNG"))
# Close page
await page.close()
finally:
await b.close()
async def test_prerender_takes_the_opt_in_and_the_page_follows_activation(self):
if not self.has_chrome:
self.skipTest("No Chrome or Chromium installed on this system")
root = tempfile.TemporaryDirectory()
self.addCleanup(root.cleanup)
(Path(root.name) / "index.html").write_text(
"Start"
'"
'Next'
)
(Path(root.name) / "next.html").write_text(
"Next
Prerendered
"
)
handler = partial(SimpleHTTPRequestHandler, directory=root.name)
server = ThreadingHTTPServer(("127.0.0.1", 0), handler)
threading.Thread(target=server.serve_forever, daemon=True).start()
self.addCleanup(server.server_close)
self.addCleanup(server.shutdown)
start = f"http://127.0.0.1:{server.server_port}/index.html"
b = await spawn(headless=True)
try:
outcomes = {}
for prerender in (False, True):
page = await b.new_page(prerender=prerender)
await page.cdp.send("Preload.enable")
async with page.events(
"Preload.prerenderStatusUpdated", enable=False
) as updates:
await page.goto(start)
update = await updates.next(
where=lambda e: e["status"] in {"Ready", "Failure"}
)
outcomes[prerender] = (update["status"], update.get("prerenderStatus"))
if prerender:
before = page.target_id
await page.click(
one(await page.observe(), role="link", name="Next")
)
await page.wait_for(role="heading", name="Prerendered")
self.assertNotEqual(page.target_id, before)
self.assertEqual(
await page.evaluate(
"[document.title, performance.getEntriesByType('navigation')[0].activationStart > 0]"
),
["Next", True],
)
await page.close()
self.assertEqual(
outcomes,
{
False: ("Failure", "PrerenderingDisabledByDevTools"),
True: ("Ready", None),
},
)
finally:
await b.close()
PROFILE = Path("/tmp/profile")
class LaunchCommandTest(unittest.TestCase):
"""The argv albedo hands Chrome, read without starting one."""
def test_macos_launches_stay_out_of_the_login_keychain(self):
# Chrome raises a blocking "Keychain Not Found" panel when it cannot
# resolve a default keychain for its own process.
command = _command(
"/opt/chrome", PROFILE, headless=False, sandbox=True, args=[]
)
self.assertEqual("--use-mock-keychain" in command, sys.platform == "darwin")
def test_the_owned_options_are_the_ones_asked_for(self):
command = _command(
"/opt/chrome", PROFILE, headless=False, sandbox=False, args=["--lang=en"]
)
self.assertEqual(command[0], "/opt/chrome")
self.assertIn("--user-data-dir=/tmp/profile", command)
self.assertIn("--remote-debugging-port=0", command)
self.assertNotIn("--headless=new", command)
self.assertIn("--no-sandbox", command)
self.assertEqual(command[-2:], ["--lang=en", "about:blank"])
headless = _command(
"/opt/chrome", PROFILE, headless=True, sandbox=True, args=[]
)
self.assertIn("--headless=new", headless)
class ScreenshotCacheTest(unittest.TestCase):
"""Default screenshots live under the albedo home and are not kept forever."""
def test_they_go_to_the_cache_under_the_albedo_home(self):
with (
tempfile.TemporaryDirectory() as home,
mock.patch.dict(os.environ, {"ALBEDO_HOME": home}),
):
self.assertEqual(
paths.screenshots(), Path(home) / "cache" / "browser-screenshots"
)
def test_only_old_screenshots_are_pruned(self):
with tempfile.TemporaryDirectory() as directory:
folder = Path(directory)
old, fresh, other = (
folder / "old.png",
folder / "fresh.png",
folder / "n.txt",
)
for file in (old, fresh, other):
file.write_bytes(b"x")
week_ago = time.time() - paths.SCREENSHOT_TTL_SECONDS - 60
for file in (old, other):
os.utime(file, (week_ago, week_ago))
paths.prune(folder)
self.assertEqual(
sorted(p.name for p in folder.iterdir()), ["fresh.png", "n.txt"]
)
paths.prune(folder / "missing")
class BookkeepingTest(unittest.TestCase):
"""The browser's own files stay out of a cell's file-change trace."""
def test_profile_setup_runs_unobserved(self):
entered: list[bool] = []
original = hooks.unobserved
@contextlib.contextmanager
def unobserved():
entered.append(True)
yield
hooks.unobserved = unobserved
try:
chrome = OwnedChrome(None)
try:
self.assertTrue(entered)
self.assertTrue((chrome.profile / ".albedo-browser-profile").exists())
finally:
chrome._release()
finally:
hooks.unobserved = original
if __name__ == "__main__":
unittest.main()