Skip to content

Commit 27956a6

Browse files
authored
Merge branch 'main' into issue-resume-preview-error-resilience
2 parents 062f032 + 317e3e4 commit 27956a6

3 files changed

Lines changed: 477 additions & 0 deletions

File tree

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
import { expect, it, describe, vi, beforeEach, beforeAll } from 'vitest';
2+
import { render, screen, fireEvent } from '@testing-library/react';
3+
import React, { Component, ErrorInfo, ReactNode } from 'react';
4+
5+
const mockTelemetry = {
6+
trackException: vi.fn(),
7+
};
8+
9+
vi.mock('../utils/telemetry', () => ({
10+
telemetry: {
11+
trackException: (...args: unknown[]) => mockTelemetry.trackException(...args),
12+
},
13+
}));
14+
15+
interface Props {
16+
children: ReactNode;
17+
forceError?: boolean;
18+
}
19+
20+
interface State {
21+
hasError: boolean;
22+
}
23+
24+
class TestErrorBoundary extends Component<Props, State> {
25+
public state: State = {
26+
hasError: this.props.forceError || false,
27+
};
28+
29+
public componentDidMount() {
30+
if (this.props.forceError) {
31+
this.componentDidCatch(new Error('Database connectivity error'), { componentStack: '' });
32+
}
33+
}
34+
35+
public componentDidCatch(error: Error, errorInfo: ErrorInfo) {
36+
mockTelemetry.trackException(error, errorInfo);
37+
}
38+
39+
public render() {
40+
if (this.state.hasError) {
41+
return (
42+
<div data-testid="error-fallback">
43+
<h2>Something went wrong</h2>
44+
<button onClick={() => this.setState({ hasError: false })}>Retry</button>
45+
</div>
46+
);
47+
}
48+
49+
return this.props.children;
50+
}
51+
}
52+
53+
describe('WallOfLove Error Resilience', () => {
54+
let WallOfLoveModule: React.ComponentType<{ tweets: unknown[] }>;
55+
56+
beforeAll(async () => {
57+
Object.defineProperty(window, 'matchMedia', {
58+
writable: true,
59+
value: vi.fn().mockImplementation((query) => ({
60+
matches: false,
61+
media: query,
62+
onchange: null,
63+
addListener: vi.fn(),
64+
removeListener: vi.fn(),
65+
addEventListener: vi.fn(),
66+
removeEventListener: vi.fn(),
67+
dispatchEvent: vi.fn(),
68+
})),
69+
});
70+
71+
const mod = await import('./WallOfLove');
72+
WallOfLoveModule = mod.WallOfLove;
73+
});
74+
75+
beforeEach(() => {
76+
vi.clearAllMocks();
77+
vi.spyOn(console, 'error').mockImplementation(() => {});
78+
});
79+
80+
it('should maintain hydration stability when initial data is missing', () => {
81+
const { container } = render(<WallOfLoveModule tweets={[]} />);
82+
expect(container).toBeDefined();
83+
});
84+
85+
it('should render error recovery UI when runtime exception occurs', () => {
86+
render(
87+
<TestErrorBoundary forceError={true}>
88+
<WallOfLoveModule tweets={[]} />
89+
</TestErrorBoundary>
90+
);
91+
92+
expect(screen.getByTestId('error-fallback')).toBeDefined();
93+
expect(screen.getByText(/something went wrong/i)).toBeDefined();
94+
});
95+
96+
it('should log exception to dev-telemetry tracker on failure', () => {
97+
render(
98+
<TestErrorBoundary forceError={true}>
99+
<WallOfLoveModule tweets={[]} />
100+
</TestErrorBoundary>
101+
);
102+
103+
expect(mockTelemetry.trackException).toHaveBeenCalled();
104+
});
105+
106+
it('should prevent full screen crashes using localized boundary containment', () => {
107+
render(
108+
<div>
109+
<header data-testid="app-header">App Header</header>
110+
<TestErrorBoundary forceError={true}>
111+
<WallOfLoveModule tweets={[]} />
112+
</TestErrorBoundary>
113+
</div>
114+
);
115+
expect(screen.getByTestId('app-header')).toBeDefined();
116+
});
117+
118+
it('should trigger user reset or reload path on recovery panel click', () => {
119+
render(
120+
<TestErrorBoundary forceError={true}>
121+
<WallOfLoveModule tweets={[]} />
122+
</TestErrorBoundary>
123+
);
124+
125+
const resetButton = screen.getByRole('button', { name: /retry/i });
126+
fireEvent.click(resetButton);
127+
128+
expect(screen.queryByTestId('error-fallback')).toBeNull();
129+
});
130+
});
Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2+
import { render, screen, fireEvent, act } from '@testing-library/react';
3+
import type { DashboardPeriod } from '@/utils/dashboardPeriod';
4+
import type { ActivityData } from '@/types/dashboard';
5+
import HistoricalTrendView from './HistoricalTrendView';
6+
import '@testing-library/jest-dom/vitest';
7+
8+
beforeEach(() => {
9+
const MockResizeObserver = class {
10+
observe = vi.fn();
11+
unobserve = vi.fn();
12+
disconnect = vi.fn();
13+
};
14+
vi.stubGlobal('ResizeObserver', MockResizeObserver);
15+
});
16+
17+
afterEach(() => {
18+
vi.unstubAllGlobals();
19+
});
20+
21+
vi.mock('next/navigation', () => ({
22+
useRouter: () => ({
23+
push: vi.fn(),
24+
}),
25+
}));
26+
27+
vi.mock('framer-motion', async () => {
28+
const React = await import('react');
29+
const MotionDiv = React.forwardRef<
30+
HTMLDivElement,
31+
React.HTMLAttributes<HTMLDivElement> & Record<string, unknown>
32+
>((props, ref) => {
33+
const domProps = { ...props };
34+
delete domProps.whileInView;
35+
delete domProps.viewport;
36+
delete domProps.initial;
37+
delete domProps.animate;
38+
delete domProps.exit;
39+
delete domProps.transition;
40+
return React.createElement('div', { ref, ...domProps });
41+
});
42+
MotionDiv.displayName = 'MotionDiv';
43+
return {
44+
motion: {
45+
div: MotionDiv,
46+
},
47+
AnimatePresence: ({ children }: { children: React.ReactNode }) => <>{children}</>,
48+
};
49+
});
50+
51+
const mockPeriod: DashboardPeriod = {
52+
kind: 'month',
53+
month: '2026-06',
54+
label: 'June 2026',
55+
from: '2026-06-01T00:00:00.000Z',
56+
to: '2026-06-30T23:59:59.999Z',
57+
};
58+
59+
const mockActivity: ActivityData[] = Array.from({ length: 30 }, (_, index) => ({
60+
date: `2026-06-${String(index + 1).padStart(2, '0')}`,
61+
count: index % 3 === 0 ? 5 : 0,
62+
intensity: index % 3 === 0 ? 2 : 0,
63+
}));
64+
65+
describe('HistoricalTrendView - Accessibility & Aria compliance (Variation 4)', () => {
66+
// Case 1: Verifies interactive grids expose valid cell coordinate label configurations
67+
it('Case 1: Inspect markup landmarks to verify the correct usage of accessible coordinate labels on container modules', () => {
68+
render(<HistoricalTrendView username="ashish" activity={mockActivity} period={mockPeriod} />);
69+
70+
const grid = screen.getByRole('grid');
71+
expect(grid).toBeInTheDocument();
72+
73+
const rows = screen.getAllByRole('row');
74+
expect(rows.length).toBeGreaterThan(0);
75+
76+
const cells = screen.getAllByRole('gridcell');
77+
expect(cells.length).toBeGreaterThan(0);
78+
79+
cells.forEach((cell) => {
80+
expect(cell).toHaveAttribute('aria-label');
81+
expect(cell.getAttribute('aria-label')).toMatch(/contributions? on/i);
82+
});
83+
});
84+
85+
// Case 2: Confirms active form and keyboard interaction nodes maintain focus indicator rings
86+
it('Case 2: Assert interactive nodes designed to accept focus maintain visible outline class parameters', () => {
87+
const { container } = render(
88+
<HistoricalTrendView username="ashish" activity={mockActivity} period={mockPeriod} />
89+
);
90+
91+
// Verify inputs can receive focus natively to test accessibility behaviors
92+
const inputs = container.querySelectorAll('input');
93+
expect(inputs.length).toBeGreaterThan(0);
94+
act(() => {
95+
inputs.forEach((input) => {
96+
input.focus();
97+
expect(document.activeElement).toBe(input);
98+
});
99+
});
100+
101+
// Verify grid cells are natively capable of receiving focus
102+
const cells = screen.getAllByRole('gridcell');
103+
expect(cells.length).toBeGreaterThan(0);
104+
act(() => {
105+
cells.forEach((cell) => {
106+
expect(cell).toHaveProperty('tabIndex', 0);
107+
cell.focus();
108+
expect(document.activeElement).toBe(cell);
109+
});
110+
});
111+
});
112+
113+
// Case 3: Confirms focused cells successfully spawn announced overlays reflecting target string definitions
114+
it('Case 3: Verify the trend data tooltip triggers accurately match and announce their descriptive accessibility target nodes', async () => {
115+
render(<HistoricalTrendView username="ashish" activity={mockActivity} period={mockPeriod} />);
116+
117+
const cells = screen.getAllByRole('gridcell');
118+
const firstCell = cells[0];
119+
const cellLabel = firstCell.getAttribute('aria-label');
120+
121+
await act(async () => {
122+
fireEvent.focus(firstCell);
123+
});
124+
125+
const tooltip = await screen.findByRole('tooltip');
126+
expect(tooltip).toBeInTheDocument();
127+
expect(tooltip).toHaveTextContent(cellLabel || '');
128+
});
129+
130+
// Case 4: Evaluates chronological tab indexes across controls and grid structures
131+
it('Case 4: Test keyboard control path selectors to ensure normal tab ordering', () => {
132+
const { container } = render(
133+
<HistoricalTrendView username="ashish" activity={mockActivity} period={mockPeriod} />
134+
);
135+
136+
// Using lowercase tabindex in resilient selector query to fetch focusable elements
137+
const focusable = Array.from(
138+
container.querySelectorAll('button, input, [tabindex]:not([tabindex="-1"])')
139+
) as HTMLElement[];
140+
141+
expect(focusable.length).toBeGreaterThan(0);
142+
143+
const previousBtn = screen.getByRole('button', { name: /previous/i });
144+
const nextBtn = screen.getByRole('button', { name: /next/i });
145+
const cells = screen.getAllByRole('gridcell');
146+
147+
expect(previousBtn).toBeInTheDocument();
148+
expect(nextBtn).toBeInTheDocument();
149+
expect(cells[0]).toBeInTheDocument();
150+
151+
const prevIndex = focusable.indexOf(previousBtn);
152+
const nextIndex = focusable.indexOf(nextBtn);
153+
const firstCellIndex = focusable.indexOf(cells[0]);
154+
155+
expect(prevIndex).toBeGreaterThanOrEqual(0);
156+
expect(nextIndex).toBeGreaterThanOrEqual(0);
157+
expect(firstCellIndex).toBeGreaterThanOrEqual(0);
158+
159+
expect(prevIndex).toBeLessThan(nextIndex);
160+
161+
const monthInput = container.querySelector('input[name="month"]');
162+
const yearInput = container.querySelector('input[name="year"]');
163+
164+
expect(monthInput).toBeInTheDocument();
165+
expect(yearInput).toBeInTheDocument();
166+
167+
const monthIndex = focusable.indexOf(monthInput as HTMLElement);
168+
const yearIndex = focusable.indexOf(yearInput as HTMLElement);
169+
170+
expect(monthIndex).toBeGreaterThanOrEqual(0);
171+
expect(yearIndex).toBeGreaterThanOrEqual(0);
172+
173+
expect(nextIndex).toBeLessThan(monthIndex);
174+
expect(monthIndex).toBeLessThan(yearIndex);
175+
expect(yearIndex).toBeLessThan(firstCellIndex);
176+
});
177+
178+
// Case 5: Ensures typography progression tree handles semantic levels without dropping sequence nodes
179+
it('Case 5: Confirm that standard typography headings follow a logical hierarchical numerical sequence', () => {
180+
const { container } = render(
181+
<HistoricalTrendView username="ashish" activity={mockActivity} period={mockPeriod} />
182+
);
183+
184+
const headings = Array.from(
185+
container.querySelectorAll('h1, h2, h3, h4, h5, h6')
186+
) as HTMLElement[];
187+
expect(headings.length).toBeGreaterThan(0);
188+
189+
const levels = headings.map((h) => parseInt(h.tagName.substring(1), 10));
190+
191+
for (let i = 0; i < levels.length - 1; i++) {
192+
const current = levels[i];
193+
const next = levels[i + 1];
194+
expect(next - current).toBeLessThanOrEqual(1);
195+
}
196+
});
197+
});

0 commit comments

Comments
 (0)