1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3package web
4
5import (
6 "bytes"
7 "strings"
8 "testing"
9
10 "github.com/a-h/templ"
11 "github.com/stretchr/testify/assert"
12 "github.com/stretchr/testify/require"
13)
14
15func renderToString(t *testing.T, c templ.Component) string {
16 t.Helper()
17 var b bytes.Buffer
18 require.NoError(t, c.Render(t.Context(), &b))
19 return b.String()
20}
21
22// A live banner is the content of a status region that stays in the page: the
23// empty slot of the page and the fragment of the stream are the same element
24// (same id, role and marker), so that ermiteLive can replace the children and
25// leave the region where a screen reader watches it.
26func TestLiveSlotsAreStableStatusRegions(t *testing.T) {
27 t.Parallel()
28 for _, tt := range []struct {
29 name string
30 c templ.Component
31 id string
32 text string
33 }{
34 {"empty commits slot", LiveSlot("commits-head"), "commits-head", ""},
35 {"empty mr slot", LiveSlot("mr-notice"), "mr-notice", ""},
36 {"commits banner", CommitsBanner("2 new commits — reload", "/x/commits/main"), "commits-head", "2 new commits — reload"},
37 {"mr notice", MRNotice("a comment was added — reload", "/x/merge-requests/1"), "mr-notice", "a comment was added — reload"},
38 } {
39 t.Run(tt.name, func(t *testing.T) {
40 t.Parallel()
41 got := renderToString(t, tt.c)
42 assert.True(t, strings.HasPrefix(got, `<div id="`+tt.id+`" class="live-slot" role="status" data-slot>`), got)
43 if tt.text == "" {
44 assert.True(t, strings.HasSuffix(got, "data-slot></div>"), "an empty slot has no child: %s", got)
45 } else {
46 assert.Contains(t, got, tt.text)
47 }
48 assert.True(t, strings.HasSuffix(got, "</div>"), got)
49 })
50 }
51}
52
53// The header's refs button is a link to the refs page, which the script turns
54// into the dialog; the filters and the buttons that need scripts are marked.
55func TestShellDegradesWithoutScripts(t *testing.T) {
56 t.Parallel()
57 l := Layout{Repo: "demo", Ref: "main", Short: "abc1234", Assets: Assets{}}
58 got := renderToString(t, Header(l))
59 assert.Contains(t, got, `<a class="btn ref" href="/demo/refs" data-fire="refs"`)
60 assert.NotContains(t, got, "hx-on:click", "no handler is inlined in the header")
61 for _, want := range []string{`class="btn search" data-js`, `class="btn icon" data-js data-fire="help"`} {
62 assert.Contains(t, got, want)
63 }
64}
65
66// Data reaches the scripts through data-* attributes, never spliced into an
67// attribute that is evaluated.
68func TestDiffControlsPassDataThroughAttributes(t *testing.T) {
69 t.Parallel()
70 files := renderToString(t, FileList([]DiffFile{{ID: "f1'x", Path: "a.go", Status: "M"}}))
71 assert.NotContains(t, files, ":aria-current", "the file in view is marked by ermite.js")
72 assert.NotContains(t, files, "f1'x'", "no id is spliced into a script")
73
74 link := renderToString(t, viewLink(DiffView{Both: true, View: "split"}, "split", "Split", "d", "?view=split"))
75 assert.Contains(t, link, `data-fire="split"`)
76 assert.Contains(t, link, `this.dataset.view`)
77 assert.NotContains(t, link, "=== 'split'")
78}