Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions src/components/ErrorBoundary.jsx
Original file line number Diff line number Diff line change
@@ -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) {
Expand All @@ -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 (
<div className="min-h-screen flex items-center justify-center bg-[#0D0F0E]">
<Spinner size="lg" />
</div>
);
}

return (
<div className="min-h-screen flex items-center justify-center px-4 bg-[#0D0F0E]">
<div className="max-w-md w-full rounded-[14px] border border-[#28312D] bg-[#141716] p-8 text-center shadow-none">
Expand Down
36 changes: 36 additions & 0 deletions src/components/ErrorBoundary.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(() => {
Expand All @@ -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(
<ErrorBoundary>
<ThrowChunkError />
</ErrorBoundary>,
);

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(
<ErrorBoundary>
<ThrowChunkError />
</ErrorBoundary>,
);

expect(screen.getByText('Algo salió mal')).toBeInTheDocument();
});
});
Loading