test: add SseProvider tests (TDD red)
This commit is contained in:
@@ -0,0 +1,135 @@
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { render } from '@testing-library/react'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { SseProvider } from '../SseProvider'
|
||||
|
||||
const mockInvalidateQueries = vi.fn()
|
||||
|
||||
vi.mock('@tanstack/react-query', async () => {
|
||||
const actual = await vi.importActual('@tanstack/react-query')
|
||||
return { ...actual, useQueryClient: () => ({ invalidateQueries: mockInvalidateQueries }) }
|
||||
})
|
||||
|
||||
vi.mock('@/shared/model/auth', () => ({
|
||||
$token: { defaultState: null, subscribe: () => () => {}, getState: () => null, watch: () => () => {}, on: () => {}, reset: () => {} },
|
||||
}))
|
||||
|
||||
let mockToken: string | null = null
|
||||
let mockEventHandlers: Record<string, (event: MessageEvent) => void> = {}
|
||||
let mockCloseCalls = 0
|
||||
|
||||
class MockEventSource {
|
||||
url: string
|
||||
constructor(url: string) {
|
||||
this.url = url
|
||||
mockCloseCalls = 0
|
||||
mockEventHandlers = {}
|
||||
}
|
||||
addEventListener(type: string, handler: (event: MessageEvent) => void) {
|
||||
mockEventHandlers[type] = handler
|
||||
}
|
||||
removeEventListener(type: string, _handler: (event: MessageEvent) => void) {
|
||||
delete mockEventHandlers[type]
|
||||
}
|
||||
close() { mockCloseCalls++ }
|
||||
}
|
||||
|
||||
vi.mock('@/shared/lib/sse', () => ({
|
||||
createEventStream: (token: string) => {
|
||||
mockToken = token
|
||||
return new MockEventSource(`/api/sse/stream?token=${token}`) as unknown as EventSource
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('effector-react', async () => {
|
||||
const actual = await vi.importActual('effector-react')
|
||||
return { ...actual, useUnit: () => mockToken }
|
||||
})
|
||||
|
||||
function renderSse() {
|
||||
const qc = new QueryClient({ defaultOptions: { queries: { retry: false } } })
|
||||
return render(<QueryClientProvider client={qc}><SseProvider /></QueryClientProvider>)
|
||||
}
|
||||
|
||||
describe('SseProvider', () => {
|
||||
afterEach(() => {
|
||||
mockToken = null
|
||||
mockInvalidateQueries.mockReset()
|
||||
mockCloseCalls = 0
|
||||
mockEventHandlers = {}
|
||||
})
|
||||
|
||||
it('renders nothing (returns null)', () => {
|
||||
mockToken = null
|
||||
const { container } = renderSse()
|
||||
expect(container.innerHTML).toBe('')
|
||||
})
|
||||
|
||||
it('does not create EventSource when token is null', () => {
|
||||
mockToken = null
|
||||
renderSse()
|
||||
expect(mockToken).toBeNull()
|
||||
})
|
||||
|
||||
it('creates EventSource when token is set', () => {
|
||||
mockToken = 'test-jwt'
|
||||
renderSse()
|
||||
expect(mockToken).toBe('test-jwt')
|
||||
})
|
||||
|
||||
it('closes EventSource on unmount', () => {
|
||||
mockToken = 'test-jwt'
|
||||
const { unmount } = renderSse()
|
||||
expect(mockCloseCalls).toBe(0)
|
||||
unmount()
|
||||
expect(mockCloseCalls).toBe(1)
|
||||
})
|
||||
|
||||
it('invalidates unread-count and conversations on message:new', () => {
|
||||
mockToken = 'test-jwt'
|
||||
renderSse()
|
||||
const handler = mockEventHandlers['message:new']
|
||||
expect(handler).toBeDefined()
|
||||
handler(new MessageEvent('message:new', { data: JSON.stringify({ orderId: 'o1' }) }))
|
||||
|
||||
expect(mockInvalidateQueries).toHaveBeenCalledWith({ queryKey: ['me', 'messages', 'unread-count'] })
|
||||
expect(mockInvalidateQueries).toHaveBeenCalledWith({ queryKey: ['me', 'conversations'] })
|
||||
expect(mockInvalidateQueries).toHaveBeenCalledWith({ queryKey: ['me', 'orders', 'o1'] })
|
||||
expect(mockInvalidateQueries).toHaveBeenCalledWith({ queryKey: ['admin', 'orders', 'o1'] })
|
||||
})
|
||||
|
||||
it('invalidates order queries on order:statusChanged', () => {
|
||||
mockToken = 'test-jwt'
|
||||
renderSse()
|
||||
const handler = mockEventHandlers['order:statusChanged']
|
||||
handler(new MessageEvent('order:statusChanged', { data: JSON.stringify({ orderId: 'o2' }) }))
|
||||
expect(mockInvalidateQueries).toHaveBeenCalledWith({ queryKey: ['me', 'orders', 'o2'] })
|
||||
expect(mockInvalidateQueries).toHaveBeenCalledWith({ queryKey: ['admin', 'orders', 'o2'] })
|
||||
})
|
||||
|
||||
it('invalidates order queries on order:updated', () => {
|
||||
mockToken = 'test-jwt'
|
||||
renderSse()
|
||||
const handler = mockEventHandlers['order:updated']
|
||||
handler(new MessageEvent('order:updated', { data: JSON.stringify({ orderId: 'o3' }) }))
|
||||
expect(mockInvalidateQueries).toHaveBeenCalledWith({ queryKey: ['me', 'orders', 'o3'] })
|
||||
expect(mockInvalidateQueries).toHaveBeenCalledWith({ queryKey: ['admin', 'orders', 'o3'] })
|
||||
})
|
||||
|
||||
it('invalidates admin queries on order:new', () => {
|
||||
mockToken = 'test-jwt'
|
||||
renderSse()
|
||||
const handler = mockEventHandlers['order:new']
|
||||
handler(new MessageEvent('order:new', { data: JSON.stringify({ orderId: 'o4' }) }))
|
||||
expect(mockInvalidateQueries).toHaveBeenCalledWith({ queryKey: ['admin', 'orders', 'summary'] })
|
||||
expect(mockInvalidateQueries).toHaveBeenCalledWith({ queryKey: ['admin', 'orders'] })
|
||||
})
|
||||
|
||||
it('handles invalid JSON gracefully', () => {
|
||||
mockToken = 'test-jwt'
|
||||
renderSse()
|
||||
const handler = mockEventHandlers['message:new']
|
||||
expect(() => { handler(new MessageEvent('message:new', { data: ':heartbit' })) }).not.toThrow()
|
||||
expect(mockInvalidateQueries).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user