import type { ReactNode } from "react";
import { act, createElement, useState } from "react";
import type { Root } from "react-dom/client";
import { createRoot } from "react-dom/client";
import { afterEach, beforeEach, expect, it, vi } from "vitest";
import { ErrorBoundary } from "../src/components/error-boundary";
import { DropdownContent } from "../src/components/ui/dropdown-content";
// Native presentation bindings only; the menu content and both error boundaries
// are real. Portal placement/opening is covered by the native Maestro flow.
vi.mock("react-native", () => {
const View = ({
children,
testID,
}: {
children: ReactNode;
testID?: string;
}) => createElement("div", { "data-testid": testID }, children);
return { View, Text: View, Platform: { OS: "android", select: () => null } };
});
vi.mock("../src/components/ui/button", () => {
return {
Button: ({
children,
onPress,
}: {
children: ReactNode;
onPress: () => void;
}) => createElement("button", { onClick: onPress }, children),
};
});
vi.mock("../src/components/ui/text", () => {
return {
Text: ({ children }: { children: ReactNode }) =>
createElement("span", null, children),
};
});
let container: HTMLDivElement;
let root: Root;
beforeEach(() => {
vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
vi.spyOn(console, "error").mockImplementation(() => {});
container = document.createElement("div");
document.body.appendChild(container);
root = createRoot(container);
});
afterEach(async () => {
await act(async () => root.unmount());
container.remove();
vi.restoreAllMocks();
vi.unstubAllGlobals();
});
async function click(label: string) {
const button = [...container.querySelectorAll("button")].find(
(element) => element.textContent === label,
);
expect(button, `button ${label}`).toBeDefined();
await act(async () => button!.click());
}
it.each(["component", "render callback"])(
"contains a failing %s and preserves the screen through retry, dismiss, and reopen",
async (kind) => {
let failing = true;
function Content() {
if (failing) throw new Error("profile render failed");
return Recovered profile;
}
function Screen() {
const [draft, setDraft] = useState("Unsent message");
const [open, setOpen] = useState(false);
return (
<>
{open && (
: () => Content(),
},
]}
onDismiss={() => setOpen(false)}
/>
)}
>
);
}
await act(async () => {
root.render(
App crashed}>
,
);
});
await click("Unsent message");
await click("Open menu");
expect(
container.querySelector('[data-testid="menu-error"]'),
).not.toBeNull();
expect(container.textContent).not.toContain("App crashed");
expect(container.textContent).toContain("Edited unsent message");
// A persistent failure remains local, and retry can later recover in place.
await click("Try again");
expect(
container.querySelector('[data-testid="menu-error"]'),
).not.toBeNull();
failing = false;
await click("Try again");
expect(container.querySelector('[data-testid="menu-error"]')).toBeNull();
expect(container.textContent).toContain("Recovered profile");
expect(container.textContent).toContain("Edited unsent message");
// Fail again on a subsequent render, dismiss the failed menu, then reopen
// without inheriting the old boundary's failed state or resetting the draft.
failing = true;
await act(async () => {
root.render(
App crashed}>
,
);
});
expect(
container.querySelector('[data-testid="menu-error"]'),
).not.toBeNull();
await click("Dismiss");
expect(container.querySelector('[data-testid="menu-error"]')).toBeNull();
expect(container.textContent).toContain("Edited unsent message");
failing = false;
await click("Open menu");
expect(container.textContent).toContain("Recovered profile");
expect(container.textContent).toContain("Edited unsent message");
expect(container.textContent).not.toContain("App crashed");
},
);