Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 35 additions & 6 deletions apps/web/src/components/device/DeviceStreamView.test.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,25 @@
import { act } from "react";
import { act, useSyncExternalStore } from "react";
import { create, type ReactTestRenderer } from "react-test-renderer";
import { EnvironmentId } from "@t3tools/contracts";
import { afterEach, expect, it, vi } from "vite-plus/test";
import { afterEach, beforeEach, expect, it, vi } from "vite-plus/test";

// Like the real atom, a refresh yields a new access object and re-renders subscribers.
const accessStore = {
value: { httpBase: "http://test", wsBase: "ws://test", query: {}, credentials: true },
listeners: new Set<() => void>(),
refresh() {
accessStore.value = { ...accessStore.value };
for (const listener of accessStore.listeners) listener();
},
subscribe(listener: () => void) {
accessStore.listeners.add(listener);
return () => accessStore.listeners.delete(listener);
},
};
vi.mock("~/state/device", () => ({
useDeviceHubAccess: () => access,
refreshDeviceHubAccess: vi.fn(),
useDeviceHubAccess: () => useSyncExternalStore(accessStore.subscribe, () => accessStore.value),
refreshDeviceHubAccess: () => accessStore.refresh(),
}));
const access = { httpBase: "http://test", wsBase: "ws://test", query: {}, credentials: true };
import { DeviceStreamView } from "./DeviceStreamView";

class Image extends EventTarget {
Expand All @@ -19,6 +31,10 @@ class Image extends EventTarget {
}
}
let renderer: ReactTestRenderer | undefined;
let primes = 0;
beforeEach(() => {
primes = 0;
});
afterEach(async () => {
await act(async () => renderer?.unmount());
renderer = undefined;
Expand All @@ -29,7 +45,10 @@ afterEach(async () => {
async function setup() {
vi.useFakeTimers();
vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
vi.stubGlobal("fetch", () => Promise.resolve(new Response("prime")));
vi.stubGlobal("fetch", () => {
primes++;
return Promise.resolve(new Response("prime"));
});
vi.stubGlobal(
"WebSocket",
class {
Expand Down Expand Up @@ -106,3 +125,13 @@ it("offers Reconnect after the shared timeout and receives a frame after retry w
expect(renderer!.root.findAllByType("button")).toHaveLength(0);
expect(vi.getTimerCount()).toBe(0);
});

it("starts exactly one new stream per Reconnect press", async () => {
await setup();
await act(async () => {
await vi.advanceTimersByTimeAsync(15_000);
});
expect(primes).toBe(1);
await act(async () => renderer!.root.findByType("button").props.onClick());
expect(primes).toBe(2);
});
3 changes: 2 additions & 1 deletion apps/web/src/components/device/DeviceStreamView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -336,7 +336,8 @@ export function DeviceStreamView(props: {
size="sm"
variant="outline"
onClick={() => {
refreshDeviceHubAccess(props.environmentId);
// An expired ticket surfaces as unauthorized on restart and
// refreshes access through the effect; no need to mint one here.
clientRef.current?.stop();
clientRef.current?.start();
}}
Expand Down
Loading