summaryrefslogtreecommitdiff
path: root/vrsite/app_test.go
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-08-01 16:18:19 -0300
committerGabriel Schneider <[email protected]>2026-08-01 18:21:40 -0300
commit151ea12251ed876aa87bb486da1bc337e25369e9 (patch)
tree0363d90b5df6fc8b17b1737ef2793ba3825ba4d4 /vrsite/app_test.go
parent8ec4e135e318c7799b2576a8729493c063da4a97 (diff)
downloadnotevi-151ea12251ed876aa87bb486da1bc337e25369e9.tar.gz
notevi-151ea12251ed876aa87bb486da1bc337e25369e9.zip
Diffstat (limited to 'vrsite/app_test.go')
-rw-r--r--vrsite/app_test.go98
1 files changed, 95 insertions, 3 deletions
diff --git a/vrsite/app_test.go b/vrsite/app_test.go
index 4db74d4..403903a 100644
--- a/vrsite/app_test.go
+++ b/vrsite/app_test.go
@@ -31,6 +31,12 @@ func testSite(t *testing.T) *Site {
return s
}
+// testThemes stands in for whatever themes this machine happens to have, so
+// the picker's two files are the same in every test run.
+func testThemes(dark, light *Theme) *themeSet {
+ return themesFrom([]*Theme{dark, light}, dark, light)
+}
+
func TestBuildModel(t *testing.T) {
s := testSite(t)
if s.Change != "aaa" {
@@ -51,6 +57,42 @@ func TestBuildModel(t *testing.T) {
}
}
+func TestNoteThreadRecursesThroughRepliesAndMentions(t *testing.T) {
+ entries := parseLog([]byte(logLines(
+ `{"op":"note","session":"s","agent":"a","change":"aaa","note_id":1,"text":"root"}`,
+ `{"op":"note","session":"s","agent":"a","change":"aaa","note_id":2,"reply_to":1,"text":"child"}`,
+ `{"op":"note","session":"s","agent":"a","change":"aaa","note_id":3,"reply_to":2,"text":"grandchild"}`,
+ `{"op":"note","session":"s","agent":"a","change":"aaa","note_id":4,"text":"[mentions root](../note/1.html)"}`,
+ `{"op":"note","session":"s","agent":"a","change":"aaa","note_id":5,"reply_to":3,"text":"[also mentions child](note/2)"}`,
+ `{"op":"note","session":"s","agent":"a","change":"aaa","note_id":6,"reply_to":5,"text":"arbitrarily deeper"}`,
+ )))
+ s := buildModel("test", entries, "")
+ root, child := s.NoteByID(1), s.NoteByID(2)
+ if len(root.Replies) != 1 || root.Replies[0].ID != 2 || len(root.Mentions) != 1 || root.Mentions[0].ID != 4 {
+ t.Fatalf("root edges = replies %+v, mentions %+v", root.Replies, root.Mentions)
+ }
+ if len(child.Mentions) != 1 || child.Mentions[0].ID != 5 {
+ t.Fatalf("child mentions = %+v", child.Mentions)
+ }
+
+ thread := s.NoteThread(1)
+ if thread == nil || len(thread.Children) != 2 || thread.Children[0].Via != "reply" || thread.Children[1].Via != "mention" {
+ t.Fatalf("root thread = %+v", thread)
+ }
+ // 1 → 2 → 3 → 5 → 6 proves the renderer's data is recursive rather
+ // than a fixed parent/child pair.
+ deep := thread.Children[0].Children[0].Children[0]
+ if deep.Note.ID != 5 || len(deep.Children) != 1 || deep.Children[0].Note.ID != 6 {
+ t.Fatalf("deep reply branch = %+v", deep)
+ }
+ // Note 5 also mentions note 2. It is fully rendered under note 3, then left
+ // as a DAG reference on the second path instead of recursing twice.
+ join := thread.Children[0].Children[1]
+ if join.Note.ID != 5 || !join.Reference || join.Via != "mention" {
+ t.Errorf("DAG join = %+v", join)
+ }
+}
+
// A ninth agent gets no color instead of overflowing the uint8 mask (and the
// per-mask CSS it would generate).
func TestAgentColorsAreBounded(t *testing.T) {
@@ -326,7 +368,8 @@ func testServer(t *testing.T) (*server, string, string) {
if err := os.WriteFile(logPath, []byte(logLines(entry, note)), 0o644); err != nil {
t.Fatal(err)
}
- sv := newServer(logPath, repo, "test", "tester", loadTheme("", "", "dark"), loadTheme("", "", "light"))
+ dark, light := loadTheme("", "", "dark"), loadTheme("", "", "light")
+ sv := newServer(logPath, repo, "test", "tester", dark, light, testThemes(dark, light))
return sv, logPath, change
}
@@ -352,7 +395,9 @@ func TestServerPages(t *testing.T) {
{"/code/hello.txt", "three"},
{"/jj", "revset"},
{"/change/" + change, "first change"},
- {"/style.css", "--am1"},
+ {"/style.css", ".am1 {"},
+ {"/themes.css", `:root[data-theme="zed/one-light"]`},
+ {"/themes.js", "VRSITE_THEMES"},
{"/app.js", "vrsite-mode"},
{"/htmx.min.js", "htmx"},
} {
@@ -553,7 +598,7 @@ func TestServerHoldsNoStateOfItsOwn(t *testing.T) {
h.ServeHTTP(httptest.NewRecorder(), r)
// a second server that has only ever seen the log file
- fresh := newServer(logPath, sv.repo, "test", "tester", sv.theme, sv.light).handler()
+ fresh := newServer(logPath, sv.repo, "test", "tester", sv.theme, sv.light, sv.themes).handler()
for _, path := range []string{"/", "/notes", "/notes?scope=all", "/code/hello.txt",
"/files", "/tree", "/change/" + change} {
was := agoRe.ReplaceAllString(get(t, h, path, false).Body.String(), "AGO")
@@ -662,6 +707,43 @@ func TestAddNoteAppendsToLog(t *testing.T) {
}
}
+func TestFocusedNoteRendersAndAppendsAnArbitrarilyDeepThread(t *testing.T) {
+ sv, logPath, change := testServer(t)
+ h := sv.handler()
+ post := func(values url.Values) {
+ t.Helper()
+ values.Set("change", change)
+ r := httptest.NewRequest("POST", "/notes", strings.NewReader(values.Encode()))
+ r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
+ w := httptest.NewRecorder()
+ h.ServeHTTP(w, r)
+ if w.Code != http.StatusSeeOther {
+ t.Fatalf("POST /notes = %d: %s", w.Code, w.Body)
+ }
+ }
+ post(url.Values{"reply_to": {"1"}, "text": {"child from focused view"}})
+ post(url.Values{"reply_to": {"2"}, "text": {"grandchild from focused view"}})
+ post(url.Values{"text": {"[backlink to root](/note/1)"}})
+
+ entries := parseLog(mustRead(t, logPath))
+ if len(entries) < 5 || entries[len(entries)-3].ReplyTo != 1 || entries[len(entries)-2].ReplyTo != 2 || entries[len(entries)-1].ReplyTo != 0 {
+ t.Fatalf("thread entries were not appended as explicit replies plus a plain mention: %+v", entries)
+ }
+ body := get(t, h, "/note/1", false).Body.String()
+ for _, want := range []string{
+ "child from focused view", "grandchild from focused view", "backlink to root",
+ `class="threadnode via-reply"`, `class="threadnode via-mention"`,
+ `name="reply_to" value="2"`, "reply to #2", "Replies &amp; mentions",
+ } {
+ if !strings.Contains(body, want) {
+ t.Errorf("focused thread is missing %q", want)
+ }
+ }
+ if strings.Index(body, "grandchild from focused view") < strings.Index(body, "child from focused view") {
+ t.Error("grandchild rendered outside its parent branch")
+ }
+}
+
func TestAddNoteRejectsCrossOriginAndEmptyText(t *testing.T) {
sv, _, _ := testServer(t)
h := sv.handler()
@@ -683,6 +765,16 @@ func TestAddNoteRejectsCrossOriginAndEmptyText(t *testing.T) {
if w.Code != http.StatusBadRequest {
t.Errorf("empty note = %d, want 400", w.Code)
}
+
+ r = httptest.NewRequest("POST", "/notes", strings.NewReader(url.Values{
+ "text": {"orphan"}, "reply_to": {"999"},
+ }.Encode()))
+ r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
+ w = httptest.NewRecorder()
+ h.ServeHTTP(w, r)
+ if w.Code != http.StatusBadRequest {
+ t.Errorf("missing reply target = %d, want 400", w.Code)
+ }
}
func TestJJQueries(t *testing.T) {