// SPDX-License-Identifier: AGPL-3.0-or-later package web import ( "bytes" "strings" "testing" "bnema.dev/ermite/internal/domain" "bnema.dev/ermite/internal/ports" "github.com/a-h/templ" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func renderToString(t *testing.T, c templ.Component) string { t.Helper() var b bytes.Buffer require.NoError(t, c.Render(t.Context(), &b)) return b.String() } // A live banner is the content of a status region that stays in the page: the // empty slot of the page and the fragment of the stream are the same element // (same id, role and marker), so that ermiteLive can replace the children and // leave the region where a screen reader watches it. func TestLiveSlotsAreStableStatusRegions(t *testing.T) { t.Parallel() for _, tt := range []struct { name string c templ.Component id string text string }{ {"empty commits slot", LiveSlot("commits-head"), "commits-head", ""}, {"empty mr slot", LiveSlot("mr-notice"), "mr-notice", ""}, {"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"}, {"commits banner", CommitsBanner("2 new commits — reload", "/x/commits/main"), "commits-head", "2 new commits — reload"}, {"mr notice", MRNotice("a comment was added — reload", "/x/merge-requests/1"), "mr-notice", "a comment was added — reload"}, } { t.Run(tt.name, func(t *testing.T) { t.Parallel() got := renderToString(t, tt.c) assert.True(t, strings.HasPrefix(got, `
`), got) if tt.text == "" { assert.True(t, strings.HasSuffix(got, "data-slot>
"), "an empty slot has no child: %s", got) } else { assert.Contains(t, got, tt.text) } assert.True(t, strings.HasSuffix(got, ""), got) }) } } // The header's refs button is a link to the refs page, which the script turns // into the dialog; the filters and the buttons that need scripts are marked. func TestShellDegradesWithoutScripts(t *testing.T) { t.Parallel() l := Layout{Repo: "demo", Ref: "main", Short: "abc1234", Assets: Assets{}} got := renderToString(t, Header(l)) assert.Contains(t, got, ` 0 { assert.Equal(t, "mr-head", fs[0].event) assert.Equal(t, `
`, fs[0].html) assert.NotContains(t, fs[0].html, "the branch changed") } } } // Release lifecycle events invalidate release pages and the summary's release // fact without putting notes or asset metadata in SSE payloads. func TestReleaseLiveUpdates(t *testing.T) { for _, ev := range []domain.Event{domain.ReleasePublished{}, domain.ReleaseUpdated{}, domain.ReleaseDeleted{}, domain.ReleaseUnpublished{}} { we := ports.WatchEvent{Event: ev} require.True(t, liveEvent(we), ev.EventKind()) for _, view := range []string{"releases", "release", "summary", "refs", "mr"} { st := &liveStream{repo: "proj", view: view} got, err := st.render(t.Context(), we) require.NoError(t, err) if view == "release" || view == "releases" || view == "summary" { assert.Equal(t, gap, got) } else { assert.Empty(t, got) } } } }