148 lines
6.5 KiB
Go
148 lines
6.5 KiB
Go
package webserver
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func readStaticFile(t *testing.T, name string) string {
|
|
t.Helper()
|
|
data, err := os.ReadFile(filepath.Join("..", "static", name))
|
|
if err != nil {
|
|
t.Fatalf("read %s: %v", name, err)
|
|
}
|
|
return string(data)
|
|
}
|
|
|
|
func requireContains(t *testing.T, text, want, context string) {
|
|
t.Helper()
|
|
if !strings.Contains(text, want) {
|
|
t.Fatalf("%s missing %q", context, want)
|
|
}
|
|
}
|
|
|
|
func requireNotContains(t *testing.T, text, unwanted, context string) {
|
|
t.Helper()
|
|
if strings.Contains(text, unwanted) {
|
|
t.Fatalf("%s unexpectedly contains %q", context, unwanted)
|
|
}
|
|
}
|
|
|
|
func requireFunctionContains(t *testing.T, source, functionName, want string) {
|
|
t.Helper()
|
|
start := strings.Index(source, "function "+functionName+"(")
|
|
if start < 0 {
|
|
t.Fatalf("missing function %s", functionName)
|
|
}
|
|
next := strings.Index(source[start+1:], "\nfunction ")
|
|
end := len(source)
|
|
if next >= 0 {
|
|
end = start + 1 + next
|
|
}
|
|
body := source[start:end]
|
|
if !strings.Contains(body, want) {
|
|
t.Fatalf("function %s missing %q", functionName, want)
|
|
}
|
|
}
|
|
|
|
func TestStaticLiveRegionAnnouncementsStayWired(t *testing.T) {
|
|
html := readStaticFile(t, "skald.html")
|
|
js := readStaticFile(t, "skald.js")
|
|
|
|
requireContains(t, html, `id="chat-announcements"`, "chat live region")
|
|
requireContains(t, html, `role="status"`, "chat live region")
|
|
requireContains(t, html, `aria-live="polite"`, "chat live region")
|
|
requireContains(t, html, `id="urgent-announcements"`, "urgent live region")
|
|
requireContains(t, html, `role="alert"`, "urgent live region")
|
|
requireContains(t, html, `aria-live="assertive"`, "urgent live region")
|
|
|
|
requireFunctionContains(t, js, "announceChat", "chat-announcements")
|
|
requireFunctionContains(t, js, "announceUrgent", "urgent-announcements")
|
|
requireFunctionContains(t, js, "announceLiveRegion", "state.queue.push")
|
|
requireFunctionContains(t, js, "announceLiveRegion", "flushLiveRegion")
|
|
requireFunctionContains(t, js, "flushLiveRegion", "appendChild")
|
|
requireFunctionContains(t, js, "flushLiveRegion", "if(text === undefined) {\n announcement.replaceChildren();")
|
|
requireFunctionContains(t, js, "addToChatbox", "announceChat")
|
|
requireFunctionContains(t, js, "setUserStatus", "announceUrgent(`${username}: hand raised`)")
|
|
requireFunctionContains(t, js, "setUserStatus", "announceUrgent(`${username}: hand lowered`)")
|
|
}
|
|
|
|
func TestCaptionOverlayHasBeenRemoved(t *testing.T) {
|
|
html := readStaticFile(t, "skald.html")
|
|
js := readStaticFile(t, "skald.js")
|
|
css := readStaticFile(t, "skald.css")
|
|
|
|
requireNotContains(t, html, "captions", "hall page")
|
|
requireNotContains(t, js, "caption", "hall script")
|
|
requireNotContains(t, css, "caption", "hall styles")
|
|
}
|
|
|
|
func TestHallHeadingIncludesCurrentStatus(t *testing.T) {
|
|
html := readStaticFile(t, "skald.html")
|
|
js := readStaticFile(t, "skald.js")
|
|
|
|
requireContains(t, html, "Skald Version 2026.07.08", "hall page title")
|
|
requireContains(t, js, "const skaldVersionLabel = 'Skald Version 2026.07.08'", "version label")
|
|
requireFunctionContains(t, js, "setTitle", "hallStatus.locked ? 'locked' : 'unlocked'")
|
|
requireFunctionContains(t, js, "setTitle", "status += ', recording'")
|
|
requireFunctionContains(t, js, "setTitle", "${skaldVersionLabel}")
|
|
requireFunctionContains(t, js, "gotJoined", "hallStatus.locked = false")
|
|
requireFunctionContains(t, js, "gotJoined", "hallStatus.recording = false")
|
|
requireFunctionContains(t, js, "gotJoined", "hallRecording = !!hallStatus.recording")
|
|
requireFunctionContains(t, js, "gotUserMessage", "hallRecording = true")
|
|
requireFunctionContains(t, js, "gotUserMessage", "hallRecording = false")
|
|
}
|
|
|
|
func TestGlobalShortcutAliasesStayDocumented(t *testing.T) {
|
|
js := readStaticFile(t, "skald.js")
|
|
|
|
requireFunctionContains(t, js, "isGlobalShortcut", "e.altKey && (e.ctrlKey || e.shiftKey)")
|
|
requireFunctionContains(t, js, "isGlobalShortcut", "e.key.toLowerCase() === key")
|
|
requireContains(t, js, "Control+Alt+H or Alt+Shift+H: Raise or lower hand", "shortcut help")
|
|
requireContains(t, js, "Control+Alt+C or Alt+Shift+C: Expand or collapse chat", "shortcut help")
|
|
requireContains(t, js, "Control+Alt+T or Alt+Shift+T: Mute or unmute microphone", "shortcut help")
|
|
requireContains(t, js, "if(isGlobalShortcut(e, 'h'))", "hand shortcut")
|
|
requireContains(t, js, "if(isGlobalShortcut(e, 'c'))", "chat shortcut")
|
|
requireContains(t, js, "if(isGlobalShortcut(e, 't'))", "microphone shortcut")
|
|
}
|
|
|
|
func TestTimedUnlockCommandStaysDocumentedAndUsesServerAnnouncements(t *testing.T) {
|
|
js := readStaticFile(t, "skald.js")
|
|
|
|
requireContains(t, js, "[minutes[m|M] | HH:MM | h:mm AM/PM]", "unlock command help")
|
|
requireContains(t, js, "Intl.DateTimeFormat().resolvedOptions().timeZone", "unlock timezone")
|
|
requireContains(t, js, "timezoneOffset: new Date().getTimezoneOffset()", "unlock timezone fallback")
|
|
requireContains(t, js, "serverConnection.hallAction('unlock', {", "scheduled unlock request")
|
|
requireFunctionContains(t, js, "gotUserMessage", "displayMessage(text, false)")
|
|
requireFunctionContains(t, js, "displayError", "if(announce)")
|
|
requireFunctionContains(t, js, "displayError", "announceChat(text)")
|
|
}
|
|
|
|
func TestMicrophoneHardStopLivesInSelfMenu(t *testing.T) {
|
|
html := readStaticFile(t, "skald.html")
|
|
js := readStaticFile(t, "skald.js")
|
|
|
|
requireNotContains(t, html, `id="unpresentbutton"`, "toolbar")
|
|
requireNotContains(t, html, `aria-label="Disable microphone"`, "toolbar")
|
|
requireNotContains(t, js, "getButtonElement('unpresentbutton')", "toolbar handler")
|
|
requireNotContains(t, js, "setVisibility('unpresentbutton'", "toolbar visibility")
|
|
requireFunctionContains(t, js, "userMenu", "Turn microphone off")
|
|
requireFunctionContains(t, js, "userMenu", "closeUpMedia('audio')")
|
|
requireFunctionContains(t, js, "userMenu", "Restart audio connection")
|
|
}
|
|
|
|
func TestRecordingControlLivesInSelfMenu(t *testing.T) {
|
|
js := readStaticFile(t, "skald.js")
|
|
|
|
requireFunctionContains(t, js, "canRecordHall", "serverConnection.permissions.indexOf('record') >= 0")
|
|
requireFunctionContains(t, js, "recordingsUrl", "hall.split('/').map(part => encodeURIComponent(part)).join('/')")
|
|
requireFunctionContains(t, js, "recordingPredicate", "if(canRecordHall())")
|
|
requireFunctionContains(t, js, "userMenu", "if(canRecordHall())")
|
|
requireFunctionContains(t, js, "userMenu", "hallRecording ? 'Stop recording' : 'Start recording'")
|
|
requireFunctionContains(t, js, "userMenu", "serverConnection.hallAction(hallRecording ? 'unrecord' : 'record')")
|
|
requireFunctionContains(t, js, "userMenu", "Open recordings (opens in new tab)")
|
|
requireFunctionContains(t, js, "userMenu", "window.open(recordingsUrl(), '_blank', 'noopener')")
|
|
}
|