|
19 | 19 | */ |
20 | 20 |
|
21 | 21 | import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; |
22 | | -import { useAppStore } from './store'; |
| 22 | +import { useAppStore, FAULTS_REQUEST_TIMEOUT_MS } from './store'; |
23 | 23 | import type { Fault } from './types'; |
24 | 24 |
|
25 | 25 | vi.mock('react-toastify', () => ({ |
@@ -83,6 +83,26 @@ afterEach(() => { |
83 | 83 | useAppStore.setState({ isConnected: false, client: null, faults: [] } as never); |
84 | 84 | }); |
85 | 85 |
|
| 86 | +describe('subscribeFaultStream', () => { |
| 87 | + it('hands refreshing back to polling when the stream closes without an error', async () => { |
| 88 | + const streamClient = { |
| 89 | + GET: vi.fn(async () => ({ data: { items: [] }, error: undefined })), |
| 90 | + streams: { |
| 91 | + faults: () => ({ |
| 92 | + close: vi.fn(), |
| 93 | + [Symbol.asyncIterator]: () => ({ next: async () => ({ done: true, value: undefined }) }), |
| 94 | + }), |
| 95 | + }, |
| 96 | + }; |
| 97 | + connected(streamClient); |
| 98 | + |
| 99 | + useAppStore.getState().subscribeFaultStream(); |
| 100 | + expect(useAppStore.getState().faultStreamCleanup).not.toBeNull(); |
| 101 | + |
| 102 | + await vi.waitFor(() => expect(useAppStore.getState().faultStreamCleanup).toBeNull()); |
| 103 | + }); |
| 104 | +}); |
| 105 | + |
86 | 106 | describe('fetchFaults change detection', () => { |
87 | 107 | it('takes up a fault that is now reported by a different entity', async () => { |
88 | 108 | connected(clientReturning([raw()])); |
@@ -153,6 +173,48 @@ describe('fetchFaults against a moving connection', () => { |
153 | 173 | expect(client.GET).toHaveBeenCalledTimes(1); |
154 | 174 | }); |
155 | 175 |
|
| 176 | + it('is not wedged by a request that never answers', async () => { |
| 177 | + const hung = { GET: vi.fn(() => new Promise(() => {})) }; |
| 178 | + connected(hung); |
| 179 | + void useAppStore.getState().fetchFaults(); |
| 180 | + |
| 181 | + useAppStore.getState().disconnect(); |
| 182 | + const healthy = clientReturning([raw()]); |
| 183 | + connected(healthy); |
| 184 | + await useAppStore.getState().fetchFaults(); |
| 185 | + |
| 186 | + expect(healthy.GET).toHaveBeenCalledTimes(1); |
| 187 | + expect(useAppStore.getState().faults).toHaveLength(1); |
| 188 | + }); |
| 189 | + |
| 190 | + it('gives up on a request the gateway never answers', async () => { |
| 191 | + // The abort is the behaviour under test, so its own log line is not a surprise. |
| 192 | + const logged = vi.spyOn(console, 'error').mockImplementation(() => {}); |
| 193 | + vi.useFakeTimers(); |
| 194 | + try { |
| 195 | + const hung = { |
| 196 | + GET: vi.fn((_path: string, init: { signal?: AbortSignal }) => { |
| 197 | + return new Promise((_resolve, reject) => { |
| 198 | + init.signal?.addEventListener('abort', () => reject(new Error('aborted'))); |
| 199 | + }); |
| 200 | + }), |
| 201 | + }; |
| 202 | + connected(hung); |
| 203 | + const first = useAppStore.getState().fetchFaults(); |
| 204 | + await vi.advanceTimersByTimeAsync(FAULTS_REQUEST_TIMEOUT_MS + 100); |
| 205 | + await first; |
| 206 | + |
| 207 | + const healthy = clientReturning([raw()]); |
| 208 | + useAppStore.setState({ client: healthy } as never); |
| 209 | + await useAppStore.getState().fetchFaults(); |
| 210 | + |
| 211 | + expect(healthy.GET).toHaveBeenCalledTimes(1); |
| 212 | + } finally { |
| 213 | + vi.useRealTimers(); |
| 214 | + logged.mockRestore(); |
| 215 | + } |
| 216 | + }); |
| 217 | + |
156 | 218 | it('lets a forced refresh through so a cleared fault is not read back from an older answer', async () => { |
157 | 219 | const { client, release } = deferredClient(); |
158 | 220 | connected(client); |
|
0 commit comments