"""Installing as an app, and the composer's single send/stop button.""" from __future__ import annotations from pathlib import Path from fastapi.testclient import TestClient from lembas.services import settings_store from lembas.web.templating import STATIC_DIR # --- Manifest ---------------------------------------------------------------- def test_the_manifest_is_readable_when_signed_out(client: TestClient): """A browser fetches the manifest outside any page's session.""" response = client.get("/manifest.webmanifest") assert response.status_code == 200 assert response.headers["content-type"].startswith("application/manifest+json") def test_the_manifest_carries_the_instance_name(client: TestClient, db, registered): """The name moved to /admin/customization with the rest of the identity.""" client.post( "/admin/customization/identity", data={"instance_name": "Rivendell", "tagline": ""}, follow_redirects=False, ) assert client.get("/manifest.webmanifest").json()["name"] == "Rivendell" def test_the_manifest_offers_a_maskable_icon(client: TestClient): """Without one, Android crops the corners off the wafer.""" icons = client.get("/manifest.webmanifest").json()["icons"] assert any(icon["purpose"] == "maskable" for icon in icons) assert any(icon["sizes"] == "512x512" and icon["purpose"] == "any" for icon in icons) def test_every_manifest_icon_exists(client: TestClient): for icon in client.get("/manifest.webmanifest").json()["icons"]: assert client.get(icon["src"]).status_code == 200, icon["src"] def test_the_manifest_starts_at_the_chat(client: TestClient): payload = client.get("/manifest.webmanifest").json() assert payload["start_url"] == "/chat" assert payload["scope"] == "/" assert payload["display"] == "standalone" # --- Service worker ---------------------------------------------------------- def test_the_worker_is_served_from_the_root(client: TestClient): """A worker under /static/js/ would have scope /static/js/ and control nothing.""" response = client.get("/sw.js") assert response.status_code == 200 assert response.headers["content-type"].startswith("text/javascript") def test_the_worker_is_never_cached(client: TestClient): """A stale worker keeps serving a stale cache.""" assert "no-store" in client.get("/sw.js").headers["cache-control"] def test_the_worker_leaves_the_api_alone(): """The reply stream, the unread poll and attachment downloads all live under /api/. A cached response on any of them is at best stale.""" source = (STATIC_DIR / "js" / "sw.js").read_text() assert '"/api/"' in source assert "text/event-stream" in source def test_every_precached_asset_exists(client: TestClient): """addAll is all-or-nothing in most implementations, and a missing entry is invisible until someone opens the developer tools.""" source = (STATIC_DIR / "js" / "sw.js").read_text() shell = source.split("var SHELL = [", 1)[1].split("];", 1)[0] paths = [line.strip().strip('",') for line in shell.splitlines() if '"' in line] assert paths for path in paths: assert client.get(path).status_code == 200, path def test_the_offline_page_stands_on_its_own(client: TestClient): """Cached at install time, so it must render with no user and no chats.""" response = client.get("/offline") assert response.status_code == 200 assert 'class="sidebar' not in response.text assert 'id="thread' not in response.text def test_the_page_links_the_manifest_and_the_apple_icon(client: TestClient, registered): page = client.get("/chat").text assert 'rel="manifest"' in page assert 'rel="apple-touch-icon"' in page assert 'name="theme-color"' in page # --- Send and Stop ----------------------------------------------------------- def test_the_hidden_attribute_wins_over_component_styles(): """`.btn` is display: inline-flex, which beats the browser's own `[hidden] { display: none }`. Without this rule a button hidden from JavaScript stays on screen -- which is how Stop came to sit permanently beside Send.""" css = (STATIC_DIR / "css" / "app.css").read_text() assert "[hidden]" in css assert "display: none !important" in css def test_the_composer_has_exactly_one_send_button(client: TestClient, db, registered): """One button that becomes Stop, not two that take turns being hidden.""" _add_a_model(db) page = client.get("/chat").text assert page.count("data-composer-action") == 1 def test_the_send_button_carries_both_icons(client: TestClient, db, registered): """Rendered together and chosen in CSS, so the swap costs no layout and cannot flash an empty button.""" _add_a_model(db) page = client.get("/chat").text assert "composer__icon--send" in page assert "composer__icon--stop" in page def test_the_composer_starts_in_the_send_state(client: TestClient, db, registered): _add_a_model(db) assert 'data-composer-action="send"' in client.get("/chat").text def _add_a_model(db): from lembas.db.models import Connection, Model connection = Connection(name="c", base_url="http://127.0.0.1:1", api_key_encrypted="") db.add(connection) db.commit() db.add(Model(connection_id=connection.id, model_id="m")) db.commit() # --- The icons themselves ---------------------------------------------------- def test_the_generated_icons_are_committed(): """They come from scripts/build_artwork.py and are committed like the SVGs; the running application has no rasteriser.""" for name in ( "icon-192.png", "icon-512.png", "icon-maskable-512.png", "apple-touch-icon-180.png", ): path = Path(STATIC_DIR) / "img" / name assert path.exists(), name assert path.read_bytes()[:8] == b"\x89PNG\r\n\x1a\n", name def test_the_mic_appears_only_when_dictation_is_configured( client: TestClient, db, registered ): _add_a_model(db) assert "data-mic" not in client.get("/chat").text settings_store.update( db, {"stt_enabled": True, "stt_base_url": "http://stt"}, key=settings_store.AUDIO, ) assert "data-mic" in client.get("/chat").text