Skip to content

Commit c10cf29

Browse files
committed
Merge branch 'differentLanguageSupport' of https://github.com/kartikrautan/template-playground into differentLanguageSupport
Signed-off-by: kartik <kartikrautan0@gmail.com>
2 parents d6199d2 + 9a71e91 commit c10cf29

File tree

3 files changed

+263
-3
lines changed

3 files changed

+263
-3
lines changed

src/components/ErrorBoundary.tsx

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import { Component, ErrorInfo, ReactNode } from 'react';
2+
import useAppStore from '../store/store';
3+
4+
interface Props {
5+
children: ReactNode;
6+
showDevDetails?: boolean;
7+
}
8+
9+
interface State {
10+
hasError: boolean;
11+
error: Error | null;
12+
errorInfo: ErrorInfo | null;
13+
}
14+
15+
class ErrorBoundary extends Component<Props, State> {
16+
constructor(props: Props) {
17+
super(props);
18+
this.state = { hasError: false, error: null, errorInfo: null };
19+
}
20+
21+
static getDerivedStateFromError(error: Error): State {
22+
return { hasError: true, error, errorInfo: null };
23+
}
24+
25+
componentDidCatch(error: Error, errorInfo: ErrorInfo): void {
26+
console.error('ErrorBoundary caught an error:', error, errorInfo);
27+
this.setState({ errorInfo });
28+
}
29+
30+
render() {
31+
if (this.state.hasError) {
32+
// Get theme colors from store
33+
const { backgroundColor, textColor } = useAppStore.getState();
34+
const isDarkMode = backgroundColor === '#121212';
35+
const showDevDetails = this.props.showDevDetails ?? import.meta.env.DEV;
36+
37+
return (
38+
<div style={{
39+
display: 'flex',
40+
flexDirection: 'column',
41+
alignItems: 'center',
42+
justifyContent: 'center',
43+
height: '100vh',
44+
padding: '2rem',
45+
textAlign: 'center',
46+
backgroundColor: backgroundColor
47+
}}>
48+
<h1 style={{ fontSize: '2rem', marginBottom: '1rem', color: '#d32f2f' }}>
49+
Something went wrong
50+
</h1>
51+
<p style={{ fontSize: '1rem', marginBottom: '2rem', color: textColor, maxWidth: '600px', opacity: 0.8 }}>
52+
We apologize for the inconvenience. An unexpected error has occurred.
53+
</p>
54+
<button
55+
type="button"
56+
onClick={() => window.location.reload()}
57+
style={{
58+
padding: '0.75rem 1.5rem',
59+
fontSize: '1rem',
60+
backgroundColor: '#19c6c7',
61+
color: 'white',
62+
border: 'none',
63+
borderRadius: '4px',
64+
cursor: 'pointer'
65+
}}
66+
>
67+
Reload Page
68+
</button>
69+
{this.state.error && showDevDetails && (
70+
<details style={{ marginTop: '2rem', maxWidth: '800px', textAlign: 'left' }}>
71+
<summary style={{ cursor: 'pointer', color: textColor, opacity: 0.8, fontSize: '0.9rem' }}>
72+
Error details
73+
</summary>
74+
<pre style={{
75+
marginTop: '1rem',
76+
padding: '1rem',
77+
backgroundColor: isDarkMode ? '#2d2d2d' : '#fff',
78+
color: textColor,
79+
border: `1px solid ${isDarkMode ? '#444' : '#ddd'}`,
80+
borderRadius: '4px',
81+
overflow: 'auto',
82+
fontSize: '0.85rem'
83+
}}>
84+
{this.state.error.toString()}
85+
{this.state.errorInfo?.componentStack && (
86+
<>
87+
{'\n\nComponent Stack:'}
88+
{this.state.errorInfo.componentStack}
89+
</>
90+
)}
91+
</pre>
92+
</details>
93+
)}
94+
</div>
95+
);
96+
}
97+
98+
return this.props.children;
99+
}
100+
}
101+
102+
export default ErrorBoundary;

src/main.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,15 @@ import React from "react";
66
import ReactDOM from "react-dom/client";
77
import { BrowserRouter } from "react-router-dom";
88
import App from "./App.tsx";
9+
import ErrorBoundary from "./components/ErrorBoundary.tsx";
910
import "./index.css";
1011

1112
ReactDOM.createRoot(document.getElementById("root")!).render(
1213
<React.StrictMode>
13-
<BrowserRouter>
14-
<App />
15-
</BrowserRouter>
14+
<ErrorBoundary>
15+
<BrowserRouter>
16+
<App />
17+
</BrowserRouter>
18+
</ErrorBoundary>
1619
</React.StrictMode>
1720
);
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
import { render, screen, waitFor } from "@testing-library/react";
2+
import { describe, it, expect, vi, beforeEach, afterEach, type MockInstance } from "vitest";
3+
import "@testing-library/jest-dom";
4+
import ErrorBoundary from "../../components/ErrorBoundary";
5+
import useAppStore from "../../store/store";
6+
7+
// Component that throws an error for testing
8+
const ThrowError = ({ shouldThrow }: { shouldThrow: boolean }) => {
9+
if (shouldThrow) {
10+
throw new Error("Test error");
11+
}
12+
return <div>Child component</div>;
13+
};
14+
15+
describe("ErrorBoundary", () => {
16+
let consoleErrorSpy: MockInstance;
17+
18+
beforeEach(() => {
19+
// Suppress console.error for cleaner test output
20+
consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
21+
});
22+
23+
afterEach(() => {
24+
consoleErrorSpy.mockRestore();
25+
});
26+
27+
it("should render children when there is no error", () => {
28+
render(
29+
<ErrorBoundary>
30+
<ThrowError shouldThrow={false} />
31+
</ErrorBoundary>
32+
);
33+
34+
expect(screen.getByText("Child component")).toBeInTheDocument();
35+
});
36+
37+
it("should catch error and render fallback UI", () => {
38+
render(
39+
<ErrorBoundary>
40+
<ThrowError shouldThrow={true} />
41+
</ErrorBoundary>
42+
);
43+
44+
expect(screen.getByText("Something went wrong")).toBeInTheDocument();
45+
expect(
46+
screen.getByText(/We apologize for the inconvenience/)
47+
).toBeInTheDocument();
48+
expect(screen.queryByText("Child component")).not.toBeInTheDocument();
49+
});
50+
51+
it("should render reload button that calls window.location.reload", () => {
52+
const reloadMock = vi.fn();
53+
// Mock the entire location object
54+
const originalLocation = window.location;
55+
delete (window as any).location;
56+
window.location = { ...originalLocation, reload: reloadMock } as any;
57+
58+
render(
59+
<ErrorBoundary>
60+
<ThrowError shouldThrow={true} />
61+
</ErrorBoundary>
62+
);
63+
64+
const reloadButton = screen.getByRole("button", { name: /Reload Page/i });
65+
expect(reloadButton).toBeInTheDocument();
66+
67+
reloadButton.click();
68+
expect(reloadMock).toHaveBeenCalledTimes(1);
69+
70+
// Restore original location
71+
delete (window as any).location;
72+
window.location = originalLocation as any;
73+
});
74+
75+
it("should display error details in development mode", () => {
76+
render(
77+
<ErrorBoundary showDevDetails={true}>
78+
<ThrowError shouldThrow={true} />
79+
</ErrorBoundary>
80+
);
81+
82+
const errorDetails = screen.getByText("Error details");
83+
expect(errorDetails).toBeInTheDocument();
84+
85+
const errorMessage = screen.getByText(/Error: Test error/);
86+
expect(errorMessage).toBeInTheDocument();
87+
});
88+
89+
it("should not display error details in production mode", () => {
90+
render(
91+
<ErrorBoundary showDevDetails={false}>
92+
<ThrowError shouldThrow={true} />
93+
</ErrorBoundary>
94+
);
95+
96+
expect(screen.queryByText("Error details")).not.toBeInTheDocument();
97+
});
98+
99+
it("should call componentDidCatch and log error", () => {
100+
render(
101+
<ErrorBoundary>
102+
<ThrowError shouldThrow={true} />
103+
</ErrorBoundary>
104+
);
105+
106+
expect(consoleErrorSpy).toHaveBeenCalled();
107+
expect(consoleErrorSpy).toHaveBeenCalledWith(
108+
"ErrorBoundary caught an error:",
109+
expect.any(Error),
110+
expect.any(Object)
111+
);
112+
});
113+
114+
it("should use theme colors from store", () => {
115+
// Capture previous state and set dark mode
116+
const previousBackgroundColor = useAppStore.getState().backgroundColor;
117+
const previousTextColor = useAppStore.getState().textColor;
118+
useAppStore.setState({ backgroundColor: '#121212', textColor: '#ffffff' });
119+
120+
try {
121+
render(
122+
<ErrorBoundary>
123+
<ThrowError shouldThrow={true} />
124+
</ErrorBoundary>
125+
);
126+
127+
const container = screen.getByText("Something went wrong").parentElement;
128+
expect(container).toHaveStyle({ backgroundColor: '#121212' });
129+
} finally {
130+
// Restore previous state to avoid test order-dependency
131+
useAppStore.setState({
132+
backgroundColor: previousBackgroundColor,
133+
textColor: previousTextColor
134+
});
135+
}
136+
});
137+
138+
it("should display component stack in development mode", async () => {
139+
render(
140+
<ErrorBoundary showDevDetails={true}>
141+
<ThrowError shouldThrow={true} />
142+
</ErrorBoundary>
143+
);
144+
145+
const errorDetails = screen.getByText("Error details");
146+
expect(errorDetails).toBeInTheDocument();
147+
148+
// Component stack should be present in the pre element
149+
// Use waitFor since errorInfo is set via setState in componentDidCatch
150+
await waitFor(() => {
151+
const preElement = screen.getByText(/Error: Test error/).closest('pre');
152+
expect(preElement?.textContent).toContain('Component Stack:');
153+
});
154+
});
155+
});

0 commit comments

Comments
 (0)