Files
fusion/packages/dashboard/app/hooks/__tests__/usePreviewEmbed.test.ts
Fusion 315892a286 feat(FN-2173): add auto-fallback flow for dev server preview
- Add preview embed diagnostics with richer error classification and fallback state handling
- Improve PreviewIframe and DevServerView retry UX for timeout and iframe failure states
- Adjust preview layout/styles to prevent clipping and header overflow while keeping fallback panels centered
- Expand dashboard tests to cover fallback transitions, retry flows, and responsive CSS behavior
2026-04-20 22:10:07 -07:00

331 lines
8.5 KiB
TypeScript

import { act, renderHook, waitFor } from "@testing-library/react";
import { afterEach, describe, expect, it, vi } from "vitest";
import { usePreviewEmbed } from "../usePreviewEmbed";
async function flushMicrotasks(): Promise<void> {
await act(async () => {
await Promise.resolve();
await Promise.resolve();
});
}
describe("usePreviewEmbed", () => {
afterEach(() => {
vi.useRealTimers();
});
it("initial status is unknown when URL is null", () => {
const { result } = renderHook(() => usePreviewEmbed(null));
expect(result.current.embedStatus).toBe("unknown");
expect(result.current.isEmbedded).toBe(false);
expect(result.current.isBlocked).toBe(false);
expect(result.current.embedContext).toBeNull();
});
it("status transitions to loading when URL is set", async () => {
const { result } = renderHook(() => usePreviewEmbed("http://localhost:3000"));
await waitFor(() => {
expect(result.current.embedStatus).toBe("loading");
});
});
it("resets to loading when URL changes", async () => {
const { result, rerender } = renderHook(
({ url }) => usePreviewEmbed(url),
{ initialProps: { url: "http://localhost:3000" as string | null } },
);
await flushMicrotasks();
act(() => {
result.current.setEmbedStatus("embedded");
});
expect(result.current.embedStatus).toBe("embedded");
rerender({ url: "http://localhost:4000" });
await waitFor(() => {
expect(result.current.embedStatus).toBe("loading");
});
});
it("setEmbedStatus updates status and computed booleans", async () => {
const { result } = renderHook(() => usePreviewEmbed("http://localhost:3000"));
await flushMicrotasks();
act(() => {
result.current.setEmbedStatus("embedded");
});
expect(result.current.embedStatus).toBe("embedded");
expect(result.current.isEmbedded).toBe(true);
expect(result.current.isBlocked).toBe(false);
act(() => {
result.current.setEmbedStatus("error");
});
expect(result.current.embedStatus).toBe("error");
expect(result.current.isEmbedded).toBe(false);
expect(result.current.isBlocked).toBe(true);
});
it("resetEmbedStatus sets status to unknown", async () => {
const { result } = renderHook(() => usePreviewEmbed("http://localhost:3000"));
await flushMicrotasks();
act(() => {
result.current.setEmbedStatus("embedded");
});
expect(result.current.embedStatus).toBe("embedded");
act(() => {
result.current.resetEmbedStatus();
});
expect(result.current.embedStatus).toBe("unknown");
expect(result.current.embedContext).toBeNull();
});
it("handleIframeLoad marks blocked for same-origin about:blank and handleIframeError marks error", async () => {
const { result } = renderHook(() => usePreviewEmbed("http://localhost:3000"));
await flushMicrotasks();
const mutableRef = result.current.iframeRef as unknown as {
current: { src: string; contentWindow: { location: { href: string } } } | null;
};
mutableRef.current = {
src: "http://localhost:3000",
contentWindow: {
location: {
href: "about:blank",
},
},
};
act(() => {
result.current.handleIframeLoad();
});
expect(result.current.embedStatus).toBe("blocked");
act(() => {
result.current.handleIframeError();
});
expect(result.current.embedStatus).toBe("error");
});
it("loading timeout transitions to blocked after default loadTimeoutMs", async () => {
vi.useFakeTimers();
const { result } = renderHook(() => usePreviewEmbed("http://localhost:3000"));
await flushMicrotasks();
expect(result.current.embedStatus).toBe("loading");
act(() => {
vi.advanceTimersByTime(10000);
});
expect(result.current.embedStatus).toBe("blocked");
expect(result.current.embedContext).toContain("taking longer than expected");
});
it("custom loadTimeoutMs is respected", async () => {
vi.useFakeTimers();
const { result } = renderHook(() => usePreviewEmbed("http://localhost:3000", { loadTimeoutMs: 5000 }));
await flushMicrotasks();
expect(result.current.embedStatus).toBe("loading");
act(() => {
vi.advanceTimersByTime(4999);
});
expect(result.current.embedStatus).toBe("loading");
act(() => {
vi.advanceTimersByTime(1);
});
expect(result.current.embedStatus).toBe("blocked");
});
it("timeout is cleared when embedStatus changes to embedded before timeout", async () => {
vi.useFakeTimers();
const { result } = renderHook(() => usePreviewEmbed("http://localhost:3000"));
await flushMicrotasks();
act(() => {
vi.advanceTimersByTime(5000);
result.current.setEmbedStatus("embedded");
});
act(() => {
vi.advanceTimersByTime(10000);
});
expect(result.current.embedStatus).toBe("embedded");
expect(result.current.embedContext).toBeNull();
});
it("timeout is cleared on URL change", async () => {
vi.useFakeTimers();
const { result, rerender } = renderHook(
({ url }) => usePreviewEmbed(url),
{ initialProps: { url: "http://localhost:3000" as string | null } },
);
await flushMicrotasks();
act(() => {
vi.advanceTimersByTime(5000);
});
rerender({ url: "http://localhost:4000" });
await flushMicrotasks();
act(() => {
vi.advanceTimersByTime(5000);
});
expect(result.current.embedStatus).toBe("loading");
});
it("retry resets embedStatus to unknown", async () => {
const { result } = renderHook(() => usePreviewEmbed("http://localhost:3000"));
await flushMicrotasks();
act(() => {
result.current.setEmbedStatus("blocked");
});
expect(result.current.embedStatus).toBe("blocked");
act(() => {
result.current.retry();
});
expect(result.current.embedStatus).toBe("unknown");
expect(result.current.embedContext).toBeNull();
});
it("retry works after timeout has already fired", async () => {
vi.useFakeTimers();
const { result } = renderHook(() => usePreviewEmbed("http://localhost:3000"));
await flushMicrotasks();
act(() => {
vi.advanceTimersByTime(10000);
});
expect(result.current.embedStatus).toBe("blocked");
act(() => {
result.current.retry();
});
expect(result.current.embedStatus).toBe("unknown");
expect(result.current.embedContext).toBeNull();
act(() => {
vi.advanceTimersByTime(15000);
});
expect(result.current.embedStatus).toBe("unknown");
});
it("retry clears pending timeout when called during loading", async () => {
vi.useFakeTimers();
const { result } = renderHook(() => usePreviewEmbed("http://localhost:3000"));
await flushMicrotasks();
act(() => {
vi.advanceTimersByTime(2000);
result.current.retry();
});
expect(result.current.embedStatus).toBe("unknown");
act(() => {
vi.advanceTimersByTime(12000);
});
expect(result.current.embedStatus).toBe("unknown");
});
it("stale timer is cleared when status changes away from loading", async () => {
vi.useFakeTimers();
const { result } = renderHook(() => usePreviewEmbed("http://localhost:3000"));
await flushMicrotasks();
act(() => {
result.current.setEmbedStatus("embedded");
});
act(() => {
vi.advanceTimersByTime(15000);
});
expect(result.current.embedStatus).toBe("embedded");
});
it("embedContext is null for embedded and unknown statuses", async () => {
const { result } = renderHook(() => usePreviewEmbed("http://localhost:3000"));
await flushMicrotasks();
act(() => {
result.current.setEmbedStatus("embedded");
});
expect(result.current.embedContext).toBeNull();
act(() => {
result.current.setEmbedStatus("unknown");
});
expect(result.current.embedContext).toBeNull();
});
it("embedContext has message for blocked status", async () => {
const { result } = renderHook(() => usePreviewEmbed("http://localhost:3000"));
await flushMicrotasks();
act(() => {
result.current.setEmbedStatus("blocked");
});
expect(result.current.embedContext).toContain("iframe embedding");
});
it("embedContext has message for error status", async () => {
const { result } = renderHook(() => usePreviewEmbed("http://localhost:3000"));
await flushMicrotasks();
act(() => {
result.current.setEmbedStatus("error");
});
expect(result.current.embedContext).toContain("could not be loaded");
});
});