"""Smoke: the relay serves the app, the app boots, you can sign in, and the kind-1 forum is parked (no forum UI, empty home page, empty notifications).""" import json import os import urllib.error import urllib.request import pytest from conftest import ( ARTIFACTS_DIR, RELAY_URL, TEST_NSEC, TEST_PK, VAULT_PASSWORD, visible, wait_wasm_ready, ) BUNDLES = [ "app.wasm", "relay-proxy.wasm", "store.wasm", "verify.wasm", "profile.wasm", "notif.wasm", "signer.wasm", ] READY_STATUS = ("verify", "store", "rproxy", "signer", "app", "domain", "profile", "notif") class TestStatic: """The relay serves a deployable build out of web/static.""" def test_index(self, relay): html = urllib.request.urlopen(f"{relay['url']}/").read().decode() assert "" in html assert 'id="app-root"' in html def test_bundles(self, relay): for name in BUNDLES: body = urllib.request.urlopen(f"{relay['url']}/{name}").read() assert body[:4] == b"\x00asm", f"{name} is not a wasm module" def test_parked_bundles_not_served(self, relay): """feed.wasm and mls.wasm are park-state: not built, not served.""" for name in ("feed.wasm", "mls.wasm"): try: body = urllib.request.urlopen(f"{relay['url']}/{name}").read() except urllib.error.HTTPError: continue assert body[:4] != b"\x00asm", f"{name} must not be served" @pytest.fixture(scope="session") def app(page): """Sign in through the real signer panel and land in the shell.""" sign_in(page) return page def sign_in(pg): """Create a vault with a known nsec through the signer panel, then sign in. This is the user's path: the login button opens the signer panel, the panel creates the vault and imports the nsec, and the identity's own Login button switches to it and shows the shell. Nothing here reloads the page - a reload depends on the unlocked session being restored first and races it. """ pg.get_by_role("button", name="login").click() pg.wait_for_selector("input[placeholder='nsec1...']", timeout=15000) pg.fill("input[placeholder='Vault password']", VAULT_PASSWORD) pg.fill("input[placeholder='nsec1...']", TEST_NSEC) pg.get_by_role("button", name="create vault with nsec").click() # The identity must be in the vault and the vault on disk before signing # in: the identity add and the vault write are separate async steps. pg.wait_for_function( "pk => window.__musiquay_get_public_key().then(k => k === pk)", arg=TEST_PK, timeout=20000, ) pg.wait_for_function("() => !!localStorage.getItem('musiquay-vault')", timeout=10000) # The identity's own Login button: switch to it and enter the shell. pg.wait_for_selector("text=Identities", timeout=15000) pg.evaluate("""() => { const b = Array.from(document.querySelectorAll('button')) .find(e => (e.textContent || '').trim() === 'Login'); if (b) b.click(); }""") pg.wait_for_selector("#content-area", state="attached", timeout=30000) pg.wait_for_selector("#home-page", timeout=30000) class TestBoot: def test_login_page(self, page): """Before sign-in: the login page is branded and translated.""" body = page.inner_text("body") assert "MUSIQUAY" in body assert "the digital home for the music scene" in body assert "subtitle" not in body, "i18n fell back to the key" def test_boot_status(self, page): """Every worker reports ok, and the feed worker reports parked.""" # The workers report as they come up; the last line lands after the app # is ready, so wait for the record to settle before reading it. page.wait_for_function( """() => { const d = JSON.parse(localStorage.getItem('__musiquay_boot_diag') || '{}').status || {}; return d.feed === 'parked' && ['verify','store','rproxy','signer','app','domain','profile','notif'] .every(k => d[k] === 'ok'); }""", timeout=30000, ) status = json.loads(page.evaluate("() => localStorage.getItem('__musiquay_boot_diag')"))["status"] assert status.get("feed") == "parked" for name in READY_STATUS: assert status.get(name) == "ok", f"{name} = {status.get(name)}" assert page.evaluate("() => document.body.getAttribute('data-wasm-ready')") == "1" def test_no_page_errors(self, page): assert page.boot_errors == [] class TestShell: def test_signed_in(self, app): """Auto-login put the seeded identity in the shell.""" assert visible(app, "#content-area") assert app.evaluate("() => localStorage.getItem('musiquay-pubkey')") == TEST_PK assert app.evaluate("() => document.querySelector('input[placeholder=\"nsec1...\"]') === null") def test_home_page_is_the_main_area(self, app): assert visible(app, "#home-page") for parked in ("#feed-page", "#notif-page", "#compose-page", "#search-page", "#thread-page"): assert not visible(app, parked), f"{parked} must not be shown at the root" def test_no_forum_ui(self, app): """No kind-1 forum surface: no forum text, no feed/compose nav buttons.""" assert app.evaluate("() => document.body.innerText.toLowerCase().includes('forum')") is False labels = app.evaluate( "() => Array.from(document.querySelectorAll('button, a'))" ".map(e => (e.innerText || '').trim().toLowerCase())" ) assert "feed" not in labels assert "compose" not in labels assert app.evaluate("() => document.querySelectorAll('svg').length") > 0, "shell did not render" def test_forum_routes_land_on_home(self, app): """Every forum route collapses onto the empty home page.""" for route in ("/feed", "/feed/follows", "/t/abc123", "/compose", "/search"): app.goto(f"{RELAY_URL}{route}", wait_until="domcontentloaded") wait_wasm_ready(app) app.wait_for_selector("#home-page", timeout=15000) assert visible(app, "#home-page"), f"{route} did not land on home" def test_notifications_empty(self, app): app.goto(f"{RELAY_URL}/notifications", wait_until="domcontentloaded") wait_wasm_ready(app) app.wait_for_selector("#notif-page", timeout=15000) assert visible(app, "#notif-page") rows = app.evaluate( "() => document.querySelectorAll('#notif-page [data-notif-row], #notif-page .notif-row').length" ) assert rows == 0, f"notifications are not empty ({rows} rows)" def test_screenshot(self, app): path = os.path.join(ARTIFACTS_DIR, "musiquay-signed-in.png") app.goto(RELAY_URL, wait_until="domcontentloaded") wait_wasm_ready(app) app.wait_for_selector("#home-page") app.screenshot(path=path, full_page=False) assert os.path.getsize(path) > 5000