skillhub/web/e2e/promotions-review.spec.ts
XiaoSeS 4d71a16ddd fix(notification): complete polling migration
Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>
2026-09-03 11:38:02 +08:00

358 lines
14 KiB
TypeScript

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<PromotionStatus, ReturnType<typeof promotion>[]> = {
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()
})
})