diff options
| author | Gabriel Schneider <[email protected]> | 2026-08-01 16:18:19 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-08-01 18:21:40 -0300 |
| commit | 151ea12251ed876aa87bb486da1bc337e25369e9 (patch) | |
| tree | 0363d90b5df6fc8b17b1737ef2793ba3825ba4d4 /vrsite/app_test.go | |
| parent | 8ec4e135e318c7799b2576a8729493c063da4a97 (diff) | |
| download | notevi-151ea12251ed876aa87bb486da1bc337e25369e9.tar.gz notevi-151ea12251ed876aa87bb486da1bc337e25369e9.zip | |
Diffstat (limited to 'vrsite/app_test.go')
| -rw-r--r-- | vrsite/app_test.go | 98 |
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 & 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) { |
