diff --git a/pages/package.json b/pages/package.json index c9ffceb4..2733e101 100644 --- a/pages/package.json +++ b/pages/package.json @@ -7,6 +7,7 @@ "dev": "webpack serve", "build": "NODE_ENV=production webpack --mode production", "typecheck": "tsc --noEmit", + "test": "vitest run", "lint": "eslint src/", "size": "size-limit" }, @@ -36,10 +37,14 @@ "@babel/preset-typescript": "^7.23.3", "@eslint/js": "^10.0.1", "@size-limit/file": "^13.0.1", + "@testing-library/jest-dom": "^7.0.0", + "@testing-library/react": "^16.3.2", + "@testing-library/user-event": "^14.6.1", "@types/dompurify": "^3.0.5", "@types/react": "^18.2.0", "@types/react-dom": "^18.2.0", "@types/three": "^0.185.0", + "@vitejs/plugin-react": "^6.0.4", "autoprefixer": "^10.4.16", "babel-loader": "^9.1.3", "copy-webpack-plugin": "^14.0.0", @@ -47,6 +52,7 @@ "eslint": "^10.8.0", "eslint-plugin-react-hooks": "^7.1.1", "html-webpack-plugin": "^5.5.3", + "jsdom": "^30.0.0", "postcss": "^8.5.15", "postcss-loader": "^7.3.3", "size-limit": "^13.0.1", @@ -54,6 +60,7 @@ "tailwindcss": "^3.3.5", "typescript": "^5.3.2", "typescript-eslint": "^8.65.0", + "vitest": "^4.1.10", "webpack": "^5.89.0", "webpack-cli": "^5.1.4", "webpack-dev-server": "^5.2.4" diff --git a/pages/src/components/ErrorBoundary.test.tsx b/pages/src/components/ErrorBoundary.test.tsx new file mode 100644 index 00000000..e4ccf09d --- /dev/null +++ b/pages/src/components/ErrorBoundary.test.tsx @@ -0,0 +1,71 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { render, screen, fireEvent } from '@testing-library/react'; +import ErrorBoundary from './ErrorBoundary'; + +const Thrower: React.FC<{ msg: string }> = ({ msg }) => { + throw new Error(msg); +}; + +function mockReload(): ReturnType { + const reloadSpy = vi.fn(); + vi.spyOn(window, 'location', 'get').mockReturnValue({ + reload: reloadSpy, + href: 'http://localhost/', + } as unknown as Location); + return reloadSpy; +} + +describe('ErrorBoundary', () => { + beforeEach(() => { + sessionStorage.clear(); + vi.restoreAllMocks(); + }); + + it('reloads the page on a chunk load error', () => { + const reloadSpy = mockReload(); + render( + + + + ); + expect(reloadSpy).toHaveBeenCalled(); + }); + + it('does not reload twice when the guard is already set', () => { + const reloadSpy = mockReload(); + sessionStorage.setItem('ocr-chunk-reload-attempted', 'true'); + render( + + + + ); + expect(reloadSpy).not.toHaveBeenCalled(); + }); + + it('renders fallback without reloading on a non-chunk error', () => { + const reloadSpy = mockReload(); + render( + + + + ); + expect(screen.getByText('fallback-ui')).toBeTruthy(); + expect(reloadSpy).not.toHaveBeenCalled(); + }); + + it('reset() clears the guard and reloads the page', () => { + const reloadSpy = mockReload(); + const fallback = (_e: Error, reset: () => void) => ( + + ); + render( + + + + ); + sessionStorage.setItem('ocr-chunk-reload-attempted', 'true'); + fireEvent.click(screen.getByText('reload')); + expect(sessionStorage.getItem('ocr-chunk-reload-attempted')).toBeNull(); + expect(reloadSpy).toHaveBeenCalled(); + }); +}); diff --git a/pages/src/components/ErrorBoundary.tsx b/pages/src/components/ErrorBoundary.tsx index 843ac353..6343a32f 100644 --- a/pages/src/components/ErrorBoundary.tsx +++ b/pages/src/components/ErrorBoundary.tsx @@ -50,7 +50,7 @@ export default class ErrorBoundary extends React.Component