91e75e620b
CI: cua-driver distro-compat matrix / Resolve release version (push) Waiting to run
CI: cua-driver distro-compat matrix / debian:12 (glibc 2.36) (push) Blocked by required conditions
CI: cua-driver distro-compat matrix / fedora:41 (glibc 2.40) (push) Blocked by required conditions
CI: cua-driver distro-compat matrix / rockylinux:9 (glibc 2.34) (push) Blocked by required conditions
CI: cua-driver distro-compat matrix / ubuntu:22.04 (glibc 2.35) (push) Blocked by required conditions
CI: cua-driver distro-compat matrix / ubuntu:24.04 (glibc 2.39) (push) Blocked by required conditions
CI: cua-driver distro-compat matrix / Distro compat summary (push) Blocked by required conditions
CI: Nix Linux Rust source / Nix / compositor build (push) Waiting to run
CI: Nix Linux Rust source / Nix / driver package (push) Waiting to run
CI: Nix Linux Rust source / Nix / Rust unit tests (push) Waiting to run
CI: Rust Linux unit / Rust Linux unit and compile (push) Waiting to run
CI: Rust Windows unit / Rust Windows unit and compile (push) Waiting to run
CI: SPDX Headers / Check SPDX headers (warn-only) (push) Waiting to run
CD: Docs MCP Server / build (linux/amd64) (push) Waiting to run
CD: Docs MCP Server / build (linux/arm64) (push) Waiting to run
CD: Docs MCP Server / merge (push) Blocked by required conditions
946 lines
37 KiB
Python
946 lines
37 KiB
Python
"""Integration tests for every sandbox interface method across all guest OS types.
|
|
|
|
Imports exclusively from `cua` (the meta-package). Tests auto-skip via
|
|
skip_if_unsupported() when the required local runtime is absent.
|
|
|
|
Interface coverage:
|
|
sb.shell — Shell.run()
|
|
sb.mouse — click, right_click, double_click, move, scroll, mouse_down, mouse_up, drag
|
|
sb.keyboard — type, keypress, key_down, key_up
|
|
sb.screen — screenshot, screenshot_base64, size
|
|
sb.clipboard — get, set
|
|
sb.tunnel — forward (single, multi, await form)
|
|
sb.terminal — create, send_input, resize, close
|
|
sb.window — get_active_title
|
|
sb.mobile — tap, long_press, double_tap, type_text, swipe, scroll_up/down/left/right,
|
|
fling, gesture, pinch_in, pinch_out, key, home, back, recents, power,
|
|
volume_up, volume_down, enter, backspace, wake, notifications,
|
|
close_notifications
|
|
|
|
Guest OS coverage:
|
|
Linux container (Image.linux()) — shell, clipboard, tunnel, terminal
|
|
Linux VM (Image.linux(kind="vm")) — + screen, mouse, keyboard, window
|
|
macOS VM (Image.macos()) — shell, screen, mouse, keyboard, clipboard,
|
|
tunnel, terminal, window
|
|
Windows VM (Image.windows()) — shell, screen, mouse, keyboard, clipboard,
|
|
tunnel, terminal, window
|
|
Android VM (Image.android()) — shell, screen, mobile.*
|
|
|
|
Run with:
|
|
pytest tests/test_interfaces.py -v
|
|
pytest tests/test_interfaces.py -v -k "linux and shell"
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import base64
|
|
|
|
import httpx
|
|
import pytest
|
|
|
|
from cua import Image, Sandbox, skip_if_unsupported
|
|
|
|
# ── helpers ──────────────────────────────────────────────────────────────────
|
|
|
|
|
|
async def _shell(sb, cmd: str, timeout: int = 30) -> str:
|
|
r = await sb.shell.run(cmd, timeout=timeout)
|
|
assert r.success, f"Command failed (rc={r.returncode}): {r.stderr}"
|
|
return r.stdout.strip()
|
|
|
|
|
|
async def _start_http_server(sb, port: int) -> None:
|
|
"""Start a minimal Python HTTP echo server inside the sandbox.
|
|
|
|
Uses python3 with POSIX background operator on Linux/macOS.
|
|
macOS VMs ship python3; Linux containers have it in the base image.
|
|
"""
|
|
server_code = (
|
|
"import http.server as h, socketserver as ss; "
|
|
f"ss.TCPServer(('0.0.0.0',{port}),"
|
|
"type('H',(h.BaseHTTPRequestHandler,),"
|
|
"{'do_GET':lambda s:(s.send_response(200),s.send_header('Content-Type','text/plain')"
|
|
",s.end_headers(),s.wfile.write(b'ok'))})"
|
|
").serve_forever()"
|
|
)
|
|
await sb.shell.run(f'python3 -c "{server_code}" &', timeout=5)
|
|
await asyncio.sleep(1)
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
# Shell — Linux container
|
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
class TestShellLinux:
|
|
_IMAGE = Image.linux()
|
|
|
|
def setup_method(self):
|
|
skip_if_unsupported(self._IMAGE)
|
|
|
|
async def test_run_success(self):
|
|
async with Sandbox.ephemeral(self._IMAGE, local=True) as sb:
|
|
r = await sb.shell.run("echo hello")
|
|
assert r.success
|
|
assert r.returncode == 0
|
|
assert "hello" in r.stdout
|
|
|
|
async def test_run_failure_returncode(self):
|
|
async with Sandbox.ephemeral(self._IMAGE, local=True) as sb:
|
|
r = await sb.shell.run("false")
|
|
assert not r.success
|
|
assert r.returncode != 0
|
|
|
|
async def test_run_stderr(self):
|
|
async with Sandbox.ephemeral(self._IMAGE, local=True) as sb:
|
|
r = await sb.shell.run("ls /no_such_dir_xyz 2>&1; true")
|
|
# At minimum the command ran
|
|
assert r.returncode is not None
|
|
|
|
async def test_run_pipeline(self):
|
|
async with Sandbox.ephemeral(self._IMAGE, local=True) as sb:
|
|
r = await sb.shell.run("echo hello | tr a-z A-Z")
|
|
assert r.success
|
|
assert "HELLO" in r.stdout
|
|
|
|
async def test_run_multiline_output(self):
|
|
async with Sandbox.ephemeral(self._IMAGE, local=True) as sb:
|
|
r = await sb.shell.run("printf 'a\\nb\\nc\\n'")
|
|
assert r.success
|
|
assert r.stdout.strip().splitlines() == ["a", "b", "c"]
|
|
|
|
async def test_run_sequential_state(self):
|
|
async with Sandbox.ephemeral(self._IMAGE, local=True) as sb:
|
|
r1 = await sb.shell.run("echo persistent > /tmp/st.txt")
|
|
assert r1.success
|
|
r2 = await sb.shell.run("cat /tmp/st.txt")
|
|
assert r2.success
|
|
assert "persistent" in r2.stdout
|
|
|
|
async def test_run_timeout(self):
|
|
async with Sandbox.ephemeral(self._IMAGE, local=True) as sb:
|
|
with pytest.raises(BaseException): # noqa: B017
|
|
await sb.shell.run("sleep 60", timeout=2)
|
|
|
|
async def test_commandresult_fields(self):
|
|
async with Sandbox.ephemeral(self._IMAGE, local=True) as sb:
|
|
r = await sb.shell.run("echo out; echo err >&2; exit 0")
|
|
assert isinstance(r.stdout, str)
|
|
assert isinstance(r.stderr, str)
|
|
assert isinstance(r.returncode, int)
|
|
assert r.success is True
|
|
|
|
|
|
class TestShellMacOS:
|
|
_IMAGE = Image.macos()
|
|
|
|
def setup_method(self):
|
|
skip_if_unsupported(self._IMAGE)
|
|
|
|
async def test_run_success(self):
|
|
async with Sandbox.ephemeral(self._IMAGE, local=True) as sb:
|
|
r = await sb.shell.run("echo hello macos")
|
|
assert r.success
|
|
assert "hello" in r.stdout
|
|
|
|
async def test_run_pipeline(self):
|
|
async with Sandbox.ephemeral(self._IMAGE, local=True) as sb:
|
|
r = await sb.shell.run("echo hello | tr a-z A-Z")
|
|
assert r.success
|
|
assert "HELLO" in r.stdout
|
|
|
|
|
|
class TestShellWindows:
|
|
_IMAGE = Image.windows()
|
|
|
|
def setup_method(self):
|
|
skip_if_unsupported(self._IMAGE)
|
|
|
|
async def test_run_success(self):
|
|
async with Sandbox.ephemeral(self._IMAGE, local=True) as sb:
|
|
r = await sb.shell.run("echo hello windows")
|
|
assert r.success
|
|
assert "hello" in r.stdout
|
|
|
|
async def test_run_failure(self):
|
|
async with Sandbox.ephemeral(self._IMAGE, local=True) as sb:
|
|
r = await sb.shell.run("exit 1")
|
|
assert not r.success
|
|
|
|
|
|
class TestShellAndroid:
|
|
_IMAGE = Image.android()
|
|
|
|
def setup_method(self):
|
|
skip_if_unsupported(self._IMAGE)
|
|
|
|
async def test_run_adb_shell(self):
|
|
async with Sandbox.ephemeral(self._IMAGE, local=True) as sb:
|
|
r = await sb.shell.run("echo hello android")
|
|
assert r.success
|
|
assert "hello" in r.stdout
|
|
|
|
async def test_run_getprop(self):
|
|
async with Sandbox.ephemeral(self._IMAGE, local=True) as sb:
|
|
r = await sb.shell.run("getprop ro.build.version.release")
|
|
assert r.success
|
|
assert r.stdout.strip() # non-empty version string
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
# Clipboard — Linux container, macOS, Windows
|
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
class TestClipboardLinux:
|
|
_IMAGE = Image.linux()
|
|
|
|
def setup_method(self):
|
|
skip_if_unsupported(self._IMAGE)
|
|
|
|
async def test_set_and_get(self):
|
|
async with Sandbox.ephemeral(self._IMAGE, local=True) as sb:
|
|
await sb.clipboard.set("hello clipboard")
|
|
text = await sb.clipboard.get()
|
|
assert text == "hello clipboard"
|
|
|
|
async def test_overwrite(self):
|
|
async with Sandbox.ephemeral(self._IMAGE, local=True) as sb:
|
|
await sb.clipboard.set("first")
|
|
await sb.clipboard.set("second")
|
|
assert await sb.clipboard.get() == "second"
|
|
|
|
async def test_unicode(self):
|
|
async with Sandbox.ephemeral(self._IMAGE, local=True) as sb:
|
|
payload = "héllo wörld 🌸"
|
|
await sb.clipboard.set(payload)
|
|
assert await sb.clipboard.get() == payload
|
|
|
|
async def test_multiline(self):
|
|
async with Sandbox.ephemeral(self._IMAGE, local=True) as sb:
|
|
payload = "line1\nline2\nline3"
|
|
await sb.clipboard.set(payload)
|
|
assert await sb.clipboard.get() == payload
|
|
|
|
async def test_empty_string(self):
|
|
async with Sandbox.ephemeral(self._IMAGE, local=True) as sb:
|
|
await sb.clipboard.set("non-empty")
|
|
await sb.clipboard.set("")
|
|
assert await sb.clipboard.get() == ""
|
|
|
|
|
|
class TestClipboardMacOS:
|
|
_IMAGE = Image.macos()
|
|
|
|
def setup_method(self):
|
|
skip_if_unsupported(self._IMAGE)
|
|
|
|
async def test_set_and_get(self):
|
|
async with Sandbox.ephemeral(self._IMAGE, local=True) as sb:
|
|
await sb.clipboard.set("macos clipboard")
|
|
assert await sb.clipboard.get() == "macos clipboard"
|
|
|
|
async def test_unicode(self):
|
|
async with Sandbox.ephemeral(self._IMAGE, local=True) as sb:
|
|
payload = "café ☕"
|
|
await sb.clipboard.set(payload)
|
|
assert await sb.clipboard.get() == payload
|
|
|
|
|
|
class TestClipboardWindows:
|
|
_IMAGE = Image.windows()
|
|
|
|
def setup_method(self):
|
|
skip_if_unsupported(self._IMAGE)
|
|
|
|
async def test_set_and_get(self):
|
|
async with Sandbox.ephemeral(self._IMAGE, local=True) as sb:
|
|
await sb.clipboard.set("windows clipboard")
|
|
assert await sb.clipboard.get() == "windows clipboard"
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
# Screen — Linux VM, macOS, Windows, Android
|
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
_LINUX_VM = Image.linux(kind="vm")
|
|
|
|
|
|
class TestScreenLinuxVM:
|
|
def setup_method(self):
|
|
skip_if_unsupported(_LINUX_VM)
|
|
|
|
async def test_screenshot_returns_bytes(self):
|
|
async with Sandbox.ephemeral(_LINUX_VM, local=True) as sb:
|
|
data = await sb.screen.screenshot()
|
|
assert isinstance(data, bytes) and len(data) > 0
|
|
|
|
async def test_screenshot_png_magic(self):
|
|
async with Sandbox.ephemeral(_LINUX_VM, local=True) as sb:
|
|
data = await sb.screen.screenshot(format="png")
|
|
assert data[:8] == b"\x89PNG\r\n\x1a\n"
|
|
|
|
async def test_screenshot_jpeg(self):
|
|
async with Sandbox.ephemeral(_LINUX_VM, local=True) as sb:
|
|
data = await sb.screen.screenshot(format="jpeg", quality=80)
|
|
assert data[:2] == b"\xff\xd8"
|
|
|
|
async def test_screenshot_base64(self):
|
|
async with Sandbox.ephemeral(_LINUX_VM, local=True) as sb:
|
|
b64 = await sb.screen.screenshot_base64()
|
|
assert isinstance(b64, str)
|
|
assert len(base64.b64decode(b64)) > 0
|
|
|
|
async def test_size(self):
|
|
async with Sandbox.ephemeral(_LINUX_VM, local=True) as sb:
|
|
w, h = await sb.screen.size()
|
|
assert isinstance(w, int) and w >= 640
|
|
assert isinstance(h, int) and h >= 480
|
|
|
|
|
|
class TestScreenMacOS:
|
|
_IMAGE = Image.macos()
|
|
|
|
def setup_method(self):
|
|
skip_if_unsupported(self._IMAGE)
|
|
|
|
async def test_screenshot_returns_bytes(self):
|
|
async with Sandbox.ephemeral(self._IMAGE, local=True) as sb:
|
|
data = await sb.screen.screenshot()
|
|
assert isinstance(data, bytes) and len(data) > 0
|
|
|
|
async def test_screenshot_png_magic(self):
|
|
async with Sandbox.ephemeral(self._IMAGE, local=True) as sb:
|
|
data = await sb.screen.screenshot(format="png")
|
|
assert data[:8] == b"\x89PNG\r\n\x1a\n"
|
|
|
|
async def test_screenshot_base64(self):
|
|
async with Sandbox.ephemeral(self._IMAGE, local=True) as sb:
|
|
b64 = await sb.screen.screenshot_base64()
|
|
decoded = base64.b64decode(b64)
|
|
assert len(decoded) > 0
|
|
|
|
async def test_size(self):
|
|
async with Sandbox.ephemeral(self._IMAGE, local=True) as sb:
|
|
w, h = await sb.screen.size()
|
|
assert w >= 640 and h >= 480
|
|
|
|
|
|
class TestScreenWindows:
|
|
_IMAGE = Image.windows()
|
|
|
|
def setup_method(self):
|
|
skip_if_unsupported(self._IMAGE)
|
|
|
|
async def test_screenshot_returns_bytes(self):
|
|
async with Sandbox.ephemeral(self._IMAGE, local=True) as sb:
|
|
data = await sb.screen.screenshot()
|
|
assert isinstance(data, bytes) and len(data) > 0
|
|
|
|
async def test_size(self):
|
|
async with Sandbox.ephemeral(self._IMAGE, local=True) as sb:
|
|
w, h = await sb.screen.size()
|
|
assert w > 0 and h > 0
|
|
|
|
|
|
class TestScreenAndroid:
|
|
_IMAGE = Image.android()
|
|
|
|
def setup_method(self):
|
|
skip_if_unsupported(self._IMAGE)
|
|
|
|
async def test_screenshot_returns_bytes(self):
|
|
async with Sandbox.ephemeral(self._IMAGE, local=True) as sb:
|
|
data = await sb.screen.screenshot()
|
|
assert isinstance(data, bytes) and len(data) > 0
|
|
|
|
async def test_size(self):
|
|
async with Sandbox.ephemeral(self._IMAGE, local=True) as sb:
|
|
w, h = await sb.screen.size()
|
|
assert w > 0 and h > 0
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
# Mouse — Linux VM, macOS, Windows
|
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
class TestMouseLinuxVM:
|
|
def setup_method(self):
|
|
skip_if_unsupported(_LINUX_VM)
|
|
|
|
async def test_click(self):
|
|
async with Sandbox.ephemeral(_LINUX_VM, local=True) as sb:
|
|
w, h = await sb.screen.size()
|
|
await sb.mouse.click(w // 2, h // 2) # must not raise
|
|
|
|
async def test_right_click(self):
|
|
async with Sandbox.ephemeral(_LINUX_VM, local=True) as sb:
|
|
w, h = await sb.screen.size()
|
|
await sb.mouse.right_click(w // 2, h // 2)
|
|
|
|
async def test_double_click(self):
|
|
async with Sandbox.ephemeral(_LINUX_VM, local=True) as sb:
|
|
w, h = await sb.screen.size()
|
|
await sb.mouse.double_click(w // 2, h // 2)
|
|
|
|
async def test_move(self):
|
|
async with Sandbox.ephemeral(_LINUX_VM, local=True) as sb:
|
|
await sb.mouse.move(100, 100)
|
|
await sb.mouse.move(200, 200)
|
|
|
|
async def test_scroll(self):
|
|
async with Sandbox.ephemeral(_LINUX_VM, local=True) as sb:
|
|
w, h = await sb.screen.size()
|
|
await sb.mouse.scroll(w // 2, h // 2, scroll_y=3)
|
|
|
|
async def test_mouse_down_and_up(self):
|
|
async with Sandbox.ephemeral(_LINUX_VM, local=True) as sb:
|
|
w, h = await sb.screen.size()
|
|
await sb.mouse.mouse_down(w // 2, h // 2)
|
|
await sb.mouse.mouse_up(w // 2, h // 2)
|
|
|
|
async def test_drag(self):
|
|
async with Sandbox.ephemeral(_LINUX_VM, local=True) as sb:
|
|
w, h = await sb.screen.size()
|
|
await sb.mouse.drag(100, h // 2, 300, h // 2)
|
|
|
|
|
|
class TestMouseMacOS:
|
|
_IMAGE = Image.macos()
|
|
|
|
def setup_method(self):
|
|
skip_if_unsupported(self._IMAGE)
|
|
|
|
async def test_click(self):
|
|
async with Sandbox.ephemeral(self._IMAGE, local=True) as sb:
|
|
w, h = await sb.screen.size()
|
|
await sb.mouse.click(w // 2, h // 2)
|
|
|
|
async def test_right_click(self):
|
|
async with Sandbox.ephemeral(self._IMAGE, local=True) as sb:
|
|
w, h = await sb.screen.size()
|
|
await sb.mouse.right_click(w // 2, h // 2)
|
|
|
|
async def test_double_click(self):
|
|
async with Sandbox.ephemeral(self._IMAGE, local=True) as sb:
|
|
w, h = await sb.screen.size()
|
|
await sb.mouse.double_click(w // 2, h // 2)
|
|
|
|
async def test_move(self):
|
|
async with Sandbox.ephemeral(self._IMAGE, local=True) as sb:
|
|
await sb.mouse.move(100, 100)
|
|
|
|
async def test_scroll(self):
|
|
async with Sandbox.ephemeral(self._IMAGE, local=True) as sb:
|
|
w, h = await sb.screen.size()
|
|
await sb.mouse.scroll(w // 2, h // 2, scroll_y=3)
|
|
|
|
async def test_mouse_down_and_up(self):
|
|
async with Sandbox.ephemeral(self._IMAGE, local=True) as sb:
|
|
w, h = await sb.screen.size()
|
|
await sb.mouse.mouse_down(w // 2, h // 2)
|
|
await sb.mouse.mouse_up(w // 2, h // 2)
|
|
|
|
async def test_drag(self):
|
|
async with Sandbox.ephemeral(self._IMAGE, local=True) as sb:
|
|
w, h = await sb.screen.size()
|
|
await sb.mouse.drag(100, h // 2, 300, h // 2)
|
|
|
|
|
|
class TestMouseWindows:
|
|
_IMAGE = Image.windows()
|
|
|
|
def setup_method(self):
|
|
skip_if_unsupported(self._IMAGE)
|
|
|
|
async def test_click(self):
|
|
async with Sandbox.ephemeral(self._IMAGE, local=True) as sb:
|
|
w, h = await sb.screen.size()
|
|
await sb.mouse.click(w // 2, h // 2)
|
|
|
|
async def test_right_click(self):
|
|
async with Sandbox.ephemeral(self._IMAGE, local=True) as sb:
|
|
w, h = await sb.screen.size()
|
|
await sb.mouse.right_click(w // 2, h // 2)
|
|
|
|
async def test_double_click(self):
|
|
async with Sandbox.ephemeral(self._IMAGE, local=True) as sb:
|
|
w, h = await sb.screen.size()
|
|
await sb.mouse.double_click(w // 2, h // 2)
|
|
|
|
async def test_move(self):
|
|
async with Sandbox.ephemeral(self._IMAGE, local=True) as sb:
|
|
await sb.mouse.move(100, 100)
|
|
|
|
async def test_scroll(self):
|
|
async with Sandbox.ephemeral(self._IMAGE, local=True) as sb:
|
|
w, h = await sb.screen.size()
|
|
await sb.mouse.scroll(w // 2, h // 2, scroll_y=3)
|
|
|
|
async def test_mouse_down_and_up(self):
|
|
async with Sandbox.ephemeral(self._IMAGE, local=True) as sb:
|
|
w, h = await sb.screen.size()
|
|
await sb.mouse.mouse_down(w // 2, h // 2)
|
|
await sb.mouse.mouse_up(w // 2, h // 2)
|
|
|
|
async def test_drag(self):
|
|
async with Sandbox.ephemeral(self._IMAGE, local=True) as sb:
|
|
w, h = await sb.screen.size()
|
|
await sb.mouse.drag(100, h // 2, 300, h // 2)
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
# Keyboard — Linux VM, macOS, Windows
|
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
class TestKeyboardLinuxVM:
|
|
def setup_method(self):
|
|
skip_if_unsupported(_LINUX_VM)
|
|
|
|
async def test_type(self):
|
|
async with Sandbox.ephemeral(_LINUX_VM, local=True) as sb:
|
|
await sb.keyboard.type("hello") # must not raise
|
|
|
|
async def test_keypress_single(self):
|
|
async with Sandbox.ephemeral(_LINUX_VM, local=True) as sb:
|
|
await sb.keyboard.keypress("enter")
|
|
|
|
async def test_keypress_combination(self):
|
|
async with Sandbox.ephemeral(_LINUX_VM, local=True) as sb:
|
|
await sb.keyboard.keypress(["ctrl", "a"])
|
|
|
|
async def test_keypress_string_list(self):
|
|
async with Sandbox.ephemeral(_LINUX_VM, local=True) as sb:
|
|
await sb.keyboard.keypress(["shift", "tab"])
|
|
|
|
async def test_key_down_and_up(self):
|
|
async with Sandbox.ephemeral(_LINUX_VM, local=True) as sb:
|
|
await sb.keyboard.key_down("shift")
|
|
await sb.keyboard.key_up("shift")
|
|
|
|
async def test_type_unicode(self):
|
|
async with Sandbox.ephemeral(_LINUX_VM, local=True) as sb:
|
|
await sb.keyboard.type("héllo")
|
|
|
|
|
|
class TestKeyboardMacOS:
|
|
_IMAGE = Image.macos()
|
|
|
|
def setup_method(self):
|
|
skip_if_unsupported(self._IMAGE)
|
|
|
|
async def test_type(self):
|
|
async with Sandbox.ephemeral(self._IMAGE, local=True) as sb:
|
|
await sb.keyboard.type("hello macos")
|
|
|
|
async def test_keypress_single(self):
|
|
async with Sandbox.ephemeral(self._IMAGE, local=True) as sb:
|
|
await sb.keyboard.keypress("enter")
|
|
|
|
async def test_keypress_combination(self):
|
|
async with Sandbox.ephemeral(self._IMAGE, local=True) as sb:
|
|
await sb.keyboard.keypress(["cmd", "a"])
|
|
|
|
async def test_key_down_and_up(self):
|
|
async with Sandbox.ephemeral(self._IMAGE, local=True) as sb:
|
|
await sb.keyboard.key_down("shift")
|
|
await sb.keyboard.key_up("shift")
|
|
|
|
|
|
class TestKeyboardWindows:
|
|
_IMAGE = Image.windows()
|
|
|
|
def setup_method(self):
|
|
skip_if_unsupported(self._IMAGE)
|
|
|
|
async def test_type(self):
|
|
async with Sandbox.ephemeral(self._IMAGE, local=True) as sb:
|
|
await sb.keyboard.type("hello windows")
|
|
|
|
async def test_keypress_single(self):
|
|
async with Sandbox.ephemeral(self._IMAGE, local=True) as sb:
|
|
await sb.keyboard.keypress("enter")
|
|
|
|
async def test_keypress_combination(self):
|
|
async with Sandbox.ephemeral(self._IMAGE, local=True) as sb:
|
|
await sb.keyboard.keypress(["ctrl", "a"])
|
|
|
|
async def test_key_down_and_up(self):
|
|
async with Sandbox.ephemeral(self._IMAGE, local=True) as sb:
|
|
await sb.keyboard.key_down("shift")
|
|
await sb.keyboard.key_up("shift")
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
# Tunnel — Linux container, macOS, Windows
|
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
class TestTunnelLinux:
|
|
_IMAGE = Image.linux()
|
|
|
|
def setup_method(self):
|
|
skip_if_unsupported(self._IMAGE)
|
|
|
|
async def test_forward_single_context_manager(self):
|
|
async with Sandbox.ephemeral(self._IMAGE, local=True) as sb:
|
|
await _start_http_server(sb, 8888)
|
|
async with sb.tunnel.forward(8888) as t:
|
|
assert t.host
|
|
assert t.port > 0
|
|
assert t.sandbox_port == 8888
|
|
assert t.url.startswith("http://")
|
|
async with httpx.AsyncClient() as client:
|
|
r = await client.get(t.url, timeout=5)
|
|
assert r.status_code == 200
|
|
|
|
async def test_tunnel_info_repr(self):
|
|
async with Sandbox.ephemeral(self._IMAGE, local=True) as sb:
|
|
await _start_http_server(sb, 8889)
|
|
async with sb.tunnel.forward(8889) as t:
|
|
assert "8889" in repr(t)
|
|
|
|
async def test_forward_multiple_ports(self):
|
|
async with Sandbox.ephemeral(self._IMAGE, local=True) as sb:
|
|
await _start_http_server(sb, 8890)
|
|
await _start_http_server(sb, 8891)
|
|
async with sb.tunnel.forward(8890, 8891) as tunnels:
|
|
assert 8890 in tunnels
|
|
assert 8891 in tunnels
|
|
async with httpx.AsyncClient() as client:
|
|
for t in tunnels.values():
|
|
r = await client.get(t.url, timeout=5)
|
|
assert r.status_code == 200
|
|
|
|
async def test_forward_await_form(self):
|
|
async with Sandbox.ephemeral(self._IMAGE, local=True) as sb:
|
|
await _start_http_server(sb, 8892)
|
|
t = await sb.tunnel.forward(8892)
|
|
try:
|
|
async with httpx.AsyncClient() as client:
|
|
r = await client.get(t.url, timeout=5)
|
|
assert r.status_code == 200
|
|
finally:
|
|
await t.close()
|
|
|
|
async def test_forward_no_ports_raises(self):
|
|
async with Sandbox.ephemeral(self._IMAGE, local=True) as sb:
|
|
with pytest.raises(ValueError):
|
|
sb.tunnel.forward()
|
|
|
|
|
|
class TestTunnelMacOS:
|
|
_IMAGE = Image.macos()
|
|
|
|
def setup_method(self):
|
|
skip_if_unsupported(self._IMAGE)
|
|
|
|
async def test_forward_single(self):
|
|
async with Sandbox.ephemeral(self._IMAGE, local=True) as sb:
|
|
await _start_http_server(sb, 8888)
|
|
async with sb.tunnel.forward(8888) as t:
|
|
assert t.sandbox_port == 8888
|
|
async with httpx.AsyncClient() as client:
|
|
r = await client.get(t.url, timeout=5)
|
|
assert r.status_code == 200
|
|
|
|
|
|
class TestTunnelWindows:
|
|
_IMAGE = Image.windows()
|
|
|
|
def setup_method(self):
|
|
skip_if_unsupported(self._IMAGE)
|
|
|
|
async def test_forward_single(self):
|
|
async with Sandbox.ephemeral(self._IMAGE, local=True) as sb:
|
|
await _start_http_server(sb, 8888)
|
|
async with sb.tunnel.forward(8888) as t:
|
|
async with httpx.AsyncClient() as client:
|
|
r = await client.get(t.url, timeout=5)
|
|
assert r.status_code == 200
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
# Terminal (PTY) — Linux container, macOS, Windows
|
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
class TestTerminalLinux:
|
|
_IMAGE = Image.linux()
|
|
|
|
def setup_method(self):
|
|
skip_if_unsupported(self._IMAGE)
|
|
|
|
async def test_create_returns_pid(self):
|
|
async with Sandbox.ephemeral(self._IMAGE, local=True) as sb:
|
|
info = await sb.terminal.create()
|
|
assert "pid" in info and isinstance(info["pid"], int)
|
|
await sb.terminal.close(info["pid"])
|
|
|
|
async def test_create_custom_shell(self):
|
|
async with Sandbox.ephemeral(self._IMAGE, local=True) as sb:
|
|
info = await sb.terminal.create(shell="sh")
|
|
assert info["pid"] > 0
|
|
await sb.terminal.close(info["pid"])
|
|
|
|
async def test_create_custom_dimensions(self):
|
|
async with Sandbox.ephemeral(self._IMAGE, local=True) as sb:
|
|
info = await sb.terminal.create(cols=120, rows=40)
|
|
assert info.get("cols") == 120
|
|
assert info.get("rows") == 40
|
|
await sb.terminal.close(info["pid"])
|
|
|
|
async def test_send_input(self):
|
|
async with Sandbox.ephemeral(self._IMAGE, local=True) as sb:
|
|
info = await sb.terminal.create()
|
|
await sb.terminal.send_input(info["pid"], "echo hello\n")
|
|
await asyncio.sleep(0.3)
|
|
await sb.terminal.close(info["pid"])
|
|
|
|
async def test_resize(self):
|
|
async with Sandbox.ephemeral(self._IMAGE, local=True) as sb:
|
|
info = await sb.terminal.create(cols=80, rows=24)
|
|
await sb.terminal.resize(info["pid"], cols=160, rows=48)
|
|
await sb.terminal.close(info["pid"])
|
|
|
|
async def test_close_returns_exit_code_or_none(self):
|
|
async with Sandbox.ephemeral(self._IMAGE, local=True) as sb:
|
|
info = await sb.terminal.create()
|
|
await sb.terminal.send_input(info["pid"], "exit 0\n")
|
|
await asyncio.sleep(0.3)
|
|
result = await sb.terminal.close(info["pid"])
|
|
assert result is None or isinstance(result, int)
|
|
|
|
async def test_multiple_sessions_independent(self):
|
|
async with Sandbox.ephemeral(self._IMAGE, local=True) as sb:
|
|
a = await sb.terminal.create()
|
|
b = await sb.terminal.create()
|
|
assert a["pid"] != b["pid"]
|
|
await sb.terminal.close(a["pid"])
|
|
await sb.terminal.close(b["pid"])
|
|
|
|
|
|
class TestTerminalMacOS:
|
|
_IMAGE = Image.macos()
|
|
|
|
def setup_method(self):
|
|
skip_if_unsupported(self._IMAGE)
|
|
|
|
async def test_create_and_close(self):
|
|
async with Sandbox.ephemeral(self._IMAGE, local=True) as sb:
|
|
info = await sb.terminal.create()
|
|
assert info["pid"] > 0
|
|
await sb.terminal.send_input(info["pid"], "echo hi\n")
|
|
await asyncio.sleep(0.3)
|
|
await sb.terminal.close(info["pid"])
|
|
|
|
async def test_resize(self):
|
|
async with Sandbox.ephemeral(self._IMAGE, local=True) as sb:
|
|
info = await sb.terminal.create(cols=80, rows=24)
|
|
await sb.terminal.resize(info["pid"], 120, 40)
|
|
await sb.terminal.close(info["pid"])
|
|
|
|
|
|
class TestTerminalWindows:
|
|
_IMAGE = Image.windows()
|
|
|
|
def setup_method(self):
|
|
skip_if_unsupported(self._IMAGE)
|
|
|
|
async def test_create_and_close(self):
|
|
async with Sandbox.ephemeral(self._IMAGE, local=True) as sb:
|
|
info = await sb.terminal.create(shell="cmd.exe")
|
|
assert info["pid"] > 0
|
|
await sb.terminal.close(info["pid"])
|
|
|
|
async def test_send_input(self):
|
|
async with Sandbox.ephemeral(self._IMAGE, local=True) as sb:
|
|
info = await sb.terminal.create(shell="cmd.exe")
|
|
await sb.terminal.send_input(info["pid"], "echo hello\r\n")
|
|
await asyncio.sleep(0.5)
|
|
await sb.terminal.close(info["pid"])
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
# Window — Linux VM, macOS, Windows
|
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
class TestWindowLinuxVM:
|
|
def setup_method(self):
|
|
skip_if_unsupported(_LINUX_VM)
|
|
|
|
async def test_get_active_title_returns_string(self):
|
|
async with Sandbox.ephemeral(_LINUX_VM, local=True) as sb:
|
|
title = await sb.window.get_active_title()
|
|
assert isinstance(title, str)
|
|
|
|
|
|
class TestWindowMacOS:
|
|
_IMAGE = Image.macos()
|
|
|
|
def setup_method(self):
|
|
skip_if_unsupported(self._IMAGE)
|
|
|
|
async def test_get_active_title_returns_string(self):
|
|
async with Sandbox.ephemeral(self._IMAGE, local=True) as sb:
|
|
title = await sb.window.get_active_title()
|
|
assert isinstance(title, str)
|
|
|
|
|
|
class TestWindowWindows:
|
|
_IMAGE = Image.windows()
|
|
|
|
def setup_method(self):
|
|
skip_if_unsupported(self._IMAGE)
|
|
|
|
async def test_get_active_title_returns_string(self):
|
|
async with Sandbox.ephemeral(self._IMAGE, local=True) as sb:
|
|
title = await sb.window.get_active_title()
|
|
assert isinstance(title, str)
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
# Mobile — Android only
|
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
class TestMobileAndroid:
|
|
_IMAGE = Image.android()
|
|
|
|
def setup_method(self):
|
|
skip_if_unsupported(self._IMAGE)
|
|
|
|
async def _sb(self):
|
|
return Sandbox.ephemeral(self._IMAGE, local=True)
|
|
|
|
# Touch
|
|
async def test_tap(self):
|
|
async with Sandbox.ephemeral(self._IMAGE, local=True) as sb:
|
|
w, h = await sb.screen.size()
|
|
await sb.mobile.tap(w // 2, h // 2)
|
|
|
|
async def test_long_press(self):
|
|
async with Sandbox.ephemeral(self._IMAGE, local=True) as sb:
|
|
w, h = await sb.screen.size()
|
|
await sb.mobile.long_press(w // 2, h // 2, duration_ms=500)
|
|
|
|
async def test_double_tap(self):
|
|
async with Sandbox.ephemeral(self._IMAGE, local=True) as sb:
|
|
w, h = await sb.screen.size()
|
|
await sb.mobile.double_tap(w // 2, h // 2)
|
|
|
|
async def test_type_text(self):
|
|
async with Sandbox.ephemeral(self._IMAGE, local=True) as sb:
|
|
await sb.mobile.type_text("hello")
|
|
|
|
# Gestures
|
|
async def test_swipe(self):
|
|
async with Sandbox.ephemeral(self._IMAGE, local=True) as sb:
|
|
w, h = await sb.screen.size()
|
|
await sb.mobile.swipe(w // 2, h * 3 // 4, w // 2, h // 4)
|
|
|
|
async def test_scroll_up(self):
|
|
async with Sandbox.ephemeral(self._IMAGE, local=True) as sb:
|
|
w, h = await sb.screen.size()
|
|
await sb.mobile.scroll_up(w // 2, h // 2)
|
|
|
|
async def test_scroll_down(self):
|
|
async with Sandbox.ephemeral(self._IMAGE, local=True) as sb:
|
|
w, h = await sb.screen.size()
|
|
await sb.mobile.scroll_down(w // 2, h // 2)
|
|
|
|
async def test_scroll_left(self):
|
|
async with Sandbox.ephemeral(self._IMAGE, local=True) as sb:
|
|
w, h = await sb.screen.size()
|
|
await sb.mobile.scroll_left(w // 2, h // 2)
|
|
|
|
async def test_scroll_right(self):
|
|
async with Sandbox.ephemeral(self._IMAGE, local=True) as sb:
|
|
w, h = await sb.screen.size()
|
|
await sb.mobile.scroll_right(w // 2, h // 2)
|
|
|
|
async def test_fling(self):
|
|
async with Sandbox.ephemeral(self._IMAGE, local=True) as sb:
|
|
w, h = await sb.screen.size()
|
|
await sb.mobile.fling(w // 2, h * 3 // 4, w // 2, h // 4)
|
|
|
|
async def test_pinch_in(self):
|
|
async with Sandbox.ephemeral(self._IMAGE, local=True) as sb:
|
|
w, h = await sb.screen.size()
|
|
await sb.mobile.pinch_in(w // 2, h // 2, spread=200)
|
|
|
|
async def test_pinch_out(self):
|
|
async with Sandbox.ephemeral(self._IMAGE, local=True) as sb:
|
|
w, h = await sb.screen.size()
|
|
await sb.mobile.pinch_out(w // 2, h // 2, spread=200)
|
|
|
|
async def test_gesture_two_finger(self):
|
|
async with Sandbox.ephemeral(self._IMAGE, local=True) as sb:
|
|
w, h = await sb.screen.size()
|
|
cx, cy = w // 2, h // 2
|
|
await sb.mobile.gesture(
|
|
(cx - 20, cy),
|
|
(cx - 200, cy),
|
|
(cx + 20, cy),
|
|
(cx + 200, cy),
|
|
)
|
|
|
|
async def test_gesture_invalid_raises(self):
|
|
async with Sandbox.ephemeral(self._IMAGE, local=True) as sb:
|
|
with pytest.raises(ValueError):
|
|
await sb.mobile.gesture((100, 100), (200, 200), (300, 300))
|
|
|
|
# Hardware keys
|
|
async def test_key_home(self):
|
|
async with Sandbox.ephemeral(self._IMAGE, local=True) as sb:
|
|
await sb.mobile.home()
|
|
|
|
async def test_key_back(self):
|
|
async with Sandbox.ephemeral(self._IMAGE, local=True) as sb:
|
|
await sb.mobile.back()
|
|
|
|
async def test_key_recents(self):
|
|
async with Sandbox.ephemeral(self._IMAGE, local=True) as sb:
|
|
await sb.mobile.recents()
|
|
|
|
async def test_key_volume_up(self):
|
|
async with Sandbox.ephemeral(self._IMAGE, local=True) as sb:
|
|
await sb.mobile.volume_up()
|
|
|
|
async def test_key_volume_down(self):
|
|
async with Sandbox.ephemeral(self._IMAGE, local=True) as sb:
|
|
await sb.mobile.volume_down()
|
|
|
|
async def test_key_enter(self):
|
|
async with Sandbox.ephemeral(self._IMAGE, local=True) as sb:
|
|
await sb.mobile.enter()
|
|
|
|
async def test_key_backspace(self):
|
|
async with Sandbox.ephemeral(self._IMAGE, local=True) as sb:
|
|
await sb.mobile.backspace()
|
|
|
|
async def test_key_power(self):
|
|
async with Sandbox.ephemeral(self._IMAGE, local=True) as sb:
|
|
await sb.mobile.power()
|
|
|
|
async def test_key_arbitrary(self):
|
|
async with Sandbox.ephemeral(self._IMAGE, local=True) as sb:
|
|
await sb.mobile.key(3) # HOME keycode
|
|
|
|
# System
|
|
async def test_wake(self):
|
|
async with Sandbox.ephemeral(self._IMAGE, local=True) as sb:
|
|
await sb.mobile.wake()
|
|
|
|
async def test_notifications(self):
|
|
async with Sandbox.ephemeral(self._IMAGE, local=True) as sb:
|
|
await sb.mobile.notifications()
|
|
|
|
async def test_close_notifications(self):
|
|
async with Sandbox.ephemeral(self._IMAGE, local=True) as sb:
|
|
await sb.mobile.close_notifications()
|