mirror of
https://github.com/iflytek/skillhub.git
synced 2026-08-28 11:25:00 +00:00
- Add ariaLabel prop to CopyButton for screen reader differentiation
- Pass per-user aria-label: "Copy user ID for {username}"
- Add truncation for long userIds (max-w-[14rem] + title tooltip)
- Scope E2E copy-button assertions to userId cell to avoid false positives
- Assert on span.font-mono for userId text to exclude button text
- Use toHaveText instead of getByRole name for "Copied" feedback check
138 lines
5.7 KiB
TypeScript
138 lines
5.7 KiB
TypeScript
import { expect, test } from '@playwright/test'
|
|
import { setEnglishLocale } from './helpers/auth-fixtures'
|
|
|
|
// This spec verifies the admin UI surfaces userId next to username and supports one-click copy.
|
|
// We rely on the `X-Mock-User-Id: local-admin` mock profile (the same approach used by other
|
|
// admin specs such as `my-namespaces-data.spec.ts`). The mock profile activates a deterministic
|
|
// admin session without exercising the full SSO path; treat results here as evidence of the
|
|
// admin UI behavior, not of the real-service auth path.
|
|
test.describe('Admin Users - UserId Column', () => {
|
|
test.beforeEach(async ({ page, context }) => {
|
|
await setEnglishLocale(page)
|
|
await context.grantPermissions(['clipboard-read', 'clipboard-write'])
|
|
|
|
await page.context().setExtraHTTPHeaders({
|
|
'X-Mock-User-Id': 'local-admin',
|
|
})
|
|
|
|
await Promise.all([
|
|
page.waitForResponse(
|
|
(resp) => resp.url().includes('/api/v1/admin/users') && resp.status() === 200,
|
|
),
|
|
page.goto('/admin/users'),
|
|
])
|
|
|
|
await expect(page.getByRole('columnheader', { name: 'User ID' })).toBeVisible()
|
|
})
|
|
|
|
test('userId column appears after username column', async ({ page }) => {
|
|
const headers = page.getByRole('columnheader')
|
|
const headerTexts = await headers.allTextContents()
|
|
|
|
const usernameIndex = headerTexts.findIndex((text) => text.includes('Username'))
|
|
const userIdIndex = headerTexts.findIndex((text) => text.includes('User ID'))
|
|
|
|
expect(usernameIndex).toBeGreaterThanOrEqual(0)
|
|
expect(userIdIndex).toBe(usernameIndex + 1)
|
|
})
|
|
|
|
test('userId values are displayed in table rows', async ({ page }) => {
|
|
const firstRow = page.getByRole('row').nth(1)
|
|
await expect(firstRow).toBeVisible()
|
|
|
|
// Assert on the monospace span specifically, not the full cell (which includes button text).
|
|
const userIdSpan = firstRow.getByRole('cell').nth(1).locator('span.font-mono')
|
|
const userIdText = (await userIdSpan.textContent())?.trim() ?? ''
|
|
expect(userIdText.length).toBeGreaterThan(0)
|
|
})
|
|
|
|
test('copy button exists for each userId row', async ({ page }) => {
|
|
const rowCount = await page.getByRole('row').count()
|
|
test.skip(rowCount <= 1, 'No data rows available to assert copy buttons')
|
|
|
|
// Scope to the userId cell (2nd column) in each data row to avoid false positives
|
|
// from other copy buttons that may appear elsewhere on the page.
|
|
const dataRows = page.getByRole('row').filter({ hasNot: page.getByRole('columnheader') })
|
|
const rows = await dataRows.count()
|
|
for (let i = 0; i < rows; i++) {
|
|
const userIdCell = dataRows.nth(i).getByRole('cell').nth(1)
|
|
await expect(userIdCell.getByRole('button', { name: /copy/i })).toBeVisible()
|
|
}
|
|
})
|
|
|
|
test('clicking copy button copies the row userId to clipboard', async ({ page }) => {
|
|
const firstRow = page.getByRole('row').nth(1)
|
|
await expect(firstRow).toBeVisible()
|
|
|
|
// Extract the displayed userId from the monospace span inside the 2nd column cell.
|
|
const userIdSpan = firstRow.getByRole('cell').nth(1).locator('span.font-mono')
|
|
const expectedUserId = (await userIdSpan.textContent())?.trim() ?? ''
|
|
expect(expectedUserId.length).toBeGreaterThan(0)
|
|
|
|
const copyButton = firstRow.getByRole('cell').nth(1).getByRole('button').first()
|
|
await copyButton.click()
|
|
|
|
// Wait for the button text to flip to "Copied" as feedback.
|
|
await expect(copyButton).toHaveText(/copied/i)
|
|
|
|
const clipboardText = await page.evaluate(() => navigator.clipboard.readText())
|
|
expect(clipboardText).toBe(expectedUserId)
|
|
})
|
|
|
|
test('copy button shows feedback after clicking', async ({ page }) => {
|
|
const firstRow = page.getByRole('row').nth(1)
|
|
await expect(firstRow).toBeVisible()
|
|
|
|
const copyButton = firstRow.getByRole('cell').nth(1).getByRole('button').first()
|
|
await copyButton.click()
|
|
|
|
// Wait for the button text to flip to "Copied" as feedback.
|
|
await expect(copyButton).toHaveText(/copied/i)
|
|
})
|
|
|
|
test('userId column persists after triggering a search', async ({ page }) => {
|
|
const searchInput = page.getByLabel('Search users')
|
|
await expect(searchInput).toBeVisible()
|
|
|
|
// Trigger the actual search by clicking the Search button and wait for the API response,
|
|
// rather than relying on debounced input.
|
|
await searchInput.fill('admin')
|
|
await Promise.all([
|
|
page.waitForResponse(
|
|
(resp) => resp.url().includes('/api/v1/admin/users') && resp.status() === 200,
|
|
),
|
|
page.getByRole('button', { name: 'Search', exact: true }).click(),
|
|
])
|
|
|
|
await expect(page.getByRole('columnheader', { name: 'User ID' })).toBeVisible()
|
|
})
|
|
|
|
test('userId column persists after applying a status filter', async ({ page }) => {
|
|
// Open the status filter Select and pick "Active". This exercises the filter path that the
|
|
// previous test version skipped entirely.
|
|
await page.getByLabel('Status filter').click()
|
|
await Promise.all([
|
|
page.waitForResponse(
|
|
(resp) => resp.url().includes('/api/v1/admin/users') && resp.status() === 200,
|
|
),
|
|
page.getByRole('option', { name: 'Active' }).click(),
|
|
])
|
|
|
|
await expect(page.getByRole('columnheader', { name: 'User ID' })).toBeVisible()
|
|
})
|
|
|
|
test('userId column persists across pagination', async ({ page }) => {
|
|
const nextButton = page.getByRole('button', { name: /next/i })
|
|
const isReachable = (await nextButton.isVisible()) && (await nextButton.isEnabled())
|
|
test.skip(!isReachable, 'Pagination not reachable with current data set')
|
|
|
|
await Promise.all([
|
|
page.waitForResponse(
|
|
(resp) => resp.url().includes('/api/v1/admin/users') && resp.status() === 200,
|
|
),
|
|
nextButton.click(),
|
|
])
|
|
|
|
await expect(page.getByRole('columnheader', { name: 'User ID' })).toBeVisible()
|
|
})
|
|
})
|