modified: .codex_playwright_version
modified: .vscode/launch.json modified: package-lock.json modified: package.json modified: src/components/AddProductDialog.tsx new file: src/screens/ActivePickListScreen.additional.test.tsx new file: src/screens/ManageProductsScreen.additional.test.tsx modified: src/screens/ManageProductsScreen.test.tsx
This commit is contained in:
@@ -1 +1 @@
|
|||||||
1.56.1
|
|
||||||
|
|||||||
Vendored
+7
@@ -31,6 +31,13 @@
|
|||||||
"request": "launch",
|
"request": "launch",
|
||||||
"command": "npm run test:coverage",
|
"command": "npm run test:coverage",
|
||||||
"cwd": "${workspaceFolder}"
|
"cwd": "${workspaceFolder}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Lint and Build",
|
||||||
|
"type": "node-terminal",
|
||||||
|
"request": "launch",
|
||||||
|
"command": "npx tsc -noEmit && npm run build",
|
||||||
|
"cwd": "${workspaceFolder}"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
-11
@@ -12,7 +12,6 @@
|
|||||||
"@emotion/styled": "^11.14.1",
|
"@emotion/styled": "^11.14.1",
|
||||||
"@mui/icons-material": "^7.3.5",
|
"@mui/icons-material": "^7.3.5",
|
||||||
"@mui/material": "^7.3.5",
|
"@mui/material": "^7.3.5",
|
||||||
"@types/uuid": "^11.0.0",
|
|
||||||
"@zxing/browser": "^0.1.5",
|
"@zxing/browser": "^0.1.5",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"dexie": "^4.2.1",
|
"dexie": "^4.2.1",
|
||||||
@@ -2456,16 +2455,6 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@types/uuid": {
|
|
||||||
"version": "11.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-11.0.0.tgz",
|
|
||||||
"integrity": "sha512-HVyk8nj2m+jcFRNazzqyVKiZezyhDKrGUA3jlEcg/nZ6Ms+qHwocba1Y/AaVaznJTAM9xpdFSh+ptbNrhOGvZA==",
|
|
||||||
"deprecated": "This is a stub types definition. uuid provides its own type definitions, so you do not need this installed.",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"uuid": "*"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||||
"version": "8.47.0",
|
"version": "8.47.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.47.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.47.0.tgz",
|
||||||
|
|||||||
@@ -17,7 +17,6 @@
|
|||||||
"@emotion/styled": "^11.14.1",
|
"@emotion/styled": "^11.14.1",
|
||||||
"@mui/icons-material": "^7.3.5",
|
"@mui/icons-material": "^7.3.5",
|
||||||
"@mui/material": "^7.3.5",
|
"@mui/material": "^7.3.5",
|
||||||
"@types/uuid": "^11.0.0",
|
|
||||||
"@zxing/browser": "^0.1.5",
|
"@zxing/browser": "^0.1.5",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"dexie": "^4.2.1",
|
"dexie": "^4.2.1",
|
||||||
|
|||||||
@@ -266,11 +266,14 @@ export const AddProductDialog = ({
|
|||||||
open={open}
|
open={open}
|
||||||
onClose={handleDialogClose}
|
onClose={handleDialogClose}
|
||||||
aria-label="Add product dialog"
|
aria-label="Add product dialog"
|
||||||
|
aria-labelledby="add-product-dialog-title"
|
||||||
|
data-testid="add-product-dialog"
|
||||||
fullWidth
|
fullWidth
|
||||||
maxWidth="sm"
|
maxWidth="sm"
|
||||||
PaperProps={{ role: 'form' }}
|
PaperProps={{ role: 'form' }}
|
||||||
|
BackdropProps={{ 'data-testid': 'add-product-backdrop' }}
|
||||||
>
|
>
|
||||||
<DialogTitle sx={{ pr: 6 }}>
|
<DialogTitle id="add-product-dialog-title" sx={{ pr: 6 }}>
|
||||||
Add product
|
Add product
|
||||||
<IconButton
|
<IconButton
|
||||||
aria-label="Close add product"
|
aria-label="Close add product"
|
||||||
@@ -332,7 +335,13 @@ export const AddProductDialog = ({
|
|||||||
</Stack>
|
</Stack>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
|
||||||
<Dialog open={scannerOpen} onClose={() => setScannerOpen(false)} aria-label="Scan barcode">
|
<Dialog
|
||||||
|
open={scannerOpen}
|
||||||
|
onClose={() => setScannerOpen(false)}
|
||||||
|
aria-label="Scan barcode"
|
||||||
|
data-testid="scan-barcode-dialog"
|
||||||
|
BackdropProps={{ 'data-testid': 'scan-barcode-backdrop' }}
|
||||||
|
>
|
||||||
<DialogTitle>Scan barcode</DialogTitle>
|
<DialogTitle>Scan barcode</DialogTitle>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<BarcodeScannerView
|
<BarcodeScannerView
|
||||||
|
|||||||
@@ -0,0 +1,284 @@
|
|||||||
|
// src/screens/ActivePickListScreen.additional.test.tsx
|
||||||
|
import { MemoryRouter, Route, Routes } from 'react-router-dom';
|
||||||
|
import { render, screen, within, waitFor, cleanup } from '@testing-library/react';
|
||||||
|
import userEvent from '@testing-library/user-event';
|
||||||
|
import { describe, expect, it, vi, beforeEach } from 'vitest';
|
||||||
|
import ActivePickListScreen from './ActivePickListScreen';
|
||||||
|
import { PickItem } from '../models/PickItem';
|
||||||
|
import { Product } from '../models/Product';
|
||||||
|
|
||||||
|
const addMock = vi.fn();
|
||||||
|
const updateMock = vi.fn();
|
||||||
|
const deleteMock = vi.fn();
|
||||||
|
const pickItemsMock = vi.fn<() => PickItem[]>();
|
||||||
|
const productsMock = vi.fn<() => Product[]>();
|
||||||
|
const pickListMock = vi.fn();
|
||||||
|
|
||||||
|
// allow tests to mutate categories returned by the mocked hook
|
||||||
|
let categoriesVar: any[] = [{ id: 'cat-1', name: 'Drinks', created_at: 0, updated_at: 0 }];
|
||||||
|
|
||||||
|
vi.mock('../hooks/dataHooks', () => ({
|
||||||
|
usePickItems: () => pickItemsMock(),
|
||||||
|
useProducts: () => productsMock(),
|
||||||
|
usePickList: () => pickListMock(),
|
||||||
|
useAreas: () => [{ id: 'area-1', name: 'Front Counter', created_at: 0, updated_at: 0 }],
|
||||||
|
useCategories: () => categoriesVar,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../context/DBProvider', () => ({
|
||||||
|
useDatabase: () => ({
|
||||||
|
pickItems: {
|
||||||
|
add: addMock,
|
||||||
|
update: updateMock,
|
||||||
|
get: vi.fn(async (id: string) => {
|
||||||
|
const items: PickItem[] = pickItemsMock() ?? [];
|
||||||
|
return items.find((it) => it.id === id);
|
||||||
|
}),
|
||||||
|
delete: deleteMock,
|
||||||
|
where: (_f: string) => ({
|
||||||
|
equals: (val: any) => ({
|
||||||
|
toArray: async () => {
|
||||||
|
const items: PickItem[] = pickItemsMock() ?? [];
|
||||||
|
return items.filter((it) => it.pick_list_id === val);
|
||||||
|
},
|
||||||
|
count: async () => {
|
||||||
|
const items: PickItem[] = pickItemsMock() ?? [];
|
||||||
|
return items.filter((it) => it.pick_list_id === val).length;
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const defaultProducts: Product[] = [
|
||||||
|
{
|
||||||
|
id: 'prod-1',
|
||||||
|
name: 'Cola',
|
||||||
|
category: 'cat-1',
|
||||||
|
unit_type: 'unit',
|
||||||
|
bulk_name: 'box',
|
||||||
|
barcode: '111',
|
||||||
|
archived: false,
|
||||||
|
created_at: 0,
|
||||||
|
updated_at: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'prod-2',
|
||||||
|
name: 'Chips',
|
||||||
|
category: 'cat-1',
|
||||||
|
unit_type: 'unit',
|
||||||
|
bulk_name: 'box',
|
||||||
|
barcode: '222',
|
||||||
|
archived: false,
|
||||||
|
created_at: 0,
|
||||||
|
updated_at: 0,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
addMock.mockReset();
|
||||||
|
updateMock.mockReset();
|
||||||
|
deleteMock.mockReset();
|
||||||
|
pickItemsMock.mockReset();
|
||||||
|
productsMock.mockReset();
|
||||||
|
pickListMock.mockReset();
|
||||||
|
categoriesVar = [{ id: 'cat-1', name: 'Drinks', created_at: 0, updated_at: 0 }];
|
||||||
|
|
||||||
|
productsMock.mockReturnValue(defaultProducts);
|
||||||
|
pickListMock.mockReturnValue({
|
||||||
|
id: 'list-1',
|
||||||
|
area_id: 'area-1',
|
||||||
|
created_at: 0,
|
||||||
|
categories: ['cat-1'],
|
||||||
|
auto_add_new_products: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('ActivePickListScreen - additional branches', () => {
|
||||||
|
const getProductInput = () => {
|
||||||
|
const byTestId = screen.queryByTestId('product-search-input');
|
||||||
|
if (byTestId) return byTestId;
|
||||||
|
return screen.getByPlaceholderText('Search products');
|
||||||
|
};
|
||||||
|
|
||||||
|
it('increments / decrements / toggles carton / toggles status / deletes an item', async () => {
|
||||||
|
pickItemsMock.mockReturnValue([
|
||||||
|
{
|
||||||
|
id: 'item-1',
|
||||||
|
pick_list_id: 'list-1',
|
||||||
|
product_id: 'prod-1',
|
||||||
|
quantity: 1,
|
||||||
|
is_carton: false,
|
||||||
|
status: 'pending',
|
||||||
|
created_at: 0,
|
||||||
|
updated_at: 0,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const user = userEvent.setup();
|
||||||
|
render(
|
||||||
|
<MemoryRouter initialEntries={['/pick-lists/1']}>
|
||||||
|
<Routes>
|
||||||
|
<Route path="/pick-lists/:id" element={<ActivePickListScreen />} />
|
||||||
|
</Routes>
|
||||||
|
</MemoryRouter>,
|
||||||
|
);
|
||||||
|
|
||||||
|
const inc = screen.getByLabelText('Increase quantity');
|
||||||
|
await user.click(inc);
|
||||||
|
expect(updateMock).toHaveBeenCalled();
|
||||||
|
|
||||||
|
const dec = screen.getByLabelText('Decrease quantity');
|
||||||
|
await user.click(dec);
|
||||||
|
expect(updateMock).toHaveBeenCalled();
|
||||||
|
|
||||||
|
const cartonButton = screen.getByLabelText(/Switch to carton packaging/i);
|
||||||
|
await user.click(cartonButton);
|
||||||
|
expect(updateMock).toHaveBeenCalled();
|
||||||
|
|
||||||
|
const checkbox = screen.getByLabelText('Toggle picked status') as HTMLInputElement;
|
||||||
|
await user.click(checkbox);
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(updateMock).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
const deleteBtn = screen.getByLabelText('Delete item');
|
||||||
|
await user.click(deleteBtn);
|
||||||
|
const confirmDelete = await screen.findByRole('button', { name: /delete/i });
|
||||||
|
await user.click(confirmDelete);
|
||||||
|
expect(deleteMock).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handleMarkAllPicked updates items when items exist', async () => {
|
||||||
|
// normal case: itemState present -> mark all picked should update
|
||||||
|
pickItemsMock.mockReturnValue([
|
||||||
|
{
|
||||||
|
id: 'item-2',
|
||||||
|
pick_list_id: 'list-1',
|
||||||
|
product_id: 'prod-2',
|
||||||
|
quantity: 1,
|
||||||
|
is_carton: false,
|
||||||
|
status: 'pending',
|
||||||
|
created_at: 0,
|
||||||
|
updated_at: 0,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const user = userEvent.setup();
|
||||||
|
render(
|
||||||
|
<MemoryRouter initialEntries={['/pick-lists/1']}>
|
||||||
|
<Routes>
|
||||||
|
<Route path="/pick-lists/:id" element={<ActivePickListScreen />} />
|
||||||
|
</Routes>
|
||||||
|
</MemoryRouter>,
|
||||||
|
);
|
||||||
|
|
||||||
|
const pickCompleteBtn = screen.getByRole('button', { name: /pick complete/i });
|
||||||
|
await user.click(pickCompleteBtn);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(updateMock).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('packaging radios are disabled when unique packaging count === 1 and enabled when both present', async () => {
|
||||||
|
// single packaging type (all units)
|
||||||
|
pickItemsMock.mockReturnValue([
|
||||||
|
{
|
||||||
|
id: 'item-3',
|
||||||
|
pick_list_id: 'list-1',
|
||||||
|
product_id: 'prod-1',
|
||||||
|
quantity: 1,
|
||||||
|
is_carton: false,
|
||||||
|
status: 'pending',
|
||||||
|
created_at: 0,
|
||||||
|
updated_at: 0,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
render(
|
||||||
|
<MemoryRouter initialEntries={['/pick-lists/1']}>
|
||||||
|
<Routes>
|
||||||
|
<Route path="/pick-lists/:id" element={<ActivePickListScreen />} />
|
||||||
|
</Routes>
|
||||||
|
</MemoryRouter>,
|
||||||
|
);
|
||||||
|
|
||||||
|
const unitsWrapper = screen.getByTestId('packaging-filter-units');
|
||||||
|
const cartonsWrapper = screen.getByTestId('packaging-filter-cartons');
|
||||||
|
const unitsInput = (unitsWrapper as HTMLElement).querySelector('input') as HTMLInputElement;
|
||||||
|
const cartonsInput = (cartonsWrapper as HTMLElement).querySelector('input') as HTMLInputElement;
|
||||||
|
expect(unitsInput.disabled).toBe(true);
|
||||||
|
expect(cartonsInput.disabled).toBe(true);
|
||||||
|
|
||||||
|
// cleanup before re-rendering to avoid duplicate data-testid nodes
|
||||||
|
cleanup();
|
||||||
|
|
||||||
|
// both cartons and units present -> radios enabled
|
||||||
|
pickItemsMock.mockReturnValue([
|
||||||
|
{
|
||||||
|
id: 'item-4',
|
||||||
|
pick_list_id: 'list-1',
|
||||||
|
product_id: 'prod-1',
|
||||||
|
quantity: 1,
|
||||||
|
is_carton: false,
|
||||||
|
status: 'pending',
|
||||||
|
created_at: 0,
|
||||||
|
updated_at: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'item-5',
|
||||||
|
pick_list_id: 'list-1',
|
||||||
|
product_id: 'prod-2',
|
||||||
|
quantity: 1,
|
||||||
|
is_carton: true,
|
||||||
|
status: 'pending',
|
||||||
|
created_at: 0,
|
||||||
|
updated_at: 0,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
render(
|
||||||
|
<MemoryRouter initialEntries={['/pick-lists/1']}>
|
||||||
|
<Routes>
|
||||||
|
<Route path="/pick-lists/:id" element={<ActivePickListScreen />} />
|
||||||
|
</Routes>
|
||||||
|
</MemoryRouter>,
|
||||||
|
);
|
||||||
|
|
||||||
|
const unitsWrapper2 = screen.getByTestId('packaging-filter-units');
|
||||||
|
const cartonsWrapper2 = screen.getByTestId('packaging-filter-cartons');
|
||||||
|
const unitsInput2 = (unitsWrapper2 as HTMLElement).querySelector('input') as HTMLInputElement;
|
||||||
|
const cartonsInput2 = (cartonsWrapper2 as HTMLElement).querySelector('input') as HTMLInputElement;
|
||||||
|
expect(unitsInput2.disabled).toBe(false);
|
||||||
|
expect(cartonsInput2.disabled).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('categoryOptions resolves legacy names to ids when pickList.categories contains names', () => {
|
||||||
|
// Set categoriesVar so useCategories returns an id `c1` for 'Drinks'
|
||||||
|
categoriesVar = [{ id: 'c1', name: 'Drinks', created_at: 0, updated_at: 0 }];
|
||||||
|
|
||||||
|
// pickList with legacy name 'Drinks'
|
||||||
|
pickListMock.mockReturnValue({
|
||||||
|
id: 'list-1',
|
||||||
|
area_id: 'area-1',
|
||||||
|
created_at: 0,
|
||||||
|
categories: ['Drinks'],
|
||||||
|
auto_add_new_products: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
render(
|
||||||
|
<MemoryRouter initialEntries={['/pick-lists/1']}>
|
||||||
|
<Routes>
|
||||||
|
<Route path="/pick-lists/:id" element={<ActivePickListScreen />} />
|
||||||
|
</Routes>
|
||||||
|
</MemoryRouter>,
|
||||||
|
);
|
||||||
|
|
||||||
|
const select = screen.getByLabelText(/Filter by category/i) as HTMLSelectElement;
|
||||||
|
const option = Array.from(select.options).find((o) => o.value === 'c1');
|
||||||
|
expect(option).toBeDefined();
|
||||||
|
expect(option?.text).toMatch(/Drinks/i);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
// src/screens/ManageProductsScreen.additional.test.tsx
|
||||||
|
import { MemoryRouter } from 'react-router-dom';
|
||||||
|
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
||||||
|
import userEvent from '@testing-library/user-event';
|
||||||
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
import ManageProductsScreen from './ManageProductsScreen';
|
||||||
|
|
||||||
|
const mockUseProducts = vi.fn();
|
||||||
|
const mockUseCategories = vi.fn();
|
||||||
|
|
||||||
|
vi.mock('../hooks/dataHooks', () => ({
|
||||||
|
useProducts: () => mockUseProducts(),
|
||||||
|
useCategories: () => mockUseCategories(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
// DB mock
|
||||||
|
const productsDb = {
|
||||||
|
get: vi.fn(),
|
||||||
|
put: vi.fn(),
|
||||||
|
where: vi.fn(), // must return { equals: () => ({ first: async () => ... }) }
|
||||||
|
filter: vi.fn(),
|
||||||
|
};
|
||||||
|
const categoriesDb = {
|
||||||
|
where: vi.fn(),
|
||||||
|
get: vi.fn(),
|
||||||
|
add: vi.fn(),
|
||||||
|
};
|
||||||
|
const pickListsDb = {
|
||||||
|
toArray: vi.fn(),
|
||||||
|
};
|
||||||
|
const pickItemsDb = {
|
||||||
|
add: vi.fn(),
|
||||||
|
};
|
||||||
|
const mockDb: any = {
|
||||||
|
products: productsDb,
|
||||||
|
categories: categoriesDb,
|
||||||
|
pickLists: pickListsDb,
|
||||||
|
pickItems: pickItemsDb,
|
||||||
|
transaction: vi.fn(async (_mode: string, ...args: any[]) => {
|
||||||
|
const cb = args[args.length - 1];
|
||||||
|
if (typeof cb === 'function') return cb();
|
||||||
|
return undefined;
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
vi.mock('../context/DBProvider', () => ({
|
||||||
|
useDatabase: () => mockDb,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('uuid', () => ({ v4: () => 'new-product-id' }));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mockUseProducts.mockReset();
|
||||||
|
mockUseCategories.mockReset();
|
||||||
|
Object.values(productsDb).forEach((f) => typeof f === 'function' && (f as any).mockReset && (f as any).mockReset());
|
||||||
|
Object.values(categoriesDb).forEach((f) => typeof f === 'function' && (f as any).mockReset && (f as any).mockReset());
|
||||||
|
pickListsDb.toArray.mockReset();
|
||||||
|
pickItemsDb.add.mockReset();
|
||||||
|
mockDb.transaction.mockReset();
|
||||||
|
|
||||||
|
// Default product DB stubs
|
||||||
|
productsDb.where.mockImplementation(() => ({ equals: () => ({ first: async () => undefined }) }));
|
||||||
|
productsDb.filter.mockImplementation(() => ({ delete: vi.fn() }));
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('ManageProductsScreen update product category creation branch', () => {
|
||||||
|
it('creates a new category when updates.category is an unknown name and saves product', async () => {
|
||||||
|
const existingProduct = {
|
||||||
|
id: 'prod-2',
|
||||||
|
name: 'Another Product',
|
||||||
|
category: 'cat-old',
|
||||||
|
barcode: '654321',
|
||||||
|
unit_type: 'unit',
|
||||||
|
bulk_name: 'pack',
|
||||||
|
archived: false,
|
||||||
|
created_at: 0,
|
||||||
|
updated_at: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
mockUseProducts.mockReturnValue([existingProduct]);
|
||||||
|
mockUseCategories.mockReturnValue([]); // no categories available
|
||||||
|
|
||||||
|
productsDb.get.mockResolvedValue(existingProduct);
|
||||||
|
|
||||||
|
// products.where('barcode').equals(value).first() must exist for uniqueness check
|
||||||
|
productsDb.where.mockImplementation((field?: string) => ({
|
||||||
|
equals: (value?: string) => ({
|
||||||
|
first: async () => undefined,
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
// categories.where('name').equals(...).first => undefined (no matching category name)
|
||||||
|
categoriesDb.where.mockImplementation(() => ({ equals: () => ({ first: async () => undefined }) }));
|
||||||
|
categoriesDb.get.mockResolvedValue(undefined); // isExistingId check -> undefined
|
||||||
|
categoriesDb.add.mockResolvedValue('new-cat-id');
|
||||||
|
|
||||||
|
productsDb.put.mockResolvedValue('prod-2');
|
||||||
|
productsDb.filter.mockImplementation(() => ({ delete: vi.fn() }));
|
||||||
|
|
||||||
|
const user = userEvent.setup();
|
||||||
|
render(
|
||||||
|
<MemoryRouter>
|
||||||
|
<ManageProductsScreen />
|
||||||
|
</MemoryRouter>,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Open edit for the product
|
||||||
|
await user.click(screen.getByLabelText(/edit another product/i));
|
||||||
|
|
||||||
|
// Find the category field corresponding to that row (value should be 'cat-old')
|
||||||
|
const categoryInputs = screen.getAllByLabelText(/category/i);
|
||||||
|
const categoryField = categoryInputs.find((i) => (i as HTMLInputElement).value === 'cat-old') as HTMLInputElement;
|
||||||
|
expect(categoryField).toBeDefined();
|
||||||
|
|
||||||
|
// Change category to a new name that doesn't exist
|
||||||
|
fireEvent.change(categoryField, { target: { value: 'New Category Name' } });
|
||||||
|
|
||||||
|
// Click product-row Save button
|
||||||
|
const saveButtons = screen.getAllByRole('button', { name: /save product/i });
|
||||||
|
const productSaveButton = saveButtons.find((b) => b.getAttribute('aria-label') === 'Save product' || b.querySelector('svg') !== null) ?? saveButtons[0];
|
||||||
|
expect(productSaveButton).toBeDefined();
|
||||||
|
await user.click(productSaveButton as HTMLElement);
|
||||||
|
|
||||||
|
// Wait for categories.add to be called (new category created) and product saved
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(categoriesDb.add).toHaveBeenCalled();
|
||||||
|
expect(productsDb.put).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -251,16 +251,31 @@ describe('ManageProductsScreen barcode lookup', () => {
|
|||||||
</MemoryRouter>,
|
</MemoryRouter>,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// open dialog and wait for it to be present
|
||||||
await openAddProductDialog(user);
|
await openAddProductDialog(user);
|
||||||
|
await screen.findByTestId('add-product-dialog');
|
||||||
|
|
||||||
await user.type(screen.getByLabelText(/name/i), 'existing product');
|
// click the close icon and wait for dialog to be removed
|
||||||
|
await user.click(screen.getByRole('button', { name: /close add product/i }));
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.queryByTestId('add-product-dialog')).not.toBeInTheDocument();
|
||||||
|
}, { timeout: 2000 });
|
||||||
|
|
||||||
const saveBtn = findSaveButton();
|
// reopen the dialog and wait for it to appear
|
||||||
expect(saveBtn).toBeTruthy();
|
await openAddProductDialog(user);
|
||||||
await user.click(saveBtn as HTMLElement);
|
const dialog = await screen.findByTestId('add-product-dialog', {}, { timeout: 2000 });
|
||||||
|
expect(dialog).toBeInTheDocument();
|
||||||
|
|
||||||
|
// find the MUI backdrop by test-id and click it
|
||||||
|
const backdrop = await screen.findByTestId('add-product-backdrop', {}, { timeout: 2000 });
|
||||||
|
expect(backdrop).toBeTruthy();
|
||||||
|
await user.click(backdrop);
|
||||||
|
|
||||||
|
// finally wait for dialog to be removed
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.queryByTestId('add-product-dialog')).not.toBeInTheDocument();
|
||||||
|
}, { timeout: 2000 });
|
||||||
|
|
||||||
expect(await screen.findByText(/product with this name already exists/i)).toBeVisible();
|
|
||||||
expect(mockDb.products.add).not.toHaveBeenCalled();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('informs the user when barcode lookup happens offline', async () => {
|
it('informs the user when barcode lookup happens offline', async () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user