From 9822cad8ebad81c554f1207ca05a773d2bdd097e Mon Sep 17 00:00:00 2001 From: beatz174-bit Date: Tue, 2 Dec 2025 17:22:09 +1000 Subject: [PATCH 1/3] Fix lint errors --- e2e/fixtures.ts | 14 +- e2e/picklist.spec.ts | 2 +- eslint.config.js | 12 ++ playwright.config.ts | 2 +- scripts/playwright-collect-coverage.cjs | 2 +- src/components/AddProductDialog.tsx | 27 +-- src/components/EditableEntityList.tsx | 10 +- src/components/ProductRow.additional.test.tsx | 2 +- src/db/migrations.ts | 10 +- .../ActivePickListScreen.additional.test.tsx | 13 +- .../ActivePickListScreen.more.test.tsx | 2 +- src/screens/ActivePickListScreen.test.tsx | 13 -- src/screens/ActivePickListScreen.tsx | 2 +- src/screens/ManageAreasScreen.tsx | 2 +- ...ManageCategoriesScreen.additional.test.tsx | 2 +- src/screens/ManageCategoriesScreen.tsx | 8 +- .../ManageProductsScreen.additional.test.tsx | 4 +- src/screens/ManageProductsScreen.test.tsx | 2 +- src/screens/ManageProductsScreen.tsx | 8 +- .../importExportService.additional.test.ts | 2 +- src/services/importExportService.ts | 175 ++++++++---------- src/test/makeNamedError.ts | 2 +- src/testUtils/mockDb.ts | 26 +-- src/testUtils/stubDownloads.ts | 10 +- 24 files changed, 171 insertions(+), 181 deletions(-) diff --git a/e2e/fixtures.ts b/e2e/fixtures.ts index 9c43afe..e3a9028 100644 --- a/e2e/fixtures.ts +++ b/e2e/fixtures.ts @@ -7,17 +7,15 @@ const coverageDir = path.join(process.cwd(), 'coverage-reports', 'e2e', '.nyc_ou async function writeCoverageFile(page: Page, testInfo: TestInfo) { try { - const coverage = await page.evaluate(() => (globalThis as any).__coverage__ ?? null); + const coverage = await page.evaluate( + () => (globalThis as { __coverage__?: unknown }).__coverage__ ?? null, + ); if (!coverage || Object.keys(coverage).length === 0) return; await fs.mkdir(coverageDir, { recursive: true }); const titleParts = - typeof testInfo.titlePath === 'function' - ? testInfo.titlePath() - : Array.isArray((testInfo as any).titlePath) - ? (testInfo as any).titlePath - : [testInfo.title]; + typeof testInfo.titlePath === 'function' ? testInfo.titlePath() : [testInfo.title]; const safeTitle = titleParts .filter(Boolean) .map((part) => part.replace(/[^a-zA-Z0-9-_]+/g, '_')) @@ -35,8 +33,8 @@ async function writeCoverageFile(page: Page, testInfo: TestInfo) { } export const test = base.extend({ - page: async ({ page }, use, testInfo) => { - await use(page); + page: async ({ page }, applyPageFixture, testInfo) => { + await applyPageFixture(page); await writeCoverageFile(page, testInfo); }, }); diff --git a/e2e/picklist.spec.ts b/e2e/picklist.spec.ts index df0d2fc..cdab014 100644 --- a/e2e/picklist.spec.ts +++ b/e2e/picklist.spec.ts @@ -1,4 +1,4 @@ -import { expect, test, Page } from './fixtures'; +import { expect, test } from './fixtures'; import { areaName, diff --git a/eslint.config.js b/eslint.config.js index 25e19b6..83df941 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -50,4 +50,16 @@ export default tseslint.config( }, }, }, + { + files: ['scripts/**/*.cjs'], + languageOptions: { + sourceType: 'commonjs', + globals: { + ...globals.node, + }, + }, + rules: { + '@typescript-eslint/no-require-imports': 'off', + }, + }, ); diff --git a/playwright.config.ts b/playwright.config.ts index d5dd01c..c17f5b7 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -1,4 +1,4 @@ -import { defineConfig, devices } from '@playwright/test'; +import { defineConfig } from '@playwright/test'; import { defineCoverageReporterConfig } from '@bgotink/playwright-coverage'; import path from 'path'; diff --git a/scripts/playwright-collect-coverage.cjs b/scripts/playwright-collect-coverage.cjs index 8a4af70..ae25007 100755 --- a/scripts/playwright-collect-coverage.cjs +++ b/scripts/playwright-collect-coverage.cjs @@ -3,7 +3,7 @@ const fs = require('fs'); const path = require('path'); const { test } = require('@playwright/test'); -test.afterEach(async ({ page }, testInfo) => { +test.afterEach(async ({ page }) => { try { // evaluate coverage from the page const coverage = await page.evaluate(() => (globalThis.__coverage__ || {})); diff --git a/src/components/AddProductDialog.tsx b/src/components/AddProductDialog.tsx index 393baee..9c43fbe 100644 --- a/src/components/AddProductDialog.tsx +++ b/src/components/AddProductDialog.tsx @@ -17,7 +17,7 @@ import { v4 as uuidv4 } from 'uuid'; import { useDatabase } from '../context/DBProvider'; import { useProducts } from '../hooks/dataHooks'; import { BarcodeScannerView } from './BarcodeScannerView'; -import { ExternalProductInfo, fetchProductFromOFF } from '../modules/openFoodFacts'; +import { fetchProductFromOFF } from '../modules/openFoodFacts'; import { DEFAULT_BULK_NAME, DEFAULT_UNIT_TYPE, Product } from '../models/Product'; import type { BackdropProps } from '@mui/material/Backdrop'; import type { FormHelperTextProps } from '@mui/material/FormHelperText'; @@ -48,7 +48,6 @@ export const AddProductDialog = ({ const [nameError, setNameError] = useState(''); const [scannerOpen, setScannerOpen] = useState(false); const [lookupStatus, setLookupStatus] = useState<'idle' | 'loading' | 'found' | 'notfound' | 'offline'>('idle'); - const [externalProduct, setExternalProduct] = useState(null); const resetForm = useCallback(() => { setName(''); @@ -57,7 +56,6 @@ export const AddProductDialog = ({ setBarcodeError(''); setNameError(''); setLookupStatus('idle'); - setExternalProduct(null); setScannerOpen(false); }, []); @@ -73,19 +71,16 @@ export const AddProductDialog = ({ } else { resetForm(); } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [open, categoryOptions, initialBarcode]); + }, [category, categoryOptions, initialBarcode, lookupBarcode, open, resetForm]); useEffect(() => { if (!open || !barcode) return; void lookupBarcode(barcode); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [open, barcode]); + }, [barcode, lookupBarcode, open]); useEffect(() => { if (!barcode) { setLookupStatus('idle'); - setExternalProduct(null); } setBarcodeError(''); }, [barcode]); @@ -185,26 +180,23 @@ export const AddProductDialog = ({ [db.pickItems, db.pickLists, db.categories], ); - async function lookupBarcode(code: string) { + const lookupBarcode = useCallback(async (code: string) => { if (!code) return; if (typeof navigator !== 'undefined' && 'onLine' in navigator && navigator.onLine === false) { setLookupStatus('offline'); - setExternalProduct(null); return; } setLookupStatus('loading'); const result = await fetchProductFromOFF(code); if (result) { - setExternalProduct(result); setLookupStatus('found'); if (result.name) { setName(result.name || ''); } } else { - setExternalProduct(null); setLookupStatus('notfound'); } - } + }, []); const handleSubmit = async () => { setNameError(''); @@ -253,16 +245,17 @@ export const AddProductDialog = ({ onFeedback?.({ text: 'Product added.', severity: 'success' }); resetForm(); onClose(); - } catch (err: any) { - if (err?.name === 'DuplicateNameError') { + } catch (err: unknown) { + if (err instanceof Error && err.name === 'DuplicateNameError') { setNameError(err.message || 'A product with this name already exists.'); return; } - if (err?.name === 'DuplicateBarcodeError') { + if (err instanceof Error && err.name === 'DuplicateBarcodeError') { setBarcodeError(err.message || 'This barcode is already assigned to another product.'); return; } - onFeedback?.({ text: `Failed to add product: ${err?.message ?? String(err)}`, severity: 'error' }); + const fallbackMessage = err instanceof Error ? err.message : String(err); + onFeedback?.({ text: `Failed to add product: ${fallbackMessage}`, severity: 'error' }); } }; diff --git a/src/components/EditableEntityList.tsx b/src/components/EditableEntityList.tsx index 6fd23ec..a6bc4ee 100644 --- a/src/components/EditableEntityList.tsx +++ b/src/components/EditableEntityList.tsx @@ -4,7 +4,6 @@ import DeleteIcon from '@mui/icons-material/Delete'; import EditIcon from '@mui/icons-material/Edit'; import { Alert, - AlertColor, Button, IconButton, List, @@ -70,7 +69,8 @@ export const EditableEntityList = ({ setNewName(''); } } catch (error) { - setFeedback({ text: `Unable to add ${entityLabel.toLowerCase()}.`, severity: 'error' }); + const message = error instanceof Error ? error.message : String(error); + setFeedback({ text: `Unable to add ${entityLabel.toLowerCase()}: ${message}`, severity: 'error' }); } }; @@ -97,7 +97,8 @@ export const EditableEntityList = ({ cancelEditing(); } } catch (error) { - setFeedback({ text: `Unable to update ${entityLabel.toLowerCase()}.`, severity: 'error' }); + const message = error instanceof Error ? error.message : String(error); + setFeedback({ text: `Unable to update ${entityLabel.toLowerCase()}: ${message}`, severity: 'error' }); } }; @@ -115,7 +116,8 @@ export const EditableEntityList = ({ }); } } catch (error) { - setFeedback({ text: `Unable to delete ${entityLabel.toLowerCase()}.`, severity: 'error' }); + const message = error instanceof Error ? error.message : String(error); + setFeedback({ text: `Unable to delete ${entityLabel.toLowerCase()}: ${message}`, severity: 'error' }); } }; diff --git a/src/components/ProductRow.additional.test.tsx b/src/components/ProductRow.additional.test.tsx index 558046a..caeef51 100644 --- a/src/components/ProductRow.additional.test.tsx +++ b/src/components/ProductRow.additional.test.tsx @@ -1,6 +1,6 @@ // src/components/ProductRow.additional.test.tsx import React from 'react'; -import { render, screen, fireEvent, waitFor, cleanup } from '@testing-library/react'; +import { render, screen, waitFor, cleanup } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { describe, it, expect, vi, afterEach } from 'vitest'; diff --git a/src/db/migrations.ts b/src/db/migrations.ts index f404f49..006ad75 100644 --- a/src/db/migrations.ts +++ b/src/db/migrations.ts @@ -1,6 +1,7 @@ // src/db/migrations.ts -import { StockFillDB } from './index'; import { v4 as uuidv4 } from 'uuid'; +import type { PickList } from '../models/PickList'; +import { StockFillDB } from './index'; export const applyMigrations = async (db: StockFillDB) => { // Ensure DB is open and ready @@ -58,9 +59,10 @@ export const applyMigrations = async (db: StockFillDB) => { const pickLists = await db.pickLists.toArray(); await Promise.all( pickLists.map(async (pl) => { - if (!Array.isArray((pl as any).categories)) return; + const categories = (pl as PickList).categories; + if (!Array.isArray(categories)) return; - const newCats = (pl as any).categories.map((entry: string) => { + const newCats = categories.map((entry: string) => { // If the entry is already an id we know, keep it if (updatedById.has(entry)) return entry; // If entry is a name, return its id (if exists) @@ -71,7 +73,7 @@ export const applyMigrations = async (db: StockFillDB) => { }); // Update only if changed - if (JSON.stringify(newCats) !== JSON.stringify((pl as any).categories)) { + if (JSON.stringify(newCats) !== JSON.stringify(categories)) { await db.pickLists.update(pl.id, { categories: newCats }); } }), diff --git a/src/screens/ActivePickListScreen.additional.test.tsx b/src/screens/ActivePickListScreen.additional.test.tsx index add78da..d4cdbb7 100644 --- a/src/screens/ActivePickListScreen.additional.test.tsx +++ b/src/screens/ActivePickListScreen.additional.test.tsx @@ -1,11 +1,12 @@ // 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 { render, screen, 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'; +import { Category } from '../models/Category'; const addMock = vi.fn(); const updateMock = vi.fn(); @@ -15,7 +16,7 @@ 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 }]; +let categoriesVar: Category[] = [{ id: 'cat-1', name: 'Drinks', created_at: 0, updated_at: 0 }]; vi.mock('../hooks/dataHooks', () => ({ usePickItems: () => pickItemsMock(), @@ -35,7 +36,7 @@ vi.mock('../context/DBProvider', () => ({ return items.find((it) => it.id === id); }), delete: deleteMock, - where: (_f: string) => ({ + where: () => ({ equals: (val: any) => ({ toArray: async () => { const items: PickItem[] = pickItemsMock() ?? []; @@ -96,12 +97,6 @@ beforeEach(() => { }); 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([ { diff --git a/src/screens/ActivePickListScreen.more.test.tsx b/src/screens/ActivePickListScreen.more.test.tsx index 888478f..eeb506f 100644 --- a/src/screens/ActivePickListScreen.more.test.tsx +++ b/src/screens/ActivePickListScreen.more.test.tsx @@ -29,7 +29,7 @@ vi.mock('../context/DBProvider', () => ({ update: updateMock, get: vi.fn(async (id: string) => (pickItemsMock() || []).find((it) => it.id === id)), delete: deleteMock, - where: (_f: string) => ({ + where: () => ({ equals: (val: any) => ({ toArray: async () => (pickItemsMock() || []).filter((it) => it.pick_list_id === val), count: async () => (pickItemsMock() || []).filter((it) => it.pick_list_id === val).length, diff --git a/src/screens/ActivePickListScreen.test.tsx b/src/screens/ActivePickListScreen.test.tsx index 0b4c859..987459c 100644 --- a/src/screens/ActivePickListScreen.test.tsx +++ b/src/screens/ActivePickListScreen.test.tsx @@ -79,17 +79,6 @@ describe('ActivePickListScreen product search', () => { * - otherwise fall back to placeholder 'Search products'. */ - - // helper to get the packaging radio input. Tests were expecting to call .querySelector('input') - // on a wrapper with data-testid; preserve that behavior but return the actual radio element. - const getPackagingRadioInput = (testId: 'packaging-filter-all' | 'packaging-filter-units' | 'packaging-filter-cartons') => { - const wrapper = screen.getByTestId(testId); - // FormControlLabel renders the input nested — find it - const input = (wrapper as HTMLElement).querySelector('input'); - if (!input) throw new Error(`Could not find input inside ${testId}`); - return input; - }; - // Keep the original getRadio shape for minimal change const getRadio = (testId: string) => { // try testid wrapper -> radio inside, otherwise find radio by label name @@ -137,8 +126,6 @@ describe('ActivePickListScreen product search', () => { await user.click(combobox); await user.type(combobox, 'cola'); - const listbox = await screen.findByRole('listbox'); - expect(await screen.findByRole('option', { name: /cola \(drinks\)/i })).toBeVisible(); expect(screen.queryByRole('option', { name: /chips \(snacks\)/i })).not.toBeInTheDocument(); }); diff --git a/src/screens/ActivePickListScreen.tsx b/src/screens/ActivePickListScreen.tsx index 25f51eb..1722f49 100644 --- a/src/screens/ActivePickListScreen.tsx +++ b/src/screens/ActivePickListScreen.tsx @@ -489,7 +489,7 @@ const ActivePickListScreen = () => { SelectProps={{ native: true }} label="Filter by category" value={categoryFilter} - onChange={(e) => setCategoryFilter((e.target.value as any) ?? 'all')} + onChange={(e) => setCategoryFilter(e.target.value || 'all')} sx={{ width: { xs: '100%', sm: 240 }, ml: { xs: 0, sm: 2 }, mt: { xs: 1, sm: 0 } }} > diff --git a/src/screens/ManageAreasScreen.tsx b/src/screens/ManageAreasScreen.tsx index 911e823..5c72e22 100644 --- a/src/screens/ManageAreasScreen.tsx +++ b/src/screens/ManageAreasScreen.tsx @@ -44,7 +44,7 @@ const ManageAreasScreen = () => { entities={areas.map((area) => ({ id: area.id, name: area.name }))} onAdd={addArea} onUpdate={saveArea} - onDelete={(areaId, areaName) => deleteArea(areaId)} + onDelete={(areaId) => deleteArea(areaId)} /> ); diff --git a/src/screens/ManageCategoriesScreen.additional.test.tsx b/src/screens/ManageCategoriesScreen.additional.test.tsx index df9e5e8..49c5578 100644 --- a/src/screens/ManageCategoriesScreen.additional.test.tsx +++ b/src/screens/ManageCategoriesScreen.additional.test.tsx @@ -36,7 +36,7 @@ vi.mock('../context/DBProvider', () => ({ categories: { add: categoryAddMock, update: categoryUpdateMock, delete: categoryDeleteMock }, products: { where: () => ({ - equals: (_: string) => ({ + equals: () => ({ count: async () => 0, modify: async (changes: any) => productModifyMock(changes), }), diff --git a/src/screens/ManageCategoriesScreen.tsx b/src/screens/ManageCategoriesScreen.tsx index 81de25d..8132f1a 100644 --- a/src/screens/ManageCategoriesScreen.tsx +++ b/src/screens/ManageCategoriesScreen.tsx @@ -4,6 +4,7 @@ import { v4 as uuidv4 } from 'uuid'; import { ActionOutcome, EditableEntityList } from '../components/EditableEntityList'; import { useDatabase } from '../context/DBProvider'; import { useCategories, useProducts } from '../hooks/dataHooks'; +import type { PickList } from '../models/PickList'; const ManageCategoriesScreen = () => { const db = useDatabase(); @@ -55,10 +56,11 @@ const ManageCategoriesScreen = () => { const pickLists = await db.pickLists.toArray(); await Promise.all( pickLists.map(async (pickList) => { - if (!Array.isArray((pickList as any).categories)) return; - const needsUpdate = (pickList as any).categories.includes(category.name); + const pickListCategories = (pickList as PickList).categories; + if (!Array.isArray(pickListCategories)) return; + const needsUpdate = pickListCategories.includes(category.name); if (!needsUpdate) return; - const updatedCategories = (pickList as any).categories.map((existing: string) => + const updatedCategories = pickListCategories.map((existing: string) => existing === category.name ? trimmed : existing, ); await db.pickLists.update(pickList.id, { categories: updatedCategories }); diff --git a/src/screens/ManageProductsScreen.additional.test.tsx b/src/screens/ManageProductsScreen.additional.test.tsx index 27ce4f3..f4a5afb 100644 --- a/src/screens/ManageProductsScreen.additional.test.tsx +++ b/src/screens/ManageProductsScreen.additional.test.tsx @@ -83,8 +83,8 @@ describe('ManageProductsScreen update product category creation branch', () => { productsDb.get.mockResolvedValue(existingProduct); // products.where('barcode').equals(value).first() must exist for uniqueness check - productsDb.where.mockImplementation((field?: string) => ({ - equals: (value?: string) => ({ + productsDb.where.mockImplementation(() => ({ + equals: () => ({ first: async () => undefined, }), })); diff --git a/src/screens/ManageProductsScreen.test.tsx b/src/screens/ManageProductsScreen.test.tsx index 32604a9..fd976cc 100644 --- a/src/screens/ManageProductsScreen.test.tsx +++ b/src/screens/ManageProductsScreen.test.tsx @@ -483,7 +483,7 @@ describe('ManageProductsScreen auto-adding products to pick lists', () => { // ensure the categories DB has a 'Snacks' row mockDb.categories.toArray.mockResolvedValue([{ id: 'cat-1', name: 'Snacks', created_at: 0, updated_at: 0 }]); mockDb.categories.where.mockImplementation(() => ({ - equals: (value: string) => ({ + equals: () => ({ first: async () => ({ id: 'cat-1', name: 'Snacks', created_at: 0, updated_at: 0 }), }), })); diff --git a/src/screens/ManageProductsScreen.tsx b/src/screens/ManageProductsScreen.tsx index 2fc071a..598785e 100644 --- a/src/screens/ManageProductsScreen.tsx +++ b/src/screens/ManageProductsScreen.tsx @@ -112,7 +112,6 @@ const ManageProductsScreen = () => { setPendingBarcode(state.newBarcode); setAddProductDialogOpen(true); } - // eslint-disable-next-line react-hooks/exhaustive-deps }, [location.state]); const updateProduct = async ( @@ -172,14 +171,15 @@ const ManageProductsScreen = () => { ); setFeedback({ text: 'Product updated.', severity: 'success' }); - } catch (err: any) { + } catch (err: unknown) { console.error('Failed to update product', err); - if (err?.name === 'DuplicateNameError' || err?.name === 'DuplicateBarcodeError') { + if (err instanceof Error && (err.name === 'DuplicateNameError' || err.name === 'DuplicateBarcodeError')) { throw err; } - setFeedback({ text: `Failed to update product: ${err?.message ?? String(err)}`, severity: 'error' }); + const message = err instanceof Error ? err.message : String(err); + setFeedback({ text: `Failed to update product: ${message}`, severity: 'error' }); } }; diff --git a/src/services/importExportService.additional.test.ts b/src/services/importExportService.additional.test.ts index 4a8f166..0f65db9 100644 --- a/src/services/importExportService.additional.test.ts +++ b/src/services/importExportService.additional.test.ts @@ -1,5 +1,5 @@ // src/services/importExportService.additional.test.ts -import { describe, it, expect, vi } from 'vitest'; +import { describe, it, expect } from 'vitest'; import JSZip from 'jszip'; import { importFiles } from './importExportService'; import { createMockDb } from '../testUtils/mockDb'; diff --git a/src/services/importExportService.ts b/src/services/importExportService.ts index 543fe37..7a1ab9e 100644 --- a/src/services/importExportService.ts +++ b/src/services/importExportService.ts @@ -3,13 +3,8 @@ import Papa from 'papaparse'; import { v4 as uuidv4 } from 'uuid'; import { StockFillDB } from '../db'; import { ImportExportLog, ImportExportLogSummary } from '../models/ImportExportLog'; -import { Area } from '../models/Area'; -import { Category } from '../models/Category'; -import { PickItem, PickItemStatus } from '../models/PickItem'; -import { PickList } from '../models/PickList'; import { Product, DEFAULT_BULK_NAME, DEFAULT_UNIT_TYPE } from '../models/Product'; import { normalizeName, inferTypeFromName } from '../utils/stringUtils'; -import { coerceBoolean, coerceNumber } from '../utils/convUtils'; import { triggerDownload } from '../platform/web'; export { normalizeName, inferTypeFromName } from '../utils/stringUtils'; export { coerceBoolean, coerceNumber } from '../utils/convUtils'; @@ -56,7 +51,6 @@ const readFiles = async (files: File[]) => { if (file.name.toLowerCase().endsWith('.zip')) { const zip = await JSZip.loadAsync(file); const entries = Object.values(zip.files).filter((entry) => !entry.dir); - // eslint-disable-next-line no-await-in-loop for (const entry of entries) { const content = await entry.async('string'); parsed.push({ name: entry.name, content }); @@ -244,25 +238,25 @@ export const importFiles = async ( const parsedRows: Partial[]>> = {}; -let productRows: Record[] = []; + const productRows: Record[] = []; -for (const file of parsedFiles) { - const rows = await parseCsv(file.content); - addDetail(`Parsed ${rows.length} rows from ${file.name}`); - if (!rows || rows.length === 0) continue; + for (const file of parsedFiles) { + const rows = await parseCsv(file.content); + addDetail(`Parsed ${rows.length} rows from ${file.name}`); + if (!rows || rows.length === 0) continue; - // Look at headers of first row to decide if it's product-centric - const headerKeys = Object.keys(rows[0]).map(h => h.trim().toLowerCase()); - const isProductCentric = - headerKeys.includes('category') && (headerKeys.includes('name') || headerKeys.includes('product_name')); + // Look at headers of first row to decide if it's product-centric + const headerKeys = Object.keys(rows[0]).map((header) => header.trim().toLowerCase()); + const isProductCentric = + headerKeys.includes('category') && (headerKeys.includes('name') || headerKeys.includes('product_name')); - if (isProductCentric) { - // Collect product rows for product-centric import - productRows.push(...rows); - // record that we detected products (for logging later) - if (!selectedTypes.includes('products')) selectedTypes.push('products'); - } else { - // Fallback to filename-based inference for older templates + if (isProductCentric) { + // Collect product rows for product-centric import + productRows.push(...rows); + // record that we detected products (for logging later) + if (!selectedTypes.includes('products')) selectedTypes.push('products'); + } else { + // Fallback to filename-based inference for older templates const type = inferTypeFromName(file.name) as DataType | undefined; if (!type) { addDetail(`Skipped ${file.name}: not product-centric and could not infer data type`); @@ -306,82 +300,77 @@ for (const file of parsedFiles) { }); try { - await db.transaction( - 'rw', - [db.categories, db.products], - async () => { -if (productRows.length > 0) { - // 1) Make a unique list of category names from CSV (normalized) - const uniqueCategoryNames = new Map(); // normalized -> raw - for (const row of productRows) { - const rawCat = (row.category ?? '').trim(); - if (!rawCat) continue; - const norm = normalizeName(rawCat); - if (norm && !uniqueCategoryNames.has(norm)) uniqueCategoryNames.set(norm, rawCat); - } + await db.transaction('rw', [db.categories, db.products], async () => { + if (productRows.length > 0) { + // 1) Make a unique list of category names from CSV (normalized) + const uniqueCategoryNames = new Map(); // normalized -> raw + for (const row of productRows) { + const rawCat = (row.category ?? '').trim(); + if (!rawCat) continue; + const norm = normalizeName(rawCat); + if (norm && !uniqueCategoryNames.has(norm)) uniqueCategoryNames.set(norm, rawCat); + } - // 2) Create missing categories in DB - for (const [norm, raw] of uniqueCategoryNames) { - if (!categoryNameToId.has(norm)) { - const newId = uuidv4(); - categoryNameToId.set(norm, newId); - await db.categories.add({ - id: newId, - name: raw, - created_at: now, - updated_at: now, - }); - addDetail(`Created category "${raw}"`); - } - } + // 2) Create missing categories in DB + for (const [norm, raw] of uniqueCategoryNames) { + if (!categoryNameToId.has(norm)) { + const newId = uuidv4(); + categoryNameToId.set(norm, newId); + await db.categories.add({ + id: newId, + name: raw, + created_at: now, + updated_at: now, + }); + addDetail(`Created category "${raw}"`); + } + } - // 3) Create products, linking to categories - for (const row of productRows) { - const nameRaw = (row.product_name ?? row.name ?? '').trim(); - const name = normalizeName(nameRaw); - if (!name) { - addDetail('Skipped product with empty name'); - skipped += 1; - continue; - } - if (productNameToId.has(name)) { - addDetail(`Product "${nameRaw}" exists, skipping`); - skipped += 1; - continue; - } + // 3) Create products, linking to categories + for (const row of productRows) { + const nameRaw = (row.product_name ?? row.name ?? '').trim(); + const name = normalizeName(nameRaw); + if (!name) { + addDetail('Skipped product with empty name'); + skipped += 1; + continue; + } + if (productNameToId.has(name)) { + addDetail(`Product "${nameRaw}" exists, skipping`); + skipped += 1; + continue; + } - const catRaw = (row.category ?? '').trim(); - const categoryId = catRaw ? categoryNameToId.get(normalizeName(catRaw)) : undefined; + const catRaw = (row.category ?? '').trim(); + const categoryId = catRaw ? categoryNameToId.get(normalizeName(catRaw)) : undefined; - const barcode = row.barcode?.trim(); - if (barcode && barcodeToProductId.has(barcode)) { - addDetail(`Barcode ${barcode} already exists, clearing for product "${nameRaw}"`); - } + const barcode = row.barcode?.trim(); + if (barcode && barcodeToProductId.has(barcode)) { + addDetail(`Barcode ${barcode} already exists, clearing for product "${nameRaw}"`); + } - const id = uuidv4(); - const product: Product = { - id, - name: nameRaw, - category: categoryId ?? '', - unit_type: DEFAULT_UNIT_TYPE, - bulk_name: DEFAULT_BULK_NAME, - barcode: barcode && !barcodeToProductId.has(barcode) ? barcode : undefined, - archived: false, - created_at: now, - updated_at: now, - }; - await db.products.add(product); - productNameToId.set(name, id); - if (product.barcode) { - barcodeToProductId.set(product.barcode, id); - } - inserted += 1; - addDetail(`Created product "${product.name}"`); - } -} - - }, - ); + const id = uuidv4(); + const product: Product = { + id, + name: nameRaw, + category: categoryId ?? '', + unit_type: DEFAULT_UNIT_TYPE, + bulk_name: DEFAULT_BULK_NAME, + barcode: barcode && !barcodeToProductId.has(barcode) ? barcode : undefined, + archived: false, + created_at: now, + updated_at: now, + }; + await db.products.add(product); + productNameToId.set(name, id); + if (product.barcode) { + barcodeToProductId.set(product.barcode, id); + } + inserted += 1; + addDetail(`Created product "${product.name}"`); + } + } + }); } catch (error) { errors += 1; addDetail(`Import failed: ${(error as Error).message}`); diff --git a/src/test/makeNamedError.ts b/src/test/makeNamedError.ts index 80f419f..c3f9c83 100644 --- a/src/test/makeNamedError.ts +++ b/src/test/makeNamedError.ts @@ -1,5 +1,5 @@ export function makeNamedError(name: string, message?: string) { const error = new Error(message ?? name); - (error as any).name = name; + error.name = name; return error; } diff --git a/src/testUtils/mockDb.ts b/src/testUtils/mockDb.ts index 1fabe52..96806ad 100644 --- a/src/testUtils/mockDb.ts +++ b/src/testUtils/mockDb.ts @@ -4,6 +4,7 @@ import type { Category } from '../models/Category'; import type { Area } from '../models/Area'; import type { PickList } from '../models/PickList'; import type { PickItem } from '../models/PickItem'; +import type { ImportExportLog } from '../models/ImportExportLog'; export class MockTable { items: T[]; @@ -39,19 +40,19 @@ export class MockTable { this.items = this.items.filter((i) => i.id !== id); } - where(field: string) { + where(field: K) { return { - equals: (val: any) => ({ - first: async () => this.items.find((it: any) => it[field] === val), - count: async () => this.items.filter((it: any) => it[field] === val).length, - filter: (pred: (it: any) => boolean) => ({ - first: async () => this.items.find((it: any) => it[field] === val && pred(it)), + equals: (val: T[K]) => ({ + first: async () => this.items.find((it) => it[field] === val), + count: async () => this.items.filter((it) => it[field] === val).length, + filter: (pred: (it: T) => boolean) => ({ + first: async () => this.items.find((it) => it[field] === val && pred(it)), }), }), }; } - filter(pred: (it: any) => boolean) { + filter(pred: (it: T) => boolean) { const filtered = this.items.filter(pred); return { delete: async () => { @@ -68,7 +69,7 @@ export const createMockDb = (data?: { areas?: Area[]; pickLists?: PickList[]; pickItems?: PickItem[]; - importExportLogs?: any[]; + importExportLogs?: ImportExportLog[]; }) => { return { products: new MockTable(data?.products ?? []), @@ -76,9 +77,12 @@ export const createMockDb = (data?: { areas: new MockTable(data?.areas ?? []), pickLists: new MockTable(data?.pickLists ?? []), pickItems: new MockTable(data?.pickItems ?? []), - importExportLogs: new MockTable(data?.importExportLogs ?? []), - transaction: async (_mode: string, ...args: any[]) => { - const cb = args[args.length - 1]; + importExportLogs: new MockTable(data?.importExportLogs ?? []), + transaction: async ( + _mode: string, + ...args: Array | (() => unknown)> + ) => { + const cb = args.at(-1); if (typeof cb === 'function') return cb(); return undefined; }, diff --git a/src/testUtils/stubDownloads.ts b/src/testUtils/stubDownloads.ts index cd957e0..767967a 100644 --- a/src/testUtils/stubDownloads.ts +++ b/src/testUtils/stubDownloads.ts @@ -1,5 +1,11 @@ -export const stubDownloads = (vi: any) => { - const anchor = { href: '', download: '', click: vi.fn() } as any; +type VitestMocker = typeof import('vitest')['vi']; + +export const stubDownloads = (vi: VitestMocker) => { + const anchor: { href: string; download: string; click: ReturnType } = { + href: '', + download: '', + click: vi.fn(), + }; const createObjectURL = vi.fn(() => 'blob:url'); const revokeObjectURL = vi.fn(); vi.stubGlobal('document', { createElement: () => anchor }); From ba9e3f6c17108c9e34264985821998ea27b410b8 Mon Sep 17 00:00:00 2001 From: beatz174-bit Date: Tue, 2 Dec 2025 17:31:03 +1000 Subject: [PATCH 2/3] Fix barcode lookup initialization and unit test script --- package.json | 1 + src/components/AddProductDialog.tsx | 52 ++++++++++++++--------------- 2 files changed, 27 insertions(+), 26 deletions(-) diff --git a/package.json b/package.json index 46bd052..14299b8 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "preview": "vite preview", "lint": "eslint .", "test": "vitest", + "test:unit": "vitest run", "test:coverage": "vitest run --coverage", "test:e2e": "playwright test", "coverage:unit": "npm run test:coverage", diff --git a/src/components/AddProductDialog.tsx b/src/components/AddProductDialog.tsx index 9c43fbe..84b4a95 100644 --- a/src/components/AddProductDialog.tsx +++ b/src/components/AddProductDialog.tsx @@ -59,32 +59,6 @@ export const AddProductDialog = ({ setScannerOpen(false); }, []); - useEffect(() => { - if (open) { - if (categoryOptions.length > 0 && !categoryOptions.includes(category)) { - setCategory(categoryOptions[0]); - } - if (initialBarcode) { - setBarcode(initialBarcode); - void lookupBarcode(initialBarcode); - } - } else { - resetForm(); - } - }, [category, categoryOptions, initialBarcode, lookupBarcode, open, resetForm]); - - useEffect(() => { - if (!open || !barcode) return; - void lookupBarcode(barcode); - }, [barcode, lookupBarcode, open]); - - useEffect(() => { - if (!barcode) { - setLookupStatus('idle'); - } - setBarcodeError(''); - }, [barcode]); - const categoryMap = useMemo(() => new Map(categoryOptions.map((c) => [c, c])), [categoryOptions]); const findBarcodeConflict = useCallback( @@ -198,6 +172,32 @@ export const AddProductDialog = ({ } }, []); + useEffect(() => { + if (open) { + if (categoryOptions.length > 0 && !categoryOptions.includes(category)) { + setCategory(categoryOptions[0]); + } + if (initialBarcode) { + setBarcode(initialBarcode); + void lookupBarcode(initialBarcode); + } + } else { + resetForm(); + } + }, [category, categoryOptions, initialBarcode, lookupBarcode, open, resetForm]); + + useEffect(() => { + if (!open || !barcode) return; + void lookupBarcode(barcode); + }, [barcode, lookupBarcode, open]); + + useEffect(() => { + if (!barcode) { + setLookupStatus('idle'); + } + setBarcodeError(''); + }, [barcode]); + const handleSubmit = async () => { setNameError(''); setBarcodeError(''); From b286b1b8819072212acd672fbdc35cf1f69718f9 Mon Sep 17 00:00:00 2001 From: beatz174-bit Date: Tue, 2 Dec 2025 17:42:55 +1000 Subject: [PATCH 3/3] Fix mock DB transaction type constraint --- src/testUtils/mockDb.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/testUtils/mockDb.ts b/src/testUtils/mockDb.ts index 96806ad..1737a7e 100644 --- a/src/testUtils/mockDb.ts +++ b/src/testUtils/mockDb.ts @@ -80,7 +80,7 @@ export const createMockDb = (data?: { importExportLogs: new MockTable(data?.importExportLogs ?? []), transaction: async ( _mode: string, - ...args: Array | (() => unknown)> + ...args: Array | (() => unknown)> ) => { const cb = args.at(-1); if (typeof cb === 'function') return cb();