All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 14m14s
574 lines
21 KiB
TypeScript
574 lines
21 KiB
TypeScript
// src/features/flyer/ExtractedDataTable.test.tsx
|
|
import React from 'react';
|
|
import { render, screen, fireEvent, within, prettyDOM } from '@testing-library/react';
|
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
import { ExtractedDataTable, ExtractedDataTableProps } from './ExtractedDataTable';
|
|
import type { FlyerItem, MasterGroceryItem, ShoppingList } from '../../types';
|
|
import { useAuth } from '../../hooks/useAuth';
|
|
import {
|
|
createMockFlyerItem,
|
|
createMockMasterGroceryItem,
|
|
createMockShoppingList,
|
|
createMockShoppingListItem,
|
|
createMockUser,
|
|
createMockUserProfile,
|
|
} from '../../tests/utils/mockFactories';
|
|
import { useUserData } from '../../hooks/useUserData';
|
|
import { useMasterItems } from '../../hooks/useMasterItems';
|
|
import { useWatchedItems } from '../../hooks/useWatchedItems';
|
|
import { useShoppingLists } from '../../hooks/useShoppingLists';
|
|
|
|
// Mock all the data hooks
|
|
vi.mock('../../hooks/useAuth');
|
|
vi.mock('../../hooks/useUserData');
|
|
vi.mock('../../hooks/useMasterItems');
|
|
vi.mock('../../hooks/useWatchedItems');
|
|
vi.mock('../../hooks/useShoppingLists');
|
|
|
|
const mockUser = createMockUser({ user_id: 'user-123', email: 'test@example.com' });
|
|
const mockUserProfile = createMockUserProfile({ user: mockUser });
|
|
|
|
const mockMasterItems: MasterGroceryItem[] = [
|
|
createMockMasterGroceryItem({
|
|
master_grocery_item_id: 1,
|
|
name: 'Apples',
|
|
category_id: 1,
|
|
category_name: 'Produce',
|
|
}),
|
|
createMockMasterGroceryItem({
|
|
master_grocery_item_id: 2,
|
|
name: 'Milk',
|
|
category_id: 2,
|
|
category_name: 'Dairy',
|
|
}),
|
|
createMockMasterGroceryItem({
|
|
master_grocery_item_id: 3,
|
|
name: 'Chicken Breast',
|
|
category_id: 3,
|
|
category_name: 'Meat',
|
|
}),
|
|
];
|
|
|
|
const mockFlyerItems: FlyerItem[] = [
|
|
createMockFlyerItem({
|
|
flyer_item_id: 101,
|
|
item: 'Gala Apples',
|
|
price_display: '$1.99/lb',
|
|
price_in_cents: 199,
|
|
quantity: 'per lb',
|
|
unit_price: { value: 1.99, unit: 'lb' },
|
|
master_item_id: 1,
|
|
category_name: 'Produce',
|
|
flyer_id: 1,
|
|
}),
|
|
createMockFlyerItem({
|
|
flyer_item_id: 102,
|
|
item: '2% Milk',
|
|
price_display: '$4.50',
|
|
price_in_cents: 450,
|
|
quantity: '4L',
|
|
unit_price: { value: 1.125, unit: 'L' },
|
|
master_item_id: 2,
|
|
category_name: 'Dairy',
|
|
flyer_id: 1,
|
|
}),
|
|
createMockFlyerItem({
|
|
flyer_item_id: 103,
|
|
item: 'Boneless Chicken',
|
|
price_display: '$8.00/kg',
|
|
price_in_cents: 800,
|
|
quantity: 'per kg',
|
|
unit_price: { value: 8.0, unit: 'kg' },
|
|
master_item_id: 3,
|
|
category_name: 'Meat',
|
|
flyer_id: 1,
|
|
}),
|
|
createMockFlyerItem({
|
|
flyer_item_id: 104,
|
|
item: 'Mystery Soda',
|
|
price_display: '$1.00',
|
|
price_in_cents: 100,
|
|
quantity: '1 can',
|
|
unit_price: { value: 1.0, unit: 'can' },
|
|
master_item_id: undefined,
|
|
category_name: 'Beverages',
|
|
flyer_id: 1,
|
|
}), // Unmatched item
|
|
createMockFlyerItem({
|
|
flyer_item_id: 105,
|
|
item: 'Apples',
|
|
price_display: '$2.50/lb',
|
|
price_in_cents: 250,
|
|
quantity: 'per lb',
|
|
unit_price: { value: 2.5, unit: 'lb' },
|
|
master_item_id: 1,
|
|
category_name: 'Produce',
|
|
flyer_id: 1,
|
|
}), // Item name matches canonical name
|
|
];
|
|
|
|
const mockShoppingLists: ShoppingList[] = [
|
|
createMockShoppingList({
|
|
shopping_list_id: 1,
|
|
name: 'My List',
|
|
user_id: 'user-123',
|
|
items: [
|
|
createMockShoppingListItem({
|
|
shopping_list_item_id: 1,
|
|
shopping_list_id: 1,
|
|
master_item_id: 2,
|
|
quantity: 1,
|
|
is_purchased: false,
|
|
}),
|
|
], // Contains Milk
|
|
}),
|
|
];
|
|
|
|
const mockAddWatchedItem = vi.fn();
|
|
const mockAddItemToList = vi.fn();
|
|
|
|
const defaultProps: ExtractedDataTableProps = {
|
|
items: mockFlyerItems,
|
|
unitSystem: 'imperial' as const,
|
|
// The following props are now sourced from the mocked hooks below,
|
|
// so they are not needed here and have been removed from the component's signature.
|
|
// totalActiveItems: mockFlyerItems.length,
|
|
// watchedItems: [],
|
|
// masterItems: mockMasterItems,
|
|
// user: mockUser,
|
|
// onAddItem: mockAddWatchedItem,
|
|
// shoppingLists: mockShoppingLists,
|
|
// activeListId: 1,
|
|
// onAddItemToList: mockAddItemToList,
|
|
};
|
|
|
|
describe('ExtractedDataTable', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
// Provide default mocks for all hooks
|
|
vi.mocked(useAuth).mockReturnValue({
|
|
userProfile: mockUserProfile,
|
|
authStatus: 'AUTHENTICATED',
|
|
isLoading: false,
|
|
login: vi.fn(),
|
|
logout: vi.fn(),
|
|
updateProfile: vi.fn(),
|
|
});
|
|
vi.mocked(useUserData).mockReturnValue({
|
|
watchedItems: [],
|
|
shoppingLists: mockShoppingLists,
|
|
setWatchedItems: vi.fn(),
|
|
setShoppingLists: vi.fn(),
|
|
isLoading: false,
|
|
error: null,
|
|
});
|
|
vi.mocked(useMasterItems).mockReturnValue({
|
|
masterItems: mockMasterItems,
|
|
isLoading: false,
|
|
error: null,
|
|
});
|
|
vi.mocked(useWatchedItems).mockReturnValue({
|
|
addWatchedItem: mockAddWatchedItem,
|
|
watchedItems: [],
|
|
removeWatchedItem: vi.fn(),
|
|
error: null,
|
|
});
|
|
vi.mocked(useShoppingLists).mockReturnValue({
|
|
activeListId: 1,
|
|
addItemToList: mockAddItemToList,
|
|
shoppingLists: mockShoppingLists, // Add this to satisfy the component's dependency
|
|
setActiveListId: vi.fn(),
|
|
createList: vi.fn(),
|
|
deleteList: vi.fn(),
|
|
updateItemInList: vi.fn(),
|
|
removeItemFromList: vi.fn(),
|
|
isCreatingList: false,
|
|
isDeletingList: false,
|
|
isAddingItem: false,
|
|
isUpdatingItem: false,
|
|
isRemovingItem: false,
|
|
error: null,
|
|
});
|
|
});
|
|
|
|
it('should render an empty state message if no items are provided', () => {
|
|
render(<ExtractedDataTable items={[]} unitSystem="imperial" />);
|
|
expect(screen.getByText('No items extracted yet.')).toBeInTheDocument();
|
|
});
|
|
|
|
it('should render the table with items', () => {
|
|
render(<ExtractedDataTable {...defaultProps} />);
|
|
expect(screen.getByRole('heading', { name: /item list/i })).toBeInTheDocument();
|
|
expect(screen.getByText('Gala Apples')).toBeInTheDocument();
|
|
expect(screen.getByText('2% Milk')).toBeInTheDocument();
|
|
});
|
|
|
|
it('should not show watch/add to list buttons for anonymous users', () => {
|
|
vi.mocked(useAuth).mockReturnValue({
|
|
userProfile: null,
|
|
authStatus: 'SIGNED_OUT',
|
|
isLoading: false,
|
|
login: vi.fn(),
|
|
logout: vi.fn(),
|
|
updateProfile: vi.fn(),
|
|
});
|
|
render(<ExtractedDataTable {...defaultProps} />);
|
|
expect(screen.queryByRole('button', { name: /watch/i })).not.toBeInTheDocument();
|
|
expect(screen.queryByRole('button', { name: /add to list/i })).not.toBeInTheDocument();
|
|
});
|
|
|
|
describe('Item States and Interactions', () => {
|
|
it('should highlight watched items and hide the watch button', () => {
|
|
vi.mocked(useUserData).mockReturnValue({
|
|
watchedItems: [mockMasterItems[0]], // 'Apples' is watched
|
|
shoppingLists: [],
|
|
setWatchedItems: vi.fn(),
|
|
setShoppingLists: vi.fn(),
|
|
isLoading: false,
|
|
error: null,
|
|
});
|
|
render(<ExtractedDataTable {...defaultProps} />);
|
|
const appleItemRow = screen.getByText('Gala Apples').closest('tr');
|
|
expect(appleItemRow).toHaveTextContent('Gala Apples');
|
|
expect(appleItemRow!.querySelector('.font-bold')).toBeInTheDocument(); // Watched items are bold
|
|
expect(appleItemRow!.querySelector('button[title*="Watch"]')).not.toBeInTheDocument();
|
|
});
|
|
|
|
it('should show the watch button for unwatched, matched items', () => {
|
|
render(<ExtractedDataTable {...defaultProps} />);
|
|
// Find the specific table row for the item first to make the test more specific.
|
|
const chickenItemRow = screen.getByText('Boneless Chicken').closest('tr')!;
|
|
// Now, find the watch button *within* that row.
|
|
const watchButton = within(chickenItemRow).getByTitle(
|
|
"Add 'Chicken Breast' to your watchlist",
|
|
);
|
|
expect(watchButton).toBeInTheDocument();
|
|
|
|
fireEvent.click(watchButton);
|
|
expect(mockAddWatchedItem).toHaveBeenCalledWith('Chicken Breast', 'Meat');
|
|
});
|
|
|
|
it('should not show watch or add to list buttons for unmatched items', () => {
|
|
render(<ExtractedDataTable {...defaultProps} />);
|
|
const sodaItemRow = screen.getByText('Mystery Soda').closest('tr');
|
|
expect(sodaItemRow!.querySelector('button[title*="Watch"]')).not.toBeInTheDocument();
|
|
expect(sodaItemRow!.querySelector('button[title*="list"]')).not.toBeInTheDocument();
|
|
});
|
|
|
|
it('should hide the add to list button for items already in the active list', () => {
|
|
// 'Milk' (master_item_id: 2) is in the active shopping list
|
|
vi.mocked(useShoppingLists).mockReturnValue({
|
|
activeListId: 1,
|
|
addItemToList: mockAddItemToList,
|
|
shoppingLists: mockShoppingLists, // 'Milk' is in this list
|
|
setActiveListId: vi.fn(),
|
|
createList: vi.fn(),
|
|
deleteList: vi.fn(),
|
|
updateItemInList: vi.fn(),
|
|
removeItemFromList: vi.fn(),
|
|
isCreatingList: false,
|
|
isDeletingList: false,
|
|
isAddingItem: false,
|
|
isUpdatingItem: false,
|
|
isRemovingItem: false,
|
|
error: null,
|
|
});
|
|
render(<ExtractedDataTable {...defaultProps} />);
|
|
const milkItemRow = screen.getByText('2% Milk').closest('tr')!;
|
|
// Use a more specific query to target the "shopping list" button and not the "watchlist" button.
|
|
const addToListButton = within(milkItemRow).queryByTitle(/add milk to list/i);
|
|
expect(addToListButton).not.toBeInTheDocument();
|
|
});
|
|
|
|
it('should show the add to list button for items not in the list', () => {
|
|
vi.mocked(useShoppingLists).mockReturnValue({
|
|
activeListId: 1,
|
|
addItemToList: mockAddItemToList,
|
|
shoppingLists: [{ ...mockShoppingLists[0], items: [] }], // Empty list
|
|
setActiveListId: vi.fn(),
|
|
createList: vi.fn(),
|
|
deleteList: vi.fn(),
|
|
updateItemInList: vi.fn(),
|
|
removeItemFromList: vi.fn(),
|
|
isCreatingList: false,
|
|
isDeletingList: false,
|
|
isAddingItem: false,
|
|
isUpdatingItem: false,
|
|
isRemovingItem: false,
|
|
error: null,
|
|
});
|
|
render(<ExtractedDataTable {...defaultProps} />);
|
|
const appleItemRow = screen.getByText('Gala Apples').closest('tr')!;
|
|
// Correct the title query to match the actual rendered title.
|
|
const addToListButton = within(appleItemRow).getByTitle('Add Apples to list');
|
|
expect(addToListButton).toBeInTheDocument();
|
|
|
|
fireEvent.click(addToListButton!);
|
|
expect(mockAddItemToList).toHaveBeenCalledWith(1, { masterItemId: 1 }); // The component calls the hook's function
|
|
});
|
|
|
|
it('should disable the add to list button if no list is active', () => {
|
|
vi.mocked(useShoppingLists).mockReturnValue({
|
|
activeListId: null,
|
|
shoppingLists: [],
|
|
addItemToList: mockAddItemToList,
|
|
setActiveListId: vi.fn(),
|
|
createList: vi.fn(),
|
|
deleteList: vi.fn(),
|
|
updateItemInList: vi.fn(),
|
|
removeItemFromList: vi.fn(),
|
|
isCreatingList: false,
|
|
isDeletingList: false,
|
|
isAddingItem: false,
|
|
isUpdatingItem: false,
|
|
isRemovingItem: false,
|
|
error: null,
|
|
});
|
|
render(<ExtractedDataTable {...defaultProps} />);
|
|
// Multiple buttons will have this title, so we must use `getAllByTitle`.
|
|
const addToListButtons = screen.getAllByTitle('Select a shopping list first');
|
|
// Assert that at least one such button exists and that they are all disabled.
|
|
expect(addToListButtons.length).toBeGreaterThan(0);
|
|
addToListButtons.forEach((button) => expect(button).toBeDisabled());
|
|
});
|
|
|
|
it('should display the canonical name when it differs from the item name', () => {
|
|
render(<ExtractedDataTable {...defaultProps} />);
|
|
// For 'Gala Apples', canonical is 'Apples'
|
|
expect(screen.getByText('(Canonical: Apples)')).toBeInTheDocument();
|
|
// For '2% Milk', canonical is 'Milk'
|
|
expect(screen.getByText('(Canonical: Milk)')).toBeInTheDocument();
|
|
});
|
|
|
|
it('should not display the canonical name when it is the same as the item name (case-insensitive)', () => {
|
|
render(<ExtractedDataTable {...defaultProps} />);
|
|
// Find the row for the item where item.name === canonical.name
|
|
const matchingNameItemRow = screen.getByText('$2.50/lb').closest('tr')!;
|
|
// Ensure the redundant canonical name is not displayed
|
|
expect(within(matchingNameItemRow).queryByText(/\(Canonical: .*\)/)).not.toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
describe('Sorting and Filtering', () => {
|
|
it('should sort watched items to the top', () => {
|
|
// Watch 'Chicken Breast' (normally 3rd) and 'Apples' (normally 1st)
|
|
vi.mocked(useUserData).mockReturnValue({
|
|
watchedItems: [mockMasterItems[2], mockMasterItems[0]],
|
|
shoppingLists: [],
|
|
setWatchedItems: vi.fn(),
|
|
setShoppingLists: vi.fn(),
|
|
isLoading: false,
|
|
error: null,
|
|
});
|
|
render(<ExtractedDataTable {...defaultProps} />);
|
|
|
|
// Get all rows from the table body
|
|
const rows = screen.getAllByRole('row');
|
|
// Extract the primary item name from each row to check the sort order
|
|
const itemNamesInOrder = rows.map(
|
|
(row) => row.querySelector('div.font-semibold, div.font-bold')?.textContent,
|
|
);
|
|
|
|
// Assert the order is correct: watched items first, then others.
|
|
// 'Gala Apples' (101) and 'Apples' (105) both have master_item_id 1, which is watched.
|
|
// The implementation sorts watched items to the top, and then sorts alphabetically within each group (watched/unwatched).
|
|
const expectedOrder = [
|
|
'Apples', // Watched
|
|
'Boneless Chicken', // Watched
|
|
'Gala Apples', // Watched
|
|
'2% Milk',
|
|
'Mystery Soda', // Unwatched
|
|
];
|
|
expect(itemNamesInOrder).toEqual(expectedOrder);
|
|
});
|
|
|
|
it('should filter items by category', () => {
|
|
render(<ExtractedDataTable {...defaultProps} />);
|
|
const categoryFilter = screen.getByLabelText('Filter by category');
|
|
|
|
// Initial state
|
|
expect(screen.getByText('Gala Apples')).toBeInTheDocument();
|
|
expect(screen.getByText('2% Milk')).toBeInTheDocument();
|
|
|
|
// Filter by Dairy
|
|
fireEvent.change(categoryFilter, { target: { value: 'Dairy' } });
|
|
expect(screen.queryByText('Gala Apples')).not.toBeInTheDocument();
|
|
expect(screen.getByText('2% Milk')).toBeInTheDocument();
|
|
|
|
// Check empty state for filter
|
|
fireEvent.change(categoryFilter, { target: { value: 'Produce' } });
|
|
fireEvent.change(categoryFilter, { target: { value: 'Snacks' } }); // A category with no items
|
|
expect(screen.getByText('No items found for the selected category.')).toBeInTheDocument();
|
|
});
|
|
|
|
it('should not render the category filter if there is only one category', () => {
|
|
const singleCategoryItems = mockFlyerItems.filter((item) => item.category_name === 'Produce');
|
|
render(<ExtractedDataTable {...defaultProps} items={singleCategoryItems} />);
|
|
expect(screen.queryByLabelText('Filter by category')).not.toBeInTheDocument();
|
|
});
|
|
|
|
it('should allow switching filter back to All Categories', () => {
|
|
render(<ExtractedDataTable {...defaultProps} />);
|
|
const categoryFilter = screen.getByLabelText('Filter by category');
|
|
|
|
// Filter to Dairy
|
|
fireEvent.change(categoryFilter, { target: { value: 'Dairy' } });
|
|
expect(screen.queryByText('Gala Apples')).not.toBeInTheDocument();
|
|
expect(screen.getByText('2% Milk')).toBeInTheDocument();
|
|
|
|
// Filter back to All
|
|
fireEvent.change(categoryFilter, { target: { value: 'all' } });
|
|
expect(screen.getByText('Gala Apples')).toBeInTheDocument();
|
|
expect(screen.getByText('2% Milk')).toBeInTheDocument();
|
|
});
|
|
|
|
it('should sort items alphabetically within watched and unwatched groups', () => {
|
|
const items = [
|
|
createMockFlyerItem({
|
|
flyer_item_id: 1,
|
|
item: 'Yam',
|
|
master_item_id: 3,
|
|
category_name: 'Produce',
|
|
}), // Unwatched
|
|
createMockFlyerItem({
|
|
flyer_item_id: 2,
|
|
item: 'Zebra',
|
|
master_item_id: 1,
|
|
category_name: 'Produce',
|
|
}), // Watched
|
|
createMockFlyerItem({
|
|
flyer_item_id: 3,
|
|
item: 'Banana',
|
|
master_item_id: 4,
|
|
category_name: 'Produce',
|
|
}), // Unwatched
|
|
createMockFlyerItem({
|
|
flyer_item_id: 4,
|
|
item: 'Apple',
|
|
master_item_id: 2,
|
|
category_name: 'Produce',
|
|
}), // Watched
|
|
];
|
|
|
|
vi.mocked(useUserData).mockReturnValue({
|
|
watchedItems: [
|
|
createMockMasterGroceryItem({ master_grocery_item_id: 1, name: 'Zebra' }),
|
|
createMockMasterGroceryItem({ master_grocery_item_id: 2, name: 'Apple' }),
|
|
],
|
|
shoppingLists: [],
|
|
setWatchedItems: vi.fn(),
|
|
setShoppingLists: vi.fn(),
|
|
isLoading: false,
|
|
error: null,
|
|
});
|
|
|
|
render(<ExtractedDataTable {...defaultProps} items={items} />);
|
|
|
|
const rows = screen.getAllByRole('row');
|
|
// Extract item names based on the bold/semibold classes used for names
|
|
const itemNames = rows.map((row) => {
|
|
const nameEl = row.querySelector('.font-bold, .font-semibold');
|
|
return nameEl?.textContent;
|
|
});
|
|
|
|
// Expected: Watched items first (Apple, Zebra), then Unwatched (Banana, Yam)
|
|
expect(itemNames).toEqual(['Apple', 'Zebra', 'Banana', 'Yam']);
|
|
});
|
|
});
|
|
|
|
describe('Data Edge Cases', () => {
|
|
it('should render correctly when masterItems list is empty', () => {
|
|
vi.mocked(useMasterItems).mockReturnValue({
|
|
masterItems: [], // No master items
|
|
isLoading: false,
|
|
error: null,
|
|
});
|
|
render(<ExtractedDataTable {...defaultProps} />);
|
|
// No canonical names should be resolved or displayed
|
|
expect(screen.queryByText(/\(Canonical: .*\)/)).not.toBeInTheDocument();
|
|
// If canonical name isn't resolved (because masterItems is empty), the Add to list button should NOT appear
|
|
const appleItemRow = screen.getByText('Gala Apples').closest('tr')!;
|
|
expect(
|
|
within(appleItemRow).queryByTitle('Select a shopping list first'),
|
|
).not.toBeInTheDocument();
|
|
});
|
|
|
|
it('should correctly format unit price for metric system', () => {
|
|
render(<ExtractedDataTable {...defaultProps} unitSystem="metric" />);
|
|
const chickenItemRow = screen.getByText('Boneless Chicken').closest('tr')!;
|
|
|
|
// --- Debugging Logs Start ---
|
|
console.log('--- DEBUG: Metric System Test Case ---');
|
|
|
|
// 1. Log the full HTML structure of the row to see how text is nested
|
|
console.log('DOM Structure:', prettyDOM(chickenItemRow));
|
|
|
|
// 2. Log all text content in the row to verify if '$8.00' exists in isolation
|
|
console.log('Row Text Content:', chickenItemRow?.textContent);
|
|
|
|
// 3. Find all elements that look like price containers to see their exact text
|
|
const potentialPrices = within(chickenItemRow).queryAllByText(/\$/);
|
|
console.log(`Found ${potentialPrices.length} elements containing "$":`);
|
|
potentialPrices.forEach((el, idx) => {
|
|
console.log(` [${idx}] Tag: <${el.tagName.toLowerCase()}>, Text: "${el.textContent}"`);
|
|
});
|
|
|
|
// 4. Test if strict matching is the cause of failure
|
|
const strictMatch = within(chickenItemRow).queryByText('$8.00');
|
|
const laxMatch = within(chickenItemRow).queryByText('$8.00', { exact: false });
|
|
console.log(`Match Check: Strict='$8.00' found? ${!!strictMatch}`);
|
|
console.log(`Match Check: Lax='$8.00' found? ${!!laxMatch}`);
|
|
console.log('--- DEBUG End ---');
|
|
// --- Debugging Logs End ---
|
|
|
|
// Fix: Use { exact: false } because the text is "$8.00/kg" (broken into "$8.00" + "/kg" or just one string)
|
|
// Based on the error log provided, it is likely inside a single span as "$8.00/kg"
|
|
expect(within(chickenItemRow).getByText('$8.00', { exact: false })).toBeInTheDocument();
|
|
|
|
// Check for the unit suffix, which might be in a separate element or part of the string
|
|
expect(within(chickenItemRow).getAllByText(/\/kg/i).length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it('should handle activeListId pointing to a non-existent list', () => {
|
|
vi.mocked(useShoppingLists).mockReturnValue({
|
|
activeListId: 999, // Non-existent
|
|
shoppingLists: mockShoppingLists,
|
|
addItemToList: mockAddItemToList,
|
|
setActiveListId: vi.fn(),
|
|
createList: vi.fn(),
|
|
deleteList: vi.fn(),
|
|
updateItemInList: vi.fn(),
|
|
removeItemFromList: vi.fn(),
|
|
isCreatingList: false,
|
|
isDeletingList: false,
|
|
isAddingItem: false,
|
|
isUpdatingItem: false,
|
|
isRemovingItem: false,
|
|
error: null,
|
|
});
|
|
|
|
render(<ExtractedDataTable {...defaultProps} />);
|
|
|
|
// Should behave as if item is not in list (Add button enabled)
|
|
const appleItemRow = screen.getByText('Gala Apples').closest('tr')!;
|
|
const addToListButton = within(appleItemRow).getByTitle('Add Apples to list');
|
|
expect(addToListButton).toBeInTheDocument();
|
|
expect(addToListButton).not.toBeDisabled();
|
|
});
|
|
|
|
it('should display numeric quantity in parentheses if available', () => {
|
|
const itemWithQtyNum = createMockFlyerItem({
|
|
flyer_item_id: 999,
|
|
item: 'Bulk Rice',
|
|
quantity: 'Bag',
|
|
quantity_num: 5,
|
|
unit_price: { value: 10, unit: 'kg' },
|
|
category_name: 'Pantry',
|
|
flyer_id: 1,
|
|
});
|
|
render(<ExtractedDataTable {...defaultProps} items={[itemWithQtyNum]} />);
|
|
expect(screen.getByText('(5)')).toBeInTheDocument();
|
|
});
|
|
});
|
|
});
|