diff --git a/web/e2e/admin-users-userid-column.spec.ts b/web/e2e/admin-users-userid-column.spec.ts index 1a780808..75f947d1 100644 --- a/web/e2e/admin-users-userid-column.spec.ts +++ b/web/e2e/admin-users-userid-column.spec.ts @@ -1,135 +1,133 @@ 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']) - // Use mock admin user await page.context().setExtraHTTPHeaders({ - 'X-Mock-User-Id': 'local-admin' + 'X-Mock-User-Id': 'local-admin', }) - await page.goto('/admin/users') - await page.waitForResponse(resp => resp.url().includes('/api/v1/admin/users') && resp.status() === 200) + 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 }) => { - // Wait for table to load - await expect(page.getByRole('columnheader', { name: 'Username' })).toBeVisible() - - // Check userId column exists - const userIdHeader = page.getByRole('columnheader', { name: 'User ID' }) - await expect(userIdHeader).toBeVisible() - - // Verify column order: Username should come before User ID 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')) + const usernameIndex = headerTexts.findIndex((text) => text.includes('Username')) + const userIdIndex = headerTexts.findIndex((text) => text.includes('User ID')) expect(usernameIndex).toBeGreaterThanOrEqual(0) - expect(userIdIndex).toBeGreaterThan(usernameIndex) + expect(userIdIndex).toBe(usernameIndex + 1) }) test('userId values are displayed in table rows', async ({ page }) => { - await expect(page.getByRole('columnheader', { name: 'User ID' })).toBeVisible() - - // Wait for at least one row to load const firstRow = page.getByRole('row').nth(1) await expect(firstRow).toBeVisible() - // Check that userId cells contain non-empty values - const userIdCells = page.getByRole('cell').filter({ hasText: /^[a-zA-Z0-9-]+$/ }) - const count = await userIdCells.count() - expect(count).toBeGreaterThan(0) + // The userId cell is the 2nd column (index 1). It should contain a non-empty identifier. + const userIdCell = firstRow.getByRole('cell').nth(1) + const userIdText = (await userIdCell.textContent())?.trim() ?? '' + expect(userIdText.length).toBeGreaterThan(0) }) - test('copy button exists for each userId', async ({ page }) => { - await expect(page.getByRole('columnheader', { name: 'User ID' })).toBeVisible() + 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') - // Wait for rows to load - const rows = page.getByRole('row') - const rowCount = await rows.count() - - if (rowCount <= 1) { - test.skip() - } - - // Check for copy buttons (they should have aria-label or be buttons) - const copyButtons = page.getByRole('button').filter({ hasText: /copy/i }) + const copyButtons = page.getByRole('button', { name: /copy/i }) const buttonCount = await copyButtons.count() - - expect(buttonCount).toBeGreaterThan(0) + // At least one copy button per row body should be present. + expect(buttonCount).toBeGreaterThanOrEqual(rowCount - 1) }) - test('clicking copy button copies userId to clipboard', async ({ page }) => { - await expect(page.getByRole('columnheader', { name: 'User ID' })).toBeVisible() - - // Find first copy button in the table + test('clicking copy button copies the row userId to clipboard', async ({ page }) => { const firstRow = page.getByRole('row').nth(1) await expect(firstRow).toBeVisible() - // Get the userId text before clicking - const userIdCell = firstRow.getByRole('cell').nth(1) // Assuming userId is 2nd column - const userIdText = await userIdCell.textContent() + // 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) - // Click the copy button const copyButton = firstRow.getByRole('button', { name: /copy/i }).first() await copyButton.click() - // Wait for clipboard to update - await page.waitForTimeout(100) + // CopyButton flips its label to "Copied" once the clipboard write resolves; wait for that. + await expect(firstRow.getByRole('button', { name: /copied/i })).toBeVisible() - // Verify clipboard content const clipboardText = await page.evaluate(() => navigator.clipboard.readText()) - expect(clipboardText).toBeTruthy() - expect(clipboardText.length).toBeGreaterThan(0) + expect(clipboardText).toBe(expectedUserId) }) test('copy button shows feedback after clicking', async ({ page }) => { - await expect(page.getByRole('columnheader', { name: 'User ID' })).toBeVisible() - const firstRow = page.getByRole('row').nth(1) await expect(firstRow).toBeVisible() const copyButton = firstRow.getByRole('button', { name: /copy/i }).first() await copyButton.click() - // Check for "Copied" feedback - await expect(page.getByText(/copied/i)).toBeVisible({ timeout: 2000 }) + await expect(firstRow.getByRole('button', { name: /copied/i })).toBeVisible() }) - test('userId column persists after search/filter', async ({ page }) => { + 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() + }) - // Perform a search if search input exists - const searchInput = page.getByPlaceholder(/search/i).first() - if (await searchInput.isVisible()) { - await searchInput.fill('test') - await page.waitForTimeout(500) - } + 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(), + ]) - // Verify userId column still exists await expect(page.getByRole('columnheader', { name: 'User ID' })).toBeVisible() }) test('userId column persists across pagination', async ({ page }) => { - await expect(page.getByRole('columnheader', { name: 'User ID' })).toBeVisible() - - // Check if pagination exists 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') - if (await nextButton.isVisible() && await nextButton.isEnabled()) { - await nextButton.click() - await page.waitForTimeout(500) + await Promise.all([ + page.waitForResponse( + (resp) => resp.url().includes('/api/v1/admin/users') && resp.status() === 200, + ), + nextButton.click(), + ]) - // Verify userId column still exists on next page - await expect(page.getByRole('columnheader', { name: 'User ID' })).toBeVisible() - } else { - test.skip() - } + await expect(page.getByRole('columnheader', { name: 'User ID' })).toBeVisible() }) })