import { expect, test, type Page } from '@playwright/test' import { setEnglishLocale } from './helpers/auth-fixtures' type PromotionStatus = 'PENDING' | 'APPROVED' | 'REJECTED' function promotion(id: number, status: PromotionStatus, name: string, reviewedAt: string | null = null) { return { id, sourceSkillId: id + 100, sourceSkillDisplayName: name, sourceSkillSummary: `Summary for ${name}`, sourceNamespace: 'team-ai', sourceSkillSlug: name.toLowerCase().replaceAll(' ', '-'), sourceVersion: '1.3.0', sourceVersionFileCount: 23, sourceVersionTotalSize: 1_843_200, sourceSkillDownloadCount: 18, sourceSkillStarCount: 5, targetNamespace: 'global', targetSkillId: status === 'PENDING' ? undefined : id + 200, status, submittedBy: 'owner-1', submittedByName: 'Owner One', reviewedBy: status === 'PENDING' ? undefined : 'admin-1', reviewedByName: status === 'PENDING' ? undefined : 'Admin One', reviewComment: status === 'REJECTED' ? 'Needs clearer documentation before promotion.' : 'Looks good.', submittedAt: '2026-06-18T12:00:00Z', reviewedAt, } } test.describe('Promotion review dashboard', () => { let unexpectedPromotionRequests: string[] let expectedPromotionRequests: string[] test.beforeEach(async ({ page }) => { unexpectedPromotionRequests = [] expectedPromotionRequests = [] await setEnglishLocale(page) await page.context().setExtraHTTPHeaders({ 'X-Mock-User-Id': 'local-admin', }) await page.route('**/api/v1/auth/me', async (route) => { await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ code: 0, msg: 'success', data: { userId: 'local-admin', displayName: 'Local Admin', email: 'local-admin@example.com', avatarUrl: '', oauthProvider: 'mock', platformRoles: ['SUPER_ADMIN'], }, timestamp: new Date().toISOString(), requestId: 'e2e-auth', }), }) }) await page.route('**/api/web/notifications/unread-count', async (route) => { await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ code: 0, msg: 'success', data: { count: 0 }, timestamp: new Date().toISOString(), requestId: 'e2e-notifications', }), }) }) await page.route('**/api/web/me/namespaces', async (route) => { await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ code: 0, msg: 'success', data: [], timestamp: new Date().toISOString(), requestId: 'e2e-namespaces', }), }) }) }) async function installPromotionRouteMock(page: Page, expectedSignatures: string[]) { expectedPromotionRequests = [...expectedSignatures] await page.route('**/api/web/promotions**', async (route) => { const request = route.request() const url = new URL(request.url()) const allowedParams = new Set(['status', 'page', 'size', 'sortBy', 'sortDirection']) const extraParams = Array.from(url.searchParams.keys()).filter((key) => !allowedParams.has(key)) if (request.method() !== 'GET' || url.pathname !== '/api/web/promotions' || extraParams.length > 0) { unexpectedPromotionRequests.push(url.toString()) await route.fulfill({ status: 400, contentType: 'application/json', body: JSON.stringify({ code: 400, msg: 'unexpected promotion request shape', data: null, timestamp: new Date().toISOString(), requestId: 'e2e-promotions-error', }), }) return } const statusParam = url.searchParams.get('status') if (statusParam === null) { unexpectedPromotionRequests.push(url.toString()) await route.fulfill({ status: 400, contentType: 'application/json', body: JSON.stringify({ code: 400, msg: 'promotion request must include explicit status', data: null, timestamp: new Date().toISOString(), requestId: 'e2e-promotions-error', }), }) return } const statusValues: PromotionStatus[] = ['PENDING', 'APPROVED', 'REJECTED'] if (!statusValues.includes(statusParam as PromotionStatus)) { unexpectedPromotionRequests.push(url.toString()) await route.fulfill({ status: 400, contentType: 'application/json', body: JSON.stringify({ code: 400, msg: `unexpected status ${statusParam}`, data: null, timestamp: new Date().toISOString(), requestId: 'e2e-promotions-error', }), }) return } const status = statusParam as PromotionStatus const sortBy = url.searchParams.get('sortBy') const sortDirectionParam = url.searchParams.get('sortDirection') const requestSignature = `${status}|${sortBy ?? 'none'}|${sortDirectionParam ?? 'none'}` const expectedSignature = expectedPromotionRequests.shift() if (requestSignature !== expectedSignature) { unexpectedPromotionRequests.push(`${url.toString()} expected ${expectedSignature ?? 'no more requests'}`) await route.fulfill({ status: 400, contentType: 'application/json', body: JSON.stringify({ code: 400, msg: 'unexpected promotion request order', data: null, timestamp: new Date().toISOString(), requestId: 'e2e-promotions-error', }), }) return } if (status === 'PENDING' && (sortBy !== null || sortDirectionParam !== null)) { unexpectedPromotionRequests.push(url.toString()) await route.fulfill({ status: 400, contentType: 'application/json', body: JSON.stringify({ code: 400, msg: 'pending request must not include history sort params', data: null, timestamp: new Date().toISOString(), requestId: 'e2e-promotions-error', }), }) return } if (status !== 'PENDING' && (sortBy !== 'reviewedAt' || !['ASC', 'DESC'].includes(sortDirectionParam ?? ''))) { unexpectedPromotionRequests.push(url.toString()) await route.fulfill({ status: 400, contentType: 'application/json', body: JSON.stringify({ code: 400, msg: 'history request must include reviewedAt sort params', data: null, timestamp: new Date().toISOString(), requestId: 'e2e-promotions-error', }), }) return } const dataByStatus: Record[]> = { PENDING: [promotion(1, 'PENDING', 'Knowledge Helper')], APPROVED: [ promotion(2, 'APPROVED', 'Newest Approved', '2026-06-18T09:00:00Z'), promotion(3, 'APPROVED', 'Oldest Approved', '2026-06-17T09:00:00Z'), ], REJECTED: [ promotion(4, 'REJECTED', 'Newest Rejected', '2026-06-18T08:00:00Z'), promotion(5, 'REJECTED', 'Oldest Rejected', '2026-06-16T08:00:00Z'), ], } const items = [...dataByStatus[status]] if (status !== 'PENDING' && sortDirectionParam === 'ASC') { items.reverse() } await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ code: 0, msg: 'success', data: { items, total: items.length, page: 0, size: 20 }, timestamp: new Date().toISOString(), requestId: 'e2e-promotions', }), }) }) } function expectPromotionRequestsSatisfied() { expect(unexpectedPromotionRequests).toEqual([]) expect(expectedPromotionRequests).toEqual([]) } test('shows enhanced pending cards and sorts approved/rejected history by reviewed time', async ({ page }) => { await installPromotionRouteMock(page, [ 'PENDING|none|none', 'APPROVED|reviewedAt|DESC', 'APPROVED|reviewedAt|ASC', 'REJECTED|reviewedAt|DESC', 'REJECTED|reviewedAt|ASC', ]) const pendingRequest = page.waitForRequest((request) => { const url = new URL(request.url()) return url.pathname === '/api/web/promotions' && url.searchParams.get('status') === 'PENDING' && !url.searchParams.has('sortBy') && !url.searchParams.has('sortDirection') }) await page.goto('/dashboard/promotions') await pendingRequest await expect(page.getByRole('heading', { name: 'Promotion Review' })).toBeVisible() await expect(page.getByRole('heading', { name: 'Knowledge Helper' })).toBeVisible() await expect(page.getByText('@team-ai/knowledge-helper -> @global')).toBeVisible() await expect(page.getByText('Summary for Knowledge Helper')).toBeVisible() await expect(page.getByText(/Jun 18, 2026/)).toBeVisible() await expect(page.getByText('v1.3.0')).toBeVisible() await expect(page.getByText('Submitter Owner One')).toBeVisible() await expect(page.getByText('23 files')).toBeVisible() await expect(page.getByText('1.8 MB')).toBeVisible() await expect(page.getByText('18 downloads')).toBeVisible() await expect(page.getByText('5 stars')).toBeVisible() const approvedDescRequest = page.waitForRequest((request) => { const url = new URL(request.url()) return url.pathname === '/api/web/promotions' && url.searchParams.get('status') === 'APPROVED' && url.searchParams.get('sortBy') === 'reviewedAt' && url.searchParams.get('sortDirection') === 'DESC' }) await page.getByRole('tab', { name: 'Approved' }).click() await approvedDescRequest const approvedTable = page.getByRole('table', { name: 'Promotion history' }) await expect(approvedTable).toBeVisible() await expect(approvedTable.getByRole('row').nth(1)).toContainText('Newest Approved') await expect(approvedTable.getByRole('row').nth(2)).toContainText('Oldest Approved') const approvedAscRequest = page.waitForRequest((request) => { const url = new URL(request.url()) return url.pathname === '/api/web/promotions' && url.searchParams.get('status') === 'APPROVED' && url.searchParams.get('sortBy') === 'reviewedAt' && url.searchParams.get('sortDirection') === 'ASC' }) await page.getByRole('button', { name: 'Sort by reviewed time ascending' }).click() await approvedAscRequest await expect(page.getByRole('button', { name: 'Sort by reviewed time descending' })).toBeVisible() await expect(approvedTable.getByRole('row').nth(1)).toContainText('Oldest Approved') await expect(approvedTable.getByRole('row').nth(2)).toContainText('Newest Approved') const rejectedDescRequest = page.waitForRequest((request) => { const url = new URL(request.url()) return url.pathname === '/api/web/promotions' && url.searchParams.get('status') === 'REJECTED' && url.searchParams.get('sortBy') === 'reviewedAt' && url.searchParams.get('sortDirection') === 'DESC' }) await page.getByRole('tab', { name: 'Rejected' }).click() await rejectedDescRequest await expect(page.getByRole('button', { name: 'Sort by reviewed time ascending' })).toBeVisible() const rejectedAscRequest = page.waitForRequest((request) => { const url = new URL(request.url()) return url.pathname === '/api/web/promotions' && url.searchParams.get('status') === 'REJECTED' && url.searchParams.get('sortBy') === 'reviewedAt' && url.searchParams.get('sortDirection') === 'ASC' }) await page.getByRole('button', { name: 'Sort by reviewed time ascending' }).click() await rejectedAscRequest await expect(page.getByRole('button', { name: 'Sort by reviewed time descending' })).toBeVisible() await page.getByRole('tab', { name: 'Approved' }).click() await expect(page.getByRole('button', { name: 'Sort by reviewed time descending' })).toBeVisible() expectPromotionRequestsSatisfied() }) test('sorter can be toggled from the keyboard', async ({ page }) => { await installPromotionRouteMock(page, [ 'PENDING|none|none', 'APPROVED|reviewedAt|DESC', 'APPROVED|reviewedAt|ASC', ]) await page.goto('/dashboard/promotions') const approvedDescRequest = page.waitForRequest((request) => { const url = new URL(request.url()) return url.pathname === '/api/web/promotions' && url.searchParams.get('status') === 'APPROVED' && url.searchParams.get('sortBy') === 'reviewedAt' && url.searchParams.get('sortDirection') === 'DESC' }) await page.getByRole('tab', { name: 'Approved' }).click() await approvedDescRequest const approvedAscRequest = page.waitForRequest((request) => { const url = new URL(request.url()) return url.pathname === '/api/web/promotions' && url.searchParams.get('status') === 'APPROVED' && url.searchParams.get('sortBy') === 'reviewedAt' && url.searchParams.get('sortDirection') === 'ASC' }) await page.getByRole('button', { name: 'Sort by reviewed time ascending' }).focus() await page.keyboard.press('Enter') await approvedAscRequest await expect(page.getByRole('button', { name: 'Sort by reviewed time descending' })).toBeVisible() expectPromotionRequestsSatisfied() }) })