Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Comment on lines 10 to +13
},
"dependencies": {
"@devcard/shared": "workspace:*"
Expand All @@ -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"
}
}
5 changes: 4 additions & 1 deletion apps/web/src/routes/u/[username]/+page.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
81 changes: 81 additions & 0 deletions apps/web/src/routes/u/[username]/__tests__/page.server.test.ts
Original file line number Diff line number Diff line change
@@ -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' };
}
Comment on lines +3 to +12
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');
});
});
7 changes: 5 additions & 2 deletions apps/web/vite.config.ts
Original file line number Diff line number Diff line change
@@ -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',
},
});