mirror of
https://github.com/reconurge/flowsint.git
synced 2026-11-08 14:35:41 -06:00
test(frontend): type sse.test.ts against the real fetch-event-source types
Uses the library's own FetchEventSourceInit/EventSourceMessage types instead of any for the mocked options object, so the test would catch a real signature drift in connectSSE's usage of the library.
This commit is contained in:
@@ -1,14 +1,16 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
import type { FetchEventSourceInit, EventSourceMessage } from '@microsoft/fetch-event-source'
|
||||
import type { SSEEnvelope } from './sse'
|
||||
|
||||
// Capture the options passed to fetchEventSource so we can drive its callbacks.
|
||||
const calls: any[] = []
|
||||
const fetchEventSourceMock = vi.fn((url: string, opts: any) => {
|
||||
const calls: { url: string; opts: FetchEventSourceInit }[] = []
|
||||
const fetchEventSourceMock = vi.fn((url: string, opts: FetchEventSourceInit) => {
|
||||
calls.push({ url, opts })
|
||||
return new Promise(() => {}) // never resolves; we drive callbacks manually
|
||||
})
|
||||
|
||||
vi.mock('@microsoft/fetch-event-source', () => ({
|
||||
fetchEventSource: (url: string, opts: any) => fetchEventSourceMock(url, opts)
|
||||
fetchEventSource: (url: string, opts: FetchEventSourceInit) => fetchEventSourceMock(url, opts)
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/auth-store', () => ({
|
||||
@@ -20,7 +22,15 @@ import { connectSSE } from './sse'
|
||||
const lastOpts = () => calls[calls.length - 1].opts
|
||||
|
||||
const fakeResponse = (status: number, contentType: string) =>
|
||||
({ ok: status >= 200 && status < 300, status, headers: { get: () => contentType } }) as any
|
||||
({
|
||||
ok: status >= 200 && status < 300,
|
||||
status,
|
||||
headers: { get: () => contentType }
|
||||
}) as unknown as Response
|
||||
|
||||
// The tests only ever care about `data` — id/event are required by the
|
||||
// library's type but irrelevant to connectSSE's own parsing.
|
||||
const message = (data: string): EventSourceMessage => ({ id: '', event: '', data })
|
||||
|
||||
beforeEach(() => {
|
||||
calls.length = 0
|
||||
@@ -35,45 +45,47 @@ describe('connectSSE', () => {
|
||||
})
|
||||
|
||||
it('forwards parsed envelopes and skips "connected"', () => {
|
||||
const received: any[] = []
|
||||
const received: SSEEnvelope[] = []
|
||||
connectSSE({ url: 'http://x', onMessage: (m) => received.push(m) })
|
||||
const o = lastOpts()
|
||||
o.onmessage({ data: JSON.stringify({ event: 'connected', data: 'hi' }) })
|
||||
o.onmessage({ data: JSON.stringify({ event: 'log', data: '{"msg":"a"}' }) })
|
||||
o.onmessage?.(message(JSON.stringify({ event: 'connected', data: 'hi' })))
|
||||
o.onmessage?.(message(JSON.stringify({ event: 'log', data: '{"msg":"a"}' })))
|
||||
expect(received).toEqual([{ event: 'log', data: '{"msg":"a"}' }])
|
||||
})
|
||||
|
||||
it('ignores empty and malformed messages without throwing', () => {
|
||||
const received: any[] = []
|
||||
const received: SSEEnvelope[] = []
|
||||
connectSSE({ url: 'http://x', onMessage: (m) => received.push(m) })
|
||||
const o = lastOpts()
|
||||
o.onmessage({ data: '' })
|
||||
o.onmessage({ data: 'not-json' })
|
||||
o.onmessage?.(message(''))
|
||||
o.onmessage?.(message('not-json'))
|
||||
expect(received).toEqual([])
|
||||
})
|
||||
|
||||
it('aborts the request when the disposer is called', () => {
|
||||
const dispose = connectSSE({ url: 'http://x', onMessage: () => {} })
|
||||
expect(lastOpts().signal.aborted).toBe(false)
|
||||
expect(lastOpts().signal?.aborted).toBe(false)
|
||||
dispose()
|
||||
expect(lastOpts().signal.aborted).toBe(true)
|
||||
expect(lastOpts().signal?.aborted).toBe(true)
|
||||
})
|
||||
|
||||
it('throws (no retry) on a non-event-stream open', async () => {
|
||||
connectSSE({ url: 'http://x', onMessage: () => {} })
|
||||
await expect(lastOpts().onopen(fakeResponse(401, 'application/json'))).rejects.toThrow()
|
||||
await expect(lastOpts().onopen?.(fakeResponse(401, 'application/json'))).rejects.toThrow()
|
||||
})
|
||||
|
||||
it('accepts a valid event-stream open', async () => {
|
||||
connectSSE({ url: 'http://x', onMessage: () => {} })
|
||||
await expect(lastOpts().onopen(fakeResponse(200, 'text/event-stream'))).resolves.toBeUndefined()
|
||||
await expect(
|
||||
lastOpts().onopen?.(fakeResponse(200, 'text/event-stream'))
|
||||
).resolves.toBeUndefined()
|
||||
})
|
||||
|
||||
it('returns increasing backoff for transient errors', () => {
|
||||
connectSSE({ url: 'http://x', onMessage: () => {} })
|
||||
const o = lastOpts()
|
||||
const first = o.onerror(new Error('network'))
|
||||
const second = o.onerror(new Error('network'))
|
||||
const first = o.onerror?.(new Error('network'))
|
||||
const second = o.onerror?.(new Error('network'))
|
||||
expect(first).toBe(2000)
|
||||
expect(second).toBe(4000)
|
||||
})
|
||||
@@ -83,26 +95,26 @@ describe('connectSSE', () => {
|
||||
const o = lastOpts()
|
||||
let fatal: unknown
|
||||
try {
|
||||
await o.onopen(fakeResponse(403, 'application/json'))
|
||||
await o.onopen?.(fakeResponse(403, 'application/json'))
|
||||
} catch (e) {
|
||||
fatal = e
|
||||
}
|
||||
expect(() => o.onerror(fatal)).toThrow()
|
||||
expect(() => o.onerror?.(fatal)).toThrow()
|
||||
})
|
||||
|
||||
it('ignores valid JSON that is not an envelope', () => {
|
||||
const received: any[] = []
|
||||
const received: SSEEnvelope[] = []
|
||||
connectSSE({ url: 'http://x', onMessage: (m) => received.push(m) })
|
||||
const o = lastOpts()
|
||||
o.onmessage({ data: '42' })
|
||||
o.onmessage({ data: 'null' })
|
||||
o.onmessage({ data: '{"data":"no-event"}' })
|
||||
o.onmessage?.(message('42'))
|
||||
o.onmessage?.(message('null'))
|
||||
o.onmessage?.(message('{"data":"no-event"}'))
|
||||
expect(received).toEqual([])
|
||||
})
|
||||
|
||||
it('calls onError once when the connection promise rejects (non-fatal)', async () => {
|
||||
const errors: unknown[] = []
|
||||
fetchEventSourceMock.mockImplementationOnce((url: string, opts: any) => {
|
||||
fetchEventSourceMock.mockImplementationOnce((url: string, opts: FetchEventSourceInit) => {
|
||||
calls.push({ url, opts })
|
||||
return Promise.reject(new Error('boom'))
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user