feat: articles pagination

This commit is contained in:
Florian Sylvain committed 2026-10-11 19:14:37 +02:00
1 parent a2d224240c
commit 73f4b539fe
45 files changed
+3528 -317

No files matched your search

@@ -8,9 +8,10 @@ import { useSessionStore } from '@/stores/session'
import type { ActivityItem } from '@/lib/activity'
import HomeView from '../HomeView.vue'
import { anonymous, makeItem, target } from './activityFixtures'
import { articleEnvelope, articleListRoute } from './articleEnvelope'
const ACTIVITY_ROUTE = 'GET /v1/activity?size=5'
const ARTICLES_ROUTE = 'GET /v1/article/?format=html'
const ARTICLES_ROUTE = articleListRoute({ size: 5, sort: 'updated_at:desc' })
type Handler = () => Response | Promise<Response>
@@ -95,7 +96,7 @@ describe('Home, owner or admin, the last five events with the actor name', () =>
it.each(['owner', 'admin'] as const)('shows five rows with names for %s', async (role) => {
await mountHome(role, {
[ACTIVITY_ROUTE]: () => page(recentItems(5)),
[ARTICLES_ROUTE]: () => response(200, articles),
[ARTICLES_ROUTE]: () => response(200, articleEnvelope(articles, { size: 5 })),
})
expect(card().find('h2').text()).toBe('Recent activity')
@@ -125,7 +126,7 @@ describe('Home, regular user, own events only and no actor name', () => {
it.each([['editor' as const], [undefined]])('shows only the time for %s', async (role) => {
await mountHome(role as undefined, {
[ACTIVITY_ROUTE]: () => page(recentItems(3)),
[ARTICLES_ROUTE]: () => response(200, articles),
[ARTICLES_ROUTE]: () => response(200, articleEnvelope(articles, { size: 5 })),
})
expect(card().find('h2').text()).toBe('Your recent activity')
@@ -146,7 +147,7 @@ describe('Home rows, failure and security markers', () => {
]
await mountHome('owner', {
[ACTIVITY_ROUTE]: () => page(items),
[ARTICLES_ROUTE]: () => response(200, articles),
[ARTICLES_ROUTE]: () => response(200, articleEnvelope(articles, { size: 5 })),
})
const marks = rows().map((row) => ({
@@ -177,7 +178,7 @@ describe('Home rows, unknown action, failed login and not interactive', () => {
]
await mountHome('owner', {
[ACTIVITY_ROUTE]: () => page(items),
[ARTICLES_ROUTE]: () => response(200, articles),
[ARTICLES_ROUTE]: () => response(200, articleEnvelope(articles, { size: 5 })),
})
expect(rows()[0]!.text()).toContain('article.archive')
@@ -188,7 +189,7 @@ describe('Home rows, unknown action, failed login and not interactive', () => {
it('has no button or link inside a row and keeps the route on a click', async () => {
await mountHome('owner', {
[ACTIVITY_ROUTE]: () => page(recentItems(2)),
[ARTICLES_ROUTE]: () => response(200, articles),
[ARTICLES_ROUTE]: () => response(200, articleEnvelope(articles, { size: 5 })),
})
const row = rows()[0]!
@@ -202,7 +203,7 @@ describe('Home rows, unknown action, failed login and not interactive', () => {
const items = [makeItem({ id: 1, target: target('article', '8', 'T'.repeat(150)) })]
await mountHome('owner', {
[ACTIVITY_ROUTE]: () => page(items),
[ARTICLES_ROUTE]: () => response(200, articles),
[ARTICLES_ROUTE]: () => response(200, articleEnvelope(articles, { size: 5 })),
})
expect(rows()[0]!.find('[data-testid="recent-activity-sentence"]').classes()).toContain(
@@ -223,7 +224,7 @@ describe('Home card, skeleton rows only after 150 ms', () => {
new Promise((done) => {
resolve = () => done(page(recentItems(5)))
}),
[ARTICLES_ROUTE]: () => response(200, articles),
[ARTICLES_ROUTE]: () => response(200, articleEnvelope(articles, { size: 5 })),
})
expect(card().attributes('aria-busy')).toBeUndefined()
@@ -246,7 +247,7 @@ describe('Home card, skeleton rows only after 150 ms', () => {
vi.useFakeTimers()
await mountHome('owner', {
[ACTIVITY_ROUTE]: () => page(recentItems(5)),
[ARTICLES_ROUTE]: () => response(200, articles),
[ARTICLES_ROUTE]: () => response(200, articleEnvelope(articles, { size: 5 })),
})
await vi.advanceTimersByTimeAsync(200)
@@ -267,7 +268,7 @@ describe('Home card, error shows a retryable alert that leaves the page alone',
attempt += 1
return attempt === 1 ? (failure() as Response) : page(recentItems(2))
},
[ARTICLES_ROUTE]: () => response(200, articles),
[ARTICLES_ROUTE]: () => response(200, articleEnvelope(articles, { size: 5 })),
})
const alert = card().find('[role="alert"]')
@@ -292,7 +293,7 @@ describe('Home card, empty when there are no events yet', () => {
])('says so for %s', async (role, text) => {
await mountHome(role as 'owner', {
[ACTIVITY_ROUTE]: () => page([], 0),
[ARTICLES_ROUTE]: () => response(200, articles),
[ARTICLES_ROUTE]: () => response(200, articleEnvelope(articles, { size: 5 })),
})
const empty = card().find('[data-testid="recent-activity-empty"]')
@@ -316,7 +317,7 @@ describe('Home card and article list are independent', () => {
it('shows the articles empty state and the card error together', async () => {
await mountHome('owner', {
[ACTIVITY_ROUTE]: () => response(500),
[ARTICLES_ROUTE]: () => response(200, []),
[ARTICLES_ROUTE]: () => response(200, articleEnvelope([], { size: 5 })),
})
expect(wrapper!.text()).toContain('Write your first article')
@@ -328,7 +329,7 @@ describe('Home layout puts Recent activity full width below Recently updated', (
it('renders the section after the recent list inside the same single column', async () => {
await mountHome('owner', {
[ACTIVITY_ROUTE]: () => page(recentItems(1)),
[ARTICLES_ROUTE]: () => response(200, articles),
[ARTICLES_ROUTE]: () => response(200, articleEnvelope(articles, { size: 5 })),
})
const column = card().element.parentElement!
@@ -344,7 +345,7 @@ describe('Home card, the request URL', () => {
it('asks for exactly /v1/activity?size=5 and nothing else', async () => {
await mountHome('owner', {
[ACTIVITY_ROUTE]: () => page(recentItems(5)),
[ARTICLES_ROUTE]: () => response(200, articles),
[ARTICLES_ROUTE]: () => response(200, articleEnvelope(articles, { size: 5 })),
})
const activityCalls = calls.filter((call) => call.includes('/v1/activity'))
@@ -356,7 +357,7 @@ describe('Home card, "View all" opens the activity page', () => {
it('navigates to /activity', async () => {
await mountHome('owner', {
[ACTIVITY_ROUTE]: () => page(recentItems(1)),
[ARTICLES_ROUTE]: () => response(200, articles),
[ARTICLES_ROUTE]: () => response(200, articleEnvelope(articles, { size: 5 })),
})
const push = vi.spyOn(router, 'push')
@@ -373,7 +374,7 @@ describe('Home card, the French interface', () => {
setLocale('fr')
await mountHome('owner', {
[ACTIVITY_ROUTE]: () => page([], 0),
[ARTICLES_ROUTE]: () => response(200, articles),
[ARTICLES_ROUTE]: () => response(200, articleEnvelope(articles, { size: 5 })),
})
expect(card().find('h2').text()).toBe('Activité récente')