Skip to content

Commit d93a243

Browse files
committed
Add Instagram feed to homepage with duplicate filtering
- Add InstagramFeed component to display posts in a grid - Add instagram.js lib for fetching posts from Graph API - Add instagramFilter.js lib to suppress posts that have matching blog entries - Add instagramUrl field to blog posts in TinaCMS - Integrate feed into homepage, fetching up to 8 posts at build time
1 parent eff69e2 commit d93a243

10 files changed

Lines changed: 805 additions & 2 deletions

File tree

__tests__/instagram.test.js

Lines changed: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
1+
import {describe, it, expect, vi, beforeEach, afterEach} from 'vitest'
2+
import {
3+
createMockFetch,
4+
createTestLifecycle,
5+
withEnvVars,
6+
testData,
7+
} from './test-utils'
8+
9+
const lifecycle = createTestLifecycle()
10+
11+
describe('instagram', () => {
12+
const envHelper = withEnvVars({
13+
INSTAGRAM_BUSINESS_ACCOUNT_ID: 'test-account-id',
14+
INSTAGRAM_ACCESS_TOKEN: 'test-access-token',
15+
})
16+
17+
beforeEach(() => {
18+
lifecycle.beforeEach()
19+
envHelper.setup()
20+
})
21+
22+
afterEach(() => {
23+
lifecycle.afterEach()
24+
envHelper.teardown()
25+
})
26+
27+
describe('formatInstagramPost', () => {
28+
it('formats IMAGE type post correctly', async () => {
29+
const {formatInstagramPost} = await import('../lib/instagram')
30+
const post = testData.instagramPost({
31+
id: 'img123',
32+
media_type: 'IMAGE',
33+
media_url: 'https://example.com/photo.jpg',
34+
permalink: 'https://instagram.com/p/img123/',
35+
caption: 'Photo caption',
36+
timestamp: '2024-01-15T12:00:00Z',
37+
})
38+
39+
const result = formatInstagramPost(post)
40+
41+
expect(result.id).toBe('img123')
42+
expect(result.type).toBe('image')
43+
expect(result.imageUrl).toBe('https://example.com/photo.jpg')
44+
expect(result.videoUrl).toBeNull()
45+
expect(result.permalink).toBe('https://instagram.com/p/img123/')
46+
expect(result.caption).toBe('Photo caption')
47+
})
48+
49+
it('formats VIDEO type post with thumbnail', async () => {
50+
const {formatInstagramPost} = await import('../lib/instagram')
51+
const post = testData.instagramPost({
52+
id: 'vid456',
53+
media_type: 'VIDEO',
54+
media_url: 'https://example.com/video.mp4',
55+
thumbnail_url: 'https://example.com/thumb.jpg',
56+
permalink: 'https://instagram.com/p/vid456/',
57+
caption: 'Video caption',
58+
timestamp: '2024-01-15T12:00:00Z',
59+
})
60+
61+
const result = formatInstagramPost(post)
62+
63+
expect(result.type).toBe('video')
64+
expect(result.imageUrl).toBe('https://example.com/thumb.jpg')
65+
expect(result.videoUrl).toBe('https://example.com/video.mp4')
66+
})
67+
68+
it('formats CAROUSEL_ALBUM type correctly', async () => {
69+
const {formatInstagramPost} = await import('../lib/instagram')
70+
const post = testData.instagramPost({
71+
id: 'car789',
72+
media_type: 'CAROUSEL_ALBUM',
73+
media_url: 'https://example.com/carousel.jpg',
74+
})
75+
76+
const result = formatInstagramPost(post)
77+
78+
expect(result.type).toBe('carousel_album')
79+
expect(result.imageUrl).toBe('https://example.com/carousel.jpg')
80+
expect(result.videoUrl).toBeNull()
81+
})
82+
83+
it('handles lowercase media_type', async () => {
84+
const {formatInstagramPost} = await import('../lib/instagram')
85+
const post = testData.instagramPost({media_type: 'image'})
86+
87+
const result = formatInstagramPost(post)
88+
89+
expect(result.type).toBe('image')
90+
})
91+
92+
it('handles empty/missing caption', async () => {
93+
const {formatInstagramPost} = await import('../lib/instagram')
94+
const post = testData.instagramPost({caption: undefined})
95+
96+
const result = formatInstagramPost(post)
97+
98+
expect(result.caption).toBe('')
99+
})
100+
101+
it('converts timestamp to Date object', async () => {
102+
const {formatInstagramPost} = await import('../lib/instagram')
103+
const post = testData.instagramPost({timestamp: '2024-01-15T12:00:00Z'})
104+
105+
const result = formatInstagramPost(post)
106+
107+
expect(result.date).toBeInstanceOf(Date)
108+
expect(result.date.toISOString()).toBe('2024-01-15T12:00:00.000Z')
109+
})
110+
})
111+
112+
describe('fetchInstagramPosts', () => {
113+
it('returns empty array when credentials missing', async () => {
114+
envHelper.teardown() // Remove credentials
115+
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
116+
117+
const {fetchInstagramPosts} = await import('../lib/instagram')
118+
const result = await fetchInstagramPosts()
119+
120+
expect(result).toEqual([])
121+
expect(warnSpy).toHaveBeenCalledWith(
122+
expect.stringContaining('Instagram credentials not configured')
123+
)
124+
warnSpy.mockRestore()
125+
})
126+
127+
it('calls correct API URL with credentials', async () => {
128+
const mockFetch = createMockFetch({data: []})
129+
global.fetch = mockFetch
130+
131+
const {fetchInstagramPosts} = await import('../lib/instagram')
132+
await fetchInstagramPosts()
133+
134+
expect(mockFetch).toHaveBeenCalledTimes(1)
135+
const callUrl = mockFetch.mock.calls[0][0]
136+
expect(callUrl).toContain('graph.facebook.com')
137+
expect(callUrl).toContain('test-account-id')
138+
expect(callUrl).toContain('access_token=test-access-token')
139+
})
140+
141+
it('uses default limit of 12', async () => {
142+
const mockFetch = createMockFetch({data: []})
143+
global.fetch = mockFetch
144+
145+
const {fetchInstagramPosts} = await import('../lib/instagram')
146+
await fetchInstagramPosts()
147+
148+
const callUrl = mockFetch.mock.calls[0][0]
149+
expect(callUrl).toContain('limit=12')
150+
})
151+
152+
it('uses custom limit when provided', async () => {
153+
const mockFetch = createMockFetch({data: []})
154+
global.fetch = mockFetch
155+
156+
const {fetchInstagramPosts} = await import('../lib/instagram')
157+
await fetchInstagramPosts({limit: 5})
158+
159+
const callUrl = mockFetch.mock.calls[0][0]
160+
expect(callUrl).toContain('limit=5')
161+
})
162+
163+
it('returns posts from successful response', async () => {
164+
const mockPosts = [
165+
testData.instagramPost({id: 'post1'}),
166+
testData.instagramPost({id: 'post2'}),
167+
]
168+
global.fetch = createMockFetch({data: mockPosts})
169+
170+
const {fetchInstagramPosts} = await import('../lib/instagram')
171+
const result = await fetchInstagramPosts()
172+
173+
expect(result).toHaveLength(2)
174+
expect(result[0].id).toBe('post1')
175+
expect(result[1].id).toBe('post2')
176+
})
177+
178+
it('returns empty array on API error response', async () => {
179+
global.fetch = createMockFetch(
180+
{error: {message: 'Invalid token'}},
181+
{ok: false, status: 400}
182+
)
183+
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
184+
185+
const {fetchInstagramPosts} = await import('../lib/instagram')
186+
const result = await fetchInstagramPosts()
187+
188+
expect(result).toEqual([])
189+
expect(errorSpy).toHaveBeenCalledWith(
190+
'Instagram API error:',
191+
expect.any(Object)
192+
)
193+
errorSpy.mockRestore()
194+
})
195+
196+
it('returns empty array on network error', async () => {
197+
global.fetch = vi.fn().mockRejectedValue(new Error('Network error'))
198+
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
199+
200+
const {fetchInstagramPosts} = await import('../lib/instagram')
201+
const result = await fetchInstagramPosts()
202+
203+
expect(result).toEqual([])
204+
expect(errorSpy).toHaveBeenCalledWith(
205+
'Failed to fetch Instagram posts:',
206+
expect.any(Error)
207+
)
208+
errorSpy.mockRestore()
209+
})
210+
})
211+
212+
describe('getInstagramFeed', () => {
213+
it('fetches and formats posts', async () => {
214+
const mockPosts = [
215+
testData.instagramPost({id: 'feed1', media_type: 'IMAGE'}),
216+
testData.instagramPost({id: 'feed2', media_type: 'VIDEO'}),
217+
]
218+
global.fetch = createMockFetch({data: mockPosts})
219+
220+
const {getInstagramFeed} = await import('../lib/instagram')
221+
const result = await getInstagramFeed()
222+
223+
expect(result).toHaveLength(2)
224+
expect(result[0].id).toBe('feed1')
225+
expect(result[0].type).toBe('image')
226+
expect(result[1].id).toBe('feed2')
227+
expect(result[1].type).toBe('video')
228+
})
229+
230+
it('passes limit through to fetchInstagramPosts', async () => {
231+
const mockFetch = createMockFetch({data: []})
232+
global.fetch = mockFetch
233+
234+
const {getInstagramFeed} = await import('../lib/instagram')
235+
await getInstagramFeed({limit: 8})
236+
237+
const callUrl = mockFetch.mock.calls[0][0]
238+
expect(callUrl).toContain('limit=8')
239+
})
240+
241+
it('returns empty array when fetch returns empty', async () => {
242+
global.fetch = createMockFetch({data: []})
243+
244+
const {getInstagramFeed} = await import('../lib/instagram')
245+
const result = await getInstagramFeed()
246+
247+
expect(result).toEqual([])
248+
})
249+
})
250+
})

0 commit comments

Comments
 (0)