Version banner, File locations section, final CSP (no unsafe-inline styles, frame-src none, object-src none)
The MTR viewer draws PDFs with pdf.js instead of an iframe the server's frame-ancestors refused. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
@@ -0,0 +1,130 @@
|
||||
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();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user