Summary
Add focused unit and hook tests for NIP-51 kind 30005 video lists: parsing of relay events into VideoList, and the main TanStack Query / mutation paths in useVideoLists.ts. This complements the upload pipeline coverage from #295 / PR #308 by locking down another high-traffic user feature (lists on cards, dialogs, and list pages).
Problem
src/hooks/useVideoLists.ts has no colocated test file while it powers list discovery, AddToListDialog, badges, and mutations (create / add / remove / delete).
parseVideoList is a non-exported pure function inside the hook module; behaviour (tags → VideoList) is only exercised at runtime through the UI.
ListDetailPage.tsx contains a second, similar parseVideoList implementation with slightly different rules (e.g. multiple video kinds). Without tests on a single canonical parser, the two can drift and cause subtle bugs.
Expected outcome
- Make
parseVideoList unit-testable (preferred: extract to a small pure module under src/lib/, e.g. parseVideoListFromEvent.ts, and import it from useVideoLists.ts; acceptable alternative: export the function from the hook file only if extraction is explicitly out of scope for this PR).
- Add
useVideoLists.test.ts (or split parseVideoListFromEvent.test.ts + useVideoLists.test.ts if clearer) with deterministic mocks — no real relay connections.
What needs to be tested
A. parseVideoList (pure)
- Missing
d tag → returns null.
- Minimal valid list →
id, name (falls back to d), pubkey, createdAt, empty videoCoordinates, default playOrder chronological.
a tags → only coordinates starting with ${SHORT_VIDEO_KIND}: are included; unrelated a tags ignored.
t tags → collected as tags array in order (or stable order as implemented).
collaborative / collaborator → isCollaborative and allowedCollaborators parsed correctly.
thumbnail-event → thumbnailEventId set.
play-order → reverse | manual | shuffle preserved; invalid / missing → chronological.
- Non-empty
content → current stub leaves privateCoordinates empty; assert documented behaviour (no crash, coordinates unchanged vs public-only path).
B. useVideoLists query (with mocks)
- With
targetPubkey set (via useCurrentUser mock or explicit pubkey arg per hook API), nostr.query receives filter kinds: [30005], authors: [pubkey], limit: 100.
- Returned events are parsed, nulls dropped, sorted newest first by
createdAt.
- Query disabled when appropriate (mirror
enabled logic for “no pubkey” vs “browse all” cases — assert both branches).
C. useVideosInLists
- When
videoId is set, query uses #a filter shape ${SHORT_VIDEO_KIND}:*:${videoId} (or exact pattern from source).
enabled: false when videoId is missing.
D. useCreateVideoList mutation
- Throws when user is not logged in (mock
useCurrentUser without user).
publishEvent called with kind: 30005, content: '', and tags including d, title, optional description / image / t / collaborative + collaborator / thumbnail-event / play-order / a coordinates as implemented.
onSuccess updates ['video-lists', user.pubkey] cache (replace vs prepend behaviour — assert against current implementation).
E. useAddVideoToList / useRemoveVideoFromList
- Add:
nostr.query fetches current list; if video already in list, no publishEvent (or early return — match implementation).
- Remove: rebuilt tags preserve metadata paths (description, image,
t, collaborative fields, thumbnail, play-order) per current code.
- Errors: empty query result →
'List not found'; invalid parse → 'Invalid list format'.
F. useDeleteVideoList
- Publishes kind 5 with
a tag 30005:${user.pubkey}:${listId}.
onSuccess filters list out of ['video-lists', user.pubkey] and invalidates related query keys (as in source).
G. useTrendingVideoLists / useFollowedUsersLists (light)
- Assert filter shape (
since window for trending, authors slice for followed), and that lists with zero videos are filtered out where the implementation does so.
(Adjust section G if timeboxed: minimum is A + B + D + F; C and E are high value for regression prevention.)
Mocking strategy
Follow patterns from src/hooks/useProfileStats.test.ts / useVideoUpload.test.ts (after #308):
vi.mock('@nostrify/react') or wrap useNostr to return { query: vi.fn() }.
vi.mock('@/hooks/useNostrPublish') for publishEvent / mutateAsync.
vi.mock('@/hooks/useCurrentUser') for user / pubkey.
QueryClientProvider + renderHook from @testing-library/react where hook integration is tested.
Acceptance criteria
Related files
| File |
Role |
src/hooks/useVideoLists.ts |
Hooks + parsing (source of truth for this issue) |
src/pages/ListDetailPage.tsx |
Duplicate parseVideoList — document drift risk |
src/hooks/useProfileStats.test.ts |
Reference for Vitest + React Query hook mocks |
src/hooks/useVideoUpload.test.ts |
Reference once #308 lands |
src/types/video.ts |
SHORT_VIDEO_KIND |
Notes
- This issue is tests + minimal extraction only; do not change list protocol semantics beyond what tests require to observe behaviour.
- NIP-04 private list content remains a stub; tests should only assert current stub behaviour.
Summary
Add focused unit and hook tests for NIP-51 kind 30005 video lists: parsing of relay events into
VideoList, and the main TanStack Query / mutation paths inuseVideoLists.ts. This complements the upload pipeline coverage from #295 / PR #308 by locking down another high-traffic user feature (lists on cards, dialogs, and list pages).Problem
src/hooks/useVideoLists.tshas no colocated test file while it powers list discovery,AddToListDialog, badges, and mutations (create / add / remove / delete).parseVideoListis a non-exported pure function inside the hook module; behaviour (tags →VideoList) is only exercised at runtime through the UI.ListDetailPage.tsxcontains a second, similarparseVideoListimplementation with slightly different rules (e.g. multiple video kinds). Without tests on a single canonical parser, the two can drift and cause subtle bugs.Expected outcome
parseVideoListunit-testable (preferred: extract to a small pure module undersrc/lib/, e.g.parseVideoListFromEvent.ts, and import it fromuseVideoLists.ts; acceptable alternative: export the function from the hook file only if extraction is explicitly out of scope for this PR).useVideoLists.test.ts(or splitparseVideoListFromEvent.test.ts+useVideoLists.test.tsif clearer) with deterministic mocks — no real relay connections.What needs to be tested
A.
parseVideoList(pure)dtag → returnsnull.id,name(falls back tod),pubkey,createdAt, emptyvideoCoordinates, defaultplayOrderchronological.atags → only coordinates starting with${SHORT_VIDEO_KIND}:are included; unrelatedatags ignored.ttags → collected astagsarray in order (or stable order as implemented).collaborative/collaborator→isCollaborativeandallowedCollaboratorsparsed correctly.thumbnail-event→thumbnailEventIdset.play-order→reverse|manual|shufflepreserved; invalid / missing →chronological.content→ current stub leavesprivateCoordinatesempty; assert documented behaviour (no crash, coordinates unchanged vs public-only path).B.
useVideoListsquery (with mocks)targetPubkeyset (viauseCurrentUsermock or explicitpubkeyarg per hook API),nostr.queryreceives filterkinds: [30005],authors: [pubkey],limit: 100.createdAt.enabledlogic for “no pubkey” vs “browse all” cases — assert both branches).C.
useVideosInListsvideoIdis set, query uses#afilter shape${SHORT_VIDEO_KIND}:*:${videoId}(or exact pattern from source).enabled: falsewhenvideoIdis missing.D.
useCreateVideoListmutationuseCurrentUserwithoutuser).publishEventcalled withkind: 30005,content: '', and tags includingd,title, optionaldescription/image/t/collaborative+collaborator/thumbnail-event/play-order/acoordinates as implemented.onSuccessupdates['video-lists', user.pubkey]cache (replace vs prepend behaviour — assert against current implementation).E.
useAddVideoToList/useRemoveVideoFromListnostr.queryfetches current list; if video already in list, nopublishEvent(or early return — match implementation).t, collaborative fields, thumbnail, play-order) per current code.'List not found'; invalid parse →'Invalid list format'.F.
useDeleteVideoListatag30005:${user.pubkey}:${listId}.onSuccessfilters list out of['video-lists', user.pubkey]and invalidates related query keys (as in source).G.
useTrendingVideoLists/useFollowedUsersLists(light)sincewindow for trending,authorsslice for followed), and that lists with zero videos are filtered out where the implementation does so.(Adjust section G if timeboxed: minimum is A + B + D + F; C and E are high value for regression prevention.)
Mocking strategy
Follow patterns from
src/hooks/useProfileStats.test.ts/useVideoUpload.test.ts(after #308):vi.mock('@nostrify/react')or wrapuseNostrto return{ query: vi.fn() }.vi.mock('@/hooks/useNostrPublish')forpublishEvent/mutateAsync.vi.mock('@/hooks/useCurrentUser')foruser/pubkey.QueryClientProvider+renderHookfrom@testing-library/reactwhere hook integration is tested.Acceptance criteria
parseVideoListlogic is covered by pure unit tests (extracted module or exported helper).src/hooks/and/orsrc/lib/as appropriate;npm run test(or at leastnpx vitest runfor the new files) passes locally.npx tsc --noEmitclean.ListDetailPageduplicate parser with the tested implementation to prevent drift.Related files
src/hooks/useVideoLists.tssrc/pages/ListDetailPage.tsxparseVideoList— document drift risksrc/hooks/useProfileStats.test.tssrc/hooks/useVideoUpload.test.tssrc/types/video.tsSHORT_VIDEO_KINDNotes