Refactor stock API to replace numeric flags with boolean values, add getItemLabel endpoint, and update tests/documentation
ci/woodpecker/push/woodpecker Pipeline was successful

This commit is contained in:
2026-05-01 23:51:05 +02:00
parent 34e339eb44
commit 1fe56a232b
12 changed files with 193 additions and 47 deletions
+4 -4
View File
@@ -61,12 +61,12 @@ describe('api/client', () => {
const url = buildKitchenApiUrl(store, 'kitchen/items/grouped', {
search_name: 'Milk + eggs',
expanded: 1,
expanded: true,
ignored: '',
});
expect(url).toBe(
'https://api.example.com/my%20db/kitchen/items/grouped?search_name=Milk+%2B+eggs&expanded=1',
'https://api.example.com/my%20db/kitchen/items/grouped?search_name=Milk+%2B+eggs&expanded=true',
);
});
@@ -88,14 +88,14 @@ describe('api/client', () => {
name: 'Rice',
},
query: {
label: 1,
label: true,
},
});
expect(payload).toEqual({ ok: true });
const [url, request] = fetchSpy.mock.calls[0];
expect(url).toBe('/kitchen-db/kitchen/items?label=1');
expect(url).toBe('/kitchen-db/kitchen/items?label=true');
expect(request.method).toBe('POST');
expect(request.body).toBe('{"name":"Rice"}');
expect(request.headers.get('Accept')).toBe('application/json');
+68 -2
View File
@@ -1,6 +1,72 @@
import { describe, expect, it } from 'vitest';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { formatPrintErrorMessage } from '../../src/api/labels.js';
const apiRequestMock = vi.fn();
vi.mock('../../src/api/client.js', () => ({
getPath(key) {
const paths = {
items: 'kitchen/items',
};
return paths[key];
},
apiRequest: (...args) => apiRequestMock(...args),
}));
const {
formatPrintErrorMessage,
getItemLabel,
previewLabel,
} = await import('../../src/api/labels.js');
describe('api/labels', () => {
beforeEach(() => {
apiRequestMock.mockReset();
});
it('previewLabel uses boolean label/preview query flags', async () => {
apiRequestMock.mockResolvedValueOnce({
label: 'YWJj',
});
const response = await previewLabel({ config: { database: 'db' } }, { name: 'Rice' });
expect(apiRequestMock).toHaveBeenCalledWith(
{ config: { database: 'db' } },
'kitchen/items',
{
method: 'POST',
body: { name: 'Rice' },
accept: 'image/svg+xml, image/png, application/json',
query: { label: true, preview: true },
},
);
expect(response).toEqual({
objectUrl: 'data:image/png;base64,YWJj',
contentType: 'image/png',
});
});
it('getItemLabel fetches PNG from /label endpoint', async () => {
apiRequestMock.mockResolvedValueOnce({
label: 'YWJj',
});
const response = await getItemLabel({ config: { database: 'db' } }, 'item-1');
expect(apiRequestMock).toHaveBeenCalledWith(
{ config: { database: 'db' } },
'kitchen/items/item-1/label',
{
method: 'GET',
accept: 'image/png, application/json',
},
);
expect(response).toEqual({
objectUrl: 'data:image/png;base64,YWJj',
contentType: 'image/png',
});
});
});
describe('api/labels formatPrintErrorMessage', () => {
it('maps printer_unavailable payload to user-friendly message', () => {
+34 -7
View File
@@ -20,6 +20,7 @@ const {
listGroupedStockEntries,
listKitchenChanges,
listStockEntries,
markStockGone,
lookupItemByIdentifier,
lookupItemDetails,
patchStockItem,
@@ -84,7 +85,7 @@ describe('api/stock', () => {
await listGroupedStockEntries(
{ config: { database: 'db' } },
{ expanded: 0, searchName: 'Rice', limit: 10, offset: 0 },
{ expanded: false, searchName: 'Rice', limit: 10, offset: 0 },
);
expect(apiRequestMock).toHaveBeenCalledWith(
@@ -92,7 +93,7 @@ describe('api/stock', () => {
'kitchen/items/grouped',
{
query: {
expanded: 0,
expanded: false,
search_name: 'Rice',
limit: 10,
offset: 0,
@@ -108,7 +109,7 @@ describe('api/stock', () => {
const response = await listGroupedStockEntries(
{ config: { database: 'db' } },
{ expanded: 1, searchName: 'Rice' },
{ expanded: true, searchName: 'Rice' },
);
expect(response).toHaveLength(101);
@@ -116,13 +117,13 @@ describe('api/stock', () => {
1,
{ config: { database: 'db' } },
'kitchen/items/grouped',
{ query: { expanded: 1, search_name: 'Rice', limit: 100, offset: 0 } },
{ query: { expanded: true, 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 } },
{ query: { expanded: true, search_name: 'Rice', limit: 100, offset: 100 } },
);
});
@@ -151,7 +152,7 @@ describe('api/stock', () => {
expect(apiRequestMock).toHaveBeenCalledWith(
{ config: { database: 'db' } },
'kitchen/items/item-2',
{ query: { allow_inactive: 1 } },
{ query: { allow_inactive: true } },
);
});
@@ -297,7 +298,7 @@ describe('api/stock', () => {
expect(apiRequestMock).toHaveBeenCalledWith(
{ config: { database: 'db' } },
'kitchen/items/item-1/lookup',
{ method: 'POST', query: { update: 1 } },
{ method: 'POST', query: { update: true } },
);
expect(response).toEqual({
status: 'ok',
@@ -426,4 +427,30 @@ describe('api/stock', () => {
});
expect(apiRequestMock).toHaveBeenCalledTimes(1);
});
it('markStockGone uses /use endpoint for consumed reason', async () => {
apiRequestMock.mockResolvedValueOnce(null);
const result = await markStockGone({ config: { database: 'db' } }, 'item-1', 'consumed');
expect(result).toEqual({ status: 'gone', reason: 'consumed' });
expect(apiRequestMock).toHaveBeenCalledWith(
{ config: { database: 'db' } },
'kitchen/items/item-1/use',
{ method: 'POST' },
);
});
it('markStockGone uses /stock endpoint for non-consumed reasons', async () => {
apiRequestMock.mockResolvedValueOnce({ status: 'OK', stock: { id: 3 } });
const result = await markStockGone({ config: { database: 'db' } }, 'item-1', 'spoiled');
expect(result).toEqual({ status: 'gone', reason: 'spoiled' });
expect(apiRequestMock).toHaveBeenCalledWith(
{ config: { database: 'db' } },
'kitchen/items/item-1/stock',
{ method: 'POST', body: { level: 'gone', gone_reason: 'spoiled' } },
);
});
});
+1 -1
View File
@@ -130,6 +130,6 @@ describe('stock mark-gone behavior', () => {
message: 'Beans was marked used and removed from the group.',
});
expect(listGroupedStockEntriesMock).toHaveBeenCalledTimes(1);
expect(listGroupedStockEntriesMock).toHaveBeenCalledWith(store, { expanded: 0 });
expect(listGroupedStockEntriesMock).toHaveBeenCalledWith(store, { expanded: false });
});
});
@@ -141,12 +141,12 @@ describe('stock list grouped-first behavior', () => {
await data.init();
expect(data.viewMode).toBe('grouped');
expect(listGroupedStockEntriesMock).toHaveBeenNthCalledWith(1, store, { expanded: 0 });
expect(listGroupedStockEntriesMock).toHaveBeenNthCalledWith(1, store, { expanded: false });
expect(listStockEntriesMock).not.toHaveBeenCalled();
await Promise.resolve();
expect(listGroupedStockEntriesMock).toHaveBeenNthCalledWith(2, store, { expanded: 1 });
expect(listGroupedStockEntriesMock).toHaveBeenNthCalledWith(2, store, { expanded: true });
await data.switchView('items');
expect(listStockEntriesMock).toHaveBeenCalledTimes(1);
@@ -162,7 +162,7 @@ describe('stock list grouped-first behavior', () => {
.mockResolvedValueOnce(createGroupedExpanded());
const data = stockListPageData({ isConnected: true, addAlert: vi.fn() });
await data.loadGroupedEntries({ expanded: 0, resetVisible: true });
await data.loadGroupedEntries({ expanded: false, resetVisible: true });
expect(data.groupDisplayItems(data.groupedEntries[0])).toEqual([]);
expect(data.hasGroupedChildStubs(data.groupedEntries[0])).toBe(true);
@@ -226,7 +226,7 @@ describe('stock list grouped-first behavior', () => {
data.groupedLoaded = true;
data.groupedEntries = createGroupedSummary().map((group) => data.indexGroup(group));
const pending = data.loadGroupedEntries({ expanded: 0, background: true });
const pending = data.loadGroupedEntries({ expanded: false, background: true });
expect(data.state.isRefreshing).toBe(true);
expect(data.groupedEntries).toHaveLength(1);