| ... | @@ -1,13 +1,25 @@ | ... | @@ -1,13 +1,25 @@ |
| 1 | import { act } from "react"; | 1 | import { act, useSyncExternalStore } from "react"; |
| 2 | import { create, type ReactTestRenderer } from "react-test-renderer"; | 2 | import { create, type ReactTestRenderer } from "react-test-renderer"; |
| 3 | import { EnvironmentId } from "@t3tools/contracts"; | 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 | vi.mock("~/state/device", () => ({ | 19 | vi.mock("~/state/device", () => ({ |
| 7 | useDeviceHubAccess: () => access, | 20 | useDeviceHubAccess: () => useSyncExternalStore(accessStore.subscribe, () => accessStore.value), |
| 8 | refreshDeviceHubAccess: vi.fn(), | 21 | refreshDeviceHubAccess: () => accessStore.refresh(), |
| 9 | })); | 22 | })); |
| 10 | const access = { httpBase: "http://test", wsBase: "ws://test", query: {}, credentials: true }; | | |
| 11 | import { DeviceStreamView } from "./DeviceStreamView"; | 23 | import { DeviceStreamView } from "./DeviceStreamView"; |
| 12 | | 24 | |
| 13 | class Image extends EventTarget { | 25 | class Image extends EventTarget { |
| ... | @@ -19,6 +31,10 @@ class Image extends EventTarget { | ... | @@ -19,6 +31,10 @@ class Image extends EventTarget { |
| 19 | } | 31 | } |
| 20 | } | 32 | } |
| 21 | let renderer: ReactTestRenderer | undefined; | 33 | let renderer: ReactTestRenderer | undefined; |
| | 34 | let primes = 0; |
| | 35 | beforeEach(() => { |
| | 36 | primes = 0; |
| | 37 | }); |
| 22 | afterEach(async () => { | 38 | afterEach(async () => { |
| 23 | await act(async () => renderer?.unmount()); | 39 | await act(async () => renderer?.unmount()); |
| 24 | renderer = undefined; | 40 | renderer = undefined; |
| ... | @@ -29,7 +45,10 @@ afterEach(async () => { | ... | @@ -29,7 +45,10 @@ afterEach(async () => { |
| 29 | async function setup() { | 45 | async function setup() { |
| 30 | vi.useFakeTimers(); | 46 | vi.useFakeTimers(); |
| 31 | vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true); | 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 | vi.stubGlobal( | 52 | vi.stubGlobal( |
| 34 | "WebSocket", | 53 | "WebSocket", |
| 35 | class { | 54 | class { |
| ... | @@ -106,3 +125,13 @@ it("offers Reconnect after the shared timeout and receives a frame after retry w | ... | @@ -106,3 +125,13 @@ it("offers Reconnect after the shared timeout and receives a frame after retry w |
| 106 | expect(renderer!.root.findAllByType("button")).toHaveLength(0); | 125 | expect(renderer!.root.findAllByType("button")).toHaveLength(0); |
| 107 | expect(vi.getTimerCount()).toBe(0); | 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 | }); |