- 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>
200 lines
6.1 KiB
TypeScript
200 lines
6.1 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
|
|
const mockGetTariffs = vi.fn()
|
|
const mockPayloadFind = vi.fn()
|
|
const mockGetPayload = vi.fn(() => ({ find: mockPayloadFind }))
|
|
|
|
vi.mock('@/lib/ezy', () => ({ getTariffs: mockGetTariffs }))
|
|
vi.mock('payload', () => ({ getPayload: mockGetPayload }))
|
|
vi.mock('@payload-config', () => ({ default: {} }))
|
|
|
|
const ezyTariffs = [
|
|
{ id: 1, name: 'Дорослий', price: 250 },
|
|
{ id: 2, name: 'Дитячий', price: 150 },
|
|
]
|
|
|
|
const dbTariffs = [
|
|
{ ezy_id: 1, display_name: 'Дорослий (UA)', category_tag: 'adult', sort: 0 },
|
|
{ ezy_id: 2, display_name: 'Дитячий (UA)', category_tag: 'child', sort: 1 },
|
|
]
|
|
|
|
let GET: () => Promise<Response>
|
|
|
|
beforeEach(async () => {
|
|
vi.resetModules()
|
|
mockGetTariffs.mockReset()
|
|
mockPayloadFind.mockReset()
|
|
const mod = await import('@/app/api/tickets/tariffs/route')
|
|
GET = mod.GET
|
|
})
|
|
|
|
describe('GET /api/tickets/tariffs', () => {
|
|
it('returns merged tariffs sorted by sort field', async () => {
|
|
mockGetTariffs.mockResolvedValueOnce(ezyTariffs)
|
|
mockPayloadFind.mockResolvedValueOnce({ docs: dbTariffs })
|
|
|
|
const res = await GET()
|
|
expect(res.status).toBe(200)
|
|
const { tariffs } = await res.json()
|
|
expect(tariffs).toHaveLength(2)
|
|
expect(tariffs[0].id).toBe(1)
|
|
expect(tariffs[0].name).toBe('Дорослий (UA)')
|
|
expect(tariffs[0].price).toBe(250)
|
|
})
|
|
|
|
it('filters out ezy tariffs with no matching DB record', async () => {
|
|
mockGetTariffs.mockResolvedValueOnce([...ezyTariffs, { id: 99, name: 'Unknown', price: 0 }])
|
|
mockPayloadFind.mockResolvedValueOnce({ docs: dbTariffs })
|
|
|
|
const res = await GET()
|
|
const { tariffs } = await res.json()
|
|
expect(tariffs).toHaveLength(2)
|
|
expect(tariffs.map((t: { id: number }) => t.id)).not.toContain(99)
|
|
})
|
|
|
|
it('uses db display_name over ezy name', async () => {
|
|
mockGetTariffs.mockResolvedValueOnce([{ id: 1, name: 'EZY Name', price: 300 }])
|
|
mockPayloadFind.mockResolvedValueOnce({
|
|
docs: [{ ezy_id: 1, display_name: 'DB Name', sort: 0 }],
|
|
})
|
|
|
|
const res = await GET()
|
|
const { tariffs } = await res.json()
|
|
expect(tariffs[0].name).toBe('DB Name')
|
|
})
|
|
|
|
it('falls back to DB tariffs when ezy throws, with warning', async () => {
|
|
mockGetTariffs.mockRejectedValueOnce(new Error('ezy down'))
|
|
mockPayloadFind.mockResolvedValueOnce({
|
|
docs: [
|
|
{
|
|
ezy_id: 1,
|
|
display_name: 'Adult',
|
|
last_synced_price: 250,
|
|
last_synced_name: 'Adult',
|
|
sort: 0,
|
|
},
|
|
],
|
|
})
|
|
|
|
const res = await GET()
|
|
expect(res.status).toBe(200)
|
|
const json = await res.json()
|
|
expect(json.warning).toMatch(/outdated/i)
|
|
expect(json.tariffs[0].stale).toBe(true)
|
|
})
|
|
|
|
it('returns 503 when ezy fails and DB is empty', async () => {
|
|
mockGetTariffs.mockRejectedValueOnce(new Error('ezy down'))
|
|
mockPayloadFind.mockResolvedValueOnce({ docs: [] })
|
|
|
|
const res = await GET()
|
|
expect(res.status).toBe(503)
|
|
})
|
|
|
|
it('returns 503 when both ezy and DB fail', async () => {
|
|
mockGetTariffs.mockRejectedValueOnce(new Error('ezy down'))
|
|
mockPayloadFind.mockRejectedValueOnce(new Error('DB down'))
|
|
|
|
const res = await GET()
|
|
expect(res.status).toBe(503)
|
|
const json = await res.json()
|
|
expect(json.error).toBeDefined()
|
|
})
|
|
|
|
it('treats null sort as 0 for ordering purposes', async () => {
|
|
mockGetTariffs.mockResolvedValueOnce([
|
|
{ id: 1, name: 'A', price: 100 },
|
|
{ id: 2, name: 'B', price: 200 },
|
|
])
|
|
mockPayloadFind.mockResolvedValueOnce({
|
|
docs: [
|
|
{ ezy_id: 2, display_name: 'B', sort: null },
|
|
{ ezy_id: 1, display_name: 'A', sort: 1 },
|
|
],
|
|
})
|
|
|
|
const res = await GET()
|
|
const { tariffs } = await res.json()
|
|
expect(tariffs[0].id).toBe(2) // null → 0, sorts before 1
|
|
expect(tariffs[1].id).toBe(1)
|
|
})
|
|
|
|
it('falls back to ezy name when display_name is null', async () => {
|
|
mockGetTariffs.mockResolvedValueOnce([{ id: 1, name: 'Ezy Name', price: 100 }])
|
|
mockPayloadFind.mockResolvedValueOnce({
|
|
docs: [{ ezy_id: 1, display_name: null, sort: 0 }],
|
|
})
|
|
|
|
const res = await GET()
|
|
const { tariffs } = await res.json()
|
|
expect(tariffs[0].name).toBe('Ezy Name')
|
|
})
|
|
|
|
it('includes categoryTag, description, image, and icon in merged response', async () => {
|
|
mockGetTariffs.mockResolvedValueOnce([{ id: 1, name: 'Adult', price: 250 }])
|
|
mockPayloadFind.mockResolvedValueOnce({
|
|
docs: [
|
|
{
|
|
ezy_id: 1,
|
|
display_name: 'Adult UA',
|
|
category_tag: 'adult',
|
|
description: 'Full-day pass',
|
|
image: { url: '/img/adult.jpg' },
|
|
icon: '🎿',
|
|
sort: 0,
|
|
},
|
|
],
|
|
})
|
|
|
|
const res = await GET()
|
|
const { tariffs } = await res.json()
|
|
expect(tariffs[0].categoryTag).toBe('adult')
|
|
expect(tariffs[0].description).toBe('Full-day pass')
|
|
expect(tariffs[0].image).toEqual({ url: '/img/adult.jpg' })
|
|
expect(tariffs[0].icon).toBe('🎿')
|
|
})
|
|
|
|
it('fallback response uses last_synced_price and includes stale:true', async () => {
|
|
mockGetTariffs.mockRejectedValueOnce(new Error('ezy down'))
|
|
mockPayloadFind.mockResolvedValueOnce({
|
|
docs: [
|
|
{
|
|
ezy_id: 3,
|
|
display_name: 'Weekend',
|
|
last_synced_name: 'Weekend pass',
|
|
last_synced_price: 199,
|
|
category_tag: 'weekend',
|
|
description: null,
|
|
image: null,
|
|
icon: null,
|
|
sort: 0,
|
|
},
|
|
],
|
|
})
|
|
|
|
const res = await GET()
|
|
const { tariffs } = await res.json()
|
|
expect(tariffs[0].price).toBe(199)
|
|
expect(tariffs[0].stale).toBe(true)
|
|
expect(tariffs[0].id).toBe(3)
|
|
})
|
|
|
|
it('returns tariffs sorted in ascending sort order', async () => {
|
|
mockGetTariffs.mockResolvedValueOnce([
|
|
{ id: 2, name: 'B', price: 100 },
|
|
{ id: 1, name: 'A', price: 200 },
|
|
])
|
|
mockPayloadFind.mockResolvedValueOnce({
|
|
docs: [
|
|
{ ezy_id: 1, display_name: 'A', sort: 0 },
|
|
{ ezy_id: 2, display_name: 'B', sort: 5 },
|
|
],
|
|
})
|
|
|
|
const res = await GET()
|
|
const { tariffs } = await res.json()
|
|
expect(tariffs[0].id).toBe(1)
|
|
expect(tariffs[1].id).toBe(2)
|
|
})
|
|
})
|