feat(members): implement /members page — Kachel-Ansicht (E2, issue #48)
Backend: - Rename V006 migration to V026 (avoid conflict with existing V006) - Migration adds invalidated_at + partial unique index on household_invite Frontend: - Toast.svelte — new system component (message + dismiss) - SegmentedControl.svelte — new system component (options, value, onchange) - members/+page.server.ts — loads members + active invite - members/[userId]/+server.ts — DELETE/PATCH proxy - members/invites/+server.ts — POST (regenerate) proxy - MemberCard.svelte — tile with avatar, kebab, inline role edit - RemoveDialog.svelte — confirmation dialog (desktop modal + BottomSheet mobile) - InviteCard.svelte + InvitePanel.svelte — invite management UI - MemberGrid.svelte — responsive 4/2-col grid with sorted members - members/+page.svelte — page composing all components with optimistic updates Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
74
frontend/src/routes/(app)/members/page.server.test.ts
Normal file
74
frontend/src/routes/(app)/members/page.server.test.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
|
||||
vi.mock('$lib/server/api', () => ({
|
||||
apiClient: vi.fn(() => ({
|
||||
GET: vi.fn()
|
||||
}))
|
||||
}));
|
||||
|
||||
vi.mock('$env/dynamic/private', () => ({ env: { BACKEND_URL: 'http://localhost:8080' } }));
|
||||
|
||||
describe('members page.server load', () => {
|
||||
let load: any;
|
||||
|
||||
beforeEach(async () => {
|
||||
vi.resetModules();
|
||||
const mod = await import('./+page.server');
|
||||
load = mod.load;
|
||||
});
|
||||
|
||||
it('returns members and currentUserId', async () => {
|
||||
const mockGet = vi.fn().mockImplementation((path: string) => {
|
||||
if (path === '/v1/households/mine/members') {
|
||||
return {
|
||||
data: [
|
||||
{ userId: 'u1', displayName: 'Sarah', role: 'planner', joinedAt: '2024-01-01T00:00:00Z' },
|
||||
{ userId: 'u2', displayName: 'Tom', role: 'member', joinedAt: '2024-02-01T00:00:00Z' }
|
||||
]
|
||||
};
|
||||
}
|
||||
if (path === '/v1/households/mine/invites') {
|
||||
return {
|
||||
data: {
|
||||
data: {
|
||||
inviteCode: 'ABC123',
|
||||
shareUrl: 'https://x.com/join/ABC123',
|
||||
expiresAt: '2024-12-01T00:00:00Z'
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
return { data: null };
|
||||
});
|
||||
|
||||
const { apiClient } = await import('$lib/server/api');
|
||||
(apiClient as ReturnType<typeof vi.fn>).mockReturnValue({ GET: mockGet });
|
||||
|
||||
const result = await load({
|
||||
fetch: vi.fn(),
|
||||
locals: { benutzer: { id: 'u1', name: 'Sarah', email: 'sarah@example.com' }, haushalt: {} }
|
||||
} as any);
|
||||
|
||||
expect(result.members).toHaveLength(2);
|
||||
expect(result.currentUserId).toBe('u1');
|
||||
expect(result.activeInvite).toBeDefined();
|
||||
});
|
||||
|
||||
it('returns null activeInvite when no active invite exists', async () => {
|
||||
const mockGet = vi.fn().mockImplementation((path: string) => {
|
||||
if (path === '/v1/households/mine/members') return { data: [] };
|
||||
if (path === '/v1/households/mine/invites') return { data: null };
|
||||
return { data: null };
|
||||
});
|
||||
|
||||
const { apiClient } = await import('$lib/server/api');
|
||||
(apiClient as ReturnType<typeof vi.fn>).mockReturnValue({ GET: mockGet });
|
||||
|
||||
const result = await load({
|
||||
fetch: vi.fn(),
|
||||
locals: { benutzer: { id: 'u1', name: 'Sarah', email: 'sarah@example.com' }, haushalt: {} }
|
||||
} as any);
|
||||
|
||||
expect(result.activeInvite).toBeNull();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user