package web
import (
"strings"
"testing"
)
// The composer offers two views of one draft: Rich, reading its blocks
// from this repository's markup-blocks and its pickers' lists from
// ref.suggest, and Markdown, the textarea the form posts. There is no third Preview: Rich renders through the same
// pipeline. Without script the tab bar stays hidden and the textarea is
// the composer.
func TestTheComposerOffersRichAndMarkdown(t *testing.T) {
var b strings.Builder
if err := noHooks.template(forumAnnouncePage).ExecuteTemplate(&b, "content", pageData{Repo: "alpha", Composer: true}); err != nil {
t.Fatal(err)
}
s := b.String()
if !strings.Contains(s, `data-hook="composer-rich"`) || !strings.Contains(s, `data-blocks="/r/alpha/markup-blocks"`) {
t.Error("FAIL: the composer has no Rich view reading this repository's blocks")
}
if !strings.Contains(s, `data-suggest="/r/alpha/ref.suggest"`) {
t.Error("FAIL: the Rich view's pickers do not ask this repository")
}
if !strings.Contains(s, `data-hook="composer-write"`) || strings.Contains(s, `data-tab="preview"`) {
t.Error("FAIL: the composer's tabs are not Rich and Markdown")
}
if !strings.Contains(s, `<div class="f-tabs" hidden>`) || !strings.Contains(s, `name="body"`) {
t.Error("FAIL: without script the composer is not the plain textarea")
}
}
// "/" opens the search everywhere except where somebody is typing, and
// the Rich composer is a place where somebody is typing: it is an
// editable element, not an input, so the shortcut has to ask.
func TestSlashSearchLeavesTheRichComposerAlone(t *testing.T) {
shortcuts := strings.Count(baseHTML, `evt.key == '/'`)
guarded := strings.Count(baseHTML, `evt.key == '/' && !/INPUT|TEXTAREA|SELECT/.test(document.activeElement.tagName) && !document.activeElement.isContentEditable`)
if shortcuts == 0 || guarded != shortcuts {
t.Errorf("FAIL: %d of %d search shortcuts skip an editable element", guarded, shortcuts)
}
}
// Whoever may edit a post (forum.get hands them its source: the author,
// whoever answers for an agent author, a moderator) sees Edit beside
// Reply, and the edit page is the composer holding the post's text.
func TestAnAuthorEditsTheirPostInTheComposer(t *testing.T) {
data := pageData{Repo: "alpha", ForumRoot: "abc123", ForumViewer: "lou", CanWrite: true,
ForumPosts: []forumPostRow{
{Hash: "aaaaaaaaaaaa1111", User: "lou-bot", HTML: "<p>my agent's</p>", Source: "my agent's", Date: "2026-09-16T09:00:00Z",
Edits: 1, EditedBy: "lou"},
{Hash: "bbbbbbbbbbbb2222", User: "mia", HTML: "<p>hers</p>", Date: "2026-09-16T09:05:00Z"},
}}
var b strings.Builder
if err := noHooks.template(forumThreadPage).ExecuteTemplate(&b, "forum-posts", data); err != nil {
t.Fatal(err)
}
if n := strings.Count(b.String(), `data-hook="post-edit"`); n != 1 {
t.Errorf("FAIL: %d Edit buttons, want one, on the post the viewer may edit", n)
}
if !strings.Contains(b.String(), `/r/alpha/forum/abc123/edit/aaaaaaaaaaaa1111`) {
t.Error("FAIL: Edit does not lead to the post the viewer may edit")
}
if !strings.Contains(b.String(), "edited by lou") {
t.Error("FAIL: an edit by someone other than the author does not say who made it")
}
var e strings.Builder
edit := pageData{Repo: "alpha", ForumRoot: "abc123",
ForumPosts: []forumPostRow{{Hash: "aaaaaaaaaaaa1111", User: "lou", Source: "the **text**"}}}
if err := noHooks.template(forumEditPage).ExecuteTemplate(&e, "content", edit); err != nil {
t.Fatal(err)
}
if !strings.Contains(e.String(), `data-hook="composer-rich"`) || !strings.Contains(e.String(), `name="body"`) ||
!strings.Contains(e.String(), "the **text**") {
t.Error("FAIL: the edit page is not the composer holding the post's text")
}
}
// A repository's channel box is Rich: a host for the editor reading
// this repository's blocks and pickers, and the Aa button that docks
// its bar. The textarea stays the field the form posts, and the box
// without script. A surface without the channel's machinery (a team
// room) keeps the plain box.
func TestTheChannelBoxIsRich(t *testing.T) {
s := chatContent(t, pageData{Repo: "alpha", ChatViewer: "dan"})
for _, want := range []string{`class="f-rich chat-rich"`, `data-blocks="/r/alpha/markup-blocks"`,
`data-suggest="/r/alpha/ref.suggest"`, `data-hook="chat-aa"`, `class="composer-in" name="text"`} {
if !strings.Contains(s, want) {
t.Errorf("FAIL: the channel box is missing %s", want)
}
}
if s := chatContent(t, pageData{Repo: "alpha", ChatViewer: "dan", ChatOn: "/t/crew"}); strings.Contains(s, "chat-rich") {
t.Error("FAIL: a surface without the channel's machinery draws the Rich box")
}
}
// A markdown wiki page is edited in the composer, Rich by default; a
// page in fossil's wiki markup keeps the plain box, because Rich writes
// markdown and that page is not.
func TestAMarkdownWikiPageIsEditedInRich(t *testing.T) {
edit := func(rich bool) string {
var b strings.Builder
d := pageData{Repo: "alpha", Wiki: wikiDetail{Title: "Home"}, EditSource: "# Home\n\nwords\n", WikiRich: rich}
if err := noHooks.template(wikiEditPage).ExecuteTemplate(&b, "content", d); err != nil {
t.Fatal(err)
}
return b.String()
}
md := edit(true)
if !strings.Contains(md, `data-hook="composer-rich"`) || !strings.Contains(md, `name="body"`) || !strings.Contains(md, "words") {
t.Error("FAIL: a markdown page is not edited in the composer holding its text")
}
if fw := edit(false); strings.Contains(fw, `data-hook="composer-rich"`) || !strings.Contains(fw, `<textarea name="body"`) {
t.Error("FAIL: a fossil-wiki page is offered Rich, which would write markdown into it")
}
}