feat: articles pagination
This commit is contained in:
1 parent
a2d224240c
commit
73f4b539fe
45 files changed
+3528
-317
No files matched your search
@@ -32,6 +32,26 @@ const VISUAL_REFERENCE =
|
||||
const officeBoard = 'RenewCMS Screens Back-office.dc.html'
|
||||
|
||||
const extraSources: Record<string, Source[]> = {
|
||||
articles: [
|
||||
{
|
||||
board: officeBoard,
|
||||
group: 'Articles: pagination',
|
||||
states: [
|
||||
'First page',
|
||||
'Middle page',
|
||||
'Last page',
|
||||
'Single page',
|
||||
'Loading next page',
|
||||
'Error loading a page',
|
||||
'Page out of range',
|
||||
'Search resets to page 1',
|
||||
'Empty after search',
|
||||
'Page size menu',
|
||||
'Focus-visible',
|
||||
],
|
||||
alias: {},
|
||||
},
|
||||
],
|
||||
activity: [
|
||||
{
|
||||
board: officeBoard,
|
||||
|
||||
+77
-17
@@ -673,6 +673,40 @@ function articlesFixture() {
|
||||
]
|
||||
}
|
||||
|
||||
interface ListedArticle {
|
||||
title: string
|
||||
is_online: boolean
|
||||
}
|
||||
|
||||
function foldTitle(value: string) {
|
||||
return value
|
||||
.normalize('NFD')
|
||||
.replace(/[\u0300-\u036f]/g, '')
|
||||
.toLowerCase()
|
||||
}
|
||||
|
||||
function articleEnvelopeFor(all: ListedArticle[], url: URL) {
|
||||
const page = Number(url.searchParams.get('page') ?? 1)
|
||||
const size = Number(url.searchParams.get('size') ?? 25)
|
||||
const q = url.searchParams.get('q') ?? ''
|
||||
const status = url.searchParams.get('status') ?? ''
|
||||
const matching = all
|
||||
.filter((article) => !status || article.is_online === (status === 'online'))
|
||||
.filter((article) => !q || foldTitle(article.title).includes(foldTitle(q)))
|
||||
const online = all.filter((article) => article.is_online).length
|
||||
return {
|
||||
items: matching.slice((page - 1) * size, page * size),
|
||||
total: matching.length,
|
||||
page,
|
||||
size,
|
||||
counts: { all: all.length, online, draft: all.length - online },
|
||||
}
|
||||
}
|
||||
|
||||
function articleListMock(all: ListedArticle[]): ApiMock {
|
||||
return { status: 200, bodyFor: (url) => articleEnvelopeFor(all, url) }
|
||||
}
|
||||
|
||||
function articlesFixtureWithImages() {
|
||||
return articlesFixture().map((article) =>
|
||||
article.id === 4
|
||||
@@ -691,11 +725,37 @@ function articlesFixtureWithImages() {
|
||||
function articlesApi(overrides: Record<string, ApiMock> = {}): Record<string, ApiMock> {
|
||||
return {
|
||||
'GET /v1/auth/me': verifiedSession,
|
||||
'GET /v1/article/': { status: 200, body: articlesFixture() },
|
||||
'GET /v1/article/': articleListMock(articlesFixture()),
|
||||
...overrides,
|
||||
}
|
||||
}
|
||||
|
||||
const clockSensitiveArticles = (path: string): VisualState => ({
|
||||
path,
|
||||
skip: 'clock-sensitive',
|
||||
})
|
||||
|
||||
const pagination: Record<string, VisualState> = {
|
||||
'First page': clockSensitiveArticles('/article'),
|
||||
'Middle page': clockSensitiveArticles('/article?page=7'),
|
||||
'Last page': clockSensitiveArticles('/article?page=13'),
|
||||
'Single page': clockSensitiveArticles('/article'),
|
||||
'Loading next page': clockSensitiveArticles('/article?page=6'),
|
||||
'Error loading a page': clockSensitiveArticles('/article?page=6'),
|
||||
'Page out of range': clockSensitiveArticles('/article?page=99'),
|
||||
'Search resets to page 1': clockSensitiveArticles('/article?page=7'),
|
||||
'Empty after search': {
|
||||
path: '/article',
|
||||
api: articlesApi(),
|
||||
steps: async (page) => {
|
||||
await page.getByPlaceholder('Search by title').fill('tiles')
|
||||
await page.getByText('No articles match "tiles"').waitFor()
|
||||
},
|
||||
},
|
||||
'Page size menu': clockSensitiveArticles('/article?page=2'),
|
||||
'Focus-visible': clockSensitiveArticles('/article?page=7'),
|
||||
}
|
||||
|
||||
async function openArticleRowMenu(page: Page, title: string) {
|
||||
await page.getByRole('button', { name: `Actions for ${title}` }).click()
|
||||
}
|
||||
@@ -4000,7 +4060,7 @@ export const visualStates: Record<string, Record<string, VisualState>> = {
|
||||
api: {
|
||||
'GET /v1/auth/me': verifiedSession,
|
||||
'GET /v1/activity': homeActivityMock,
|
||||
'GET /v1/article/': { status: 200, body: sixArticlesFixture },
|
||||
'GET /v1/article/': articleListMock(sixArticlesFixture),
|
||||
'GET /v1/system/update': { status: 200, body: updateStatusBody() },
|
||||
},
|
||||
steps: openAccountMenu,
|
||||
@@ -4324,6 +4384,7 @@ export const visualStates: Record<string, Record<string, VisualState>> = {
|
||||
}),
|
||||
},
|
||||
articles: {
|
||||
...pagination,
|
||||
Default: {
|
||||
path: '/article',
|
||||
now: articlesNow,
|
||||
@@ -4351,7 +4412,7 @@ export const visualStates: Record<string, Record<string, VisualState>> = {
|
||||
path: '/article',
|
||||
now: articlesNow,
|
||||
api: articlesApi({
|
||||
'GET /v1/article/': { status: 200, body: articlesFixture(), hang: true },
|
||||
'GET /v1/article/': { ...articleListMock(articlesFixture()), hang: true },
|
||||
}),
|
||||
exception: {
|
||||
maxPercent: 10.6,
|
||||
@@ -4362,7 +4423,7 @@ export const visualStates: Record<string, Record<string, VisualState>> = {
|
||||
Empty: {
|
||||
path: '/article',
|
||||
now: articlesNow,
|
||||
api: articlesApi({ 'GET /v1/article/': { status: 200, body: [] } }),
|
||||
api: articlesApi({ 'GET /v1/article/': articleListMock([]) }),
|
||||
exception: {
|
||||
maxPercent: 3.6,
|
||||
reason: 'Same UTable/EmptyState geometry offset as Default, carried into the empty layout',
|
||||
@@ -4387,12 +4448,11 @@ export const visualStates: Record<string, Record<string, VisualState>> = {
|
||||
path: '/article',
|
||||
now: articlesNow,
|
||||
api: articlesApi({
|
||||
'GET /v1/article/': {
|
||||
status: 200,
|
||||
body: articlesFixture()
|
||||
'GET /v1/article/': articleListMock(
|
||||
articlesFixture()
|
||||
.slice(0, 4)
|
||||
.map((article) => ({ ...article, is_online: true })),
|
||||
},
|
||||
),
|
||||
}),
|
||||
steps: async (page) => {
|
||||
await page.getByRole('tab', { name: /^Drafts/ }).focus()
|
||||
@@ -4478,7 +4538,7 @@ export const visualStates: Record<string, Record<string, VisualState>> = {
|
||||
path: '/article',
|
||||
now: articlesNow,
|
||||
api: articlesApi({
|
||||
'GET /v1/article/': { status: 200, body: articlesFixtureWithImages() },
|
||||
'GET /v1/article/': articleListMock(articlesFixtureWithImages()),
|
||||
}),
|
||||
steps: async (page) => {
|
||||
await openArticleRowMenu(page, 'Winter checklist for timber windows')
|
||||
@@ -4490,7 +4550,7 @@ export const visualStates: Record<string, Record<string, VisualState>> = {
|
||||
path: '/article',
|
||||
now: articlesNow,
|
||||
api: articlesApi({
|
||||
'GET /v1/article/': { status: 200, body: articlesFixtureWithImages() },
|
||||
'GET /v1/article/': articleListMock(articlesFixtureWithImages()),
|
||||
'DELETE /v1/article/4': { status: 204, hang: true },
|
||||
}),
|
||||
steps: async (page) => {
|
||||
@@ -4506,7 +4566,7 @@ export const visualStates: Record<string, Record<string, VisualState>> = {
|
||||
path: '/article',
|
||||
now: articlesNow,
|
||||
api: articlesApi({
|
||||
'GET /v1/article/': { status: 200, body: articlesFixtureWithImages() },
|
||||
'GET /v1/article/': articleListMock(articlesFixtureWithImages()),
|
||||
'DELETE /v1/article/4': { status: 500 },
|
||||
}),
|
||||
steps: async (page) => {
|
||||
@@ -4523,7 +4583,7 @@ export const visualStates: Record<string, Record<string, VisualState>> = {
|
||||
path: '/article',
|
||||
now: articlesNow,
|
||||
api: articlesApi({
|
||||
'GET /v1/article/': { status: 200, body: articlesFixtureWithImages() },
|
||||
'GET /v1/article/': articleListMock(articlesFixtureWithImages()),
|
||||
'DELETE /v1/article/4': { status: 204 },
|
||||
}),
|
||||
steps: async (page) => {
|
||||
@@ -4545,7 +4605,7 @@ export const visualStates: Record<string, Record<string, VisualState>> = {
|
||||
'GET /v1/auth/me': verifiedSession,
|
||||
'GET /v1/activity': homeActivityMock,
|
||||
'GET /v1/system/update': { status: 200, body: updateStatusBody() },
|
||||
'GET /v1/article/': { status: 200, body: sixArticlesFixture },
|
||||
'GET /v1/article/': articleListMock(sixArticlesFixture),
|
||||
},
|
||||
},
|
||||
Loading: {
|
||||
@@ -4555,7 +4615,7 @@ export const visualStates: Record<string, Record<string, VisualState>> = {
|
||||
'GET /v1/auth/me': verifiedSession,
|
||||
'GET /v1/activity': { ...homeActivityMock, hang: true },
|
||||
'GET /v1/system/update': { status: 200, body: updateStatusBody() },
|
||||
'GET /v1/article/': { status: 200, body: sixArticlesFixture, hang: true },
|
||||
'GET /v1/article/': { ...articleListMock(sixArticlesFixture), hang: true },
|
||||
},
|
||||
},
|
||||
'Empty, first run': {
|
||||
@@ -4565,7 +4625,7 @@ export const visualStates: Record<string, Record<string, VisualState>> = {
|
||||
'GET /v1/auth/me': verifiedSession,
|
||||
'GET /v1/activity': homeActivityMock,
|
||||
'GET /v1/system/update': { status: 200, body: updateStatusBody() },
|
||||
'GET /v1/article/': { status: 200, body: [] },
|
||||
'GET /v1/article/': articleListMock([]),
|
||||
},
|
||||
},
|
||||
Error: {
|
||||
@@ -4584,7 +4644,7 @@ export const visualStates: Record<string, Record<string, VisualState>> = {
|
||||
api: {
|
||||
'GET /v1/auth/me': verifiedSession,
|
||||
'GET /v1/activity': homeActivityMock,
|
||||
'GET /v1/article/': { status: 200, body: sixArticlesFixture },
|
||||
'GET /v1/article/': articleListMock(sixArticlesFixture),
|
||||
'GET /v1/system/update': { status: 200, body: updateAvailable() },
|
||||
},
|
||||
},
|
||||
@@ -4594,7 +4654,7 @@ export const visualStates: Record<string, Record<string, VisualState>> = {
|
||||
api: {
|
||||
'GET /v1/auth/me': verifiedSession,
|
||||
'GET /v1/activity': homeActivityMock,
|
||||
'GET /v1/article/': { status: 200, body: sixArticlesFixture },
|
||||
'GET /v1/article/': articleListMock(sixArticlesFixture),
|
||||
'GET /v1/system/update': { status: 200, body: updateJob('ready') },
|
||||
},
|
||||
},
|
||||
|
||||
Reference in new issue
Block a user