131 lines
5.4 KiB
TypeScript
131 lines
5.4 KiB
TypeScript
import { act, fireEvent, render, renderHook, screen, waitFor } from "@testing-library/react";
|
|||
|
|
import { describe, expect, it, vi } from "vitest";
|
||
|
|
import { useEvents } from "../auth/useEvents";
|
||
|
|
import { buildOf, onStreamReconnect, streamReconnected } from "../lib/buildWatch";
|
||
|
|
import { callsTo, mockFetch } from "../test/fetchMock";
|
||
|
|
import { UPDATED_TEXT, VersionBanner } from "./VersionBanner";
|
||
|
|
|
||
|
|
/** /api/version answers with whatever `now.build` holds at the time of the call. */
|
||
|
|
function server(first: string) {
|
||
|
|
const now = { build: first };
|
||
|
|
const fetch = mockFetch({ "/api/version": () => ({ body: { build: now.build, stack: "ci", site_tz: "America/Denver" } }) });
|
||
|
|
const versionCalls = () => callsTo(fetch, "GET", "/api/version").length;
|
||
|
|
return { now, fetch, versionCalls };
|
||
|
|
}
|
||
|
|
|
||
|
|
describe("VersionBanner", () => {
|
||
|
|
it("stays hidden while the build is the same, and shows on focus once it changes", async () => {
|
||
|
|
const s = server("sha-aaa");
|
||
|
|
const onReload = vi.fn();
|
||
|
|
render(<VersionBanner minGapMs={0} onReload={onReload} />);
|
||
|
|
await waitFor(() => expect(s.versionCalls()).toBe(1));
|
||
|
|
|
||
|
|
act(() => void window.dispatchEvent(new Event("focus")));
|
||
|
|
await waitFor(() => expect(s.versionCalls()).toBe(2));
|
||
|
|
expect(screen.queryByTestId("version-banner")).toBeNull();
|
||
|
|
|
||
|
|
s.now.build = "sha-bbb";
|
||
|
|
act(() => void window.dispatchEvent(new Event("focus")));
|
||
|
|
const banner = await screen.findByTestId("version-banner");
|
||
|
|
expect(banner).toHaveTextContent(UPDATED_TEXT);
|
||
|
|
expect(banner).toHaveTextContent("Packet Station was updated. Reload to get the new version.");
|
||
|
|
// Never reloads by itself.
|
||
|
|
expect(onReload).not.toHaveBeenCalled();
|
||
|
|
fireEvent.click(screen.getByRole("button", { name: "Reload" }));
|
||
|
|
expect(onReload).toHaveBeenCalledTimes(1);
|
||
|
|
});
|
||
|
|
|
||
|
|
it("checks again when the event stream reconnects", async () => {
|
||
|
|
const s = server("1");
|
||
|
|
render(<VersionBanner minGapMs={0} />);
|
||
|
|
await waitFor(() => expect(s.versionCalls()).toBe(1));
|
||
|
|
s.now.build = "2";
|
||
|
|
act(() => streamReconnected());
|
||
|
|
expect(await screen.findByTestId("version-banner")).toBeInTheDocument();
|
||
|
|
});
|
||
|
|
|
||
|
|
it("checks again on a timer", async () => {
|
||
|
|
const s = server("1");
|
||
|
|
render(<VersionBanner minGapMs={0} intervalMs={40} />);
|
||
|
|
await waitFor(() => expect(s.versionCalls()).toBe(1));
|
||
|
|
s.now.build = "2";
|
||
|
|
expect(await screen.findByTestId("version-banner")).toBeInTheDocument();
|
||
|
|
});
|
||
|
|
|
||
|
|
it("checks again when a hidden tab comes back", async () => {
|
||
|
|
const s = server("1");
|
||
|
|
render(<VersionBanner minGapMs={0} />);
|
||
|
|
await waitFor(() => expect(s.versionCalls()).toBe(1));
|
||
|
|
s.now.build = "2";
|
||
|
|
act(() => void document.dispatchEvent(new Event("visibilitychange")));
|
||
|
|
expect(await screen.findByTestId("version-banner")).toBeInTheDocument();
|
||
|
|
});
|
||
|
|
|
||
|
|
it("asks at most once per gap when focus comes in bursts", async () => {
|
||
|
|
const s = server("1");
|
||
|
|
render(<VersionBanner />);
|
||
|
|
await waitFor(() => expect(s.versionCalls()).toBe(1));
|
||
|
|
act(() => {
|
||
|
|
window.dispatchEvent(new Event("focus"));
|
||
|
|
window.dispatchEvent(new Event("focus"));
|
||
|
|
streamReconnected();
|
||
|
|
});
|
||
|
|
await new Promise((r) => setTimeout(r, 20));
|
||
|
|
expect(s.versionCalls()).toBe(1);
|
||
|
|
});
|
||
|
|
|
||
|
|
it("rides out a server that is down or restarting", async () => {
|
||
|
|
let answer: { status?: number; body: unknown } = { body: { build: "1" } };
|
||
|
|
const fetch = mockFetch({ "/api/version": () => answer });
|
||
|
|
render(<VersionBanner minGapMs={0} />);
|
||
|
|
await waitFor(() => expect(callsTo(fetch, "GET", "/api/version")).toHaveLength(1));
|
||
|
|
answer = { status: 502, body: { detail: "Bad gateway" } };
|
||
|
|
act(() => void window.dispatchEvent(new Event("focus")));
|
||
|
|
await waitFor(() => expect(callsTo(fetch, "GET", "/api/version")).toHaveLength(2));
|
||
|
|
expect(screen.queryByTestId("version-banner")).toBeNull();
|
||
|
|
answer = { body: { build: "1" } };
|
||
|
|
act(() => void window.dispatchEvent(new Event("focus")));
|
||
|
|
await waitFor(() => expect(callsTo(fetch, "GET", "/api/version")).toHaveLength(3));
|
||
|
|
expect(screen.queryByTestId("version-banner")).toBeNull();
|
||
|
|
answer = { body: { build: "2" } };
|
||
|
|
act(() => void window.dispatchEvent(new Event("focus")));
|
||
|
|
expect(await screen.findByTestId("version-banner")).toBeInTheDocument();
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe("buildOf", () => {
|
||
|
|
it("reads build, falls back to version, and ignores odd shapes", () => {
|
||
|
|
expect(buildOf({ build: " sha-1 ", version: "0.1" })).toBe("sha-1");
|
||
|
|
expect(buildOf({ version: "0.1" })).toBe("0.1");
|
||
|
|
expect(buildOf({ build: "", version: "" })).toBeNull();
|
||
|
|
expect(buildOf("1.2.3")).toBeNull();
|
||
|
|
expect(buildOf(null)).toBeNull();
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe("useEvents reconnect", () => {
|
||
|
|
it("reports a reconnect on the second open of the same stream, not the first", () => {
|
||
|
|
const sources: FakeEventSource[] = [];
|
||
|
|
class FakeEventSource {
|
||
|
|
listeners = new Map<string, () => void>();
|
||
|
|
constructor() {
|
||
|
|
sources.push(this);
|
||
|
|
}
|
||
|
|
addEventListener(type: string, fn: () => void) {
|
||
|
|
this.listeners.set(type, fn);
|
||
|
|
}
|
||
|
|
close() {}
|
||
|
|
}
|
||
|
|
vi.stubGlobal("EventSource", FakeEventSource);
|
||
|
|
const seen = vi.fn();
|
||
|
|
const off = onStreamReconnect(seen);
|
||
|
|
renderHook(() => useEvents("sess-1", { onRevoked: () => undefined }));
|
||
|
|
const es = sources.at(-1);
|
||
|
|
act(() => es?.listeners.get("open")?.());
|
||
|
|
expect(seen).not.toHaveBeenCalled();
|
||
|
|
act(() => es?.listeners.get("open")?.());
|
||
|
|
expect(seen).toHaveBeenCalledTimes(1);
|
||
|
|
off();
|
||
|
|
});
|
||
|
|
});
|