From d5b09bac128f7f895933b22795524aa1b9dc2e52 Mon Sep 17 00:00:00 2001 From: DammyAji Date: Fri, 26 Jun 2026 16:26:16 +0100 Subject: [PATCH] test: add integration tests for empty state on zero-result creator search (#452) --- ...andingPage.emptyState.integration.test.tsx | 172 ++++++++++++++++++ 1 file changed, 172 insertions(+) create mode 100644 src/pages/__tests__/LandingPage.emptyState.integration.test.tsx diff --git a/src/pages/__tests__/LandingPage.emptyState.integration.test.tsx b/src/pages/__tests__/LandingPage.emptyState.integration.test.tsx new file mode 100644 index 0000000..254ae44 --- /dev/null +++ b/src/pages/__tests__/LandingPage.emptyState.integration.test.tsx @@ -0,0 +1,172 @@ +import type { ComponentProps, ReactNode } from 'react'; +import { fireEvent, render, screen, waitFor } from '@testing-library/react'; +import { MemoryRouter } from 'react-router'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import LandingPage from '@/pages/LandingPage'; +import { courseService, type Course } from '@/services/course.service'; + +vi.mock('@/services/course.service', () => ({ + courseService: { getCourses: vi.fn() }, +})); + +vi.mock('@/hooks/useNetworkMismatch', () => ({ + useNetworkMismatch: () => ({ + isMismatch: false, + expectedChainName: 'Stellar Testnet', + }), +})); + +vi.mock('@/hooks/useStaleData', () => ({ + useStaleData: () => ({ + stale: false, + ageMs: 0, + msUntilStale: 60_000, + revalidate: vi.fn(), + }), +})); + +vi.mock('@/components/common/StellarConnectionQualityBadge', async () => { + const React = await import('react'); + return { + default: () => React.createElement('div', { role: 'status' }, 'RPC good'), + }; +}); + +vi.mock('@/components/common/CreatorCard', async () => { + const React = await import('react'); + return { + default: ({ creator }: { creator: { title: string } }) => + React.createElement( + 'article', + { 'aria-label': `Creator ${creator.title}` }, + creator.title + ), + }; +}); + +vi.mock('framer-motion', async () => { + const React = await import('react'); + type MotionDivProps = ComponentProps<'div'> & { + layout?: boolean; + transition?: unknown; + }; + return { + AnimatePresence: ({ children }: { children: ReactNode }) => + React.createElement(React.Fragment, null, children), + LayoutGroup: ({ children }: { children: ReactNode }) => + React.createElement(React.Fragment, null, children), + motion: { + div: ({ children, ...props }: MotionDivProps) => { + const { layout, transition, ...divProps } = props; + void layout; + void transition; + return React.createElement('div', divProps, children); + }, + h1: ({ children, ...props }: ComponentProps<'h1'>) => + React.createElement('h1', props, children), + button: ({ children, ...props }: ComponentProps<'button'>) => + React.createElement('button', props, children), + }, + }; +}); + +const mockGetCourses = vi.mocked(courseService.getCourses); + +const creator: Course = { + id: 'creator-a', + title: 'Creator Alpha', + description: 'Digital artist', + price: 0.05, + priceStroops: 500_000, + creatorShareSupply: 100, + instructorId: 'creator-a', + category: 'Art', + level: 'BEGINNER', + isVerified: true, +}; + +const mockMatchMedia = () => { + Object.defineProperty(window, 'matchMedia', { + writable: true, + value: vi.fn().mockImplementation((query: string) => ({ + matches: false, + media: query, + onchange: null, + addEventListener: vi.fn(), + removeEventListener: vi.fn(), + addListener: vi.fn(), + removeListener: vi.fn(), + dispatchEvent: vi.fn(), + })), + }); +}; + +describe('LandingPage empty state integration (#452)', () => { + beforeEach(() => { + mockMatchMedia(); + window.localStorage.clear(); + window.sessionStorage.clear(); + mockGetCourses.mockReset(); + }); + + it('renders the empty state when API returns zero creators and a search term is entered', async () => { + mockGetCourses.mockResolvedValue([]); + render(); + await waitFor(() => expect(mockGetCourses).toHaveBeenCalledTimes(1)); + + fireEvent.change( + screen.getByPlaceholderText(/search creators by name or handle/i), + { target: { value: 'nobody' } } + ); + + expect( + await screen.findByRole('status', { name: /no creators found/i }) + ).toBeInTheDocument(); + }); + + it('renders the empty state when no creators match the search query', async () => { + mockGetCourses.mockResolvedValue([creator]); + render(); + await waitFor(() => expect(mockGetCourses).toHaveBeenCalledTimes(1)); + + fireEvent.change( + screen.getByPlaceholderText(/search creators by name or handle/i), + { target: { value: 'xyznotfound' } } + ); + + expect( + await screen.findByRole('status', { name: /no creators found/i }) + ).toBeInTheDocument(); + }); + + it('clear button resets the search input, hides the empty state, and re-fetches the full list', async () => { + mockGetCourses.mockResolvedValue([creator]); + render(); + await waitFor(() => expect(mockGetCourses).toHaveBeenCalledTimes(1)); + + // Type a query that yields no matches + fireEvent.change( + screen.getByPlaceholderText(/search creators by name or handle/i), + { target: { value: 'xyznotfound' } } + ); + await screen.findByRole('status', { name: /no creators found/i }); + + // Click the "Reset Search" button rendered by EmptyState + fireEvent.click(screen.getByRole('button', { name: /reset search/i })); + + // Empty state must disappear and the creator card must reappear + await waitFor(() => { + expect( + screen.queryByRole('status', { name: /no creators found/i }) + ).not.toBeInTheDocument(); + }); + expect( + screen.getByRole('article', { name: /creator alpha/i }) + ).toBeInTheDocument(); + + // Search input must be cleared + expect( + screen.getByPlaceholderText(/search creators by name or handle/i) + ).toHaveValue(''); + }); +});