| ... | ... | @@ -1,13 +1,25 @@ |
| 1 | | import { act } from "react"; |
| 1 | import { act, useSyncExternalStore } from "react"; |
| 2 | 2 | import { create, type ReactTestRenderer } from "react-test-renderer"; |
| 3 | 3 | import { EnvironmentId } from "@t3tools/contracts"; |
| 4 | | import { afterEach, expect, it, vi } from "vite-plus/test"; |
| 4 | import { afterEach, beforeEach, expect, it, vi } from "vite-plus/test"; |
| 5 | 5 | |
| 6 | // Like the real atom, a refresh yields a new access object and re-renders subscribers. |
| 7 | const accessStore = { |
| 8 | value: { httpBase: "http://test", wsBase: "ws://test", query: {}, credentials: true }, |
| 9 | listeners: new Set<() => void>(), |
| 10 | refresh() { |
| 11 | accessStore.value = { ...accessStore.value }; |
| 12 | for (const listener of accessStore.listeners) listener(); |
| 13 | }, |
| 14 | subscribe(listener: () => void) { |
| 15 | accessStore.listeners.add(listener); |
| 16 | return () => accessStore.listeners.delete(listener); |
| 17 | }, |
| 18 | }; |
| 6 | 19 | vi.mock("~/state/device", () => ({ |
| 7 | | useDeviceHubAccess: () => access, |
| 8 | | refreshDeviceHubAccess: vi.fn(), |
| 20 | useDeviceHubAccess: () => useSyncExternalStore(accessStore.subscribe, () => accessStore.value), |
| 21 | refreshDeviceHubAccess: () => accessStore.refresh(), |
| 9 | 22 | })); |
| 10 | | const access = { httpBase: "http://test", wsBase: "ws://test", query: {}, credentials: true }; |
| 11 | 23 | import { DeviceStreamView } from "./DeviceStreamView"; |
| 12 | 24 | |
| 13 | 25 | class Image extends EventTarget { |
| ... | ... | @@ -19,6 +31,10 @@ class Image extends EventTarget { |
| 19 | 31 | } |
| 20 | 32 | } |
| 21 | 33 | let renderer: ReactTestRenderer | undefined; |
| 34 | let primes = 0; |
| 35 | beforeEach(() => { |
| 36 | primes = 0; |
| 37 | }); |
| 22 | 38 | afterEach(async () => { |
| 23 | 39 | await act(async () => renderer?.unmount()); |
| 24 | 40 | renderer = undefined; |
| ... | ... | @@ -29,7 +45,10 @@ afterEach(async () => { |
| 29 | 45 | async function setup() { |
| 30 | 46 | vi.useFakeTimers(); |
| 31 | 47 | vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true); |
| 32 | | vi.stubGlobal("fetch", () => Promise.resolve(new Response("prime"))); |
| 48 | vi.stubGlobal("fetch", () => { |
| 49 | primes++; |
| 50 | return Promise.resolve(new Response("prime")); |
| 51 | }); |
| 33 | 52 | vi.stubGlobal( |
| 34 | 53 | "WebSocket", |
| 35 | 54 | class { |
| ... | ... | @@ -106,3 +125,13 @@ it("offers Reconnect after the shared timeout and receives a frame after retry w |
| 106 | 125 | expect(renderer!.root.findAllByType("button")).toHaveLength(0); |
| 107 | 126 | expect(vi.getTimerCount()).toBe(0); |
| 108 | 127 | }); |
| 128 | |
| 129 | it("starts exactly one new stream per Reconnect press", async () => { |
| 130 | await setup(); |
| 131 | await act(async () => { |
| 132 | await vi.advanceTimersByTimeAsync(15_000); |
| 133 | }); |
| 134 | expect(primes).toBe(1); |
| 135 | await act(async () => renderer!.root.findByType("button").props.onClick()); |
| 136 | expect(primes).toBe(2); |
| 137 | }); |