Skip to content

Commit 1f5ed29

Browse files
Merge branch 'main' into resumesection
2 parents c4a6ac0 + aed644b commit 1f5ed29

3 files changed

Lines changed: 283 additions & 0 deletions

File tree

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { fireEvent, render, screen } from '@testing-library/react';
2+
import { describe, expect, it, vi } from 'vitest';
3+
4+
import CopyRepoButton from './CopyRepoButton';
5+
6+
vi.mock('lucide-react', () => ({
7+
Copy: () => <svg aria-hidden="true" data-testid="copy-icon" />,
8+
}));
9+
10+
Object.assign(navigator, {
11+
clipboard: {
12+
writeText: vi.fn().mockResolvedValue(undefined),
13+
},
14+
});
15+
16+
describe('CopyRepoButton accessibility behavior', () => {
17+
it('renders as an accessible button with visible label', () => {
18+
render(<CopyRepoButton />);
19+
20+
expect(screen.getByRole('button', { name: /copy url/i })).toBeDefined();
21+
});
22+
23+
it('keeps decorative copy icon hidden from assistive technology', () => {
24+
render(<CopyRepoButton />);
25+
26+
expect(screen.getByTestId('copy-icon').getAttribute('aria-hidden')).toBe('true');
27+
});
28+
29+
it('is keyboard focusable through the native button element', () => {
30+
render(<CopyRepoButton />);
31+
32+
const button = screen.getByRole('button', { name: /copy url/i });
33+
button.focus();
34+
35+
expect(document.activeElement).toBe(button);
36+
});
37+
38+
it('updates accessible button name after successful copy action', async () => {
39+
render(<CopyRepoButton />);
40+
41+
const button = screen.getByRole('button', { name: /copy url/i });
42+
fireEvent.click(button);
43+
44+
expect(await screen.findByRole('button', { name: /copied/i })).toBeDefined();
45+
});
46+
47+
it('preserves semantic button role after interaction', async () => {
48+
render(<CopyRepoButton />);
49+
50+
fireEvent.click(screen.getByRole('button', { name: /copy url/i }));
51+
52+
expect(await screen.findByRole('button', { name: /copied/i })).toBeDefined();
53+
expect(screen.getAllByRole('button')).toHaveLength(1);
54+
});
55+
});
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import { describe, it, expect, expectTypeOf } from 'vitest';
2+
import { z } from 'zod';
3+
4+
// Type definitions used by the contributors page
5+
type ExpectedContributor = {
6+
id: number;
7+
username: string;
8+
avatarUrl: string;
9+
contributions: number;
10+
};
11+
12+
type PageProps = {
13+
contributors: ExpectedContributor[];
14+
showAvatars?: boolean;
15+
topN?: number;
16+
};
17+
18+
describe('ContributorsPage type & schema compiler checks (Variation 10)', () => {
19+
it('Case 1: Validate core property shapes match design boundaries', () => {
20+
type Matching = {
21+
id: number;
22+
username: string;
23+
avatarUrl: string;
24+
contributions: number;
25+
};
26+
27+
expectTypeOf<Matching>().toEqualTypeOf<ExpectedContributor>();
28+
});
29+
30+
it('Case 2: Ensure invalid parameters are blocked via static assignability', () => {
31+
type Invalid = {
32+
id: string; // wrong type
33+
username: number; // wrong type
34+
contributions?: string; // wrong and optional
35+
};
36+
37+
expectTypeOf<Invalid>().not.toMatchTypeOf<ExpectedContributor>();
38+
});
39+
40+
it('Case 3: Optional fields accepted safely without compile-time warnings', () => {
41+
const minimal: PageProps = {
42+
contributors: [{ id: 1, username: 'bob', avatarUrl: '/img/bob.png', contributions: 3 }],
43+
};
44+
45+
expect(minimal.contributors.length).toBe(1);
46+
type MinimalType = typeof minimal;
47+
expectTypeOf<MinimalType>().toMatchTypeOf<PageProps>();
48+
});
49+
50+
it('Case 4: Zod runtime schema flags out-of-bound structural types with flat reports', () => {
51+
const contributorSchema = z
52+
.object({
53+
id: z.number().int().positive(),
54+
username: z.string().min(1),
55+
avatarUrl: z.string().min(1),
56+
contributions: z.number().int().nonnegative(),
57+
})
58+
.strict();
59+
60+
const bad = {
61+
id: -5,
62+
username: '',
63+
avatarUrl: 12345,
64+
contributions: -1,
65+
extra: 'unexpected',
66+
} as unknown;
67+
68+
try {
69+
contributorSchema.parse(bad);
70+
expect(false).toBe(true);
71+
} catch (err) {
72+
const zErr = err as z.ZodError;
73+
expect(zErr.issues.length).toBeGreaterThan(0);
74+
// flat validation: all issue paths are shallow (no deep nesting)
75+
const allShallow = zErr.issues.every((i) => i.path.length <= 1);
76+
expect(allShallow).toBe(true);
77+
}
78+
});
79+
80+
it('Case 5: Correct payloads safely clear validation limits', () => {
81+
const contributorSchema = z.object({
82+
id: z.number().int().positive(),
83+
username: z.string().min(1),
84+
avatarUrl: z.string().min(1),
85+
contributions: z.number().int().nonnegative(),
86+
});
87+
88+
const good = {
89+
id: 2,
90+
username: 'alice',
91+
avatarUrl: 'https://cdn.test/a.png',
92+
contributions: 10,
93+
};
94+
95+
const parsed = contributorSchema.parse(good);
96+
expect(parsed).toEqual(good);
97+
type ParsedType = z.infer<typeof contributorSchema>;
98+
expectTypeOf<ParsedType>().toEqualTypeOf<ExpectedContributor>();
99+
});
100+
});
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import React from 'react';
2+
import { render, screen, act } from '@testing-library/react';
3+
import { describe, it, expect } from 'vitest';
4+
import Template from './template';
5+
6+
// Contrast utilities (sRGB luminance & ratio)
7+
function hexToRgb(hex: string) {
8+
const sanitized = hex.replace('#', '');
9+
const bigint = parseInt(
10+
sanitized.length === 3
11+
? sanitized
12+
.split('')
13+
.map((c) => c + c)
14+
.join('')
15+
: sanitized,
16+
16
17+
);
18+
return {
19+
r: (bigint >> 16) & 255,
20+
g: (bigint >> 8) & 255,
21+
b: bigint & 255,
22+
};
23+
}
24+
25+
function linearizeChannel(channel: number) {
26+
const srgb = channel / 255;
27+
return srgb <= 0.03928 ? srgb / 12.92 : Math.pow((srgb + 0.055) / 1.055, 2.4);
28+
}
29+
30+
function luminance(hex: string) {
31+
const { r, g, b } = hexToRgb(hex);
32+
return 0.2126 * linearizeChannel(r) + 0.7152 * linearizeChannel(g) + 0.0722 * linearizeChannel(b);
33+
}
34+
35+
function contrastRatio(hex1: string, hex2: string) {
36+
const L1 = luminance(hex1);
37+
const L2 = luminance(hex2);
38+
const lighter = Math.max(L1, L2);
39+
const darker = Math.min(L1, L2);
40+
return (lighter + 0.05) / (darker + 0.05);
41+
}
42+
43+
describe('App Template theme contrast checks (Variation 3)', () => {
44+
it('Case 1: Emulate a light preset and assert structural identifiers appear', () => {
45+
// Simulate global light preset
46+
document.documentElement.setAttribute('data-theme', 'light');
47+
48+
render(
49+
<Template>
50+
<div data-testid="child-light">Light mode content</div>
51+
</Template>
52+
);
53+
54+
expect(document.documentElement.getAttribute('data-theme')).toBe('light');
55+
expect(screen.getByTestId('child-light')).toBeTruthy();
56+
});
57+
58+
it('Case 2: Emulate a dark preset and verify class adaptations align', () => {
59+
// Simulate dark preset via class
60+
document.documentElement.classList.add('theme-dark');
61+
render(
62+
<Template>
63+
<div data-testid="child-dark">Dark mode content</div>
64+
</Template>
65+
);
66+
67+
expect(document.documentElement.classList.contains('theme-dark')).toBe(true);
68+
expect(screen.getByTestId('child-dark')).toBeTruthy();
69+
});
70+
71+
it('Case 3: Style token strings contain premium color tokens meeting accessibility contrast', () => {
72+
// Example tokens (background and foreground)
73+
const bg = '#ffffff';
74+
const fg = '#0f172a'; // Tailwind slate-900 similar
75+
76+
const ratio = contrastRatio(bg, fg);
77+
// WCAG AA for normal text requires >= 4.5
78+
expect(ratio).toBeGreaterThanOrEqual(4.5);
79+
});
80+
81+
it('Case 4: Background overlays do not clip or hide text colors', () => {
82+
const { container } = render(
83+
<Template>
84+
<div style={{ position: 'relative' }}>
85+
<div
86+
data-testid="overlay"
87+
style={{ position: 'absolute', inset: 0, zIndex: 0, opacity: 0.6 }}
88+
/>
89+
<div data-testid="text" style={{ position: 'relative', zIndex: 1 }}>
90+
Visible text
91+
</div>
92+
</div>
93+
</Template>
94+
);
95+
96+
const overlay = screen.getByTestId('overlay');
97+
const text = screen.getByTestId('text');
98+
99+
// Validate DOM stacking via zIndex values (overlay beneath text)
100+
const overlayZ = (overlay as HTMLElement).style.zIndex || '0';
101+
const textZ = (text as HTMLElement).style.zIndex || '0';
102+
expect(Number(textZ)).toBeGreaterThanOrEqual(Number(overlayZ));
103+
104+
// Ensure text node remains in document and not occluded by being removed
105+
expect(container.contains(text)).toBe(true);
106+
});
107+
108+
it('Case 5: Custom style variables and class bindings remain active across transitions', () => {
109+
// set custom variable
110+
document.documentElement.style.setProperty('--brand', '#123456');
111+
// Render template once
112+
render(
113+
<Template>
114+
<div data-testid="brand">Brand</div>
115+
</Template>
116+
);
117+
118+
// Toggle theme attribute to simulate a global transition
119+
act(() => {
120+
document.documentElement.setAttribute('data-theme', 'dark');
121+
});
122+
123+
const preserved = document.documentElement.style.getPropertyValue('--brand');
124+
expect(preserved).toBe('#123456');
125+
expect(document.documentElement.getAttribute('data-theme')).toBe('dark');
126+
expect(screen.getByTestId('brand')).toBeTruthy();
127+
});
128+
});

0 commit comments

Comments
 (0)