Files
lonc/tests/features/dashboard/dashboard-page.test.js
T
bblaz 6ca09cdf1f
ci/woodpecker/push/woodpecker Pipeline was successful
Add inactive item fallback for dashboard change feed lookups
2026-04-12 22:46:28 +02:00

194 lines
5.9 KiB
JavaScript

import { beforeEach, describe, expect, it, vi } from 'vitest';
const listKitchenChangesMock = vi.fn();
const getStockEntryMock = vi.fn();
const fetchLocationsMock = vi.fn();
vi.mock('../../../src/api/stock.js', () => ({
listKitchenChanges: (...args) => listKitchenChangesMock(...args),
getStockEntry: (...args) => getStockEntryMock(...args),
}));
vi.mock('../../../src/api/locations.js', () => ({
fetchLocations: (...args) => fetchLocationsMock(...args),
}));
const { dashboardPageData, renderDashboardPage } = await import('../../../src/features/dashboard/dashboard-page.js');
describe('features/dashboard/dashboard-page', () => {
beforeEach(() => {
listKitchenChangesMock.mockReset();
getStockEntryMock.mockReset();
fetchLocationsMock.mockReset();
});
it('renders dashboard with recent changes section', () => {
const html = renderDashboardPage();
expect(html).toContain('Recent changes');
expect(html).toContain('x-data="dashboardPage()"');
expect(html).toContain('Saved means the backend created or updated a record.');
});
it('loads recent changes on init and renders item-focused state lines', async () => {
listKitchenChangesMock.mockResolvedValueOnce({
since: null,
nextCursor: null,
changes: [{
type: 'item',
action: 'upsert',
timestamp: '2026-04-10T10:00:00Z',
item: {
uuid_b64: 'u1',
name: 'Rice',
stock_type: 'measured',
quantity: 3,
uom_symbol: 'kg',
level: 'good',
expire_date: '2026-04-21',
location_initial_uuid_b64: 'loc1',
},
}],
});
fetchLocationsMock.mockResolvedValueOnce({
flat: [{ uuid_b64: 'loc1', pathLabel: 'Pantry / Shelf A' }],
});
const store = {
isConnected: true,
setActiveKitchen: vi.fn(),
addAlert: vi.fn(),
};
const data = dashboardPageData(store);
await data.init();
expect(listKitchenChangesMock).toHaveBeenCalledWith(store, { limit: 10 });
expect(data.recentChanges).toHaveLength(1);
expect(data.changesState.error).toBe('');
expect(data.changeHeadline(data.recentChanges[0])).toBe('Item saved: Rice');
expect(data.changeStateLine(data.recentChanges[0])).toContain('Quantity: 3 kg');
expect(data.changeStateLine(data.recentChanges[0])).toContain('Location: Pantry / Shelf A');
expect(getStockEntryMock).not.toHaveBeenCalled();
});
it('resolves stock event item context via item lookup when needed', async () => {
listKitchenChangesMock.mockResolvedValueOnce({
since: null,
nextCursor: null,
changes: [{
type: 'stock',
action: 'upsert',
timestamp: '2026-04-10T10:00:00Z',
stock: {
item_uuid_b64: 'item-uuid-1',
quantity: 0.5,
uom_symbol: 'kg',
level: 'some',
location_uuid_b64: 'loc2',
},
}],
});
getStockEntryMock.mockResolvedValueOnce({
uuid_b64: 'item-uuid-1',
name: 'Flour',
stock_type: 'measured',
expire_date: '2026-05-02',
});
fetchLocationsMock.mockResolvedValueOnce({
flat: [{ uuid_b64: 'loc2', pathLabel: 'Pantry / Bin 2' }],
});
const data = dashboardPageData({
isConnected: true,
setActiveKitchen: vi.fn(),
addAlert: vi.fn(),
});
await data.refreshChanges();
expect(data.changeHeadline(data.recentChanges[0])).toBe('Stock saved: Flour');
expect(data.changeStateLine(data.recentChanges[0])).toContain('Quantity: 0.5 kg');
expect(data.changeStateLine(data.recentChanges[0])).toContain('Level: Some');
expect(data.changeStateLine(data.recentChanges[0])).toContain('Location: Pantry / Bin 2');
expect(getStockEntryMock).toHaveBeenCalledWith(expect.anything(), 'item-uuid-1');
});
it('retries stock event item lookup with allowInactive after 404', async () => {
listKitchenChangesMock.mockResolvedValueOnce({
since: null,
nextCursor: null,
changes: [{
type: 'stock',
action: 'upsert',
timestamp: '2026-04-10T10:00:00Z',
stock: {
item_uuid_b64: 'item-uuid-2',
quantity: 1,
uom_symbol: 'pcs',
},
}],
});
getStockEntryMock
.mockRejectedValueOnce(Object.assign(new Error('Not found'), { status: 404 }))
.mockResolvedValueOnce({
uuid_b64: 'item-uuid-2',
name: 'Archived pasta',
stock_type: 'measured',
});
fetchLocationsMock.mockResolvedValueOnce({ flat: [] });
const store = {
isConnected: true,
setActiveKitchen: vi.fn(),
addAlert: vi.fn(),
};
const data = dashboardPageData(store);
await data.refreshChanges();
expect(getStockEntryMock).toHaveBeenNthCalledWith(1, store, 'item-uuid-2');
expect(getStockEntryMock).toHaveBeenNthCalledWith(
2,
store,
'item-uuid-2',
{ allowInactive: true },
);
expect(data.changeHeadline(data.recentChanges[0])).toBe('Stock saved: Archived pasta');
expect(data.changeStateLine(data.recentChanges[0])).toContain('Quantity: 1 pcs');
});
it('keeps empty state when API returns no changes', async () => {
listKitchenChangesMock.mockResolvedValueOnce({
since: null,
nextCursor: null,
changes: [],
});
fetchLocationsMock.mockResolvedValueOnce({ flat: [] });
const data = dashboardPageData({
isConnected: true,
setActiveKitchen: vi.fn(),
addAlert: vi.fn(),
});
await data.refreshChanges();
expect(data.recentChanges).toEqual([]);
expect(data.changesState.error).toBe('');
});
it('captures refresh errors in async state', async () => {
listKitchenChangesMock.mockRejectedValueOnce(new Error('Feed unavailable'));
const data = dashboardPageData({
isConnected: true,
setActiveKitchen: vi.fn(),
addAlert: vi.fn(),
});
await data.refreshChanges();
expect(data.changesState.error).toBe('Feed unavailable');
expect(data.recentChanges).toEqual([]);
});
});