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 {"mergeability slot, as sent by the stream", MRMergeabilityFragment(MergeabilityView{State: "up_to_date", Label: "Ready to merge", FFOk: true, MergeOk: true}), "mr-mergeability", "Ready to merge"},
37 {"commits banner", CommitsBanner("2 new commits — reload", "/x/commits/main"), "commits-head", "2 new commits — reload"},
38 {"mr notice", MRNotice("a comment was added — reload", "/x/merge-requests/1"), "mr-notice", "a comment was added — reload"},
39 } {
40 t.Run(tt.name, func(t *testing.T) {
41 t.Parallel()
42 got := renderToString(t, tt.c)
43 assert.True(t, strings.HasPrefix(got, `<div id="`+tt.id+`" class="live-slot" role="status" data-slot>`), got)
44 if tt.text == "" {
45 assert.True(t, strings.HasSuffix(got, "data-slot></div>"), "an empty slot has no child: %s", got)
46 } else {
47 assert.Contains(t, got, tt.text)
48 }
49 assert.True(t, strings.HasSuffix(got, "</div>"), got)
50 })
51 }
52}
53
54// The header's refs button is a link to the refs page, which the script turns
55// into the dialog; the filters and the buttons that need scripts are marked.
56func TestShellDegradesWithoutScripts(t *testing.T) {
57 t.Parallel()
58 l := Layout{Repo: "demo", Ref: "main", Short: "abc1234", Assets: Assets{}}
59 got := renderToString(t, Header(l))
60 assert.Contains(t, got, `<a class="btn ref" href="/demo/refs" data-fire="refs"`)
61 assert.NotContains(t, got, "hx-on:click", "no handler is inlined in the header")
62 assert.Contains(t, got, `class="btn search" data-js`)
63 assert.NotContains(t, got, `data-fire="help"`, "the help button is in the footer only")
64 assert.Contains(t, renderToString(t, Footer(l)), `class="foot-keys" data-js data-fire="help"`)
65}
66
67// Data reaches the scripts through data-* attributes, never spliced into an
68// attribute that is evaluated.
69func TestDiffControlsPassDataThroughAttributes(t *testing.T) {
70 t.Parallel()
71 files := renderToString(t, FileList([]DiffFile{{ID: "f1'x", Path: "a.go", Status: "M"}}))
72 assert.NotContains(t, files, ":aria-current", "the file in view is marked by ermite.js")
73 assert.NotContains(t, files, "f1'x'", "no id is spliced into a script")
74
75 link := renderToString(t, viewLink(DiffView{Both: true, View: "split"}, "split", "Split", "d", "?view=split"))
76 assert.Contains(t, link, `data-fire="split"`)
77 assert.Contains(t, link, `this.dataset.view`)
78 assert.NotContains(t, link, "=== 'split'")
79}