- 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>
158 lines
5.4 KiB
TypeScript
158 lines
5.4 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
|
|
const mockFetch = vi.fn()
|
|
vi.stubGlobal('fetch', mockFetch)
|
|
|
|
function okResponse(data: unknown): Response {
|
|
return {
|
|
ok: true,
|
|
status: 200,
|
|
json: async () => data,
|
|
} as unknown as Response
|
|
}
|
|
|
|
function errorResponse(status: number): Response {
|
|
return { ok: false, status, json: async () => ({}) } as unknown as Response
|
|
}
|
|
|
|
// Reset module cache between tests so tariffsCache is cleared
|
|
let getTariffs: () => Promise<unknown>
|
|
let createPayment: (
|
|
email: string,
|
|
items: Array<{ tariff: string; count: string }>
|
|
) => Promise<{ url: string }>
|
|
|
|
beforeEach(async () => {
|
|
vi.resetModules()
|
|
mockFetch.mockReset()
|
|
vi.stubEnv('EZY_ACTIVITY', 'test-activity')
|
|
vi.stubEnv('EZY_PARTNER_KEY', 'test-partner-key')
|
|
const mod = await import('@/lib/ezy')
|
|
getTariffs = mod.getTariffs
|
|
createPayment = mod.createPayment
|
|
})
|
|
|
|
const sampleTariffs = [
|
|
{ id: 1, name: 'Adult', price: 250 },
|
|
{ id: 2, name: 'Child', price: 150 },
|
|
]
|
|
|
|
describe('getTariffs', () => {
|
|
it('fetches and returns tariffs on success', async () => {
|
|
mockFetch.mockResolvedValueOnce(okResponse(sampleTariffs))
|
|
const result = await getTariffs()
|
|
expect(result).toEqual(sampleTariffs)
|
|
expect(mockFetch).toHaveBeenCalledOnce()
|
|
})
|
|
|
|
it('returns cached data on second call within 5 minutes', async () => {
|
|
mockFetch.mockResolvedValueOnce(okResponse(sampleTariffs))
|
|
await getTariffs()
|
|
await getTariffs() // should hit cache
|
|
expect(mockFetch).toHaveBeenCalledOnce()
|
|
})
|
|
|
|
it('throws when EZY_ACTIVITY env var is missing', async () => {
|
|
vi.stubEnv('EZY_ACTIVITY', '')
|
|
vi.resetModules()
|
|
const mod = await import('@/lib/ezy')
|
|
await expect(mod.getTariffs()).rejects.toThrow('EZY_ACTIVITY')
|
|
})
|
|
|
|
it('throws when the API returns a non-ok status', async () => {
|
|
mockFetch.mockResolvedValueOnce(errorResponse(503))
|
|
await expect(getTariffs()).rejects.toThrow('503')
|
|
})
|
|
|
|
it('throws when the API response fails Zod validation', async () => {
|
|
// Missing required `price` field
|
|
mockFetch.mockResolvedValueOnce(okResponse([{ id: 1, name: 'Bad' }]))
|
|
await expect(getTariffs()).rejects.toThrow()
|
|
})
|
|
|
|
it('retries up to 2 times on network failure, then throws', async () => {
|
|
const err = new Error('network error')
|
|
mockFetch.mockRejectedValue(err)
|
|
await expect(getTariffs()).rejects.toThrow('network error')
|
|
// 1 initial + 2 retries = 3 calls
|
|
expect(mockFetch).toHaveBeenCalledTimes(3)
|
|
})
|
|
|
|
it('re-fetches from API after the 5-minute cache TTL has expired', async () => {
|
|
vi.useFakeTimers()
|
|
mockFetch.mockResolvedValue(okResponse(sampleTariffs))
|
|
|
|
await getTariffs() // populates cache
|
|
expect(mockFetch).toHaveBeenCalledTimes(1)
|
|
|
|
vi.advanceTimersByTime(5 * 60 * 1_000 + 1) // expire cache
|
|
await getTariffs()
|
|
expect(mockFetch).toHaveBeenCalledTimes(2)
|
|
|
|
vi.useRealTimers()
|
|
})
|
|
})
|
|
|
|
describe('createPayment', () => {
|
|
it('returns the payment URL on success', async () => {
|
|
mockFetch.mockResolvedValueOnce(okResponse({ url: 'https://pay.example.com/order/123' }))
|
|
const result = await createPayment('user@example.com', [{ tariff: 't1', count: '2' }])
|
|
expect(result.url).toBe('https://pay.example.com/order/123')
|
|
})
|
|
|
|
it('throws when EZY_PARTNER_KEY env var is missing', async () => {
|
|
vi.stubEnv('EZY_PARTNER_KEY', '')
|
|
vi.resetModules()
|
|
const mod = await import('@/lib/ezy')
|
|
await expect(
|
|
mod.createPayment('user@example.com', [{ tariff: 't1', count: '1' }])
|
|
).rejects.toThrow('EZY_PARTNER_KEY')
|
|
})
|
|
|
|
it('throws when the API returns a non-ok status', async () => {
|
|
mockFetch.mockResolvedValueOnce(errorResponse(400))
|
|
await expect(createPayment('user@example.com', [{ tariff: 't1', count: '1' }])).rejects.toThrow(
|
|
'400'
|
|
)
|
|
})
|
|
|
|
it('throws when the API response fails Zod validation (missing url)', async () => {
|
|
mockFetch.mockResolvedValueOnce(okResponse({ form: null }))
|
|
await expect(
|
|
createPayment('user@example.com', [{ tariff: 't1', count: '1' }])
|
|
).rejects.toThrow()
|
|
})
|
|
|
|
it('includes email and items in the POST body', async () => {
|
|
mockFetch.mockResolvedValueOnce(okResponse({ url: 'https://pay.example.com/1' }))
|
|
await createPayment('buyer@example.com', [{ tariff: 'vip', count: '3' }])
|
|
|
|
const [, init] = mockFetch.mock.calls[0] as [string, RequestInit & { body: FormData }]
|
|
const body = init.body as FormData
|
|
expect(body.get('email')).toBe('buyer@example.com')
|
|
expect(body.get('partner_key')).toBe('test-partner-key')
|
|
})
|
|
|
|
it('retries up to 2 times on network failure, then throws', async () => {
|
|
mockFetch.mockRejectedValue(new Error('network error'))
|
|
await expect(
|
|
createPayment('user@example.com', [{ tariff: 't1', count: '1' }])
|
|
).rejects.toThrow('network error')
|
|
expect(mockFetch).toHaveBeenCalledTimes(3)
|
|
})
|
|
|
|
it('serializes items as JSON in the order FormData field', async () => {
|
|
mockFetch.mockResolvedValueOnce(okResponse({ url: 'https://pay.example.com/2' }))
|
|
await createPayment('buyer@example.com', [
|
|
{ tariff: 'adult', count: '2' },
|
|
{ tariff: 'child', count: '1' },
|
|
])
|
|
|
|
const [, init] = mockFetch.mock.calls[0] as [string, RequestInit & { body: FormData }]
|
|
const order = JSON.parse((init.body as FormData).get('order') as string)
|
|
expect(order).toEqual([
|
|
{ tariff: 'adult', count: '2' },
|
|
{ tariff: 'child', count: '1' },
|
|
])
|
|
})
|
|
})
|