Files
trycua--cua/tests/test_interfaces.py
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 13:03:19 +08:00

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()