test(web): align namespace flows with unified space navigation

Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>
This commit is contained in:
XiaoSeS 2026-10-15 09:00:00 +08:00
parent 3bfd8a322a
commit 1d7daeb150
7 changed files with 95 additions and 36 deletions

View file

@ -50,7 +50,9 @@ test.describe('Batch Import Members (Real API)', () => {
}
await page.goto(`/dashboard/namespaces/${namespace.slug}/members`)
await expect(page.getByRole('heading', { name: 'Member Management' })).toBeVisible()
await expect(page.getByRole('heading', { name: namespace.displayName, exact: true })).toBeVisible()
await expect(page.getByRole('navigation', { name: 'Space management' })
.getByRole('link', { name: 'Members', exact: true })).toHaveAttribute('aria-current', 'page')
const batchButton = page.getByRole('button', { name: 'Batch Import' })
await expect(batchButton).toBeVisible()

View file

@ -43,6 +43,8 @@ test.describe('Dashboard Routes (Real API)', () => {
await expect(page.getByRole('heading', { name: 'Namespace not found' })).toBeVisible()
await page.goto('/dashboard/namespaces/e2e-missing-namespace/reviews')
await expect(page.getByRole('heading', { name: 'Namespace Reviews' })).toBeVisible()
const reviewSection = page.getByRole('region', { name: 'Namespace Reviews' })
await expect(reviewSection).toBeVisible()
await expect(reviewSection.getByText('No review records', { exact: true })).toBeVisible()
})
})

View file

@ -1,13 +1,31 @@
import { expect, test } from '@playwright/test'
import { expect, test, type Page } from '@playwright/test'
import { setEnglishLocale } from './helpers/auth-fixtures'
import { E2eTestDataBuilder } from './helpers/test-data-builder'
import { loginWithCredentials } from './helpers/session'
async function findNamespaceCard(page: Page, slug: string) {
await page.goto('/dashboard/namespaces')
await expect(page.getByRole('heading', { name: 'My Namespaces' })).toBeVisible()
const card = page.getByTestId(`namespace-card-${slug}`)
const next = page.getByRole('button', { name: 'Next', exact: true })
const firstCard = page.locator('[data-testid^="namespace-card-"]').first()
await expect(firstCard).toBeVisible()
while (!(await card.isVisible()) && await next.count() > 0 && await next.isEnabled()) {
const previousId = await firstCard.getAttribute('data-testid')
await next.click()
await expect(firstCard).not.toHaveAttribute('data-testid', previousId!)
}
await expect(card.getByText(`@${slug}`, { exact: true })).toBeVisible()
return card
}
test.describe('My Namespaces Data (Real API)', () => {
test.beforeEach(async ({ page }) => {
test.beforeEach(async ({ page }, testInfo) => {
await setEnglishLocale(page)
await page.context().setExtraHTTPHeaders({
'X-Mock-User-Id': 'local-admin',
})
await loginWithCredentials(page, {
username: process.env.E2E_ADMIN_USERNAME ?? process.env.BOOTSTRAP_ADMIN_USERNAME ?? 'admin',
password: process.env.E2E_ADMIN_PASSWORD ?? process.env.BOOTSTRAP_ADMIN_PASSWORD ?? 'ChangeMe!2026',
}, testInfo)
})
test('shows namespace created by request helper', async ({ page }, testInfo) => {
@ -15,11 +33,8 @@ test.describe('My Namespaces Data (Real API)', () => {
await builder.init()
try {
const namespace = await builder.ensureWritableNamespace()
await page.goto('/dashboard/namespaces')
await expect(page.getByRole('heading', { name: 'My Namespaces' })).toBeVisible()
await expect(page.getByText(`@${namespace.slug}`)).toBeVisible()
const namespace = await builder.createNamespace('e2e-list')
await findNamespaceCard(page, namespace.slug)
} finally {
await builder.cleanup()
}
@ -32,11 +47,12 @@ test.describe('My Namespaces Data (Real API)', () => {
try {
const namespace = await builder.createNamespace('e2e-delete')
await page.goto('/dashboard/namespaces')
const namespaceCard = page.getByTestId(`namespace-card-${namespace.slug}`)
await expect(namespaceCard.getByText(`@${namespace.slug}`)).toBeVisible()
const namespaceCard = await findNamespaceCard(page, namespace.slug)
await namespaceCard.getByRole('button', { name: 'Enter space', exact: true }).click()
await page.getByRole('navigation', { name: 'Space management' })
.getByRole('link', { name: 'Settings', exact: true }).click()
await expect(page.getByRole('heading', { name: 'Space lifecycle', exact: true })).toBeVisible()
await page.getByTestId(`delete-namespace-${namespace.slug}`).click()
await expect(page.getByTestId('namespace-action-dialog-delete')).toBeVisible()
@ -48,7 +64,8 @@ test.describe('My Namespaces Data (Real API)', () => {
const deleteResponse = await deleteResponsePromise
expect(deleteResponse.ok()).toBeTruthy()
await expect(page.getByText(`@${namespace.slug}`)).toHaveCount(0)
await expect(page).toHaveURL(/\/dashboard\/namespaces$/)
await expect(page.getByTestId(`namespace-card-${namespace.slug}`)).toHaveCount(0)
} finally {
await builder.cleanup()
}
@ -66,10 +83,11 @@ test.describe('My Namespaces Data (Real API)', () => {
description: 'Prevents namespace deletion during Playwright validation',
})
await page.goto('/dashboard/namespaces')
const namespaceCard = page.getByTestId(`namespace-card-${namespace.slug}`)
await expect(namespaceCard.getByText(`@${namespace.slug}`)).toBeVisible()
const namespaceCard = await findNamespaceCard(page, namespace.slug)
await namespaceCard.getByRole('button', { name: 'Enter space', exact: true }).click()
await page.getByRole('navigation', { name: 'Space management' })
.getByRole('link', { name: 'Settings', exact: true }).click()
await expect(page.getByRole('heading', { name: 'Space lifecycle', exact: true })).toBeVisible()
await expect(page.getByTestId(`delete-namespace-${namespace.slug}`)).toHaveCount(0)
} finally {
await builder.cleanup()

View file

@ -18,8 +18,11 @@ test.describe('Namespace Members Data (Real API)', () => {
await page.goto(`/dashboard/namespaces/${namespace.slug}/members`)
await expect(page.getByRole('heading', { name: 'Member Management' })).toBeVisible()
await expect(page.getByText(`@${namespace.slug}`).first()).toBeVisible()
await expect(page.getByRole('heading', { name: namespace.displayName, exact: true })).toBeVisible()
await expect(page.getByRole('navigation', { name: 'Space management' })
.getByRole('link', { name: 'Members', exact: true })).toHaveAttribute('aria-current', 'page')
await expect(page.getByText(`@${namespace.slug}`, { exact: true })).toBeVisible()
await expect(page.getByRole('table')).toBeVisible()
} finally {
await builder.cleanup()
}

View file

@ -16,7 +16,10 @@ test.describe('Namespace Review Detail Access (Real API)', () => {
await page.goto(`/dashboard/namespaces/${seeded.namespace.slug}/reviews`)
await expect(page.getByRole('heading', { name: 'Namespace Reviews' })).toBeVisible()
await expect(page.getByRole('heading', { name: seeded.namespace.displayName, exact: true })).toBeVisible()
await expect(page.getByRole('navigation', { name: 'Space management' })
.getByRole('link', { name: 'Reviews', exact: true })).toHaveAttribute('aria-current', 'page')
await expect(page.getByRole('region', { name: 'Namespace Reviews' })).toBeVisible()
await expect(page.getByText(`${seeded.namespace.slug}/${seeded.skill.slug}`)).toBeVisible()
await page.getByRole('link', { name: 'Open review' }).first().click()
@ -37,7 +40,12 @@ test.describe('Namespace Review Detail Access (Real API)', () => {
await page.goto('/dashboard/reviews')
await expect(page).toHaveURL(/\/dashboard\/namespaces\/.+\/reviews$/)
await expect(page.getByRole('heading', { name: 'Namespace Reviews' })).toBeVisible()
const selectedSlug = decodeURIComponent(new URL(page.url()).pathname.split('/')[3])
await expect(page.getByRole('heading', { level: 1 })).toBeVisible()
await expect(page.getByText(`@${selectedSlug}`, { exact: true })).toBeVisible()
await expect(page.getByRole('navigation', { name: 'Space management' })
.getByRole('link', { name: 'Reviews', exact: true })).toHaveAttribute('aria-current', 'page')
await expect(page.getByRole('region', { name: 'Namespace Reviews' })).toBeVisible()
} finally {
await seeded?.cleanup()
}

View file

@ -15,8 +15,12 @@ test.describe('Namespace Reviews Data (Real API)', () => {
seeded = await createNamespaceReviewData(browser, page, testInfo)
await page.goto(`/dashboard/namespaces/${seeded.namespace.slug}/reviews`)
await expect(page.getByRole('heading', { name: 'Namespace Reviews' })).toBeVisible()
await expect(page.getByText(`Review tasks for ${seeded.namespace.displayName}`)).toBeVisible()
await expect(page.getByRole('heading', { name: seeded.namespace.displayName, exact: true })).toBeVisible()
await expect(page.getByRole('navigation', { name: 'Space management' })
.getByRole('link', { name: 'Reviews', exact: true })).toHaveAttribute('aria-current', 'page')
await expect(page.getByRole('region', { name: 'Namespace Reviews' })).toBeVisible()
await expect(page.getByText(`${seeded.namespace.slug}/${seeded.skill.slug}`, { exact: true })).toBeVisible()
await expect(page.getByRole('combobox', { name: 'Review status' })).toHaveText('Pending')
} finally {
await seeded?.cleanup()
}

View file

@ -1,12 +1,15 @@
import { expect, test } from '@playwright/test'
import { setEnglishLocale } from './helpers/auth-fixtures'
import { registerSession } from './helpers/session'
import { loginWithCredentials } 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)
await loginWithCredentials(page, {
username: process.env.E2E_ADMIN_USERNAME ?? process.env.BOOTSTRAP_ADMIN_USERNAME ?? 'admin',
password: process.env.E2E_ADMIN_PASSWORD ?? process.env.BOOTSTRAP_ADMIN_PASSWORD ?? 'ChangeMe!2026',
}, testInfo)
})
test('shows namespace page with skills and no pagination when under 20 skills', async ({ page }, testInfo) => {
@ -14,17 +17,28 @@ test.describe('Namespace Skill List Pagination (Real API)', () => {
await builder.init()
try {
const namespace = await builder.ensureWritableNamespace()
const namespace = await builder.createNamespace('e2e-pagination-small')
await builder.publishSkill(namespace.slug)
const searchResponsePromise = page.waitForResponse((response) => {
const url = new URL(response.url())
return url.pathname === '/api/web/skills'
&& url.searchParams.get('namespace') === namespace.slug
})
await page.goto(`/space/${namespace.slug}`)
const searchResponse = await searchResponsePromise
expect(searchResponse.ok()).toBeTruthy()
const search: { data: { total: number } } = await searchResponse.json()
expect(search.data.total).toBeLessThanOrEqual(20)
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)
// Fresh namespace keeps this fixture independent of previous tests.
const skillList = page.getByRole('heading', { name: 'Skills', exact: true }).locator('..')
await expect(skillList.getByRole('navigation', { name: 'Pagination', exact: true })).toHaveCount(0)
await expect(skillList.getByRole('button', { name: 'Previous', exact: true })).toHaveCount(0)
await expect(skillList.getByRole('button', { name: 'Next', exact: true })).toHaveCount(0)
} finally {
await builder.cleanup()
}
@ -35,7 +49,7 @@ test.describe('Namespace Skill List Pagination (Real API)', () => {
await builder.init()
try {
const namespace = await builder.ensureWritableNamespace()
const namespace = await builder.createNamespace('e2e-pagination-many')
// Build a fake skill list to simulate >20 skills without hitting rate limits.
const fakeSkill = (i: number) => ({
@ -77,8 +91,10 @@ test.describe('Namespace Skill List Pagination (Real API)', () => {
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()
const skillList = page.getByRole('heading', { name: 'Skills', exact: true }).locator('..')
const pagination = skillList.getByRole('navigation', { name: 'Pagination', exact: true })
const previousButton = pagination.getByRole('button', { name: 'Previous', exact: true })
const nextButton = pagination.getByRole('button', { name: 'Next', exact: true })
await expect(previousButton).toBeVisible()
await expect(nextButton).toBeVisible()
@ -87,16 +103,22 @@ test.describe('Namespace Skill List Pagination (Real API)', () => {
await expect(previousButton).toBeDisabled()
await expect(nextButton).toBeEnabled()
await expect(skillList.getByText('Fake Skill 0', { exact: true })).toBeVisible()
// Navigate to second page
await nextButton.click()
// Second page: Previous should be enabled
await expect(previousButton).toBeEnabled()
await expect(nextButton).toBeDisabled()
await expect(skillList.getByText('Fake Skill 20', { exact: true })).toBeVisible()
await expect(skillList.getByText('Fake Skill 0', { exact: true })).toHaveCount(0)
await expect(pagination.getByRole('button', { name: 'Go to page 2', exact: true })).toHaveAttribute('aria-current', 'page')
// Navigate back to first page
await previousButton.click()
await expect(previousButton).toBeDisabled()
await expect(nextButton).toBeEnabled()
await expect(skillList.getByText('Fake Skill 0', { exact: true })).toBeVisible()
} finally {
await builder.cleanup()
}