~bigbes/lethe

ref: 964d8022994f1c6260df9565e1882ebaaf15badc lethe/web/src/features/settings/useSavedSearches.test.ts -rw-r--r-- 3.6 KiB
964d8022 — Eugene Blikh test(lethe): register savedsearch repo+handler in e2e steward graph a month ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { renderHook, act, waitFor } from '@testing-library/react'
import React from 'react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { useSavedSearches, useCreateSavedSearch } from './useSavedSearches'
import type { SavedSearchDTO } from '../../api/adapters'

// ── Mock apiFetch ─────────────────────────────────────────────────────────────

const mockApiFetch = vi.fn()

vi.mock('../../api/client', () => ({
  apiFetch: (...args: unknown[]) => mockApiFetch(...args),
  apiFetchVoid: vi.fn().mockResolvedValue(undefined),
  AuthError: class AuthError extends Error {
    name = 'AuthError'
    constructor(message: string) { super(message) }
  },
  APIError: class APIError extends Error {
    name = 'APIError'
    status: number
    code: string
    constructor(message: string, status: number, code: string) {
      super(message)
      this.status = status
      this.code = code
    }
  },
}))

// ── Test wrapper ──────────────────────────────────────────────────────────────

function makeWrapper(queryClient: QueryClient) {
  return function Wrapper({ children }: { children: React.ReactNode }) {
    return React.createElement(QueryClientProvider, { client: queryClient }, children)
  }
}

// ── Tests ─────────────────────────────────────────────────────────────────────

describe('useSavedSearches + useCreateSavedSearch — cache invalidation', () => {
  let queryClient: QueryClient

  beforeEach(() => {
    queryClient = new QueryClient({
      defaultOptions: {
        queries: { retry: false },
        mutations: { retry: false },
      },
    })
    mockApiFetch.mockReset()
  })

  it('invalidates saved-searches after createSavedSearch succeeds', async () => {
    const emptyResponse = { saved_searches: [] as SavedSearchDTO[] }
    const filledResponse = {
      saved_searches: [
        { name: 'a', query: 'q', created_at: 0, updated_at: 0 } satisfies SavedSearchDTO,
      ],
    }
    // POST returns the created row
    const createdRow: SavedSearchDTO = { name: 'a', query: 'q', created_at: 0, updated_at: 0 }

    // First call → initial list fetch (empty)
    // Second call → re-fetch after invalidation (one row)
    // Third call may come from the mutation POST
    mockApiFetch
      .mockResolvedValueOnce(emptyResponse)   // initial GET
      .mockResolvedValueOnce(createdRow)      // POST create
      .mockResolvedValueOnce(filledResponse)  // re-fetch after invalidation

    const wrapper = makeWrapper(queryClient)

    const { result: listResult } = renderHook(() => useSavedSearches(), { wrapper })
    const { result: mutResult } = renderHook(() => useCreateSavedSearch(), { wrapper })

    // Wait for initial fetch to settle
    await waitFor(() => expect(listResult.current.isSuccess).toBe(true))
    expect(listResult.current.data).toEqual([])

    // Call mutate
    act(() => {
      mutResult.current.mutate({ name: 'a', query: 'q' })
    })

    // Wait for mutation success + re-fetch
    await waitFor(() => expect(mutResult.current.isSuccess).toBe(true))
    await waitFor(() => {
      expect(listResult.current.data?.length).toBe(1)
    })

    expect(listResult.current.data?.[0].name).toBe('a')
  })
})