Implement upsert label flow and use-based mark gone handling
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/push/woodpecker Pipeline was successful
This commit is contained in:
@@ -0,0 +1,143 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
const listKitchenChangesMock = vi.fn();
|
||||
const getStockEntryMock = vi.fn();
|
||||
const fetchLocationsMock = vi.fn();
|
||||
|
||||
vi.mock('../../../src/api/stock.js', () => ({
|
||||
listKitchenChanges: (...args) => listKitchenChangesMock(...args),
|
||||
getStockEntry: (...args) => getStockEntryMock(...args),
|
||||
}));
|
||||
|
||||
vi.mock('../../../src/api/locations.js', () => ({
|
||||
fetchLocations: (...args) => fetchLocationsMock(...args),
|
||||
}));
|
||||
|
||||
const { dashboardPageData, renderDashboardPage } = await import('../../../src/features/dashboard/dashboard-page.js');
|
||||
|
||||
describe('features/dashboard/dashboard-page', () => {
|
||||
it('renders dashboard with recent changes section', () => {
|
||||
const html = renderDashboardPage();
|
||||
expect(html).toContain('Recent changes');
|
||||
expect(html).toContain('x-data="dashboardPage()"');
|
||||
expect(html).toContain('Saved means the backend created or updated a record.');
|
||||
});
|
||||
|
||||
it('loads recent changes on init and renders item-focused state lines', async () => {
|
||||
listKitchenChangesMock.mockResolvedValueOnce({
|
||||
since: null,
|
||||
nextCursor: null,
|
||||
changes: [{
|
||||
type: 'item',
|
||||
action: 'upsert',
|
||||
timestamp: '2026-04-10T10:00:00Z',
|
||||
item: {
|
||||
uuid_b64: 'u1',
|
||||
name: 'Rice',
|
||||
stock_type: 'measured',
|
||||
quantity: 3,
|
||||
uom_symbol: 'kg',
|
||||
level: 'good',
|
||||
expire_date: '2026-04-21',
|
||||
location_initial_uuid_b64: 'loc1',
|
||||
},
|
||||
}],
|
||||
});
|
||||
fetchLocationsMock.mockResolvedValueOnce({
|
||||
flat: [{ uuid_b64: 'loc1', pathLabel: 'Pantry / Shelf A' }],
|
||||
});
|
||||
|
||||
const store = {
|
||||
isConnected: true,
|
||||
setActiveKitchen: vi.fn(),
|
||||
addAlert: vi.fn(),
|
||||
};
|
||||
const data = dashboardPageData(store);
|
||||
|
||||
await data.init();
|
||||
|
||||
expect(listKitchenChangesMock).toHaveBeenCalledWith(store, { limit: 10 });
|
||||
expect(data.recentChanges).toHaveLength(1);
|
||||
expect(data.changesState.error).toBe('');
|
||||
expect(data.changeHeadline(data.recentChanges[0])).toBe('Item saved: Rice');
|
||||
expect(data.changeStateLine(data.recentChanges[0])).toContain('Quantity: 3 kg');
|
||||
expect(data.changeStateLine(data.recentChanges[0])).toContain('Location: Pantry / Shelf A');
|
||||
expect(getStockEntryMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('resolves stock event item context via item lookup when needed', async () => {
|
||||
listKitchenChangesMock.mockResolvedValueOnce({
|
||||
since: null,
|
||||
nextCursor: null,
|
||||
changes: [{
|
||||
type: 'stock',
|
||||
action: 'upsert',
|
||||
timestamp: '2026-04-10T10:00:00Z',
|
||||
stock: {
|
||||
item_uuid_b64: 'item-uuid-1',
|
||||
quantity: 0.5,
|
||||
uom_symbol: 'kg',
|
||||
level: 'some',
|
||||
location_uuid_b64: 'loc2',
|
||||
},
|
||||
}],
|
||||
});
|
||||
getStockEntryMock.mockResolvedValueOnce({
|
||||
uuid_b64: 'item-uuid-1',
|
||||
name: 'Flour',
|
||||
stock_type: 'measured',
|
||||
expire_date: '2026-05-02',
|
||||
});
|
||||
fetchLocationsMock.mockResolvedValueOnce({
|
||||
flat: [{ uuid_b64: 'loc2', pathLabel: 'Pantry / Bin 2' }],
|
||||
});
|
||||
|
||||
const data = dashboardPageData({
|
||||
isConnected: true,
|
||||
setActiveKitchen: vi.fn(),
|
||||
addAlert: vi.fn(),
|
||||
});
|
||||
|
||||
await data.refreshChanges();
|
||||
|
||||
expect(data.changeHeadline(data.recentChanges[0])).toBe('Stock saved: Flour');
|
||||
expect(data.changeStateLine(data.recentChanges[0])).toContain('Quantity: 0.5 kg');
|
||||
expect(data.changeStateLine(data.recentChanges[0])).toContain('Level: Some');
|
||||
expect(data.changeStateLine(data.recentChanges[0])).toContain('Location: Pantry / Bin 2');
|
||||
expect(getStockEntryMock).toHaveBeenCalledWith(expect.anything(), 'item-uuid-1');
|
||||
});
|
||||
|
||||
it('keeps empty state when API returns no changes', async () => {
|
||||
listKitchenChangesMock.mockResolvedValueOnce({
|
||||
since: null,
|
||||
nextCursor: null,
|
||||
changes: [],
|
||||
});
|
||||
fetchLocationsMock.mockResolvedValueOnce({ flat: [] });
|
||||
|
||||
const data = dashboardPageData({
|
||||
isConnected: true,
|
||||
setActiveKitchen: vi.fn(),
|
||||
addAlert: vi.fn(),
|
||||
});
|
||||
|
||||
await data.refreshChanges();
|
||||
|
||||
expect(data.recentChanges).toEqual([]);
|
||||
expect(data.changesState.error).toBe('');
|
||||
});
|
||||
|
||||
it('captures refresh errors in async state', async () => {
|
||||
listKitchenChangesMock.mockRejectedValueOnce(new Error('Feed unavailable'));
|
||||
const data = dashboardPageData({
|
||||
isConnected: true,
|
||||
setActiveKitchen: vi.fn(),
|
||||
addAlert: vi.fn(),
|
||||
});
|
||||
|
||||
await data.refreshChanges();
|
||||
|
||||
expect(data.changesState.error).toBe('Feed unavailable');
|
||||
expect(data.recentChanges).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,86 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
const applyItemUpsertMock = vi.fn();
|
||||
const previewItemUpsertMock = vi.fn();
|
||||
|
||||
vi.mock('../../../src/api/stock.js', () => ({
|
||||
applyItemUpsert: (...args) => applyItemUpsertMock(...args),
|
||||
previewItemUpsert: (...args) => previewItemUpsertMock(...args),
|
||||
searchItemDefinitions: vi.fn(async () => []),
|
||||
}));
|
||||
|
||||
vi.mock('../../../src/api/labels.js', () => ({
|
||||
previewLabel: vi.fn(async () => ({ objectUrl: 'blob:preview' })),
|
||||
}));
|
||||
|
||||
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 create upsert-first submit', () => {
|
||||
it('builds upsert payload with selected template uuid', () => {
|
||||
const store = {
|
||||
isConnected: false,
|
||||
activeKitchen: { id: 7 },
|
||||
addAlert: vi.fn(),
|
||||
};
|
||||
const data = labelCreatePageData(store);
|
||||
data.form = {
|
||||
...data.form,
|
||||
itemUuidB64: 'uuid-template-1',
|
||||
name: 'Beans',
|
||||
description: 'Dry beans',
|
||||
stockType: 'measured',
|
||||
quantity: '2',
|
||||
uom: 'kg',
|
||||
level: '',
|
||||
productionDate: '2026-04-10',
|
||||
expirationDate: '2026-08-10',
|
||||
locationId: '',
|
||||
identifierCode: '12345',
|
||||
};
|
||||
|
||||
const payload = data.buildUpsertPayload();
|
||||
|
||||
expect(payload.uuid_b64).toBe('uuid-template-1');
|
||||
expect(payload.identifier_code).toBe('12345');
|
||||
expect(payload.item.name).toBe('Beans');
|
||||
expect(payload.item.quantity_initial).toBe(2);
|
||||
});
|
||||
|
||||
it('create uses applyItemUpsert and sets operation-aware success message', async () => {
|
||||
applyItemUpsertMock.mockResolvedValueOnce({
|
||||
operation: 'update',
|
||||
item: { name: 'Rice' },
|
||||
});
|
||||
|
||||
const addAlert = vi.fn();
|
||||
const store = {
|
||||
isConnected: false,
|
||||
activeKitchen: { id: 3 },
|
||||
addAlert,
|
||||
};
|
||||
const data = labelCreatePageData(store);
|
||||
data.validateBeforeSubmit = () => true;
|
||||
data.form = {
|
||||
...data.form,
|
||||
name: 'Rice',
|
||||
stockType: 'binary',
|
||||
locationId: '',
|
||||
productionDate: '2026-04-10',
|
||||
itemUuidB64: 'uuid-rice-1',
|
||||
};
|
||||
|
||||
await data.create();
|
||||
|
||||
expect(applyItemUpsertMock).toHaveBeenCalledTimes(1);
|
||||
expect(applyItemUpsertMock.mock.calls[0][1].uuid_b64).toBe('uuid-rice-1');
|
||||
expect(data.successMessage).toBe('Rice was updated successfully.');
|
||||
expect(addAlert).toHaveBeenCalledWith({
|
||||
type: 'success',
|
||||
message: 'Rice was updated successfully.',
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,71 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
const useStockItemMock = vi.fn();
|
||||
const getStockEntryMock = vi.fn();
|
||||
|
||||
vi.mock('../../../src/api/stock.js', () => ({
|
||||
useStockItem: (...args) => useStockItemMock(...args),
|
||||
getStockEntry: (...args) => getStockEntryMock(...args),
|
||||
adjustStockEntry: vi.fn(),
|
||||
listStockEntries: vi.fn(),
|
||||
listGroupedStockEntries: vi.fn(),
|
||||
updateStockItem: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../../../src/api/locations.js', () => ({
|
||||
fetchLocations: vi.fn(async () => ({ flat: [], tree: [] })),
|
||||
}));
|
||||
|
||||
const { stockDetailPageData } = await import('../../../src/features/stock/stock-detail-page.js');
|
||||
const { stockListPageData } = await import('../../../src/features/stock/stock-list-page.js');
|
||||
|
||||
describe('stock mark-gone behavior', () => {
|
||||
beforeEach(() => {
|
||||
useStockItemMock.mockReset();
|
||||
getStockEntryMock.mockReset();
|
||||
globalThis.window = {
|
||||
__loncApp: {
|
||||
navigate: vi.fn(),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
delete globalThis.window;
|
||||
});
|
||||
|
||||
it('stock detail markGone uses /use and shows info for already gone', async () => {
|
||||
useStockItemMock.mockResolvedValueOnce({ status: 'already_gone' });
|
||||
const addAlert = vi.fn();
|
||||
const data = stockDetailPageData({ addAlert });
|
||||
data.entry = { uuid_b64: 'item-1', name: 'Rice' };
|
||||
|
||||
await data.markGone();
|
||||
|
||||
expect(useStockItemMock).toHaveBeenCalledWith({ addAlert }, 'item-1');
|
||||
expect(addAlert).toHaveBeenCalledWith({
|
||||
type: 'info',
|
||||
message: 'Rice was already out of stock.',
|
||||
});
|
||||
expect(globalThis.window.__loncApp.navigate).toHaveBeenCalledWith('/stock');
|
||||
});
|
||||
|
||||
it('stock list markGone removes entry and uses /use path', async () => {
|
||||
useStockItemMock.mockResolvedValueOnce({ status: 'used' });
|
||||
const addAlert = vi.fn();
|
||||
const data = stockListPageData({ addAlert, isConnected: false });
|
||||
data.entries = [{ id: 1, uuid_b64: 'item-1', name: 'Flour' }];
|
||||
data.editForms = { 1: { level: 'plenty', quantity: 1 } };
|
||||
data.editErrors = {};
|
||||
|
||||
await data.markGone(data.entries[0]);
|
||||
|
||||
expect(useStockItemMock).toHaveBeenCalledWith({ addAlert, isConnected: false }, 'item-1');
|
||||
expect(data.entries).toEqual([]);
|
||||
expect(addAlert).toHaveBeenCalledWith({
|
||||
type: 'success',
|
||||
message: 'Flour was marked gone and removed from the list.',
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user