oficina / meet Public

Clone
git clone https://amadan.net/oficina/meet

Plain git — no account needed to clone.

Download

Download this file

1package callstest
2
3import (
4 "net/http"
5 "regexp"
6 "strings"
7 "testing"
8 "time"
9)
10
11func TestHoldingPageShowsTitleStandingAndFloor(t *testing.T) {
12 x, sqid, _ := memberWithMeeting(t)
13 c := x.browser(t)
14 c.signIn("paul@example.com")
15 rec := c.get("/m/" + sqid + "/join")
16 body := rec.Body.String()
17 for _, want := range []string{"Board meeting", "You’ll join as paul.", "Calls outside your network may not connect yet.", `rst-tone="warning"`, "End meeting for everyone"} {
18 if !strings.Contains(body, want) {
19 t.Errorf("holding page lacks %q", want)
20 }
21 }
22}
23
24// A meeting that exists, a person who is signed in, and no standing
25// toward it. The earlier version of this test asked for a meeting that
26// did not exist, so it passed before standingFor was ever called
27// (review C1-8).
28func TestHoldingPageIs404ForNoStanding(t *testing.T) {
29 x, first, tok := memberWithMeeting(t)
30
31 // A signed-in non-member: an outsider who followed the first
32 // meeting's link, so they hold a grant for that meeting and none
33 // for any other.
34 g := x.browser(t)
35 g.get("/j/" + tok)
36 if dest := g.signIn("guest@example.com"); dest != "/m/"+first+"/join" {
37 t.Fatalf("outsider landed on %s", dest)
38 }
39
40 member := x.browser(t)
41 member.signIn("paul@example.com")
42 second, _ := createMeeting(t, member, "A meeting they were not invited to", "")
43
44 if rec := g.get("/m/" + second + "/join"); rec.Code != http.StatusNotFound {
45 t.Fatalf("a real meeting with no grant: %d, want 404", rec.Code)
46 }
47 // And the meeting they do hold a grant for, once it has run out.
48 x.advance(2*time.Hour + time.Minute)
49 if rec := g.get("/m/" + first + "/join"); rec.Code != http.StatusNotFound {
50 t.Fatalf("a real meeting with an expired grant: %d, want 404", rec.Code)
51 }
52}
53
54// Fix round 1, Important 12: once this session already holds a peer in
55// the room, the holding page points at the takeover confirm page rather
56// than offering a plain join that would collide with it.
57func TestHoldingPageOffersTakeoverWhenThisSessionIsAlreadyInTheRoom(t *testing.T) {
58 x, sqid, _ := memberWithMeeting(t)
59 m := x.browser(t)
60 m.signIn("paul@example.com")
61 if rec := m.get("/m/" + sqid + "/join"); strings.Contains(rec.Body.String(), "already open in another tab") {
62 t.Fatal("holding page offers takeover before any stream is open")
63 }
64 s := m.stream("/m/" + sqid + "/stream")
65 defer s.close()
66 s.hello()
67 rec := m.get("/m/" + sqid + "/join")
68 body := rec.Body.String()
69 if !strings.Contains(body, "already open in another tab") || !strings.Contains(body, "/m/"+sqid+"/takeover") {
70 t.Fatalf("holding page lacks the takeover offer once a peer is live: %s", body)
71 }
72 // devices.js's own join button must not offer a fresh stream that
73 // would 409: once this session already has a peer here, the
74 // #devices block's join target is the takeover confirm page, not
75 // the room.
76 if !strings.Contains(body, `data-join-href="/m/`+sqid+`/takeover"`) {
77 t.Fatalf("holding page's join target should switch to takeover once a peer is live: %s", body)
78 }
79}
80
81// Task 7: devices.js takes over the region the slice-1 placeholder
82// held. The script tag lives in <head> (the room page's own pattern,
83// and the CSP's default-src 'self' wants no inline script), the
84// #devices block still carries the sqid the script reads, and its
85// default join target is the room itself — not the takeover page,
86// which is only for a session that already has a peer here (the next
87// test).
88func TestHoldingPageHasDevicesScriptAndNoLegacyPlaceholder(t *testing.T) {
89 x, sqid, _ := memberWithMeeting(t)
90 c := x.browser(t)
91 c.signIn("paul@example.com")
92 rec := c.get("/m/" + sqid + "/join")
93 body := rec.Body.String()
94
95 headEnd := strings.Index(body, "</head>")
96 // asset() fingerprints the filename (static/devices.<hash>.js), so
97 // the literal "devices.js" never appears — match the stable prefix,
98 // as room_test.go does for calls.css.
99 script := strings.Index(body, "/static/devices.")
100 if headEnd < 0 || script < 0 || script > headEnd {
101 t.Fatalf("devices.js is not inside <head>: head ends at %d, script at %d\n%s", headEnd, script, body)
102 }
103 if !strings.Contains(body, `id="devices" data-sqid="`+sqid+`"`) {
104 t.Fatalf("holding page lacks the devices block's data-sqid: %s", body)
105 }
106 if !strings.Contains(body, `data-join-href="/m/`+sqid+`"`) {
107 t.Fatalf("holding page's default join target should be the room: %s", body)
108 }
109 if !strings.Contains(body, "<noscript>") || !strings.Contains(body, "needs JavaScript") {
110 t.Fatal("holding page lacks its scriptless paragraph")
111 }
112 if strings.Contains(body, "Calls coming soon") {
113 t.Fatal("holding page still carries the slice-1 placeholder")
114 }
115}
116
117// Fix round 1, Important 5, restated for the reserved layout (shared
118// chrome R2): #devices is in the layout from the first paint, so that
119// devices.js revealing it does not push the page down, and without
120// script a <noscript> stylesheet hides it instead — a regression that
121// dropped either would show a bare, unstyled device block to every
122// browser with script disabled, or move the page under one with it.
123func TestHoldingPageDevicesBlockIsHiddenOnlyWithoutScript(t *testing.T) {
124 x, sqid, _ := memberWithMeeting(t)
125 c := x.browser(t)
126 c.signIn("paul@example.com")
127 rec := c.get("/m/" + sqid + "/join")
128 body := rec.Body.String()
129 if !strings.Contains(body, `data-join-href="/m/`+sqid+`">`) {
130 t.Fatalf("holding page's #devices block should be in the layout from the first paint: %s", body)
131 }
132 m := regexp.MustCompile(`<noscript><link rel="stylesheet" href="(/static/noscript\.[0-9a-f]+\.css)"></noscript>`).FindStringSubmatch(body)
133 if m == nil {
134 t.Fatalf("holding page has no <noscript> stylesheet to hide #devices: %s", body)
135 }
136 css := c.get(m[1]).Body.String()
137 if !strings.Contains(css, "#devices { display: none; }") {
138 t.Fatalf("the noscript sheet does not hide #devices: %s", css)
139 }
140}
141
142// The two toggles carry their accessible name in aria-label and their
143// state in aria-pressed and a child span, so the name a screen reader
144// tracks never changes under it and the state is never carried by
145// colour alone (AGENTS.md's UX conventions). The server renders all
146// three; devices.js only ever moves the attribute and the span's word,
147// which is why the markup is worth asserting here rather than in a
148// browser.
149func TestHoldingPageTogglesNameThemselvesAndShowTheirState(t *testing.T) {
150 x, sqid, _ := memberWithMeeting(t)
151 c := x.browser(t)
152 c.signIn("paul@example.com")
153 body := c.get("/m/" + sqid + "/join").Body.String()
154 for _, want := range []string{
155 `id="toggle-mute" rst-btn aria-label="Microphone" aria-pressed="false"`,
156 `<span id="toggle-mute-state" class="calls-toggle-state" aria-hidden="true">On</span>`,
157 `id="toggle-camera" rst-btn aria-label="Camera" aria-pressed="false"`,
158 `<span id="toggle-camera-state" class="calls-toggle-state" aria-hidden="true">On</span>`,
159 } {
160 if !strings.Contains(body, want) {
161 t.Fatalf("holding page's toggles: missing %q\n%s", want, body)
162 }
163 }
164}
165
166func TestHoldingPageIs404ForEndedMeetings(t *testing.T) {
167 x, sqid, _ := memberWithMeeting(t)
168 m := x.browser(t)
169 m.signIn("paul@example.com")
170 m.post("/meetings/"+sqid+"/end", nil)
171 if rec := m.get("/m/" + sqid + "/join"); rec.Code != http.StatusNotFound {
172 t.Fatalf("ended: %d", rec.Code)
173 }
174}
175
176// Studio audio is the unprocessed capture option (the microphone asked
177// for with echo cancellation, noise suppression and automatic gain all
178// off, and a higher Opus ceiling to send it through). Three facts about
179// the markup are worth holding here rather than in a browser, because
180// devices.js only ever moves the checkbox's `checked` and the row's
181// `hidden` and would go on "working" with any of them broken:
182//
183// - it is the vendored field-check and not a hand-rolled checkbox,
184// which AGENTS.md's field rule requires — rst-switch is that
185// partial's own attribute and nothing else in these pages emits it;
186// - the row starts hidden, like the speaker row, so a scriptless
187// browser is not shown a control that cannot do anything;
188// - the help sentence carries the id devices.js wires to the input
189// with aria-describedby, so a rename here that left the script
190// alone would silently unwire the explanation.
191func TestHoldingPageOffersStudioAudioAsAVendoredField(t *testing.T) {
192 x, sqid, _ := memberWithMeeting(t)
193 c := x.browser(t)
194 c.signIn("paul@example.com")
195 body := c.get("/m/" + sqid + "/join").Body.String()
196 for _, want := range []string{
197 `<div id="raw-row" class="calls-raw-row">`,
198 `<label rst-switch>`,
199 `name="raw-audio"`,
200 `id="raw-help"`,
201 } {
202 if !strings.Contains(body, want) {
203 t.Fatalf("holding page lacks %q for studio audio: %s", want, body)
204 }
205 }
206 // The sentence has to say what is being turned off. "Studio audio"
207 // alone is a name, not an explanation, and someone reaching for it
208 // on a laptop should be told why not before they hear the echo.
209 for _, want := range []string{"echo cancellation", "noise suppression", "automatic gain"} {
210 if !strings.Contains(body, want) {
211 t.Fatalf("studio audio's help sentence does not mention %q: %s", want, body)
212 }
213 }
214}
215