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:
147
frontend/src/routes/(app)/members/MemberCard.test.ts
Normal file
147
frontend/src/routes/(app)/members/MemberCard.test.ts
Normal file
@@ -0,0 +1,147 @@
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { render, screen } from '@testing-library/svelte';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import MemberCard from './MemberCard.svelte';
|
||||
|
||||
const plannerMember = {
|
||||
userId: 'u1',
|
||||
displayName: 'Sarah',
|
||||
role: 'planner',
|
||||
joinedAt: '2024-01-01T00:00:00Z'
|
||||
};
|
||||
|
||||
const regularMember = {
|
||||
userId: 'u2',
|
||||
displayName: 'Tom',
|
||||
role: 'member',
|
||||
joinedAt: '2024-02-01T00:00:00Z'
|
||||
};
|
||||
|
||||
describe('MemberCard', () => {
|
||||
it('shows the member display name', () => {
|
||||
render(MemberCard, {
|
||||
props: {
|
||||
member: plannerMember,
|
||||
isCurrentUser: false,
|
||||
isPlanner: false,
|
||||
onremove: vi.fn(),
|
||||
onrolechange: vi.fn()
|
||||
}
|
||||
});
|
||||
expect(screen.getByText('Sarah')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows "Du"-badge when isCurrentUser is true', () => {
|
||||
render(MemberCard, {
|
||||
props: {
|
||||
member: plannerMember,
|
||||
isCurrentUser: true,
|
||||
isPlanner: false,
|
||||
onremove: vi.fn(),
|
||||
onrolechange: vi.fn()
|
||||
}
|
||||
});
|
||||
expect(screen.getByText('Du')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not show kebab button when isCurrentUser is true', () => {
|
||||
render(MemberCard, {
|
||||
props: {
|
||||
member: plannerMember,
|
||||
isCurrentUser: true,
|
||||
isPlanner: true,
|
||||
onremove: vi.fn(),
|
||||
onrolechange: vi.fn()
|
||||
}
|
||||
});
|
||||
expect(screen.queryByTestId('kebab-btn')).toBeNull();
|
||||
});
|
||||
|
||||
it('does not show kebab button when viewer is not a planner', () => {
|
||||
render(MemberCard, {
|
||||
props: {
|
||||
member: regularMember,
|
||||
isCurrentUser: false,
|
||||
isPlanner: false,
|
||||
onremove: vi.fn(),
|
||||
onrolechange: vi.fn()
|
||||
}
|
||||
});
|
||||
expect(screen.queryByTestId('kebab-btn')).toBeNull();
|
||||
});
|
||||
|
||||
it('shows kebab button for other members when viewer is planner', () => {
|
||||
render(MemberCard, {
|
||||
props: {
|
||||
member: regularMember,
|
||||
isCurrentUser: false,
|
||||
isPlanner: true,
|
||||
onremove: vi.fn(),
|
||||
onrolechange: vi.fn()
|
||||
}
|
||||
});
|
||||
expect(screen.getByTestId('kebab-btn')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('opens dropdown when kebab is clicked', async () => {
|
||||
render(MemberCard, {
|
||||
props: {
|
||||
member: regularMember,
|
||||
isCurrentUser: false,
|
||||
isPlanner: true,
|
||||
onremove: vi.fn(),
|
||||
onrolechange: vi.fn()
|
||||
}
|
||||
});
|
||||
await userEvent.click(screen.getByTestId('kebab-btn'));
|
||||
expect(screen.getByText('Rolle ändern')).toBeInTheDocument();
|
||||
expect(screen.getByText('Entfernen')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('calls onremove when "Entfernen" is clicked in dropdown', async () => {
|
||||
const onremove = vi.fn();
|
||||
render(MemberCard, {
|
||||
props: {
|
||||
member: regularMember,
|
||||
isCurrentUser: false,
|
||||
isPlanner: true,
|
||||
onremove,
|
||||
onrolechange: vi.fn()
|
||||
}
|
||||
});
|
||||
await userEvent.click(screen.getByTestId('kebab-btn'));
|
||||
await userEvent.click(screen.getByText('Entfernen'));
|
||||
expect(onremove).toHaveBeenCalledWith(regularMember);
|
||||
});
|
||||
|
||||
it('shows SegmentedControl when "Rolle ändern" is clicked', async () => {
|
||||
render(MemberCard, {
|
||||
props: {
|
||||
member: regularMember,
|
||||
isCurrentUser: false,
|
||||
isPlanner: true,
|
||||
onremove: vi.fn(),
|
||||
onrolechange: vi.fn()
|
||||
}
|
||||
});
|
||||
await userEvent.click(screen.getByTestId('kebab-btn'));
|
||||
await userEvent.click(screen.getByText('Rolle ändern'));
|
||||
expect(screen.getByRole('group')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('closes dropdown on Escape key', async () => {
|
||||
render(MemberCard, {
|
||||
props: {
|
||||
member: regularMember,
|
||||
isCurrentUser: false,
|
||||
isPlanner: true,
|
||||
onremove: vi.fn(),
|
||||
onrolechange: vi.fn()
|
||||
}
|
||||
});
|
||||
await userEvent.click(screen.getByTestId('kebab-btn'));
|
||||
expect(screen.getByText('Entfernen')).toBeInTheDocument();
|
||||
await userEvent.keyboard('{Escape}');
|
||||
expect(screen.queryByText('Entfernen')).toBeNull();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user