1 """Smoke: the relay serves the app, the app boots, you can sign in, and the
2 kind-1 forum is parked (no forum UI, empty home page, empty notifications)."""
3 4 import json
5 import os
6 import urllib.error
7 import urllib.request
8 9 import pytest
10 11 from conftest import (
12 ARTIFACTS_DIR,
13 RELAY_URL,
14 TEST_NSEC,
15 TEST_PK,
16 VAULT_PASSWORD,
17 visible,
18 wait_wasm_ready,
19 )
20 21 BUNDLES = [
22 "app.wasm",
23 "relay-proxy.wasm",
24 "store.wasm",
25 "verify.wasm",
26 "profile.wasm",
27 "notif.wasm",
28 "signer.wasm",
29 ]
30 31 READY_STATUS = ("verify", "store", "rproxy", "signer", "app", "domain", "profile", "notif")
32 33 34 class TestStatic:
35 """The relay serves a deployable build out of web/static."""
36 37 def test_index(self, relay):
38 html = urllib.request.urlopen(f"{relay['url']}/").read().decode()
39 assert "<!DOCTYPE html>" in html
40 assert 'id="app-root"' in html
41 42 def test_bundles(self, relay):
43 for name in BUNDLES:
44 body = urllib.request.urlopen(f"{relay['url']}/{name}").read()
45 assert body[:4] == b"\x00asm", f"{name} is not a wasm module"
46 47 def test_parked_bundles_not_served(self, relay):
48 """feed.wasm and mls.wasm are park-state: not built, not served."""
49 for name in ("feed.wasm", "mls.wasm"):
50 try:
51 body = urllib.request.urlopen(f"{relay['url']}/{name}").read()
52 except urllib.error.HTTPError:
53 continue
54 assert body[:4] != b"\x00asm", f"{name} must not be served"
55 56 57 @pytest.fixture(scope="session")
58 def app(page):
59 """Sign in through the real signer panel and land in the shell."""
60 sign_in(page)
61 return page
62 63 64 def sign_in(pg):
65 """Create a vault with a known nsec through the signer panel, then sign in.
66 67 This is the user's path: the login button opens the signer panel, the panel
68 creates the vault and imports the nsec, and the identity's own Login button
69 switches to it and shows the shell. Nothing here reloads the page - a
70 reload depends on the unlocked session being restored first and races it.
71 """
72 pg.get_by_role("button", name="login").click()
73 pg.wait_for_selector("input[placeholder='nsec1...']", timeout=15000)
74 pg.fill("input[placeholder='Vault password']", VAULT_PASSWORD)
75 pg.fill("input[placeholder='nsec1...']", TEST_NSEC)
76 pg.get_by_role("button", name="create vault with nsec").click()
77 78 # The identity must be in the vault and the vault on disk before signing
79 # in: the identity add and the vault write are separate async steps.
80 pg.wait_for_function(
81 "pk => window.__musiquay_get_public_key().then(k => k === pk)",
82 arg=TEST_PK,
83 timeout=20000,
84 )
85 pg.wait_for_function("() => !!localStorage.getItem('musiquay-vault')", timeout=10000)
86 87 # The identity's own Login button: switch to it and enter the shell.
88 pg.wait_for_selector("text=Identities", timeout=15000)
89 pg.evaluate("""() => {
90 const b = Array.from(document.querySelectorAll('button'))
91 .find(e => (e.textContent || '').trim() === 'Login');
92 if (b) b.click();
93 }""")
94 pg.wait_for_selector("#content-area", state="attached", timeout=30000)
95 pg.wait_for_selector("#home-page", timeout=30000)
96 97 98 class TestBoot:
99 def test_login_page(self, page):
100 """Before sign-in: the login page is branded and translated."""
101 body = page.inner_text("body")
102 assert "MUSIQUAY" in body
103 assert "the digital home for the music scene" in body
104 assert "subtitle" not in body, "i18n fell back to the key"
105 106 def test_boot_status(self, page):
107 """Every worker reports ok, and the feed worker reports parked."""
108 # The workers report as they come up; the last line lands after the app
109 # is ready, so wait for the record to settle before reading it.
110 page.wait_for_function(
111 """() => {
112 const d = JSON.parse(localStorage.getItem('__musiquay_boot_diag') || '{}').status || {};
113 return d.feed === 'parked' && ['verify','store','rproxy','signer','app','domain','profile','notif']
114 .every(k => d[k] === 'ok');
115 }""",
116 timeout=30000,
117 )
118 status = json.loads(page.evaluate("() => localStorage.getItem('__musiquay_boot_diag')"))["status"]
119 assert status.get("feed") == "parked"
120 for name in READY_STATUS:
121 assert status.get(name) == "ok", f"{name} = {status.get(name)}"
122 assert page.evaluate("() => document.body.getAttribute('data-wasm-ready')") == "1"
123 124 def test_no_page_errors(self, page):
125 assert page.boot_errors == []
126 127 128 class TestShell:
129 def test_signed_in(self, app):
130 """Auto-login put the seeded identity in the shell."""
131 assert visible(app, "#content-area")
132 assert app.evaluate("() => localStorage.getItem('musiquay-pubkey')") == TEST_PK
133 assert app.evaluate("() => document.querySelector('input[placeholder=\"nsec1...\"]') === null")
134 135 def test_home_page_is_the_main_area(self, app):
136 assert visible(app, "#home-page")
137 for parked in ("#feed-page", "#notif-page", "#compose-page", "#search-page", "#thread-page"):
138 assert not visible(app, parked), f"{parked} must not be shown at the root"
139 140 def test_no_forum_ui(self, app):
141 """No kind-1 forum surface: no forum text, no feed/compose nav buttons."""
142 assert app.evaluate("() => document.body.innerText.toLowerCase().includes('forum')") is False
143 labels = app.evaluate(
144 "() => Array.from(document.querySelectorAll('button, a'))"
145 ".map(e => (e.innerText || '').trim().toLowerCase())"
146 )
147 assert "feed" not in labels
148 assert "compose" not in labels
149 assert app.evaluate("() => document.querySelectorAll('svg').length") > 0, "shell did not render"
150 151 def test_forum_routes_land_on_home(self, app):
152 """Every forum route collapses onto the empty home page."""
153 for route in ("/feed", "/feed/follows", "/t/abc123", "/compose", "/search"):
154 app.goto(f"{RELAY_URL}{route}", wait_until="domcontentloaded")
155 wait_wasm_ready(app)
156 app.wait_for_selector("#home-page", timeout=15000)
157 assert visible(app, "#home-page"), f"{route} did not land on home"
158 159 def test_notifications_empty(self, app):
160 app.goto(f"{RELAY_URL}/notifications", wait_until="domcontentloaded")
161 wait_wasm_ready(app)
162 app.wait_for_selector("#notif-page", timeout=15000)
163 assert visible(app, "#notif-page")
164 rows = app.evaluate(
165 "() => document.querySelectorAll('#notif-page [data-notif-row], #notif-page .notif-row').length"
166 )
167 assert rows == 0, f"notifications are not empty ({rows} rows)"
168 169 def test_screenshot(self, app):
170 path = os.path.join(ARTIFACTS_DIR, "musiquay-signed-in.png")
171 app.goto(RELAY_URL, wait_until="domcontentloaded")
172 wait_wasm_ready(app)
173 app.wait_for_selector("#home-page")
174 app.screenshot(path=path, full_page=False)
175 assert os.path.getsize(path) > 5000
176