an app to share curated trails sidetrail.app
1

Configure Feed

Select the types of activity you want to include in your feed.

1/** 2 * User Profile Tests 3 * 4 * Scenarios covering user profiles: published trails, completed trails, 5 * and how activity affects what's shown. 6 */ 7 8import { describe, it, expect } from "vitest"; 9import * as queries from "../queries"; 10import { emit, emitDelete, ALICE, BOB, CAROL, generateTid } from "./helpers"; 11 12const now = () => new Date().toISOString(); 13 14describe("Viewing a user's published trails", () => { 15 it("shows all trails created by the user", async () => { 16 await emit("app.sidetrail.trail", ALICE.did, { 17 $type: "app.sidetrail.trail", 18 title: "Alice's First Trail", 19 description: "Her debut", 20 stops: [{ tid: generateTid(), title: "Stop", content: "Content" }], 21 accentColor: "#ff0000", 22 backgroundColor: "#ffffff", 23 createdAt: now(), 24 }); 25 26 await emit("app.sidetrail.trail", ALICE.did, { 27 $type: "app.sidetrail.trail", 28 title: "Alice's Second Trail", 29 description: "Another one", 30 stops: [{ tid: generateTid(), title: "Stop", content: "Content" }], 31 accentColor: "#00ff00", 32 backgroundColor: "#f0f0f0", 33 createdAt: now(), 34 }); 35 36 const publishedTrails = await queries.loadUserPublishedTrails(ALICE.handle); 37 expect(publishedTrails).toHaveLength(2); 38 }); 39 40 it("orders published trails by createdAt descending", async () => { 41 await emit("app.sidetrail.trail", ALICE.did, { 42 $type: "app.sidetrail.trail", 43 title: "Old Trail", 44 description: "First", 45 stops: [{ tid: generateTid(), title: "Stop", content: "Content" }], 46 accentColor: "#111", 47 backgroundColor: "#eee", 48 createdAt: "2024-01-01T10:00:00Z", 49 }); 50 51 await emit("app.sidetrail.trail", ALICE.did, { 52 $type: "app.sidetrail.trail", 53 title: "New Trail", 54 description: "Last", 55 stops: [{ tid: generateTid(), title: "Stop", content: "Content" }], 56 accentColor: "#222", 57 backgroundColor: "#ddd", 58 createdAt: "2024-06-01T10:00:00Z", 59 }); 60 61 const publishedTrails = await queries.loadUserPublishedTrails(ALICE.handle); 62 expect(publishedTrails[0].title).toBe("New Trail"); 63 expect(publishedTrails[1].title).toBe("Old Trail"); 64 }); 65 66 it("published trail cards show active walkers", async () => { 67 const stopTid = generateTid(); 68 const trail = await emit("app.sidetrail.trail", ALICE.did, { 69 $type: "app.sidetrail.trail", 70 title: "Popular Trail", 71 description: "Many walkers", 72 stops: [{ tid: stopTid, title: "Stop", content: "Content" }], 73 accentColor: "#333", 74 backgroundColor: "#ccc", 75 createdAt: now(), 76 }); 77 78 await emit("app.sidetrail.walk", BOB.did, { 79 $type: "app.sidetrail.walk", 80 trail: { uri: trail.uri, cid: trail.cid }, 81 visitedStops: [stopTid], 82 createdAt: now(), 83 updatedAt: now(), 84 }); 85 86 const publishedTrails = await queries.loadUserPublishedTrails(ALICE.handle); 87 const activeWalkers = await queries.loadTrailActiveWalkers(publishedTrails[0].uri); 88 expect(activeWalkers).toHaveLength(1); 89 expect(activeWalkers[0].handle).toBe(BOB.handle); 90 }); 91 92 it("does not show other users' trails", async () => { 93 await emit("app.sidetrail.trail", ALICE.did, { 94 $type: "app.sidetrail.trail", 95 title: "Alice's Trail", 96 description: "By Alice", 97 stops: [{ tid: generateTid(), title: "Stop", content: "Content" }], 98 accentColor: "#aaa", 99 backgroundColor: "#bbb", 100 createdAt: now(), 101 }); 102 103 await emit("app.sidetrail.trail", BOB.did, { 104 $type: "app.sidetrail.trail", 105 title: "Bob's Trail", 106 description: "By Bob", 107 stops: [{ tid: generateTid(), title: "Stop", content: "Content" }], 108 accentColor: "#ccc", 109 backgroundColor: "#ddd", 110 createdAt: now(), 111 }); 112 113 const aliceTrails = await queries.loadUserPublishedTrails(ALICE.handle); 114 expect(aliceTrails).toHaveLength(1); 115 expect(aliceTrails[0].title).toBe("Alice's Trail"); 116 117 const bobTrails = await queries.loadUserPublishedTrails(BOB.handle); 118 expect(bobTrails).toHaveLength(1); 119 expect(bobTrails[0].title).toBe("Bob's Trail"); 120 }); 121}); 122 123describe("Viewing a user's completed trails", () => { 124 it("shows trails the user has completed", async () => { 125 const trail = await emit("app.sidetrail.trail", ALICE.did, { 126 $type: "app.sidetrail.trail", 127 title: "Completable Trail", 128 description: "Finish this", 129 stops: [{ tid: generateTid(), title: "Stop", content: "Content" }], 130 accentColor: "#ff0000", 131 backgroundColor: "#ffffff", 132 createdAt: now(), 133 }); 134 135 await emit("app.sidetrail.completion", BOB.did, { 136 $type: "app.sidetrail.completion", 137 trail: { uri: trail.uri, cid: trail.cid }, 138 createdAt: now(), 139 }); 140 141 const completedTrails = await queries.loadUserCompletedTrails(BOB.handle); 142 expect(completedTrails).toHaveLength(1); 143 expect(completedTrails[0].title).toBe("Completable Trail"); 144 expect(completedTrails[0].creator.handle).toBe(ALICE.handle); 145 }); 146 147 it("orders completed trails by completion date descending", async () => { 148 const trail1 = await emit("app.sidetrail.trail", ALICE.did, { 149 $type: "app.sidetrail.trail", 150 title: "First Completed", 151 description: "Test", 152 stops: [{ tid: generateTid(), title: "Stop", content: "Content" }], 153 accentColor: "#111", 154 backgroundColor: "#eee", 155 createdAt: now(), 156 }); 157 158 const trail2 = await emit("app.sidetrail.trail", ALICE.did, { 159 $type: "app.sidetrail.trail", 160 title: "Last Completed", 161 description: "Test", 162 stops: [{ tid: generateTid(), title: "Stop", content: "Content" }], 163 accentColor: "#222", 164 backgroundColor: "#ddd", 165 createdAt: now(), 166 }); 167 168 await emit("app.sidetrail.completion", BOB.did, { 169 $type: "app.sidetrail.completion", 170 trail: { uri: trail1.uri, cid: trail1.cid }, 171 createdAt: "2024-01-01T10:00:00Z", 172 }); 173 174 await emit("app.sidetrail.completion", BOB.did, { 175 $type: "app.sidetrail.completion", 176 trail: { uri: trail2.uri, cid: trail2.cid }, 177 createdAt: "2024-06-01T10:00:00Z", 178 }); 179 180 const completedTrails = await queries.loadUserCompletedTrails(BOB.handle); 181 expect(completedTrails[0].title).toBe("Last Completed"); 182 expect(completedTrails[1].title).toBe("First Completed"); 183 }); 184 185 it("dedupes multiple completions of same trail (shows most recent)", async () => { 186 const trail = await emit("app.sidetrail.trail", ALICE.did, { 187 $type: "app.sidetrail.trail", 188 title: "Re-completable", 189 description: "Completed twice", 190 stops: [{ tid: generateTid(), title: "Stop", content: "Content" }], 191 accentColor: "#333", 192 backgroundColor: "#ccc", 193 createdAt: now(), 194 }); 195 196 // First completion 197 await emit("app.sidetrail.completion", BOB.did, { 198 $type: "app.sidetrail.completion", 199 trail: { uri: trail.uri, cid: trail.cid }, 200 createdAt: "2024-01-01T10:00:00Z", 201 }); 202 203 // Second completion (newer) 204 await emit("app.sidetrail.completion", BOB.did, { 205 $type: "app.sidetrail.completion", 206 trail: { uri: trail.uri, cid: trail.cid }, 207 createdAt: "2024-06-01T10:00:00Z", 208 }); 209 210 const completedTrails = await queries.loadUserCompletedTrails(BOB.handle); 211 expect(completedTrails).toHaveLength(1); 212 expect(completedTrails[0].completedAt.toISOString()).toBe("2024-06-01T10:00:00.000Z"); 213 }); 214 215 it("excludes completions where trail was deleted", async () => { 216 const trail = await emit("app.sidetrail.trail", ALICE.did, { 217 $type: "app.sidetrail.trail", 218 title: "Soon Deleted", 219 description: "Will be orphaned", 220 stops: [{ tid: generateTid(), title: "Stop", content: "Content" }], 221 accentColor: "#444", 222 backgroundColor: "#bbb", 223 createdAt: now(), 224 }); 225 226 await emit("app.sidetrail.completion", BOB.did, { 227 $type: "app.sidetrail.completion", 228 trail: { uri: trail.uri, cid: trail.cid }, 229 createdAt: now(), 230 }); 231 232 let completedTrails = await queries.loadUserCompletedTrails(BOB.handle); 233 expect(completedTrails).toHaveLength(1); 234 235 // Trail gets deleted 236 await emitDelete("app.sidetrail.trail", trail.uri); 237 238 completedTrails = await queries.loadUserCompletedTrails(BOB.handle); 239 expect(completedTrails).toHaveLength(0); 240 }); 241}); 242 243describe("Profile showing both published and completed", () => { 244 it("user can have both published and completed trails", async () => { 245 // Alice publishes a trail 246 await emit("app.sidetrail.trail", ALICE.did, { 247 $type: "app.sidetrail.trail", 248 title: "Alice's Published Trail", 249 description: "Created by Alice", 250 stops: [{ tid: generateTid(), title: "Stop", content: "Content" }], 251 accentColor: "#ff0000", 252 backgroundColor: "#ffffff", 253 createdAt: now(), 254 }); 255 256 // Bob publishes a trail 257 const bobTrail = await emit("app.sidetrail.trail", BOB.did, { 258 $type: "app.sidetrail.trail", 259 title: "Bob's Trail", 260 description: "Created by Bob", 261 stops: [{ tid: generateTid(), title: "Stop", content: "Content" }], 262 accentColor: "#00ff00", 263 backgroundColor: "#f0f0f0", 264 createdAt: now(), 265 }); 266 267 // Alice completes Bob's trail 268 await emit("app.sidetrail.completion", ALICE.did, { 269 $type: "app.sidetrail.completion", 270 trail: { uri: bobTrail.uri, cid: bobTrail.cid }, 271 createdAt: now(), 272 }); 273 274 const publishedTrails = await queries.loadUserPublishedTrails(ALICE.handle); 275 expect(publishedTrails).toHaveLength(1); 276 expect(publishedTrails[0].title).toBe("Alice's Published Trail"); 277 278 const completedTrails = await queries.loadUserCompletedTrails(ALICE.handle); 279 expect(completedTrails).toHaveLength(1); 280 expect(completedTrails[0].title).toBe("Bob's Trail"); 281 }); 282}); 283 284describe("Deleting a completion", () => { 285 it("removes completion from user's profile", async () => { 286 const trail = await emit("app.sidetrail.trail", ALICE.did, { 287 $type: "app.sidetrail.trail", 288 title: "Uncomplete Trail", 289 description: "Remove completion", 290 stops: [{ tid: generateTid(), title: "Stop", content: "Content" }], 291 accentColor: "#555", 292 backgroundColor: "#aaa", 293 createdAt: now(), 294 }); 295 296 const completion = await emit("app.sidetrail.completion", BOB.did, { 297 $type: "app.sidetrail.completion", 298 trail: { uri: trail.uri, cid: trail.cid }, 299 createdAt: now(), 300 }); 301 302 let completedTrails = await queries.loadUserCompletedTrails(BOB.handle); 303 expect(completedTrails).toHaveLength(1); 304 305 await emitDelete("app.sidetrail.completion", completion.uri); 306 307 completedTrails = await queries.loadUserCompletedTrails(BOB.handle); 308 expect(completedTrails).toHaveLength(0); 309 }); 310}); 311 312describe("Empty profile states", () => { 313 it("user with no activity has empty lists", async () => { 314 const publishedTrails = await queries.loadUserPublishedTrails(ALICE.handle); 315 const completedTrails = await queries.loadUserCompletedTrails(ALICE.handle); 316 expect(publishedTrails).toHaveLength(0); 317 expect(completedTrails).toHaveLength(0); 318 }); 319}); 320 321describe("Profile trail deletion cascade", () => { 322 it("deleting a trail removes it from author's published trails", async () => { 323 const trail = await emit("app.sidetrail.trail", ALICE.did, { 324 $type: "app.sidetrail.trail", 325 title: "Ephemeral Trail", 326 description: "Will be deleted", 327 stops: [{ tid: generateTid(), title: "Stop", content: "Content" }], 328 accentColor: "#666", 329 backgroundColor: "#999", 330 createdAt: now(), 331 }); 332 333 let publishedTrails = await queries.loadUserPublishedTrails(ALICE.handle); 334 expect(publishedTrails).toHaveLength(1); 335 336 await emitDelete("app.sidetrail.trail", trail.uri); 337 338 publishedTrails = await queries.loadUserPublishedTrails(ALICE.handle); 339 expect(publishedTrails).toHaveLength(0); 340 }); 341}); 342 343describe("Profile with multiple completers", () => { 344 it("each user's profile shows their own completions", async () => { 345 const trail = await emit("app.sidetrail.trail", ALICE.did, { 346 $type: "app.sidetrail.trail", 347 title: "Popular Trail", 348 description: "Many completers", 349 stops: [{ tid: generateTid(), title: "Stop", content: "Content" }], 350 accentColor: "#777", 351 backgroundColor: "#888", 352 createdAt: now(), 353 }); 354 355 await emit("app.sidetrail.completion", BOB.did, { 356 $type: "app.sidetrail.completion", 357 trail: { uri: trail.uri, cid: trail.cid }, 358 createdAt: now(), 359 }); 360 361 await emit("app.sidetrail.completion", CAROL.did, { 362 $type: "app.sidetrail.completion", 363 trail: { uri: trail.uri, cid: trail.cid }, 364 createdAt: now(), 365 }); 366 367 const bobCompleted = await queries.loadUserCompletedTrails(BOB.handle); 368 expect(bobCompleted).toHaveLength(1); 369 370 const carolCompleted = await queries.loadUserCompletedTrails(CAROL.handle); 371 expect(carolCompleted).toHaveLength(1); 372 373 // Alice (author) has no completions 374 const aliceCompleted = await queries.loadUserCompletedTrails(ALICE.handle); 375 expect(aliceCompleted).toHaveLength(0); 376 377 const alicePublished = await queries.loadUserPublishedTrails(ALICE.handle); 378 expect(alicePublished).toHaveLength(1); 379 }); 380});