Files
saphid--frame-control/tests/e2e/test_device.py
T
saphidandClaude Opus 5.5 0181071b83 Tests against a fake Frame, and a headset smoke test
tests/fakeframe: a container that stands in for the Frame (Arch Linux, or
Arch Linux ARM on arm64) with sshd, rsync, Valve's steamos-devkit-service
and hooks (vendored unmodified), a fake Steam client for the devkit pipe and
the DevTools port (the app's JavaScript runs in Node against stand-in
SteamClient/appStore objects), stubs for steam, wpctl, flatpak, podman,
Lepton and friends, battery and thermal files under /sys, and fault
switches (fakeframe-ctl): pairing mode, approve/deny/timeout, Steam not
running, headset asleep, sshd off, disk full, runtimes missing. A second
container is the computer running Frame Control.

tests/e2e: 29 tests driving the real ui/server.py, frame_connect.py and
frame_titles.py against it; skipped unless FRAME_E2E=1. scripts/e2e.sh
builds, runs and tears down; CI runs it on ubuntu-24.04-arm.

tests/smoke + scripts/frame-smoke.sh: the core cases against a real Frame,
recorded with its BUILD_ID, cleaning up after itself; --pair to pair a
throwaway key. docs/testing.md describes the layers.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-27 17:40:09 +10:00

87 lines
4.5 KiB
Python

"""Status, Steam library, volume, clipboard, Flatpaks and the desktop capture,
through the server against the fake Frame."""
import unittest
import harness
from harness import api, ctl, launches, ok, state, wait_for
harness.require()
class Device(harness.FrameTestCase):
def test_status(self):
s = ok('GET', '/api/status')
self.assertEqual(s['hostname'], 'frame')
self.assertEqual(s['os'], {'version': '0.3.0', 'build': '20260922.6101926', 'variant': 'vr'})
b = s['battery']
self.assertEqual((b['percent'], b['status'], b['health']), (76, 'Charging', 'Good'))
self.assertAlmostEqual(b['watts'], 9.62, places=1)
self.assertAlmostEqual(b['tempC'], 31.2, places=3)
self.assertEqual(s['power'], {'type': 'C PD [PD_PPS]', 'watts': 20.0})
self.assertEqual(s['temp'], 41.5)
self.assertEqual(s['wifi'], {'ssid': 'Fake:Frame Wi-Fi', 'signal': 72})
self.assertEqual(s['volume'], {'level': 0.4, 'muted': False})
self.assertEqual(s['services'], {'steamvr': True, 'desktop': True, 'lepton': False, 'rdp': False})
# Runtimes and Lepton are Steam "apps" too; the status hides them.
self.assertEqual([g['name'] for g in s['games']], ['Beat Saber'])
self.assertIsNotNone(s['disk']['home'])
ctl('battery', 'capacity=15', 'status=Discharging', 'current_now=-900000')
b = ok('GET', '/api/status')['battery']
self.assertEqual((b['percent'], b['status']), (15, 'Discharging'))
self.assertLess(b['watts'], 0)
def test_volume_and_mute(self):
ok('POST', '/api/volume', {'level': 0.55, 'muted': True})
self.assertEqual(state()['volume'], {'level': 0.55, 'muted': True})
self.assertEqual(ok('GET', '/api/status')['volume'], {'level': 0.55, 'muted': True})
status, out, _ = api('POST', '/api/volume', {'level': 2})
self.assertEqual(status, 400, out)
def test_clipboard_goes_to_klipper(self):
text = 'héllo from the e2e tests\nline two, with a trailing newline\n'
out = ok('POST', '/api/clipboard', {'text': text})
# ${#text} counts bytes or characters depending on the session's locale.
self.assertRegex(out['message'], r'^copied via Klipper \(\d+ chars\)$')
self.assertEqual(state()['clipboard'], [text])
def test_flatpak_install_and_remove(self):
ok('POST', '/api/flatpak', {'id': 'org.videolan.VLC', 'action': 'install'})
self.assertEqual([f['id'] for f in ok('GET', '/api/status')['flatpaks']], ['org.videolan.VLC'])
ok('POST', '/api/flatpak', {'id': 'org.videolan.VLC', 'action': 'uninstall'})
self.assertEqual(state()['flatpaks'], [])
status, out, _ = api('POST', '/api/flatpak', {'id': 'org.example.missing', 'action': 'install'})
self.assertEqual(status, 502)
self.assertIn('Nothing matches org.example.missing', out['error'])
def test_desktop_capture(self):
status, png, headers = api('GET', '/api/screenshot')
self.assertEqual(status, 200, png)
self.assertTrue(png.startswith(b'\x89PNG\r\n\x1a\n'))
self.assertEqual(headers.get('X-Capture-Source'), 'gamescope')
def test_steam_library_and_installs(self):
owned = ok('GET', '/api/steam/owned')
self.assertEqual(owned['country'], 'AU')
games = {g['id']: g for g in owned['games']}
self.assertEqual(set(games), {2379780, 274190, 620980})
self.assertEqual((games[2379780]['frame'], games[620980]['installed']), (3, True))
# Balatro queues at once; Broforce stops at the options dialog, which
# frame_steam.py accepts with ContinueInstall().
for appid, name in ((2379780, 'Balatro'), (274190, 'Broforce')):
out = ok('POST', '/api/steam', {'appid': appid, 'action': 'install'})
self.assertEqual(out, {'state': 'downloading', 'message': f'{name} is queued to download on the Frame'})
self.assertEqual(ok('GET', '/api/steam/owned')['download']['appid'], 274190)
def test_launch_and_store_page(self):
ok('POST', '/api/launch', {'appid': 620980})
run = wait_for(lambda: launches('rungameid'), 10, 'the launch to reach Steam')[-1]
self.assertEqual((run['appid'], run['started']), (620980, True))
ok('POST', '/api/steam', {'appid': 1145360, 'action': 'store'})
wait_for(lambda: state()['steam']['pages'], 10, 'the store page')
self.assertEqual(state()['steam']['pages'][0]['title'], 'Hades on Steam')
if __name__ == '__main__':
unittest.main()