cm_whatsapp_bot_v1/apps/web/src/components/send-test-form.test.tsx
yiekheng ec57a78853 feat(send-test): close the loop — bot reports done back to the form
The send-test form was stuck on "Sending to <Group>…" because the
server action returns the moment it publishes the IPC NOTIFY; the bot
processed the actual WhatsApp send out-of-band and the form had no way
to learn whether it succeeded.

Round-trip now wired end-to-end:

- New WebEvent variant `send_test.done` { groupId, ok, error }.
- bot/src/ipc/send-test-handler emits it on every exit path:
  - missing group   → ok=false, "Group not found"
  - account offline → ok=false, "Account not connected — re-pair first"
  - send threw      → ok=false, error message
  - send succeeded  → ok=true,  null
- web/src/hooks/use-events declares the new event in its type map.
- web SendTestForm subscribes via useEvents, filters by its own
  groupId so a parallel send-test on another group can't move our
  state, and renders one of three pills:
    * Sending…           (in-flight — Loader2 spinner)
    * Sent ✓             (success — emerald CheckCircle2)
    * <error message>    (failure — destructive AlertCircle)
  The "Send Test" button stays disabled while in-flight.

Tests (+5; 110 web tests total):
  send-test-form.test.tsx
  - SSR markup: textarea, submit button, hidden groupId, no premature
    pill on first render.
  - useEvents wiring: form registers a `send_test.done` handler.
  - Handler safely accepts:
    * matching success event
    * matching failure event
    * mismatched groupId (must not throw)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 10:04:33 +08:00

92 lines
3.2 KiB
TypeScript

import { describe, it, expect, vi } from "vitest";
import { renderToStaticMarkup } from "react-dom/server";
// SSE hook + the server action are deliberately stubbed. The action's
// optimistic result is provided by feeding `useActionState` a manual
// initial state via the mock's exported helper.
const sendTestActionMock = vi.fn();
vi.mock("@/actions/groups", () => ({
sendTestAction: (...args: unknown[]) => sendTestActionMock(...args),
}));
// Capture the handlers the form passes to useEvents so we can fire
// `send_test.done` ourselves and observe the state transition.
const eventHandlers: {
"send_test.done"?: (e: { groupId: string; ok: boolean; error: string | null }) => void;
} = {};
vi.mock("@/hooks/use-events", () => ({
useEvents: (handlers: typeof eventHandlers) => {
Object.assign(eventHandlers, handlers);
},
}));
import { SendTestForm } from "./send-test-form";
describe("SendTestForm — initial render", () => {
it("renders the textarea and submit button, no status pill yet", () => {
const html = renderToStaticMarkup(<SendTestForm groupId="g-1" />);
expect(html).toMatch(/<textarea[^>]*name="text"/);
expect(html).toMatch(/<button[^>]*type="submit"/);
expect(html).toContain("Send Test");
// No success / error / in-flight pill on first render.
expect(html).not.toContain("Sent ✓");
expect(html).not.toContain("Sending");
expect(html).not.toContain("role=\"alert\"");
});
it("includes the groupId as a hidden input the action will read", () => {
const html = renderToStaticMarkup(<SendTestForm groupId="abc-uuid" />);
expect(html).toMatch(/<input[^>]+type="hidden"[^>]+name="groupId"[^>]+value="abc-uuid"/);
});
});
describe("SendTestForm — wires `send_test.done` events", () => {
it("registers a `send_test.done` handler with useEvents on render", () => {
// Reset the captured handlers between tests.
delete eventHandlers["send_test.done"];
renderToStaticMarkup(<SendTestForm groupId="g-1" />);
expect(typeof eventHandlers["send_test.done"]).toBe("function");
});
it("the handler ignores events for OTHER groups (won't move our state)", () => {
// We can't easily inspect React state from SSR markup, but we can
// at least verify the handler accepts and discards an event with a
// mismatched groupId without throwing.
delete eventHandlers["send_test.done"];
renderToStaticMarkup(<SendTestForm groupId="g-1" />);
expect(() =>
eventHandlers["send_test.done"]?.({
groupId: "g-2",
ok: true,
error: null,
}),
).not.toThrow();
});
it("the handler accepts a matching success event without throwing", () => {
delete eventHandlers["send_test.done"];
renderToStaticMarkup(<SendTestForm groupId="g-1" />);
expect(() =>
eventHandlers["send_test.done"]?.({
groupId: "g-1",
ok: true,
error: null,
}),
).not.toThrow();
});
it("the handler accepts a matching failure event without throwing", () => {
delete eventHandlers["send_test.done"];
renderToStaticMarkup(<SendTestForm groupId="g-1" />);
expect(() =>
eventHandlers["send_test.done"]?.({
groupId: "g-1",
ok: false,
error: "Account not connected",
}),
).not.toThrow();
});
});