mirror of
https://github.com/iflytek/skillhub.git
synced 2026-09-24 00:55:35 +00:00
358 lines
14 KiB
TypeScript
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()
|
|
})
|
|
})
|