239 lines
6.7 KiB
TypeScript
239 lines
6.7 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
import { nextSort, parseArticleQuery, sameArticleQuery, toRouteQuery } from '@/lib/articleListQuery'
|
|
|
|
describe('parseArticleQuery', () => {
|
|
it('falls back to page 1, size 25, no search and the All tab', () => {
|
|
expect(parseArticleQuery({})).toEqual({
|
|
page: 1,
|
|
size: 25,
|
|
q: '',
|
|
status: '',
|
|
sort: '',
|
|
site: '',
|
|
seo: '',
|
|
lang: '',
|
|
tab: '',
|
|
author: 0,
|
|
})
|
|
})
|
|
|
|
it('reads valid values', () => {
|
|
expect(
|
|
parseArticleQuery({
|
|
page: '7',
|
|
size: '50',
|
|
q: 'plaster',
|
|
status: 'draft',
|
|
sort: 'title:asc',
|
|
}),
|
|
).toEqual({
|
|
page: 7,
|
|
size: 50,
|
|
q: 'plaster',
|
|
status: 'draft',
|
|
sort: 'title:asc',
|
|
site: '',
|
|
seo: '',
|
|
lang: '',
|
|
tab: '',
|
|
author: 0,
|
|
})
|
|
})
|
|
|
|
it.each(['abc', '0', '-2', '2.5', '', '1e2', '99999999999999999999999'])(
|
|
'treats page "%s" as 1',
|
|
(page) => {
|
|
expect(parseArticleQuery({ page }).page).toBe(1)
|
|
},
|
|
)
|
|
|
|
it.each(['10', 'abc', '0', '26', ''])('treats size "%s" as 25', (size) => {
|
|
expect(parseArticleQuery({ size }).size).toBe(25)
|
|
})
|
|
|
|
it('ignores an unknown status', () => {
|
|
expect(parseArticleQuery({ status: 'archived' }).status).toBe('')
|
|
})
|
|
|
|
it('reads the first value of a repeated parameter and caps the search at 200 characters', () => {
|
|
expect(parseArticleQuery({ page: ['3', '4'] }).page).toBe(3)
|
|
expect(parseArticleQuery({ q: 'a'.repeat(250) }).q).toHaveLength(200)
|
|
})
|
|
})
|
|
|
|
describe('localized list parameters', () => {
|
|
it('reads lang, tab and author', () => {
|
|
expect(parseArticleQuery({ lang: 'fr', tab: 'missing', author: '12' })).toMatchObject({
|
|
lang: 'fr',
|
|
tab: 'missing',
|
|
author: 12,
|
|
})
|
|
})
|
|
|
|
it.each(['english', 'x', '', 'fr_FR', '12'])('falls back from the lang "%s"', (lang) => {
|
|
expect(parseArticleQuery({ lang }).lang).toBe('')
|
|
})
|
|
|
|
it.each(['review', 'all', 'published', ''])('falls back from the tab "%s"', (tab) => {
|
|
expect(parseArticleQuery({ tab }).tab).toBe('')
|
|
})
|
|
|
|
it.each(['abc', '0', '-1', '', '1.5'])('falls back from the author "%s"', (author) => {
|
|
expect(parseArticleQuery({ author }).author).toBe(0)
|
|
})
|
|
|
|
it('drops the status when the tab is Not in language', () => {
|
|
expect(parseArticleQuery({ tab: 'missing', status: 'draft' }).status).toBe('')
|
|
})
|
|
|
|
it('writes lang, tab and author and omits their defaults', () => {
|
|
expect(
|
|
toRouteQuery({
|
|
...parseArticleQuery({}),
|
|
lang: 'fr',
|
|
tab: 'missing',
|
|
author: 4,
|
|
}),
|
|
).toEqual({ lang: 'fr', tab: 'missing', author: '4' })
|
|
expect(toRouteQuery(parseArticleQuery({}))).toEqual({})
|
|
})
|
|
|
|
it('tells queries apart by lang, tab and author', () => {
|
|
const base = parseArticleQuery({})
|
|
expect(sameArticleQuery(base, { ...base, lang: 'fr' })).toBe(false)
|
|
expect(sameArticleQuery(base, { ...base, tab: 'missing' })).toBe(false)
|
|
expect(sameArticleQuery(base, { ...base, author: 3 })).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('sort parameter', () => {
|
|
it.each(['title:asc', 'status:desc', 'created_at:asc', 'created_at:desc'])(
|
|
'keeps %s',
|
|
(sort) => {
|
|
expect(parseArticleQuery({ sort }).sort).toBe(sort)
|
|
},
|
|
)
|
|
|
|
it.each(['body:asc', 'title', 'title:up', 'TITLE:ASC', ''])('drops invalid "%s"', (sort) => {
|
|
expect(parseArticleQuery({ sort }).sort).toBe('')
|
|
})
|
|
|
|
it('treats the default as no sort', () => {
|
|
expect(parseArticleQuery({ sort: 'updated_at:desc' }).sort).toBe('')
|
|
})
|
|
|
|
it('omits the default from the route and treats it as equal to no sort', () => {
|
|
const base = { page: 1, size: 25, q: '', status: '' as const, site: '', seo: '' as const }
|
|
expect(toRouteQuery({ ...base, sort: 'updated_at:desc' })).toEqual({})
|
|
expect(toRouteQuery({ ...base, sort: 'title:asc' })).toEqual({
|
|
sort: 'title:asc',
|
|
})
|
|
expect(sameArticleQuery({ ...base, sort: '' }, { ...base, sort: 'updated_at:desc' })).toBe(
|
|
true,
|
|
)
|
|
expect(sameArticleQuery({ ...base, sort: 'title:asc' }, { ...base, sort: '' })).toBe(false)
|
|
})
|
|
|
|
it('toggles: first click ascending, second descending, other column restarts', () => {
|
|
expect(nextSort('', 'title')).toBe('title:asc')
|
|
expect(nextSort('title:asc', 'title')).toBe('title:desc')
|
|
expect(nextSort('title:desc', 'title')).toBe('title:asc')
|
|
expect(nextSort('title:asc', 'status')).toBe('status:asc')
|
|
expect(nextSort('', 'updated_at')).toBe('updated_at:asc')
|
|
expect(nextSort('updated_at:asc', 'updated_at')).toBe('updated_at:desc')
|
|
})
|
|
})
|
|
|
|
describe('toRouteQuery', () => {
|
|
it('omits every default', () => {
|
|
expect(
|
|
toRouteQuery({
|
|
page: 1,
|
|
size: 25,
|
|
q: '',
|
|
status: '',
|
|
sort: '',
|
|
site: '',
|
|
seo: '',
|
|
}),
|
|
).toEqual({})
|
|
})
|
|
|
|
it('keeps what differs from the defaults', () => {
|
|
expect(
|
|
toRouteQuery({
|
|
page: 2,
|
|
size: 5,
|
|
q: 'plaster',
|
|
status: 'online',
|
|
sort: 'title:desc',
|
|
site: '',
|
|
seo: '',
|
|
}),
|
|
).toEqual({
|
|
page: '2',
|
|
size: '5',
|
|
q: 'plaster',
|
|
status: 'online',
|
|
sort: 'title:desc',
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('sameArticleQuery', () => {
|
|
it('compares every field', () => {
|
|
const base = {
|
|
page: 1,
|
|
size: 25,
|
|
q: '',
|
|
status: '' as const,
|
|
sort: '',
|
|
site: '',
|
|
seo: '',
|
|
}
|
|
expect(sameArticleQuery(base, { ...base })).toBe(true)
|
|
expect(sameArticleQuery(base, { ...base, page: 2 })).toBe(false)
|
|
expect(sameArticleQuery(base, { ...base, size: 5 })).toBe(false)
|
|
expect(sameArticleQuery(base, { ...base, q: 'a' })).toBe(false)
|
|
expect(sameArticleQuery(base, { ...base, status: 'draft' })).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('site parameter', () => {
|
|
it('reads the site key and writes it back to the route', () => {
|
|
const parsed = parseArticleQuery({ site: 'journal' })
|
|
expect(parsed.site).toBe('journal')
|
|
expect(toRouteQuery(parsed)).toEqual({ site: 'journal' })
|
|
})
|
|
|
|
it('omits an empty site and tells two queries apart by their site', () => {
|
|
const base = { page: 1, size: 25, q: '', status: '' as const, sort: '', seo: '' as const }
|
|
expect(toRouteQuery({ ...base, site: '' })).toEqual({})
|
|
expect(sameArticleQuery({ ...base, site: 'journal' }, { ...base, site: '' })).toBe(false)
|
|
expect(sameArticleQuery({ ...base, site: 'journal' }, { ...base, site: 'journal' })).toBe(
|
|
true,
|
|
)
|
|
})
|
|
})
|
|
|
|
describe('seo parameter', () => {
|
|
it('reads only the value suggestions and writes it after the site', () => {
|
|
const parsed = parseArticleQuery({ site: 'journal', seo: 'suggestions' })
|
|
expect(parsed.seo).toBe('suggestions')
|
|
expect(Object.keys(toRouteQuery(parsed))).toEqual(['site', 'seo'])
|
|
})
|
|
|
|
it.each(['maybe', '', 'READY', 'ready', 'hidden', 'suggestions,x'])(
|
|
'drops the unknown value "%s"',
|
|
(seo) => {
|
|
expect(parseArticleQuery({ seo }).seo).toBe('')
|
|
},
|
|
)
|
|
|
|
it('tells two queries apart by their seo filter', () => {
|
|
const base = parseArticleQuery({})
|
|
expect(sameArticleQuery(base, { ...base, seo: 'suggestions' })).toBe(false)
|
|
expect(sameArticleQuery(base, { ...base })).toBe(true)
|
|
})
|
|
})
|