Skip to content

test(lists): add unit tests for video list parsing and list hooks #337

@DSanich

Description

@DSanich

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

  1. 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).
  2. 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 listid, 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 / collaboratorisCollaborative and allowedCollaborators parsed correctly.
  • thumbnail-eventthumbnailEventId set.
  • play-orderreverse | 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

  • parseVideoList logic is covered by pure unit tests (extracted module or exported helper).
  • New test file(s) under src/hooks/ and/or src/lib/ as appropriate; npm run test (or at least npx vitest run for the new files) passes locally.
  • No real network; no live relay.
  • npx tsc --noEmit clean.
  • Optional follow-up issue (or same PR if small): align ListDetailPage duplicate parser with the tested implementation to prevent drift.

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions