Extend dashboard tests: integrate listStockEvents mock, update stock transition handling, and enhance recent changes validation
ci/woodpecker/push/woodpecker Pipeline was successful

This commit is contained in:
2026-05-06 22:40:09 +02:00
parent 55d6218dd3
commit 114e31ba58
+46 -13
View File
@@ -1,11 +1,13 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'; import { beforeEach, describe, expect, it, vi } from 'vitest';
const listKitchenChangesMock = vi.fn(); const listKitchenChangesMock = vi.fn();
const listStockEventsMock = vi.fn();
const getStockEntryMock = vi.fn(); const getStockEntryMock = vi.fn();
const fetchLocationsMock = vi.fn(); const fetchLocationsMock = vi.fn();
vi.mock('../../../src/api/stock.js', () => ({ vi.mock('../../../src/api/stock.js', () => ({
listKitchenChanges: (...args) => listKitchenChangesMock(...args), listKitchenChanges: (...args) => listKitchenChangesMock(...args),
listStockEvents: (...args) => listStockEventsMock(...args),
getStockEntry: (...args) => getStockEntryMock(...args), getStockEntry: (...args) => getStockEntryMock(...args),
})); }));
@@ -18,6 +20,7 @@ const { dashboardPageData, renderDashboardPage } = await import('../../../src/fe
describe('features/dashboard/dashboard-page', () => { describe('features/dashboard/dashboard-page', () => {
beforeEach(() => { beforeEach(() => {
listKitchenChangesMock.mockReset(); listKitchenChangesMock.mockReset();
listStockEventsMock.mockReset();
getStockEntryMock.mockReset(); getStockEntryMock.mockReset();
fetchLocationsMock.mockReset(); fetchLocationsMock.mockReset();
}); });
@@ -26,10 +29,11 @@ describe('features/dashboard/dashboard-page', () => {
const html = renderDashboardPage(); const html = renderDashboardPage();
expect(html).toContain('Recent changes'); expect(html).toContain('Recent changes');
expect(html).toContain('x-data="dashboardPage()"'); expect(html).toContain('x-data="dashboardPage()"');
expect(html).toContain('Saved means the backend created or updated a record.'); expect(html).toContain('Latest item and stock updates, including used and inactive stock.');
expect(html).toContain('recent-change-list');
}); });
it('loads recent changes on init and renders item-focused state lines', async () => { it('loads recent changes on init and renders item-focused details', async () => {
listKitchenChangesMock.mockResolvedValueOnce({ listKitchenChangesMock.mockResolvedValueOnce({
since: null, since: null,
nextCursor: null, nextCursor: null,
@@ -62,16 +66,20 @@ describe('features/dashboard/dashboard-page', () => {
await data.init(); await data.init();
expect(listKitchenChangesMock).toHaveBeenCalledWith(store, { limit: 10 }); expect(listKitchenChangesMock).toHaveBeenCalledWith(store, { limit: 200 });
expect(data.recentChanges).toHaveLength(1); expect(data.recentChanges).toHaveLength(1);
expect(data.changesState.error).toBe(''); expect(data.changesState.error).toBe('');
expect(data.changeHeadline(data.recentChanges[0])).toBe('Item saved: Rice'); expect(data.changeHeadline(data.recentChanges[0])).toBe('Rice');
expect(data.changeStateLine(data.recentChanges[0])).toContain('Quantity: 3 kg'); expect(data.changeKindLabel(data.recentChanges[0])).toBe('Updated');
expect(data.changeStateLine(data.recentChanges[0])).toContain('Location: Pantry / Shelf A'); expect(data.changeSubtitle(data.recentChanges[0])).toBe('Item details were updated.');
expect(data.changeDetails(data.recentChanges[0])).toEqual(expect.arrayContaining([
{ label: 'Quantity', value: '3 kg' },
{ label: 'Location', value: 'Pantry / Shelf A' },
]));
expect(getStockEntryMock).not.toHaveBeenCalled(); expect(getStockEntryMock).not.toHaveBeenCalled();
}); });
it('resolves stock event item context via item lookup when needed', async () => { it('resolves stock event item context and renders stock transitions', async () => {
listKitchenChangesMock.mockResolvedValueOnce({ listKitchenChangesMock.mockResolvedValueOnce({
since: null, since: null,
nextCursor: null, nextCursor: null,
@@ -80,6 +88,7 @@ describe('features/dashboard/dashboard-page', () => {
action: 'upsert', action: 'upsert',
timestamp: '2026-04-10T10:00:00Z', timestamp: '2026-04-10T10:00:00Z',
stock: { stock: {
id: 11,
item_uuid_b64: 'item-uuid-1', item_uuid_b64: 'item-uuid-1',
quantity: 0.5, quantity: 0.5,
uom_symbol: 'kg', uom_symbol: 'kg',
@@ -88,6 +97,10 @@ describe('features/dashboard/dashboard-page', () => {
}, },
}], }],
}); });
listStockEventsMock.mockResolvedValueOnce([
{ id: 11, quantity: 0.5, uom_symbol: 'kg', level: 'some' },
{ id: 10, quantity: 1, uom_symbol: 'kg', level: 'good' },
]);
getStockEntryMock.mockResolvedValueOnce({ getStockEntryMock.mockResolvedValueOnce({
uuid_b64: 'item-uuid-1', uuid_b64: 'item-uuid-1',
name: 'Flour', name: 'Flour',
@@ -106,11 +119,23 @@ describe('features/dashboard/dashboard-page', () => {
await data.refreshChanges(); await data.refreshChanges();
expect(data.changeHeadline(data.recentChanges[0])).toBe('Stock saved: Flour'); expect(data.changeHeadline(data.recentChanges[0])).toBe('Flour');
expect(data.changeStateLine(data.recentChanges[0])).toContain('Quantity: 0.5 kg'); expect(data.changeKindLabel(data.recentChanges[0])).toBe('Stock changed');
expect(data.changeStateLine(data.recentChanges[0])).toContain('Level: Some'); expect(data.changeStockTransition(data.recentChanges[0])).toEqual({
expect(data.changeStateLine(data.recentChanges[0])).toContain('Location: Pantry / Bin 2'); previous: '1 kg · Good',
current: '0.5 kg · Some',
});
expect(data.changeDetails(data.recentChanges[0])).toEqual(expect.arrayContaining([
{ label: 'Level', value: 'Some' },
{ label: 'Location', value: 'Pantry / Bin 2' },
]));
expect(getStockEntryMock).toHaveBeenCalledWith(expect.anything(), 'item-uuid-1'); expect(getStockEntryMock).toHaveBeenCalledWith(expect.anything(), 'item-uuid-1');
expect(listStockEventsMock).toHaveBeenCalledWith(expect.anything(), 'item-uuid-1', {
allowInactive: true,
limit: 50,
orderBy: 'id',
orderDir: 'desc',
});
}); });
it('retries stock event item lookup with allowInactive after 404', async () => { it('retries stock event item lookup with allowInactive after 404', async () => {
@@ -122,12 +147,16 @@ describe('features/dashboard/dashboard-page', () => {
action: 'upsert', action: 'upsert',
timestamp: '2026-04-10T10:00:00Z', timestamp: '2026-04-10T10:00:00Z',
stock: { stock: {
id: 12,
item_uuid_b64: 'item-uuid-2', item_uuid_b64: 'item-uuid-2',
quantity: 1, quantity: 1,
uom_symbol: 'pcs', uom_symbol: 'pcs',
}, },
}], }],
}); });
listStockEventsMock.mockResolvedValueOnce([
{ id: 12, quantity: 1, uom_symbol: 'pcs' },
]);
getStockEntryMock getStockEntryMock
.mockRejectedValueOnce(Object.assign(new Error('Not found'), { status: 404 })) .mockRejectedValueOnce(Object.assign(new Error('Not found'), { status: 404 }))
.mockResolvedValueOnce({ .mockResolvedValueOnce({
@@ -153,8 +182,12 @@ describe('features/dashboard/dashboard-page', () => {
'item-uuid-2', 'item-uuid-2',
{ allowInactive: true }, { allowInactive: true },
); );
expect(data.changeHeadline(data.recentChanges[0])).toBe('Stock saved: Archived pasta'); expect(data.changeHeadline(data.recentChanges[0])).toBe('Archived pasta');
expect(data.changeStateLine(data.recentChanges[0])).toContain('Quantity: 1 pcs'); expect(data.changeKindLabel(data.recentChanges[0])).toBe('New item');
expect(data.changeStockTransition(data.recentChanges[0])).toEqual({
previous: 'Initial stock',
current: '1 pcs',
});
}); });
it('keeps empty state when API returns no changes', async () => { it('keeps empty state when API returns no changes', async () => {