Skip to content

Commit 2fbb0b2

Browse files
test(theme-switch): add error resilience coverage (JhaSourav07#3019)
## Description Fixes JhaSourav07#2838 Adds error resilience and exception-handling test coverage for the theme switch component. ### Added Tests * Verifies component behavior when rendering encounters unexpected errors * Verifies fallback handling under failure scenarios * Verifies recovery behavior after transient failures * Verifies resilience against invalid or unexpected state transitions * Verifies stable rendering without unhandled exceptions ## Pillar * [ ] 🎨 Pillar 1 — New Theme Design * [ ] 📐 Pillar 2 — Geometric SVG Improvement * [ ] 🕐 Pillar 3 — Timezone Logic Optimization * [x] 🛠️ Other (Bug fix, refactoring, docs) ## Visual Preview N/A (test-only changes) ## Checklist before requesting a review: * [x] I have read the `CONTRIBUTING.md` file. * [ ] I have tested these changes locally (`localhost:3000/api/streak?user=YOUR_USERNAME`). * [ ] I have run `npm run format` and `npm run lint` locally and resolved all errors (CI will fail otherwise). * [x] My commits follow the Conventional Commits format (e.g., `feat(themes): ...`, `fix(calculate): ...`). * [ ] I have updated `README.md` if I added a new theme or URL parameter. * [x] I have started the repo. * [x] I have made sure that i have only one commit to merge in this PR. * [ ] The SVG output matches the CommitPulse "premium quality" aesthetic standard (no raw elements, smooth animations, correct fonts). * [x] (Recommended) I joined the CommitPulse Discord community for contributor discussions, mentorship, and faster PR support.
1 parent 0089d0e commit 2fbb0b2

1 file changed

Lines changed: 169 additions & 0 deletions

File tree

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
import React, { type ErrorInfo } from 'react';
2+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
3+
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
4+
import { ThemeToggleButton, createAnimation } from './theme-switch';
5+
6+
type ErrorBoundaryProps = React.PropsWithChildren<{
7+
onError?: (error: Error, info: ErrorInfo) => void;
8+
onReset?: () => void;
9+
}>;
10+
11+
interface ErrorBoundaryState {
12+
hasError: boolean;
13+
}
14+
15+
class TestErrorBoundary extends React.Component<ErrorBoundaryProps, ErrorBoundaryState> {
16+
constructor(props: ErrorBoundaryProps) {
17+
super(props);
18+
this.state = { hasError: false };
19+
}
20+
21+
static getDerivedStateFromError() {
22+
return { hasError: true };
23+
}
24+
25+
componentDidCatch(error: Error, info: ErrorInfo) {
26+
this.props.onError?.(error, info);
27+
}
28+
29+
handleReset = () => {
30+
this.setState({ hasError: false });
31+
this.props.onReset?.();
32+
};
33+
34+
render() {
35+
if (this.state.hasError) {
36+
return (
37+
<div>
38+
<p>Error recovery panel</p>
39+
<button onClick={this.handleReset}>Try again</button>
40+
</div>
41+
);
42+
}
43+
44+
return this.props.children as React.ReactElement;
45+
}
46+
}
47+
48+
describe('ThemeSwitch error resilience', () => {
49+
let originalLocalStorage: Storage;
50+
let originalMatchMedia: typeof window.matchMedia;
51+
let originalCreateElement: Document['createElement'];
52+
53+
beforeEach(() => {
54+
originalLocalStorage = window.localStorage;
55+
originalMatchMedia = window.matchMedia;
56+
originalCreateElement = document.createElement.bind(document);
57+
58+
Object.defineProperty(window, 'matchMedia', {
59+
configurable: true,
60+
writable: true,
61+
value: vi.fn().mockReturnValue({
62+
matches: false,
63+
addListener: vi.fn(),
64+
removeListener: vi.fn(),
65+
addEventListener: vi.fn(),
66+
removeEventListener: vi.fn(),
67+
dispatchEvent: vi.fn(),
68+
onchange: null,
69+
}),
70+
});
71+
});
72+
73+
afterEach(() => {
74+
Object.defineProperty(window, 'localStorage', {
75+
configurable: true,
76+
writable: true,
77+
value: originalLocalStorage,
78+
});
79+
80+
Object.defineProperty(window, 'matchMedia', {
81+
configurable: true,
82+
writable: true,
83+
value: originalMatchMedia,
84+
});
85+
86+
document.createElement = originalCreateElement;
87+
vi.restoreAllMocks();
88+
});
89+
90+
it('renders ThemeToggleButton without crashing and shows placeholder before hydration', () => {
91+
render(
92+
<TestErrorBoundary>
93+
<ThemeToggleButton />
94+
</TestErrorBoundary>
95+
);
96+
97+
const button = screen.getByRole('button', { name: /toggle theme/i });
98+
expect(button).toBeTruthy();
99+
});
100+
101+
it('recovers gracefully when localStorage throws during initial theme resolution', () => {
102+
const errorSpy = vi.fn();
103+
const mockStorage = {
104+
getItem: vi.fn(() => {
105+
throw new Error('LocalStorage unavailable');
106+
}),
107+
setItem: vi.fn(),
108+
removeItem: vi.fn(),
109+
clear: vi.fn(),
110+
key: vi.fn(),
111+
length: 0,
112+
} as unknown as Storage;
113+
114+
Object.defineProperty(window, 'localStorage', {
115+
configurable: true,
116+
writable: true,
117+
value: mockStorage,
118+
});
119+
120+
render(
121+
<TestErrorBoundary onError={errorSpy}>
122+
<ThemeToggleButton />
123+
</TestErrorBoundary>
124+
);
125+
126+
expect(errorSpy).toHaveBeenCalled();
127+
expect(screen.getByText(/Error recovery panel/i)).toBeTruthy();
128+
});
129+
130+
it('allows the error recovery panel reset button to restore the UI', async () => {
131+
const errorSpy = vi.fn();
132+
const resetSpy = vi.fn();
133+
const mockStorage = {
134+
getItem: vi.fn(() => {
135+
throw new Error('LocalStorage unavailable');
136+
}),
137+
setItem: vi.fn(),
138+
removeItem: vi.fn(),
139+
clear: vi.fn(),
140+
key: vi.fn(),
141+
length: 0,
142+
} as unknown as Storage;
143+
144+
Object.defineProperty(window, 'localStorage', {
145+
configurable: true,
146+
writable: true,
147+
value: mockStorage,
148+
});
149+
150+
render(
151+
<TestErrorBoundary onError={errorSpy} onReset={resetSpy}>
152+
<ThemeToggleButton />
153+
</TestErrorBoundary>
154+
);
155+
156+
const resetButton = screen.getByRole('button', { name: /try again/i });
157+
fireEvent.click(resetButton);
158+
159+
expect(resetSpy).toHaveBeenCalled();
160+
});
161+
162+
it('generates a safe animation object for circle variant without throwing', () => {
163+
const animation = createAnimation('circle', 'center', true, 'https://example.com/test.gif');
164+
165+
expect(animation).toBeDefined();
166+
expect(animation.name).toContain('circle-center-blur');
167+
expect(animation.css).toContain('animation-duration');
168+
});
169+
});

0 commit comments

Comments
 (0)