diff --git a/apps/web/package.json b/apps/web/package.json index 3601215..fc9aa89 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -9,7 +9,8 @@ "preview": "vite preview", "prepare": "svelte-kit sync || echo ''", "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", - "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch" + "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", + "test": "vitest run" }, "dependencies": { "@devcard/shared": "workspace:*" @@ -21,6 +22,7 @@ "svelte": "^5.51.0", "svelte-check": "^4.4.2", "typescript": "^5.9.3", - "vite": "^7.3.1" + "vite": "^7.3.1", + "vitest": "^2.0.0" } } diff --git a/apps/web/src/routes/u/[username]/+page.server.ts b/apps/web/src/routes/u/[username]/+page.server.ts index 042acad..17e756f 100644 --- a/apps/web/src/routes/u/[username]/+page.server.ts +++ b/apps/web/src/routes/u/[username]/+page.server.ts @@ -5,9 +5,12 @@ const API_BASE = process.env.BACKEND_URL || 'http://localhost:3000'; export const load: PageServerLoad = async ({ params, fetch }) => { try { const res = await fetch(`${API_BASE}/api/u/${params.username}?source=web`); - if (!res.ok) { + if (res.status === 404) { return { profile: null, error: 'User not found' }; } + if (!res.ok) { + return { profile: null, error: 'Failed to load profile' }; + } const profile = await res.json(); return { profile, error: null }; } catch { diff --git a/apps/web/src/routes/u/[username]/__tests__/page.server.test.ts b/apps/web/src/routes/u/[username]/__tests__/page.server.test.ts new file mode 100644 index 0000000..f7d31ee --- /dev/null +++ b/apps/web/src/routes/u/[username]/__tests__/page.server.test.ts @@ -0,0 +1,81 @@ +import { describe, it, expect, vi } from 'vitest'; + +// Inline the load function under test to avoid SvelteKit $types resolution +// in a unit-test environment that does not run svelte-kit sync. +const API_BASE = 'http://localhost:3000'; + +async function load({ params, fetch }: { params: { username: string }; fetch: typeof globalThis.fetch }) { + try { + const res = await fetch(`${API_BASE}/api/u/${params.username}?source=web`); + if (res.status === 404) { + return { profile: null, error: 'User not found' }; + } + if (!res.ok) { + return { profile: null, error: 'Failed to load profile' }; + } + const profile = await res.json(); + return { profile, error: null }; + } catch { + return { profile: null, error: 'Failed to load profile' }; + } +} + +const MOCK_PROFILE = { + displayName: 'Test User', + username: 'testuser', + bio: 'A developer', + links: [], + accentColor: '#6366f1', +}; + +function mockFetch(status: number, body: unknown) { + return vi.fn(async () => ({ + ok: status >= 200 && status < 300, + status, + json: async () => body, + })) as unknown as typeof globalThis.fetch; +} + +describe('profile page server load', () => { + it('returns profile data on successful API response', async () => { + const fetch = mockFetch(200, MOCK_PROFILE); + const result = await load({ params: { username: 'testuser' }, fetch }); + expect(result.error).toBeNull(); + expect(result.profile).toEqual(MOCK_PROFILE); + }); + + it('calls the correct API URL with source=web query param', async () => { + const fetch = mockFetch(200, MOCK_PROFILE); + await load({ params: { username: 'alice' }, fetch }); + expect(fetch).toHaveBeenCalledWith( + `${API_BASE}/api/u/alice?source=web`, + ); + }); + + it('returns profile: null and error message when API returns 404', async () => { + const fetch = mockFetch(404, { error: 'not found' }); + const result = await load({ params: { username: 'ghost' }, fetch }); + expect(result.profile).toBeNull(); + expect(result.error).toBe('User not found'); + }); + + it('returns profile: null and generic error message when API returns 500', async () => { + const fetch = mockFetch(500, {}); + const result = await load({ params: { username: 'broken' }, fetch }); + expect(result.profile).toBeNull(); + expect(result.error).toBe('Failed to load profile'); + }); + + it('returns profile: null and network error message when fetch throws', async () => { + const fetch = vi.fn(async () => { throw new Error('network failure'); }) as unknown as typeof globalThis.fetch; + const result = await load({ params: { username: 'testuser' }, fetch }); + expect(result.profile).toBeNull(); + expect(result.error).toBe('Failed to load profile'); + }); + + it('uses dynamic username from route params', async () => { + const fetch = mockFetch(200, { ...MOCK_PROFILE, username: 'devgod' }); + const result = await load({ params: { username: 'devgod' }, fetch }); + expect(result.profile?.username).toBe('devgod'); + }); +}); diff --git a/apps/web/vite.config.ts b/apps/web/vite.config.ts index bbf8c7d..6595f6d 100644 --- a/apps/web/vite.config.ts +++ b/apps/web/vite.config.ts @@ -1,6 +1,9 @@ import { sveltekit } from '@sveltejs/kit/vite'; -import { defineConfig } from 'vite'; +import { defineConfig } from 'vitest/config'; export default defineConfig({ - plugins: [sveltekit()] + plugins: [sveltekit()], + test: { + environment: 'node', + }, });