import base64 from io import BytesIO from pathlib import Path from fastapi.testclient import TestClient from PIL import Image from app.config.store import ConfigStore from app.demo_library import DEMO_ANIMATION_ID, DEMO_ANIMATION_NAME from app.main import create_app, static_content_version from app.network.manager import MockNetworkManager from app.power.voltage import VoltageMonitor def solid_scene(color=(12, 34, 56)): return { "version": 1, "width": 64, "height": 64, "pixelRgb": base64.b64encode(bytes(color) * (64 * 64)).decode("ascii"), "elements": [], } def test_status_config_and_display_api(tmp_path): client = TestClient(create_app(project_root=tmp_path, driver_kind="mock")) status = client.get("/api/status") assert status.status_code == 200 assert status.json()["screen"]["driver"] == "mock" assert status.json()["screen"]["driver_options"]["rows"] == 64 assert status.json()["screen"]["driver_options"]["scan_rows"] == 32 assert status.json()["screen"]["driver_options"]["row_address_bits"] == 5 assert status.json()["screen"]["driver_options"]["pwm_bits"] == 7 assert status.json()["screen"]["hardware_mapping"] == "walnutpi-pi-bank-pwm-oe-v2" assert status.json()["screen"]["driver_options"]["limit_refresh_rate_hz"] == 100 assert status.json()["screen"]["driver_status"]["actual_refresh_rate_hz"] is None assert status.json()["screen"]["driver_status"]["panel_scan_rate_hz"] is None assert status.json()["screen"]["driver_status"]["completed_scans"] is None assert status.json()["screen"]["driver_status"]["scans_per_frame"] is None assert status.json()["power"]["screen_input_voltage"]["status"] == "starting" assert status.json()["state"]["animation_playback"] == { "active": False, "session_id": None, "animation_id": None, "animation_revision": None, "frame_id": None, "frame_index": None, "frame_count": None, "position_ms": None, "total_duration_ms": None, "paused": None, "speed": None, "supported_speeds": [0.5, 1.0, 1.5, 2.0], } assert status.json()["power"]["low_voltage_protection"] == { "enabled": False, "mode": "disabled", "brightness_limit_percent": None, "reading_stale": False, "revision": 0, "sampling_interval_seconds": 5.0, "enforcement_error_code": None, } assert status.json()["resources"] == { "status": "starting", "sampled_at": None, "cpu": { "application_percent": None, "total_percent": None, "logical_cpu_count": None, "application_core_equivalent": None, "cores_percent": [], }, "memory": { "application_bytes": None, "application_percent": None, "total_percent": None, }, "error_code": None, } assert status.json()["ui"]["preview_refresh_interval_ms"] == 1000 assert status.json()["system"]["performance_mode"] == { "requested": False, "available": False, "effective": False, "current_governors": {}, "restore_governors": {}, "last_error": "当前内核未提供 CPU 调频策略;请检查候选内核是否回退", } assert status.json()["state"]["display_test"] == { "active": False, "color": None, "brightness": None, } config = client.put( "/api/config", json={ "orientation": 180, "brightness": 30, "preview_refresh_interval_ms": 99, "matrix_refresh_rate_limit_hz": 80, "custom_test_color": "#a1b2c3", "workspace_order": ["templates", "device", "settings", "canvas", "text", "animations"], "animation_preview_max_concurrent": 50, }, ) assert config.status_code == 200 assert config.json()["orientation"] == 180 assert config.json()["preview_refresh_interval_ms"] == 99 assert config.json()["matrix_refresh_rate_limit_hz"] == 80 assert config.json()["custom_test_color"] == "#A1B2C3" assert config.json()["workspace_order"] == [ "templates", "device", "settings", "canvas", "text", "animations", ] assert client.get("/api/config").json()["preview_refresh_interval_ms"] == 99 assert client.get("/api/config").json()["low_voltage_protection_enabled"] is False assert client.get("/api/status").json()["ui"]["preview_refresh_interval_ms"] == 99 assert client.get("/api/status").json()["screen"]["driver_options"][ "limit_refresh_rate_hz" ] == 80 assert "schema_version" not in config.json() assert config.json()["performance_mode_enabled"] is False assert config.json()["animation_preview_max_concurrent"] == 50 homepage = client.get("/") assert 'id="topbar-cpu"' in homepage.text assert 'id="topbar-memory"' in homepage.text assert 'id="topbar-low-voltage-warning"' in homepage.text assert 'id="low-voltage-protection-enabled"' in homepage.text assert 'id="brightness-protection-status"' in homepage.text assert 'id="resource-details-dialog"' in homepage.text assert 'id="matrix-refresh-rate-grid"' in homepage.text assert homepage.text.count("data-matrix-refresh-rate=") == 7 assert 'aria-controls="resource-details-dialog"' in homepage.text assert "资源占用详情" in homepage.text assert ( homepage.text.index('id="topbar-cpu"') < homepage.text.index('id="topbar-memory"') < homepage.text.index('id="topbar-low-voltage-warning"') < homepage.text.index('id="topbar-voltage"') ) for value in (0, 60001): assert client.put( "/api/config", json={"preview_refresh_interval_ms": value}, ).status_code == 400 for value in (True, 1.5, "100"): assert client.put( "/api/config", json={"preview_refresh_interval_ms": value}, ).status_code == 422 for value in (1, 2, 50): response = client.put( "/api/config", json={"animation_preview_max_concurrent": value}, ) assert response.status_code == 200 assert response.json()["animation_preview_max_concurrent"] == value for value in (0, 51): assert client.put( "/api/config", json={"animation_preview_max_concurrent": value}, ).status_code == 400 for value in (None, True, 2.0, "2", [], {}): assert client.put( "/api/config", json={"animation_preview_max_concurrent": value}, ).status_code == 422 for value in (None, 0, 1, "true", [], {}): assert client.put( "/api/config", json={"low_voltage_protection_enabled": value}, ).status_code == 422 assert client.get("/api/config").json()["low_voltage_protection_enabled"] is False for value in (15, 20, 30, 45, 60, 80, 100): response = client.put( "/api/config", json={"matrix_refresh_rate_limit_hz": value}, ) assert response.status_code == 200 assert response.json()["matrix_refresh_rate_limit_hz"] == value assert client.get("/api/status").json()["screen"]["driver_options"][ "limit_refresh_rate_hz" ] == value for value in (0, 14, 16, 101): assert client.put( "/api/config", json={"matrix_refresh_rate_limit_hz": value}, ).status_code == 400 for value in (True, 60.0, "60", [], {}): assert client.put( "/api/config", json={"matrix_refresh_rate_limit_hz": value}, ).status_code == 422 for value in (None, True, 123, "red", "#12345"): response = client.put("/api/config", json={"custom_test_color": value}) assert response.status_code in {400, 422} saved_workspace_order = client.get("/api/config").json()["workspace_order"] for value in (None, [], ["device", "device"], ["bad id"], [1, "device"], "device"): response = client.put("/api/config", json={"workspace_order": value}) assert response.status_code in {400, 422} assert client.get("/api/config").json()["workspace_order"] == saved_workspace_order legacy_config = client.put("/api/config", json={"default_font": "default", "default_text_size": 14}) assert legacy_config.status_code == 200 assert legacy_config.json()["default_text_size"] == 14 fill = client.post("/api/display/fill", json={"color": "#00FF00"}) assert fill.status_code == 200 assert fill.json() == {"ok": True, "mode": "fill"} text = client.post("/api/display/text", json={"text": "OK", "size": 12, "x": 4, "y": 24}) assert text.status_code == 200 assert text.json()["mode"] == "text" diagnostic = client.post("/api/display/diagnostic", json={"mode": "corners_lines"}) assert diagnostic.status_code == 200 assert diagnostic.json() == {"ok": True, "mode": "diagnostic:corners_lines"} def test_display_test_api_is_temporary_persistent_across_reads_and_restorable(tmp_path): app = create_app(project_root=tmp_path, driver_kind="mock") client = TestClient(app) assert client.put("/api/config", json={"brightness": 31}).status_code == 200 assert client.post("/api/display/fill", json={"color": "#010203"}).status_code == 200 started = client.post("/api/display/test/fill", json={"color": "#FF0000"}) assert started.status_code == 200 assert started.json()["display_test"] == { "active": True, "color": "#FF0000", "brightness": 50, } assert started.json()["effective_brightness"] == 50 assert client.get("/api/config").json()["brightness"] == 31 assert client.get("/api/status").json()["state"]["display_test"]["active"] is True changed = client.put("/api/display/test/brightness", json={"brightness": 72}) assert changed.status_code == 200 assert changed.json()["display_test"]["brightness"] == 72 switched = client.post("/api/display/test/fill", json={"color": "#0000FF"}) assert switched.json()["display_test"]["brightness"] == 72 exited = client.delete("/api/display/test") assert exited.status_code == 200 assert exited.json()["display_test"]["active"] is False assert client.delete("/api/display/test").status_code == 200 final = client.get("/api/status").json()["state"] assert final["mode"] == "fill" assert final["brightness"] == 31 assert final["effective_brightness"] == 31 restored = Image.open(BytesIO(client.get("/api/display/current-frame").content)) assert restored.getpixel((0, 0)) == (1, 2, 3) assert client.put("/api/display/test/brightness", json={"brightness": 50}).status_code == 409 assert client.put("/api/display/test/brightness", json={"brightness": True}).status_code == 422 assert client.post("/api/display/test/fill", json={"color": "bad"}).status_code == 400 def test_device_storage_api_and_successful_persistent_write_invalidate_cache(tmp_path): class FakeStorageMonitor: def __init__(self): self.invalidations = 0 def get_status(self): return { "sampled_at": "2026-07-29T00:00:00Z", "device_total_bytes": 1000, "device_free_bytes": 700, "software_bytes": 200, "templates_bytes": 50, } def invalidate(self): self.invalidations += 1 fake = FakeStorageMonitor() app = create_app( project_root=tmp_path, driver_kind="mock", storage_monitor_factory=lambda _program, _data: fake, ) client = TestClient(app) response = client.get("/api/device-storage") assert response.status_code == 200 assert response.json()["software_bytes"] == 200 assert response.headers["cache-control"] == "no-store" assert fake.invalidations == 0 assert client.put("/api/config", json={"custom_test_color": "#112233"}).status_code == 200 assert fake.invalidations == 1 assert client.post("/api/display/test/fill", json={"color": "#112233"}).status_code == 200 assert client.put("/api/display/test/brightness", json={"brightness": 45}).status_code == 200 assert fake.invalidations == 1 def test_current_display_frame_api_is_logical_and_side_effect_free(tmp_path): app = create_app(project_root=tmp_path, driver_kind="mock") client = TestClient(app) frame = bytearray([0, 0, 0] * (64 * 64)) frame[(7 * 64 + 2) * 3:(7 * 64 + 2) * 3 + 3] = bytes((12, 34, 56)) app.state.display_service.show_rgb_bytes(bytes(frame), mode="composition") app.state.display_service.set_orientation(270) before = client.get("/api/status").json()["state"] response = client.get("/api/display/current-frame") image = Image.open(BytesIO(response.content)) assert response.status_code == 200 assert response.headers["content-type"] == "image/png" assert response.headers["cache-control"] == "no-store" assert image.mode == "RGB" assert image.size == (64, 64) assert image.getpixel((2, 7)) == (12, 34, 56) assert client.get("/api/status").json()["state"] == before def test_animation_playback_api_controls_frame_and_rejects_invalid_or_stale_sessions(tmp_path): app = create_app(project_root=tmp_path, driver_kind="mock") with TestClient(app) as client: animation = client.post("/api/animations", json={"name": "播放控制"}).json() first = client.post( f"/api/animations/{animation['id']}/frames", headers={"If-Match": f'"{animation["revision"]}"'}, json={"scene": solid_scene((255, 0, 0)), "duration_ms": 200}, ).json() second = client.post( f"/api/animations/{animation['id']}/frames", headers={"If-Match": f'"{first["revision"]}"'}, json={"scene": solid_scene((0, 255, 0)), "duration_ms": 300}, ).json() played = client.post( f"/api/animations/{animation['id']}/play", headers={"If-Match": f'"{second["revision"]}"'}, ) assert played.status_code == 200 playback = client.get("/api/status").json()["state"]["animation_playback"] session_id = playback["session_id"] assert playback["frame_count"] == 2 assert playback["total_duration_ms"] == 500 controlled = client.patch( "/api/display/animation-playback", json={ "session_id": session_id, "position_ms": 250, "paused": True, "speed": 1.5, }, ) assert controlled.status_code == 200 assert controlled.json()["state_revision"] >= 1 result = controlled.json()["animation_playback"] assert result["position_ms"] == 250 assert result["frame_index"] == 2 assert result["paused"] is True assert result["speed"] == 1.5 frame = Image.open(BytesIO(client.get("/api/display/current-frame").content)) assert frame.getpixel((0, 0)) == (0, 255, 0) invalid_payloads = [ {"session_id": session_id}, {"session_id": session_id, "position_ms": 500}, {"session_id": session_id, "position_ms": True}, {"session_id": session_id, "paused": 1}, {"session_id": session_id, "speed": 1.25}, {"session_id": session_id, "speed": "2"}, {"session_id": session_id, "paused": None}, {"session_id": session_id, "paused": True, "unknown": 1}, ] for payload in invalid_payloads: assert client.patch("/api/display/animation-playback", json=payload).status_code == 422 replayed = client.post( f"/api/animations/{animation['id']}/play", headers={"If-Match": f'"{second["revision"]}"'}, ) assert replayed.status_code == 200 reset = client.get("/api/status").json()["state"]["animation_playback"] assert reset["session_id"] != session_id assert reset["position_ms"] < 100 assert reset["paused"] is False assert reset["speed"] == 1.0 stale = client.patch( "/api/display/animation-playback", json={"session_id": session_id, "paused": True}, ) assert stale.status_code == 409 def test_create_app_explicit_project_root_ignores_storage_environment( tmp_path, monkeypatch, ): environment_data = tmp_path / "environment-data" environment_runtime = tmp_path / "environment-runtime" monkeypatch.setenv("MATRIX_DATA_DIR", str(environment_data)) monkeypatch.setenv("STATE_DIRECTORY", str(tmp_path / "state-data")) monkeypatch.setenv("MATRIX_RUNTIME_DIR", str(environment_runtime)) monkeypatch.setenv("RUNTIME_DIRECTORY", str(tmp_path / "systemd-runtime")) explicit = tmp_path / "explicit" app = create_app(project_root=explicit, driver_kind="mock") assert app.state.config_store.data_dir == explicit / "data" assert app.state.config_store.runtime_dir == explicit / "data" / "runtime" assert not environment_data.exists() assert not environment_runtime.exists() def test_consecutive_brightness_updates_keep_config_status_and_driver_in_sync(tmp_path): app = create_app(project_root=tmp_path, driver_kind="mock") client = TestClient(app) for value in (20, 55, 37): response = client.put("/api/config", json={"brightness": value}) assert response.status_code == 200 assert response.json()["brightness"] == value status = client.get("/api/status") assert status.status_code == 200 assert status.json()["state"]["brightness"] == 37 assert status.json()["screen"]["driver_options"]["brightness"] == 37 assert app.state.config_store.config["brightness"] == 37 def test_voltage_status_calibration_and_reset_do_not_change_display_state(tmp_path): class Device: def __enter__(self): return self def __exit__(self, exc_type, exc, traceback): return None def configure(self): return None def read_fresh(self): return 13115, 0x0C app = create_app( project_root=tmp_path, driver_kind="mock", voltage_monitor_factory=lambda store: VoltageMonitor(store, device_factory=Device), ) client = TestClient(app) app.state.voltage_monitor.sample_now() state_before = client.get("/api/status").json()["state"] invalid = client.post( "/api/power/voltage/calibration/preview", json={"reference_volts": 4.0}, ) assert invalid.status_code == 400 preview = client.post( "/api/power/voltage/calibration/preview", json={"reference_volts": 5.0}, ) assert preview.status_code == 200 proposal = preview.json() assert proposal["uncalibrated_volts"] > 4.9 assert client.get("/api/config").json()["voltage_calibrated_at"] is None confirmed = client.post( "/api/power/voltage/calibration/confirm", json={"proposal_id": proposal["proposal_id"]}, ) assert confirmed.status_code == 200 assert confirmed.json()["screen_input_voltage"]["calibrated"] is True assert confirmed.json()["config"]["voltage_calibrated_at"].endswith("Z") assert client.post( "/api/power/voltage/calibration/confirm", json={"proposal_id": proposal["proposal_id"]}, ).status_code == 409 reset = client.post("/api/power/voltage/calibration/reset", json={}) assert reset.status_code == 200 assert reset.json()["config"]["voltage_calibration_factor"] == 1.0 assert reset.json()["config"]["voltage_calibrated_at"] is None assert client.get("/api/status").json()["state"] == state_before def test_low_voltage_protection_api_overlays_without_changing_user_state(tmp_path): class MutableDevice: raw = 11777 # Approximately 4.49V with the Unit ADC divider. def __enter__(self): return self def __exit__(self, exc_type, exc, traceback): return None def configure(self): return None def read_fresh(self): return self.raw, 0x0C device = MutableDevice() app = create_app( project_root=tmp_path, driver_kind="mock", voltage_monitor_factory=lambda store: VoltageMonitor( store, device_factory=lambda: device, ), ) client = TestClient(app) enabled = client.put( "/api/config", json={"low_voltage_protection_enabled": True}, ) assert enabled.status_code == 200 assert enabled.json()["low_voltage_protection_enabled"] is True critical = client.get("/api/status").json() assert critical["power"]["low_voltage_protection"]["mode"] == "critical" assert critical["power"]["low_voltage_protection"]["brightness_limit_percent"] == 35 assert critical["power"]["low_voltage_protection"]["sampling_interval_seconds"] == 0.5 assert critical["state"]["mode"] == "low_voltage_indicator" assert critical["state"]["brightness"] == 40 assert critical["state"]["effective_brightness"] == 35 assert critical["state"]["revision"] == 0 indicator = Image.open(BytesIO(client.get("/api/display/current-frame").content)) assert {color for _count, color in indicator.getcolors(maxcolors=4096)} == { (0, 0, 0), (255, 0, 0), } assert client.post("/api/display/fill", json={"color": "#00FF00"}).status_code == 200 assert client.put( "/api/config", json={"orientation": 90, "brightness": 77}, ).status_code == 200 while_critical = client.get("/api/status").json() assert while_critical["state"]["mode"] == "low_voltage_indicator" assert while_critical["state"]["brightness"] == 77 assert while_critical["state"]["effective_brightness"] == 35 assert app.state.config_store.config["last_mode"] == "fill" user_revision = while_critical["state"]["revision"] protection_revision = while_critical["power"]["low_voltage_protection"]["revision"] device.raw = 12853 # Approximately 4.90V. app.state.voltage_monitor.sample_now() first_recovery = client.get("/api/status").json() assert first_recovery["power"]["low_voltage_protection"]["mode"] == "critical" app.state.voltage_monitor.sample_now() recovered = client.get("/api/status").json() assert recovered["power"]["low_voltage_protection"]["mode"] == "inactive" assert recovered["power"]["low_voltage_protection"]["revision"] > protection_revision assert recovered["state"]["revision"] == user_revision assert recovered["state"]["mode"] == "fill" assert recovered["state"]["brightness"] == 77 assert recovered["state"]["effective_brightness"] == 77 restored = Image.open(BytesIO(client.get("/api/display/current-frame").content)) assert {color for _count, color in restored.getcolors(maxcolors=4096)} == { (0, 255, 0), } def test_startup_sample_applies_critical_overlay_before_smile(tmp_path): class CriticalDevice: def __enter__(self): return self def __exit__(self, exc_type, exc, traceback): return None def configure(self): return None def read_fresh(self): return 11777, 0x0C ConfigStore(project_root=tmp_path).update({ "low_voltage_protection_enabled": True, "last_mode": "fill", }) app = create_app( project_root=tmp_path, driver_kind="mock", startup_boot_id="low-voltage-startup", voltage_monitor_factory=lambda store: VoltageMonitor( store, device_factory=CriticalDevice, ), ) with TestClient(app) as client: status = client.get("/api/status").json() assert status["power"]["low_voltage_protection"]["mode"] == "critical" assert status["state"]["mode"] == "low_voltage_indicator" assert status["state"]["effective_brightness"] == 35 assert status["state"]["startup_indicator_active"] is False assert status["state"]["animation_active"] is True assert status["state"]["current_content"] == { "category": "system", "id": None, "name": "低电压提示", } assert app.state.config_store.config["last_mode"] == "fill" def test_bad_color_returns_400(tmp_path): client = TestClient(create_app(project_root=tmp_path, driver_kind="mock")) response = client.post("/api/display/fill", json={"color": "green"}) assert response.status_code == 400 def test_shared_color_palette_api(tmp_path): app = create_app(project_root=tmp_path, driver_kind="mock") app.state.config_store.update({"color_palette": []}) client = TestClient(app) added = client.post("/api/colors/palette", json={"color": "#aabbcc"}) assert added.status_code == 200 assert added.json() == {"colors": ["#AABBCC"], "limit": 24} duplicate = client.post("/api/colors/palette", json={"color": "#AABBCC"}) assert duplicate.status_code == 200 assert duplicate.json()["colors"] == ["#AABBCC"] invalid = client.post("/api/colors/palette", json={"color": "blue"}) assert invalid.status_code == 400 deleted = client.delete("/api/colors/palette/AABBCC") assert deleted.status_code == 200 assert deleted.json()["colors"] == [] assert client.delete("/api/colors/palette/AABBCC").status_code == 200 def test_shared_color_palette_limit(tmp_path): app = create_app(project_root=tmp_path, driver_kind="mock") app.state.config_store.update({"color_palette": []}) client = TestClient(app) for index in range(24): response = client.post("/api/colors/palette", json={"color": f"#{index:06X}"}) assert response.status_code == 200 overflow = client.post("/api/colors/palette", json={"color": "#FFFFFF"}) assert overflow.status_code == 409 def test_canvas_websocket(tmp_path): app = create_app(project_root=tmp_path, driver_kind="mock") client = TestClient(app) legacy_frame = bytes([0, 0, 0]) * (64 * 64) composition_frame = bytes([18, 52, 86]) * (64 * 64) with client.websocket_connect("/ws/canvas") as websocket: websocket.send_json({ "type": "frame_rgb", "width": 64, "height": 64, "encoding": "base64_rgb888", "data": base64.b64encode(legacy_frame).decode("ascii"), }) assert websocket.receive_json() == {"type": "ack", "mode": "canvas", "applied": True} assert app.state.config_store.config["last_mode"] == "canvas" websocket.send_json({ "type": "frame_rgb", "source": "composition", "width": 64, "height": 64, "encoding": "base64_rgb888", "data": base64.b64encode(composition_frame).decode("ascii"), }) assert websocket.receive_json() == {"type": "ack", "mode": "composition", "applied": True} assert app.state.config_store.config["last_mode"] == "composition" frame_path = tmp_path / "data" / "runtime" / "last_frame.png" frame_before_error = frame_path.read_bytes() websocket.send_json({ "type": "frame_rgb", "source": "canvas", "width": 64, "height": 64, "encoding": "base64_rgb888", "data": base64.b64encode(legacy_frame).decode("ascii"), }) error = websocket.receive_json() assert error["type"] == "error" assert "source" in error["message"] assert app.state.config_store.config["last_mode"] == "composition" assert frame_path.read_bytes() == frame_before_error websocket.send_json({"type": "frame_rgb", "width": 1, "height": 1, "encoding": "base64_rgb888", "data": ""}) assert websocket.receive_json()["type"] == "error" def test_preview_endpoints_return_png_without_display_side_effects(tmp_path): client = TestClient(create_app(project_root=tmp_path, driver_kind="mock")) fill = client.post("/api/display/fill", json={"color": "#123456"}) assert fill.status_code == 200 frame_path = tmp_path / "data" / "runtime" / "last_frame.png" frame_before = frame_path.read_bytes() status_before = client.get("/api/status").json()["state"] text_preview = client.post("/api/preview/text", json={ "text": "UP", "size": 12, "x": 4, "y": 24, "color": "#FFFFFF", "background": "#000000", }) assert text_preview.status_code == 200 assert text_preview.headers["content-type"] == "image/png" text_image = Image.open(BytesIO(text_preview.content)) assert text_image.size == (64, 64) assert text_image.mode == "RGB" text_layer_preview = client.post("/api/preview/text-layer", json={ "text": "TOP", "size": 12, "x": 4, "y": 24, "color": "#FF8000", }) assert text_layer_preview.status_code == 200 assert text_layer_preview.headers["content-type"] == "image/png" text_layer_image = Image.open(BytesIO(text_layer_preview.content)) assert text_layer_image.size == (64, 64) assert text_layer_image.mode == "RGBA" assert text_layer_image.getpixel((63, 63)) == (0, 0, 0, 0) assert text_layer_image.getchannel("A").getbbox() is not None diagnostic_preview = client.post("/api/preview/diagnostic", json={"mode": "corners_lines"}) assert diagnostic_preview.status_code == 200 diagnostic_image = Image.open(BytesIO(diagnostic_preview.content)) assert diagnostic_image.size == (64, 64) assert diagnostic_image.getpixel((0, 0)) == (255, 0, 0) assert client.get("/api/status").json()["state"] == status_before assert frame_path.read_bytes() == frame_before def test_multilingual_text_preview_uses_covered_fonts_and_rejects_emoji(tmp_path): client = TestClient(create_app(project_root=tmp_path, driver_kind="mock")) samples = ("你好", "日本", "한글", "Привет", "Ελλάδα", "مرحبا", "שלום", "สวัสดี") for sample in samples: response = client.post("/api/preview/text-layer", json={ "text": sample, "font": "default", "size": 18, "x": 0, "y": 16, "color": "#FFFFFF", }) assert response.status_code == 200, (sample, response.text) image = Image.open(BytesIO(response.content)) assert image.mode == "RGBA" assert image.getchannel("A").getbbox() is not None unsupported = client.post("/api/preview/text-layer", json={ "text": "😀", "font": "default", "size": 18, "x": 0, "y": 16, "color": "#FFFFFF", }) assert unsupported.status_code == 400 assert "U+1F600" in unsupported.json()["detail"] def test_composition_can_be_reapplied_after_diagnostic_override(tmp_path): app = create_app(project_root=tmp_path, driver_kind="mock") client = TestClient(app) composition_frame = bytearray([0, 0, 0] * (64 * 64)) composition_frame[0:3] = bytes((12, 34, 56)) composition_frame[-3:] = bytes((78, 90, 123)) encoded_frame = base64.b64encode(composition_frame).decode("ascii") with client.websocket_connect("/ws/canvas") as websocket: websocket.send_json({ "type": "frame_rgb", "source": "composition", "width": 64, "height": 64, "encoding": "base64_rgb888", "data": encoded_frame, }) assert websocket.receive_json()["mode"] == "composition" diagnostic = client.post("/api/display/diagnostic", json={"mode": "corners_lines"}) assert diagnostic.status_code == 200 assert app.state.config_store.config["last_mode"] == "diagnostic:corners_lines" diagnostic_image = Image.open(tmp_path / "data" / "runtime" / "last_frame.png") assert diagnostic_image.getpixel((0, 0)) == (255, 0, 0) with client.websocket_connect("/ws/canvas") as websocket: websocket.send_json({ "type": "frame_rgb", "source": "composition", "width": 64, "height": 64, "encoding": "base64_rgb888", "data": encoded_frame, }) assert websocket.receive_json() == { "type": "ack", "mode": "composition", "applied": True, } restored = Image.open(tmp_path / "data" / "runtime" / "last_frame.png") assert restored.tobytes() == bytes(composition_frame) assert app.state.config_store.config["last_mode"] == "composition" def test_mobile_workspace_assets_are_served(tmp_path): app = create_app(project_root=tmp_path, driver_kind="mock") client = TestClient(app) index = client.get("/") assert index.status_code == 200 assert index.headers["cache-control"] == "no-store" assert f'' in index.text assert f'/static/{app.state.app_version}/style.css' in index.text assert f'/static/{app.state.app_version}/app.js' in index.text assert 'id="workspace-drawer"' in index.text assert 'id="workspace-order-toggle"' in index.text assert "自定义项目位置" in index.text assert 'id="workspace-settings"' in index.text assert 'id="topbar-voltage"' in index.text assert 'id="start-voltage-calibration"' in index.text assert 'id="voltage-calibration-dialog"' in index.text assert 'id="board-stage"' in index.text assert 'id="scene-canvas"' in index.text assert 'id="pixel-grid"' in index.text assert 'id="pixel-edit-toggle"' in index.text assert 'id="pixel-edit-status"' in index.text assert 'id="text-element-list"' in index.text assert 'id="create-text"' in index.text assert 'id="workspace-templates"' in index.text assert 'id="workspace-animations"' in index.text assert 'id="animation-frames"' in index.text assert 'id="animation-edit-notice"' in index.text assert 'id="animation-preview-image"' in index.text assert 'id="animation-board-preview"' in index.text assert 'id="animation-dirty-message"' in index.text assert 'id="library-copy-dialog"' in index.text assert 'id="template-grid"' in index.text assert 'id="save-template"' in index.text assert 'id="template-dialog-note"' in index.text assert 'id="animation-apply-all"' not in index.text assert 'id="template-confirm-dialog"' in index.text assert 'id="config-font"' not in index.text assert 'id="text-font"' in index.text assert 'role="combobox"' in index.text assert 'id="import-font"' in index.text assert 'accept=".ttf,.otf,.ttc,.otc"' in index.text assert "选择设备已有字体,或导入 TTF、OTF、TTC、OTC 文件" in index.text assert 'id="text-background"' not in index.text assert 'type="color"' not in index.text assert index.text.count("data-color-picker") == 4 settings = index.text[index.text.index('id="workspace-settings"'):] assert settings.index("