huskies: merge 949
This commit is contained in:
@@ -0,0 +1,196 @@
|
||||
/**
|
||||
* Frontend seam test: drive a real React component against a fixture derived
|
||||
* from the actual RPC response (the canonical `CONTRACT_FIXTURES` shared with
|
||||
* the Rust side via the snapshot file).
|
||||
*
|
||||
* The first test renders `SettingsPage` against the well-formed fixture and
|
||||
* asserts the form populates with values from the RPC response — proving the
|
||||
* backend ↔ frontend wire shape lines up end-to-end without hand-rolled
|
||||
* fixtures.
|
||||
*
|
||||
* The second test feeds a *malformed* RPC response (a frame missing the
|
||||
* required envelope `ok` field) and asserts the `rpc.ts` client surfaces a
|
||||
* visible error in the rendered UI instead of leaving the page empty.
|
||||
*/
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||
import { SettingsPage } from "./SettingsPage";
|
||||
import { CONTRACT_FIXTURES } from "../api/rpcContract";
|
||||
import snapshot from "../api/rpcContract.snapshot.json";
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
interface MockSocket {
|
||||
url: string;
|
||||
onopen: ((ev: Event) => void) | null;
|
||||
onmessage: ((ev: { data: string }) => void) | null;
|
||||
onerror: ((ev: Event) => void) | null;
|
||||
onclose: ((ev: CloseEvent) => void) | null;
|
||||
readyState: number;
|
||||
send(data: string): void;
|
||||
close(): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Install a `WebSocket` shim that hands each registered method a single
|
||||
* canned frame. Callers register either a normal RPC result or a
|
||||
* deliberately malformed frame body (returned verbatim — i.e. the body
|
||||
* literally has no `ok` field, simulating a server bug).
|
||||
*/
|
||||
function installSeamWs(replies: {
|
||||
[method: string]: { kind: "ok"; result: unknown } | { kind: "raw"; body: object };
|
||||
}) {
|
||||
const instances: MockSocket[] = [];
|
||||
class SeamWs implements MockSocket {
|
||||
static readonly CONNECTING = 0;
|
||||
static readonly OPEN = 1;
|
||||
static readonly CLOSING = 2;
|
||||
static readonly CLOSED = 3;
|
||||
url: string;
|
||||
onopen: ((ev: Event) => void) | null = null;
|
||||
onmessage: ((ev: { data: string }) => void) | null = null;
|
||||
onerror: ((ev: Event) => void) | null = null;
|
||||
onclose: ((ev: CloseEvent) => void) | null = null;
|
||||
readyState = 0;
|
||||
constructor(url: string) {
|
||||
this.url = url;
|
||||
instances.push(this);
|
||||
queueMicrotask(() => {
|
||||
this.readyState = 1;
|
||||
this.onopen?.(new Event("open"));
|
||||
});
|
||||
}
|
||||
send(data: string) {
|
||||
let frame: {
|
||||
correlation_id?: string;
|
||||
method?: string;
|
||||
};
|
||||
try {
|
||||
frame = JSON.parse(data);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
const { correlation_id, method } = frame;
|
||||
if (!correlation_id || !method) return;
|
||||
queueMicrotask(() => {
|
||||
const reply = replies[method];
|
||||
if (!reply) {
|
||||
this.onmessage?.({
|
||||
data: JSON.stringify({
|
||||
kind: "rpc_response",
|
||||
version: 1,
|
||||
correlation_id,
|
||||
ok: false,
|
||||
error: `no fixture for ${method}`,
|
||||
code: "NOT_FOUND",
|
||||
}),
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (reply.kind === "ok") {
|
||||
this.onmessage?.({
|
||||
data: JSON.stringify({
|
||||
kind: "rpc_response",
|
||||
version: 1,
|
||||
correlation_id,
|
||||
ok: true,
|
||||
result: reply.result,
|
||||
}),
|
||||
});
|
||||
return;
|
||||
}
|
||||
// raw: deliberately malformed envelope (no `ok` field)
|
||||
this.onmessage?.({
|
||||
data: JSON.stringify({
|
||||
kind: "rpc_response",
|
||||
version: 1,
|
||||
correlation_id,
|
||||
...reply.body,
|
||||
}),
|
||||
});
|
||||
});
|
||||
}
|
||||
close() {
|
||||
this.readyState = 3;
|
||||
}
|
||||
}
|
||||
vi.stubGlobal("WebSocket", SeamWs);
|
||||
return instances;
|
||||
}
|
||||
|
||||
describe("SettingsPage seam test", () => {
|
||||
it("renders ProjectSettings from the typed RPC contract fixture", async () => {
|
||||
// Sanity: the in-source fixture mirrors the on-disk snapshot file. If
|
||||
// this trips, the contract has drifted from the Rust side.
|
||||
expect(CONTRACT_FIXTURES["settings.put_project"].result).toEqual(
|
||||
snapshot["settings.put_project"].result,
|
||||
);
|
||||
|
||||
const fixture = CONTRACT_FIXTURES["settings.put_project"].result;
|
||||
installSeamWs({
|
||||
"settings.get_project": { kind: "ok", result: fixture },
|
||||
});
|
||||
|
||||
const onBack = vi.fn();
|
||||
render(<SettingsPage onBack={onBack} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByDisplayValue(String(fixture.max_retries))).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Field driven directly by the RPC payload populates the form.
|
||||
expect(
|
||||
screen.getByDisplayValue(String(fixture.watcher_sweep_interval_secs)),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByDisplayValue(String(fixture.watcher_done_retention_secs)),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows a visible error when the RPC response is malformed", async () => {
|
||||
// `body` lacks the envelope `ok` field. The fixed `rpc.ts` client
|
||||
// should reject loudly with a `MALFORMED` error instead of letting
|
||||
// the page render empty.
|
||||
installSeamWs({
|
||||
"settings.get_project": {
|
||||
kind: "raw",
|
||||
body: { result: { not_actually_settings: true } },
|
||||
},
|
||||
});
|
||||
|
||||
const onBack = vi.fn();
|
||||
render(<SettingsPage onBack={onBack} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/Malformed RPC response/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// And critically — no empty form is rendered.
|
||||
expect(screen.queryByText(/default qa/i)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("user can edit and the new value flows through settings.put_project RPC", async () => {
|
||||
const fixture = CONTRACT_FIXTURES["settings.put_project"].result;
|
||||
const updated = { ...fixture, max_retries: 9 };
|
||||
installSeamWs({
|
||||
"settings.get_project": { kind: "ok", result: fixture },
|
||||
"settings.put_project": { kind: "ok", result: updated },
|
||||
});
|
||||
|
||||
const onBack = vi.fn();
|
||||
render(<SettingsPage onBack={onBack} />);
|
||||
|
||||
const maxRetriesInput = (await screen.findByDisplayValue(
|
||||
String(fixture.max_retries),
|
||||
)) as HTMLInputElement;
|
||||
|
||||
fireEvent.change(maxRetriesInput, { target: { value: "9" } });
|
||||
fireEvent.click(screen.getByRole("button", { name: /save/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByDisplayValue("9")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user