mirror of
https://seed.radicle.dev/rad:z4D5UCArafTzTQpDZNQRuqswh3ury.git
synced 2026-10-24 05:50:35 +08:00
Revision and ReviewPage built threads and checked who may resolve a comment with their own near-identical copies.
182 lines
5.1 KiB
TypeScript
182 lines
5.1 KiB
TypeScript
import { describe, expect, test } from "vitest";
|
|
|
|
import {
|
|
buildThreads,
|
|
codeThreadsByReview,
|
|
discussionThreadsByReview,
|
|
groupThreadsByFile,
|
|
isCodeRoot,
|
|
reviewDiscussionThreads,
|
|
reviewIdByComment,
|
|
revisionDiscussionThreads,
|
|
revisionIdByComment,
|
|
} from "@app/lib/threads";
|
|
|
|
import {
|
|
author,
|
|
comment,
|
|
location,
|
|
review,
|
|
revision,
|
|
thread,
|
|
} from "./support/cobs";
|
|
|
|
const alice = author("alice");
|
|
const bob = author("bob");
|
|
const code = location("a.ts", "new", 1);
|
|
|
|
function ids(threads: { root: { id: string }; replies: { id: string }[] }[]) {
|
|
return threads.map(t => [t.root.id, ...t.replies.map(r => r.id)]);
|
|
}
|
|
|
|
describe("buildThreads", () => {
|
|
test("attaches replies to their root, oldest first", () => {
|
|
const threads = buildThreads(
|
|
[
|
|
comment({ id: "root" }),
|
|
comment({ id: "late", replyTo: "root", timestamp: 20 }),
|
|
comment({ id: "early", replyTo: "root", timestamp: 10 }),
|
|
comment({ id: "other" }),
|
|
],
|
|
c => !c.replyTo,
|
|
);
|
|
|
|
expect(ids(threads)).toEqual([["root", "early", "late"], ["other"]]);
|
|
});
|
|
|
|
test("isCodeRoot picks located comments that aren't replies", () => {
|
|
expect(isCodeRoot(comment({ location: code }))).toBe(true);
|
|
expect(isCodeRoot(comment({ location: code, replyTo: "x" }))).toBe(false);
|
|
expect(isCodeRoot(comment({ location: null }))).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("threads by review", () => {
|
|
const reviews = [
|
|
review({
|
|
id: "r1",
|
|
comments: [
|
|
comment({ id: "code", location: code }),
|
|
comment({ id: "code-reply", location: code, replyTo: "code" }),
|
|
comment({ id: "general" }),
|
|
comment({ id: "on-review", location: code, replyTo: "r1" }),
|
|
],
|
|
}),
|
|
review({ id: "r2", comments: [] }),
|
|
];
|
|
|
|
test("codeThreadsByReview keeps reviews with code threads", () => {
|
|
const map = codeThreadsByReview(reviews);
|
|
|
|
expect([...map.keys()]).toEqual(["r1"]);
|
|
expect(ids(map.get("r1") ?? [])).toEqual([["code", "code-reply"]]);
|
|
});
|
|
|
|
test("a reply to the review itself starts a discussion thread", () => {
|
|
expect(ids(reviewDiscussionThreads(reviews[0]))).toEqual([
|
|
["general"],
|
|
["on-review"],
|
|
]);
|
|
});
|
|
|
|
test("discussionThreadsByReview keeps reviews with discussion threads", () => {
|
|
expect([...discussionThreadsByReview(reviews).keys()]).toEqual(["r1"]);
|
|
});
|
|
|
|
test("reviewIdByComment maps roots and replies to their review", () => {
|
|
const map = reviewIdByComment(
|
|
codeThreadsByReview(reviews),
|
|
discussionThreadsByReview(reviews),
|
|
);
|
|
|
|
expect(Object.fromEntries(map)).toEqual({
|
|
code: "r1",
|
|
"code-reply": "r1",
|
|
general: "r1",
|
|
"on-review": "r1",
|
|
});
|
|
});
|
|
});
|
|
|
|
test("revisionIdByComment maps discussion comments to their revision", () => {
|
|
const map = revisionIdByComment([
|
|
revision({ id: "v1", discussion: [comment({ id: "a" })] }),
|
|
revision({ id: "v2", discussion: [comment({ id: "b" })] }),
|
|
]);
|
|
|
|
expect(Object.fromEntries(map)).toEqual({ a: "v1", b: "v2" });
|
|
});
|
|
|
|
test("groupThreadsByFile groups by path in order of first appearance", () => {
|
|
const groups = groupThreadsByFile([
|
|
thread(comment({ id: "b1", location: location("b.ts", "new", 1) })),
|
|
thread(comment({ id: "a1", location: location("a.ts", "new", 1) })),
|
|
thread(comment({ id: "b2", location: location("b.ts", "old", 4) })),
|
|
thread(comment({ id: "none", location: null })),
|
|
]);
|
|
|
|
expect(groups.map(g => [g.path, ...g.threads.map(t => t.root.id)])).toEqual([
|
|
["b.ts", "b1", "b2"],
|
|
["a.ts", "a1"],
|
|
]);
|
|
});
|
|
|
|
describe("revisionDiscussionThreads", () => {
|
|
test("collects each revision's plain comments and replies to it", () => {
|
|
const threads = revisionDiscussionThreads([
|
|
revision({
|
|
id: "v1",
|
|
discussion: [
|
|
comment({ id: "c1" }),
|
|
comment({ id: "reply", replyTo: "c1" }),
|
|
comment({ id: "code", location: code }),
|
|
comment({ id: "on-revision", replyTo: "v1" }),
|
|
],
|
|
}),
|
|
revision({ id: "v2", discussion: [comment({ id: "c2" })] }),
|
|
]);
|
|
|
|
expect(ids(threads)).toEqual([["c1", "reply"], ["on-revision"], ["c2"]]);
|
|
});
|
|
|
|
test("skips the revision's own description comment", () => {
|
|
expect(
|
|
revisionDiscussionThreads([
|
|
revision({ id: "v1", discussion: [comment({ id: "v1" })] }),
|
|
]),
|
|
).toEqual([]);
|
|
});
|
|
|
|
test("skips a comment repeating its author's review summary", () => {
|
|
const threads = revisionDiscussionThreads([
|
|
revision({
|
|
id: "v1",
|
|
reviews: [review({ author: alice, summary: "LGTM" })],
|
|
discussion: [
|
|
comment({
|
|
id: "repeat",
|
|
author: alice,
|
|
edits: [edit(alice, "LGTM")],
|
|
}),
|
|
comment({
|
|
id: "other-author",
|
|
author: bob,
|
|
edits: [edit(bob, "LGTM")],
|
|
}),
|
|
comment({
|
|
id: "edited",
|
|
author: alice,
|
|
edits: [edit(alice, "LGTM"), edit(alice, "Changed")],
|
|
}),
|
|
],
|
|
}),
|
|
]);
|
|
|
|
expect(ids(threads)).toEqual([["other-author"], ["edited"]]);
|
|
});
|
|
});
|
|
|
|
function edit(by: typeof alice, body: string) {
|
|
return { author: by, timestamp: 0, body };
|
|
}
|