From 2fb27c820e3bac2869e8f1f3d58171dfb973e37e Mon Sep 17 00:00:00 2001 From: Leonardo Trevizo Date: Tue, 14 Oct 2025 16:30:31 -0600 Subject: [PATCH 1/4] Fix multiple Api calls Fixed the bug where multiple calls to the backend where made for no reason ## Key changes - Updated DataProvider - Added exposed component to interact with the one and only DataContext context. --- .../input-bars/InputSearch.tsx | 23 ++-- .../segment2-new_product/ProductForm.tsx | 4 +- .../segment3-table/InventoryTable.tsx | 21 ++-- .../InventoryTablePageSelector.tsx | 6 +- .../InventoryMetricsTable.tsx | 4 +- inventory-manager/src/context/DataContext.tsx | 112 +++++++++++------- 6 files changed, 102 insertions(+), 68 deletions(-) diff --git a/inventory-manager/src/components/page-content/segment1-search_product/input-bars/InputSearch.tsx b/inventory-manager/src/components/page-content/segment1-search_product/input-bars/InputSearch.tsx index b50783f..8a008bc 100644 --- a/inventory-manager/src/components/page-content/segment1-search_product/input-bars/InputSearch.tsx +++ b/inventory-manager/src/components/page-content/segment1-search_product/input-bars/InputSearch.tsx @@ -8,18 +8,21 @@ interface Props { } const InputSearch: React.FC = ({parameter}) => { - - const {setParams} = useSearchContext(); + const {name, category, setParams} = useSearchContext(); + const timerRef = React.useRef(null); function onChange(e: React.ChangeEvent) { const next = e.target.value; - if (!e.target.value) { - if (parameter === 'name') setParams({name: null}); - if (parameter === 'category') setParams({category: null}); - } else { - if (parameter === 'name') setParams({name: next}); - if (parameter === 'category') setParams({category: next}); - } + if (timerRef.current) window.clearTimeout(timerRef.current); + timerRef.current = window.setTimeout(() => { + if (!e.target.value) { + if (parameter === 'name') setParams({name: null}); + if (parameter === 'category') setParams({category: null}); + } else { + if (parameter === 'name' && next !== name) setParams({name: next}); + if (parameter === 'category' && next !== category) setParams({category: next}); + } + }, 400); } const getPlaceholder = (): string => { @@ -34,6 +37,6 @@ const InputSearch: React.FC = ({parameter}) => { onChange={onChange} /> ); -} +}; export default InputSearch; \ No newline at end of file diff --git a/inventory-manager/src/components/page-content/segment2-new_product/ProductForm.tsx b/inventory-manager/src/components/page-content/segment2-new_product/ProductForm.tsx index f6435f4..f5c0c69 100644 --- a/inventory-manager/src/components/page-content/segment2-new_product/ProductForm.tsx +++ b/inventory-manager/src/components/page-content/segment2-new_product/ProductForm.tsx @@ -4,7 +4,7 @@ import type {Product} from "../../../types/Product"; import {createProduct, updateProduct} from "../../../services/Requests"; import {useSearchContext} from "../../../context/SearchContext"; import dayjs from "dayjs"; -import {useProductsData} from "../../../context/DataContext"; +import {useDataContext} from "../../../context/DataContext"; interface ProductFormProps { @@ -17,7 +17,7 @@ interface ProductFormProps { const ProductForm: React.FC = ({initialValues, mode = "create", onClose}) => { const [form] = Form.useForm(); const {stockQuantity, setParams} = useSearchContext(); - const {categories} = useProductsData(); + const {categories} = useDataContext(); const handleSave = async (values: Product) => { diff --git a/inventory-manager/src/components/page-content/segment3-table/InventoryTable.tsx b/inventory-manager/src/components/page-content/segment3-table/InventoryTable.tsx index 0b1e457..c104406 100644 --- a/inventory-manager/src/components/page-content/segment3-table/InventoryTable.tsx +++ b/inventory-manager/src/components/page-content/segment3-table/InventoryTable.tsx @@ -4,7 +4,7 @@ import type {TableColumnsType, TableProps} from 'antd'; import type {Product} from "../../../types/Product"; import {SearchOutlined} from '@ant-design/icons'; import {useSearchContext} from "../../../context/SearchContext"; -import {useProductsData} from "../../../context/DataContext"; +import {useDataContext} from "../../../context/DataContext"; import {deleteProduct, markInStock, markOutOfStock} from "../../../services/Requests"; import ProductForm from "../segment2-new_product/ProductForm"; @@ -19,21 +19,28 @@ const InventoryTable: React.FC = () => { setEditingProduct(null); } - const {products, loading} = useProductsData(); const searchInput = useRef(null); - const {stockQuantity, page, setParams} = useSearchContext(); - + const {stockQuantity, name, category, page, setParams} = useSearchContext(); + const {products, loading} = useDataContext() const handleTableChange: TableProps['onChange'] = (_pagination, filters, sorter) => { setParams({page: ((page as number))}); - if ((filters.name !== undefined) && ((filters.name as unknown as string) !== '') && (filters.name !== null)) { + if ((filters.name !== undefined) && + ((filters.name as unknown as string) !== '') && + (filters.name !== null) && + (filters.name as unknown as string !== name)) { setParams({name: filters.name?.[0] as unknown as string}); } - if (filters.category !== undefined && (filters.category as unknown as string) !== '' && filters.category !== null) { + if (filters.category !== undefined && + (filters.category as unknown as string) !== '' && + filters.category !== null && + (filters.category as unknown as string !== category)) { setParams({category: filters.category?.[0] as unknown as string}); } - if (filters.stockQuantity?.[0] && filters.stockQuantity?.[0] !== null) { + if (filters.stockQuantity?.[0] && + filters.stockQuantity?.[0] !== null && + filters.stockQuantity?.[0] as unknown as number !== stockQuantity) { setParams({stockQuantity: filters.stockQuantity?.[0] as unknown as number}); } const sortObj = Array.isArray(sorter) ? sorter : [sorter]; diff --git a/inventory-manager/src/components/page-content/segment3-table/InventoryTablePageSelector.tsx b/inventory-manager/src/components/page-content/segment3-table/InventoryTablePageSelector.tsx index 89e962e..ed0afd0 100644 --- a/inventory-manager/src/components/page-content/segment3-table/InventoryTablePageSelector.tsx +++ b/inventory-manager/src/components/page-content/segment3-table/InventoryTablePageSelector.tsx @@ -1,18 +1,18 @@ import React from 'react'; import {Pagination, type PaginationProps} from 'antd'; import {useSearchContext} from "../../../context/SearchContext"; -import {useProductsData} from "../../../context/DataContext"; +import {useDataContext} from "../../../context/DataContext"; const InventoryTablePageSelector: React.FC = () => { const {page, setParams} = useSearchContext(); - const {total} = useProductsData(); + const {total} = useDataContext() const handlePageChange: PaginationProps['onChange'] = (pagination) => { setParams({page: ((pagination as number) - 1)}); } return ( { - const {summary} = useProductsData(); + const {summary} = useDataContext(); const columns: TableColumnsType = [ { diff --git a/inventory-manager/src/context/DataContext.tsx b/inventory-manager/src/context/DataContext.tsx index 1466ac2..866cac3 100644 --- a/inventory-manager/src/context/DataContext.tsx +++ b/inventory-manager/src/context/DataContext.tsx @@ -1,4 +1,4 @@ -import React, {createContext, useEffect, useState} from "react"; +import React, {createContext, useContext, useEffect, useRef, useState} from "react"; import type {CategorySummary, Product} from "../types/Product"; import {getCategories, getFilteredProducts, getProducts, getSummary} from "../services/Requests"; import {useSearchContext} from "./SearchContext"; @@ -13,9 +13,26 @@ interface ProductDataContextProps { refreshProducts: () => Promise; } -const DataContext = createContext(undefined); +const defaultValues = { + products: undefined, + loading: true, + error: null, + total: null, + categories: [], + summary: undefined, + refreshProducts: async () => { + }, +}; + +const DataContext = createContext(defaultValues); + +export const useDataContext = () => useContext(DataContext); export const DataProvider: React.FC<{ children: React.ReactNode }> = ({children}) => { + const {name, category, stockQuantity, page, sort} = useSearchContext(); + const lastKeyRef = (useRef(null)); + const inFlightRef = (useRef(false)); + const [products, setProducts] = useState(); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); @@ -23,48 +40,21 @@ export const DataProvider: React.FC<{ children: React.ReactNode }> = ({children} const [categories, setCategories] = useState([""]); const [summary, setSummary] = useState(); - - const refreshProducts = async () => { - setLoading(true); - try { - const fetched = await getProducts({page: 0}); - setProducts(fetched.products); - setTotal(fetched.totalPages); - const fetchedCategories = await getCategories(); - setCategories(fetchedCategories.data); - const fetchedSummary = await getSummary(); - setSummary(fetchedSummary.data); - - } catch (err: any) { - setError(err.message || "Unknown error"); - } finally { - setLoading(false); - } - }; - useEffect(() => { - refreshProducts().then(); - }, []); - - return ( - - {children} - - ); -}; + const paramsKey = JSON.stringify({ + name: name ?? null, + category: category ?? null, + stockQuantity: stockQuantity ?? null, + page: page ?? null, + sort: Array.isArray(sort) ? sort : (sort ?? null) + }); -export function useProductsData() { - const {name, category, stockQuantity, page, sort} = useSearchContext(); - - const [products, setProducts] = useState([]); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - const [total, setTotal] = useState(0); - const [categories, setCategories] = useState([""]); - const [summary, setSummary] = useState(); + if (lastKeyRef.current === paramsKey) return; + if (inFlightRef.current) return; + inFlightRef.current = true; + lastKeyRef.current = paramsKey; - useEffect(() => { const fetchData = async () => { setLoading(true); try { @@ -90,14 +80,48 @@ export function useProductsData() { setSummary(fetchedSummary.data); } catch (err: any) { - setError(err.message); + let msg: string; + if (typeof err === 'object' && err !== null) { + msg = + (err as any)?.response?.data?.message ?? + (err as any)?.response?.data ?? + (err as Error)?.message ?? + 'Unknown error'; + setError(String(msg)); + + } else { + msg = String(err); + } + setError(msg); } finally { setLoading(false); } }; - fetchData().then(); + fetchData().then(() => inFlightRef.current = false); }, [name, category, stockQuantity, page, sort]); - return {products, loading, error, total, categories, summary}; -} \ No newline at end of file + const refreshProducts = async () => { + setLoading(true); + try { + const fetched = await getProducts({page: 0}); + setProducts(fetched.products); + setTotal(fetched.totalPages); + const fetchedCategories = await getCategories(); + setCategories(fetchedCategories.data); + const fetchedSummary = await getSummary(); + setSummary(fetchedSummary.data); + + } catch (err: any) { + setError(err.message || "Unknown error"); + } finally { + setLoading(false); + } + }; + + return ( + + {children} + + ); +}; From 8dc72e35a64d4650ec1fba7d338fb88c6090a078 Mon Sep 17 00:00:00 2001 From: Leonardo Trevizo Date: Tue, 14 Oct 2025 16:37:12 -0600 Subject: [PATCH 2/4] Updated testing configuration Upgraded the testing configuration to be compatible with vitest new variables, and avoid problems in the future. ## Key changes - Updated gitignore to avoid constantly modifying package lock - Commiting for the last time package-lock changes - Added test parameters in app config to allow smooth testing - Added jsdom dependency to devDependencies -Added globals to frame config to allow testing, easier. Signed-off-by: Leonardo Trevizo --- inventory-manager/.gitignore | 1 + inventory-manager/package-lock.json | 523 +++++++++++++++++++++++++++- inventory-manager/package.json | 2 + inventory-manager/tsconfig.app.json | 5 +- inventory-manager/vite.config.ts | 6 + 5 files changed, 522 insertions(+), 15 deletions(-) diff --git a/inventory-manager/.gitignore b/inventory-manager/.gitignore index a547bf3..b02a1ff 100644 --- a/inventory-manager/.gitignore +++ b/inventory-manager/.gitignore @@ -11,6 +11,7 @@ node_modules dist dist-ssr *.local +package-lock.json # Editor directories and files .vscode/* diff --git a/inventory-manager/package-lock.json b/inventory-manager/package-lock.json index 183f5ac..90284e7 100644 --- a/inventory-manager/package-lock.json +++ b/inventory-manager/package-lock.json @@ -8,6 +8,7 @@ "name": "inventory-manager", "version": "0.0.0", "dependencies": { + "@testing-library/user-event": "^14.6.1", "antd": "^5.27.1", "axios": "^1.11.0", "dayjs": "^1.11.18", @@ -27,6 +28,7 @@ "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-refresh": "^0.4.22", "globals": "^16.4.0", + "jsdom": "^26.1.0", "typescript": "~5.9.3", "typescript-eslint": "^8.45.0", "vite": "^7.1.7", @@ -137,11 +139,31 @@ "react": ">=16.9.0" } }, + "node_modules/@asamuzakjp/css-color": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", + "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^2.1.3", + "@csstools/css-color-parser": "^3.0.9", + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3", + "lru-cache": "^10.4.3" + } + }, + "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, "node_modules/@babel/code-frame": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", @@ -293,7 +315,6 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -428,6 +449,121 @@ "node": ">=6.9.0" } }, + "node_modules/@csstools/color-helpers": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", + "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/css-calc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", + "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", + "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^5.1.0", + "@csstools/css-calc": "^2.1.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", + "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", + "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/@emotion/hash": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz", @@ -1646,7 +1782,6 @@ "version": "10.4.1", "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", - "dev": true, "license": "MIT", "peer": true, "dependencies": { @@ -1718,11 +1853,23 @@ } } }, + "node_modules/@testing-library/user-event": { + "version": "14.6.1", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz", + "integrity": "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==", + "license": "MIT", + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "peerDependencies": { + "@testing-library/dom": ">=7.21.4" + } + }, "node_modules/@types/aria-query": { "version": "5.0.4", "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", - "dev": true, "license": "MIT", "peer": true }, @@ -2262,6 +2409,16 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -2283,7 +2440,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, "license": "MIT", "peer": true, "engines": { @@ -2382,7 +2538,6 @@ "version": "5.3.0", "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", - "dev": true, "license": "Apache-2.0", "dependencies": { "dequal": "^2.0.3" @@ -2686,12 +2841,40 @@ "dev": true, "license": "MIT" }, + "node_modules/cssstyle": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz", + "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^3.2.0", + "rrweb-cssom": "^0.8.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", "license": "MIT" }, + "node_modules/data-urls": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/dayjs": { "version": "1.11.18", "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.18.tgz", @@ -2716,6 +2899,13 @@ } } }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, "node_modules/deep-eql": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", @@ -2746,7 +2936,6 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -2756,7 +2945,6 @@ "version": "0.5.16", "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", - "dev": true, "license": "MIT", "peer": true }, @@ -2781,6 +2969,19 @@ "dev": true, "license": "ISC" }, + "node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -3422,6 +3623,60 @@ "node": ">= 0.4" } }, + "node_modules/html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^3.1.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -3502,6 +3757,13 @@ "node": ">=0.12.0" } }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -3513,7 +3775,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true, "license": "MIT" }, "node_modules/js-yaml": { @@ -3529,6 +3790,46 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsdom": { + "version": "26.1.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz", + "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssstyle": "^4.2.1", + "data-urls": "^5.0.0", + "decimal.js": "^10.5.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.16", + "parse5": "^7.2.1", + "rrweb-cssom": "^0.8.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^5.1.1", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.1.1", + "ws": "^8.18.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", @@ -3653,7 +3954,6 @@ "version": "1.5.0", "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", - "dev": true, "license": "MIT", "peer": true, "bin": { @@ -3787,6 +4087,13 @@ "dev": true, "license": "MIT" }, + "node_modules/nwsapi": { + "version": "2.2.22", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.22.tgz", + "integrity": "sha512-ujSMe1OWVn55euT1ihwCI1ZcAaAU3nxUiDwfDQldc51ZXaB9m2AyOn6/jh1BLe2t/G8xd6uKG1UBF2aZJeg2SQ==", + "dev": true, + "license": "MIT" + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -3850,6 +4157,19 @@ "node": ">=6" } }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -3891,7 +4211,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, "license": "ISC" }, "node_modules/picomatch": { @@ -3950,7 +4269,6 @@ "version": "27.5.1", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", - "dev": true, "license": "MIT", "peer": true, "dependencies": { @@ -3966,7 +4284,6 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, "license": "MIT", "peer": true, "engines": { @@ -4650,7 +4967,6 @@ "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", - "dev": true, "license": "MIT", "peer": true }, @@ -4785,6 +5101,13 @@ "fsevents": "~2.3.2" } }, + "node_modules/rrweb-cssom": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", + "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", + "dev": true, + "license": "MIT" + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -4809,6 +5132,26 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, "node_modules/scheduler": { "version": "0.27.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", @@ -4965,6 +5308,13 @@ "node": ">=8" } }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, "node_modules/throttle-debounce": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-5.0.2.tgz", @@ -5066,6 +5416,26 @@ "node": ">=14.0.0" } }, + "node_modules/tldts": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", + "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^6.1.86" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", + "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", + "dev": true, + "license": "MIT" + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -5085,6 +5455,32 @@ "integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==", "license": "MIT" }, + "node_modules/tough-cookie": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", + "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^6.1.32" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/ts-api-utils": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", @@ -5412,6 +5808,66 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -5455,6 +5911,45 @@ "node": ">=0.10.0" } }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", diff --git a/inventory-manager/package.json b/inventory-manager/package.json index 3fa6ac2..448faf1 100644 --- a/inventory-manager/package.json +++ b/inventory-manager/package.json @@ -21,6 +21,7 @@ "@eslint/js": "^9.36.0", "@testing-library/jest-dom": "^6.8.0", "@testing-library/react": "^16.3.0", + "@testing-library/user-event": "^14.6.1", "@types/node": "^24.6.0", "@types/react": "^19.1.16", "@types/react-dom": "^19.1.9", @@ -29,6 +30,7 @@ "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-refresh": "^0.4.22", "globals": "^16.4.0", + "jsdom": "^26.1.0", "typescript": "~5.9.3", "typescript-eslint": "^8.45.0", "vite": "^7.1.7", diff --git a/inventory-manager/tsconfig.app.json b/inventory-manager/tsconfig.app.json index a9b5a59..efb587f 100644 --- a/inventory-manager/tsconfig.app.json +++ b/inventory-manager/tsconfig.app.json @@ -24,5 +24,8 @@ "noFallthroughCasesInSwitch": true, "noUncheckedSideEffectImports": true }, - "include": ["src"] + "include": [ + "src", + "node_modules/vitest/globals.d.ts" + ] } diff --git a/inventory-manager/vite.config.ts b/inventory-manager/vite.config.ts index 4217b13..bc89c30 100644 --- a/inventory-manager/vite.config.ts +++ b/inventory-manager/vite.config.ts @@ -16,4 +16,10 @@ export default defineConfig({ }, }, }, + test: { + environment: 'jsdom', + isolate: true, + env: { TZ: 'UTC' }, + sequence: { concurrent: false }, + }, }) \ No newline at end of file From dd66def4722708fa6c239651a9dc2b8a06f346cd Mon Sep 17 00:00:00 2001 From: Leonardo Trevizo Date: Tue, 14 Oct 2025 16:38:59 -0600 Subject: [PATCH 3/4] Context testing Added two more testing files to test the context, to make testing more robust. ## Key changes - Created DataContext.test.tsx - Created SearchContext.test.tsx Signed-off-by: Leonardo Trevizo --- .../src/context/DataContext.test.tsx | 175 ++++++++++++++++++ .../src/context/SearchContext.test.tsx | 125 +++++++++++++ 2 files changed, 300 insertions(+) create mode 100644 inventory-manager/src/context/DataContext.test.tsx create mode 100644 inventory-manager/src/context/SearchContext.test.tsx diff --git a/inventory-manager/src/context/DataContext.test.tsx b/inventory-manager/src/context/DataContext.test.tsx new file mode 100644 index 0000000..7e73056 --- /dev/null +++ b/inventory-manager/src/context/DataContext.test.tsx @@ -0,0 +1,175 @@ +/* @vitest-environment jsdom */ +import React from "react"; +import {describe, it, vi, expect, beforeEach, afterEach} from "vitest"; +import userEvent from '@testing-library/user-event' +import {cleanup, render, waitFor} from "@testing-library/react"; +import {SearchProvider, useSearchContext} from "./SearchContext.tsx"; +import {getCategories, getFilteredProducts, getSummary} from "../services/Requests.ts"; +import {DataProvider, useDataContext} from "./DataContext.tsx"; +import type {AxiosResponse} from "axios"; +import type {Product} from "../types/Product.ts"; + +vi.mock("../services/Requests", () => ( + { + getProducts: vi.fn(), + getCategories: vi.fn(), + getSummary: vi.fn(), + getFilteredProducts: vi.fn(), + createProduct: vi.fn(), + updateProduct: vi.fn(), + markOutOfStock: vi.fn(), + markInStock: vi.fn(), + deleteProduct: vi.fn(), + + } + ) +); + +const Consumer: React.FC = () => { + const {setParams} = useSearchContext(); + const {products, loading, total, error, categories, summary} = useDataContext(); + return ( +
+
{products ? "yes" : "no"}
+
{String(loading)}
+
{error ?? ""}
+
{total}
+
{categories?.length ?? 0}
+
{summary ? "yes" : "no"}
+ + +
+ ); +}; + +function renderWithProvider() { + const user = userEvent.setup() + const view = render( + + + + + ); + return {user, ...view}; +} + +beforeEach(() => { + vi.clearAllMocks(); +}); + +afterEach(() => { + vi.useRealTimers(); + cleanup(); + vi.resetAllMocks(); +}); + +describe("DataContext initial tests", () => { + it("starts with nothing", () => { + const {getByTestId} = renderWithProvider(); + expect(getByTestId("products").textContent).toBe("no"); + expect(getByTestId("loading").textContent).toBe("true"); + expect(getByTestId("error").textContent).toBe(""); + expect(getByTestId("total").textContent).toBe("0"); + expect(getByTestId("categories").textContent).toBe("1"); + expect(getByTestId("summary").textContent).toBe("no"); + }); +}); + +describe("DataContext triggers", () => { + it("trigger fetch", async () => { + vi.mocked(getFilteredProducts).mockResolvedValueOnce({ + data: { + products: [ + {id: 1, name: 'Watermelon'} + ], + totalPages: 1 + } + } as unknown as AxiosResponse); + const {user, getByTestId} = renderWithProvider(); + await user.click(getByTestId("trigger-fetch")); + + expect(vi.mocked(getFilteredProducts)).toHaveBeenCalledWith( + expect.objectContaining({ + name: "Watermelon", + category: "Fruit", + stockQuantity: 15, + page: 0, + })); + await waitFor(() => + expect(getByTestId("loading").textContent).toBe("false")); + expect(getByTestId("products").textContent).toBe("yes"); + expect(getByTestId("total").textContent).toBe("1"); + }) + + it("trigger fetches", async () => { + vi.mocked(getFilteredProducts).mockResolvedValueOnce({ + data: { + products: [ + {id: 1, name: 'Watermelon'}, + {id: 2, name: 'Water'}, + {id: 3, name: 'Apple'} + ], + totalPages: 1 + } + } as unknown as AxiosResponse); + const {user, getByTestId} = renderWithProvider(); + await user.click(getByTestId("trigger-fetches")); + + expect(vi.mocked(getFilteredProducts)).toHaveBeenCalledWith( + expect.objectContaining({name: "water"})); + await waitFor(() => + expect(getByTestId("loading").textContent).toBe("false")); + expect(getByTestId("products").textContent).toBe("yes"); + expect(getByTestId("total").textContent).toBe("1"); + }); + + it("error handling", async () => { + vi.mocked(getFilteredProducts).mockRejectedValueOnce(new Error("Network fail")); + + vi.mocked(getFilteredProducts).mockResolvedValueOnce({ + data: {products: [], totalPages: 0}, + } as unknown as AxiosResponse); + vi.mocked(getCategories).mockResolvedValue({data: []} as unknown as AxiosResponse); + vi.mocked(getSummary).mockResolvedValue({data: []} as unknown as AxiosResponse); + const {user, getByTestId} = renderWithProvider(); + + await user.click(getByTestId("trigger-fetch")); + + await waitFor(() => + expect(getByTestId("loading").textContent).toBe("false")); + expect(getByTestId("error").textContent).toMatch(/Network fail/i); + }); +}) + + + + + + + + + + + + diff --git a/inventory-manager/src/context/SearchContext.test.tsx b/inventory-manager/src/context/SearchContext.test.tsx new file mode 100644 index 0000000..3580aee --- /dev/null +++ b/inventory-manager/src/context/SearchContext.test.tsx @@ -0,0 +1,125 @@ +/* @vitest-environment jsdom */ +import React from "react"; +import {describe, it, expect, beforeEach, vi, afterEach} from "vitest"; +import userEvent from '@testing-library/user-event' +import {cleanup, render} from "@testing-library/react"; +import {SearchProvider, useSearchContext} from "./SearchContext.tsx"; + +const Consumer: React.FC = () => { + const clientSim = useSearchContext(); + return ( +
+
{clientSim.name}
+
{clientSim.category}
+
{clientSim.stockQuantity}
+
{clientSim.page}
+
{clientSim.sort}
+ + + +
+ ); +}; + +function renderWithProvider() { + const user = userEvent.setup() + const view = render( + + + ); + return {user, ...view}; +} + +beforeEach(() => { + vi.clearAllMocks(); +}); + +afterEach(() => { + vi.useRealTimers(); + cleanup(); + vi.resetAllMocks(); +}); + +describe("SearchContext tests", () => { + it("exposes initial state", () => { + const {getByTestId} = renderWithProvider(); + expect(getByTestId("name").textContent).toBe(""); + expect(getByTestId("category").textContent).toBe(""); + expect(getByTestId("stockQuantity").textContent).toBe('0'); + expect(getByTestId("page").textContent).toBe('0'); + }); + + it("set all to context", async () => { + const {user, getByRole, getByTestId} = renderWithProvider(); + await user.click(getByRole('button', {name: /setAll/i})); + + expect(getByTestId("name").textContent).toBe("Watermelon"); + expect(getByTestId("category").textContent).toBe("Fruits"); + expect(getByTestId("stockQuantity").textContent).toBe('15'); + expect(getByTestId("page").textContent).toBe('1'); + }); + + it("make small change to context", async () => { + const {user, getByRole, getByTestId} = renderWithProvider(); + await user.click(getByRole('button', {name: /setChange/i})); + + expect(getByTestId("name").textContent).toBe("water"); + expect(getByTestId("category").textContent).toBe(""); + expect(getByTestId("stockQuantity").textContent).toBe('0'); + expect(getByTestId("page").textContent).toBe('0'); + }); + + it("reset context state", async () => { + const {user, getByRole, getByTestId} = renderWithProvider(); + await user.click(getByRole('button', {name: /setIgnored/i})); + + expect(getByTestId("name").textContent).toBe(""); + expect(getByTestId("category").textContent).toBe(""); + expect(getByTestId("stockQuantity").textContent).toBe('0'); + expect(getByTestId("page").textContent).toBe('0'); + + const first = renderWithProvider(); + first.unmount() + + await first.user.click(first.getByRole('button', {name: /setAll/i})) + + expect(getByTestId("name").textContent).toBe("Watermelon"); + expect(getByTestId("category").textContent).toBe("Fruits"); + expect(getByTestId("stockQuantity").textContent).toBe('15'); + expect(getByTestId("page").textContent).toBe('1'); + + }); +}) From 93acceff75244d17acb8b6d9ed98573378129a3d Mon Sep 17 00:00:00 2001 From: Leonardo Trevizo Date: Wed, 15 Oct 2025 14:45:13 -0600 Subject: [PATCH 4/4] Testing working again Testing fixed after the migration from CRA to Vite. For this fix was necessary to strengthen the contexts and all the components that make contact with the Api <- Refactored these files. Added test script to package to allow npm to run the tests. ## Key changes - Fixes InventoryTablePageSelector.test.tsx, InventoryTable.test.tsx, NewProductButton.test.tsx, ProductForm.test.tsx. - Updated DataContext.test.tsx, SearchContext.test.tsx - Refactored for the new context management: - InventoryTable.tsx, - InventoryTableObj.tsx, - InventoryTablePageSelector.tsx - EncoraContent.tsx ## Breaking or pending changes - SearchBar component testing is still pending - Update documentation to the new version Signed-off-by: Leonardo Trevizo --- inventory-manager/package.json | 1 + .../components/page-content/EncoraContent.tsx | 2 +- .../NewProductButton.test.tsx | 87 ++++-- .../segment2-new_product/ProductForm.test.tsx | 151 +++++++--- .../segment3-table/InventoryTable.test.tsx | 140 --------- .../{segment => }/InventoryTableObj.tsx | 4 +- .../segment/InventoryTable.test.tsx | 239 ++++++++++++++++ .../{ => segment}/InventoryTable.tsx | 24 +- .../InventoryTablePageSelector.test.tsx | 267 ++++++++++++++++++ .../InventoryTablePageSelector.tsx | 5 +- .../src/context/DataContext.test.tsx | 31 +- .../src/context/SearchContext.test.tsx | 19 +- 12 files changed, 732 insertions(+), 238 deletions(-) delete mode 100644 inventory-manager/src/components/page-content/segment3-table/InventoryTable.test.tsx rename inventory-manager/src/components/page-content/segment3-table/{segment => }/InventoryTableObj.tsx (69%) create mode 100644 inventory-manager/src/components/page-content/segment3-table/segment/InventoryTable.test.tsx rename inventory-manager/src/components/page-content/segment3-table/{ => segment}/InventoryTable.tsx (93%) create mode 100644 inventory-manager/src/components/page-content/segment3-table/segment/InventoryTablePageSelector.test.tsx rename inventory-manager/src/components/page-content/segment3-table/{ => segment}/InventoryTablePageSelector.tsx (79%) diff --git a/inventory-manager/package.json b/inventory-manager/package.json index 448faf1..0c46d57 100644 --- a/inventory-manager/package.json +++ b/inventory-manager/package.json @@ -7,6 +7,7 @@ "dev": "vite", "build": "tsc -b && vite build", "lint": "eslint .", + "test": "vitest", "preview": "vite preview" }, "dependencies": { diff --git a/inventory-manager/src/components/page-content/EncoraContent.tsx b/inventory-manager/src/components/page-content/EncoraContent.tsx index 9e38c8a..30ecf3d 100644 --- a/inventory-manager/src/components/page-content/EncoraContent.tsx +++ b/inventory-manager/src/components/page-content/EncoraContent.tsx @@ -1,7 +1,7 @@ import React from 'react'; import {Col, Layout} from 'antd'; import NewProductButton from "./segment2-new_product/NewProductButton"; -import InventoryTableObj from "./segment3-table/segment/InventoryTableObj"; +import InventoryTableObj from "./segment3-table/InventoryTableObj"; import {SearchProvider} from "../../context/SearchContext"; import InventoryMetricsTable from "./segment4-metrics/InventoryMetricsTable"; import {DataProvider} from "../../context/DataContext"; diff --git a/inventory-manager/src/components/page-content/segment2-new_product/NewProductButton.test.tsx b/inventory-manager/src/components/page-content/segment2-new_product/NewProductButton.test.tsx index 9057d91..614d378 100644 --- a/inventory-manager/src/components/page-content/segment2-new_product/NewProductButton.test.tsx +++ b/inventory-manager/src/components/page-content/segment2-new_product/NewProductButton.test.tsx @@ -1,25 +1,72 @@ -global.matchMedia = global.matchMedia || function() { - return { - matches: false, - addListener: jest.fn(), - removeListener: jest.fn(), - addEventListener: jest.fn(), - removeEventListener: jest.fn(), - dispatchEvent: jest.fn(), - }; -}; +/* @vitest-environment jsdom */ +/// +import "@testing-library/jest-dom/vitest"; +import {describe, it, expect, vi, beforeAll, afterAll, beforeEach, afterEach} from "vitest"; +import {cleanup, render} from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import NewProductButton from "./NewProductButton"; -import React from 'react'; -import {render, screen, fireEvent} from '@testing-library/react'; -import NewProductButton from './NewProductButton'; +const __realGetComputedStyle = window.getComputedStyle; +const __realMatchMedia = window.matchMedia as any; -describe('NewProductButton', () => { - test('renders the button and opens the modal on click', () => { - render(); - const addButton = screen.getByText('Add new product'); - expect(addButton).toBeInTheDocument(); - fireEvent.click(addButton); - expect(screen.getByText('Add new product to inventory')).toBeInTheDocument(); +function renderWithUser() { + const user = userEvent.setup() + const view = render(); + return {user, ...view}; +} + + +describe("NewProductButton", () => { + beforeAll(() => { + window.getComputedStyle = (elt: Element, _pseudo?: string | null) => { + const style = __realGetComputedStyle(elt); + const gpv = (prop: string) => { + if (typeof (style as any).getPropertyValue === "function") { + return (style as any).getPropertyValue(prop); + } + if ( + prop === "animation-duration" || + prop === "animation-delay" || + prop === "transition-duration" || + prop === "transition-delay" + ) { + return "0s"; + } + return ""; + }; + return {...style, getPropertyValue: gpv} as CSSStyleDeclaration; + }; + window.matchMedia = (query: string) => ({ + matches: false, + media: query, + onchange: null, + addListener: vi.fn(), + removeListener: vi.fn(), + addEventListener: vi.fn(), + removeEventListener: vi.fn(), + dispatchEvent: vi.fn(), + }); + }); + afterAll(() => { + window.getComputedStyle = __realGetComputedStyle; + window.matchMedia = __realMatchMedia; + }); + beforeEach(() => { + vi.clearAllMocks(); }); + afterEach(() => { + vi.useRealTimers(); + cleanup(); + vi.resetAllMocks(); + }); + + it("renders the button and opens the modal on click", async () => { + const {getByRole, getByText} = renderWithUser(); + const addButton = getByRole("button", { name: /add new product/i }); + expect(addButton).toBeInTheDocument(); + await userEvent.click(addButton); + + expect(getByText(/add new product to inventory/i)).toBeInTheDocument(); + }); }); diff --git a/inventory-manager/src/components/page-content/segment2-new_product/ProductForm.test.tsx b/inventory-manager/src/components/page-content/segment2-new_product/ProductForm.test.tsx index 266eb32..d691d29 100644 --- a/inventory-manager/src/components/page-content/segment2-new_product/ProductForm.test.tsx +++ b/inventory-manager/src/components/page-content/segment2-new_product/ProductForm.test.tsx @@ -1,50 +1,123 @@ -global.matchMedia = global.matchMedia || function () { - return { - matches: false, - addListener: jest.fn(), - removeListener: jest.fn(), - addEventListener: jest.fn(), - removeEventListener: jest.fn(), - dispatchEvent: jest.fn(), - }; -}; +/* @vitest-environment jsdom */ +/// +import "@testing-library/jest-dom/vitest"; +import {describe, it, expect, vi, beforeAll, afterAll, beforeEach, afterEach} from "vitest"; +import {render, waitFor, cleanup} from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import {createProduct, updateProduct} from "../../../services/Requests"; +import ProductForm from "./ProductForm"; +import type {Product} from "../../../types/Product.ts"; -import React from 'react'; -import {render, screen, waitFor} from '@testing-library/react'; -import ProductForm from './ProductForm'; -import {useSearchContext} from '../../context/SearchContext'; -import {useProductsData} from '../../context/DataContext'; -import {createProduct} from '../../services/Requests'; -import userEvent from '@testing-library/user-event'; +const __realGetComputedStyle = window.getComputedStyle; +const __realMatchMedia = window.matchMedia as any; -jest.mock('../../context/SearchContext'); -jest.mock('../../context/DataContext'); -jest.mock('../../services/Requests'); +vi.mock("../../../services/Requests", () => ({ + createProduct: vi.fn(), + updateProduct: vi.fn() +})); -describe('ProductForm', () => { - beforeEach(() => { - (useSearchContext as jest.Mock).mockReturnValue({ - stockQuantity: 0, - setParams: jest.fn(), - }); - (useProductsData as jest.Mock).mockReturnValue({ - categories: ['Fruit', 'Vegetables'], +const productsCreate: Product = { + id: "p1", + name: "Coke", + category: "Beverages", + unitPrice: 1.5, + stockQuantity: 12, + creationDate: new Date("13-10-2025") +}; +const productsEdit: Product = { + id: "p1", + name: "Coke Light", + category: "Beverages", + unitPrice: 1.5, + stockQuantity: 12, + creationDate: new Date("13-10-2025") +}; + +function renderWithUsers(mode: "create" | "edit" | undefined, initialValues?: Product) { + const onClose = vi.fn(); + const user = userEvent.setup() + const view = render( + + ); + return {user, ...view}; +} + +describe("ProductForm", () => { + beforeAll(() => { + window.getComputedStyle = (elt: Element, _pseudo?: string | null) => { + const style = __realGetComputedStyle(elt); + const gpv = (prop: string) => { + if (typeof (style as any).getPropertyValue === "function") { + return (style as any).getPropertyValue(prop); + } + if ( + prop === "animation-duration" || + prop === "animation-delay" || + prop === "transition-duration" || + prop === "transition-delay" + ) { + return "0s"; + } + return ""; + }; + return {...style, getPropertyValue: gpv} as CSSStyleDeclaration; + }; + window.matchMedia = (query: string) => ({ + matches: false, + media: query, + onchange: null, + addListener: vi.fn(), + removeListener: vi.fn(), + addEventListener: vi.fn(), + removeEventListener: vi.fn(), + dispatchEvent: vi.fn(), }); }); + afterAll(() => { + window.getComputedStyle = __realGetComputedStyle; + window.matchMedia = __realMatchMedia; + }); + beforeEach(() => { + vi.clearAllMocks(); + vi.mocked(createProduct).mockResolvedValue({} as any); + }); + afterEach(() => { + cleanup(); + vi.resetAllMocks(); + }); + + it("submits form in create mode", async () => { + vi.mocked(createProduct).mockResolvedValueOnce(productsCreate); + const {getAllByText, getByLabelText, getByText} = renderWithUsers("create"); + + await userEvent.type(getByLabelText(/name/i), "Coke"); + await userEvent.click(getByLabelText(/category/i)); + await userEvent.type(getByLabelText(/category/i), "Beverage"); + const options = getAllByText("Beverage"); + await userEvent.click(options[options.length - 1]); + await userEvent.type(getByLabelText(/stock/i), "12"); + await userEvent.type(getByLabelText(/unit price/i), "1.5"); + await userEvent.click(getByText("Save")); - test('submits form in create mode', async () => { - const onClose = jest.fn(); - (createProduct as jest.Mock).mockResolvedValue({}); - render(); - await userEvent.type(screen.getByLabelText(/Name/i), 'Apple'); - await userEvent.click(screen.getByLabelText(/Category/i)); - const options = screen.getAllByText('Fruit'); - await userEvent.click(options[1]); - await userEvent.type(screen.getByLabelText(/Stock/i), '10'); - await userEvent.type(screen.getByLabelText(/Unit Price/i), '1'); - await userEvent.click(screen.getByText('Save')); await waitFor(() => { expect(createProduct).toHaveBeenCalled(); }); }); + + it("submits form in edit mode", async () => { + vi.mocked(updateProduct).mockResolvedValueOnce(productsEdit); + const {getByLabelText, getByText} = renderWithUsers("edit", productsEdit); + + await userEvent.type(getByLabelText(/name/i), "Coke Light"); + await userEvent.type(getByLabelText(/unit price/i), "1.55"); + await userEvent.click(getByText("Save")); + + await waitFor(() => { + expect(updateProduct).toHaveBeenCalled(); + }); + }); + }); diff --git a/inventory-manager/src/components/page-content/segment3-table/InventoryTable.test.tsx b/inventory-manager/src/components/page-content/segment3-table/InventoryTable.test.tsx deleted file mode 100644 index 8ce71d7..0000000 --- a/inventory-manager/src/components/page-content/segment3-table/InventoryTable.test.tsx +++ /dev/null @@ -1,140 +0,0 @@ -import {deleteProduct, markOutOfStock} from "../../../services/Requests"; - -global.matchMedia = global.matchMedia || function() { - return { - matches: false, - addListener: jest.fn(), - removeListener: jest.fn(), - addEventListener: jest.fn(), - removeEventListener: jest.fn(), - dispatchEvent: jest.fn(), - }; -}; - - -import React from 'react'; -import { render, screen, fireEvent } from '@testing-library/react'; -import InventoryTable from './InventoryTable'; -import { useProductsData } from '../../context/DataContext'; -import { useSearchContext } from '../../context/SearchContext'; -import {waitFor} from "@testing-library/dom"; - -jest.mock('../../context/DataContext'); -jest.mock('../../context/SearchContext'); -jest.mock('../../services/Requests'); - -beforeAll(() => { - jest.spyOn(console, 'error').mockImplementation((...args) => { - if ( - typeof args[0] === 'string' && - args[0].includes('Columns should all contain `filteredValue`') - ) { - return; - } - console.error(...args); - }); -}); - -beforeEach(() => { - (useProductsData as jest.Mock).mockReturnValue({ - products: [{ id: 1, category: 'Fruit', name: 'Apple', unitPrice: 1, expirationDate: '2023-06-22', stockQuantity: 10 }], - loading: false, - categories: ['Fruit', 'Vegetables'], - error: null, - total: 1, - summary: [], - refreshProducts: jest.fn(), - }); - (useSearchContext as jest.Mock).mockReturnValue({ - stockQuantity: 0, - page: 1, - setParams: jest.fn(), - }); -}); - -describe('InventoryTable', () => { - it('renders the table with products', async () => { - (useProductsData as jest.Mock).mockReturnValue({ - products: [ - { id: 1, category: 'Fruit', name: 'Apple', unitPrice: 1, expirationDate: '2023-06-22', stockQuantity: 10 }, - ], - loading: false, - }); - (useSearchContext as jest.Mock).mockReturnValue({ - stockQuantity: 0, - page: 1, - setParams: jest.fn(), - }); - - render(); - - expect(screen.getByText('Apple')).toBeInTheDocument(); - expect(screen.getByText('Name')).toBeInTheDocument(); - expect(screen.getByText('Fruit')).toBeInTheDocument(); - expect(screen.getByText('Category')).toBeInTheDocument(); - expect(screen.getByText('10')).toBeInTheDocument(); - expect(screen.getByText('Stock')).toBeInTheDocument(); - }); - it('opens the edit modal when Edit is clicked', async () => { - (useProductsData as jest.Mock).mockReturnValue({ - products: [ - { id: 1, category: 'Fruit', name: 'Apple', unitPrice: 1, expirationDate: '2023-06-22', stockQuantity: 10 }, - ], - loading: false, - categories: ['Fruit', 'Vegetables'], - error: null, - total: 1, - summary: [], - refreshProducts: jest.fn(), - }); - (useSearchContext as jest.Mock).mockReturnValue({ - stockQuantity: 0, - page: 1, - setParams: jest.fn(), - }); - - render(); - - const editButton = screen.getByText('Edit'); - fireEvent.click(editButton); - - expect(screen.getByText('Edit Product')).toBeInTheDocument(); - }); - test('should call deleteProduct when Delete is clicked', async () => { - (deleteProduct as jest.Mock).mockResolvedValueOnce({}); - - render(); - - const deleteLink = screen.getByText('Delete'); - fireEvent.click(deleteLink); - - const confirmButton = screen.getByText('Yes'); - fireEvent.click(confirmButton); - - await waitFor(() => { - expect(deleteProduct).toHaveBeenCalledWith(1); - }); - }); - test('should call markOutOfStock when clicking "Change availability"', async () => { - (useProductsData as jest.Mock).mockReturnValueOnce({ - products: [{ id: 1, category: 'Fruit', name: 'Apple', unitPrice: 1, expirationDate: '2023-06-22', stockQuantity: 10 }], - loading: false, - categories: ['Fruit', 'Vegetables'], - error: null, - total: 1, - summary: [], - refreshProducts: jest.fn(), - }); - render(); - - const checkbox = screen.getAllByRole('checkbox'); - fireEvent.click(checkbox[1]); - - const changeButton = screen.getByText('Change availability'); - fireEvent.click(changeButton); - - await waitFor(() => { - expect(markOutOfStock).toHaveBeenCalledWith(1); - }); - }); -}); diff --git a/inventory-manager/src/components/page-content/segment3-table/segment/InventoryTableObj.tsx b/inventory-manager/src/components/page-content/segment3-table/InventoryTableObj.tsx similarity index 69% rename from inventory-manager/src/components/page-content/segment3-table/segment/InventoryTableObj.tsx rename to inventory-manager/src/components/page-content/segment3-table/InventoryTableObj.tsx index 6a47253..633ba9e 100644 --- a/inventory-manager/src/components/page-content/segment3-table/segment/InventoryTableObj.tsx +++ b/inventory-manager/src/components/page-content/segment3-table/InventoryTableObj.tsx @@ -1,5 +1,5 @@ -import InventoryTable from "../InventoryTable"; -import InventoryTablePageSelector from "../InventoryTablePageSelector"; +import InventoryTable from "./segment/InventoryTable"; +import InventoryTablePageSelector from "./segment/InventoryTablePageSelector"; import React from "react"; import {Content} from "antd/es/layout/layout"; diff --git a/inventory-manager/src/components/page-content/segment3-table/segment/InventoryTable.test.tsx b/inventory-manager/src/components/page-content/segment3-table/segment/InventoryTable.test.tsx new file mode 100644 index 0000000..c4ad053 --- /dev/null +++ b/inventory-manager/src/components/page-content/segment3-table/segment/InventoryTable.test.tsx @@ -0,0 +1,239 @@ +/* @vitest-environment jsdom */ +/// +import "@testing-library/jest-dom/vitest"; +import {afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi} from "vitest"; +import {type ByRoleMatcher, type ByRoleOptions, cleanup, render, waitFor, within} from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import {SearchProvider} from "../../../../context/SearchContext.tsx"; +import {DataProvider} from "../../../../context/DataContext.tsx"; +import InventoryTable from "./InventoryTable.tsx"; +import {deleteProduct, getFilteredProducts} from "../../../../services/Requests"; +import type {Product} from "../../../../types/Product.ts"; +import type {AxiosResponse} from "axios"; + +const __realGetComputedStyle = window.getComputedStyle; +const __realMatchMedia = window.matchMedia as any; +vi.mock("../../../../services/Requests", () => ({ + getFilteredProducts: vi.fn(), + getCategories: vi.fn(), + getSummary: vi.fn(), + deleteProduct: vi.fn(), +})); + +const productsPage1: Product[] = [ + { + id: "p1", + name: "Coke", + category: "Beverages", + unitPrice: 1.5, + stockQuantity: 12, + creationDate: new Date("13-10-2025") + }, + { + id: "p2", + name: "Apple", + category: "Fruit", + unitPrice: 0.9, + stockQuantity: 30, + creationDate: new Date("13-10-2025") + }, +]; +const productsAfterSort: Product[] = [ + { + id: "p2", + name: "Apple", + category: "Fruit", + unitPrice: 0.9, + stockQuantity: 30, + creationDate: new Date("13-10-2025") + }, + { + id: "p1", + name: "Coke", + category: "Beverages", + unitPrice: 1.5, + stockQuantity: 12, + creationDate: new Date("13-10-2025") + } +]; +const productsAfterDelete: Product[] = [ + { + id: "p2", + name: "Apple", + category: "Fruit", + unitPrice: 0.9, + stockQuantity: 30, + creationDate: new Date("13-10-2025") + }, +]; + +function getFirstColumnTexts( + getByRole: ( + role: ByRoleMatcher, + options?: (ByRoleOptions | undefined) + ) => HTMLElement): string[] { + const table = getByRole("table"); + const tbody = within(table).getAllByRole("rowgroup")[1]; // [0]=thead, [1]=tbody + const rows = within(tbody).getAllByRole("row"); + return rows.map((row) => { + const cells = within(row).getAllByRole("cell"); + return cells[2].textContent?.trim() || ""; + }); +} + +function renderWithProviders() { + const user = userEvent.setup() + const view = render( + + + + + ); + return {user, ...view}; +} + +describe("InventoryTable tests", () => { + beforeAll(() => { + window.getComputedStyle = (elt: Element, _pseudo?: string | null) => { + const style = __realGetComputedStyle(elt); + const gpv = (prop: string) => { + if (typeof (style as any).getPropertyValue === "function") { + return (style as any).getPropertyValue(prop); + } + if ( + prop === "animation-duration" || + prop === "animation-delay" || + prop === "transition-duration" || + prop === "transition-delay" + ) { + return "0s"; + } + return ""; + }; + return {...style, getPropertyValue: gpv} as CSSStyleDeclaration; + }; + window.matchMedia = (query: string) => ({ + matches: false, + media: query, + onchange: null, + addListener: vi.fn(), + removeListener: vi.fn(), + addEventListener: vi.fn(), + removeEventListener: vi.fn(), + dispatchEvent: vi.fn(), + }); + }); + afterAll(() => { + window.getComputedStyle = __realGetComputedStyle; + window.matchMedia = __realMatchMedia; + }); + beforeEach(() => { + vi.clearAllMocks(); + }); + afterEach(() => { + vi.useRealTimers(); + cleanup(); + vi.resetAllMocks(); + }); + + it("renders rows with data after start", async () => { + vi.mocked(getFilteredProducts).mockResolvedValueOnce({ + data: { + products: productsPage1, + totalPages: Math.ceil(productsPage1.length / 10) + } + } as unknown as AxiosResponse); + const {getByRole, getByText} = renderWithProviders(); + + await waitFor(() => { + expect(getByRole("table")).toBeInTheDocument(); + expect(getByText("Coke")).toBeVisible(); + expect(getByText("Apple")).toBeVisible(); + }); + + expect(getFilteredProducts).toHaveBeenCalledTimes(1); + }); + + it("sorts by Name column (asc/desc) when header is clicked", async () => { + vi.mocked(getFilteredProducts).mockResolvedValueOnce({ + data: { + products: productsPage1, + totalPages: Math.ceil(productsPage1.length / 10) + } + } as unknown as AxiosResponse); + vi.mocked(getFilteredProducts).mockResolvedValueOnce({ + data: { + products: productsAfterSort, + totalPages: Math.ceil(productsPage1.length / 10) + } + } as unknown as AxiosResponse); + vi.mocked(getFilteredProducts).mockResolvedValueOnce({ + data: { + products: productsPage1, + totalPages: Math.ceil(productsPage1.length / 10) + } + } as unknown as AxiosResponse); + + const {getByRole, findByText} = renderWithProviders(); + await findByText("Coke"); + await findByText("Apple"); + const nameHeader = getByRole("columnheader", {name: /name/i}); + + await userEvent.click(nameHeader); + await findByText("Coke"); + await waitFor(() => { + expect(getFirstColumnTexts(getByRole)).toEqual(["Apple", "Coke"]); + }); + + await userEvent.click(nameHeader); + await findByText("Coke"); + await waitFor(() => { + expect(getFirstColumnTexts(getByRole)).toEqual(["Coke", "Apple"]); + }); + }); + + it("deletes a row after confirm and the table reflects the removal", async () => { + vi.mocked(getFilteredProducts).mockResolvedValueOnce({ + data: { + products: productsPage1, + totalPages: Math.ceil(productsPage1.length / 10) + } + } as unknown as AxiosResponse); + vi.mocked(deleteProduct("p1")); + vi.mocked(getFilteredProducts).mockResolvedValueOnce({ + data: { + products: productsAfterDelete, + totalPages: Math.ceil(productsPage1.length / 10) + } + } as unknown as AxiosResponse); + const { + getByRole, + findByText, + queryByText, + getAllByRole, + getByText + } = renderWithProviders(); + await findByText("Coke"); + await findByText("Apple"); + + const deleteButton = getAllByRole("button", {name: /delete/i})[0]; + await userEvent.click(deleteButton); + + const confirmBtn = + getByRole("button", {name: /^yes$/i}); + await userEvent.click(confirmBtn!); + await findByText("Apple"); + + await waitFor(() => { + expect(deleteProduct).toHaveBeenCalledTimes(2); + expect(deleteProduct).toHaveBeenCalledWith("p1"); + }); + + await waitFor(() => { + expect(queryByText("Coke")).not.toBeInTheDocument(); + expect(getByText("Apple")).toBeVisible(); + }); + + expect(getFilteredProducts).toHaveBeenCalledTimes(2); + }); +}); \ No newline at end of file diff --git a/inventory-manager/src/components/page-content/segment3-table/InventoryTable.tsx b/inventory-manager/src/components/page-content/segment3-table/segment/InventoryTable.tsx similarity index 93% rename from inventory-manager/src/components/page-content/segment3-table/InventoryTable.tsx rename to inventory-manager/src/components/page-content/segment3-table/segment/InventoryTable.tsx index c104406..5de427e 100644 --- a/inventory-manager/src/components/page-content/segment3-table/InventoryTable.tsx +++ b/inventory-manager/src/components/page-content/segment3-table/segment/InventoryTable.tsx @@ -1,12 +1,12 @@ import React, {type MouseEventHandler, useRef, useState} from 'react'; import {Button, Input, type InputRef, Modal, Popconfirm, Space, Table, type TableColumnType} from 'antd'; import type {TableColumnsType, TableProps} from 'antd'; -import type {Product} from "../../../types/Product"; +import type {Product} from "../../../../types/Product"; import {SearchOutlined} from '@ant-design/icons'; -import {useSearchContext} from "../../../context/SearchContext"; -import {useDataContext} from "../../../context/DataContext"; -import {deleteProduct, markInStock, markOutOfStock} from "../../../services/Requests"; -import ProductForm from "../segment2-new_product/ProductForm"; +import {useSearchContext} from "../../../../context/SearchContext"; +import {useDataContext} from "../../../../context/DataContext"; +import {deleteProduct, markInStock, markOutOfStock} from "../../../../services/Requests"; +import ProductForm from "../../segment2-new_product/ProductForm"; type DataIndex = keyof Product; const InventoryTable: React.FC = () => { @@ -159,27 +159,27 @@ const InventoryTable: React.FC = () => { title: 'Category', dataIndex: 'category', sorter: {multiple: 3}, - filteredValue: undefined, + filteredValue: category ? [String(category)] : null, ...getColumnSearchProps("category") }, { title: 'Name', dataIndex: 'name', sorter: {multiple: 3}, - filteredValue: undefined, + filteredValue: name ? [String(name)] : null, ...getColumnSearchProps("name") }, { title: 'Price', dataIndex: 'unitPrice', sorter: {multiple: 3}, - filteredValue: undefined + filteredValue: null }, { title: 'Expiration Date', dataIndex: 'expirationDate', sorter: {multiple: 3}, - filteredValue: undefined, + filteredValue: null, render: (_) => _ ? new Date(_).toLocaleDateString() : 'No date', }, { @@ -193,7 +193,7 @@ const InventoryTable: React.FC = () => { {text: 'No stock', value: '2'} ], filterMultiple: false, - filteredValue: stockQuantity !== undefined ? [String(stockQuantity)] : null, + filteredValue: stockQuantity ? ['1', '2'].includes(String(stockQuantity)) ? [String(stockQuantity)] : null: null, onFilter: (_value, _record) => { return true; }, @@ -202,7 +202,7 @@ const InventoryTable: React.FC = () => { { title: 'Actions', dataIndex: 'action', - filteredValue: undefined, + filteredValue: null, render: (_, record) => ( handleEdit(record)}>Edit @@ -213,7 +213,7 @@ const InventoryTable: React.FC = () => { okText="Yes" cancelText="Cancel" > - Delete + Delete ), diff --git a/inventory-manager/src/components/page-content/segment3-table/segment/InventoryTablePageSelector.test.tsx b/inventory-manager/src/components/page-content/segment3-table/segment/InventoryTablePageSelector.test.tsx new file mode 100644 index 0000000..40ae286 --- /dev/null +++ b/inventory-manager/src/components/page-content/segment3-table/segment/InventoryTablePageSelector.test.tsx @@ -0,0 +1,267 @@ +/* @vitest-environment jsdom */ +/// +import "@testing-library/jest-dom/vitest"; +import userEvent from "@testing-library/user-event"; +import type {AxiosResponse} from "axios"; +import {afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi} from "vitest"; +import type {Product} from "../../../../types/Product.ts"; +import {cleanup, render, waitFor, within} from "@testing-library/react"; +import {SearchProvider} from "../../../../context/SearchContext.tsx"; +import {DataProvider} from "../../../../context/DataContext.tsx"; +import InventoryTable from "./InventoryTable.tsx"; +import InventoryTablePageSelector from "./InventoryTablePageSelector.tsx"; +import {getFilteredProducts} from "../../../../services/Requests.ts"; + +const __realGetComputedStyle = window.getComputedStyle; +const __realMatchMedia = window.matchMedia as any; +vi.mock("../../../../services/Requests", () => ({ + getFilteredProducts: vi.fn(), +})); + +const productsPage1: Product[] = [ + { + id: "p1", + name: "Coke", + category: "Beverages", + unitPrice: 1.5, + stockQuantity: 12, + creationDate: new Date("13-10-2025") + }, + { + id: "p2", + name: "Apple", + category: "Fruit", + unitPrice: 0.9, + stockQuantity: 30, + creationDate: new Date("13-10-2025") + }, + { + id: "p3", + name: "Coke", + category: "Beverages", + unitPrice: 1.5, + stockQuantity: 12, + creationDate: new Date("13-10-2025") + }, + { + id: "p4", + name: "Apple", + category: "Fruit", + unitPrice: 0.9, + stockQuantity: 30, + creationDate: new Date("13-10-2025") + }, + { + id: "p5", + name: "Coke", + category: "Beverages", + unitPrice: 1.5, + stockQuantity: 12, + creationDate: new Date("13-10-2025") + }, + { + id: "p6", + name: "Apple", + category: "Fruit", + unitPrice: 0.9, + stockQuantity: 30, + creationDate: new Date("13-10-2025") + }, + { + id: "p7", + name: "Coke", + category: "Beverages", + unitPrice: 1.5, + stockQuantity: 12, + creationDate: new Date("13-10-2025") + }, + { + id: "p8", + name: "Apple", + category: "Fruit", + unitPrice: 0.9, + stockQuantity: 30, + creationDate: new Date("13-10-2025") + }, + { + id: "p9", + name: "Coke", + category: "Beverages", + unitPrice: 1.5, + stockQuantity: 12, + creationDate: new Date("13-10-2025") + }, + { + id: "p10", + name: "Apple", + category: "Fruit", + unitPrice: 0.9, + stockQuantity: 30, + creationDate: new Date("13-10-2025") + }, + { + id: "p11", + name: "Coke", + category: "Beverages", + unitPrice: 1.5, + stockQuantity: 12, + creationDate: new Date("13-10-2025") + }, + { + id: "p12", + name: "Apple", + category: "Fruit", + unitPrice: 0.9, + stockQuantity: 30, + creationDate: new Date("13-10-2025") + }, +]; +const productsPage2: Product[] = [ + { + id: "p13", + name: "AppleP2", + category: "Fruit", + unitPrice: 0.9, + stockQuantity: 30, + creationDate: new Date("13-10-2025") + }, + { + id: "p44", + name: "CokeP2", + category: "Beverages", + unitPrice: 1.5, + stockQuantity: 12, + creationDate: new Date("13-10-2025") + } +]; + +function renderWithProviders() { + const user = userEvent.setup() + const view = render( + + + + + + ); + return {user, ...view}; +} + +describe("InventoryTablePageSelector tests", () => { + beforeAll(() => { + window.getComputedStyle = (elt: Element, _pseudo?: string | null) => { + const style = __realGetComputedStyle(elt); + const gpv = (prop: string) => { + if (typeof (style as any).getPropertyValue === "function") { + return (style as any).getPropertyValue(prop); + } + if ( + prop === "animation-duration" || + prop === "animation-delay" || + prop === "transition-duration" || + prop === "transition-delay" + ) { + return "0s"; + } + return ""; + }; + return {...style, getPropertyValue: gpv} as CSSStyleDeclaration; + }; + window.matchMedia = (query: string) => ({ + matches: false, + media: query, + onchange: null, + addListener: vi.fn(), + removeListener: vi.fn(), + addEventListener: vi.fn(), + removeEventListener: vi.fn(), + dispatchEvent: vi.fn(), + }); + }); + afterAll(() => { + window.getComputedStyle = __realGetComputedStyle; + window.matchMedia = __realMatchMedia; + }); + beforeEach(() => { + vi.clearAllMocks(); + }); + afterEach(() => { + vi.useRealTimers(); + cleanup(); + vi.resetAllMocks(); + }); + + it("makes call to Api whenever page is changed", async () => { + vi.mocked(getFilteredProducts).mockResolvedValueOnce({ + data: { + products: productsPage1, + totalPages: Math.ceil(productsPage1.length / 10) + } + } as unknown as AxiosResponse); + vi.mocked(getFilteredProducts).mockResolvedValueOnce({ + data: { + products: productsPage2, + totalPages: Math.ceil(productsPage2.length / 10) + } + } as unknown as AxiosResponse); + + const {getByRole, getByText, findByText, findAllByText} = renderWithProviders(); + + await findAllByText("Coke"); + await findAllByText("Apple"); + + const pager = getByRole('navigation'); + const next = within(pager).getAllByRole('button')[1]; + await userEvent.click(next); + + await findByText("CokeP2"); + await waitFor(() => { + expect(getByText("CokeP2")).toBeVisible(); + expect(getByText("AppleP2")).toBeVisible(); + }); + + expect(getFilteredProducts).toHaveBeenCalledTimes(2); + }); + + it("it goes back and forth", async () => { + vi.mocked(getFilteredProducts).mockResolvedValueOnce({ + data: { + products: productsPage1, + totalPages: Math.ceil(productsPage1.length / 10) + } + } as unknown as AxiosResponse); + vi.mocked(getFilteredProducts).mockResolvedValueOnce({ + data: { + products: productsPage1, + totalPages: Math.ceil(productsPage1.length / 10) + } + } as unknown as AxiosResponse); + vi.mocked(getFilteredProducts).mockResolvedValueOnce({ + data: { + products: productsPage2, + totalPages: Math.ceil(productsPage2.length / 10) + } + } as unknown as AxiosResponse); + + const {getByRole, getAllByText, findAllByText} = renderWithProviders(); + + await findAllByText("Coke"); + + let pager = getByRole('navigation'); + const next = within(pager).getAllByRole('button')[1]; + await userEvent.click(next); + + await findAllByText("Coke"); + await waitFor(() => { + expect(getAllByText("Coke")[0]).toBeVisible(); + expect(getAllByText("Apple")[0]).toBeVisible(); + }); + + pager = getByRole('navigation'); + const prev = within(pager).getAllByRole('button')[0]; + await userEvent.click(prev); + await findAllByText("CokeP2"); + + expect(getFilteredProducts).toHaveBeenCalledTimes(3); + }); +}); diff --git a/inventory-manager/src/components/page-content/segment3-table/InventoryTablePageSelector.tsx b/inventory-manager/src/components/page-content/segment3-table/segment/InventoryTablePageSelector.tsx similarity index 79% rename from inventory-manager/src/components/page-content/segment3-table/InventoryTablePageSelector.tsx rename to inventory-manager/src/components/page-content/segment3-table/segment/InventoryTablePageSelector.tsx index ed0afd0..d15a704 100644 --- a/inventory-manager/src/components/page-content/segment3-table/InventoryTablePageSelector.tsx +++ b/inventory-manager/src/components/page-content/segment3-table/segment/InventoryTablePageSelector.tsx @@ -1,7 +1,7 @@ import React from 'react'; import {Pagination, type PaginationProps} from 'antd'; -import {useSearchContext} from "../../../context/SearchContext"; -import {useDataContext} from "../../../context/DataContext"; +import {useSearchContext} from "../../../../context/SearchContext"; +import {useDataContext} from "../../../../context/DataContext"; const InventoryTablePageSelector: React.FC = () => { const {page, setParams} = useSearchContext(); @@ -16,6 +16,7 @@ const InventoryTablePageSelector: React.FC = () => { onChange={handlePageChange} current={(page as unknown as number) + 1} showSizeChanger={false} + role={'navigation'} /> ) } diff --git a/inventory-manager/src/context/DataContext.test.tsx b/inventory-manager/src/context/DataContext.test.tsx index 7e73056..9b30389 100644 --- a/inventory-manager/src/context/DataContext.test.tsx +++ b/inventory-manager/src/context/DataContext.test.tsx @@ -22,8 +22,7 @@ vi.mock("../services/Requests", () => ( deleteProduct: vi.fn(), } - ) -); + )); const Consumer: React.FC = () => { const {setParams} = useSearchContext(); @@ -74,17 +73,16 @@ function renderWithProvider() { return {user, ...view}; } -beforeEach(() => { - vi.clearAllMocks(); -}); - -afterEach(() => { - vi.useRealTimers(); - cleanup(); - vi.resetAllMocks(); -}); - describe("DataContext initial tests", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + afterEach(() => { + vi.useRealTimers(); + cleanup(); + vi.resetAllMocks(); + }); + it("starts with nothing", () => { const {getByTestId} = renderWithProvider(); expect(getByTestId("products").textContent).toBe("no"); @@ -97,6 +95,15 @@ describe("DataContext initial tests", () => { }); describe("DataContext triggers", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + afterEach(() => { + vi.useRealTimers(); + cleanup(); + vi.resetAllMocks(); + }); + it("trigger fetch", async () => { vi.mocked(getFilteredProducts).mockResolvedValueOnce({ data: { diff --git a/inventory-manager/src/context/SearchContext.test.tsx b/inventory-manager/src/context/SearchContext.test.tsx index 3580aee..00bbff6 100644 --- a/inventory-manager/src/context/SearchContext.test.tsx +++ b/inventory-manager/src/context/SearchContext.test.tsx @@ -63,17 +63,16 @@ function renderWithProvider() { return {user, ...view}; } -beforeEach(() => { - vi.clearAllMocks(); -}); - -afterEach(() => { - vi.useRealTimers(); - cleanup(); - vi.resetAllMocks(); -}); - describe("SearchContext tests", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + afterEach(() => { + vi.useRealTimers(); + cleanup(); + vi.resetAllMocks(); + }); + it("exposes initial state", () => { const {getByTestId} = renderWithProvider(); expect(getByTestId("name").textContent).toBe("");