Align stock API with paginated backend and bump to v0.2.2
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/push/woodpecker Pipeline was successful
This commit is contained in:
+111
-18
@@ -14,14 +14,16 @@ vi.mock('../../src/api/client.js', () => ({
|
||||
}));
|
||||
|
||||
const {
|
||||
adjustStockEntry,
|
||||
applyItemUpsert,
|
||||
listGroupedStockEntries,
|
||||
listKitchenChanges,
|
||||
listStockEntries,
|
||||
lookupItemByIdentifier,
|
||||
lookupItemDetails,
|
||||
listKitchenChanges,
|
||||
patchStockItem,
|
||||
previewItemUpsert,
|
||||
updateStockItem,
|
||||
useStockItem,
|
||||
} = await import('../../src/api/stock.js');
|
||||
|
||||
@@ -30,12 +32,12 @@ describe('api/stock', () => {
|
||||
apiRequestMock.mockReset();
|
||||
});
|
||||
|
||||
it('listStockEntries forwards optional query filters', async () => {
|
||||
it('listStockEntries forwards explicit pagination query filters', async () => {
|
||||
apiRequestMock.mockResolvedValueOnce([]);
|
||||
|
||||
await listStockEntries(
|
||||
{ config: { database: 'db' } },
|
||||
{ searchName: 'Milk', limit: 20, offset: 40, cursor: 'cursor-1' },
|
||||
{ searchName: 'Milk', limit: 20, offset: 40 },
|
||||
);
|
||||
|
||||
expect(apiRequestMock).toHaveBeenCalledWith(
|
||||
@@ -46,18 +48,42 @@ describe('api/stock', () => {
|
||||
search_name: 'Milk',
|
||||
limit: 20,
|
||||
offset: 40,
|
||||
cursor: 'cursor-1',
|
||||
},
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('listGroupedStockEntries defaults to expanded=1 and forwards options', async () => {
|
||||
it('listStockEntries aggregates all pages by default', async () => {
|
||||
apiRequestMock
|
||||
.mockResolvedValueOnce(Array.from({ length: 100 }, (_, id) => ({ id: id + 1 })))
|
||||
.mockResolvedValueOnce([{ id: 101 }]);
|
||||
|
||||
const response = await listStockEntries(
|
||||
{ config: { database: 'db' } },
|
||||
{ searchName: 'Milk' },
|
||||
);
|
||||
|
||||
expect(response).toHaveLength(101);
|
||||
expect(apiRequestMock).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
{ config: { database: 'db' } },
|
||||
'kitchen/items',
|
||||
{ query: { search_name: 'Milk', limit: 100, offset: 0 } },
|
||||
);
|
||||
expect(apiRequestMock).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
{ config: { database: 'db' } },
|
||||
'kitchen/items',
|
||||
{ query: { search_name: 'Milk', limit: 100, offset: 100 } },
|
||||
);
|
||||
});
|
||||
|
||||
it('listGroupedStockEntries forwards explicit pagination options', async () => {
|
||||
apiRequestMock.mockResolvedValueOnce([]);
|
||||
|
||||
await listGroupedStockEntries(
|
||||
{ config: { database: 'db' } },
|
||||
{ expanded: 0, searchName: 'Rice', limit: 10, offset: 0, cursor: 'cursor-2' },
|
||||
{ expanded: 0, searchName: 'Rice', limit: 10, offset: 0 },
|
||||
);
|
||||
|
||||
expect(apiRequestMock).toHaveBeenCalledWith(
|
||||
@@ -69,12 +95,36 @@ describe('api/stock', () => {
|
||||
search_name: 'Rice',
|
||||
limit: 10,
|
||||
offset: 0,
|
||||
cursor: 'cursor-2',
|
||||
},
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('listGroupedStockEntries aggregates all pages by default', async () => {
|
||||
apiRequestMock
|
||||
.mockResolvedValueOnce(Array.from({ length: 100 }, (_, id) => ({ id: id + 1 })))
|
||||
.mockResolvedValueOnce([{ id: 101 }]);
|
||||
|
||||
const response = await listGroupedStockEntries(
|
||||
{ config: { database: 'db' } },
|
||||
{ expanded: 1, searchName: 'Rice' },
|
||||
);
|
||||
|
||||
expect(response).toHaveLength(101);
|
||||
expect(apiRequestMock).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
{ config: { database: 'db' } },
|
||||
'kitchen/items/grouped',
|
||||
{ query: { expanded: 1, search_name: 'Rice', limit: 100, offset: 0 } },
|
||||
);
|
||||
expect(apiRequestMock).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
{ config: { database: 'db' } },
|
||||
'kitchen/items/grouped',
|
||||
{ query: { expanded: 1, search_name: 'Rice', limit: 100, offset: 100 } },
|
||||
);
|
||||
});
|
||||
|
||||
it('listKitchenChanges returns normalized changes payload', async () => {
|
||||
apiRequestMock.mockResolvedValueOnce({
|
||||
since: 'cursor-1',
|
||||
@@ -257,6 +307,56 @@ describe('api/stock', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('updateStockItem posts stock event and re-fetches updated item', async () => {
|
||||
apiRequestMock
|
||||
.mockResolvedValueOnce({ status: 'OK', stock: { id: 1 } })
|
||||
.mockResolvedValueOnce({ uuid_b64: 'item-1', quantity: 2 });
|
||||
|
||||
const response = await updateStockItem(
|
||||
{ config: { database: 'db' } },
|
||||
'item-1',
|
||||
{ quantity: 2 },
|
||||
);
|
||||
|
||||
expect(apiRequestMock).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
{ config: { database: 'db' } },
|
||||
'kitchen/items/item-1/stock',
|
||||
{ method: 'POST', body: { quantity: 2 } },
|
||||
);
|
||||
expect(apiRequestMock).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
{ config: { database: 'db' } },
|
||||
'kitchen/items/item-1',
|
||||
);
|
||||
expect(response).toEqual({ uuid_b64: 'item-1', quantity: 2 });
|
||||
});
|
||||
|
||||
it('adjustStockEntry posts stock event and re-fetches updated item', async () => {
|
||||
apiRequestMock
|
||||
.mockResolvedValueOnce({ status: 'OK', stock: { id: 2 } })
|
||||
.mockResolvedValueOnce({ uuid_b64: 'item-1', level: 'good' });
|
||||
|
||||
const response = await adjustStockEntry(
|
||||
{ config: { database: 'db' } },
|
||||
'item-1',
|
||||
{ level: 'good' },
|
||||
);
|
||||
|
||||
expect(apiRequestMock).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
{ config: { database: 'db' } },
|
||||
'kitchen/items/item-1/stock',
|
||||
{ method: 'POST', body: { level: 'good' } },
|
||||
);
|
||||
expect(apiRequestMock).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
{ config: { database: 'db' } },
|
||||
'kitchen/items/item-1',
|
||||
);
|
||||
expect(response).toEqual({ uuid_b64: 'item-1', level: 'good' });
|
||||
});
|
||||
|
||||
it('useStockItem returns used on 204', async () => {
|
||||
apiRequestMock.mockResolvedValueOnce(null);
|
||||
|
||||
@@ -278,20 +378,13 @@ describe('api/stock', () => {
|
||||
expect(result).toEqual({ status: 'already_gone' });
|
||||
});
|
||||
|
||||
it('useStockItem falls back to delete on 404/405', async () => {
|
||||
apiRequestMock
|
||||
.mockRejectedValueOnce({ status: 404 })
|
||||
.mockResolvedValueOnce(null);
|
||||
it('useStockItem returns already_gone on 404', async () => {
|
||||
apiRequestMock.mockRejectedValueOnce({ status: 404 });
|
||||
|
||||
const result = await useStockItem({ config: { database: 'db' } }, 'item-1');
|
||||
|
||||
expect(result).toEqual({ status: 'fallback_delete' });
|
||||
expect(apiRequestMock).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
{ config: { database: 'db' } },
|
||||
'kitchen/items/item-1',
|
||||
{ method: 'DELETE' },
|
||||
);
|
||||
expect(result).toEqual({ status: 'already_gone' });
|
||||
expect(apiRequestMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('useStockItem does not fallback on unrelated client errors', async () => {
|
||||
|
||||
@@ -40,7 +40,7 @@ function createGroupedSummary() {
|
||||
first_expire_date: '2026-04-25',
|
||||
first_production_date: '2026-04-10',
|
||||
items_count: 1,
|
||||
items: [],
|
||||
items: [{ id: 100 }],
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -164,12 +164,27 @@ describe('stock list grouped-first behavior', () => {
|
||||
const data = stockListPageData({ isConnected: true, addAlert: vi.fn() });
|
||||
await data.loadGroupedEntries({ expanded: 0, resetVisible: true });
|
||||
|
||||
expect(data.groupedEntries[0].items).toEqual([]);
|
||||
expect(data.groupDisplayItems(data.groupedEntries[0])).toEqual([]);
|
||||
expect(data.hasGroupedChildStubs(data.groupedEntries[0])).toBe(true);
|
||||
|
||||
await data.hydrateGroupedEntriesInBackground();
|
||||
|
||||
expect(data.groupedHydrated).toBe(true);
|
||||
expect(data.groupedEntries[0].items).toHaveLength(1);
|
||||
expect(data.groupDisplayItems(data.groupedEntries[0])).toHaveLength(1);
|
||||
expect(data.hasGroupedChildStubs(data.groupedEntries[0])).toBe(false);
|
||||
});
|
||||
|
||||
it('preserves hydrated child details when summary refresh returns id stubs', async () => {
|
||||
const data = stockListPageData({ isConnected: true, addAlert: vi.fn() });
|
||||
|
||||
data.applyGroupedSummary(createGroupedSummary());
|
||||
data.applyGroupedHydration(createGroupedExpanded());
|
||||
|
||||
expect(data.groupDisplayItems(data.groupedEntries[0])).toHaveLength(1);
|
||||
|
||||
data.applyGroupedSummary(createGroupedSummary());
|
||||
|
||||
expect(data.groupDisplayItems(data.groupedEntries[0])).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('memoizes filtered results and invalidates when filters change', () => {
|
||||
|
||||
Reference in New Issue
Block a user