mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-12 11:47:11 +00:00
feat(frontend): add LazyBoundary for chunk-load failure recovery (#875)
* feat(frontend): add LazyBoundary for chunk-load failure recovery
When a lazy chunk fetch fails, the existing top-level ErrorBoundary shows
"Something went wrong" with a "Try again" CTA. "Try again" cannot succeed
against a chunk URL that no longer exists on the server (typical post-
deploy case where the user's tab was opened against an older bundle).
The right remedy is to reload the tab so the browser fetches the new
hashed chunks emitted by the current build.
Add LazyBoundary, a section-local error boundary that:
- Detects chunk-load errors via a substring union covering Chrome / Edge
("Failed to fetch dynamically imported module"), Safari ("Importing a
module script failed"), Firefox ("disallowed MIME type" thrown when a
deploy serves SPA index.html for a missing chunk URL), older Webpack
("Error loading dynamically imported module"), and Vite ("Loading
chunk/CSS chunk N failed").
- For chunk errors, renders a glass-card matching the LockCard aesthetic
with an AlertTriangle icon, a "This part of Sencho needs a reload"
message, and a Reload CTA that calls window.location.reload().
- For non-chunk runtime errors, falls back to "Something went wrong" +
the error message + a Try again CTA. Try again is safe on this path
because the lazy import has already resolved before the render error
fires.
- Logs to console.error in componentDidCatch so the underlying failure
is still observable.
- Has role="alert" so screen readers announce the failure.
Wrap every existing Suspense site (1 in SettingsPage, 7 in EditorLayout
including the security-history overlay, 1 in ResourcesView) with
LazyBoundary. The top-level ErrorBoundary remains the catch-all for
errors that escape the section-local boundary.
Includes a unit test enumerating each browser's documented chunk-load
message so a regression in any one runtime is caught early.
* fix(frontend): move isChunkLoadError to its own file to satisfy react-refresh/only-export-components
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { isChunkLoadError } from '../isChunkLoadError';
|
||||
|
||||
/**
|
||||
* isChunkLoadError is a substring union over the messages browsers emit
|
||||
* when a dynamically-imported chunk URL is no longer reachable. The heuristic
|
||||
* is the entire feature: a missed variant routes a stale-tab user to a
|
||||
* misleading "Try again" CTA instead of "Reload", and "Try again" can never
|
||||
* succeed against a chunk URL that no longer exists. Keep this fixture in
|
||||
* sync with the documented browser variants in LazyBoundary.tsx.
|
||||
*/
|
||||
describe('isChunkLoadError', () => {
|
||||
it('returns false for null', () => {
|
||||
expect(isChunkLoadError(null)).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false for undefined', () => {
|
||||
expect(isChunkLoadError(undefined)).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false for unrelated runtime errors', () => {
|
||||
expect(isChunkLoadError(new Error('Cannot read properties of undefined'))).toBe(false);
|
||||
expect(isChunkLoadError(new Error('Maximum update depth exceeded'))).toBe(false);
|
||||
expect(isChunkLoadError(new Error('Network request failed'))).toBe(false);
|
||||
});
|
||||
|
||||
it('matches Chrome / Edge "Failed to fetch dynamically imported module"', () => {
|
||||
const err = new Error('Failed to fetch dynamically imported module: https://app.example/assets/FleetView-abc123.js');
|
||||
expect(isChunkLoadError(err)).toBe(true);
|
||||
});
|
||||
|
||||
it('matches Safari "Importing a module script failed."', () => {
|
||||
expect(isChunkLoadError(new Error('Importing a module script failed.'))).toBe(true);
|
||||
});
|
||||
|
||||
it('matches Firefox MIME-type variant produced when a deploy serves index.html for a missing chunk', () => {
|
||||
const err = new Error(
|
||||
'Loading module from "https://app.example/assets/FleetView-abc123.js" was blocked because of a disallowed MIME type ("text/html").',
|
||||
);
|
||||
expect(isChunkLoadError(err)).toBe(true);
|
||||
});
|
||||
|
||||
it('matches older Webpack "Error loading dynamically imported module"', () => {
|
||||
expect(isChunkLoadError(new Error('Error loading dynamically imported module'))).toBe(true);
|
||||
});
|
||||
|
||||
it('matches Vite "Loading chunk N failed."', () => {
|
||||
expect(isChunkLoadError(new Error('Loading chunk 42 failed.'))).toBe(true);
|
||||
});
|
||||
|
||||
it('matches Vite "Loading CSS chunk N failed"', () => {
|
||||
expect(isChunkLoadError(new Error('Loading CSS chunk 42 failed'))).toBe(true);
|
||||
});
|
||||
|
||||
it('is case-insensitive', () => {
|
||||
expect(isChunkLoadError(new Error('FAILED TO FETCH DYNAMICALLY IMPORTED MODULE'))).toBe(true);
|
||||
expect(isChunkLoadError(new Error('Loading Chunk 12 Failed'))).toBe(true);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user