fix: detect CSS preload errors and keep banner visible during error states

- Add "unable to preload" pattern to isChunkError for Vite CSS preload failures
- Move ConnectionMonitor and ConnectionBanner outside ErrorBoundary so they
  remain visible when the error boundary catches a render crash
- Add test for CSS preload error retry
This commit is contained in:
ashim-hq
2026-04-21 00:01:59 +08:00
parent da52088e39
commit 70f9f3d51d
3 changed files with 49 additions and 30 deletions
+34 -29
View File
@@ -167,36 +167,41 @@ function ConnectionMonitor() {
export function App() { export function App() {
return ( return (
<ErrorBoundary> <>
<ConnectionMonitor /> <ConnectionMonitor />
<ConnectionBanner /> <ConnectionBanner />
<Toaster position="bottom-right" /> <ErrorBoundary>
<BrowserRouter> <Toaster position="bottom-right" />
<KeyboardShortcutProvider> <BrowserRouter>
<AuthGuard> <KeyboardShortcutProvider>
<Suspense fallback={<PageLoader />}> <AuthGuard>
<Routes> <Suspense fallback={<PageLoader />}>
<Route path="/login" element={<LoginPage />} /> <Routes>
<Route path="/change-password" element={<ChangePasswordPage />} /> <Route path="/login" element={<LoginPage />} />
<Route path="/automate" element={<AutomatePage />} /> <Route path="/change-password" element={<ChangePasswordPage />} />
<Route path="/files" element={<FilesPage />} /> <Route path="/automate" element={<AutomatePage />} />
<Route path="/fullscreen" element={<FullscreenGridPage />} /> <Route path="/files" element={<FilesPage />} />
<Route path="/privacy" element={<PrivacyPolicyPage />} /> <Route path="/fullscreen" element={<FullscreenGridPage />} />
{/* Redirects: old color tools consolidated into adjust-colors */} <Route path="/privacy" element={<PrivacyPolicyPage />} />
<Route {/* Redirects: old color tools consolidated into adjust-colors */}
path="/brightness-contrast" <Route
element={<Navigate to="/adjust-colors" replace />} path="/brightness-contrast"
/> element={<Navigate to="/adjust-colors" replace />}
<Route path="/saturation" element={<Navigate to="/adjust-colors" replace />} /> />
<Route path="/color-channels" element={<Navigate to="/adjust-colors" replace />} /> <Route path="/saturation" element={<Navigate to="/adjust-colors" replace />} />
<Route path="/color-effects" element={<Navigate to="/adjust-colors" replace />} /> <Route
<Route path="/:toolId" element={<ToolPage />} /> path="/color-channels"
<Route path="/" element={<HomePage />} /> element={<Navigate to="/adjust-colors" replace />}
</Routes> />
</Suspense> <Route path="/color-effects" element={<Navigate to="/adjust-colors" replace />} />
</AuthGuard> <Route path="/:toolId" element={<ToolPage />} />
</KeyboardShortcutProvider> <Route path="/" element={<HomePage />} />
</BrowserRouter> </Routes>
</ErrorBoundary> </Suspense>
</AuthGuard>
</KeyboardShortcutProvider>
</BrowserRouter>
</ErrorBoundary>
</>
); );
} }
+2 -1
View File
@@ -7,7 +7,8 @@ export function isChunkError(error: unknown): boolean {
msg.includes("dynamically imported module") || msg.includes("dynamically imported module") ||
msg.includes("loading chunk") || msg.includes("loading chunk") ||
msg.includes("loading css chunk") || msg.includes("loading css chunk") ||
msg.includes("failed to fetch") msg.includes("failed to fetch") ||
msg.includes("unable to preload")
); );
} }
+13
View File
@@ -33,6 +33,19 @@ describe("retryDynamicImport", () => {
expect(importFn).toHaveBeenCalledTimes(3); expect(importFn).toHaveBeenCalledTimes(3);
}); });
it("retries CSS preload errors", async () => {
const mod = { default: () => null };
const importFn = vi
.fn()
.mockRejectedValueOnce(
new TypeError("Unable to preload CSS for /assets/tool-page-DDbXBANV.css"),
)
.mockResolvedValue(mod);
const result = await retryDynamicImport(importFn, 3, 0);
expect(result).toBe(mod);
expect(importFn).toHaveBeenCalledTimes(2);
});
it("only retries chunk-related errors, not other errors", async () => { it("only retries chunk-related errors, not other errors", async () => {
const err = new Error("Some other error"); const err = new Error("Some other error");
const importFn = vi.fn().mockRejectedValue(err); const importFn = vi.fn().mockRejectedValue(err);