feat: articles pagination
This commit is contained in:
1 parent
a2d224240c
commit
73f4b539fe
45 files changed
+3528
-317
No files matched your search
@@ -0,0 +1,102 @@
|
||||
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: '' })
|
||||
})
|
||||
|
||||
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' })
|
||||
})
|
||||
|
||||
it.each(['abc', '0', '-2', '2.5', '', '1e2'])('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('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 }
|
||||
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: '' })).toEqual({})
|
||||
})
|
||||
|
||||
it('keeps what differs from the defaults', () => {
|
||||
expect(
|
||||
toRouteQuery({ page: 2, size: 5, q: 'plaster', status: 'online', sort: 'title:desc' }),
|
||||
).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: '' }
|
||||
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)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,42 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { pageItems, pageCountFor, rangeFor } from '@/lib/pagination'
|
||||
|
||||
describe('pageItems', () => {
|
||||
it('lists every page when there are seven or fewer', () => {
|
||||
expect(pageItems(3, 7)).toEqual([1, 2, 3, 4, 5, 6, 7])
|
||||
expect(pageItems(1, 1)).toEqual([1])
|
||||
})
|
||||
|
||||
it('marks the skipped range after the first page', () => {
|
||||
expect(pageItems(1, 13)).toEqual([1, 2, 'gap', 13])
|
||||
})
|
||||
|
||||
it('marks skipped ranges on both sides of a middle page', () => {
|
||||
expect(pageItems(7, 13)).toEqual([1, 'gap', 6, 7, 8, 'gap', 13])
|
||||
})
|
||||
|
||||
it('marks the skipped range before the last page', () => {
|
||||
expect(pageItems(13, 13)).toEqual([1, 'gap', 12, 13])
|
||||
})
|
||||
|
||||
it('does not mark a gap between adjacent pages', () => {
|
||||
expect(pageItems(3, 13)).toEqual([1, 2, 3, 4, 'gap', 13])
|
||||
})
|
||||
})
|
||||
|
||||
describe('pageCountFor', () => {
|
||||
it('rounds up and never drops below one', () => {
|
||||
expect(pageCountFor(312, 25)).toBe(13)
|
||||
expect(pageCountFor(6, 25)).toBe(1)
|
||||
expect(pageCountFor(0, 25)).toBe(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('rangeFor', () => {
|
||||
it('computes the shown range', () => {
|
||||
expect(rangeFor(1, 25, 312)).toEqual({ from: 1, to: 25 })
|
||||
expect(rangeFor(7, 25, 312)).toEqual({ from: 151, to: 175 })
|
||||
expect(rangeFor(13, 25, 312)).toEqual({ from: 301, to: 312 })
|
||||
expect(rangeFor(1, 25, 0)).toEqual({ from: 0, to: 0 })
|
||||
})
|
||||
})
|
||||
Reference in new issue
Block a user