Upgrade OFF lookup UX and stock detail identifier editing
This commit is contained in:
@@ -15,7 +15,10 @@ vi.mock('../../src/api/client.js', () => ({
|
||||
|
||||
const {
|
||||
applyItemUpsert,
|
||||
lookupItemByIdentifier,
|
||||
lookupItemDetails,
|
||||
listKitchenChanges,
|
||||
patchStockItem,
|
||||
previewItemUpsert,
|
||||
useStockItem,
|
||||
} = await import('../../src/api/stock.js');
|
||||
@@ -112,6 +115,101 @@ describe('api/stock', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('lookupItemByIdentifier normalizes lookup metadata fields', async () => {
|
||||
apiRequestMock.mockResolvedValueOnce({
|
||||
status: 'rate_limited',
|
||||
source: 'openfoodfacts',
|
||||
cache_hit: true,
|
||||
identifier_code: '1234',
|
||||
identifier_type: 'ean_13',
|
||||
retry_after_seconds: 42,
|
||||
payload_fetched_at: '2026-04-11T08:00:00Z',
|
||||
stale_cache: true,
|
||||
item: null,
|
||||
});
|
||||
|
||||
const response = await lookupItemByIdentifier(
|
||||
{ config: { database: 'db' } },
|
||||
'1234',
|
||||
);
|
||||
|
||||
expect(response).toEqual({
|
||||
status: 'rate_limited',
|
||||
source: 'openfoodfacts',
|
||||
cacheHit: true,
|
||||
identifierCode: '1234',
|
||||
identifierType: 'ean_13',
|
||||
retryAfterSeconds: 42,
|
||||
payloadFetchedAt: '2026-04-11T08:00:00Z',
|
||||
staleCache: true,
|
||||
item: null,
|
||||
});
|
||||
});
|
||||
|
||||
it('lookupItemDetails maps item lookup response and query flag', async () => {
|
||||
apiRequestMock.mockResolvedValueOnce({
|
||||
status: 'ok',
|
||||
found: true,
|
||||
update: true,
|
||||
identifier_code: '555',
|
||||
identifier_type: 'ean_13',
|
||||
preview: { name: 'Milk' },
|
||||
updated_fields: ['name'],
|
||||
off_payload_fetched_at: '2026-04-11T09:00:00Z',
|
||||
retry_after_seconds: null,
|
||||
stale_cache: false,
|
||||
item: { uuid_b64: 'item-1', name: 'Milk' },
|
||||
});
|
||||
|
||||
const response = await lookupItemDetails(
|
||||
{ config: { database: 'db' } },
|
||||
'item-1',
|
||||
{ update: true },
|
||||
);
|
||||
|
||||
expect(apiRequestMock).toHaveBeenCalledWith(
|
||||
{ config: { database: 'db' } },
|
||||
'kitchen/items/item-1/lookup',
|
||||
{ method: 'POST', query: { update: 1 } },
|
||||
);
|
||||
expect(response).toEqual({
|
||||
status: 'ok',
|
||||
found: true,
|
||||
update: true,
|
||||
identifierCode: '555',
|
||||
identifierType: 'ean_13',
|
||||
preview: { name: 'Milk' },
|
||||
updatedFields: ['name'],
|
||||
offPayloadFetchedAt: '2026-04-11T09:00:00Z',
|
||||
retryAfterSeconds: null,
|
||||
staleCache: false,
|
||||
item: { uuid_b64: 'item-1', name: 'Milk' },
|
||||
});
|
||||
});
|
||||
|
||||
it('patchStockItem sends PATCH to item endpoint', async () => {
|
||||
apiRequestMock.mockResolvedValueOnce({
|
||||
uuid_b64: 'item-1',
|
||||
identifier_code: '3830012345678',
|
||||
});
|
||||
|
||||
const response = await patchStockItem(
|
||||
{ config: { database: 'db' } },
|
||||
'item-1',
|
||||
{ identifier_code: '3830012345678' },
|
||||
);
|
||||
|
||||
expect(apiRequestMock).toHaveBeenCalledWith(
|
||||
{ config: { database: 'db' } },
|
||||
'kitchen/items/item-1',
|
||||
{ method: 'PATCH', body: { identifier_code: '3830012345678' } },
|
||||
);
|
||||
expect(response).toEqual({
|
||||
uuid_b64: 'item-1',
|
||||
identifier_code: '3830012345678',
|
||||
});
|
||||
});
|
||||
|
||||
it('useStockItem returns used on 204', async () => {
|
||||
apiRequestMock.mockResolvedValueOnce(null);
|
||||
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
const lookupItemByIdentifierMock = vi.fn();
|
||||
|
||||
vi.mock('../../../src/api/stock.js', () => ({
|
||||
applyItemUpsert: vi.fn(),
|
||||
previewItemUpsert: vi.fn(),
|
||||
searchItemDefinitions: vi.fn(async () => []),
|
||||
lookupItemByIdentifier: (...args) => lookupItemByIdentifierMock(...args),
|
||||
}));
|
||||
|
||||
vi.mock('../../../src/api/labels.js', () => ({
|
||||
previewLabel: vi.fn(async () => ({ objectUrl: 'blob:preview' })),
|
||||
printItemLabel: vi.fn(async () => null),
|
||||
formatPrintErrorMessage: (error) => error?.message || 'Printing failed.',
|
||||
}));
|
||||
|
||||
vi.mock('../../../src/api/locations.js', () => ({
|
||||
fetchLocations: vi.fn(async () => ({ flat: [], tree: [] })),
|
||||
}));
|
||||
|
||||
const { labelCreatePageData } = await import('../../../src/features/labels/label-create-page.js');
|
||||
|
||||
describe('label identifier lookup feedback', () => {
|
||||
it('shows retry hint for rate-limited lookup responses', () => {
|
||||
const data = labelCreatePageData({
|
||||
isConnected: false,
|
||||
activeKitchen: { id: 1 },
|
||||
addAlert: vi.fn(),
|
||||
});
|
||||
|
||||
const message = data.lookupStatusMessageWithDetails(
|
||||
{ status: 'rate_limited', retryAfterSeconds: 30 },
|
||||
'3830012345678',
|
||||
);
|
||||
|
||||
expect(message).toContain('rate-limited');
|
||||
expect(message).toContain('Retry in 30s');
|
||||
});
|
||||
|
||||
it('builds metadata-aware success message with source/cache/freshness context', () => {
|
||||
const data = labelCreatePageData({
|
||||
isConnected: false,
|
||||
activeKitchen: { id: 1 },
|
||||
addAlert: vi.fn(),
|
||||
});
|
||||
|
||||
const message = data.lookupSuccessMessage({
|
||||
source: 'openfoodfacts',
|
||||
cacheHit: true,
|
||||
staleCache: true,
|
||||
payloadFetchedAt: '2026-04-11T09:00:00Z',
|
||||
});
|
||||
|
||||
expect(message).toContain('OpenFoodFacts');
|
||||
expect(message).toContain('cache hit');
|
||||
expect(message).toContain('stale cache');
|
||||
expect(message).toContain('fetched:');
|
||||
});
|
||||
|
||||
it('applies non-ok lookup status as warning message with details', async () => {
|
||||
lookupItemByIdentifierMock.mockResolvedValueOnce({
|
||||
status: 'rate_limited',
|
||||
retryAfterSeconds: 45,
|
||||
source: 'openfoodfacts',
|
||||
cacheHit: false,
|
||||
staleCache: false,
|
||||
item: null,
|
||||
});
|
||||
|
||||
const addAlert = vi.fn();
|
||||
const data = labelCreatePageData({
|
||||
isConnected: false,
|
||||
activeKitchen: { id: 1 },
|
||||
addAlert,
|
||||
});
|
||||
data.form.identifierCode = '3830012345678';
|
||||
|
||||
await data.lookupIdentifierDetails();
|
||||
|
||||
expect(data.lookupState.error).toContain('Retry in 45s');
|
||||
expect(addAlert).toHaveBeenCalledWith({
|
||||
type: 'warning',
|
||||
message: data.lookupState.error,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -7,6 +7,8 @@ vi.mock('../../../src/api/stock.js', () => ({
|
||||
useStockItem: (...args) => useStockItemMock(...args),
|
||||
getStockEntry: (...args) => getStockEntryMock(...args),
|
||||
adjustStockEntry: vi.fn(),
|
||||
lookupItemDetails: vi.fn(),
|
||||
patchStockItem: vi.fn(),
|
||||
listStockEntries: vi.fn(),
|
||||
listGroupedStockEntries: vi.fn(),
|
||||
updateStockItem: vi.fn(),
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
const lookupItemDetailsMock = vi.fn();
|
||||
const patchStockItemMock = vi.fn();
|
||||
const getStockEntryMock = vi.fn();
|
||||
|
||||
vi.mock('../../../src/api/stock.js', () => ({
|
||||
adjustStockEntry: vi.fn(),
|
||||
getStockEntry: (...args) => getStockEntryMock(...args),
|
||||
lookupItemDetails: (...args) => lookupItemDetailsMock(...args),
|
||||
patchStockItem: (...args) => patchStockItemMock(...args),
|
||||
useStockItem: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../../../src/api/labels.js', () => ({
|
||||
printItemLabel: vi.fn(),
|
||||
formatPrintErrorMessage: (error) => error?.message || 'Printing failed.',
|
||||
}));
|
||||
|
||||
vi.mock('../../../src/api/locations.js', () => ({
|
||||
fetchLocations: vi.fn(async () => ({ flat: [], tree: [] })),
|
||||
}));
|
||||
|
||||
const { stockDetailPageData } = await import('../../../src/features/stock/stock-detail-page.js');
|
||||
|
||||
describe('stock detail identifier and OFF lookup', () => {
|
||||
beforeEach(() => {
|
||||
lookupItemDetailsMock.mockReset();
|
||||
patchStockItemMock.mockReset();
|
||||
getStockEntryMock.mockReset();
|
||||
globalThis.window = {
|
||||
__loncApp: {
|
||||
navigate: vi.fn(),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
delete globalThis.window;
|
||||
});
|
||||
|
||||
it('saves normalized identifier code via PATCH', async () => {
|
||||
patchStockItemMock.mockResolvedValueOnce({
|
||||
uuid_b64: 'item-1',
|
||||
name: 'Milk',
|
||||
identifier_code: '3830012345678',
|
||||
});
|
||||
|
||||
const addAlert = vi.fn();
|
||||
const store = { addAlert, isConnected: false };
|
||||
const data = stockDetailPageData(store);
|
||||
data.entry = { uuid_b64: 'item-1', name: 'Milk', identifier_code: '' };
|
||||
data.identifierDraft = ' 3830 0123 45678 ';
|
||||
|
||||
await data.saveIdentifierCode();
|
||||
|
||||
expect(patchStockItemMock).toHaveBeenCalledWith(store, 'item-1', {
|
||||
identifier_code: '3830012345678',
|
||||
});
|
||||
expect(data.identifierDraft).toBe('3830012345678');
|
||||
expect(addAlert).toHaveBeenCalledWith({
|
||||
type: 'success',
|
||||
message: 'Identifier code saved for Milk.',
|
||||
});
|
||||
});
|
||||
|
||||
it('refreshes OFF details and surfaces stale-cache metadata', async () => {
|
||||
lookupItemDetailsMock.mockResolvedValueOnce({
|
||||
status: 'ok',
|
||||
update: false,
|
||||
updatedFields: ['name', 'nutrition_facts'],
|
||||
staleCache: true,
|
||||
offPayloadFetchedAt: '2026-04-11T09:00:00Z',
|
||||
item: {
|
||||
uuid_b64: 'item-1',
|
||||
name: 'Milk',
|
||||
identifier_code: '3830012345678',
|
||||
},
|
||||
});
|
||||
|
||||
const addAlert = vi.fn();
|
||||
const store = { addAlert, isConnected: false };
|
||||
const data = stockDetailPageData(store);
|
||||
data.entry = { uuid_b64: 'item-1', name: 'Milk', identifier_code: '3830012345678' };
|
||||
data.identifierDraft = '3830012345678';
|
||||
|
||||
await data.runItemLookup(false);
|
||||
|
||||
expect(lookupItemDetailsMock).toHaveBeenCalledWith(store, 'item-1', { update: false });
|
||||
expect(data.offLookupFeedback.type).toBe('success');
|
||||
expect(data.offLookupFeedback.message).toContain('Using stale cache data.');
|
||||
expect(addAlert).toHaveBeenCalledWith({
|
||||
type: 'success',
|
||||
message: data.offLookupFeedback.message,
|
||||
});
|
||||
});
|
||||
|
||||
it('apply missing fields reloads entry after successful lookup', async () => {
|
||||
lookupItemDetailsMock.mockResolvedValueOnce({
|
||||
status: 'ok',
|
||||
update: true,
|
||||
updatedFields: ['description'],
|
||||
staleCache: false,
|
||||
offPayloadFetchedAt: null,
|
||||
item: null,
|
||||
});
|
||||
getStockEntryMock.mockResolvedValueOnce({
|
||||
uuid_b64: 'item-1',
|
||||
name: 'Milk',
|
||||
identifier_code: '3830012345678',
|
||||
description: 'Whole milk',
|
||||
});
|
||||
|
||||
const addAlert = vi.fn();
|
||||
const store = { addAlert, isConnected: false };
|
||||
const data = stockDetailPageData(store);
|
||||
data.entry = { uuid_b64: 'item-1', name: 'Milk', identifier_code: '3830012345678' };
|
||||
data.identifierDraft = '3830012345678';
|
||||
|
||||
await data.runItemLookup(true);
|
||||
|
||||
expect(getStockEntryMock).toHaveBeenCalledWith(store, 'item-1');
|
||||
expect(data.entry.description).toBe('Whole milk');
|
||||
expect(data.offLookupFeedback.type).toBe('success');
|
||||
});
|
||||
});
|
||||
@@ -12,6 +12,8 @@ vi.mock('../../../src/api/stock.js', () => ({
|
||||
getStockEntry: vi.fn(),
|
||||
adjustStockEntry: vi.fn(),
|
||||
useStockItem: vi.fn(),
|
||||
lookupItemDetails: vi.fn(),
|
||||
patchStockItem: vi.fn(),
|
||||
listStockEntries: vi.fn(async () => []),
|
||||
listGroupedStockEntries: vi.fn(async () => []),
|
||||
updateStockItem: vi.fn(),
|
||||
|
||||
Reference in New Issue
Block a user