Improve stock list restore and item-level refresh feedback
This commit is contained in:
@@ -3,6 +3,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
const listStockEntriesMock = vi.fn();
|
||||
const listGroupedStockEntriesMock = vi.fn();
|
||||
const listKitchenChangesMock = vi.fn();
|
||||
const getStockEntryMock = vi.fn();
|
||||
const updateStockItemMock = vi.fn();
|
||||
const useStockItemMock = vi.fn();
|
||||
const fetchLocationsMock = vi.fn();
|
||||
@@ -11,6 +12,7 @@ vi.mock('../../../src/api/stock.js', () => ({
|
||||
listStockEntries: (...args) => listStockEntriesMock(...args),
|
||||
listGroupedStockEntries: (...args) => listGroupedStockEntriesMock(...args),
|
||||
listKitchenChanges: (...args) => listKitchenChangesMock(...args),
|
||||
getStockEntry: (...args) => getStockEntryMock(...args),
|
||||
updateStockItem: (...args) => updateStockItemMock(...args),
|
||||
useStockItem: (...args) => useStockItemMock(...args),
|
||||
}));
|
||||
@@ -70,9 +72,11 @@ function createGroupedExpanded() {
|
||||
function createWindowMock() {
|
||||
const intervals = new Map();
|
||||
let nextId = 1;
|
||||
const storage = new Map();
|
||||
|
||||
return {
|
||||
location: { hash: '#/stock' },
|
||||
scrollY: 1,
|
||||
setInterval: vi.fn((fn) => {
|
||||
const id = nextId;
|
||||
nextId += 1;
|
||||
@@ -85,6 +89,16 @@ function createWindowMock() {
|
||||
addEventListener: vi.fn(),
|
||||
removeEventListener: vi.fn(),
|
||||
matchMedia: vi.fn(() => ({ matches: false })),
|
||||
scrollTo: vi.fn(),
|
||||
localStorage: {
|
||||
getItem: vi.fn((key) => storage.get(key) ?? null),
|
||||
setItem: vi.fn((key, value) => {
|
||||
storage.set(key, value);
|
||||
}),
|
||||
removeItem: vi.fn((key) => {
|
||||
storage.delete(key);
|
||||
}),
|
||||
},
|
||||
__intervals: intervals,
|
||||
};
|
||||
}
|
||||
@@ -94,6 +108,7 @@ describe('stock list grouped-first behavior', () => {
|
||||
listStockEntriesMock.mockReset();
|
||||
listGroupedStockEntriesMock.mockReset();
|
||||
listKitchenChangesMock.mockReset();
|
||||
getStockEntryMock.mockReset();
|
||||
updateStockItemMock.mockReset();
|
||||
useStockItemMock.mockReset();
|
||||
fetchLocationsMock.mockReset();
|
||||
@@ -108,6 +123,7 @@ describe('stock list grouped-first behavior', () => {
|
||||
delete globalThis.window;
|
||||
delete globalThis.requestAnimationFrame;
|
||||
delete globalThis.HTMLDetailsElement;
|
||||
delete globalThis.structuredClone;
|
||||
});
|
||||
|
||||
it('defaults to grouped mode and loads grouped summary before lazy item list', async () => {
|
||||
@@ -254,4 +270,149 @@ describe('stock list grouped-first behavior', () => {
|
||||
data.handleGroupedToggle({ target: details });
|
||||
expect(data.isGroupedCardOpen(10)).toBe(false);
|
||||
});
|
||||
|
||||
it('restores from runtime cache on back navigation and refreshes only focused item', async () => {
|
||||
listKitchenChangesMock.mockResolvedValue({ since: null, nextCursor: null, changes: [] });
|
||||
getStockEntryMock.mockResolvedValueOnce({
|
||||
id: 100,
|
||||
uuid_b64: 'item-100',
|
||||
name: 'Rice',
|
||||
description: 'Open bag',
|
||||
stock_type: 'measured',
|
||||
level: 'good',
|
||||
quantity: 2,
|
||||
uom_symbol: 'kg',
|
||||
location_initial_uuid_b64: 'loc-root',
|
||||
date: '2026-04-12',
|
||||
expire_date: '2026-04-20',
|
||||
expire_in: 8,
|
||||
});
|
||||
|
||||
const store = {
|
||||
isConnected: true,
|
||||
addAlert: vi.fn(),
|
||||
activeKitchen: { id: 1 },
|
||||
};
|
||||
const firstVisit = stockListPageData(store);
|
||||
firstVisit.entries = [
|
||||
firstVisit.indexEntry({
|
||||
id: 100,
|
||||
uuid_b64: 'item-100',
|
||||
name: 'Rice',
|
||||
description: 'Open bag',
|
||||
stock_type: 'measured',
|
||||
level: 'good',
|
||||
quantity: 1,
|
||||
uom_symbol: 'kg',
|
||||
location_initial_uuid_b64: 'loc-root',
|
||||
date: '2026-04-10',
|
||||
expire_date: '2026-04-25',
|
||||
expire_in: 13,
|
||||
}),
|
||||
];
|
||||
firstVisit.entriesVersion = 1;
|
||||
firstVisit.itemsLoaded = true;
|
||||
firstVisit.groupedEntries = createGroupedExpanded().map((group) => firstVisit.indexGroup(group));
|
||||
firstVisit.groupedVersion = 1;
|
||||
firstVisit.groupedLoaded = true;
|
||||
firstVisit.groupedHydrated = true;
|
||||
firstVisit.locations = [
|
||||
{
|
||||
id: 1,
|
||||
uuid_b64: 'loc-root',
|
||||
name: 'Pantry',
|
||||
pathLabel: 'Pantry',
|
||||
depth: 0,
|
||||
type: 'storage',
|
||||
lineage_uuid_b64: ['loc-root'],
|
||||
},
|
||||
];
|
||||
firstVisit.locationsVersion = 1;
|
||||
firstVisit.locationMap = { 'loc-root': 'Pantry' };
|
||||
firstVisit.locationDescendants = { 'loc-root': ['loc-root'] };
|
||||
firstVisit.locationLineage = { 'loc-root': ['loc-root'] };
|
||||
firstVisit.viewMode = 'grouped';
|
||||
firstVisit.filters.search = 'rice';
|
||||
firstVisit.rememberStockListContext('item-100');
|
||||
|
||||
const returnVisit = stockListPageData(store);
|
||||
returnVisit.$nextTick = vi.fn(async () => {});
|
||||
|
||||
await returnVisit.init();
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
|
||||
expect(listGroupedStockEntriesMock).not.toHaveBeenCalled();
|
||||
expect(listStockEntriesMock).not.toHaveBeenCalled();
|
||||
expect(fetchLocationsMock).not.toHaveBeenCalled();
|
||||
expect(getStockEntryMock).toHaveBeenCalledWith(store, 'item-100');
|
||||
expect(returnVisit.entries[0].quantity).toBe(2);
|
||||
expect(returnVisit.groupedEntries[0].items[0].quantity).toBe(2);
|
||||
expect(returnVisit.groupedEntries[0].quantity).toBe(2);
|
||||
expect(returnVisit.groupedEntries[0].first_expire_date).toBe('2026-04-20');
|
||||
expect(returnVisit.groupedEntries[0].date).toBe('2026-04-12');
|
||||
});
|
||||
|
||||
it('tracks item-level refresh state while focused item refresh is in progress', async () => {
|
||||
let resolveEntry;
|
||||
getStockEntryMock.mockImplementationOnce(
|
||||
() =>
|
||||
new Promise((resolve) => {
|
||||
resolveEntry = resolve;
|
||||
}),
|
||||
);
|
||||
|
||||
const data = stockListPageData({ isConnected: true, addAlert: vi.fn() });
|
||||
data.entries = [
|
||||
data.indexEntry({
|
||||
id: 100,
|
||||
uuid_b64: 'item-100',
|
||||
name: 'Rice',
|
||||
description: 'Open bag',
|
||||
stock_type: 'measured',
|
||||
level: 'good',
|
||||
quantity: 1,
|
||||
uom_symbol: 'kg',
|
||||
location_initial_uuid_b64: 'loc-root',
|
||||
date: '2026-04-10',
|
||||
expire_date: '2026-04-25',
|
||||
}),
|
||||
];
|
||||
data.entriesVersion = 1;
|
||||
data.itemsLoaded = true;
|
||||
|
||||
const pending = data.refreshFocusedItemInBackground('item-100');
|
||||
expect(data.isItemRefreshing('item-100')).toBe(true);
|
||||
|
||||
resolveEntry({
|
||||
...data.entries[0],
|
||||
quantity: 3,
|
||||
});
|
||||
await pending;
|
||||
|
||||
expect(data.isItemRefreshing('item-100')).toBe(false);
|
||||
expect(data.entries[0].quantity).toBe(3);
|
||||
});
|
||||
|
||||
it('falls back when structuredClone throws during runtime cache snapshot', async () => {
|
||||
globalThis.structuredClone = vi.fn(() => {
|
||||
throw new Error('The object can not be cloned.');
|
||||
});
|
||||
|
||||
listGroupedStockEntriesMock
|
||||
.mockResolvedValueOnce(createGroupedSummary())
|
||||
.mockResolvedValueOnce(createGroupedExpanded());
|
||||
listKitchenChangesMock.mockResolvedValue({ since: null, nextCursor: null, changes: [] });
|
||||
fetchLocationsMock.mockResolvedValue({ flat: [], tree: [] });
|
||||
|
||||
const store = { isConnected: true, addAlert: vi.fn() };
|
||||
const data = stockListPageData(store);
|
||||
data.$nextTick = vi.fn(async () => {});
|
||||
|
||||
await data.init();
|
||||
await Promise.resolve();
|
||||
|
||||
expect(data.state.error).toBe('');
|
||||
expect(data.groupedEntries.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user