diff --git a/web/e2e/namespace-skill-pagination.spec.ts b/web/e2e/namespace-skill-pagination.spec.ts new file mode 100644 index 00000000..f3d492bb --- /dev/null +++ b/web/e2e/namespace-skill-pagination.spec.ts @@ -0,0 +1,78 @@ +import { expect, test } from '@playwright/test' +import { setEnglishLocale } from './helpers/auth-fixtures' +import { registerSession } from './helpers/session' +import { E2eTestDataBuilder } from './helpers/test-data-builder' + +test.describe('Namespace Skill List Pagination (Real API)', () => { + test.beforeEach(async ({ page }, testInfo) => { + await setEnglishLocale(page) + await registerSession(page, testInfo) + }) + + test('shows namespace page with skills and no pagination when under 20 skills', async ({ page }, testInfo) => { + const builder = new E2eTestDataBuilder(page, testInfo) + await builder.init() + + try { + const namespace = await builder.ensureWritableNamespace() + await builder.publishSkill(namespace.slug) + + await page.goto(`/space/${namespace.slug}`) + + await expect(page.getByText(`@${namespace.slug}`).first()).toBeVisible() + await expect(page.getByRole('heading', { name: 'Skills', exact: true })).toBeVisible() + + // Verify pagination controls do not appear when there are fewer than 20 skills + await expect(page.getByRole('button', { name: 'Previous' })).toHaveCount(0) + await expect(page.getByRole('button', { name: 'Next' })).toHaveCount(0) + } finally { + await builder.cleanup() + } + }) + + test('shows pagination controls when there are more than 20 skills', async ({ page }, testInfo) => { + const builder = new E2eTestDataBuilder(page, testInfo) + await builder.init() + + try { + const namespace = await builder.ensureWritableNamespace() + + // Publish 21 skills to trigger pagination (PAGE_SIZE = 20) + for (let i = 0; i < 21; i += 1) { + await builder.publishSkill(namespace.slug, { + name: `e2e-skill-${i}`, + description: `Test skill ${i} for pagination`, + }) + } + + await page.goto(`/space/${namespace.slug}`) + + await expect(page.getByText(`@${namespace.slug}`).first()).toBeVisible() + await expect(page.getByRole('heading', { name: 'Skills', exact: true })).toBeVisible() + + // Verify pagination controls appear + const previousButton = page.getByRole('button', { name: 'Previous' }).first() + const nextButton = page.getByRole('button', { name: 'Next' }).first() + + await expect(previousButton).toBeVisible() + await expect(nextButton).toBeVisible() + + // First page: Previous should be disabled, Next should be enabled + await expect(previousButton).toBeDisabled() + await expect(nextButton).toBeEnabled() + + // Navigate to second page + await nextButton.click() + + // Second page: both buttons should be enabled (or Previous enabled, Next disabled if only 2 pages) + await expect(previousButton).toBeEnabled() + + // Navigate back to first page + await previousButton.click() + await expect(previousButton).toBeDisabled() + await expect(nextButton).toBeEnabled() + } finally { + await builder.cleanup() + } + }) +}) diff --git a/web/src/pages/namespace.tsx b/web/src/pages/namespace.tsx index 6754e953..69ac0127 100644 --- a/web/src/pages/namespace.tsx +++ b/web/src/pages/namespace.tsx @@ -1,12 +1,16 @@ +import { useState, useEffect } from 'react' import { useNavigate, useParams } from '@tanstack/react-router' import { useTranslation } from 'react-i18next' import { NamespaceHeader } from '@/features/namespace/namespace-header' import { SkillCard } from '@/features/skill/skill-card' import { SkeletonList } from '@/shared/components/skeleton-loader' import { EmptyState } from '@/shared/components/empty-state' +import { Pagination } from '@/shared/components/pagination' import { useSearchSkills } from '@/shared/hooks/use-skill-queries' import { useNamespaceDetail } from '@/shared/hooks/use-namespace-queries' +const PAGE_SIZE = 20 + /** * Public namespace page showing namespace metadata and the skills currently discoverable inside it. */ @@ -14,13 +18,22 @@ export function NamespacePage() { const { t } = useTranslation() const navigate = useNavigate() const { namespace } = useParams({ from: '/space/$namespace' }) + const [page, setPage] = useState(0) + + // Reset page when namespace changes + useEffect(() => { + setPage(0) + }, [namespace]) const { data: namespaceData, isLoading: isLoadingNamespace } = useNamespaceDetail(namespace) const { data: skillsData, isLoading: isLoadingSkills } = useSearchSkills({ namespace, - size: 20, + page, + size: PAGE_SIZE, }) + const totalPages = skillsData ? Math.max(Math.ceil(skillsData.total / skillsData.size), 1) : 1 + const handleSkillClick = (slug: string) => { navigate({ to: `/space/${namespace}/${encodeURIComponent(slug)}` }) } @@ -47,16 +60,22 @@ export function NamespacePage() { {isLoadingSkills ? ( ) : skillsData && skillsData.items.length > 0 ? ( -
- {skillsData.items.map((skill, idx) => ( -
- handleSkillClick(skill.slug)} - /> -
- ))} -
+ <> +
+ {skillsData.items.map((skill, idx) => ( +
+ handleSkillClick(skill.slug)} + /> +
+ ))} +
+ + {skillsData.total > PAGE_SIZE ? ( + + ) : null} + ) : (