import { fireEvent, render, screen } from "@testing-library/react";
import { Component, type ReactNode, Suspense } from "react";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { retryableLazyEditor } from "./editor-lazy";
import { MissingEditorPeersError } from "./editor-peers";
/** Catches the rejection the lazy component throws, and can mount it again. */
class ResettableBoundary extends Component<
{ children: ReactNode },
{ failure: Error | null }
> {
state = { failure: null as Error | null };
static getDerivedStateFromError(error: Error) {
return { failure: error };
}
render() {
if (this.state.failure) {
return (
);
}
return this.props.children;
}
}
function Loaded() {
return editor;
}
/** React reports a caught render error on the console; the assertions read it. */
let consoleError: ReturnType;
beforeEach(() => {
consoleError = vi.spyOn(console, "error").mockImplementation(() => {});
});
afterEach(() => {
consoleError.mockRestore();
});
function renderRetryable(load: () => Promise) {
const lazyEditor = retryableLazyEditor(load);
function Host() {
const LazyEditor = lazyEditor();
return (
loading}>
);
}
return render(
,
);
}
describe("retryableLazyEditor", () => {
it("re-attempts the load after a transient failure", async () => {
// `lazy` caches its first rejection for the life of the component, so one
// failed chunk fetch would otherwise hold the editor broken until a full
// page reload.
const load = vi
.fn<() => Promise>()
.mockRejectedValueOnce(new Error("Failed to fetch dynamically imported module"))
.mockResolvedValue(Loaded);
renderRetryable(load);
const retry = await screen.findByRole("button");
expect(retry).toHaveTextContent("Failed to fetch dynamically imported module");
expect(load).toHaveBeenCalledTimes(1);
fireEvent.click(retry);
expect(await screen.findByText("editor")).toBeInTheDocument();
expect(load).toHaveBeenCalledTimes(2);
});
it("keeps a missing peer permanent, so a remount does not import it again", async () => {
// A package does not install itself mid-session. Retrying would only
// repeat the same failure and hide the install list behind a loop.
const load = vi
.fn<() => Promise>()
.mockRejectedValue(new MissingEditorPeersError("Install @tiptap/react."));
renderRetryable(load);
const retry = await screen.findByRole("button");
expect(retry).toHaveTextContent("Install @tiptap/react.");
expect(load).toHaveBeenCalledTimes(1);
fireEvent.click(retry);
expect(await screen.findByRole("button")).toHaveTextContent("Install @tiptap/react.");
expect(load).toHaveBeenCalledTimes(1);
});
});