mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-06 08:18:39 +00:00
feat: implement react-error-boundary for component error isolation
- Add react-error-boundary dependency to webview-ui package - Create custom ErrorBoundary component with VSCode-themed fallback UI - Add internationalization support for error messages - Wrap all major view components (ChatView, SettingsView, HistoryView, etc.) with error boundaries - Implement error reporting utility for centralized error collection - Add comprehensive test coverage for ErrorBoundary component - Configure Vitest for React development mode to support testing Fixes #5731
This commit is contained in:
parent
8a3dcfb593
commit
3121885657
8 changed files with 414 additions and 46 deletions
27
pnpm-lock.yaml
generated
27
pnpm-lock.yaml
generated
|
|
@ -994,6 +994,9 @@ importers:
|
|||
react-dom:
|
||||
specifier: ^18.3.1
|
||||
version: 18.3.1(react@18.3.1)
|
||||
react-error-boundary:
|
||||
specifier: ^6.0.0
|
||||
version: 6.0.0(react@18.3.1)
|
||||
react-i18next:
|
||||
specifier: ^15.4.1
|
||||
version: 15.5.1(i18next@25.2.1(typescript@5.8.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3)
|
||||
|
|
@ -8047,6 +8050,11 @@ packages:
|
|||
peerDependencies:
|
||||
react: ^18.3.1
|
||||
|
||||
react-error-boundary@6.0.0:
|
||||
resolution: {integrity: sha512-gdlJjD7NWr0IfkPlaREN2d9uUZUlksrfOx7SX62VRerwXbMY6ftGCIZua1VG1aXFNOimhISsTq+Owp725b9SiA==}
|
||||
peerDependencies:
|
||||
react: '>=16.13.1'
|
||||
|
||||
react-hook-form@7.57.0:
|
||||
resolution: {integrity: sha512-RbEks3+cbvTP84l/VXGUZ+JMrKOS8ykQCRYdm5aYsxnDquL0vspsyNhGRO7pcH6hsZqWlPOjLye7rJqdtdAmlg==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
|
|
@ -11120,14 +11128,14 @@ snapshots:
|
|||
|
||||
'@manypkg/find-root@1.1.0':
|
||||
dependencies:
|
||||
'@babel/runtime': 7.27.4
|
||||
'@babel/runtime': 7.27.6
|
||||
'@types/node': 12.20.55
|
||||
find-up: 4.1.0
|
||||
fs-extra: 8.1.0
|
||||
|
||||
'@manypkg/get-packages@1.1.3':
|
||||
dependencies:
|
||||
'@babel/runtime': 7.27.1
|
||||
'@babel/runtime': 7.27.6
|
||||
'@changesets/types': 4.1.0
|
||||
'@manypkg/find-root': 1.1.0
|
||||
fs-extra: 8.1.0
|
||||
|
|
@ -13295,7 +13303,7 @@ snapshots:
|
|||
sirv: 3.0.1
|
||||
tinyglobby: 0.2.14
|
||||
tinyrainbow: 2.0.0
|
||||
vitest: 3.2.4(@types/debug@4.1.12)(@types/node@22.15.29)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(tsx@4.19.4)(yaml@2.8.0)
|
||||
vitest: 3.2.4(@types/debug@4.1.12)(@types/node@20.17.57)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(tsx@4.19.4)(yaml@2.8.0)
|
||||
|
||||
'@vitest/utils@3.2.4':
|
||||
dependencies:
|
||||
|
|
@ -14494,7 +14502,7 @@ snapshots:
|
|||
|
||||
dom-helpers@5.2.1:
|
||||
dependencies:
|
||||
'@babel/runtime': 7.27.4
|
||||
'@babel/runtime': 7.27.6
|
||||
csstype: 3.1.3
|
||||
|
||||
dom-serializer@2.0.0:
|
||||
|
|
@ -15931,7 +15939,7 @@ snapshots:
|
|||
|
||||
is-it-type@5.1.2:
|
||||
dependencies:
|
||||
'@babel/runtime': 7.27.4
|
||||
'@babel/runtime': 7.27.6
|
||||
globalthis: 1.0.4
|
||||
|
||||
is-map@2.0.3: {}
|
||||
|
|
@ -17931,6 +17939,11 @@ snapshots:
|
|||
react: 18.3.1
|
||||
scheduler: 0.23.2
|
||||
|
||||
react-error-boundary@6.0.0(react@18.3.1):
|
||||
dependencies:
|
||||
'@babel/runtime': 7.27.6
|
||||
react: 18.3.1
|
||||
|
||||
react-hook-form@7.57.0(react@18.3.1):
|
||||
dependencies:
|
||||
react: 18.3.1
|
||||
|
|
@ -18031,7 +18044,7 @@ snapshots:
|
|||
|
||||
react-transition-group@4.4.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
|
||||
dependencies:
|
||||
'@babel/runtime': 7.27.4
|
||||
'@babel/runtime': 7.27.6
|
||||
dom-helpers: 5.2.1
|
||||
loose-envify: 1.4.0
|
||||
prop-types: 15.8.1
|
||||
|
|
@ -18355,7 +18368,7 @@ snapshots:
|
|||
|
||||
rtl-css-js@1.16.1:
|
||||
dependencies:
|
||||
'@babel/runtime': 7.27.4
|
||||
'@babel/runtime': 7.27.6
|
||||
|
||||
run-applescript@7.0.0: {}
|
||||
|
||||
|
|
|
|||
|
|
@ -54,6 +54,7 @@
|
|||
"pretty-bytes": "^7.0.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-error-boundary": "^6.0.0",
|
||||
"react-i18next": "^15.4.1",
|
||||
"react-markdown": "^9.0.3",
|
||||
"react-remark": "^2.1.0",
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import { AccountView } from "./components/account/AccountView"
|
|||
import { useAddNonInteractiveClickListener } from "./components/ui/hooks/useNonInteractiveClick"
|
||||
import { TooltipProvider } from "./components/ui/tooltip"
|
||||
import { STANDARD_TOOLTIP_DELAY } from "./components/ui/standard-tooltip"
|
||||
import { ErrorBoundary } from "./components/common/ErrorBoundary"
|
||||
|
||||
type Tab = "settings" | "history" | "mcp" | "modes" | "chat" | "marketplace" | "account"
|
||||
|
||||
|
|
@ -169,44 +170,68 @@ const App = () => {
|
|||
// Do not conditionally load ChatView, it's expensive and there's state we
|
||||
// don't want to lose (user input, disableInput, askResponse promise, etc.)
|
||||
return showWelcome ? (
|
||||
<WelcomeView />
|
||||
<ErrorBoundary componentName="WelcomeView">
|
||||
<WelcomeView />
|
||||
</ErrorBoundary>
|
||||
) : (
|
||||
<>
|
||||
{tab === "modes" && <ModesView onDone={() => switchTab("chat")} />}
|
||||
{tab === "mcp" && <McpView onDone={() => switchTab("chat")} />}
|
||||
{tab === "history" && <HistoryView onDone={() => switchTab("chat")} />}
|
||||
{tab === "modes" && (
|
||||
<ErrorBoundary componentName="ModesView">
|
||||
<ModesView onDone={() => switchTab("chat")} />
|
||||
</ErrorBoundary>
|
||||
)}
|
||||
{tab === "mcp" && (
|
||||
<ErrorBoundary componentName="McpView">
|
||||
<McpView onDone={() => switchTab("chat")} />
|
||||
</ErrorBoundary>
|
||||
)}
|
||||
{tab === "history" && (
|
||||
<ErrorBoundary componentName="HistoryView">
|
||||
<HistoryView onDone={() => switchTab("chat")} />
|
||||
</ErrorBoundary>
|
||||
)}
|
||||
{tab === "settings" && (
|
||||
<SettingsView ref={settingsRef} onDone={() => setTab("chat")} targetSection={currentSection} />
|
||||
<ErrorBoundary componentName="SettingsView">
|
||||
<SettingsView ref={settingsRef} onDone={() => setTab("chat")} targetSection={currentSection} />
|
||||
</ErrorBoundary>
|
||||
)}
|
||||
{tab === "marketplace" && (
|
||||
<MarketplaceView
|
||||
stateManager={marketplaceStateManager}
|
||||
onDone={() => switchTab("chat")}
|
||||
targetTab={currentMarketplaceTab as "mcp" | "mode" | undefined}
|
||||
/>
|
||||
<ErrorBoundary componentName="MarketplaceView">
|
||||
<MarketplaceView
|
||||
stateManager={marketplaceStateManager}
|
||||
onDone={() => switchTab("chat")}
|
||||
targetTab={currentMarketplaceTab as "mcp" | "mode" | undefined}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
)}
|
||||
{tab === "account" && (
|
||||
<AccountView
|
||||
userInfo={cloudUserInfo}
|
||||
isAuthenticated={cloudIsAuthenticated}
|
||||
cloudApiUrl={cloudApiUrl}
|
||||
onDone={() => switchTab("chat")}
|
||||
/>
|
||||
<ErrorBoundary componentName="AccountView">
|
||||
<AccountView
|
||||
userInfo={cloudUserInfo}
|
||||
isAuthenticated={cloudIsAuthenticated}
|
||||
cloudApiUrl={cloudApiUrl}
|
||||
onDone={() => switchTab("chat")}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
)}
|
||||
<ChatView
|
||||
ref={chatViewRef}
|
||||
isHidden={tab !== "chat"}
|
||||
showAnnouncement={showAnnouncement}
|
||||
hideAnnouncement={() => setShowAnnouncement(false)}
|
||||
/>
|
||||
<HumanRelayDialog
|
||||
isOpen={humanRelayDialogState.isOpen}
|
||||
requestId={humanRelayDialogState.requestId}
|
||||
promptText={humanRelayDialogState.promptText}
|
||||
onClose={() => setHumanRelayDialogState((prev) => ({ ...prev, isOpen: false }))}
|
||||
onSubmit={(requestId, text) => vscode.postMessage({ type: "humanRelayResponse", requestId, text })}
|
||||
onCancel={(requestId) => vscode.postMessage({ type: "humanRelayCancel", requestId })}
|
||||
/>
|
||||
<ErrorBoundary componentName="ChatView">
|
||||
<ChatView
|
||||
ref={chatViewRef}
|
||||
isHidden={tab !== "chat"}
|
||||
showAnnouncement={showAnnouncement}
|
||||
hideAnnouncement={() => setShowAnnouncement(false)}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
<ErrorBoundary componentName="HumanRelayDialog">
|
||||
<HumanRelayDialog
|
||||
isOpen={humanRelayDialogState.isOpen}
|
||||
requestId={humanRelayDialogState.requestId}
|
||||
promptText={humanRelayDialogState.promptText}
|
||||
onClose={() => setHumanRelayDialogState((prev) => ({ ...prev, isOpen: false }))}
|
||||
onSubmit={(requestId, text) => vscode.postMessage({ type: "humanRelayResponse", requestId, text })}
|
||||
onCancel={(requestId) => vscode.postMessage({ type: "humanRelayCancel", requestId })}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
@ -214,15 +239,17 @@ const App = () => {
|
|||
const queryClient = new QueryClient()
|
||||
|
||||
const AppWithProviders = () => (
|
||||
<ExtensionStateContextProvider>
|
||||
<TranslationProvider>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<TooltipProvider delayDuration={STANDARD_TOOLTIP_DELAY}>
|
||||
<App />
|
||||
</TooltipProvider>
|
||||
</QueryClientProvider>
|
||||
</TranslationProvider>
|
||||
</ExtensionStateContextProvider>
|
||||
<ErrorBoundary componentName="App">
|
||||
<ExtensionStateContextProvider>
|
||||
<TranslationProvider>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<TooltipProvider delayDuration={STANDARD_TOOLTIP_DELAY}>
|
||||
<App />
|
||||
</TooltipProvider>
|
||||
</QueryClientProvider>
|
||||
</TranslationProvider>
|
||||
</ExtensionStateContextProvider>
|
||||
</ErrorBoundary>
|
||||
)
|
||||
|
||||
export default AppWithProviders
|
||||
|
|
|
|||
82
webview-ui/src/components/common/ErrorBoundary.tsx
Normal file
82
webview-ui/src/components/common/ErrorBoundary.tsx
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
import React from "react"
|
||||
import { ErrorBoundary as ReactErrorBoundary, FallbackProps } from "react-error-boundary"
|
||||
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { errorReporter } from "../../utils/errorReporting"
|
||||
|
||||
interface ErrorFallbackProps extends FallbackProps {
|
||||
componentName?: string
|
||||
}
|
||||
|
||||
function ErrorFallback({ error, resetErrorBoundary, componentName }: ErrorFallbackProps) {
|
||||
const { t } = useTranslation("common")
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center p-6 bg-vscode-editor-background border border-vscode-widget-border rounded-md m-4">
|
||||
<div className="flex items-center mb-4">
|
||||
<span className="codicon codicon-error text-vscode-errorForeground text-2xl mr-3" />
|
||||
<h2 className="text-lg font-semibold text-vscode-editor-foreground">
|
||||
{t("errorBoundary.title", "Something went wrong")}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
{componentName && (
|
||||
<p className="text-sm text-vscode-descriptionForeground mb-2">
|
||||
{t("errorBoundary.componentError", "Error in {{componentName}} component", { componentName })}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<p className="text-sm text-vscode-descriptionForeground mb-4 text-center max-w-md">
|
||||
{t(
|
||||
"errorBoundary.description",
|
||||
"An error occurred in this part of the interface. You can try to recover by clicking the button below.",
|
||||
)}
|
||||
</p>
|
||||
|
||||
<details className="mb-4 w-full max-w-md">
|
||||
<summary className="cursor-pointer text-sm text-vscode-descriptionForeground hover:text-vscode-editor-foreground">
|
||||
{t("errorBoundary.showDetails", "Show error details")}
|
||||
</summary>
|
||||
<pre className="mt-2 p-3 bg-vscode-textCodeBlock-background border border-vscode-widget-border rounded text-xs text-vscode-editor-foreground overflow-auto max-h-32">
|
||||
{error.message}
|
||||
{error.stack && (
|
||||
<>
|
||||
{"\n\n"}
|
||||
{error.stack}
|
||||
</>
|
||||
)}
|
||||
</pre>
|
||||
</details>
|
||||
|
||||
<VSCodeButton appearance="primary" onClick={resetErrorBoundary}>
|
||||
{t("errorBoundary.retry", "Try again")}
|
||||
</VSCodeButton>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
interface ErrorBoundaryProps {
|
||||
children: React.ReactNode
|
||||
componentName?: string
|
||||
onError?: (error: Error, errorInfo: React.ErrorInfo) => void
|
||||
}
|
||||
|
||||
export function ErrorBoundary({ children, componentName, onError }: ErrorBoundaryProps) {
|
||||
const handleError = (error: Error, errorInfo: React.ErrorInfo) => {
|
||||
// Report error using our error reporting utility
|
||||
errorReporter.reportError(error, errorInfo, componentName)
|
||||
|
||||
// Call custom error handler if provided (for potential Sentry integration)
|
||||
onError?.(error, errorInfo)
|
||||
}
|
||||
|
||||
return (
|
||||
<ReactErrorBoundary
|
||||
FallbackComponent={(props) => <ErrorFallback {...props} componentName={componentName} />}
|
||||
onError={handleError}>
|
||||
{children}
|
||||
</ReactErrorBoundary>
|
||||
)
|
||||
}
|
||||
|
||||
export default ErrorBoundary
|
||||
|
|
@ -0,0 +1,142 @@
|
|||
import React from "react"
|
||||
import { render, screen, fireEvent } from "@testing-library/react"
|
||||
import { vi, beforeEach, afterEach, describe, it, expect } from "vitest"
|
||||
import { ErrorBoundary } from "../ErrorBoundary"
|
||||
import { errorReporter } from "../../../utils/errorReporting"
|
||||
|
||||
// Mock the error reporter
|
||||
vi.mock("../../../utils/errorReporting", () => ({
|
||||
errorReporter: {
|
||||
reportError: vi.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
// Mock react-i18next
|
||||
vi.mock("react-i18next", () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string, defaultValue?: string, options?: any) => {
|
||||
// Handle interpolation for componentError
|
||||
if (key === "errorBoundary.componentError" && options?.componentName) {
|
||||
return `Error in ${options.componentName} component`
|
||||
}
|
||||
return defaultValue || key
|
||||
},
|
||||
}),
|
||||
}))
|
||||
|
||||
// Component that throws an error
|
||||
const ThrowError = ({ shouldThrow }: { shouldThrow: boolean }) => {
|
||||
if (shouldThrow) {
|
||||
throw new Error("Test error")
|
||||
}
|
||||
return <div>No error</div>
|
||||
}
|
||||
|
||||
describe("ErrorBoundary", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
// Suppress console.error for these tests
|
||||
vi.spyOn(console, "error").mockImplementation(() => {})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
it("renders children when there is no error", () => {
|
||||
render(
|
||||
<ErrorBoundary>
|
||||
<ThrowError shouldThrow={false} />
|
||||
</ErrorBoundary>,
|
||||
)
|
||||
|
||||
expect(screen.getByText("No error")).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("renders error fallback when there is an error", () => {
|
||||
render(
|
||||
<ErrorBoundary componentName="TestComponent">
|
||||
<ThrowError shouldThrow={true} />
|
||||
</ErrorBoundary>,
|
||||
)
|
||||
|
||||
expect(screen.getByText("Something went wrong")).toBeInTheDocument()
|
||||
expect(screen.getByText("Error in TestComponent component")).toBeInTheDocument()
|
||||
expect(screen.getByText("Try again")).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("shows error details when expanded", () => {
|
||||
render(
|
||||
<ErrorBoundary>
|
||||
<ThrowError shouldThrow={true} />
|
||||
</ErrorBoundary>,
|
||||
)
|
||||
|
||||
const detailsButton = screen.getByText("Show error details")
|
||||
fireEvent.click(detailsButton)
|
||||
|
||||
// Look for the error message in the details section (it's part of a larger text block)
|
||||
expect(screen.getByText(/Test error/)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("calls error reporter when error occurs", () => {
|
||||
const mockReportError = errorReporter.reportError as ReturnType<typeof vi.fn>
|
||||
|
||||
render(
|
||||
<ErrorBoundary componentName="TestComponent">
|
||||
<ThrowError shouldThrow={true} />
|
||||
</ErrorBoundary>,
|
||||
)
|
||||
|
||||
expect(mockReportError).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
message: "Test error",
|
||||
}),
|
||||
expect.any(Object),
|
||||
"TestComponent",
|
||||
)
|
||||
})
|
||||
|
||||
it("calls custom onError handler when provided", () => {
|
||||
const mockOnError = vi.fn()
|
||||
|
||||
render(
|
||||
<ErrorBoundary onError={mockOnError}>
|
||||
<ThrowError shouldThrow={true} />
|
||||
</ErrorBoundary>,
|
||||
)
|
||||
|
||||
expect(mockOnError).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
message: "Test error",
|
||||
}),
|
||||
expect.any(Object),
|
||||
)
|
||||
})
|
||||
|
||||
it("resets error boundary when retry button is clicked", () => {
|
||||
const TestComponent = () => {
|
||||
const [shouldThrow, setShouldThrow] = React.useState(true)
|
||||
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<button onClick={() => setShouldThrow(false)}>Fix error</button>
|
||||
<ThrowError shouldThrow={shouldThrow} />
|
||||
</ErrorBoundary>
|
||||
)
|
||||
}
|
||||
|
||||
render(<TestComponent />)
|
||||
|
||||
// Error should be shown initially
|
||||
expect(screen.getByText("Something went wrong")).toBeInTheDocument()
|
||||
|
||||
// Click retry button
|
||||
const retryButton = screen.getByText("Try again")
|
||||
fireEvent.click(retryButton)
|
||||
|
||||
// Component should be reset and try to render again
|
||||
// Since we haven't fixed the error, it should show the error again
|
||||
expect(screen.getByText("Something went wrong")).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
|
@ -51,5 +51,12 @@
|
|||
"success": {
|
||||
"imageDataUriCopied": "Image data URI copied to clipboard"
|
||||
}
|
||||
},
|
||||
"errorBoundary": {
|
||||
"title": "Something went wrong",
|
||||
"componentError": "Error in {{componentName}} component",
|
||||
"description": "An error occurred in this part of the interface. You can try to recover by clicking the button below.",
|
||||
"showDetails": "Show error details",
|
||||
"retry": "Try again"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
93
webview-ui/src/utils/errorReporting.ts
Normal file
93
webview-ui/src/utils/errorReporting.ts
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
/**
|
||||
* Error reporting utility for the webview
|
||||
* This can be extended to integrate with services like Sentry in the future
|
||||
*/
|
||||
|
||||
export interface ErrorReport {
|
||||
error: Error
|
||||
errorInfo?: React.ErrorInfo
|
||||
componentName?: string
|
||||
timestamp: number
|
||||
userAgent: string
|
||||
url: string
|
||||
}
|
||||
|
||||
class ErrorReporter {
|
||||
private errors: ErrorReport[] = []
|
||||
private maxErrors = 50 // Keep only the last 50 errors
|
||||
|
||||
/**
|
||||
* Report an error that occurred in a React component
|
||||
*/
|
||||
reportError(error: Error, errorInfo?: React.ErrorInfo, componentName?: string): void {
|
||||
const errorReport: ErrorReport = {
|
||||
error: {
|
||||
name: error.name,
|
||||
message: error.message,
|
||||
stack: error.stack,
|
||||
} as Error,
|
||||
errorInfo,
|
||||
componentName,
|
||||
timestamp: Date.now(),
|
||||
userAgent: navigator.userAgent,
|
||||
url: window.location.href,
|
||||
}
|
||||
|
||||
// Add to local storage for debugging
|
||||
this.errors.push(errorReport)
|
||||
if (this.errors.length > this.maxErrors) {
|
||||
this.errors.shift()
|
||||
}
|
||||
|
||||
// Log to console for development
|
||||
console.error(`Error in ${componentName || "component"}:`, error, errorInfo)
|
||||
|
||||
// TODO: In the future, this could send errors to Sentry or another service
|
||||
// Example:
|
||||
// if (window.Sentry) {
|
||||
// window.Sentry.captureException(error, {
|
||||
// tags: { component: componentName },
|
||||
// extra: errorInfo
|
||||
// })
|
||||
// }
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all stored error reports (useful for debugging)
|
||||
*/
|
||||
getErrors(): ErrorReport[] {
|
||||
return [...this.errors]
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all stored errors
|
||||
*/
|
||||
clearErrors(): void {
|
||||
this.errors = []
|
||||
}
|
||||
|
||||
/**
|
||||
* Get error statistics
|
||||
*/
|
||||
getErrorStats(): { total: number; byComponent: Record<string, number> } {
|
||||
const byComponent: Record<string, number> = {}
|
||||
|
||||
this.errors.forEach((error) => {
|
||||
const component = error.componentName || "unknown"
|
||||
byComponent[component] = (byComponent[component] || 0) + 1
|
||||
})
|
||||
|
||||
return {
|
||||
total: this.errors.length,
|
||||
byComponent,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Export a singleton instance
|
||||
export const errorReporter = new ErrorReporter()
|
||||
|
||||
// Make it available globally for debugging in development
|
||||
if (typeof window !== "undefined" && process.env.NODE_ENV === "development") {
|
||||
;(window as any).errorReporter = errorReporter
|
||||
}
|
||||
|
|
@ -11,6 +11,9 @@ export default defineConfig({
|
|||
environment: "jsdom",
|
||||
include: ["src/**/*.spec.ts", "src/**/*.spec.tsx"],
|
||||
},
|
||||
define: {
|
||||
"process.env.NODE_ENV": '"development"',
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": path.resolve(__dirname, "./src"),
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue