- All 8 home page sections: Hero, Locations slider, WhyParents accordion, Birthday pricing cards, Video, Gallery, Reviews slider, News - UI components: NavLink, BtnPrimary, BtnGradient, BtnDetails, AccordionItem - Layout: sticky Header (NavLink + BtnPrimary), Footer with logo - Figma Code Connect: 5 components published (.figma.tsx + figma.config.json) - Public assets: all Figma images and SVGs exported - Pages: /kvytky, /lokatsii, /blog, /dni-narodzhennia, /grupovi-vidviduvannia - Tests: Vitest unit/api suites, Playwright e2e screenshots - Payload CMS: blocks, collections, seed data updates - Hero negative-margin to extend behind sticky header - Custom Tailwind breakpoints: lg=1440px, xl=1920px - Fix ESLint config: drop FlatCompat, use eslint-config-next flat export - Add tsconfig.tsbuildinfo, test-results/, agentdb.rvf* to .gitignore Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
183 lines
5.4 KiB
TypeScript
183 lines
5.4 KiB
TypeScript
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
|
|
|
const mockFetch = vi.fn()
|
|
vi.stubGlobal('fetch', mockFetch)
|
|
|
|
let revalidateAfterChange: (args: {
|
|
doc: Record<string, unknown>
|
|
collection: { slug: string }
|
|
req: object
|
|
previousDoc: object
|
|
context: object
|
|
operation: string
|
|
}) => unknown
|
|
let revalidateGlobalAfterChange: (args: {
|
|
doc: object
|
|
global: { slug: string }
|
|
req: object
|
|
context: object
|
|
previousDoc: object
|
|
}) => unknown
|
|
|
|
beforeEach(async () => {
|
|
vi.resetModules()
|
|
mockFetch.mockReset()
|
|
vi.stubEnv('NEXT_PUBLIC_SITE_URL', 'http://localhost:3000')
|
|
vi.stubEnv('REVALIDATE_SECRET', 'test-revalidate-secret')
|
|
const mod = await import('@/hooks/revalidatePath')
|
|
revalidateAfterChange = mod.revalidateAfterChange as typeof revalidateAfterChange
|
|
revalidateGlobalAfterChange = mod.revalidateGlobalAfterChange as typeof revalidateGlobalAfterChange
|
|
})
|
|
|
|
afterEach(() => {
|
|
vi.restoreAllMocks()
|
|
})
|
|
|
|
describe('revalidateAfterChange', () => {
|
|
it('returns the doc unchanged (fire-and-forget pattern)', () => {
|
|
mockFetch.mockResolvedValueOnce({ ok: true, status: 200 })
|
|
const doc = { id: '1', slug: 'about-us' }
|
|
const result = revalidateAfterChange({
|
|
doc,
|
|
collection: { slug: 'pages' },
|
|
req: {},
|
|
previousDoc: {},
|
|
context: {},
|
|
operation: 'update',
|
|
})
|
|
expect(result).toBe(doc)
|
|
})
|
|
|
|
it('POSTs to /api/revalidate with slug and collection when doc has slug', async () => {
|
|
mockFetch.mockResolvedValueOnce({ ok: true })
|
|
revalidateAfterChange({
|
|
doc: { id: '1', slug: 'about-us' },
|
|
collection: { slug: 'pages' },
|
|
req: {},
|
|
previousDoc: {},
|
|
context: {},
|
|
operation: 'update',
|
|
})
|
|
await new Promise((r) => setTimeout(r, 0))
|
|
|
|
expect(mockFetch).toHaveBeenCalledOnce()
|
|
const [url, init] = mockFetch.mock.calls[0] as [string, RequestInit]
|
|
expect(url).toBe('http://localhost:3000/api/revalidate')
|
|
expect((init.headers as Record<string, string>)['Authorization']).toBe(
|
|
'Bearer test-revalidate-secret'
|
|
)
|
|
const body = JSON.parse(init.body as string)
|
|
expect(body).toMatchObject({ slug: 'about-us', collection: 'pages' })
|
|
})
|
|
|
|
it('sends slug as undefined when doc has no slug field', async () => {
|
|
mockFetch.mockResolvedValueOnce({ ok: true })
|
|
revalidateAfterChange({
|
|
doc: { id: '2' },
|
|
collection: { slug: 'media' },
|
|
req: {},
|
|
previousDoc: {},
|
|
context: {},
|
|
operation: 'create',
|
|
})
|
|
await new Promise((r) => setTimeout(r, 0))
|
|
|
|
const [, init] = mockFetch.mock.calls[0] as [string, RequestInit]
|
|
const body = JSON.parse(init.body as string)
|
|
expect(body.collection).toBe('media')
|
|
// doc.slug is undefined, coerced to undefined in JSON → omitted or undefined
|
|
expect(body.slug).toBeUndefined()
|
|
})
|
|
|
|
it('does not throw when fetch rejects (errors are swallowed)', () => {
|
|
mockFetch.mockRejectedValueOnce(new Error('network error'))
|
|
expect(() =>
|
|
revalidateAfterChange({
|
|
doc: { id: '1', slug: 'test' },
|
|
collection: { slug: 'pages' },
|
|
req: {},
|
|
previousDoc: {},
|
|
context: {},
|
|
operation: 'update',
|
|
})
|
|
).not.toThrow()
|
|
})
|
|
|
|
it('does not throw when revalidate endpoint returns non-ok (errors are swallowed)', async () => {
|
|
mockFetch.mockResolvedValueOnce({ ok: false, status: 401 })
|
|
expect(() =>
|
|
revalidateAfterChange({
|
|
doc: { id: '1', slug: 'test' },
|
|
collection: { slug: 'pages' },
|
|
req: {},
|
|
previousDoc: {},
|
|
context: {},
|
|
operation: 'update',
|
|
})
|
|
).not.toThrow()
|
|
})
|
|
|
|
it('uses NEXT_PUBLIC_SITE_URL env for the request URL', async () => {
|
|
vi.stubEnv('NEXT_PUBLIC_SITE_URL', 'https://shumiland.ua')
|
|
vi.resetModules()
|
|
const mod = await import('@/hooks/revalidatePath')
|
|
mockFetch.mockResolvedValueOnce({ ok: true })
|
|
;(
|
|
mod.revalidateAfterChange as typeof revalidateAfterChange
|
|
)({
|
|
doc: { id: '1', slug: 'home' },
|
|
collection: { slug: 'pages' },
|
|
req: {},
|
|
previousDoc: {},
|
|
context: {},
|
|
operation: 'update',
|
|
})
|
|
await new Promise((r) => setTimeout(r, 0))
|
|
expect(mockFetch.mock.calls[0]?.[0]).toBe('https://shumiland.ua/api/revalidate')
|
|
})
|
|
})
|
|
|
|
describe('revalidateGlobalAfterChange', () => {
|
|
it('returns the doc unchanged', () => {
|
|
mockFetch.mockResolvedValueOnce({ ok: true })
|
|
const doc = { id: 'g1' }
|
|
const result = revalidateGlobalAfterChange({
|
|
doc,
|
|
global: { slug: 'header' },
|
|
req: {},
|
|
context: {},
|
|
previousDoc: {},
|
|
})
|
|
expect(result).toBe(doc)
|
|
})
|
|
|
|
it('POSTs to /api/revalidate with global slug', async () => {
|
|
mockFetch.mockResolvedValueOnce({ ok: true })
|
|
revalidateGlobalAfterChange({
|
|
doc: {},
|
|
global: { slug: 'header' },
|
|
req: {},
|
|
context: {},
|
|
previousDoc: {},
|
|
})
|
|
await new Promise((r) => setTimeout(r, 0))
|
|
|
|
const [url, init] = mockFetch.mock.calls[0] as [string, RequestInit]
|
|
expect(url).toBe('http://localhost:3000/api/revalidate')
|
|
const body = JSON.parse(init.body as string)
|
|
expect(body).toEqual({ global: 'header' })
|
|
})
|
|
|
|
it('does not throw when fetch rejects', () => {
|
|
mockFetch.mockRejectedValueOnce(new Error('timeout'))
|
|
expect(() =>
|
|
revalidateGlobalAfterChange({
|
|
doc: {},
|
|
global: { slug: 'footer' },
|
|
req: {},
|
|
context: {},
|
|
previousDoc: {},
|
|
})
|
|
).not.toThrow()
|
|
})
|
|
})
|