From 0020989ccb642ab0a9f8042595f29db44ea8f94d Mon Sep 17 00:00:00 2001 From: dongmucat <1127093059@qq.com> Date: Mon, 27 Apr 2026 10:54:57 +0800 Subject: [PATCH 1/3] fix(namespace): add pagination to namespace skill list The namespace detail page hardcoded a limit of 20 skills with no pagination controls, so namespaces with more than 20 skills appeared truncated. Add page state, wire it into the search query, and render the shared Pagination component when total exceeds PAGE_SIZE. Also reset page to 0 when the namespace param changes. Closes #350 --- web/e2e/namespace-skill-pagination.spec.ts | 78 ++++++++++++++++++++++ web/src/pages/namespace.tsx | 41 +++++++++--- 2 files changed, 108 insertions(+), 11 deletions(-) create mode 100644 web/e2e/namespace-skill-pagination.spec.ts 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} + ) : ( Date: Mon, 27 Apr 2026 11:25:07 +0800 Subject: [PATCH 2/3] test(namespace): fix pagination e2e rate limit by intercepting search api --- web/e2e/namespace-skill-pagination.spec.ts | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/web/e2e/namespace-skill-pagination.spec.ts b/web/e2e/namespace-skill-pagination.spec.ts index f3d492bb..c6288101 100644 --- a/web/e2e/namespace-skill-pagination.spec.ts +++ b/web/e2e/namespace-skill-pagination.spec.ts @@ -36,14 +36,18 @@ test.describe('Namespace Skill List Pagination (Real API)', () => { try { const namespace = await builder.ensureWritableNamespace() + await builder.publishSkill(namespace.slug) - // 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`, - }) - } + // Intercept the search API to inflate `total` so the pagination component renders. + // This avoids publishing 21 real skills which triggers 429 rate limits in CI. + await page.route('**/api/web/skills?**', async (route) => { + const response = await route.fetch() + const body = await response.json() + if (body.data) { + body.data.total = 21 + } + await route.fulfill({ response, json: body }) + }) await page.goto(`/space/${namespace.slug}`) @@ -64,7 +68,7 @@ test.describe('Namespace Skill List Pagination (Real API)', () => { // Navigate to second page await nextButton.click() - // Second page: both buttons should be enabled (or Previous enabled, Next disabled if only 2 pages) + // Second page: Previous should be enabled await expect(previousButton).toBeEnabled() // Navigate back to first page From c1241ff83a29b5b65dd74123e74c384e5b557a6d Mon Sep 17 00:00:00 2001 From: dongmucat <1127093059@qq.com> Date: Mon, 27 Apr 2026 11:42:43 +0800 Subject: [PATCH 3/3] test(namespace): fully mock search api in pagination e2e test Root cause: two issues caused CI failure: 1. publishSkill hit 429 rate limit from other tests in the suite 2. glob pattern '?' was ambiguous for matching literal '?' in URLs Fix: remove publishSkill dependency, use regex route matching, and return fully mocked API responses with fake skill data. This tests the frontend pagination UI behavior without needing real published skills. --- web/e2e/namespace-skill-pagination.spec.ts | 42 ++++++++++++++++------ 1 file changed, 32 insertions(+), 10 deletions(-) diff --git a/web/e2e/namespace-skill-pagination.spec.ts b/web/e2e/namespace-skill-pagination.spec.ts index c6288101..64e61e09 100644 --- a/web/e2e/namespace-skill-pagination.spec.ts +++ b/web/e2e/namespace-skill-pagination.spec.ts @@ -36,17 +36,39 @@ test.describe('Namespace Skill List Pagination (Real API)', () => { try { const namespace = await builder.ensureWritableNamespace() - await builder.publishSkill(namespace.slug) - // Intercept the search API to inflate `total` so the pagination component renders. - // This avoids publishing 21 real skills which triggers 429 rate limits in CI. - await page.route('**/api/web/skills?**', async (route) => { - const response = await route.fetch() - const body = await response.json() - if (body.data) { - body.data.total = 21 - } - await route.fulfill({ response, json: body }) + // Build a fake skill list to simulate >20 skills without hitting rate limits. + const fakeSkill = (i: number) => ({ + id: 90000 + i, + namespace: namespace.slug, + slug: `fake-skill-${i}`, + displayName: `Fake Skill ${i}`, + summary: `Pagination test skill ${i}`, + downloadCount: 0, + starCount: 0, + ratingAvg: 0, + ratingCount: 0, + versions: [{ id: 90000 + i, version: '1.0.0', status: 'PUBLISHED' }], + }) + + // Intercept the search API with a regex (glob '?' is ambiguous for literal '?'). + // Return fully mocked responses to avoid needing real published skills. + await page.route(/\/api\/web\/skills\?/, async (route) => { + const url = new URL(route.request().url()) + const reqPage = Number(url.searchParams.get('page') ?? '0') + const items = reqPage === 0 + ? Array.from({ length: 20 }, (_, i) => fakeSkill(i)) + : [fakeSkill(20)] + + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + code: 0, + msg: 'success', + data: { items, total: 21, page: reqPage, size: 20 }, + }), + }) }) await page.goto(`/space/${namespace.slug}`)