Add category management: list API, UI integration, and search updates
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/pr/woodpecker Pipeline was successful

This commit is contained in:
2026-05-02 22:47:46 +02:00
parent 1fe56a232b
commit 054a7ad0dd
7 changed files with 324 additions and 11 deletions
+67
View File
@@ -0,0 +1,67 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
const apiRequestMock = vi.fn();
vi.mock('../../src/api/client.js', () => ({
getPath(key) {
const paths = {
categories: 'kitchen/categories',
};
return paths[key];
},
apiRequest: (...args) => apiRequestMock(...args),
}));
const { listCategories } = await import('../../src/api/categories.js');
describe('api/categories', () => {
beforeEach(() => {
apiRequestMock.mockReset();
});
it('forwards explicit pagination and filters', async () => {
apiRequestMock.mockResolvedValueOnce([]);
await listCategories(
{ config: { database: 'db' } },
{ searchName: 'dairy', active: true, limit: 10, offset: 20, orderBy: 'name', orderDir: 'asc' },
);
expect(apiRequestMock).toHaveBeenCalledWith(
{ config: { database: 'db' } },
'kitchen/categories',
{
query: {
search_name: 'dairy',
active: true,
order_by: 'name',
order_dir: 'asc',
limit: 10,
offset: 20,
},
},
);
});
it('aggregates category pages by default', async () => {
apiRequestMock
.mockResolvedValueOnce(Array.from({ length: 100 }, (_, id) => ({ id: id + 1 })))
.mockResolvedValueOnce([{ id: 101 }]);
const response = await listCategories({ config: { database: 'db' } }, {});
expect(response).toHaveLength(101);
expect(apiRequestMock).toHaveBeenNthCalledWith(
1,
{ config: { database: 'db' } },
'kitchen/categories',
{ query: { limit: 100, offset: 0 } },
);
expect(apiRequestMock).toHaveBeenNthCalledWith(
2,
{ config: { database: 'db' } },
'kitchen/categories',
{ query: { limit: 100, offset: 100 } },
);
});
});
@@ -7,6 +7,7 @@ const getStockEntryMock = vi.fn();
const updateStockItemMock = vi.fn();
const useStockItemMock = vi.fn();
const fetchLocationsMock = vi.fn();
const listCategoriesMock = vi.fn();
vi.mock('../../../src/api/stock.js', () => ({
listStockEntries: (...args) => listStockEntriesMock(...args),
@@ -21,6 +22,10 @@ vi.mock('../../../src/api/locations.js', () => ({
fetchLocations: (...args) => fetchLocationsMock(...args),
}));
vi.mock('../../../src/api/categories.js', () => ({
listCategories: (...args) => listCategoriesMock(...args),
}));
const { stockListPageData } = await import('../../../src/features/stock/stock-list-page.js');
function createGroupedSummary() {
@@ -112,6 +117,8 @@ describe('stock list grouped-first behavior', () => {
updateStockItemMock.mockReset();
useStockItemMock.mockReset();
fetchLocationsMock.mockReset();
listCategoriesMock.mockReset();
listCategoriesMock.mockResolvedValue([]);
globalThis.window = createWindowMock();
globalThis.requestAnimationFrame = (callback) => callback();
@@ -133,6 +140,7 @@ describe('stock list grouped-first behavior', () => {
listStockEntriesMock.mockResolvedValueOnce([]);
listKitchenChangesMock.mockResolvedValue({ since: null, nextCursor: null, changes: [] });
fetchLocationsMock.mockResolvedValue({ flat: [], tree: [] });
listCategoriesMock.mockResolvedValue([]);
const store = { isConnected: true, addAlert: vi.fn() };
const data = stockListPageData(store);
@@ -357,8 +365,10 @@ describe('stock list grouped-first behavior', () => {
await Promise.resolve();
await Promise.resolve();
expect(listGroupedStockEntriesMock).not.toHaveBeenCalled();
expect(listStockEntriesMock).not.toHaveBeenCalled();
expect(listGroupedStockEntriesMock).toHaveBeenNthCalledWith(1, store, { expanded: false });
expect(listGroupedStockEntriesMock).toHaveBeenNthCalledWith(2, store, { expanded: true });
expect(listStockEntriesMock).toHaveBeenCalledTimes(1);
expect(listStockEntriesMock).toHaveBeenCalledWith(store);
expect(fetchLocationsMock).not.toHaveBeenCalled();
expect(getStockEntryMock).toHaveBeenCalledWith(store, 'item-100');
expect(returnVisit.entries[0].quantity).toBe(2);