Files
TREK/client/src/components/Packing/usePackingTemplates.test.ts
T
Maurice 4d4ef17de8 refactor(packing): share the packing list logic between desktop and phone
usePackingItemActions, usePackingTemplates and usePackingImport take the
item, template and bulk import logic out of usePackingListPanel and
MPackingListTab, and the phone listsModel keeps only its priority
tokens.

When adding to a category the desktop looks for the placeholder in the
store and the phone in its rendered items. The desktop toasts on an
empty import, the phone runs one import at a time. The effects run in
the same order as before.
2026-10-09 15:19:41 +02:00

149 lines
6.5 KiB
TypeScript

import { act, renderHook, waitFor } from '@testing-library/react';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { buildPackingItem } from '../../../tests/helpers/factories';
import { resetAllStores, seedStore } from '../../../tests/helpers/store';
import { packingApi } from '../../api/client';
import { useTripStore } from '../../store/tripStore';
import type { PackingView } from './packingListModel';
import { usePackingTemplates } from './usePackingTemplates';
// FE-PACK-TEMPL-001 to FE-PACK-TEMPL-007
const t = (key: string, params?: Record<string, string | number>) =>
params ? `${key}:${Object.values(params).join(',')}` : key;
const BEACH = { id: 4, name: 'Beach', item_count: 12 };
const SKI = { id: 5, name: 'Ski', item_count: 30 };
const EXISTING = buildPackingItem({ id: 1, name: 'Tent' });
function setup(view: PackingView = 'common') {
const toast = { success: vi.fn(), error: vi.fn() };
const onApplied = vi.fn();
const onSaved = vi.fn();
const rendered = renderHook(() => usePackingTemplates({ tripId: 7, view, t, toast, onApplied, onSaved }));
return { ...rendered, toast, onApplied, onSaved };
}
beforeEach(() => {
resetAllStores();
seedStore(useTripStore, { packingItems: [EXISTING] });
});
afterEach(() => vi.restoreAllMocks());
describe('usePackingTemplates', () => {
it('FE-PACK-TEMPL-001: lists the templates on hand for the trip, and nothing when that fails', async () => {
const list = vi.spyOn(packingApi, 'listTemplates').mockResolvedValue({ templates: [BEACH] });
const { result } = setup();
await waitFor(() => expect(result.current.templates).toEqual([BEACH]));
expect(list).toHaveBeenCalledWith(7);
list.mockRejectedValue(new Error('offline'));
const failed = setup();
await act(async () => {});
expect(failed.result.current.templates).toEqual([]);
});
it('FE-PACK-TEMPL-002: applying lands the items in the list being looked at and reports how many', async () => {
vi.spyOn(packingApi, 'listTemplates').mockResolvedValue({ templates: [BEACH] });
const added = buildPackingItem({ id: 2, name: 'Towel', is_private: 1 });
let finish: () => void = () => {};
const apply = vi.spyOn(packingApi, 'applyTemplate').mockImplementation(
() =>
new Promise((resolve) => {
finish = () => resolve({ items: [added], count: 1 });
})
);
const { result, toast, onApplied } = setup('personal');
let done: Promise<void> = Promise.resolve();
act(() => {
done = result.current.applyTemplate(4);
});
expect(result.current.applyingTemplate).toBe(true);
await act(async () => {
finish();
await done;
});
expect(apply).toHaveBeenCalledWith(7, 4, 'personal');
expect(useTripStore.getState().packingItems).toEqual([EXISTING, added]);
expect(toast.success).toHaveBeenCalledWith('packing.templateApplied:1');
expect(onApplied).toHaveBeenCalledTimes(1);
expect(result.current.applyingTemplate).toBe(false);
});
it('FE-PACK-TEMPL-003: a template without items still reports success and keeps the list', async () => {
vi.spyOn(packingApi, 'listTemplates').mockResolvedValue({ templates: [] });
vi.spyOn(packingApi, 'applyTemplate').mockResolvedValue({ count: 0 });
const { result, toast } = setup();
await act(async () => {
await result.current.applyTemplate(4);
});
expect(useTripStore.getState().packingItems).toEqual([EXISTING]);
expect(toast.success).toHaveBeenCalledWith('packing.templateApplied:0');
});
it('FE-PACK-TEMPL-004: a failed apply is reported and leaves the surface open', async () => {
vi.spyOn(packingApi, 'listTemplates').mockResolvedValue({ templates: [] });
vi.spyOn(packingApi, 'applyTemplate').mockRejectedValue(new Error('x'));
const { result, toast, onApplied } = setup();
await act(async () => {
await result.current.applyTemplate(4);
});
expect(toast.error).toHaveBeenCalledWith('packing.templateError');
expect(onApplied).not.toHaveBeenCalled();
expect(result.current.applyingTemplate).toBe(false);
});
it('FE-PACK-TEMPL-005: saving needs a name, then saves it trimmed, clears it and reloads the list', async () => {
const list = vi.spyOn(packingApi, 'listTemplates').mockResolvedValue({ templates: [BEACH] });
const save = vi.spyOn(packingApi, 'saveAsTemplate').mockResolvedValue({ ok: true });
const { result, toast, onSaved } = setup();
await waitFor(() => expect(result.current.templates).toEqual([BEACH]));
act(() => result.current.setSaveTemplateName(' '));
await act(async () => {
await result.current.saveAsTemplate();
});
expect(save).not.toHaveBeenCalled();
list.mockResolvedValue({ templates: [BEACH, SKI] });
act(() => result.current.setSaveTemplateName(' Ski '));
await act(async () => {
await result.current.saveAsTemplate();
});
expect(save).toHaveBeenCalledWith(7, 'Ski');
expect(toast.success).toHaveBeenCalledWith('packing.templateSaved');
expect(onSaved).toHaveBeenCalledTimes(1);
expect(result.current.saveTemplateName).toBe('');
await waitFor(() => expect(result.current.templates).toEqual([BEACH, SKI]));
});
it('FE-PACK-TEMPL-006: a failed save is reported and keeps the name typed', async () => {
vi.spyOn(packingApi, 'listTemplates').mockResolvedValue({ templates: [] });
vi.spyOn(packingApi, 'saveAsTemplate').mockRejectedValue(new Error('x'));
const { result, toast, onSaved } = setup();
act(() => result.current.setSaveTemplateName('Ski'));
await act(async () => {
await result.current.saveAsTemplate();
});
expect(toast.error).toHaveBeenCalledWith('common.error');
expect(onSaved).not.toHaveBeenCalled();
expect(result.current.saveTemplateName).toBe('Ski');
});
it('FE-PACK-TEMPL-007: a list that fails to reload after a save keeps the templates it had', async () => {
const list = vi.spyOn(packingApi, 'listTemplates').mockResolvedValue({ templates: [BEACH] });
vi.spyOn(packingApi, 'saveAsTemplate').mockResolvedValue({ ok: true });
const { result, toast } = setup();
await waitFor(() => expect(result.current.templates).toEqual([BEACH]));
list.mockRejectedValue(new Error('offline'));
act(() => result.current.setSaveTemplateName('Ski'));
await act(async () => {
await result.current.saveAsTemplate();
});
await waitFor(() => expect(list).toHaveBeenCalledTimes(2));
expect(result.current.templates).toEqual([BEACH]);
expect(toast.error).not.toHaveBeenCalled();
});
});