231 lines
7.8 KiB
JavaScript
231 lines
7.8 KiB
JavaScript
|
|
import { test, expect } from '@playwright/test'
|
||
|
|
import {
|
||
|
|
IT_MODEL_CODE,
|
||
|
|
IT_DISPLAY_NAME,
|
||
|
|
ADMIN_ACCOUNT,
|
||
|
|
ADMIN_PASSWORD,
|
||
|
|
USER_PASSWORD,
|
||
|
|
} from './constants.js'
|
||
|
|
|
||
|
|
async function seedAuth(page, loginData, role) {
|
||
|
|
await page.goto('/')
|
||
|
|
await page.evaluate(
|
||
|
|
([data, userRole]) => {
|
||
|
|
localStorage.setItem('access_token', data.accessToken)
|
||
|
|
localStorage.setItem('refresh_token', data.refreshToken)
|
||
|
|
localStorage.setItem(
|
||
|
|
'user_info',
|
||
|
|
JSON.stringify({
|
||
|
|
id: data.userId,
|
||
|
|
email: data.email,
|
||
|
|
nickname: data.nickname,
|
||
|
|
avatar: data.avatar,
|
||
|
|
type: data.type,
|
||
|
|
role: userRole,
|
||
|
|
}),
|
||
|
|
)
|
||
|
|
},
|
||
|
|
[loginData, role],
|
||
|
|
)
|
||
|
|
}
|
||
|
|
|
||
|
|
async function loginAsAdmin(page, request) {
|
||
|
|
const res = await request.post('/api/public/admin/login', {
|
||
|
|
data: { email: ADMIN_ACCOUNT, password: ADMIN_PASSWORD },
|
||
|
|
})
|
||
|
|
expect(res.ok()).toBeTruthy()
|
||
|
|
const json = await res.json()
|
||
|
|
expect(json.code).toBe(200)
|
||
|
|
await seedAuth(page, json.data, 'ADMIN')
|
||
|
|
return json.data.accessToken
|
||
|
|
}
|
||
|
|
|
||
|
|
async function deleteModelByCodeIfExists(request, adminToken, modelCode) {
|
||
|
|
const listRes = await request.get('/api/admin/ai/models', {
|
||
|
|
headers: { Authorization: `Bearer ${adminToken}` },
|
||
|
|
})
|
||
|
|
const list = await listRes.json()
|
||
|
|
if (list.code !== 200) return
|
||
|
|
for (const model of list.data ?? []) {
|
||
|
|
if (model.modelCode === modelCode && !model.isDefault) {
|
||
|
|
await request.delete(`/api/admin/ai/models/${model.id}`, {
|
||
|
|
headers: { Authorization: `Bearer ${adminToken}` },
|
||
|
|
})
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
async function registerUser(request, email) {
|
||
|
|
const res = await request.post('/api/public/user/register', {
|
||
|
|
data: { email, password: USER_PASSWORD, nickname: 'E2E测试用户' },
|
||
|
|
})
|
||
|
|
expect(res.ok()).toBeTruthy()
|
||
|
|
const json = await res.json()
|
||
|
|
expect(json.code).toBe(200)
|
||
|
|
}
|
||
|
|
|
||
|
|
async function loginAsUser(page, request, email) {
|
||
|
|
const res = await request.post('/api/public/user/login', {
|
||
|
|
data: { email, password: USER_PASSWORD, deviceFingerprint: 'e2e' },
|
||
|
|
})
|
||
|
|
expect(res.ok()).toBeTruthy()
|
||
|
|
const json = await res.json()
|
||
|
|
expect(json.code).toBe(200)
|
||
|
|
await seedAuth(page, json.data, 'USER')
|
||
|
|
}
|
||
|
|
|
||
|
|
const MOCK_REPLY = '浏览器联调:这是 Mock 的 AI 回复。'
|
||
|
|
|
||
|
|
/** Mock chat 与发送后的 messages 重载(前端 send 后会再次 GET messages)。 */
|
||
|
|
async function mockAiChatRoutes(page) {
|
||
|
|
const mockedSessions = new Map()
|
||
|
|
|
||
|
|
await page.route('**/api/app/ai/chat', async (route) => {
|
||
|
|
const body = route.request().postDataJSON()
|
||
|
|
const sessionId = body?.sessionId
|
||
|
|
const userMessage = body?.message ?? ''
|
||
|
|
mockedSessions.set(sessionId, [
|
||
|
|
{ id: 1, role: 'user', content: userMessage },
|
||
|
|
{ id: 2, role: 'assistant', content: MOCK_REPLY },
|
||
|
|
])
|
||
|
|
await route.fulfill({
|
||
|
|
status: 200,
|
||
|
|
contentType: 'application/json',
|
||
|
|
body: JSON.stringify({
|
||
|
|
code: 200,
|
||
|
|
message: 'success',
|
||
|
|
data: {
|
||
|
|
sessionId,
|
||
|
|
reply: MOCK_REPLY,
|
||
|
|
roundCount: 1,
|
||
|
|
maxRounds: 5,
|
||
|
|
model: IT_MODEL_CODE,
|
||
|
|
modelDisplayName: IT_DISPLAY_NAME,
|
||
|
|
costMillis: 10,
|
||
|
|
},
|
||
|
|
timestamp: Date.now(),
|
||
|
|
}),
|
||
|
|
})
|
||
|
|
})
|
||
|
|
|
||
|
|
await page.route(/\/api\/app\/ai\/sessions\/\d+\/messages(\?.*)?$/, async (route) => {
|
||
|
|
if (route.request().method() !== 'GET') {
|
||
|
|
await route.continue()
|
||
|
|
return
|
||
|
|
}
|
||
|
|
const sessionId = Number(route.request().url().match(/sessions\/(\d+)/)[1])
|
||
|
|
const messages = mockedSessions.get(sessionId)
|
||
|
|
if (!messages) {
|
||
|
|
await route.continue()
|
||
|
|
return
|
||
|
|
}
|
||
|
|
await route.fulfill({
|
||
|
|
status: 200,
|
||
|
|
contentType: 'application/json',
|
||
|
|
body: JSON.stringify({
|
||
|
|
code: 200,
|
||
|
|
message: 'success',
|
||
|
|
data: {
|
||
|
|
messages,
|
||
|
|
roundCount: 1,
|
||
|
|
modelEnabled: true,
|
||
|
|
readOnlyReason: null,
|
||
|
|
},
|
||
|
|
timestamp: Date.now(),
|
||
|
|
}),
|
||
|
|
})
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
test.describe.configure({ mode: 'serial' })
|
||
|
|
|
||
|
|
test.describe('AI 模型管理浏览器联调', () => {
|
||
|
|
const userEmail = `e2e-${Date.now()}@test.local`
|
||
|
|
|
||
|
|
test('TC-AI-MODEL-E2E-18: 管理端上架模型 → 助手页选择并新会话对话', async ({ page, request }) => {
|
||
|
|
await mockAiChatRoutes(page)
|
||
|
|
const adminToken = await loginAsAdmin(page, request)
|
||
|
|
await deleteModelByCodeIfExists(request, adminToken, IT_MODEL_CODE)
|
||
|
|
|
||
|
|
await page.goto('/admin/ai-models')
|
||
|
|
await expect(page.getByRole('heading', { name: 'AI 模型管理' })).toBeVisible()
|
||
|
|
await expect(page.locator('.el-table')).toBeVisible({ timeout: 15_000 })
|
||
|
|
|
||
|
|
const existingRow = page.locator('tr', { hasText: IT_MODEL_CODE })
|
||
|
|
if (await existingRow.count() === 0) {
|
||
|
|
await page.getByRole('button', { name: '新增模型' }).click()
|
||
|
|
const dialog = page.getByRole('dialog', { name: '新增模型' })
|
||
|
|
await dialog.getByPlaceholder('如:通义 Turbo · 快速').fill(IT_DISPLAY_NAME)
|
||
|
|
await dialog.getByPlaceholder('如:qwen-turbo').fill(IT_MODEL_CODE)
|
||
|
|
const [createResp] = await Promise.all([
|
||
|
|
page.waitForResponse(
|
||
|
|
(r) => r.url().includes('/api/admin/ai/models') && r.request().method() === 'POST',
|
||
|
|
),
|
||
|
|
dialog.getByRole('button', { name: '确定' }).click(),
|
||
|
|
])
|
||
|
|
expect(createResp.ok()).toBeTruthy()
|
||
|
|
const createBody = await createResp.json()
|
||
|
|
expect(createBody?.code).toBe(200)
|
||
|
|
await expect(dialog).toBeHidden({ timeout: 10_000 })
|
||
|
|
}
|
||
|
|
await expect(page.locator('tr', { hasText: IT_MODEL_CODE })).toBeVisible({ timeout: 10_000 })
|
||
|
|
|
||
|
|
await registerUser(request, userEmail)
|
||
|
|
await loginAsUser(page, request, userEmail)
|
||
|
|
|
||
|
|
await page.goto('/ai-assistant')
|
||
|
|
await expect(page.getByText('我是 Jog AI 助手')).toBeVisible({ timeout: 15_000 })
|
||
|
|
|
||
|
|
const modelSelect = page.locator('.model-select')
|
||
|
|
await expect(modelSelect).toBeVisible()
|
||
|
|
await modelSelect.click()
|
||
|
|
await page.getByRole('option', { name: IT_DISPLAY_NAME }).click()
|
||
|
|
|
||
|
|
await Promise.all([
|
||
|
|
page.waitForResponse(
|
||
|
|
(r) => r.url().includes('/api/app/ai/sessions') && r.request().method() === 'POST',
|
||
|
|
),
|
||
|
|
page.getByRole('button', { name: '新会话' }).click(),
|
||
|
|
])
|
||
|
|
|
||
|
|
const question = '浏览器联调:一句话介绍 REST。'
|
||
|
|
const input = page.locator('.input-area textarea')
|
||
|
|
await input.click()
|
||
|
|
await input.fill(question)
|
||
|
|
await expect(page.getByRole('button', { name: '发送' })).toBeEnabled()
|
||
|
|
await Promise.all([
|
||
|
|
page.waitForResponse((r) => r.url().includes('/api/app/ai/chat')),
|
||
|
|
page.getByRole('button', { name: '发送' }).click(),
|
||
|
|
])
|
||
|
|
await expect(page.getByText(MOCK_REPLY)).toBeVisible({ timeout: 30_000 })
|
||
|
|
})
|
||
|
|
|
||
|
|
test('TC-AI-MODEL-E2E-19: 禁用模型后旧会话只读', async ({ page, context, request }) => {
|
||
|
|
await loginAsAdmin(page, request)
|
||
|
|
|
||
|
|
await page.goto('/admin/ai-models')
|
||
|
|
const row = page.locator('tr', { hasText: IT_MODEL_CODE })
|
||
|
|
await expect(row).toBeVisible()
|
||
|
|
await row.getByRole('button', { name: '编辑' }).click()
|
||
|
|
const dialog = page.getByRole('dialog', { name: '编辑模型' })
|
||
|
|
const enabledSwitch = dialog.locator('.el-form-item', { hasText: '启用' }).locator('.el-switch')
|
||
|
|
const isChecked = await enabledSwitch.getAttribute('class')
|
||
|
|
if (isChecked?.includes('is-checked')) {
|
||
|
|
await enabledSwitch.click()
|
||
|
|
}
|
||
|
|
await dialog.getByRole('button', { name: '确定' }).click()
|
||
|
|
await expect(row.getByText('禁用')).toBeVisible({ timeout: 10_000 })
|
||
|
|
|
||
|
|
const userPage = await context.newPage()
|
||
|
|
await loginAsUser(userPage, request, userEmail)
|
||
|
|
|
||
|
|
await userPage.goto('/ai-assistant')
|
||
|
|
const sessionItem = userPage.locator('.session-item').first()
|
||
|
|
await expect(sessionItem).toBeVisible({ timeout: 15_000 })
|
||
|
|
await sessionItem.click()
|
||
|
|
|
||
|
|
await expect(userPage.getByText('该会话所用模型已停用')).toBeVisible({ timeout: 15_000 })
|
||
|
|
await expect(userPage.getByPlaceholder('请输入您的问题…')).toBeDisabled()
|
||
|
|
})
|
||
|
|
})
|