Skip to content

Commit dbd435e

Browse files
authored
Test/main navbar (#13)
* setup vitest browser tests * create navbar component * start on test for navbar * add test foor shared components like footer and navbar * add tests for all awesome privacy components * add tests for all feed components * refactpr feed service component * add test for utils * setup vitest for components * add testing library types * add linting and build to pipline * format and lint prepush * install packages for testing and prepush * remove lock file and downgrate vite-plugin-svelte (#14)
1 parent d6692ca commit dbd435e

25 files changed

Lines changed: 1199 additions & 134 deletions

.github/workflows/test.yml

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
name: Tests
1+
name: CI
22

33
on:
44
pull_request:
55

66
jobs:
7-
test:
8-
name: Unit Tests
7+
ci:
8+
name: Type Check, Lint, Test & Build
99
runs-on: ubuntu-latest
1010

1111
steps:
@@ -16,5 +16,14 @@ jobs:
1616
- name: Install dependencies
1717
run: bun install --frozen-lockfile
1818

19-
- name: Run tests
19+
- name: Type check
20+
run: bun run check
21+
22+
- name: Lint
23+
run: bun run lint
24+
25+
- name: Test
2026
run: bun run test
27+
28+
- name: Build
29+
run: bun run build

.husky/pre-push

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
bun run fix

bun.lock

Lines changed: 159 additions & 27 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,42 @@
11
{
2-
"name": "rss-privacy-news",
2+
"name": "10xprivacy",
33
"private": true,
44
"version": "0.0.1",
55
"type": "module",
6+
"author": "dottmp",
67
"scripts": {
78
"dev": "vite dev",
89
"dev:cf": "bun run build && wrangler pages dev .svelte-kit/cloudflare",
910
"build": "vite build",
1011
"preview": "vite preview",
1112
"preview:cf": "wrangler pages dev .svelte-kit/cloudflare",
12-
"prepare": "svelte-kit sync || echo ''",
13+
"prepare": "svelte-kit sync || echo '' && husky",
1314
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
1415
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
1516
"lint": "prettier --check . && eslint .",
1617
"lint:fix": "eslint --fix .",
1718
"format": "prettier --write .",
1819
"fix": "bun run format && bun run lint:fix",
19-
"test:unit": "vitest",
20-
"test:watch": "vitest --watch",
21-
"test": "bun run test:unit",
20+
"test": "vitest run",
21+
"test:ui": "vitest --ui",
22+
"test:watch": "vitest",
23+
"test:coverage": "vitest run --coverage",
2224
"sync:awesome-privacy": "./fetch-awesome-privacy.sh"
2325
},
2426
"devDependencies": {
2527
"@eslint/compat": "^2.0.2",
2628
"@eslint/js": "^9.39.2",
27-
"@sveltejs/adapter-auto": "^7.0.0",
2829
"@sveltejs/adapter-cloudflare": "^7.2.8",
2930
"@sveltejs/kit": "^2.50.2",
3031
"@sveltejs/vite-plugin-svelte": "^6.2.4",
3132
"@tailwindcss/typography": "^0.5.19",
3233
"@tailwindcss/vite": "^4.1.18",
34+
"@testing-library/jest-dom": "^6.9.1",
35+
"@testing-library/svelte": "^5.3.1",
3336
"@types/js-yaml": "^4.0.9",
3437
"@types/node": "^24",
38+
"@vitest/browser-playwright": "^4.1.1",
39+
"@vitest/coverage-v8": "^4.1.1",
3540
"daisyui": "^5.5.19",
3641
"eslint": "^9.39.2",
3742
"eslint-config-prettier": "^10.1.8",
@@ -40,6 +45,8 @@
4045
"eslint-plugin-svelte": "^3.14.0",
4146
"eslint-plugin-unused-imports": "^4.4.1",
4247
"globals": "^17.3.0",
48+
"husky": "^9.1.7",
49+
"jsdom": "^29.0.1",
4350
"prettier": "^3.8.1",
4451
"prettier-plugin-svelte": "^3.4.1",
4552
"prettier-plugin-tailwindcss": "^0.7.2",
@@ -49,7 +56,8 @@
4956
"typescript": "^5.9.3",
5057
"typescript-eslint": "^8.54.0",
5158
"vite": "^7.3.1",
52-
"vitest": "^4.1.0",
59+
"vitest": "^4.1.1",
60+
"vitest-browser-svelte": "^2.1.0",
5361
"wrangler": "^4.76.0"
5462
},
5563
"dependencies": {

src/lib/components/footer.test.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { render, screen } from '@testing-library/svelte';
2+
import { describe, expect, it } from 'vitest';
3+
4+
import Footer from './footer.svelte';
5+
6+
describe('Footer component', () => {
7+
it('GitHub repo link has correct href', () => {
8+
render(Footer);
9+
10+
const repoLink = screen.getByRole('link', { name: /GitHub repo/i });
11+
12+
expect(repoLink.getAttribute('href')).toBe('https://github.com/dottmp/10xprivacy');
13+
});
14+
15+
it('MIT License link has correct href', () => {
16+
render(Footer);
17+
18+
const licenseLink = screen.getByRole('link', { name: /MIT License/i });
19+
20+
expect(licenseLink.getAttribute('href')).toBe(
21+
'https://github.com/dottmp/10xprivacy/blob/main/LICENSE'
22+
);
23+
});
24+
});

src/lib/components/navbar.svelte

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<script lang="ts">
2+
import { resolve } from '$app/paths';
3+
import { Link } from '$lib/components/text';
4+
import ThemeChange from '$lib/components/theme-change.svelte';
5+
import { LOGO } from '$lib/configs';
6+
</script>
7+
8+
<nav class="navbar flex-col justify-center gap-y-2 bg-base-100 px-4 shadow-sm">
9+
<div class="flex w-full items-center">
10+
<!-- logo -->
11+
<div class="flex-none">
12+
<a class=" text-xl font-bold text-primary" href={resolve('/')}>
13+
<pre unselectable="on" class="text-[2px]">
14+
{LOGO}
15+
<span class="sr-only">10xPrivacy</span>
16+
</pre>
17+
</a>
18+
</div>
19+
20+
<!-- desktop nav items -->
21+
<div class=" hidden flex-1 sm:block">
22+
<ul class="flex space-x-6 px-6 [&_a]:font-semibold">
23+
<li><Link href={resolve('/privacy-news')}>Privacy News</Link></li>
24+
<li><Link href={resolve('/awesome-privacy')}>Awesome Privacy</Link></li>
25+
<li><Link href={resolve('/websites')}>Websites</Link></li>
26+
</ul>
27+
</div>
28+
29+
<!-- theme toggle -->
30+
<ThemeChange />
31+
</div>
32+
33+
<!-- mobile nav items -->
34+
<div class=" w-full flex-1 sm:hidden">
35+
<ul class="flex flex-wrap gap-x-6 gap-y-2 [&_a]:font-semibold">
36+
<li><Link href={resolve('/privacy-news')}>Privacy News</Link></li>
37+
<li><Link href={resolve('/awesome-privacy')}>Awesome Privacy</Link></li>
38+
<li><Link href={resolve('/websites')}>Websites</Link></li>
39+
</ul>
40+
</div>
41+
</nav>

src/lib/components/navbar.test.ts

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import { render, screen } from '@testing-library/svelte';
2+
import { describe, expect, it, vi } from 'vitest';
3+
4+
import Navbar from './navbar.svelte';
5+
6+
vi.mock('theme-change');
7+
8+
describe('Navbar component', () => {
9+
describe('URLs', () => {
10+
it('home link has correct href', () => {
11+
render(Navbar);
12+
13+
const homeLink = screen.getByRole('link', { name: /10xPrivacy/i });
14+
15+
expect(homeLink.getAttribute('href')).toBe('/');
16+
});
17+
18+
it('Privacy News link has correct href', () => {
19+
render(Navbar);
20+
const links = screen.getAllByRole('link', { name: /Privacy News/i });
21+
22+
expect(links).toHaveLength(2);
23+
24+
links.forEach((link) => {
25+
expect(link.getAttribute('href')).toBe('/privacy-news');
26+
});
27+
});
28+
29+
it('Awesome Privacy link has correct href', () => {
30+
render(Navbar);
31+
32+
const links = screen.getAllByRole('link', { name: /Awesome Privacy/i });
33+
34+
expect(links).toHaveLength(2);
35+
36+
links.forEach((link) => {
37+
expect(link.getAttribute('href')).toBe('/awesome-privacy');
38+
});
39+
});
40+
41+
it('Websites link has correct href', () => {
42+
render(Navbar);
43+
44+
const links = screen.getAllByRole('link', { name: /Websites/i });
45+
46+
expect(links).toHaveLength(2);
47+
48+
links.forEach((link) => {
49+
expect(link.getAttribute('href')).toBe('/websites');
50+
});
51+
});
52+
});
53+
54+
describe('Breakpoint styling', () => {
55+
it('desktop nav is hidden by default and visible at sm breakpoint', () => {
56+
render(Navbar);
57+
58+
const desktopNav = screen.getByRole('navigation').querySelector('.hidden.sm\\:block');
59+
60+
expect(desktopNav).not.toBeNull();
61+
});
62+
63+
it('mobile nav is visible by default and hidden at sm breakpoint', () => {
64+
render(Navbar);
65+
66+
const mobileNav = screen.getByRole('navigation').querySelector('.sm\\:hidden');
67+
68+
expect(mobileNav).not.toBeNull();
69+
});
70+
71+
it('desktop nav contains all nav links', () => {
72+
render(Navbar);
73+
74+
const desktopNav = screen.getByRole('navigation').querySelector('.hidden.sm\\:block');
75+
76+
const links = desktopNav?.querySelectorAll('a');
77+
78+
expect(links).toHaveLength(3);
79+
});
80+
81+
it('mobile nav contains all nav links', () => {
82+
render(Navbar);
83+
84+
const mobileNav = screen.getByRole('navigation').querySelector('.sm\\:hidden');
85+
86+
const links = mobileNav?.querySelectorAll('a');
87+
88+
expect(links).toHaveLength(3);
89+
});
90+
});
91+
});
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { render, screen } from '@testing-library/svelte';
2+
import { describe, expect, it, vi } from 'vitest';
3+
4+
import CategoriesComponent from './categories.svelte';
5+
6+
import type { Category } from '$lib/features/awesome-privacy/types';
7+
8+
vi.mock('$app/paths', () => ({
9+
resolve: (path: string) => path
10+
}));
11+
12+
const categories: Category[] = [
13+
{
14+
name: 'Essentials',
15+
sections: [
16+
{ name: 'Password Managers', services: [] },
17+
{ name: 'VPNs', services: [] }
18+
]
19+
},
20+
{
21+
name: 'Communication',
22+
sections: [
23+
{ name: 'Messaging', services: [] },
24+
{ name: 'Video Calls', services: [] }
25+
]
26+
}
27+
];
28+
29+
describe('Categories component', () => {
30+
it('renders all category names', () => {
31+
render(CategoriesComponent, { props: { categories } });
32+
33+
categories.forEach((category) => {
34+
expect(screen.getByText(category.name)).toBeTruthy();
35+
});
36+
});
37+
38+
it('renders correct href for each category', () => {
39+
render(CategoriesComponent, { props: { categories } });
40+
41+
const links = screen.getAllByRole('link');
42+
const hrefs = links.map((link) => link.getAttribute('href'));
43+
44+
expect(hrefs).toContain('/awesome-privacy/essentials');
45+
expect(hrefs).toContain('/awesome-privacy/communication');
46+
});
47+
48+
it('renders all section names', () => {
49+
render(CategoriesComponent, { props: { categories } });
50+
51+
categories
52+
.flatMap((category) => category.sections)
53+
.forEach((section) => {
54+
expect(screen.getByText(section.name)).toBeTruthy();
55+
});
56+
});
57+
58+
it('renders correct href for each section link', () => {
59+
render(CategoriesComponent, { props: { categories } });
60+
61+
const links = screen.getAllByRole('link');
62+
63+
const hrefs = links.map((link) => link.getAttribute('href'));
64+
65+
expect(hrefs).toContain('/awesome-privacy/essentials/password-managers');
66+
expect(hrefs).toContain('/awesome-privacy/essentials/vpns');
67+
expect(hrefs).toContain('/awesome-privacy/communication/messaging');
68+
expect(hrefs).toContain('/awesome-privacy/communication/video-calls');
69+
});
70+
71+
it('renders back link to /awesome-privacy', () => {
72+
render(CategoriesComponent, { props: { categories } });
73+
74+
const backLink = screen.getByRole('link', { name: /Back/i });
75+
76+
expect(backLink.getAttribute('href')).toBe('/awesome-privacy');
77+
});
78+
});

0 commit comments

Comments
 (0)