an app to share curated trails sidetrail.app
1

Configure Feed

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

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