| 1 |
|
| 2 |
|
| 3 |
|
| 4 |
Standard library only, on purpose. WebDriver is a REST protocol over HTTP, so a |
| 5 |
runner for it needs no package -- start geckodriver, POST a session, navigate, |
| 6 |
read the title mocha writes. Adding a client library here would put a |
| 7 |
`requirements.txt` beside a suite whose whole argument is that it needs no |
| 8 |
package manager. |
| 9 |
|
| 10 |
Firefox rather than Chrome, and that is not a preference. Google publishes |
| 11 |
`chrome-for-testing` for linux64 and no Linux arm64 build at all, and astra is |
| 12 |
aarch64; the Chromium in apt there is the snap shim, which cuts against taking |
| 13 |
snaps off that machine. Firefox plus geckodriver runs on both boxes, so the |
| 14 |
suite has one story rather than two. |
| 15 |
|
| 16 |
Usage: |
| 17 |
|
| 18 |
python3 crates/quasi-webview/tests/browser/run.py |
| 19 |
|
| 20 |
Needs `firefox` on PATH and `geckodriver` on PATH or in `~/.local/bin`. Exits 0 |
| 21 |
when every test passed, 1 when any failed, and 2 when the run could not happen |
| 22 |
-- a missing driver is not a red suite. |
| 23 |
|
| 24 |
|
| 25 |
import json |
| 26 |
import pathlib |
| 27 |
import shutil |
| 28 |
import subprocess |
| 29 |
import sys |
| 30 |
import time |
| 31 |
import urllib.error |
| 32 |
import urllib.request |
| 33 |
|
| 34 |
PORT = 4444 |
| 35 |
BASE = f"http://127.0.0.1:{PORT}" |
| 36 |
PAGE = pathlib.Path(__file__).resolve().parent / "index.html" |
| 37 |
|
| 38 |
|
| 39 |
def driver(): |
| 40 |
|
| 41 |
found = shutil.which("geckodriver") |
| 42 |
if found: |
| 43 |
return found |
| 44 |
local = pathlib.Path.home() / ".local" / "bin" / "geckodriver" |
| 45 |
if local.exists(): |
| 46 |
return str(local) |
| 47 |
sys.exit( |
| 48 |
"geckodriver not found. Put it on PATH or in ~/.local/bin:\n" |
| 49 |
" https://github.com/mozilla/geckodriver/releases" |
| 50 |
) |
| 51 |
|
| 52 |
|
| 53 |
def call(method, path, body=None): |
| 54 |
data = json.dumps(body).encode() if body is not None else None |
| 55 |
request = urllib.request.Request( |
| 56 |
BASE + path, |
| 57 |
data=data, |
| 58 |
method=method, |
| 59 |
headers={"Content-Type": "application/json"}, |
| 60 |
) |
| 61 |
with urllib.request.urlopen(request, timeout=60) as answer: |
| 62 |
return json.loads(answer.read() or b"{}") |
| 63 |
|
| 64 |
|
| 65 |
def listening(): |
| 66 |
|
| 67 |
for _ in range(80): |
| 68 |
try: |
| 69 |
urllib.request.urlopen(BASE + "/status", timeout=2) |
| 70 |
return True |
| 71 |
except Exception: |
| 72 |
time.sleep(0.25) |
| 73 |
return False |
| 74 |
|
| 75 |
|
| 76 |
def verdict(session): |
| 77 |
|
| 78 |
for _ in range(120): |
| 79 |
said = call("GET", f"/session/{session}/title")["value"] |
| 80 |
if said.startswith(("PASS", "FAIL")): |
| 81 |
return said |
| 82 |
time.sleep(0.25) |
| 83 |
return "TIMEOUT" |
| 84 |
|
| 85 |
|
| 86 |
def main(): |
| 87 |
if not shutil.which("firefox"): |
| 88 |
sys.exit("firefox not found on PATH.") |
| 89 |
|
| 90 |
gecko = subprocess.Popen( |
| 91 |
[driver(), "--port", str(PORT), "--log", "error"], |
| 92 |
stdout=subprocess.DEVNULL, |
| 93 |
stderr=subprocess.DEVNULL, |
| 94 |
) |
| 95 |
try: |
| 96 |
if not listening(): |
| 97 |
sys.exit(f"geckodriver did not start listening on {PORT}.") |
| 98 |
|
| 99 |
opened = call( |
| 100 |
"POST", |
| 101 |
"/session", |
| 102 |
{ |
| 103 |
"capabilities": { |
| 104 |
"alwaysMatch": { |
| 105 |
"browserName": "firefox", |
| 106 |
"moz:firefoxOptions": {"args": ["-headless"]}, |
| 107 |
} |
| 108 |
} |
| 109 |
}, |
| 110 |
)["value"] |
| 111 |
session = opened["sessionId"] |
| 112 |
print(f"firefox {opened['capabilities'].get('browserVersion')}, headless") |
| 113 |
|
| 114 |
try: |
| 115 |
call("POST", f"/session/{session}/url", {"url": PAGE.as_uri()}) |
| 116 |
said = verdict(session) |
| 117 |
report = call( |
| 118 |
"POST", |
| 119 |
f"/session/{session}/execute/sync", |
| 120 |
{ |
| 121 |
"script": "return document.getElementById('mocha').innerText;", |
| 122 |
"args": [], |
| 123 |
}, |
| 124 |
)["value"] |
| 125 |
finally: |
| 126 |
call("DELETE", f"/session/{session}") |
| 127 |
finally: |
| 128 |
gecko.terminate() |
| 129 |
|
| 130 |
print(said) |
| 131 |
if not said.startswith("PASS"): |
| 132 |
|
| 133 |
print("\n".join(line for line in report.splitlines() if line.strip())) |
| 134 |
return 1 |
| 135 |
return 0 |
| 136 |
|
| 137 |
|
| 138 |
if __name__ == "__main__": |
| 139 |
sys.exit(main()) |
| 140 |
|