oficina / meet Public

Review #24: Share's script-only buttons stay hidden; a panic through the real router

- calls-shell.css: .calls-share-actions set display:flex, which beat the
  hidden attribute, so Copy link and Share… showed without script and
  did nothing; Share… (rst-btn) did the same where the browser cannot
  share. Browser coverage: scriptless (red before the rule) and chrome
  (red with the inner rule removed).
- TestARoutePanicIsTheErrorPageThroughTheRouter panics inside a real
  route (the return cookie's clock on /signin/local) through the
  production router, signed in and out; red with the router's recoverer
  removed, where the old middleware-only test stayed green.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev pushed by paul@keymail.dev bd56680d97c455115ba9ad29d8290d7fd6a47ef9 parent 732122b
3 files changed, +81 −0
  • internal/calls/errors_test.go +50 −0
  • internal/calls/static/calls-shell.css +6 −0
  • test/browser/call.mjs +25 −0

CI passed — run details

CI log
=== vet ===
go vet ./...
=== fmt ===
=== test ===
go test ./...
ok  	amadan.net/oficina/calls/callscore	(cached)
?   	amadan.net/oficina/calls/cmd/calls	[no test files]
ok  	amadan.net/oficina/calls/internal/calls	1.515s
ok  	amadan.net/oficina/calls/internal/calls/apns	(cached)
ok  	amadan.net/oficina/calls/internal/calls/hub	(cached)
?   	amadan.net/oficina/calls/internal/calls/icons	[no test files]
ok  	amadan.net/oficina/calls/internal/calls/relay	(cached)
ok  	amadan.net/oficina/calls/internal/calls/seal	(cached)
ok  	amadan.net/oficina/calls/internal/callstest	8.525s
?   	amadan.net/oficina/calls/protocol	[no test files]
?   	amadan.net/oficina/calls/scripts/writeestilo	[no test files]
=== migration-check ===
go run amadan.net/rastrillo/rastrillo/cmd/rastrillo migration check
rastrillo migration check: models and migrations agree
=== race ===
CGO_ENABLED=1 go test -race ./internal/...
ok  	amadan.net/oficina/calls/internal/calls	9.786s
ok  	amadan.net/oficina/calls/internal/calls/apns	(cached)
ok  	amadan.net/oficina/calls/internal/calls/hub	(cached)
?   	amadan.net/oficina/calls/internal/calls/icons	[no test files]
ok  	amadan.net/oficina/calls/internal/calls/relay	(cached)
ok  	amadan.net/oficina/calls/internal/calls/seal	(cached)
ok  	amadan.net/oficina/calls/internal/callstest	25.110s
diff --git a/internal/calls/errors_test.go b/internal/calls/errors_test.go
index c6034ea..7159919 100644
--- a/internal/calls/errors_test.go
+++ b/internal/calls/errors_test.go
@@ -1,10 +1,13 @@
package calls
import (
+ "bytes"
+ "log/slog"
"net/http"
"net/http/httptest"
"strings"
"testing"
+ "time"
)
// A panic inside the router is the 500 page with a reference and the
@@ -28,3 +31,50 @@ func TestAPanicIsTheErrorPageWithAReference(t *testing.T) {
t.Fatalf("panic for a fetch: %d %q", w.Code, w.Header().Get("Content-Type"))
}
}
+
+// Astra on #24, finding 3 (Memoria #21's shape): the test above builds
+// recoverToErrorPage by hand, so taking it out of the router left it
+// green. This one panics inside a REAL route — the return cookie's clock,
+// read by /signin/local?return_to= — through the production router,
+// signed in and out, and checks the 500 page with a reference, the
+// viewer's own chrome, and that the panic's value reaches neither the
+// page nor the log.
+func TestARoutePanicIsTheErrorPageThroughTheRouter(t *testing.T) {
+ const sentinel = "secret-sentinel-title"
+ for _, signed := range []bool{true, false} {
+ f := newHomeRouterFixture(t)
+ if signed {
+ f.local(t)
+ }
+ var logs bytes.Buffer
+ f.app.logger = slog.New(slog.NewTextHandler(&logs, nil))
+ f.app.store.clk.set(func() time.Time { panic(sentinel) })
+ r := httptest.NewRequest("GET", homeTestOrigin+"/signin/local?return_to=%2Fsettings", nil)
+ r.Header.Set("Accept", "text/html")
+ for _, c := range f.cookies {
+ r.AddCookie(c)
+ }
+ w := httptest.NewRecorder()
+ func() {
+ defer func() {
+ if v := recover(); v != nil {
+ t.Fatalf("signed in=%v: the panic escaped the router: %v", signed, v)
+ }
+ }()
+ f.handler.ServeHTTP(w, r)
+ }()
+ b := w.Body.String()
+ if w.Code != http.StatusInternalServerError || !strings.Contains(b, "<title>Something went wrong · Calls</title>") || !strings.Contains(b, "rst-error-ref") {
+ t.Errorf("signed in=%v: %d, want the 500 page with a reference:\n%s", signed, w.Code, b)
+ }
+ if strings.Contains(b, "estilo-account") != signed {
+ t.Errorf("signed in=%v: the 500 page's chrome is not the viewer's", signed)
+ }
+ if strings.Contains(b, sentinel) || strings.Contains(logs.String(), sentinel) {
+ t.Errorf("signed in=%v: the panic's value reached the page or the log", signed)
+ }
+ if !strings.Contains(logs.String(), "ref=") {
+ t.Errorf("signed in=%v: no reference in the log: %s", signed, logs.String())
+ }
+ }
+}
diff --git a/internal/calls/static/calls-shell.css b/internal/calls/static/calls-shell.css
index 517d6d6..b68de91 100644
--- a/internal/calls/static/calls-shell.css
+++ b/internal/calls/static/calls-shell.css
@@ -22,6 +22,12 @@ body { background: var(--rst-bg); }
.calls-option-help { margin-block: 0.25rem var(--rst-sp-3); color: var(--rst-text-muted); font-size: var(--rst-fs-sm); }
.calls-share-link a { overflow-wrap: anywhere; }
.calls-share-actions { display: flex; flex-wrap: wrap; gap: var(--rst-sp-2); }
+/* The row is script's (share-link.js reveals it): the display above
+ would otherwise beat the hidden attribute and show two buttons that do
+ nothing without script (Astra on #24). */
+.calls-share-actions[hidden], .calls-share-actions > [hidden] { display: none; }
+/* …and Share… inside it, which share-link.js reveals only where the
+ browser can share: rst-btn's display beats hidden the same way. */
.calls-link-input { inline-size: 100%; max-inline-size: 44rem; }
/* The holding screen: who you'll join as, then the device panel, then
diff --git a/test/browser/call.mjs b/test/browser/call.mjs
index b753936..bd72271 100644
--- a/test/browser/call.mjs
+++ b/test/browser/call.mjs
@@ -5007,6 +5007,15 @@ const run = {
check(holding.cls < 0.01, 'the holding screen holds still while its devices arrive (CLS ' + holding.cls.toFixed(4) + ' ' + JSON.stringify(holding.shifts) + ')');
check(await page.locator('header[estilo-bar]').count() === 1, 'the holding screen wears the estilo bar');
+ // With script, Copy link shows, and Share… only where the browser
+ // can share: rst-btn's display used to beat its hidden attribute.
+ const [share] = await Promise.all([ctx.waitForEvent('page'), page.click('form[action$="/share"] button')]);
+ await share.waitForLoadState('load');
+ const canShare = await share.evaluate(() => !!navigator.share);
+ check(await share.locator('#copy-invitation').isVisible() && (await share.locator('#share-invitation').isVisible()) === canShare,
+ 'the Share page shows Copy link, and Share… only where the browser can share (navigator.share ' + canShare + ')');
+ await share.close();
+
await Promise.all([page.waitForURL('**/m/' + sqid), page.click('#join')]);
await page.waitForFunction(() => !!window.meet, null, { timeout: 20000 });
check(await until(async () => (await state(page)).admitted, 20000), 'joined from the holding screen');
@@ -5014,6 +5023,7 @@ const run = {
const joined = await cls();
check(joined.cls < 0.01, 'holding screen → room holds still (CLS ' + joined.cls.toFixed(4) + ' ' + JSON.stringify(joined.shifts) + ')');
+
// The title menu: the account menu's order and words.
await page.mouse.move(200, 200);
await page.click('#call-nav > summary');
@@ -5521,6 +5531,21 @@ const run = {
check(await holding.locator('#devices').isHidden(),
'the holding screen keeps its device controls hidden with no script');
+ // Share link works with no script (a POST into a new tab), and the
+ // page it opens shows the link and nothing that needs script: Copy
+ // link and Share… are script's, hidden until share-link.js reveals
+ // them. A display rule on their row used to beat the hidden
+ // attribute, leaving two buttons that did nothing (Astra on #24).
+ const [share] = await Promise.all([
+ ctx.waitForEvent('page'),
+ holding.click('form[action$="/share"] button'),
+ ]);
+ await share.waitForLoadState('domcontentloaded');
+ check(await share.locator('#invitation-link').isVisible(), 'no script: the Share page shows the invitation link (at ' + where(share) + ')');
+ check(await share.locator('#share-actions').isHidden() && await share.locator('#copy-invitation').isHidden(),
+ 'no script: the Share page shows no Copy link or Share… buttons');
+ await share.close();
+
const room = await ctx.newPage();
await room.goto(origin + '/m/' + sqid, { waitUntil: 'domcontentloaded' });
const roomText = await room.locator('body').innerText();