- B1: Next.js 15 + Payload CMS 3.0 + Postgres 16, ESLint, Prettier, Husky, Vitest - B2: 9 collections, 6 globals, 12 Page Builder blocks, access control, slugify/revalidate hooks - B3: ezy.com.ua payments, Binotel HMAC webhook, leads API, Telegram bot, Resend email, rate limiting - B4: Tariffs collection with ezy API sync (cron + manual), dynamic pricing source-of-truth - B5: 13 test files covering unit libs and all API routes - B6: Dockerfile multi-stage, docker-compose.prod.yml, nginx.conf SSL, GitHub Actions CI/CD, health endpoint - B7: docs/admin-guide-ua.md (marketer guide), docs/deploy.md (VPS instructions), README quickstart Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
109 lines
4.2 KiB
TypeScript
109 lines
4.2 KiB
TypeScript
import { describe, it, expect } from 'vitest'
|
|
import { slugifyBeforeChange } from '@/hooks/slugify'
|
|
|
|
// Minimal args satisfying CollectionBeforeChangeHook signature
|
|
function makeArgs(
|
|
data: Record<string, unknown>,
|
|
operation: 'create' | 'update'
|
|
): Parameters<typeof slugifyBeforeChange>[0] {
|
|
return {
|
|
data,
|
|
operation,
|
|
req: {} as never,
|
|
originalDoc: undefined,
|
|
context: {},
|
|
collection: {} as never,
|
|
}
|
|
}
|
|
|
|
describe('slugifyBeforeChange', () => {
|
|
describe('create operation', () => {
|
|
it('generates a latin slug from a Cyrillic title', () => {
|
|
const result = slugifyBeforeChange(makeArgs({ title: 'Шумиленд' }, 'create'))
|
|
expect((result as { slug: string }).slug).toBe('shumilend')
|
|
})
|
|
|
|
it('generates a slug for a multi-word Cyrillic title', () => {
|
|
const result = slugifyBeforeChange(makeArgs({ title: 'Диво Ліс' }, 'create'))
|
|
expect((result as { slug: string }).slug).toBe('divo-ls')
|
|
})
|
|
|
|
it('generates a slug for a Ukrainian common noun', () => {
|
|
const result = slugifyBeforeChange(makeArgs({ title: 'Новини' }, 'create'))
|
|
expect((result as { slug: string }).slug).toBe('novini')
|
|
})
|
|
|
|
it('produces only lowercase alphanumeric and hyphen characters', () => {
|
|
const result = slugifyBeforeChange(makeArgs({ title: 'Шумиленд' }, 'create'))
|
|
const slug = (result as { slug: string }).slug
|
|
expect(slug).toMatch(/^[a-z0-9-]+$/)
|
|
})
|
|
|
|
it('overwrites any slug value supplied on create', () => {
|
|
// On create, slug is always regenerated from title
|
|
const result = slugifyBeforeChange(makeArgs({ title: 'Новини', slug: 'ignore-me' }, 'create'))
|
|
expect((result as { slug: string }).slug).toBe('novini')
|
|
})
|
|
|
|
it('returns data unchanged when title is absent on create', () => {
|
|
const data = { status: 'draft' }
|
|
const result = slugifyBeforeChange(makeArgs(data, 'create'))
|
|
expect(result).toEqual(data)
|
|
})
|
|
|
|
it('returns data unchanged when title is not a string', () => {
|
|
const data = { title: 42 }
|
|
const result = slugifyBeforeChange(makeArgs(data, 'create'))
|
|
expect(result).toEqual(data)
|
|
expect((result as Record<string, unknown>)['slug']).toBeUndefined()
|
|
})
|
|
})
|
|
|
|
describe('update operation', () => {
|
|
it('does not overwrite a non-empty existing slug on update', () => {
|
|
const result = slugifyBeforeChange(
|
|
makeArgs({ title: 'New Title', slug: 'existing-slug' }, 'update')
|
|
)
|
|
expect((result as { slug: string }).slug).toBe('existing-slug')
|
|
})
|
|
|
|
it('generates slug from title when slug is empty string on update', () => {
|
|
const result = slugifyBeforeChange(makeArgs({ title: 'Новини', slug: '' }, 'update'))
|
|
expect((result as { slug: string }).slug).toBe('novini')
|
|
})
|
|
|
|
it('generates slug from title when slug is undefined on update', () => {
|
|
const result = slugifyBeforeChange(makeArgs({ title: 'Шумиленд' }, 'update'))
|
|
expect((result as { slug: string }).slug).toBe('shumilend')
|
|
})
|
|
|
|
it('returns data unchanged when slug is missing and no title on update', () => {
|
|
const data = { status: 'published' }
|
|
const result = slugifyBeforeChange(makeArgs(data, 'update'))
|
|
expect(result).toEqual(data)
|
|
})
|
|
})
|
|
|
|
describe('slug format', () => {
|
|
it('collapses multiple consecutive hyphens', () => {
|
|
// A title that produces consecutive hyphens after stripping special chars
|
|
const result = slugifyBeforeChange(makeArgs({ title: 'A B' }, 'create'))
|
|
const slug = (result as { slug: string }).slug
|
|
expect(slug).not.toMatch(/--/)
|
|
})
|
|
|
|
it('does not start or end with a hyphen', () => {
|
|
const result = slugifyBeforeChange(makeArgs({ title: 'Шумиленд' }, 'create'))
|
|
const slug = (result as { slug: string }).slug
|
|
expect(slug).not.toMatch(/^-/)
|
|
expect(slug).not.toMatch(/-$/)
|
|
})
|
|
|
|
it('produces a non-empty slug for any non-empty Cyrillic title', () => {
|
|
const result = slugifyBeforeChange(makeArgs({ title: 'Я' }, 'create'))
|
|
const slug = (result as { slug: string }).slug
|
|
expect(typeof slug).toBe('string')
|
|
expect(slug.length).toBeGreaterThan(0)
|
|
})
|
|
})
|
|
})
|