1import { afterEach, beforeEach, describe, expect, it, vi } from "vite-plus/test";
2
3import { showBootError } from "./lib/bootError";
4
5class BootElement extends EventTarget {
6 children: BootElement[] = [];
7 textContent = "";
8
9 constructor(readonly tagName: string) {
10 super();
11 }
12
13 setAttribute() {}
14
15 append(child: BootElement) {
16 this.children.push(child);
17 }
18
19 replaceChildren(...children: BootElement[]) {
20 this.children = children;
21 }
22
23 get text(): string {
24 return this.textContent + this.children.map((child) => child.text).join(" ");
25 }
26}
27
28describe("app startup failures", () => {
29 let bootShell: BootElement | null;
30
31 beforeEach(() => {
32 vi.resetModules();
33 bootShell = new BootElement("div");
34 vi.stubGlobal("document", {
35 getElementById: () => bootShell,
36 createElement: (tagName: string) => new BootElement(tagName),
37 });
38 vi.spyOn(console, "error").mockImplementation(() => {});
39 });
40
41 it("shows failures from asynchronous app startup", async () => {
42 vi.doMock("./main", () => ({ startup: Promise.reject(new Error("Startup chunks failed")) }));
43
44 await import("./bootstrap");
45 await vi.dynamicImportSettled();
46
47 expect(bootShell?.text).toContain("Startup chunks failed");
48 });
49
50 afterEach(() => {
51 vi.doUnmock("./main");
52 vi.unstubAllGlobals();
53 vi.unstubAllEnvs();
54 vi.restoreAllMocks();
55 });
56
57 it("replaces the splash when an app import throws before main can run", async () => {
58 vi.doMock("./main", () => {
59 throw new Error("@vitejs/plugin-react can't detect preamble. Something is wrong.");
60 });
61 const reload = vi.fn();
62 vi.stubGlobal("window", { location: { reload } });
63
64 await import("./bootstrap");
65 await vi.dynamicImportSettled();
66
67 expect(bootShell?.text).toContain("The Knife could not load.");
68 const reloadButton = bootShell?.children[0]?.children.find(
69 (element) => element.tagName === "button",
70 );
71 expect(reloadButton?.text).toBe("Reload");
72 reloadButton?.dispatchEvent(new Event("click"));
73 expect(reload).toHaveBeenCalledOnce();
74 });
75
76 it.each([true, false])("shows startup error details only in dev mode, DEV=%s", (dev) => {
77 vi.stubEnv("DEV", dev);
78
79 showBootError(new Error("internal module path"));
80
81 expect(bootShell?.text).toContain("The Knife could not load.");
82 expect(bootShell?.text.includes("internal module path")).toBe(dev);
83 });
84
85 it("does not replace the app after React removes the splash", () => {
86 bootShell = null;
87 const createElement = vi.spyOn(document, "createElement");
88
89 showBootError(new Error("late failure"));
90
91 expect(createElement).not.toHaveBeenCalled();
92 });
93});