Files
xteink-web-emulator/tests/browser_raw_sd.py
T
evan b22934fe0d Replace vvfat SD with browser-built raw FAT32 from OPFS
Browser SD files are persisted in OPFS (web/sdstore.js) and edited in the
Files tab. At boot the browser builds a deterministic 64 MiB FAT32 image
(web/fat32.js) with nested directories and VFAT long names, then mounts
it as a raw block device, bypassing QEMU's broken populated-vvfat WASM
coroutine path. Guest writes mutate only the ephemeral image.

Exposes a hidden #frame-generation indicator that increments on every
e-ink flush, so automation can wait on real display updates instead of
fixed sleeps. Adds tests/ with a Makefile runner: 'make test' for JS +
FAT32/mtools checks, 'make browser-test' for a headless-Firefox boot of
the official CrossPoint 1.4.1 firmware that navigates to
Test/example.txt. Bumps third_party/qemu to the SPI-SD/CMD13 fixes and
updates README/ROADMAP/ISSUES for the new architecture.
2026-07-21 07:43:07 -04:00

78 lines
2.8 KiB
Python

from pathlib import Path
import time
from selenium import webdriver
from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.common.by import By
from selenium.webdriver.firefox.options import Options
from selenium.webdriver.support.ui import WebDriverWait
ROOT = Path(__file__).resolve().parent.parent
options = Options()
options.add_argument('-headless')
driver = webdriver.Firefox(options=options)
driver.set_page_load_timeout(60)
try:
driver.get('http://127.0.0.1:8180/web/?firmware=/_scratch/crosspoint-reader-1.4.1-release-firmware.bin&variant=x3')
confirm = driver.find_element(By.CSS_SELECTOR, '[data-button="confirm"]')
WebDriverWait(driver, 8 * 60).until(lambda _: confirm.get_attribute('disabled') is None)
indicator = driver.find_element(By.ID, 'frame-generation')
def generation():
return int(indicator.get_attribute('value') or '-1')
def wait_frame(previous, timeout=180):
WebDriverWait(driver, timeout).until(lambda _: generation() > previous)
def wait_stable(quiet=5, timeout=180):
deadline = time.time() + timeout
last = generation()
stable_since = time.time()
while time.time() < deadline:
time.sleep(0.25)
current = generation()
if current != last:
last = current
stable_since = time.time()
elif time.time() - stable_since >= quiet:
return current
raise RuntimeError('display did not settle')
power = driver.find_element(By.CSS_SELECTOR, '[data-button="power"]')
before = generation()
ActionChains(driver).click_and_hold(power).perform()
wait_frame(before)
ActionChains(driver).release(power).perform()
wait_stable()
def press(button):
driver.execute_script('arguments[0].scrollIntoView({block: "center"})', button)
before = wait_stable()
ActionChains(driver).click_and_hold(button).perform()
time.sleep(0.3)
ActionChains(driver).release(button).perform()
wait_frame(before)
wait_stable()
press(confirm) # Browse Files
press(confirm) # Test
press(confirm) # example.txt
time.sleep(60)
status = driver.find_element(By.ID, 'status').text
log = driver.find_element(By.ID, 'log').text
(ROOT / '_scratch/browser-raw-sd.log').write_text(log)
driver.save_screenshot(str(ROOT / '_scratch/browser-raw-sd.png'))
bad = (
'RuntimeError', 'Guru Meditation', "panic'ed", 'Invalid SD card size',
'FS error', 'SPI:', 'SD:', 'Unexpected response to cmd',
)
matched = [marker for marker in bad if marker in log or marker in status]
if matched:
raise RuntimeError(f'emulator failure detected: {matched}')
print(f'browser raw-SD test passed at frame generation {generation()}')
finally:
driver.quit()