/** * @vitest-environment node */ import { describe, it, expect, beforeEach, vi } from 'vitest'; import type { ListingDetail } from '../listings-api'; // Provide a minimal localStorage mock before importing the store module // (Zustand persist needs getItem/setItem) const store: Record = {}; vi.stubGlobal('localStorage', { getItem: (key: string) => store[key] ?? null, setItem: (key: string, value: string) => { store[key] = value; }, removeItem: (key: string) => { delete store[key]; }, clear: () => { for (const k of Object.keys(store)) delete store[k]; }, get length() { return Object.keys(store).length; }, key: (i: number) => Object.keys(store)[i] ?? null, }); // Now import after mocks are in place const { useComparisonStore, computeComparisonStats, MAX_COMPARE, MIN_COMPARE } = await import('../comparison-store'); // Reset Zustand store between tests beforeEach(() => { useComparisonStore.setState({ selectedIds: [], listings: [], isLoading: false, error: null, }); }); // --------------------------------------------------------------------------- // Store — addToCompare / removeFromCompare / isSelected // --------------------------------------------------------------------------- describe('useComparisonStore', () => { it('adds a listing ID to the comparison set', () => { const { addToCompare } = useComparisonStore.getState(); const result = addToCompare('listing-1'); expect(result).toBe(true); expect(useComparisonStore.getState().selectedIds).toEqual(['listing-1']); }); it('does not add duplicate IDs', () => { const { addToCompare } = useComparisonStore.getState(); addToCompare('listing-1'); const result = addToCompare('listing-1'); expect(result).toBe(false); expect(useComparisonStore.getState().selectedIds).toEqual(['listing-1']); }); it('respects MAX_COMPARE limit', () => { const { addToCompare } = useComparisonStore.getState(); for (let i = 1; i <= MAX_COMPARE; i++) { expect(addToCompare(`listing-${i}`)).toBe(true); } const result = addToCompare(`listing-${MAX_COMPARE + 1}`); expect(result).toBe(false); expect(useComparisonStore.getState().selectedIds.length).toBe(MAX_COMPARE); }); it('removes a listing ID from the comparison set', () => { const store = useComparisonStore.getState(); store.addToCompare('listing-1'); store.addToCompare('listing-2'); store.removeFromCompare('listing-1'); expect(useComparisonStore.getState().selectedIds).toEqual(['listing-2']); }); it('isSelected returns correct value', () => { const store = useComparisonStore.getState(); store.addToCompare('listing-1'); expect(store.isSelected('listing-1')).toBe(true); expect(store.isSelected('listing-2')).toBe(false); }); it('clearAll resets the store', () => { const store = useComparisonStore.getState(); store.addToCompare('listing-1'); store.addToCompare('listing-2'); store.clearAll(); const state = useComparisonStore.getState(); expect(state.selectedIds).toEqual([]); expect(state.listings).toEqual([]); expect(state.error).toBeNull(); }); it('canCompare returns true when >= MIN_COMPARE items selected', () => { const store = useComparisonStore.getState(); store.addToCompare('listing-1'); expect(useComparisonStore.getState().canCompare()).toBe(false); store.addToCompare('listing-2'); expect(useComparisonStore.getState().canCompare()).toBe(true); }); it('canAdd returns false when MAX_COMPARE items selected', () => { const store = useComparisonStore.getState(); for (let i = 1; i <= MAX_COMPARE; i++) { store.addToCompare(`listing-${i}`); } expect(useComparisonStore.getState().canAdd()).toBe(false); }); it('setListings stores listing data and clears loading/error', () => { const store = useComparisonStore.getState(); store.setLoading(true); store.setError('some error'); const mockListings = [makeListing('1'), makeListing('2')]; store.setListings(mockListings); const state = useComparisonStore.getState(); expect(state.listings).toEqual(mockListings); expect(state.isLoading).toBe(false); expect(state.error).toBeNull(); }); it('removeFromCompare also removes from listings', () => { const store = useComparisonStore.getState(); store.addToCompare('listing-1'); store.addToCompare('listing-2'); store.setListings([makeListing('listing-1'), makeListing('listing-2')]); store.removeFromCompare('listing-1'); const state = useComparisonStore.getState(); expect(state.selectedIds).toEqual(['listing-2']); expect(state.listings.length).toBe(1); expect(state.listings[0]?.id).toBe('listing-2'); }); }); // --------------------------------------------------------------------------- // computeComparisonStats // --------------------------------------------------------------------------- describe('computeComparisonStats', () => { it('returns null for fewer than MIN_COMPARE listings', () => { expect(computeComparisonStats([])).toBeNull(); expect(computeComparisonStats([makeListing('1')])).toBeNull(); }); it('computes correct price range', () => { const listings = [ makeListing('1', { priceVND: '1000000000' }), makeListing('2', { priceVND: '2000000000' }), makeListing('3', { priceVND: '3000000000' }), ]; const stats = computeComparisonStats(listings); expect(stats).not.toBeNull(); expect(stats!.priceRange.min).toBe(1_000_000_000); expect(stats!.priceRange.max).toBe(3_000_000_000); expect(stats!.priceRange.avg).toBe(2_000_000_000); }); it('computes correct area range', () => { const listings = [ makeListing('1', { areaM2: 50 }), makeListing('2', { areaM2: 100 }), ]; const stats = computeComparisonStats(listings); expect(stats).not.toBeNull(); expect(stats!.areaRange.min).toBe(50); expect(stats!.areaRange.max).toBe(100); expect(stats!.areaRange.avg).toBe(75); }); it('computes pricePerM2Range when data is available', () => { const listings = [ makeListing('1', { pricePerM2: 20_000_000 }), makeListing('2', { pricePerM2: 40_000_000 }), ]; const stats = computeComparisonStats(listings); expect(stats).not.toBeNull(); expect(stats!.pricePerM2Range).not.toBeNull(); expect(stats!.pricePerM2Range!.min).toBe(20_000_000); expect(stats!.pricePerM2Range!.max).toBe(40_000_000); expect(stats!.pricePerM2Range!.avg).toBe(30_000_000); }); it('returns null pricePerM2Range when no pricePerM2 data', () => { const listings = [ makeListing('1', { pricePerM2: null }), makeListing('2', { pricePerM2: null }), ]; const stats = computeComparisonStats(listings); expect(stats).not.toBeNull(); expect(stats!.pricePerM2Range).toBeNull(); }); it('handles mixed pricePerM2 availability', () => { const listings = [ makeListing('1', { pricePerM2: 30_000_000 }), makeListing('2', { pricePerM2: null }), makeListing('3', { pricePerM2: 50_000_000 }), ]; const stats = computeComparisonStats(listings); expect(stats).not.toBeNull(); expect(stats!.pricePerM2Range).not.toBeNull(); expect(stats!.pricePerM2Range!.min).toBe(30_000_000); expect(stats!.pricePerM2Range!.max).toBe(50_000_000); expect(stats!.pricePerM2Range!.avg).toBe(40_000_000); }); }); // --------------------------------------------------------------------------- // Constants // --------------------------------------------------------------------------- describe('constants', () => { it('MAX_COMPARE is 5', () => { expect(MAX_COMPARE).toBe(5); }); it('MIN_COMPARE is 2', () => { expect(MIN_COMPARE).toBe(2); }); }); // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- function makeListing( id: string, overrides: { priceVND?: string; areaM2?: number; pricePerM2?: number | null; } = {}, ): ListingDetail { return { id, status: 'ACTIVE', transactionType: 'SALE', priceVND: overrides.priceVND ?? '1500000000', pricePerM2: overrides.pricePerM2 !== undefined ? overrides.pricePerM2 : 30_000_000, rentPriceMonthly: null, commissionPct: null, viewCount: 0, saveCount: 0, inquiryCount: 0, publishedAt: null, createdAt: '2026-01-01T00:00:00Z', property: { id: `prop-${id}`, propertyType: 'APARTMENT', title: `Test Property ${id}`, description: 'A test property', address: '123 Test St', ward: 'Ward 1', district: 'District 1', city: 'Ho Chi Minh', areaM2: overrides.areaM2 ?? 80, bedrooms: 2, bathrooms: 2, floors: null, direction: null, yearBuilt: null, legalStatus: null, amenities: [], projectName: null, latitude: null, longitude: null, media: [], }, seller: { id: 'seller-1', fullName: 'Test Seller', phone: '0912345678', }, agent: null, }; }