diff --git a/src/components/ErrorBoundary.jsx b/src/components/ErrorBoundary.jsx index a65878f..3828cde 100644 --- a/src/components/ErrorBoundary.jsx +++ b/src/components/ErrorBoundary.jsx @@ -1,5 +1,30 @@ import { Component } from 'react'; import { i18n } from '../i18n'; +import { Spinner } from './ui/Spinner'; + +/** Firefox/Safari/Chrome error strings para módulos JS que un deploy nuevo dejó sin servir. */ +const CHUNK_LOAD_ERROR_PATTERN = + /fetch dynamically imported module|loading dynamically imported module|loading chunk|importing a module script failed/i; + +const CHUNK_RELOAD_FLAG = 'winbit_chunk_reload_attempted'; + +const isChunkLoadError = (error) => CHUNK_LOAD_ERROR_PATTERN.test(error?.message ?? ''); + +const alreadyTriedReload = () => { + try { + return globalThis?.sessionStorage?.getItem(CHUNK_RELOAD_FLAG) === '1'; + } catch { + return false; + } +}; + +const markReloadAttempted = () => { + try { + globalThis?.sessionStorage?.setItem(CHUNK_RELOAD_FLAG, '1'); + } catch { + // ignore storage errors + } +}; export class ErrorBoundary extends Component { constructor(props) { @@ -13,10 +38,27 @@ export class ErrorBoundary extends Component { componentDidCatch(error, errorInfo) { console.error('Error boundary caught:', error, errorInfo); + + // Deploy nuevo invalidó un chunk que el navegador tenía cacheado: recargar + // una vez arregla solo, sin mostrar la pantalla de error al usuario. + if (isChunkLoadError(error) && !alreadyTriedReload()) { + markReloadAttempted(); + globalThis?.location?.reload(); + } } render() { if (this.state.hasError) { + // Primer chunk error: componentDidCatch ya va a recargar la página. + // Mostrar un spinner en vez del cartel de error evita el flash feo. + if (isChunkLoadError(this.state.error) && !alreadyTriedReload()) { + return ( +
+ +
+ ); + } + return (
diff --git a/src/components/ErrorBoundary.test.jsx b/src/components/ErrorBoundary.test.jsx index cb16bea..ce0a435 100644 --- a/src/components/ErrorBoundary.test.jsx +++ b/src/components/ErrorBoundary.test.jsx @@ -6,9 +6,14 @@ const ThrowError = () => { throw new Error('Test error'); }; +const ThrowChunkError = () => { + throw new Error('Failed to fetch dynamically imported module: /assets/DashboardPage-xyz.js'); +}; + describe('ErrorBoundary', () => { beforeEach(() => { vi.spyOn(console, 'error').mockImplementation(() => {}); + sessionStorage.clear(); }); afterEach(() => { @@ -33,4 +38,35 @@ describe('ErrorBoundary', () => { expect(screen.getByText('Algo salió mal')).toBeInTheDocument(); expect(screen.getByText('Recargar página')).toBeInTheDocument(); }); + + it('auto-reloads once on a stale chunk load error instead of showing the error card', () => { + const reloadSpy = vi.fn(); + const locationSpy = vi + .spyOn(globalThis, 'location', 'get') + .mockReturnValue({ reload: reloadSpy }); + + render( + + + , + ); + + expect(reloadSpy).toHaveBeenCalledTimes(1); + expect(sessionStorage.getItem('winbit_chunk_reload_attempted')).toBe('1'); + expect(screen.queryByText('Algo salió mal')).not.toBeInTheDocument(); + + locationSpy.mockRestore(); + }); + + it('falls back to the error card if a chunk error persists after the reload attempt', () => { + sessionStorage.setItem('winbit_chunk_reload_attempted', '1'); + + render( + + + , + ); + + expect(screen.getByText('Algo salió mal')).toBeInTheDocument(); + }); });