Barazo default frontend
barazo.forum
1/**
2 * Tests for reply tree builder utility.
3 */
4
5import { describe, it, expect } from 'vitest'
6import type { Reply } from '@/lib/api/types'
7import { buildReplyTree, flattenReplyTree, countDescendants } from './build-reply-tree'
8
9const TOPIC_URI = 'at://did:plc:user-001/forum.barazo.topic.post/abc123'
10const TOPIC_CID = 'bafyreib1'
11const COMMUNITY_DID = 'did:plc:community-001'
12
13function makeReply(
14 overrides: Partial<Reply> & { uri: string; parentUri: string; depth: number }
15): Reply {
16 return {
17 rkey: overrides.uri.split('/').pop()!,
18 authorDid: 'did:plc:user-001',
19 content: 'Test reply',
20 rootUri: TOPIC_URI,
21 rootCid: TOPIC_CID,
22 parentCid: 'bafyreir0',
23 communityDid: COMMUNITY_DID,
24 cid: `cid-${overrides.uri}`,
25 reactionCount: 0,
26 isAuthorDeleted: false,
27 isModDeleted: false,
28 createdAt: '2026-02-14T12:00:00.000Z',
29 indexedAt: '2026-02-14T12:00:00.000Z',
30 ...overrides,
31 }
32}
33
34describe('buildReplyTree', () => {
35 it('returns empty roots for empty array', () => {
36 const result = buildReplyTree([], TOPIC_URI)
37 expect(result).toEqual([])
38 })
39
40 it('places direct reply to topic as root', () => {
41 const reply = makeReply({
42 uri: 'at://user/reply/aaa',
43 parentUri: TOPIC_URI,
44 depth: 1,
45 })
46
47 const result = buildReplyTree([reply], TOPIC_URI)
48 expect(result).toHaveLength(1)
49 expect(result[0]!.reply.uri).toBe(reply.uri)
50 expect(result[0]!.children).toEqual([])
51 })
52
53 it('nests child reply under its parent', () => {
54 const parent = makeReply({
55 uri: 'at://user/reply/aaa',
56 parentUri: TOPIC_URI,
57 depth: 1,
58 })
59 const child = makeReply({
60 uri: 'at://user/reply/bbb',
61 parentUri: 'at://user/reply/aaa',
62 depth: 2,
63 })
64
65 const result = buildReplyTree([parent, child], TOPIC_URI)
66 expect(result).toHaveLength(1)
67 expect(result[0]!.children).toHaveLength(1)
68 expect(result[0]!.children[0]!.reply.uri).toBe(child.uri)
69 })
70
71 it('treats orphaned reply (parent not in array) as root', () => {
72 const orphan = makeReply({
73 uri: 'at://user/reply/bbb',
74 parentUri: 'at://user/reply/missing',
75 depth: 2,
76 })
77
78 const result = buildReplyTree([orphan], TOPIC_URI)
79 expect(result).toHaveLength(1)
80 expect(result[0]!.reply.uri).toBe(orphan.uri)
81 })
82
83 it('maintains chronological order for multiple root-level replies', () => {
84 const first = makeReply({
85 uri: 'at://user/reply/aaa',
86 parentUri: TOPIC_URI,
87 depth: 1,
88 createdAt: '2026-02-14T10:00:00.000Z',
89 })
90 const second = makeReply({
91 uri: 'at://user/reply/bbb',
92 parentUri: TOPIC_URI,
93 depth: 1,
94 createdAt: '2026-02-14T11:00:00.000Z',
95 })
96 const third = makeReply({
97 uri: 'at://user/reply/ccc',
98 parentUri: TOPIC_URI,
99 depth: 1,
100 createdAt: '2026-02-14T12:00:00.000Z',
101 })
102
103 const result = buildReplyTree([first, second, third], TOPIC_URI)
104 expect(result).toHaveLength(3)
105 expect(result[0]!.reply.uri).toBe(first.uri)
106 expect(result[1]!.reply.uri).toBe(second.uri)
107 expect(result[2]!.reply.uri).toBe(third.uri)
108 })
109
110 it('handles deep nesting (5+ levels)', () => {
111 const replies: Reply[] = []
112 for (let i = 1; i <= 6; i++) {
113 replies.push(
114 makeReply({
115 uri: `at://user/reply/${String(i).padStart(3, '0')}`,
116 parentUri: i === 1 ? TOPIC_URI : `at://user/reply/${String(i - 1).padStart(3, '0')}`,
117 depth: i,
118 })
119 )
120 }
121
122 const result = buildReplyTree(replies, TOPIC_URI)
123 expect(result).toHaveLength(1)
124
125 let node = result[0]!
126 for (let i = 0; i < 5; i++) {
127 expect(node.children).toHaveLength(1)
128 node = node.children[0]!
129 }
130 expect(node.children).toHaveLength(0)
131 })
132
133 it('handles mixed order input (children before parents)', () => {
134 const child = makeReply({
135 uri: 'at://user/reply/bbb',
136 parentUri: 'at://user/reply/aaa',
137 depth: 2,
138 })
139 const parent = makeReply({
140 uri: 'at://user/reply/aaa',
141 parentUri: TOPIC_URI,
142 depth: 1,
143 })
144
145 // child comes before parent in input
146 const result = buildReplyTree([child, parent], TOPIC_URI)
147 expect(result).toHaveLength(1)
148 expect(result[0]!.reply.uri).toBe(parent.uri)
149 expect(result[0]!.children).toHaveLength(1)
150 expect(result[0]!.children[0]!.reply.uri).toBe(child.uri)
151 })
152})
153
154describe('countDescendants', () => {
155 it('returns 0 for a leaf node', () => {
156 const leaf = makeReply({ uri: 'at://user/reply/aaa', parentUri: TOPIC_URI, depth: 1 })
157 expect(countDescendants({ reply: leaf, children: [] })).toBe(0)
158 })
159
160 it('counts all descendants recursively', () => {
161 const replies = [
162 makeReply({ uri: 'at://user/reply/a', parentUri: TOPIC_URI, depth: 1 }),
163 makeReply({ uri: 'at://user/reply/b', parentUri: 'at://user/reply/a', depth: 2 }),
164 makeReply({ uri: 'at://user/reply/c', parentUri: 'at://user/reply/b', depth: 3 }),
165 makeReply({ uri: 'at://user/reply/d', parentUri: 'at://user/reply/c', depth: 4 }),
166 ]
167 const tree = buildReplyTree(replies, TOPIC_URI)
168 // Root node has 3 descendants: b, c, d
169 expect(countDescendants(tree[0]!)).toBe(3)
170 // Node b has 2 descendants: c, d
171 expect(countDescendants(tree[0]!.children[0]!)).toBe(2)
172 // Node c has 1 descendant: d
173 expect(countDescendants(tree[0]!.children[0]!.children[0]!)).toBe(1)
174 })
175
176 it('counts branching descendants', () => {
177 const replies = [
178 makeReply({ uri: 'at://user/reply/a', parentUri: TOPIC_URI, depth: 1 }),
179 makeReply({ uri: 'at://user/reply/b', parentUri: 'at://user/reply/a', depth: 2 }),
180 makeReply({ uri: 'at://user/reply/c', parentUri: 'at://user/reply/a', depth: 2 }),
181 makeReply({ uri: 'at://user/reply/d', parentUri: 'at://user/reply/b', depth: 3 }),
182 ]
183 const tree = buildReplyTree(replies, TOPIC_URI)
184 // Root has 3 descendants: b, c, d
185 expect(countDescendants(tree[0]!)).toBe(3)
186 })
187})
188
189describe('flattenReplyTree', () => {
190 it('returns empty array for empty roots', () => {
191 expect(flattenReplyTree([])).toEqual([])
192 })
193
194 it('returns depth-first order', () => {
195 const root = makeReply({
196 uri: 'at://user/reply/aaa',
197 parentUri: TOPIC_URI,
198 depth: 1,
199 })
200 const child = makeReply({
201 uri: 'at://user/reply/bbb',
202 parentUri: 'at://user/reply/aaa',
203 depth: 2,
204 })
205 const grandchild = makeReply({
206 uri: 'at://user/reply/ccc',
207 parentUri: 'at://user/reply/bbb',
208 depth: 3,
209 })
210 const root2 = makeReply({
211 uri: 'at://user/reply/ddd',
212 parentUri: TOPIC_URI,
213 depth: 1,
214 })
215
216 const tree = buildReplyTree([root, child, grandchild, root2], TOPIC_URI)
217 const flat = flattenReplyTree(tree)
218
219 expect(flat.map((r) => r.uri)).toEqual([root.uri, child.uri, grandchild.uri, root2.uri])
220 })
221})