749 lines
25 KiB
TypeScript
749 lines
25 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||
import { render, screen, waitFor, fireEvent } from '@testing-library/react';
|
||
import userEvent from '@testing-library/user-event';
|
||
|
||
const { mockUseApi } = vi.hoisted(() => ({
|
||
mockUseApi: vi.fn(),
|
||
}));
|
||
|
||
const { mockListStores, mockCreateStore, mockUpdateStore, mockDeactivateStore } = vi.hoisted(
|
||
() => ({
|
||
mockListStores: vi.fn(),
|
||
mockCreateStore: vi.fn(),
|
||
mockUpdateStore: vi.fn(),
|
||
mockDeactivateStore: vi.fn(),
|
||
}),
|
||
);
|
||
|
||
vi.mock('@/lib/useApi', () => ({ useApi: mockUseApi }));
|
||
|
||
vi.mock('@/services/stores', () => ({
|
||
listStores: mockListStores,
|
||
createStore: mockCreateStore,
|
||
updateStore: mockUpdateStore,
|
||
deactivateStore: mockDeactivateStore,
|
||
}));
|
||
|
||
vi.mock('next/link', () => {
|
||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||
return { default: (props: any) => props.children };
|
||
});
|
||
|
||
import StoresPage from '../../../../src/app/(dashboard)/stores/page';
|
||
|
||
beforeEach(() => vi.clearAllMocks());
|
||
|
||
describe('StoresPage', () => {
|
||
it('shows loading skeleton when session is loading', () => {
|
||
mockUseApi.mockReturnValue({ householdId: null, isLoading: true });
|
||
|
||
render(<StoresPage />);
|
||
|
||
expect(screen.getByText('Stores')).toBeInTheDocument();
|
||
expect(screen.queryByText('Add Store')).not.toBeInTheDocument();
|
||
});
|
||
|
||
it('shows household prompt when no householdId', () => {
|
||
mockUseApi.mockReturnValue({ householdId: null, isLoading: false });
|
||
|
||
render(<StoresPage />);
|
||
|
||
expect(screen.getByText(/create or join a household/)).toBeInTheDocument();
|
||
});
|
||
|
||
it('renders store list when household exists', async () => {
|
||
mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false });
|
||
mockListStores.mockResolvedValue({
|
||
data: [
|
||
{
|
||
_id: 'st-1',
|
||
name: 'Walgreens',
|
||
tags: ['pharmacy'],
|
||
isActive: true,
|
||
createdAt: '2026-01-01T00:00:00.000Z',
|
||
updatedAt: '2026-01-01T00:00:00.000Z',
|
||
householdId: 'hh1',
|
||
createdBy: 'u-1',
|
||
},
|
||
],
|
||
pagination: { cursor: null, hasMore: false },
|
||
});
|
||
|
||
render(<StoresPage />);
|
||
|
||
await waitFor(() => {
|
||
expect(screen.getByText('Walgreens')).toBeInTheDocument();
|
||
});
|
||
});
|
||
|
||
it('shows empty state when no stores', async () => {
|
||
mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false });
|
||
mockListStores.mockResolvedValue({
|
||
data: [],
|
||
pagination: { cursor: null, hasMore: false },
|
||
});
|
||
|
||
render(<StoresPage />);
|
||
|
||
await waitFor(() => {
|
||
expect(screen.getByText(/No stores yet/)).toBeInTheDocument();
|
||
});
|
||
});
|
||
|
||
it('shows Add Store form when button clicked', async () => {
|
||
mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false });
|
||
mockListStores.mockResolvedValue({
|
||
data: [],
|
||
pagination: { cursor: null, hasMore: false },
|
||
});
|
||
|
||
render(<StoresPage />);
|
||
|
||
await waitFor(() => {
|
||
expect(screen.getByText('Add Store')).toBeInTheDocument();
|
||
});
|
||
|
||
await userEvent.click(screen.getByText('Add Store'));
|
||
|
||
expect(screen.getByPlaceholderText('e.g. Walgreens')).toBeInTheDocument();
|
||
});
|
||
|
||
it('shows error when store list fails to load', async () => {
|
||
mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false });
|
||
mockListStores.mockRejectedValue(new Error('Network error'));
|
||
|
||
render(<StoresPage />);
|
||
|
||
await waitFor(() => {
|
||
expect(screen.getByText('Network error')).toBeInTheDocument();
|
||
});
|
||
});
|
||
|
||
it('deactivates store after confirmation', async () => {
|
||
mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false });
|
||
mockListStores.mockResolvedValue({
|
||
data: [
|
||
{
|
||
_id: 'st-1',
|
||
name: 'CVS',
|
||
tags: [],
|
||
isActive: true,
|
||
createdAt: '2026-01-01T00:00:00.000Z',
|
||
updatedAt: '2026-01-01T00:00:00.000Z',
|
||
householdId: 'hh1',
|
||
createdBy: 'u-1',
|
||
},
|
||
],
|
||
pagination: { cursor: null, hasMore: false },
|
||
});
|
||
mockDeactivateStore.mockResolvedValue({});
|
||
vi.spyOn(window, 'confirm').mockReturnValue(true);
|
||
|
||
render(<StoresPage />);
|
||
|
||
await waitFor(() => {
|
||
expect(screen.getByText('CVS')).toBeInTheDocument();
|
||
});
|
||
|
||
await userEvent.click(screen.getByTitle('Deactivate'));
|
||
|
||
expect(mockDeactivateStore).toHaveBeenCalledWith('hh1', 'st-1');
|
||
});
|
||
|
||
it('creates a store on form submit', async () => {
|
||
mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false });
|
||
mockListStores.mockResolvedValue({ data: [], pagination: { cursor: null, hasMore: false } });
|
||
mockCreateStore.mockResolvedValue({
|
||
_id: 'st-new',
|
||
name: 'Walmart',
|
||
tags: [],
|
||
isActive: true,
|
||
createdAt: '2026-01-01T00:00:00.000Z',
|
||
householdId: 'hh1',
|
||
createdBy: 'u-1',
|
||
});
|
||
|
||
render(<StoresPage />);
|
||
|
||
await waitFor(() => screen.getByText('Add Store'));
|
||
await userEvent.click(screen.getByText('Add Store'));
|
||
|
||
await waitFor(() => screen.getByPlaceholderText('e.g. Walgreens'));
|
||
fireEvent.change(screen.getByPlaceholderText('e.g. Walgreens'), {
|
||
target: { value: 'Walmart' },
|
||
});
|
||
fireEvent.submit(screen.getByPlaceholderText('e.g. Walgreens').closest('form')!);
|
||
|
||
await waitFor(() =>
|
||
expect(mockCreateStore).toHaveBeenCalledWith(
|
||
'hh1',
|
||
expect.objectContaining({ name: 'Walmart' }),
|
||
),
|
||
);
|
||
});
|
||
|
||
it('shows error when create store fails', async () => {
|
||
mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false });
|
||
mockListStores.mockResolvedValue({ data: [], pagination: { cursor: null, hasMore: false } });
|
||
mockCreateStore.mockRejectedValue(new Error('Store already exists'));
|
||
|
||
render(<StoresPage />);
|
||
|
||
await waitFor(() => screen.getByText('Add Store'));
|
||
await userEvent.click(screen.getByText('Add Store'));
|
||
|
||
await waitFor(() => screen.getByPlaceholderText('e.g. Walgreens'));
|
||
fireEvent.change(screen.getByPlaceholderText('e.g. Walgreens'), {
|
||
target: { value: 'Walmart' },
|
||
});
|
||
fireEvent.submit(screen.getByPlaceholderText('e.g. Walgreens').closest('form')!);
|
||
|
||
await waitFor(() => expect(screen.getByText('Store already exists')).toBeInTheDocument());
|
||
});
|
||
|
||
it('opens edit form for a store', async () => {
|
||
mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false });
|
||
mockListStores.mockResolvedValue({
|
||
data: [
|
||
{
|
||
_id: 'st-1',
|
||
name: 'CVS',
|
||
tags: [],
|
||
isActive: true,
|
||
createdAt: '2026-01-01T00:00:00.000Z',
|
||
householdId: 'hh1',
|
||
createdBy: 'u-1',
|
||
},
|
||
],
|
||
pagination: { cursor: null, hasMore: false },
|
||
});
|
||
|
||
render(<StoresPage />);
|
||
|
||
await waitFor(() => screen.getByText('CVS'));
|
||
await userEvent.click(screen.getByTitle('Edit'));
|
||
|
||
expect(screen.getByDisplayValue('CVS')).toBeInTheDocument();
|
||
expect(screen.getByText('Edit Store')).toBeInTheDocument();
|
||
});
|
||
|
||
it('saves edited store', async () => {
|
||
mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false });
|
||
mockListStores.mockResolvedValue({
|
||
data: [
|
||
{
|
||
_id: 'st-1',
|
||
name: 'CVS',
|
||
tags: [],
|
||
isActive: true,
|
||
createdAt: '2026-01-01T00:00:00.000Z',
|
||
householdId: 'hh1',
|
||
createdBy: 'u-1',
|
||
},
|
||
],
|
||
pagination: { cursor: null, hasMore: false },
|
||
});
|
||
mockUpdateStore.mockResolvedValue({});
|
||
|
||
render(<StoresPage />);
|
||
|
||
await waitFor(() => screen.getByText('CVS'));
|
||
await userEvent.click(screen.getByTitle('Edit'));
|
||
|
||
await waitFor(() => screen.getByDisplayValue('CVS'));
|
||
fireEvent.change(screen.getByDisplayValue('CVS'), { target: { value: 'CVS Pharmacy' } });
|
||
fireEvent.submit(screen.getByDisplayValue('CVS Pharmacy').closest('form')!);
|
||
|
||
await waitFor(() =>
|
||
expect(mockUpdateStore).toHaveBeenCalledWith(
|
||
'hh1',
|
||
'st-1',
|
||
expect.objectContaining({ name: 'CVS Pharmacy' }),
|
||
),
|
||
);
|
||
});
|
||
|
||
it('dismisses error', async () => {
|
||
mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false });
|
||
mockListStores.mockRejectedValue(new Error('Network error'));
|
||
|
||
render(<StoresPage />);
|
||
|
||
await waitFor(() => screen.getByText('Network error'));
|
||
await userEvent.click(screen.getByText('Dismiss'));
|
||
|
||
expect(screen.queryByText('Network error')).not.toBeInTheDocument();
|
||
});
|
||
|
||
it('shows error when deactivate fails', async () => {
|
||
mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false });
|
||
mockListStores.mockResolvedValue({
|
||
data: [
|
||
{
|
||
_id: 'st-1',
|
||
name: 'CVS',
|
||
tags: [],
|
||
isActive: true,
|
||
createdAt: '2026-01-01T00:00:00.000Z',
|
||
householdId: 'hh1',
|
||
createdBy: 'u-1',
|
||
},
|
||
],
|
||
pagination: { cursor: null, hasMore: false },
|
||
});
|
||
mockDeactivateStore.mockRejectedValue(new Error('Deactivate failed'));
|
||
vi.spyOn(window, 'confirm').mockReturnValue(true);
|
||
|
||
render(<StoresPage />);
|
||
|
||
await waitFor(() => screen.getByTitle('Deactivate'));
|
||
await userEvent.click(screen.getByTitle('Deactivate'));
|
||
|
||
await waitFor(() => expect(screen.getByText('Deactivate failed')).toBeInTheDocument());
|
||
});
|
||
|
||
it('cancels the create store form', async () => {
|
||
mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false });
|
||
mockListStores.mockResolvedValue({ data: [], pagination: { cursor: null, hasMore: false } });
|
||
|
||
render(<StoresPage />);
|
||
|
||
await waitFor(() => screen.getByText('Add Store'));
|
||
await userEvent.click(screen.getByText('Add Store'));
|
||
|
||
await waitFor(() => screen.getByPlaceholderText('e.g. Walgreens'));
|
||
await userEvent.click(
|
||
screen.getAllByRole('button', { name: 'Cancel' })[
|
||
screen.getAllByRole('button', { name: 'Cancel' }).length - 1
|
||
]!,
|
||
);
|
||
|
||
expect(screen.queryByPlaceholderText('e.g. Walgreens')).not.toBeInTheDocument();
|
||
});
|
||
|
||
it('cancels the edit store form', async () => {
|
||
mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false });
|
||
mockListStores.mockResolvedValue({
|
||
data: [
|
||
{
|
||
_id: 'st-1',
|
||
name: 'CVS',
|
||
tags: [],
|
||
isActive: true,
|
||
createdAt: '2026-01-01T00:00:00.000Z',
|
||
householdId: 'hh1',
|
||
createdBy: 'u-1',
|
||
},
|
||
],
|
||
pagination: { cursor: null, hasMore: false },
|
||
});
|
||
|
||
render(<StoresPage />);
|
||
|
||
await waitFor(() => screen.getByText('CVS'));
|
||
await userEvent.click(screen.getByTitle('Edit'));
|
||
|
||
await waitFor(() => screen.getByDisplayValue('CVS'));
|
||
await userEvent.click(screen.getByRole('button', { name: 'Cancel' }));
|
||
|
||
expect(screen.queryByDisplayValue('CVS')).not.toBeInTheDocument();
|
||
});
|
||
|
||
it('filters stores by tag', async () => {
|
||
mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false });
|
||
mockListStores.mockResolvedValue({
|
||
data: [
|
||
{
|
||
_id: 'st-1',
|
||
name: 'Walgreens',
|
||
tags: ['pharmacy'],
|
||
isActive: true,
|
||
createdAt: '2026-01-01T00:00:00.000Z',
|
||
householdId: 'hh1',
|
||
createdBy: 'u-1',
|
||
},
|
||
{
|
||
_id: 'st-2',
|
||
name: 'Costco',
|
||
tags: ['supermarket'],
|
||
isActive: true,
|
||
createdAt: '2026-01-01T00:00:00.000Z',
|
||
householdId: 'hh1',
|
||
createdBy: 'u-1',
|
||
},
|
||
],
|
||
pagination: { cursor: null, hasMore: false },
|
||
});
|
||
|
||
render(<StoresPage />);
|
||
|
||
await waitFor(() => screen.getByText('Walgreens'));
|
||
expect(screen.getByText('Costco')).toBeInTheDocument();
|
||
|
||
fireEvent.change(screen.getByDisplayValue('All tags'), { target: { value: 'pharmacy' } });
|
||
|
||
await waitFor(() => expect(screen.queryByText('Costco')).not.toBeInTheDocument());
|
||
expect(screen.getByText('Walgreens')).toBeInTheDocument();
|
||
});
|
||
|
||
it('changes store form fields (notes, address, url)', async () => {
|
||
mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false });
|
||
mockListStores.mockResolvedValue({ data: [], pagination: { cursor: null, hasMore: false } });
|
||
|
||
render(<StoresPage />);
|
||
|
||
await waitFor(() => screen.getByText('Add Store'));
|
||
await userEvent.click(screen.getByText('Add Store'));
|
||
|
||
await waitFor(() => screen.getByPlaceholderText('e.g. Walgreens'));
|
||
fireEvent.change(screen.getByPlaceholderText('123 Main St'), {
|
||
target: { value: '456 Oak Ave' },
|
||
});
|
||
fireEvent.change(screen.getByPlaceholderText('https://walgreens.com'), {
|
||
target: { value: 'https://newurl.com' },
|
||
});
|
||
fireEvent.change(screen.getByPlaceholderText('Any notes'), {
|
||
target: { value: 'Good prices' },
|
||
});
|
||
|
||
expect(screen.getByPlaceholderText('e.g. Walgreens')).toBeInTheDocument();
|
||
});
|
||
|
||
it('toggles a preset tag and adds a custom tag', async () => {
|
||
mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false });
|
||
mockListStores.mockResolvedValue({ data: [], pagination: { cursor: null, hasMore: false } });
|
||
|
||
render(<StoresPage />);
|
||
|
||
await waitFor(() => screen.getByText('Add Store'));
|
||
await userEvent.click(screen.getByText('Add Store'));
|
||
|
||
await waitFor(() => screen.getByPlaceholderText('Custom tag...'));
|
||
|
||
// Toggle a preset tag (e.g. 'pharmacy')
|
||
await userEvent.click(screen.getByRole('button', { name: 'pharmacy' }));
|
||
// Toggle it off again
|
||
await userEvent.click(screen.getByRole('button', { name: 'pharmacy' }));
|
||
|
||
// Add a custom tag via button
|
||
fireEvent.change(screen.getByPlaceholderText('Custom tag...'), { target: { value: 'mytag' } });
|
||
await userEvent.click(screen.getByRole('button', { name: 'Add' }));
|
||
|
||
await waitFor(() => expect(screen.getByText('mytag')).toBeInTheDocument());
|
||
|
||
// Remove it via the × button
|
||
await userEvent.click(screen.getByText('×'));
|
||
expect(screen.queryByText('mytag')).not.toBeInTheDocument();
|
||
});
|
||
|
||
it('adds custom tag via Enter key', async () => {
|
||
mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false });
|
||
mockListStores.mockResolvedValue({ data: [], pagination: { cursor: null, hasMore: false } });
|
||
|
||
render(<StoresPage />);
|
||
|
||
await waitFor(() => screen.getByText('Add Store'));
|
||
await userEvent.click(screen.getByText('Add Store'));
|
||
|
||
await waitFor(() => screen.getByPlaceholderText('Custom tag...'));
|
||
fireEvent.change(screen.getByPlaceholderText('Custom tag...'), { target: { value: 'keytag' } });
|
||
fireEvent.keyDown(screen.getByPlaceholderText('Custom tag...'), { key: 'Enter' });
|
||
|
||
await waitFor(() => expect(screen.getByText('keytag')).toBeInTheDocument());
|
||
});
|
||
|
||
it('ignores custom tag via non-Enter keydown', async () => {
|
||
mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false });
|
||
mockListStores.mockResolvedValue({ data: [], pagination: { cursor: null, hasMore: false } });
|
||
|
||
render(<StoresPage />);
|
||
|
||
await waitFor(() => screen.getByText('Add Store'));
|
||
await userEvent.click(screen.getByText('Add Store'));
|
||
|
||
await waitFor(() => screen.getByPlaceholderText('Custom tag...'));
|
||
fireEvent.change(screen.getByPlaceholderText('Custom tag...'), { target: { value: 'escapetag' } });
|
||
fireEvent.keyDown(screen.getByPlaceholderText('Custom tag...'), { key: 'Escape' });
|
||
|
||
expect(screen.queryByText('escapetag')).not.toBeInTheDocument();
|
||
});
|
||
|
||
it('renders store card with missing optional parameters correctly', async () => {
|
||
mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false });
|
||
mockListStores.mockResolvedValue({
|
||
data: [
|
||
{
|
||
_id: 'st-addr',
|
||
name: 'Address Only Store',
|
||
tags: [],
|
||
isActive: true,
|
||
createdAt: '2026-01-01T00:00:00.000Z',
|
||
householdId: 'hh1',
|
||
createdBy: 'u-1',
|
||
address: '123 Unique Way',
|
||
url: undefined,
|
||
notes: undefined
|
||
},
|
||
{
|
||
_id: 'st-url',
|
||
name: 'URL Only Store',
|
||
tags: [],
|
||
isActive: true,
|
||
createdAt: '2026-01-01T00:00:00.000Z',
|
||
householdId: 'hh1',
|
||
createdBy: 'u-1',
|
||
address: undefined,
|
||
url: 'https://onlyurl.com',
|
||
notes: undefined
|
||
},
|
||
{
|
||
_id: 'st-notes',
|
||
name: 'Notes Only Store',
|
||
tags: [],
|
||
isActive: true,
|
||
createdAt: '2026-01-01T00:00:00.000Z',
|
||
householdId: 'hh1',
|
||
createdBy: 'u-1',
|
||
address: undefined,
|
||
url: undefined,
|
||
notes: 'Special instruction notes'
|
||
}
|
||
],
|
||
pagination: { cursor: null, hasMore: false }
|
||
});
|
||
|
||
render(<StoresPage />);
|
||
await waitFor(() => expect(screen.getByText('Address Only Store')).toBeInTheDocument());
|
||
expect(screen.getByText('123 Unique Way')).toBeInTheDocument();
|
||
expect(screen.getByText('https://onlyurl.com')).toBeInTheDocument();
|
||
expect(screen.getByText('Special instruction notes')).toBeInTheDocument();
|
||
});
|
||
|
||
it('changes search filter', async () => {
|
||
mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false });
|
||
mockListStores.mockResolvedValue({
|
||
data: [
|
||
{
|
||
_id: 'st-1',
|
||
name: 'Walgreens',
|
||
tags: [],
|
||
isActive: true,
|
||
createdAt: '2026-01-01T00:00:00.000Z',
|
||
householdId: 'hh1',
|
||
createdBy: 'u-1',
|
||
},
|
||
{
|
||
_id: 'st-2',
|
||
name: 'CVS',
|
||
tags: [],
|
||
isActive: true,
|
||
createdAt: '2026-01-01T00:00:00.000Z',
|
||
householdId: 'hh1',
|
||
createdBy: 'u-1',
|
||
},
|
||
],
|
||
pagination: { cursor: null, hasMore: false },
|
||
});
|
||
|
||
render(<StoresPage />);
|
||
|
||
await waitFor(() => screen.getByText('Walgreens'));
|
||
fireEvent.change(screen.getByPlaceholderText('Search stores...'), { target: { value: 'wal' } });
|
||
|
||
await waitFor(() => expect(screen.queryByText('CVS')).not.toBeInTheDocument());
|
||
expect(screen.getByText('Walgreens')).toBeInTheDocument();
|
||
});
|
||
|
||
it('toggles isActive in edit form', async () => {
|
||
mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false });
|
||
mockListStores.mockResolvedValue({
|
||
data: [
|
||
{
|
||
_id: 'st-1',
|
||
name: 'CVS',
|
||
tags: [],
|
||
isActive: true,
|
||
createdAt: '2026-01-01T00:00:00.000Z',
|
||
householdId: 'hh1',
|
||
createdBy: 'u-1',
|
||
},
|
||
],
|
||
pagination: { cursor: null, hasMore: false },
|
||
});
|
||
mockUpdateStore.mockResolvedValue({});
|
||
|
||
render(<StoresPage />);
|
||
|
||
await waitFor(() => screen.getByText('CVS'));
|
||
await userEvent.click(screen.getByTitle('Edit'));
|
||
|
||
await waitFor(() => screen.getByLabelText('Active'));
|
||
fireEvent.click(screen.getByLabelText('Active'));
|
||
|
||
expect(screen.getByLabelText('Active')).not.toBeChecked();
|
||
});
|
||
|
||
it('shows inactive stores when toggle checked', async () => {
|
||
mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false });
|
||
mockListStores.mockResolvedValue({
|
||
data: [
|
||
{
|
||
_id: 'st-1',
|
||
name: 'Active Store',
|
||
tags: [],
|
||
isActive: true,
|
||
createdAt: '2026-01-01T00:00:00.000Z',
|
||
householdId: 'hh1',
|
||
createdBy: 'u-1',
|
||
},
|
||
{
|
||
_id: 'st-2',
|
||
name: 'Old Store',
|
||
tags: [],
|
||
isActive: false,
|
||
createdAt: '2026-01-01T00:00:00.000Z',
|
||
householdId: 'hh1',
|
||
createdBy: 'u-1',
|
||
},
|
||
],
|
||
pagination: { cursor: null, hasMore: false },
|
||
});
|
||
|
||
render(<StoresPage />);
|
||
|
||
await waitFor(() => screen.getByText('Active Store'));
|
||
expect(screen.queryByText('Old Store')).not.toBeInTheDocument();
|
||
|
||
fireEvent.click(screen.getByLabelText('Show inactive'));
|
||
|
||
expect(screen.getByText('Old Store')).toBeInTheDocument();
|
||
});
|
||
|
||
it('handles create store generic error fallback', async () => {
|
||
mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false });
|
||
mockListStores.mockResolvedValue({ data: [], pagination: { cursor: null, hasMore: false } });
|
||
mockCreateStore.mockRejectedValue('Generic store save error');
|
||
|
||
render(<StoresPage />);
|
||
await waitFor(() => screen.getByText('Add Store'));
|
||
await userEvent.click(screen.getByText('Add Store'));
|
||
|
||
await waitFor(() => screen.getByPlaceholderText('e.g. Walgreens'));
|
||
fireEvent.change(screen.getByPlaceholderText('e.g. Walgreens'), { target: { value: 'Walmart' } });
|
||
fireEvent.submit(screen.getByPlaceholderText('e.g. Walgreens').closest('form')!);
|
||
|
||
await waitFor(() => expect(screen.getByText('Failed to save store')).toBeInTheDocument());
|
||
});
|
||
|
||
it('handles list stores generic error fallback', async () => {
|
||
mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false });
|
||
mockListStores.mockRejectedValue('Generic list error');
|
||
|
||
render(<StoresPage />);
|
||
await waitFor(() => expect(screen.getByText('Failed to load stores')).toBeInTheDocument());
|
||
});
|
||
|
||
it('handles deactivate store generic error fallback', async () => {
|
||
mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false });
|
||
mockListStores.mockResolvedValue({
|
||
data: [
|
||
{ _id: 'st-1', name: 'CVS', tags: [], isActive: true, createdAt: '2026-01-01T00:00:00.000Z', householdId: 'hh1', createdBy: 'u-1' }
|
||
],
|
||
pagination: { cursor: null, hasMore: false },
|
||
});
|
||
mockDeactivateStore.mockRejectedValue('Generic deactivate error');
|
||
vi.spyOn(window, 'confirm').mockReturnValue(true);
|
||
|
||
render(<StoresPage />);
|
||
await waitFor(() => screen.getByTitle('Deactivate'));
|
||
await userEvent.click(screen.getByTitle('Deactivate'));
|
||
|
||
await waitFor(() => expect(screen.getByText('Failed to deactivate store')).toBeInTheDocument());
|
||
});
|
||
|
||
it('skips deactivation if confirmation is rejected', async () => {
|
||
mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false });
|
||
mockListStores.mockResolvedValue({
|
||
data: [
|
||
{ _id: 'st-1', name: 'CVS', tags: [], isActive: true, createdAt: '2026-01-01T00:00:00.000Z', householdId: 'hh1', createdBy: 'u-1' }
|
||
],
|
||
pagination: { cursor: null, hasMore: false },
|
||
});
|
||
vi.spyOn(window, 'confirm').mockReturnValue(false);
|
||
|
||
render(<StoresPage />);
|
||
await waitFor(() => screen.getByTitle('Deactivate'));
|
||
await userEvent.click(screen.getByTitle('Deactivate'));
|
||
|
||
expect(mockDeactivateStore).not.toHaveBeenCalled();
|
||
});
|
||
|
||
it('handles empty optional fields creation gracefully', async () => {
|
||
mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false });
|
||
mockListStores.mockResolvedValue({ data: [], pagination: { cursor: null, hasMore: false } });
|
||
mockCreateStore.mockResolvedValue({} as any);
|
||
|
||
render(<StoresPage />);
|
||
await waitFor(() => screen.getByText('Add Store'));
|
||
await userEvent.click(screen.getByText('Add Store'));
|
||
|
||
await waitFor(() => screen.getByPlaceholderText('e.g. Walgreens'));
|
||
fireEvent.change(screen.getByPlaceholderText('e.g. Walgreens'), { target: { value: 'Walmart' } });
|
||
|
||
fireEvent.change(screen.getByPlaceholderText('123 Main St'), { target: { value: ' ' } });
|
||
fireEvent.change(screen.getByPlaceholderText('https://walgreens.com'), { target: { value: ' ' } });
|
||
fireEvent.change(screen.getByPlaceholderText('Any notes'), { target: { value: ' ' } });
|
||
|
||
fireEvent.submit(screen.getByPlaceholderText('e.g. Walgreens').closest('form')!);
|
||
|
||
await waitFor(() => {
|
||
expect(mockCreateStore).toHaveBeenCalledWith('hh1', {
|
||
name: 'Walmart',
|
||
address: undefined,
|
||
url: undefined,
|
||
notes: undefined,
|
||
tags: [],
|
||
isActive: true
|
||
});
|
||
});
|
||
});
|
||
|
||
it('renders empty search message when filters are applied with no match', async () => {
|
||
mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false });
|
||
mockListStores.mockResolvedValue({ data: [], pagination: { cursor: null, hasMore: false } });
|
||
|
||
render(<StoresPage />);
|
||
await waitFor(() => expect(screen.getByText(/No stores yet/)).toBeInTheDocument());
|
||
|
||
fireEvent.change(screen.getByPlaceholderText('Search stores...'), { target: { value: 'MissingName' } });
|
||
|
||
await waitFor(() => {
|
||
expect(screen.getByText('No stores match your filters.')).toBeInTheDocument();
|
||
});
|
||
});
|
||
|
||
it('prevents adding duplicate custom tags', async () => {
|
||
mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false });
|
||
mockListStores.mockResolvedValue({ data: [], pagination: { cursor: null, hasMore: false } });
|
||
|
||
render(<StoresPage />);
|
||
|
||
await waitFor(() => screen.getByText('Add Store'));
|
||
await userEvent.click(screen.getByText('Add Store'));
|
||
|
||
await waitFor(() => screen.getByPlaceholderText('Custom tag...'));
|
||
const input = screen.getByPlaceholderText('Custom tag...');
|
||
|
||
// Add tag once
|
||
fireEvent.change(input, { target: { value: 'duplicated' } });
|
||
fireEvent.keyDown(input, { key: 'Enter' });
|
||
await waitFor(() => expect(screen.getByText('duplicated')).toBeInTheDocument());
|
||
|
||
// Attempt to add it again
|
||
fireEvent.change(input, { target: { value: 'duplicated' } });
|
||
fireEvent.keyDown(input, { key: 'Enter' });
|
||
|
||
// Should only exist once in the document
|
||
expect(screen.getAllByText('duplicated')).toHaveLength(1);
|
||
});
|
||
});
|