Add category management: list API, UI integration, and search updates
This commit is contained in:
@@ -196,6 +196,9 @@ Expected shapes today:
|
||||
Used for item-level edits from stock detail (for example identifier code updates).
|
||||
- `GET /{database}/kitchen/locations`
|
||||
Returns a nested location tree.
|
||||
- `GET /{database}/kitchen/categories`
|
||||
Returns categories (paged). Frontend now resolves category labels from
|
||||
`categories_detail` when present, and falls back to this endpoint by ID.
|
||||
|
||||
## Notes
|
||||
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
import { apiRequest, getPath } from './client.js';
|
||||
|
||||
const DEFAULT_LIST_PAGE_LIMIT = 100;
|
||||
|
||||
function unwrapListPayload(payload) {
|
||||
if (Array.isArray(payload)) {
|
||||
return payload;
|
||||
}
|
||||
|
||||
return payload?.data || payload?.entries || payload?.items || payload?.categories || [];
|
||||
}
|
||||
|
||||
function hasExplicitPagination(filters = {}) {
|
||||
return (
|
||||
(filters.limit !== undefined && filters.limit !== null)
|
||||
|| (filters.offset !== undefined && filters.offset !== null)
|
||||
);
|
||||
}
|
||||
|
||||
function buildCategoryQuery(filters = {}) {
|
||||
const query = {};
|
||||
const searchName = filters.searchName || filters.search_name;
|
||||
if (searchName) {
|
||||
query.search_name = searchName;
|
||||
}
|
||||
|
||||
if (filters.active !== undefined && filters.active !== null && filters.active !== '') {
|
||||
query.active = Boolean(filters.active);
|
||||
}
|
||||
|
||||
if (filters.orderBy || filters.order_by) {
|
||||
query.order_by = filters.orderBy || filters.order_by;
|
||||
}
|
||||
|
||||
if (filters.orderDir || filters.order_dir) {
|
||||
query.order_dir = filters.orderDir || filters.order_dir;
|
||||
}
|
||||
|
||||
if (filters.expanded !== undefined && filters.expanded !== null && filters.expanded !== '') {
|
||||
query.expanded = Boolean(filters.expanded);
|
||||
}
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
async function fetchAllCategoryPages(store, baseQuery = {}) {
|
||||
const items = [];
|
||||
let offset = 0;
|
||||
|
||||
while (true) {
|
||||
const payload = await apiRequest(store, getPath('categories'), {
|
||||
query: {
|
||||
...baseQuery,
|
||||
limit: DEFAULT_LIST_PAGE_LIMIT,
|
||||
offset,
|
||||
},
|
||||
});
|
||||
const pageItems = unwrapListPayload(payload);
|
||||
items.push(...pageItems);
|
||||
|
||||
if (pageItems.length < DEFAULT_LIST_PAGE_LIMIT) {
|
||||
break;
|
||||
}
|
||||
|
||||
offset += DEFAULT_LIST_PAGE_LIMIT;
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
export async function listCategories(store, filters = {}) {
|
||||
const baseQuery = buildCategoryQuery(filters);
|
||||
|
||||
if (hasExplicitPagination(filters)) {
|
||||
const query = { ...baseQuery };
|
||||
if (filters.limit !== undefined && filters.limit !== null) {
|
||||
query.limit = filters.limit;
|
||||
}
|
||||
if (filters.offset !== undefined && filters.offset !== null) {
|
||||
query.offset = filters.offset;
|
||||
}
|
||||
|
||||
const payload = await apiRequest(store, getPath('categories'), {
|
||||
query,
|
||||
});
|
||||
return unwrapListPayload(payload);
|
||||
}
|
||||
|
||||
return fetchAllCategoryPages(store, baseQuery);
|
||||
}
|
||||
@@ -27,6 +27,7 @@ export const API_PATHS = {
|
||||
kitchens: 'kitchen/kitchens',
|
||||
items: 'kitchen/items',
|
||||
locations: 'kitchen/locations',
|
||||
categories: 'kitchen/categories',
|
||||
changes: 'kitchen/changes',
|
||||
};
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
} from '../../api/stock.js';
|
||||
import { formatPrintErrorMessage, printItemLabel } from '../../api/labels.js';
|
||||
import { fetchLocations } from '../../api/locations.js';
|
||||
import { listCategories } from '../../api/categories.js';
|
||||
import { getRouteContext } from '../../app/router.js';
|
||||
import { renderScannerModal } from '../shared/scanner-modal.js';
|
||||
import {
|
||||
@@ -145,6 +146,21 @@ export function renderStockDetailPage() {
|
||||
</dd>
|
||||
<dt class="col-5">Stock type</dt>
|
||||
<dd class="col-7" x-text="entry.stock_type || 'Stored'"></dd>
|
||||
<dt class="col-5">Main category</dt>
|
||||
<dd class="col-7" x-text="mainCategoryLabel(entry) || 'Not set'"></dd>
|
||||
<dt class="col-5">Categories</dt>
|
||||
<dd class="col-7">
|
||||
<template x-if="categoryLabels(entry).length">
|
||||
<div class="d-flex flex-wrap gap-1">
|
||||
<template x-for="label in categoryLabels(entry)" :key="label">
|
||||
<span class="badge text-bg-light border" x-text="label"></span>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
<template x-if="!categoryLabels(entry).length">
|
||||
<span class="text-body-secondary">Uncategorized</span>
|
||||
</template>
|
||||
</dd>
|
||||
</dl>
|
||||
|
||||
<div class="mt-4">
|
||||
@@ -464,6 +480,7 @@ export function stockDetailPageData(store) {
|
||||
entry: null,
|
||||
stockEvents: [],
|
||||
locationPathByUuid: {},
|
||||
categoriesById: {},
|
||||
identifierDraft: '',
|
||||
scannerManualCode: '',
|
||||
adjustment: {
|
||||
@@ -484,9 +501,10 @@ export function stockDetailPageData(store) {
|
||||
|
||||
const { params } = getRouteContext();
|
||||
await runAsyncState(this.state, async () => {
|
||||
const [entry, locations] = await Promise.all([
|
||||
const [entry, locations, categories] = await Promise.all([
|
||||
getStockEntry(store, params.id),
|
||||
fetchLocations(store).catch(() => ({ flat: [] })),
|
||||
listCategories(store, { expanded: true }).catch(() => []),
|
||||
]);
|
||||
this.entry = entry;
|
||||
this.identifierDraft = normalizeIdentifierCode(entry?.identifier_code);
|
||||
@@ -495,6 +513,11 @@ export function stockDetailPageData(store) {
|
||||
.filter((location) => location.uuid_b64)
|
||||
.map((location) => [location.uuid_b64, location.pathLabel || location.name]),
|
||||
);
|
||||
this.categoriesById = Object.fromEntries(
|
||||
categories
|
||||
.filter((category) => category?.id !== undefined && category?.id !== null)
|
||||
.map((category) => [String(category.id), category]),
|
||||
);
|
||||
this.adjustment.level = this.entry?.level || 'plenty';
|
||||
}).catch(() => {});
|
||||
this.loadStockHistory().catch(() => {});
|
||||
@@ -966,6 +989,35 @@ export function stockDetailPageData(store) {
|
||||
|
||||
return this.locationPathByUuid[locationUuid] || 'Location not resolved';
|
||||
},
|
||||
categoryLabel(category) {
|
||||
if (!category || typeof category !== 'object') {
|
||||
return '';
|
||||
}
|
||||
|
||||
return String(category.path || category.name || '').trim();
|
||||
},
|
||||
mainCategoryLabel(entry) {
|
||||
const categoryId = entry?.category;
|
||||
if (categoryId === null || categoryId === undefined || categoryId === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
const mapped = this.categoryLabel(this.categoriesById[String(categoryId)]);
|
||||
if (mapped) {
|
||||
return mapped;
|
||||
}
|
||||
|
||||
return String(categoryId).trim();
|
||||
},
|
||||
categoryLabels(entry) {
|
||||
const categoryIds = Array.isArray(entry?.categories) ? entry.categories : [];
|
||||
const labels = categoryIds.map((categoryId) => {
|
||||
const mapped = this.categoryLabel(this.categoriesById[String(categoryId)]);
|
||||
return mapped || String(categoryId || '').trim();
|
||||
});
|
||||
|
||||
return [...new Set(labels.filter(Boolean))];
|
||||
},
|
||||
nutriScoreLabel(entry) {
|
||||
const value = entry?.nutriscore_grade;
|
||||
if (!value) {
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
updateStockItem,
|
||||
} from '../../api/stock.js';
|
||||
import { fetchLocations } from '../../api/locations.js';
|
||||
import { listCategories } from '../../api/categories.js';
|
||||
import { STORAGE_KEYS } from '../../app/config.js';
|
||||
import { clearStoredValue, loadStoredValue, saveStoredValue } from '../shared/storage.js';
|
||||
import { createAsyncState } from '../shared/ui-state.js';
|
||||
@@ -221,13 +222,35 @@ function resolveLocationLabel(entry, locationMap) {
|
||||
return locationMap[entry.location_initial_uuid_b64] || 'Location not resolved';
|
||||
}
|
||||
|
||||
function searchBlob(entry, locationMap) {
|
||||
function categoryLabel(category) {
|
||||
if (!category || typeof category !== 'object') {
|
||||
return '';
|
||||
}
|
||||
|
||||
return String(category.name || category.path || '').trim();
|
||||
}
|
||||
|
||||
function resolveMainCategoryLabel(entry, categoriesById = {}) {
|
||||
const categoryId = entry?.category;
|
||||
if (categoryId === null || categoryId === undefined || categoryId === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
const mapped = categoryLabel(categoriesById[String(categoryId)]);
|
||||
if (mapped) {
|
||||
return mapped;
|
||||
}
|
||||
return String(categoryId).trim();
|
||||
}
|
||||
|
||||
function searchBlob(entry, locationMap, categoriesById = {}) {
|
||||
return [
|
||||
entry.name,
|
||||
entry.description,
|
||||
entry.level,
|
||||
entry.stock_type,
|
||||
resolveLocationLabel(entry, locationMap),
|
||||
resolveMainCategoryLabel(entry, categoriesById),
|
||||
entry.uuid_b64,
|
||||
]
|
||||
.filter(Boolean)
|
||||
@@ -235,10 +258,10 @@ function searchBlob(entry, locationMap) {
|
||||
.toLowerCase();
|
||||
}
|
||||
|
||||
function groupSearchBlob(group, locationMap) {
|
||||
function groupSearchBlob(group, locationMap, categoriesById = {}) {
|
||||
return [
|
||||
searchBlob(group, locationMap),
|
||||
...(group.items || []).map((item) => searchBlob(item, locationMap)),
|
||||
searchBlob(group, locationMap, categoriesById),
|
||||
...(group.items || []).map((item) => searchBlob(item, locationMap, categoriesById)),
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ')
|
||||
@@ -544,6 +567,9 @@ export function renderStockListPage() {
|
||||
<td>
|
||||
<div class="fw-semibold" x-text="entry.name"></div>
|
||||
<div class="small text-body-secondary" x-text="entry.description || 'No description'"></div>
|
||||
<div class="small mt-1" x-show="mainCategoryLabel(entry)">
|
||||
<span class="badge text-bg-light border" x-text="mainCategoryBadgeLabel(entry)"></span>
|
||||
</div>
|
||||
<div class="small font-monospace text-body-secondary" x-text="shortId(entry)"></div>
|
||||
<a
|
||||
class="small text-decoration-none fw-semibold"
|
||||
@@ -623,6 +649,9 @@ export function renderStockListPage() {
|
||||
<div>
|
||||
<div class="fw-semibold fs-5" x-text="entry.name"></div>
|
||||
<div class="text-body-secondary small" x-text="entry.description || 'No description'"></div>
|
||||
<div class="small mt-1" x-show="mainCategoryLabel(entry)">
|
||||
<span class="badge text-bg-light border" x-text="mainCategoryBadgeLabel(entry)"></span>
|
||||
</div>
|
||||
<div class="text-body-secondary small font-monospace" x-text="shortId(entry)"></div>
|
||||
<a
|
||||
class="small text-decoration-none fw-semibold"
|
||||
@@ -728,10 +757,16 @@ export function renderStockListPage() {
|
||||
<div>
|
||||
<div class="fw-semibold grouped-stock-summary-title" x-text="group.name"></div>
|
||||
<div class="text-body-secondary small grouped-stock-summary-description" x-show="group.description" x-text="group.description"></div>
|
||||
<div class="small mt-1" x-show="mainCategoryLabel(group)">
|
||||
<span class="badge text-bg-light border" x-text="mainCategoryBadgeLabel(group)"></span>
|
||||
</div>
|
||||
<div class="d-flex flex-wrap small grouped-stock-summary-meta">
|
||||
<span><span class="fw-semibold text-body" x-text="groupItemCount(group)"></span> item(s)</span>
|
||||
<span><span class="text-body-secondary">Latest location:</span> <span class="fw-semibold text-body" x-text="locationLabel(group)"></span></span>
|
||||
<span><span class="text-body-secondary">Quantity:</span> <span class="fw-semibold text-body" x-text="quantityLabel(group)"></span></span>
|
||||
<span>
|
||||
<span class="text-body-secondary" x-text="groupSummaryMetricLabel(group) + ':'"></span>
|
||||
<span class="fw-semibold text-body" x-text="groupSummaryMetricValue(group)"></span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grouped-stock-summary-status">
|
||||
@@ -781,6 +816,12 @@ export function renderStockListPage() {
|
||||
<span x-text="locationLabel(item)"></span>
|
||||
<span class="grouped-stock-subline-separator" aria-hidden="true">•</span>
|
||||
<span x-text="shortDescription(item.description)"></span>
|
||||
<template x-if="mainCategoryLabel(item)">
|
||||
<span>
|
||||
<span class="grouped-stock-subline-separator" aria-hidden="true">•</span>
|
||||
<span class="text-body-tertiary" x-text="'Main: ' + mainCategoryLabel(item)"></span>
|
||||
</span>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<div class="small text-body-secondary grouped-stock-item-aux">
|
||||
@@ -876,6 +917,7 @@ export function stockListPageData(store) {
|
||||
locationMap: {},
|
||||
locationDescendants: {},
|
||||
locationLineage: {},
|
||||
categoriesById: {},
|
||||
editForms: {},
|
||||
editErrors: {},
|
||||
levelOptions: LEVEL_OPTIONS,
|
||||
@@ -918,7 +960,7 @@ export function stockListPageData(store) {
|
||||
: false;
|
||||
|
||||
if (!restoredFromRuntime) {
|
||||
const initTasks = [this.loadLocations()];
|
||||
const initTasks = [this.loadLocations(), this.loadCategories()];
|
||||
if (this.viewMode === 'items') {
|
||||
initTasks.push(this.loadEntries());
|
||||
} else {
|
||||
@@ -937,6 +979,9 @@ export function stockListPageData(store) {
|
||||
if (!restoredFromRuntime && this.viewMode === 'grouped') {
|
||||
this.hydrateGroupedEntriesInBackground().catch(() => {});
|
||||
}
|
||||
if (restoredFromRuntime) {
|
||||
this.refreshLoadedViewsInBackground().catch(() => {});
|
||||
}
|
||||
if (restoredFromRuntime && restoredContext?.focusedItemUuid) {
|
||||
this.refreshFocusedItemInBackground(restoredContext.focusedItemUuid).catch(() => {});
|
||||
}
|
||||
@@ -1047,6 +1092,7 @@ export function stockListPageData(store) {
|
||||
locationMap: this.locationMap,
|
||||
locationDescendants: this.locationDescendants,
|
||||
locationLineage: this.locationLineage,
|
||||
categoriesById: this.categoriesById,
|
||||
changeCursor: this.changeCursor,
|
||||
}),
|
||||
};
|
||||
@@ -1106,6 +1152,9 @@ export function stockListPageData(store) {
|
||||
this.locationLineage = payload.locationLineage && typeof payload.locationLineage === 'object'
|
||||
? payload.locationLineage
|
||||
: {};
|
||||
this.categoriesById = payload.categoriesById && typeof payload.categoriesById === 'object'
|
||||
? payload.categoriesById
|
||||
: {};
|
||||
this.changeCursor = payload.changeCursor || this.changeCursor;
|
||||
|
||||
if (this.itemsLoaded) {
|
||||
@@ -1267,7 +1316,7 @@ export function stockListPageData(store) {
|
||||
},
|
||||
indexEntry(entry) {
|
||||
const indexed = { ...entry };
|
||||
indexed._searchBlob = searchBlob(indexed, this.locationMap);
|
||||
indexed._searchBlob = searchBlob(indexed, this.locationMap, this.categoriesById);
|
||||
return indexed;
|
||||
},
|
||||
indexGroup(group) {
|
||||
@@ -1278,7 +1327,7 @@ export function stockListPageData(store) {
|
||||
...group,
|
||||
items: indexedItems,
|
||||
};
|
||||
indexed._searchBlob = groupSearchBlob(indexed, this.locationMap);
|
||||
indexed._searchBlob = groupSearchBlob(indexed, this.locationMap, this.categoriesById);
|
||||
return indexed;
|
||||
},
|
||||
reindexSearchData() {
|
||||
@@ -1513,6 +1562,25 @@ export function stockListPageData(store) {
|
||||
this.persistRuntimeCache();
|
||||
}
|
||||
},
|
||||
async loadCategories() {
|
||||
if (!store.isConnected) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const categories = await listCategories(store, { expanded: true });
|
||||
this.categoriesById = Object.fromEntries(
|
||||
categories
|
||||
.filter((category) => category?.id !== undefined && category?.id !== null)
|
||||
.map((category) => [String(category.id), category]),
|
||||
);
|
||||
} catch {
|
||||
this.categoriesById = {};
|
||||
} finally {
|
||||
this.reindexSearchData();
|
||||
this.persistRuntimeCache();
|
||||
}
|
||||
},
|
||||
resetGroupedVisibleLimit() {
|
||||
this.groupedVisibleLimit = this.groupedPageSize;
|
||||
},
|
||||
@@ -1526,6 +1594,28 @@ export function stockListPageData(store) {
|
||||
|
||||
return group.items.filter((item) => !isGroupedChildStub(item));
|
||||
},
|
||||
mainCategoryLabel(entry) {
|
||||
return resolveMainCategoryLabel(entry, this.categoriesById);
|
||||
},
|
||||
mainCategoryBadgeLabel(entry) {
|
||||
return `Main category: ${this.mainCategoryLabel(entry)}`;
|
||||
},
|
||||
groupSummaryMetricLabel(group) {
|
||||
if (group?.stock_type === 'measured') {
|
||||
return 'Quantity';
|
||||
}
|
||||
if (group?.stock_type === 'descriptive') {
|
||||
return 'Stock level';
|
||||
}
|
||||
if (group?.stock_type === 'binary') {
|
||||
return 'Stock state';
|
||||
}
|
||||
return 'Stock';
|
||||
},
|
||||
groupSummaryMetricValue(group) {
|
||||
// Use backend-provided grouped fields as the single source of truth.
|
||||
return quantityLabel(group);
|
||||
},
|
||||
hasGroupedChildStubs(group) {
|
||||
if (!Array.isArray(group?.items)) {
|
||||
return false;
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user