1+ import { render , screen , fireEvent , waitFor } from '@testing-library/react' ;
2+ import { BlogPostList } from '../blog/blog-post-list' ;
3+ import { BlogPost } from '@/types' ;
4+
5+ // Mock data for testing
6+ const mockPosts : BlogPost [ ] = [
7+ {
8+ title : 'Building Offline-First Apps' ,
9+ slug : 'building-offline-first-apps' ,
10+ date : new Date ( '2025-01-15' ) ,
11+ excerpt : 'Real-time synchronization strategies for mobile applications' ,
12+ tags : [ 'React Native' , 'Offline' , 'Sync' ] ,
13+ category : 'Mobile Development' ,
14+ content : 'Full content here...' ,
15+ readingTime : 8 ,
16+ published : true ,
17+ } ,
18+ {
19+ title : 'Microservices Architecture Patterns' ,
20+ slug : 'microservices-architecture-patterns' ,
21+ date : new Date ( '2025-01-10' ) ,
22+ excerpt : 'Best practices for designing scalable microservices' ,
23+ tags : [ 'Microservices' , 'Architecture' , 'Scalability' ] ,
24+ category : 'Backend Development' ,
25+ content : 'Full content here...' ,
26+ readingTime : 12 ,
27+ published : true ,
28+ } ,
29+ {
30+ title : 'Modern CSS Grid Layouts' ,
31+ slug : 'modern-css-grid-layouts' ,
32+ date : new Date ( '2025-01-05' ) ,
33+ excerpt : 'Advanced CSS Grid techniques for responsive design' ,
34+ tags : [ 'CSS' , 'Grid' , 'Responsive' ] ,
35+ category : 'Frontend Development' ,
36+ content : 'Full content here...' ,
37+ readingTime : 6 ,
38+ published : true ,
39+ } ,
40+ ] ;
41+
42+ describe ( 'BlogPostList' , ( ) => {
43+ it ( 'renders list of blog posts' , ( ) => {
44+ render ( < BlogPostList posts = { mockPosts } /> ) ;
45+
46+ expect ( screen . getByText ( 'Building Offline-First Apps' ) ) . toBeInTheDocument ( ) ;
47+ expect ( screen . getByText ( 'Microservices Architecture Patterns' ) ) . toBeInTheDocument ( ) ;
48+ expect ( screen . getByText ( 'Modern CSS Grid Layouts' ) ) . toBeInTheDocument ( ) ;
49+ } ) ;
50+
51+ it ( 'displays category filter badges' , ( ) => {
52+ render ( < BlogPostList posts = { mockPosts } /> ) ;
53+
54+ // Use getAllByText to get all instances and check filter badges specifically
55+ const mobileBadges = screen . getAllByText ( 'Mobile Development' ) ;
56+ const backendBadges = screen . getAllByText ( 'Backend Development' ) ;
57+ const frontendBadges = screen . getAllByText ( 'Frontend Development' ) ;
58+
59+ expect ( mobileBadges . length ) . toBeGreaterThan ( 0 ) ;
60+ expect ( backendBadges . length ) . toBeGreaterThan ( 0 ) ;
61+ expect ( frontendBadges . length ) . toBeGreaterThan ( 0 ) ;
62+ } ) ;
63+
64+ it ( 'filters posts by category when badge is clicked' , async ( ) => {
65+ render ( < BlogPostList posts = { mockPosts } /> ) ;
66+
67+ // Get the first Mobile Development badge (filter badge)
68+ const mobileBadges = screen . getAllByText ( 'Mobile Development' ) ;
69+ const mobileBadge = mobileBadges [ 0 ] ; // Filter badge is first
70+ fireEvent . click ( mobileBadge ) ;
71+
72+ await waitFor ( ( ) => {
73+ expect ( screen . getByText ( 'Building Offline-First Apps' ) ) . toBeInTheDocument ( ) ;
74+ expect ( screen . queryByText ( 'Microservices Architecture Patterns' ) ) . not . toBeInTheDocument ( ) ;
75+ expect ( screen . queryByText ( 'Modern CSS Grid Layouts' ) ) . not . toBeInTheDocument ( ) ;
76+ } ) ;
77+ } ) ;
78+
79+ it ( 'filters posts by search term' , async ( ) => {
80+ render ( < BlogPostList posts = { mockPosts } /> ) ;
81+
82+ const searchInput = screen . getByPlaceholderText ( 'Search posts...' ) ;
83+ fireEvent . change ( searchInput , { target : { value : 'offline' } } ) ;
84+
85+ await waitFor ( ( ) => {
86+ expect ( screen . getByText ( 'Building Offline-First Apps' ) ) . toBeInTheDocument ( ) ;
87+ expect ( screen . queryByText ( 'Microservices Architecture Patterns' ) ) . not . toBeInTheDocument ( ) ;
88+ expect ( screen . queryByText ( 'Modern CSS Grid Layouts' ) ) . not . toBeInTheDocument ( ) ;
89+ } ) ;
90+ } ) ;
91+
92+ it ( 'sorts posts by date (newest first)' , ( ) => {
93+ render ( < BlogPostList posts = { mockPosts } /> ) ;
94+
95+ const posts = screen . getAllByTestId ( 'blog-post-card' ) ;
96+ expect ( posts ) . toHaveLength ( 3 ) ;
97+
98+ // Check that the first post is the newest (2025-01-15)
99+ expect ( screen . getByText ( 'Building Offline-First Apps' ) ) . toBeInTheDocument ( ) ;
100+ } ) ;
101+
102+ it ( 'displays empty state when no posts match filters' , async ( ) => {
103+ render ( < BlogPostList posts = { mockPosts } /> ) ;
104+
105+ const searchInput = screen . getByPlaceholderText ( 'Search posts...' ) ;
106+ fireEvent . change ( searchInput , { target : { value : 'nonexistent' } } ) ;
107+
108+ await waitFor ( ( ) => {
109+ expect ( screen . getByText ( 'No posts found' ) ) . toBeInTheDocument ( ) ;
110+ expect ( screen . getByText ( 'Try adjusting your search or filters' ) ) . toBeInTheDocument ( ) ;
111+ } ) ;
112+ } ) ;
113+
114+ it ( 'has responsive grid layout classes' , ( ) => {
115+ render ( < BlogPostList posts = { mockPosts } /> ) ;
116+
117+ const gridContainer = screen . getByTestId ( 'blog-posts-grid' ) ;
118+ expect ( gridContainer ) . toHaveClass ( 'grid' , 'grid-cols-1' , 'md:grid-cols-2' , 'lg:grid-cols-3' ) ;
119+ } ) ;
120+
121+ it ( 'clears filters when clear button is clicked' , async ( ) => {
122+ render ( < BlogPostList posts = { mockPosts } /> ) ;
123+
124+ // Apply a filter
125+ const searchInput = screen . getByPlaceholderText ( 'Search posts...' ) ;
126+ fireEvent . change ( searchInput , { target : { value : 'offline' } } ) ;
127+
128+ await waitFor ( ( ) => {
129+ expect ( screen . queryByText ( 'Microservices Architecture Patterns' ) ) . not . toBeInTheDocument ( ) ;
130+ } ) ;
131+
132+ // Clear filters
133+ const clearButton = screen . getByText ( 'Clear filters' ) ;
134+ fireEvent . click ( clearButton ) ;
135+
136+ await waitFor ( ( ) => {
137+ expect ( screen . getByText ( 'Microservices Architecture Patterns' ) ) . toBeInTheDocument ( ) ;
138+ expect ( screen . getByText ( 'Modern CSS Grid Layouts' ) ) . toBeInTheDocument ( ) ;
139+ } ) ;
140+ } ) ;
141+
142+ it ( 'debounces search input' , async ( ) => {
143+ jest . useFakeTimers ( ) ;
144+ render ( < BlogPostList posts = { mockPosts } /> ) ;
145+
146+ const searchInput = screen . getByPlaceholderText ( 'Search posts...' ) ;
147+ fireEvent . change ( searchInput , { target : { value : 'o' } } ) ;
148+ fireEvent . change ( searchInput , { target : { value : 'of' } } ) ;
149+ fireEvent . change ( searchInput , { target : { value : 'off' } } ) ;
150+
151+ // Fast forward timers to trigger debounced search
152+ jest . runAllTimers ( ) ;
153+
154+ await waitFor ( ( ) => {
155+ expect ( screen . getByText ( 'Building Offline-First Apps' ) ) . toBeInTheDocument ( ) ;
156+ } ) ;
157+
158+ jest . useRealTimers ( ) ;
159+ } ) ;
160+ } ) ;
0 commit comments