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>
148 lines
3.6 KiB
TypeScript
148 lines
3.6 KiB
TypeScript
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();
|
|
});
|
|
});
|