Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 4 additions & 12 deletions src/components/catalog/CatalogContent.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { memo, useMemo, useCallback, type RefObject, useEffect } from 'react';
import { memo, useMemo, useCallback, type RefObject } from 'react';
import type { ActiveColorFilter } from '@/utils/color-image-resolver';
import { Skeleton } from '@/components/ui/skeleton';
import { cn } from '@/lib/utils';
Expand All @@ -21,9 +21,6 @@ import { SparklineSalesProvider } from '@/hooks/intelligence/useSparklineSales';
import { ProductLeafCategoryProvider } from '@/hooks/products/useProductLeafCategories';
import { ScrollToTopButton } from '@/components/common/ScrollToTopButton';

// Diagnostic counter
let catalogRenderCount = 0;

interface CatalogContentProps {
viewMode: ViewMode;
shouldShowCatalogSkeleton: boolean;
Expand Down Expand Up @@ -87,11 +84,6 @@ export const CatalogContent = memo(function CatalogContent({
setActiveProductId: _setActiveProductId,
hideCategoryBadges = false,
}: CatalogContentProps) {
catalogRenderCount++;
if (process.env.NODE_ENV === 'development') {
console.log(`[CatalogContent] Render #${catalogRenderCount} - viewMode: ${viewMode}, gridColumns: ${gridColumns}, products: ${paginatedProducts.length}`);
}

const selection = useCatalogSelection(paginatedProducts, selectionMode, onSelectedCountChange);
const { selectedIds, toggleSelect: onToggleSelect } = selection;

Expand Down Expand Up @@ -158,10 +150,10 @@ export const CatalogContent = memo(function CatalogContent({
}

return (
<div
<div
className={cn(
"relative space-y-8 pb-12 duration-500 animate-in fade-in",
isLoadingMore && "opacity-80 transition-opacity"
'relative space-y-8 pb-12 duration-500 animate-in fade-in',
isLoadingMore && 'opacity-80 transition-opacity',
)}
>
<SparklineSalesProvider productIds={productIds}>
Expand Down
5 changes: 1 addition & 4 deletions src/components/common/ScrollProgress.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
import { useState, useEffect, useRef, forwardRef, useCallback } from 'react';
import { motion } from 'framer-motion';
import { useEffect, useRef } from 'react';
import { cn } from '@/lib/utils';
import { ArrowUp } from 'lucide-react';
import { useAriaLive } from '@/components/a11y';

interface ScrollProgressProps {
className?: string;
Expand Down
5 changes: 1 addition & 4 deletions src/components/novelties/NoveltyProductGrid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,8 @@ import { VirtualizedNoveltyGrid } from './VirtualizedNoveltyGrid';
import { sortProducts } from '@/utils/product-sorting';
import { SORT_OPTIONS } from '@/constants/filters';


type ViewMode = 'grid' | 'list' | 'table';


function getGridColsClass(cols: ColumnCount): string {
switch (cols) {
case 3:
Expand Down Expand Up @@ -154,9 +152,8 @@ export function NoveltyProductGrid() {
filtered = filtered.filter((p) => p.supplier_id === selectedSupplier);
if (selectedCategory !== 'all')
filtered = filtered.filter((p) => p.category_id === selectedCategory);
sortProducts(filtered as unknown as any[], sortMode);
sortProducts(filtered as unknown as Parameters<typeof sortProducts>[0], sortMode);
return filtered;

}, [products, selectedSupplier, selectedCategory, sortMode, searchQuery]);

// Reset to first page when filters change
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,23 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import '@testing-library/jest-dom';

import { render, screen, fireEvent, act, waitFor } from '@testing-library/react';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { NoveltyProductGrid } from '../NoveltyProductGrid';
import { BrowserRouter } from 'react-router-dom';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { TooltipProvider } from '@/components/ui/tooltip';
import * as React from 'react';
import { SORT_OPTIONS } from '@/constants/filters';
import type { ReactNode } from 'react';
import type { NoveltyWithDetails } from '@/hooks/products/useNovelties';

// Helper: find an input by partial placeholder text (testing-library does
// not expose this matcher out-of-the-box; the production placeholder includes
// the keyboard-shortcut hint and ellipsis so exact match is brittle).
function getByPlaceholderPartial(text: string): HTMLInputElement {
const inputs = screen.getAllByRole('textbox');
const match = inputs.find((i) => (i as HTMLInputElement).placeholder.includes(text.trim()));
if (!match) throw new Error(`No textbox with placeholder containing "${text}"`);
return match as HTMLInputElement;
}

// Mock dependencies
vi.mock('@/hooks/products', () => ({
Expand All @@ -26,7 +36,7 @@ vi.mock('@/hooks/products', () => ({
stock_quantity: 100,
min_quantity: 10,
days_remaining: 30,
status: 'active'
status: 'active',
},
{
product_id: '2',
Expand All @@ -41,18 +51,18 @@ vi.mock('@/hooks/products', () => ({
stock_quantity: 50,
min_quantity: 10,
days_remaining: 30,
status: 'active'
}
status: 'active',
},
],
isLoading: false,
isFetching: false,
error: null,
})),
useNoveltiesSelectionMode: vi.fn(({ filteredProducts }) => ({
useNoveltiesSelectionMode: vi.fn(() => ({
selectedIds: new Set(),
toggleSelect: vi.fn(),
clearSelection: vi.fn(),
noveltyToProduct: (n: any) => ({
noveltyToProduct: (n: NoveltyWithDetails) => ({
id: n.product_id,
name: n.product_name || '',
product_name: n.product_name || '',
Expand All @@ -64,12 +74,11 @@ vi.mock('@/hooks/products', () => ({
images: [n.product_image],
colors: [],
materials: [],
tags: { publicoAlvo: [], datasComemorativas: [], endomarketing: [], ramo: [], nicho: [] }
tags: { publicoAlvo: [], datasComemorativas: [], endomarketing: [], ramo: [], nicho: [] },
}),
})),
}));


vi.mock('@/stores/useFavoritesStore', () => ({
useFavoritesStore: vi.fn(() => ({
isFavorite: vi.fn(() => false),
Expand Down Expand Up @@ -105,9 +114,9 @@ vi.mock('@/components/products/LayoutPopover', () => ({

// Mock Virtualized Grid to render synchronously
vi.mock('../VirtualizedNoveltyGrid', () => ({
VirtualizedNoveltyGrid: ({ products, onProductClick, selectionMode, selectedIds, onToggleSelect }: any) => (
VirtualizedNoveltyGrid: ({ products }: { products: NoveltyWithDetails[] }) => (
<div data-testid="mock-virtualized-grid">
{products.map((p: any) => (
{products.map((p) => (
<div key={p.novelty_id} role="listitem">
<h3>{p.product_name}</h3>
<span>R$ {p.base_price}</span>
Expand All @@ -118,16 +127,13 @@ vi.mock('../VirtualizedNoveltyGrid', () => ({
}));

const queryClient = new QueryClient({

defaultOptions: { queries: { retry: false } },
});

const wrapper = ({ children }: { children: React.ReactNode }) => (
const wrapper = ({ children }: { children: ReactNode }) => (
<QueryClientProvider client={queryClient}>
<BrowserRouter>
<TooltipProvider>
{children}
</TooltipProvider>
<TooltipProvider>{children}</TooltipProvider>
</BrowserRouter>
</QueryClientProvider>
);
Expand All @@ -139,23 +145,22 @@ describe('NoveltyProductGrid Integration - Sort and Counters', () => {

it('renders products and shows correct count badge', () => {
render(<NoveltyProductGrid />, { wrapper });

expect(screen.getByText('Caneta A')).toBeInTheDocument();
expect(screen.getByText('Caneta B')).toBeInTheDocument();


// Count badge should show 2
const badge = screen.getByText('2');
expect(badge).toBeDefined();
});

it('filters by search and updates badge', async () => {
render(<NoveltyProductGrid />, { wrapper });
const searchInput = screen.getByPlaceholderRelative('Buscar novidades… /');

const searchInput = getByPlaceholderPartial('Buscar novidades');

fireEvent.change(searchInput, { target: { value: 'Caneta A' } });

await waitFor(() => {
expect(screen.queryByText('Caneta B')).toBeNull();
expect(screen.getByText('Caneta A')).toBeInTheDocument();
Expand All @@ -166,38 +171,24 @@ describe('NoveltyProductGrid Integration - Sort and Counters', () => {
});
});


it('sorts locally by price-asc', async () => {
render(<NoveltyProductGrid />, { wrapper });

// Default is newest (Caneta B then Caneta A)
const items = screen.getAllByRole('heading', { level: 3 }); // Assuming product names are h3 in cards
// In Virtualized grid, it might be different. Let's look for text content order if possible.


// Find sort select and change to price-asc
const selects = screen.getAllByRole('combobox');
const sortSelect = selects[2];

fireEvent.click(sortSelect);
const ascOption = screen.getByText('Preço (Menor → Maior)');
fireEvent.click(ascOption);

// After sorting by price asc, Caneta B (5) should be before Caneta A (10)
// Actually, newest was B then A. So order didn't change for B, but B is cheaper.
});
Comment on lines 174 to 187

it('resets page to 1 when filters change', async () => {
// This is hard to test without many products, but we can verify the useEffect dependency
render(<NoveltyProductGrid />, { wrapper });
// If it didn't crash and we see the products, initial state is ok
});
});

// Helper for finding elements with partial text in placeholder/aria
const screen = {
...require('@testing-library/react').screen,
getByPlaceholderRelative: (text: string) => {
const inputs = require('@testing-library/react').screen.getAllByRole('textbox');
return inputs.find((i: any) => i.placeholder.includes(text.trim())) as HTMLInputElement;
}
};
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, it, expect, vi } from 'vitest';
import { render, screen, fireEvent, within } from '@testing-library/react';
import { render, screen } from '@testing-library/react';
import { StickyFilterBar } from '@/components/filters/StickyFilterBar';
import { BrowserRouter } from 'react-router-dom';
import { TooltipProvider } from '@/components/ui/tooltip';
Expand All @@ -20,22 +20,20 @@ describe('ProductSort Accessibility and UI', () => {

const Wrapper = ({ children }: { children: React.ReactNode }) => (
<BrowserRouter>
<TooltipProvider>
{children}
</TooltipProvider>
<TooltipProvider>{children}</TooltipProvider>
</BrowserRouter>
);

it('should have correct accessibility attributes on the sort select', () => {
render(
<Wrapper>
<StickyFilterBar {...defaultProps} />
</Wrapper>
</Wrapper>,
);

const combobox = screen.getByRole('combobox');
expect(combobox).toBeDefined();

// Radix UI Select usually provides these
expect(combobox).toHaveAttribute('aria-expanded');
expect(combobox).toHaveAttribute('aria-autocomplete', 'none');
Expand All @@ -45,7 +43,7 @@ describe('ProductSort Accessibility and UI', () => {
const { rerender } = render(
<Wrapper>
<StickyFilterBar {...defaultProps} sortBy="name" />
</Wrapper>
</Wrapper>,
);

// Initial value check (usually displayed in the trigger)
Expand All @@ -54,7 +52,7 @@ describe('ProductSort Accessibility and UI', () => {
rerender(
<Wrapper>
<StickyFilterBar {...defaultProps} sortBy="price-asc" />
</Wrapper>
</Wrapper>,
);

expect(screen.getByText(/Preço \(Menor → Maior\)/i)).toBeDefined();
Expand All @@ -64,7 +62,7 @@ describe('ProductSort Accessibility and UI', () => {
render(
<Wrapper>
<StickyFilterBar {...defaultProps} activeFiltersCount={3} />
</Wrapper>
</Wrapper>,
);

// The "3" should appear in a badge
Expand Down
Loading
Loading