Tests refactor
This commit is contained in:
parent
245520fb50
commit
99134d8556
165 changed files with 911 additions and 531 deletions
280
packages/web/tests/app/(dashboard)/settings/page.test.tsx
Normal file
280
packages/web/tests/app/(dashboard)/settings/page.test.tsx
Normal file
|
|
@ -0,0 +1,280 @@
|
|||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
const { mockUseApi } = vi.hoisted(() => ({
|
||||
mockUseApi: vi.fn(),
|
||||
}));
|
||||
|
||||
const {
|
||||
mockCreateHousehold,
|
||||
mockJoinHousehold,
|
||||
mockGetHousehold,
|
||||
mockUpdateHousehold,
|
||||
mockGenerateInviteCode,
|
||||
} = vi.hoisted(() => ({
|
||||
mockCreateHousehold: vi.fn(),
|
||||
mockJoinHousehold: vi.fn(),
|
||||
mockGetHousehold: vi.fn(),
|
||||
mockUpdateHousehold: vi.fn(),
|
||||
mockGenerateInviteCode: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('@/lib/useApi', () => ({ useApi: mockUseApi }));
|
||||
vi.mock('@/services/households', () => ({
|
||||
createHousehold: mockCreateHousehold,
|
||||
joinHousehold: mockJoinHousehold,
|
||||
getHousehold: mockGetHousehold,
|
||||
updateHousehold: mockUpdateHousehold,
|
||||
generateInviteCode: mockGenerateInviteCode,
|
||||
}));
|
||||
|
||||
import SettingsPage from '../../../../src/app/(dashboard)/settings/page';
|
||||
|
||||
const household = {
|
||||
name: 'My House',
|
||||
inviteCode: 'ABC123',
|
||||
members: [{ userId: 'u-1', role: 'admin' }],
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('SettingsPage', () => {
|
||||
it('shows loading skeleton when loading', () => {
|
||||
mockUseApi.mockReturnValue({ householdId: null, isLoading: true, refreshProfile: vi.fn() });
|
||||
render(<SettingsPage />);
|
||||
expect(screen.getByText('Settings')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Household')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows create/join forms when no household', () => {
|
||||
mockUseApi.mockReturnValue({ householdId: null, isLoading: false, refreshProfile: vi.fn() });
|
||||
render(<SettingsPage />);
|
||||
expect(screen.getByPlaceholderText('Household name')).toBeInTheDocument();
|
||||
expect(screen.getByPlaceholderText('Invite code')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows account section always', () => {
|
||||
mockUseApi.mockReturnValue({ householdId: null, isLoading: false, refreshProfile: vi.fn() });
|
||||
render(<SettingsPage />);
|
||||
expect(screen.getByText('Account')).toBeInTheDocument();
|
||||
expect(screen.getByRole('link', { name: /Manage Keycloak Account/ })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('creates a household on form submit', async () => {
|
||||
const refreshProfile = vi.fn();
|
||||
mockUseApi.mockReturnValue({ householdId: null, isLoading: false, refreshProfile });
|
||||
mockCreateHousehold.mockResolvedValue({});
|
||||
render(<SettingsPage />);
|
||||
|
||||
await userEvent.type(screen.getByPlaceholderText('Household name'), 'New Home');
|
||||
await userEvent.click(screen.getByRole('button', { name: 'Create' }));
|
||||
|
||||
await waitFor(() => expect(mockCreateHousehold).toHaveBeenCalledWith('New Home'));
|
||||
expect(refreshProfile).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('shows error when create fails', async () => {
|
||||
mockUseApi.mockReturnValue({ householdId: null, isLoading: false, refreshProfile: vi.fn() });
|
||||
mockCreateHousehold.mockRejectedValue(new Error('Already exists'));
|
||||
render(<SettingsPage />);
|
||||
|
||||
await userEvent.type(screen.getByPlaceholderText('Household name'), 'New Home');
|
||||
await userEvent.click(screen.getByRole('button', { name: 'Create' }));
|
||||
|
||||
await waitFor(() => expect(screen.getByText('Already exists')).toBeInTheDocument());
|
||||
});
|
||||
|
||||
it('joins a household on form submit', async () => {
|
||||
const refreshProfile = vi.fn();
|
||||
mockUseApi.mockReturnValue({ householdId: null, isLoading: false, refreshProfile });
|
||||
mockJoinHousehold.mockResolvedValue({});
|
||||
render(<SettingsPage />);
|
||||
|
||||
await userEvent.type(screen.getByPlaceholderText('Invite code'), 'XYZ999');
|
||||
await userEvent.click(screen.getByRole('button', { name: 'Join' }));
|
||||
|
||||
await waitFor(() => expect(mockJoinHousehold).toHaveBeenCalledWith('XYZ999'));
|
||||
expect(refreshProfile).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('shows error when join fails', async () => {
|
||||
mockUseApi.mockReturnValue({ householdId: null, isLoading: false, refreshProfile: vi.fn() });
|
||||
mockJoinHousehold.mockRejectedValue(new Error('Invalid code'));
|
||||
render(<SettingsPage />);
|
||||
|
||||
await userEvent.type(screen.getByPlaceholderText('Invite code'), 'BAD');
|
||||
await userEvent.click(screen.getByRole('button', { name: 'Join' }));
|
||||
|
||||
await waitFor(() => expect(screen.getByText('Invalid code')).toBeInTheDocument());
|
||||
});
|
||||
|
||||
it('loads and displays household details when householdId exists', async () => {
|
||||
mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false, refreshProfile: vi.fn() });
|
||||
mockGetHousehold.mockResolvedValue(household);
|
||||
render(<SettingsPage />);
|
||||
|
||||
await waitFor(() => expect(screen.getByText('My House')).toBeInTheDocument());
|
||||
expect(screen.getByText('ABC123')).toBeInTheDocument();
|
||||
expect(screen.getByText('1')).toBeInTheDocument(); // member count
|
||||
});
|
||||
|
||||
it('shows loading household details text initially', async () => {
|
||||
mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false, refreshProfile: vi.fn() });
|
||||
mockGetHousehold.mockReturnValue(new Promise(() => {})); // never resolves
|
||||
render(<SettingsPage />);
|
||||
expect(screen.getByText('Loading household details...')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('allows editing the household name', async () => {
|
||||
mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false, refreshProfile: vi.fn() });
|
||||
mockGetHousehold.mockResolvedValue(household);
|
||||
mockUpdateHousehold.mockResolvedValue({ ...household, name: 'Updated Home' });
|
||||
render(<SettingsPage />);
|
||||
|
||||
await waitFor(() => screen.getByText('My House'));
|
||||
await userEvent.click(screen.getByRole('button', { name: 'Edit' }));
|
||||
|
||||
const input = screen.getByDisplayValue('My House');
|
||||
await userEvent.clear(input);
|
||||
await userEvent.type(input, 'Updated Home');
|
||||
await userEvent.click(screen.getByRole('button', { name: 'Save' }));
|
||||
|
||||
await waitFor(() =>
|
||||
expect(mockUpdateHousehold).toHaveBeenCalledWith('hh1', { name: 'Updated Home' }),
|
||||
);
|
||||
});
|
||||
|
||||
it('shows error when name update fails', async () => {
|
||||
mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false, refreshProfile: vi.fn() });
|
||||
mockGetHousehold.mockResolvedValue(household);
|
||||
mockUpdateHousehold.mockRejectedValue(new Error('Name taken'));
|
||||
render(<SettingsPage />);
|
||||
|
||||
await waitFor(() => screen.getByText('My House'));
|
||||
await userEvent.click(screen.getByRole('button', { name: 'Edit' }));
|
||||
const input = screen.getByDisplayValue('My House');
|
||||
await userEvent.clear(input);
|
||||
await userEvent.type(input, 'Other Home');
|
||||
await userEvent.click(screen.getByRole('button', { name: 'Save' }));
|
||||
|
||||
await waitFor(() => expect(screen.getByText('Name taken')).toBeInTheDocument());
|
||||
});
|
||||
|
||||
it('cancels editing name', async () => {
|
||||
mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false, refreshProfile: vi.fn() });
|
||||
mockGetHousehold.mockResolvedValue(household);
|
||||
render(<SettingsPage />);
|
||||
|
||||
await waitFor(() => screen.getByText('My House'));
|
||||
await userEvent.click(screen.getByRole('button', { name: 'Edit' }));
|
||||
await userEvent.click(screen.getByRole('button', { name: 'Cancel' }));
|
||||
|
||||
expect(screen.getByText('My House')).toBeInTheDocument();
|
||||
expect(screen.queryByDisplayValue('My House')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('regenerates invite code', async () => {
|
||||
mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false, refreshProfile: vi.fn() });
|
||||
mockGetHousehold.mockResolvedValue(household);
|
||||
mockGenerateInviteCode.mockResolvedValue({ ...household, inviteCode: 'NEW999' });
|
||||
render(<SettingsPage />);
|
||||
|
||||
await waitFor(() => screen.getByText('ABC123'));
|
||||
await userEvent.click(screen.getByRole('button', { name: 'Regenerate' }));
|
||||
|
||||
await waitFor(() => expect(screen.getByText('NEW999')).toBeInTheDocument());
|
||||
});
|
||||
|
||||
it('shows error when regenerate fails', async () => {
|
||||
mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false, refreshProfile: vi.fn() });
|
||||
mockGetHousehold.mockResolvedValue(household);
|
||||
mockGenerateInviteCode.mockRejectedValue(new Error('Server error'));
|
||||
render(<SettingsPage />);
|
||||
|
||||
await waitFor(() => screen.getByText('ABC123'));
|
||||
await userEvent.click(screen.getByRole('button', { name: 'Regenerate' }));
|
||||
|
||||
await waitFor(() => expect(screen.getByText('Server error')).toBeInTheDocument());
|
||||
});
|
||||
|
||||
it('saves unchanged name without calling API', async () => {
|
||||
mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false, refreshProfile: vi.fn() });
|
||||
mockGetHousehold.mockResolvedValue(household);
|
||||
render(<SettingsPage />);
|
||||
|
||||
await waitFor(() => screen.getByText('My House'));
|
||||
await userEvent.click(screen.getByRole('button', { name: 'Edit' }));
|
||||
await userEvent.click(screen.getByRole('button', { name: 'Save' }));
|
||||
|
||||
expect(mockUpdateHousehold).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('shows fallback error when non-Error thrown on create household', async () => {
|
||||
mockUseApi.mockReturnValue({ householdId: null, isLoading: false, refreshProfile: vi.fn() });
|
||||
mockCreateHousehold.mockRejectedValue('create failed');
|
||||
render(<SettingsPage />);
|
||||
|
||||
await userEvent.type(screen.getByPlaceholderText('Household name'), 'My Home');
|
||||
await userEvent.click(screen.getByRole('button', { name: 'Create' }));
|
||||
|
||||
await waitFor(() => expect(screen.getByText('Failed to create household')).toBeInTheDocument());
|
||||
});
|
||||
|
||||
it('shows fallback error when non-Error thrown on join household', async () => {
|
||||
mockUseApi.mockReturnValue({ householdId: null, isLoading: false, refreshProfile: vi.fn() });
|
||||
mockJoinHousehold.mockRejectedValue('join failed');
|
||||
render(<SettingsPage />);
|
||||
|
||||
await userEvent.type(screen.getByPlaceholderText('Invite code'), 'XYZ123');
|
||||
await userEvent.click(screen.getByRole('button', { name: 'Join' }));
|
||||
|
||||
await waitFor(() => expect(screen.getByText('Failed to join household')).toBeInTheDocument());
|
||||
});
|
||||
|
||||
it('shows fallback error when non-Error thrown on name update', async () => {
|
||||
mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false, refreshProfile: vi.fn() });
|
||||
mockGetHousehold.mockResolvedValue(household);
|
||||
mockUpdateHousehold.mockRejectedValue('update failed');
|
||||
render(<SettingsPage />);
|
||||
|
||||
await waitFor(() => screen.getByText('My House'));
|
||||
await userEvent.click(screen.getByRole('button', { name: 'Edit' }));
|
||||
const input = screen.getByDisplayValue('My House');
|
||||
await userEvent.clear(input);
|
||||
await userEvent.type(input, 'New Name');
|
||||
await userEvent.click(screen.getByRole('button', { name: 'Save' }));
|
||||
|
||||
await waitFor(() => expect(screen.getByText('Failed to update name')).toBeInTheDocument());
|
||||
});
|
||||
|
||||
it('shows fallback error when non-Error thrown on regenerate', async () => {
|
||||
mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false, refreshProfile: vi.fn() });
|
||||
mockGetHousehold.mockResolvedValue(household);
|
||||
mockGenerateInviteCode.mockRejectedValue('regen failed');
|
||||
render(<SettingsPage />);
|
||||
|
||||
await waitFor(() => screen.getByText('My House'));
|
||||
await userEvent.click(screen.getByRole('button', { name: 'Regenerate' }));
|
||||
|
||||
await waitFor(() =>
|
||||
expect(screen.getByText('Failed to regenerate invite code')).toBeInTheDocument(),
|
||||
);
|
||||
});
|
||||
|
||||
it('shows validation error when saving empty name', async () => {
|
||||
mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false, refreshProfile: vi.fn() });
|
||||
mockGetHousehold.mockResolvedValue(household);
|
||||
render(<SettingsPage />);
|
||||
|
||||
await waitFor(() => screen.getByText('My House'));
|
||||
await userEvent.click(screen.getByRole('button', { name: 'Edit' }));
|
||||
const input = screen.getByDisplayValue('My House');
|
||||
await userEvent.clear(input);
|
||||
await userEvent.click(screen.getByRole('button', { name: 'Save' }));
|
||||
|
||||
expect(screen.getByText('Name cannot be empty')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue