From d17fc5ddd788456e8940d7dd5c88dd6c6efaf359 Mon Sep 17 00:00:00 2001 From: Chad Curtis Date: Thu, 17 Jul 2025 17:18:58 +0000 Subject: [PATCH 1/2] Add default ErrorBoundary with iframe postMessage to template. --- src/components/ErrorBoundary.tsx | 143 +++++++++++++++++++++++++++++++ src/main.tsx | 7 +- src/test/ErrorBoundary.test.tsx | 90 +++++++++++++++++++ 3 files changed, 239 insertions(+), 1 deletion(-) create mode 100644 src/components/ErrorBoundary.tsx create mode 100644 src/test/ErrorBoundary.test.tsx diff --git a/src/components/ErrorBoundary.tsx b/src/components/ErrorBoundary.tsx new file mode 100644 index 0000000..52a5f91 --- /dev/null +++ b/src/components/ErrorBoundary.tsx @@ -0,0 +1,143 @@ +import { Component, ErrorInfo, ReactNode } from 'react'; + +interface ErrorBoundaryState { + hasError: boolean; + error: Error | null; + errorInfo: ErrorInfo | null; +} + +interface ErrorBoundaryProps { + children: ReactNode; + fallback?: ReactNode; +} + +interface ErrorMessage { + type: 'mkstack-error'; + error: { + message: string; + stack?: string; + componentStack?: string; + url: string; + timestamp: string; + userAgent: string; + }; +} + +export class ErrorBoundary extends Component { + constructor(props: ErrorBoundaryProps) { + super(props); + this.state = { + hasError: false, + error: null, + errorInfo: null, + }; + } + + static getDerivedStateFromError(error: Error): Partial { + return { + hasError: true, + error, + }; + } + + componentDidCatch(error: Error, errorInfo: ErrorInfo) { + console.error('Error caught by ErrorBoundary:', error, errorInfo); + + // Send error details via postMessage for iframe embedding + const errorMessage: ErrorMessage = { + type: 'mkstack-error', + error: { + message: error.message, + stack: error.stack || undefined, + componentStack: errorInfo.componentStack || undefined, + url: window.location.href, + timestamp: new Date().toISOString(), + userAgent: navigator.userAgent, + }, + }; + + // Send to parent window if in iframe, or broadcast to all + if (window.parent !== window) { + window.parent.postMessage(errorMessage, '*'); + } else { + window.postMessage(errorMessage, '*'); + } + + this.setState({ + error, + errorInfo, + }); + } + + handleReset = () => { + this.setState({ + hasError: false, + error: null, + errorInfo: null, + }); + }; + + render() { + if (this.state.hasError) { + if (this.props.fallback) { + return this.props.fallback; + } + + return ( +
+
+
+

+ Something went wrong +

+

+ An unexpected error occurred. The error has been reported. +

+
+ +
+
+ + Error details + +
+
+ Message: +

+ {this.state.error?.message} +

+
+ {this.state.error?.stack && ( +
+ Stack trace: +
+                        {this.state.error.stack}
+                      
+
+ )} +
+
+
+ +
+ + +
+
+
+ ); + } + + return this.props.children; + } +} \ No newline at end of file diff --git a/src/main.tsx b/src/main.tsx index f322051..e263904 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -3,10 +3,15 @@ import { createRoot } from 'react-dom/client'; // Import polyfills first import './lib/polyfills.ts'; +import { ErrorBoundary } from '@/components/ErrorBoundary'; import App from './App.tsx'; import './index.css'; // FIXME: a custom font should be used. Eg: // import '@fontsource-variable/'; -createRoot(document.getElementById("root")!).render(); +createRoot(document.getElementById("root")!).render( + + + +); diff --git a/src/test/ErrorBoundary.test.tsx b/src/test/ErrorBoundary.test.tsx new file mode 100644 index 0000000..14dd33f --- /dev/null +++ b/src/test/ErrorBoundary.test.tsx @@ -0,0 +1,90 @@ +import { render, screen } from '@testing-library/react'; +import { describe, it, expect, vi } from 'vitest'; +import { ErrorBoundary } from '@/components/ErrorBoundary'; + +// Mock window.postMessage +const postMessageMock = vi.fn(); +Object.defineProperty(window, 'postMessage', { + value: postMessageMock, +}); + +// Test component that throws an error +const ThrowError = ({ shouldThrow }: { shouldThrow: boolean }) => { + if (shouldThrow) { + throw new Error('Test error'); + } + return
No error
; +}; + +describe('ErrorBoundary', () => { + it('renders children when no error occurs', () => { + render( + +
Test content
+
+ ); + + expect(screen.getByText('Test content')).toBeInTheDocument(); + }); + + it('catches and displays error when child throws', () => { + // Suppress console.error for this test + const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); + + render( + + + + ); + + expect(screen.getByText('Something went wrong')).toBeInTheDocument(); + expect(screen.getByText('An unexpected error occurred. The error has been reported.')).toBeInTheDocument(); + + consoleSpy.mockRestore(); + }); + + it('sends error details via postMessage', () => { + // Suppress console.error for this test + const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); + postMessageMock.mockClear(); + + render( + + + + ); + + expect(postMessageMock).toHaveBeenCalledWith( + expect.objectContaining({ + type: 'mkstack-error', + error: expect.objectContaining({ + message: 'Test error', + stack: expect.any(String), + url: expect.any(String), + timestamp: expect.any(String), + userAgent: expect.any(String), + }), + }), + '*' + ); + + consoleSpy.mockRestore(); + }); + + it('uses custom fallback when provided', () => { + const customFallback =
Custom error message
; + + // Suppress console.error for this test + const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); + + render( + + + + ); + + expect(screen.getByText('Custom error message')).toBeInTheDocument(); + + consoleSpy.mockRestore(); + }); +}); \ No newline at end of file From 68959e0e771fee5b0900d56e636fbcc37537c579 Mon Sep 17 00:00:00 2001 From: Chad Curtis Date: Thu, 17 Jul 2025 22:19:12 +0000 Subject: [PATCH 2/2] remove iframe postmessage --- src/components/ErrorBoundary.tsx | 32 +------------------------------- src/test/ErrorBoundary.test.tsx | 32 +------------------------------- 2 files changed, 2 insertions(+), 62 deletions(-) diff --git a/src/components/ErrorBoundary.tsx b/src/components/ErrorBoundary.tsx index 52a5f91..ffa1f37 100644 --- a/src/components/ErrorBoundary.tsx +++ b/src/components/ErrorBoundary.tsx @@ -11,17 +11,7 @@ interface ErrorBoundaryProps { fallback?: ReactNode; } -interface ErrorMessage { - type: 'mkstack-error'; - error: { - message: string; - stack?: string; - componentStack?: string; - url: string; - timestamp: string; - userAgent: string; - }; -} + export class ErrorBoundary extends Component { constructor(props: ErrorBoundaryProps) { @@ -43,26 +33,6 @@ export class ErrorBoundary extends Component { @@ -43,33 +39,7 @@ describe('ErrorBoundary', () => { consoleSpy.mockRestore(); }); - it('sends error details via postMessage', () => { - // Suppress console.error for this test - const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); - postMessageMock.mockClear(); - render( - - - - ); - - expect(postMessageMock).toHaveBeenCalledWith( - expect.objectContaining({ - type: 'mkstack-error', - error: expect.objectContaining({ - message: 'Test error', - stack: expect.any(String), - url: expect.any(String), - timestamp: expect.any(String), - userAgent: expect.any(String), - }), - }), - '*' - ); - - consoleSpy.mockRestore(); - }); it('uses custom fallback when provided', () => { const customFallback =
Custom error message
;