mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-19 05:26:47 +00:00
4d4ef17de8
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.
184 lines
7.8 KiB
TypeScript
184 lines
7.8 KiB
TypeScript
import { act, renderHook, waitFor } from '@testing-library/react';
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
|
|
import { packingApi } from '../../api/client';
|
|
import { _resetNetworkMode, setForcedOffline } from '../../sync/networkMode';
|
|
import type { PackingBag } from '../../types';
|
|
import { BAG_COLORS } from './packingListPanel.constants';
|
|
import { useBagTotalsPing } from './useBagTotalsPing';
|
|
import { usePackingBags } from './usePackingBags';
|
|
|
|
vi.mock('./useBagTotalsPing', () => ({ useBagTotalsPing: vi.fn() }));
|
|
|
|
// FE-PACK-BAGS-001 to FE-PACK-BAGS-010
|
|
|
|
const t = (key: string) => key;
|
|
const bag = (over: Partial<PackingBag>): PackingBag => ({
|
|
id: 1,
|
|
trip_id: 7,
|
|
name: 'Backpack',
|
|
color: BAG_COLORS[0],
|
|
sort_order: 0,
|
|
...over,
|
|
});
|
|
const BACKPACK = bag({ id: 1 });
|
|
const DUFFEL = bag({ id: 2, name: 'Duffel', color: BAG_COLORS[1] });
|
|
|
|
function setup(bagTrackingEnabled = true) {
|
|
const toast = { error: vi.fn() };
|
|
const view = renderHook(
|
|
(props: { enabled: boolean }) => usePackingBags({ tripId: 7, bagTrackingEnabled: props.enabled, t, toast }),
|
|
{
|
|
initialProps: { enabled: bagTrackingEnabled },
|
|
}
|
|
);
|
|
return { ...view, toast };
|
|
}
|
|
|
|
beforeEach(() => {
|
|
_resetNetworkMode();
|
|
vi.mocked(useBagTotalsPing).mockClear();
|
|
});
|
|
|
|
afterEach(() => {
|
|
setForcedOffline(false);
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
describe('usePackingBags', () => {
|
|
it('FE-PACK-BAGS-001: loads the bags and the weight in no bag while bag tracking is on', async () => {
|
|
const list = vi.spyOn(packingApi, 'listBags').mockResolvedValue({ bags: [BACKPACK], unassigned_weight_grams: 450 });
|
|
const { result } = setup();
|
|
await waitFor(() => expect(result.current.bags).toEqual([BACKPACK]));
|
|
expect(list).toHaveBeenCalledWith(7);
|
|
expect(result.current.unassignedWeightGrams).toBe(450);
|
|
});
|
|
|
|
it('FE-PACK-BAGS-002: reads nothing while bag tracking is off, and starts once it is on', async () => {
|
|
const list = vi.spyOn(packingApi, 'listBags').mockResolvedValue({ bags: [BACKPACK] });
|
|
const { result, rerender } = setup(false);
|
|
await act(async () => {});
|
|
expect(list).not.toHaveBeenCalled();
|
|
expect(result.current.bags).toEqual([]);
|
|
expect(result.current.unassignedWeightGrams).toBeNull();
|
|
rerender({ enabled: true });
|
|
await waitFor(() => expect(result.current.bags).toEqual([BACKPACK]));
|
|
// A response without the server sum leaves it unknown.
|
|
expect(result.current.unassignedWeightGrams).toBeNull();
|
|
});
|
|
|
|
it('FE-PACK-BAGS-003: a failed read keeps what was there and says nothing', async () => {
|
|
vi.spyOn(packingApi, 'listBags').mockRejectedValue(new Error('offline'));
|
|
const { result, toast } = setup();
|
|
await act(async () => {});
|
|
expect(result.current.bags).toEqual([]);
|
|
expect(toast.error).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('FE-PACK-BAGS-004: re-reads the totals on the room ping, and the server totals count only online', async () => {
|
|
const list = vi.spyOn(packingApi, 'listBags').mockResolvedValue({ bags: [] });
|
|
const { result } = setup();
|
|
await waitFor(() => expect(list).toHaveBeenCalledTimes(1));
|
|
expect(useBagTotalsPing).toHaveBeenLastCalledWith(true, result.current.reloadBags);
|
|
expect(result.current.serverWeightsFresh).toBe(true);
|
|
act(() => setForcedOffline(true));
|
|
expect(result.current.serverWeightsFresh).toBe(false);
|
|
});
|
|
|
|
it('FE-PACK-BAGS-005: a new bag takes the next colour and joins the list', async () => {
|
|
vi.spyOn(packingApi, 'listBags').mockResolvedValue({ bags: [BACKPACK] });
|
|
const create = vi.spyOn(packingApi, 'createBag').mockResolvedValue({ bag: DUFFEL });
|
|
const { result } = setup();
|
|
await waitFor(() => expect(result.current.bags).toHaveLength(1));
|
|
let created: PackingBag | undefined;
|
|
await act(async () => {
|
|
created = await result.current.createBag('Duffel');
|
|
});
|
|
expect(create).toHaveBeenCalledWith(7, { name: 'Duffel', color: BAG_COLORS[1] });
|
|
expect(created).toEqual(DUFFEL);
|
|
expect(result.current.bags).toEqual([BACKPACK, DUFFEL]);
|
|
});
|
|
|
|
it('FE-PACK-BAGS-006: a failed new bag is reported and comes back empty', async () => {
|
|
vi.spyOn(packingApi, 'listBags').mockResolvedValue({ bags: [] });
|
|
vi.spyOn(packingApi, 'createBag').mockRejectedValue(new Error('x'));
|
|
const { result, toast } = setup();
|
|
let created: PackingBag | undefined = BACKPACK;
|
|
await act(async () => {
|
|
created = await result.current.createBag('Duffel');
|
|
});
|
|
expect(created).toBeUndefined();
|
|
expect(toast.error).toHaveBeenCalledWith('packing.toast.saveError');
|
|
expect(result.current.bags).toEqual([]);
|
|
});
|
|
|
|
it('FE-PACK-BAGS-007: an edit merges what the server returns into that bag only', async () => {
|
|
vi.spyOn(packingApi, 'listBags').mockResolvedValue({ bags: [BACKPACK, DUFFEL] });
|
|
const update = vi
|
|
.spyOn(packingApi, 'updateBag')
|
|
.mockResolvedValue({ bag: { ...BACKPACK, name: 'Daypack', weight_limit_grams: 7000 } });
|
|
const { result } = setup();
|
|
await waitFor(() => expect(result.current.bags).toHaveLength(2));
|
|
await act(async () => {
|
|
await result.current.updateBag(1, { name: 'Daypack', weight_limit_grams: 7000 });
|
|
});
|
|
expect(update).toHaveBeenCalledWith(7, 1, { name: 'Daypack', weight_limit_grams: 7000 });
|
|
expect(result.current.bags).toEqual([{ ...BACKPACK, name: 'Daypack', weight_limit_grams: 7000 }, DUFFEL]);
|
|
});
|
|
|
|
it('FE-PACK-BAGS-008: deleting and setting members change the list, and each failure has its message', async () => {
|
|
vi.spyOn(packingApi, 'listBags').mockResolvedValue({ bags: [BACKPACK, DUFFEL] });
|
|
vi.spyOn(packingApi, 'setBagMembers').mockResolvedValue({ members: [{ user_id: 3, username: 'ada' }] });
|
|
vi.spyOn(packingApi, 'deleteBag').mockResolvedValue({ ok: true });
|
|
const { result } = setup();
|
|
await waitFor(() => expect(result.current.bags).toHaveLength(2));
|
|
await act(async () => {
|
|
await result.current.setBagMembers(2, [3]);
|
|
});
|
|
expect(result.current.bags[1].members).toEqual([{ user_id: 3, username: 'ada' }]);
|
|
await act(async () => {
|
|
await result.current.deleteBag(1);
|
|
});
|
|
expect(result.current.bags.map((b) => b.id)).toEqual([2]);
|
|
});
|
|
|
|
it('FE-PACK-BAGS-009: failed edits, deletes and member changes leave the bags and say why', async () => {
|
|
vi.spyOn(packingApi, 'listBags').mockResolvedValue({ bags: [BACKPACK] });
|
|
vi.spyOn(packingApi, 'updateBag').mockRejectedValue(new Error('x'));
|
|
vi.spyOn(packingApi, 'deleteBag').mockRejectedValue(new Error('x'));
|
|
vi.spyOn(packingApi, 'setBagMembers').mockRejectedValue(new Error('x'));
|
|
const { result, toast } = setup();
|
|
await waitFor(() => expect(result.current.bags).toHaveLength(1));
|
|
await act(async () => {
|
|
await result.current.updateBag(1, { name: 'Daypack' });
|
|
await result.current.deleteBag(1);
|
|
await result.current.setBagMembers(1, [3]);
|
|
});
|
|
expect(toast.error.mock.calls.map((c) => c[0])).toEqual([
|
|
'common.error',
|
|
'packing.toast.deleteError',
|
|
'common.error',
|
|
]);
|
|
expect(result.current.bags).toEqual([BACKPACK]);
|
|
});
|
|
|
|
it('FE-PACK-BAGS-010: tryCreateBag tells a write that went through from a failed one', async () => {
|
|
vi.spyOn(packingApi, 'listBags').mockResolvedValue({ bags: [] });
|
|
const create = vi.spyOn(packingApi, 'createBag').mockResolvedValueOnce({}).mockRejectedValueOnce(new Error('x'));
|
|
const { result, toast } = setup();
|
|
let first: unknown = 'unset';
|
|
let second: unknown = 'unset';
|
|
await act(async () => {
|
|
first = await result.current.tryCreateBag('Duffel');
|
|
});
|
|
expect(first).toEqual({ bag: undefined });
|
|
expect(toast.error).not.toHaveBeenCalled();
|
|
await act(async () => {
|
|
second = await result.current.tryCreateBag('Tote');
|
|
});
|
|
expect(second).toBeNull();
|
|
expect(toast.error).toHaveBeenCalledWith('packing.toast.saveError');
|
|
expect(create).toHaveBeenCalledTimes(2);
|
|
});
|
|
});
|