|
- import type { Page } from '@playwright/test'
-
- import { captureScreenshot, expect, test } from './fixtures'
- import { adminCredentials, confirmMessageBox, loginAsAdmin } from './helpers'
-
- const missingProjectId = '9223372036854775807'
-
- const contentPages = [
- { path: '/content/models', childPath: `/content/models/${missingProjectId}/edit`, label: '模型制作' },
- { path: '/content/scenes', childPath: `/content/scenes/${missingProjectId}/edit`, label: '场景制作' },
- { path: '/content/training-projects', childPath: `/content/training-projects/${missingProjectId}/edit`, label: '训练编排' },
- { path: '/content/ofd', childPath: `/content/ofd/${missingProjectId}/preview`, label: '作业指导' },
- ] as const
-
- const escapeRegExp = (value: string) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
-
- const menuRow = (page: Page, name: string) => page
- .locator('.menu-table-panel .el-table__body-wrapper .el-table__row')
- .filter({
- has: page.locator('.menu-name-cell b').filter({ hasText: new RegExp(`^${escapeRegExp(name)}$`) }),
- })
- .first()
-
- const waitForMenuManagement = async (page: Page) => {
- await expect(page.locator('.menus-page')).toBeVisible()
- await expect(page.locator('.menu-table-panel .el-loading-mask:visible')).toHaveCount(0, { timeout: 15_000 })
- await expect(page.locator('.menu-table-panel .el-table__body-wrapper .el-table__row').first()).toBeVisible({ timeout: 15_000 })
- }
-
- const expectActiveContentMenu = async (page: Page, label: string, href: string) => {
- await expect(page.locator('.header-navigation .header-nav-trigger.active')).toContainText('内容制作')
- const activeItem = page.locator('.secondary-navigation a.active')
- await expect(activeItem).toContainText(label)
- await expect(activeItem).toHaveAttribute('href', href)
- }
-
- const waitForContentList = async (page: Page) => {
- await expect(page.locator('.content-projects-page .content-table-panel')).toBeVisible()
- await expect(page.locator('.content-table-panel .el-loading-mask:visible')).toHaveCount(0, { timeout: 15_000 })
- }
-
- const positionText = (row: ReturnType<typeof menuRow>) => row
- .locator('.cell')
- .filter({ hasText: /^\d+\s*\/\s*\d+$/ })
- .first()
-
- test('菜单草稿操作不落库且新版内容导航与子路由活动态正确', async ({ page }, testInfo) => {
- await loginAsAdmin(page)
-
- await test.step('管理员进入菜单管理并验证搜索与重置', async () => {
- await page.goto('/system/menus')
- await expect(page).toHaveURL(/\/system\/menus(?:\?|$)/)
- // 页头卡已去掉,页面身份改由页面标签栏承担
- await expect(page.getByRole('tab', { name: '菜单管理', exact: true })).toBeVisible()
- await waitForMenuManagement(page)
- await expect(page.locator('.secondary-navigation a.active')).toContainText('菜单管理')
-
- const toolbar = page.locator('.menu-toolbar')
- const query = toolbar.getByPlaceholder('请输入菜单名称、权限码或说明')
- await query.fill('模型制作')
- await toolbar.getByRole('button', { name: '搜索', exact: true }).click()
- await expect(menuRow(page, '模型制作')).toBeVisible()
- await expect(page.locator('.sorting-note')).toBeVisible()
- await captureScreenshot(page, testInfo, 'menu-search-model')
-
- await toolbar.getByRole('button', { name: '重置', exact: true }).click()
- await expect(query).toHaveValue('')
- await expect(page.locator('.sorting-note')).toHaveCount(0)
- await expect(menuRow(page, '场景制作')).toBeVisible()
- })
-
- const menuMutationRequests: string[] = []
- page.on('request', (request) => {
- if (/\/menus(?:\/|\?|$)/.test(request.url()) && ['POST', 'PUT', 'PATCH', 'DELETE'].includes(request.method())) {
- menuMutationRequests.push(`${request.method()} ${request.url()}`)
- }
- })
-
- await test.step('三种菜单布局预览均可切换', async () => {
- await page.getByRole('button', { name: '布局预览', exact: true }).click()
- const drawer = page.locator('.menu-preview-drawer:visible, .el-drawer:visible').last()
- const preview = drawer.locator('.menu-layout-preview')
- await expect(drawer).toBeVisible()
-
- const modes = [
- { label: '顶部一级 + 左侧二级', value: 'top-side', screenshot: 'menu-preview-top-side' },
- { label: '左侧分组', value: 'side', screenshot: 'menu-preview-side' },
- { label: '顶部下拉', value: 'top', screenshot: 'menu-preview-top' },
- ] as const
-
- for (const mode of modes) {
- await drawer.locator('.el-radio-button').filter({ hasText: mode.label }).click()
- await expect(preview).toHaveAttribute('data-mode', mode.value)
- if (mode.value === 'top-side') {
- await expect(preview.locator(':scope > header')).toBeVisible()
- await expect(preview.locator(':scope > aside')).toBeVisible()
- await expect(preview.locator('.preview-dropdown')).toHaveCount(0)
- } else if (mode.value === 'side') {
- await expect(preview.locator(':scope > header')).toHaveCount(0)
- await expect(preview.locator(':scope > aside')).toBeVisible()
- await expect(preview.locator('.preview-tree-group').first()).toBeVisible()
- } else {
- await expect(preview.locator(':scope > header')).toBeVisible()
- await expect(preview.locator(':scope > aside')).toHaveCount(0)
- await expect(preview.locator('.preview-dropdown')).toBeVisible()
- }
- await captureScreenshot(page, testInfo, mode.screenshot)
- }
-
- await page.keyboard.press('Escape')
- await expect(drawer).toBeHidden()
- })
-
- await test.step('编辑显示名称只形成草稿并可完整撤销', async () => {
- const originalName = '模型制作'
- const temporaryName = '模型制作临时草稿'
- const row = menuRow(page, originalName)
- await row.getByRole('button', { name: '编辑', exact: true }).click()
-
- const drawer = page.locator('.el-drawer:visible').last()
- await expect(drawer).toContainText('编辑菜单显示信息')
- await drawer.getByPlaceholder('请输入 2–16 个字符的菜单名称').fill(temporaryName)
- await drawer.getByRole('button', { name: '应用到草稿', exact: true }).click()
- await expect(drawer).toBeHidden()
- await expect(menuRow(page, temporaryName)).toBeVisible()
- await expect(page.getByRole('button', { name: '撤销草稿', exact: true })).toBeEnabled()
- await captureScreenshot(page, testInfo, 'menu-edit-draft')
-
- await page.getByRole('button', { name: '撤销草稿', exact: true }).click()
- await confirmMessageBox(page, '撤销修改')
- await expect(menuRow(page, originalName)).toBeVisible()
- await expect(menuRow(page, temporaryName)).toHaveCount(0)
- await expect(page.getByRole('button', { name: '撤销草稿', exact: true })).toBeDisabled()
- })
-
- await test.step('同级排序只形成草稿并可完整撤销', async () => {
- const row = menuRow(page, '模型制作')
- const originalPosition = (await positionText(row).innerText()).trim()
- const match = originalPosition.match(/^(\d+)\s*\/\s*(\d+)$/)
- expect(match, `无法识别菜单当前位置:${originalPosition}`).not.toBeNull()
- const current = Number(match?.[1])
- const total = Number(match?.[2])
- expect(total).toBeGreaterThan(1)
-
- const moveLabel = current < total ? '下移' : '上移'
- const expectedPosition = `${current < total ? current + 1 : current - 1} / ${total}`
- await row.getByRole('button', { name: moveLabel, exact: true }).click()
- await expect(positionText(menuRow(page, '模型制作'))).toHaveText(expectedPosition)
- await expect(page.getByRole('button', { name: '撤销草稿', exact: true })).toBeEnabled()
- await captureScreenshot(page, testInfo, 'menu-reorder-draft')
-
- await page.getByRole('button', { name: '撤销草稿', exact: true }).click()
- await confirmMessageBox(page, '撤销修改')
- await expect(positionText(menuRow(page, '模型制作'))).toHaveText(originalPosition)
- await expect(page.getByRole('button', { name: '撤销草稿', exact: true })).toBeDisabled()
- })
-
- await test.step('四类内容新名称、训练模式与列表活动态正确', async () => {
- for (const item of contentPages) {
- await page.goto(item.path)
- await expect(page).toHaveURL(new RegExp(`${item.path.replaceAll('/', '\\/')}(?:\\?|$)`))
- // 内容制作页面已去掉 SystemPageHeader,页面身份改由页面标签栏承担
- await expect(page.getByRole('tab', { name: item.label, exact: true })).toBeVisible()
- await waitForContentList(page)
- await expectActiveContentMenu(page, item.label, item.path)
-
- if (item.path === '/content/training-projects') {
- const modeTabs = page.locator('.training-mode-tabs button')
- await expect(modeTabs).toHaveCount(3)
- await expect(modeTabs.filter({ hasText: '虚拟仿真' })).toBeVisible()
- await expect(modeTabs.filter({ hasText: '实装实训' })).toBeVisible()
- await expect(modeTabs.filter({ hasText: '对抗训练' })).toBeVisible()
- await expect(modeTabs.filter({ hasText: '虚拟仿真' })).toHaveAttribute('aria-pressed', 'true')
- await captureScreenshot(page, testInfo, 'content-training-three-modes')
- }
-
- if (item.path === '/content/ofd') {
- await expect(page.locator('.content-table-panel')).toBeVisible()
- await captureScreenshot(page, testInfo, 'content-guide-list-or-empty')
- }
- }
- })
-
- await test.step('内容编辑、预览子路由保持对应菜单活动态', async () => {
- for (const item of contentPages) {
- await page.goto(item.childPath)
- await expect(page).toHaveURL(new RegExp(`${item.childPath.replaceAll('/', '\\/')}(?:\\?|$)`))
- await expect(page.locator('.content-editor-page, .guide-workflow-page')).toBeVisible()
- await expectActiveContentMenu(page, item.label, item.path)
- }
- await captureScreenshot(page, testInfo, 'content-child-route-active')
- })
-
- expect(menuMutationRequests, '草稿编辑、排序、预览与撤销不应调用菜单写接口').toEqual([])
- })
-
- interface ApiMenuNode {
- id: string
- name: string
- description: string
- dataVersion: number
- children: ApiMenuNode[]
- }
-
- const configurationPayload = (desired: ApiMenuNode[], versionSource: ApiMenuNode[]) => {
- const versions = new Map(versionSource.flatMap((group) => [group, ...group.children]).map((node) => [String(node.id), node.dataVersion]))
- return {
- groups: desired.map((group) => ({
- id: String(group.id),
- name: group.name,
- description: group.description,
- version: versions.get(String(group.id)),
- items: group.children.map((item) => ({
- id: String(item.id),
- name: item.name,
- description: item.description,
- version: versions.get(String(item.id)),
- })),
- })),
- }
- }
-
- test('菜单配置可原子保存并恢复原始快照', async ({ request }) => {
- const credentials = adminCredentials()
- const loginResponse = await request.post('/api/auth/v1/auth/login', {
- data: { username: credentials.username, password: credentials.password, roleCode: 'admin', rememberMe: false },
- })
- expect(loginResponse.ok()).toBeTruthy()
- const loginBody = await loginResponse.json() as { data?: { accessToken?: string } }
- const accessToken = loginBody.data?.accessToken
- expect(accessToken).toBeTruthy()
- const headers = { Authorization: `Bearer ${accessToken}` }
-
- const readTree = async () => {
- const response = await request.get('/api/auth/v1/menus/tree', { headers })
- expect(response.ok()).toBeTruthy()
- const body = await response.json() as { data?: ApiMenuNode[] }
- return body.data ?? []
- }
-
- try {
- const baseline = await readTree()
- expect(baseline).toHaveLength(5)
- const draft = structuredClone(baseline)
- const contentGroup = draft.find((group) => group.children.some((item) => item.name === '模型制作'))
- const modelItem = contentGroup?.children.find((item) => item.name === '模型制作')
- expect(modelItem).toBeTruthy()
- modelItem!.name = '模型制作验收'
- let changed = false
-
- try {
- const saveResponse = await request.put('/api/auth/v1/menus/configuration', {
- headers,
- data: configurationPayload(draft, baseline),
- })
- expect(saveResponse.ok()).toBeTruthy()
- changed = true
- const saved = await readTree()
- expect(saved.flatMap((group) => group.children).some((item) => item.name === '模型制作验收')).toBeTruthy()
- } finally {
- if (changed) {
- const current = await readTree()
- const restoreResponse = await request.put('/api/auth/v1/menus/configuration', {
- headers,
- data: configurationPayload(baseline, current),
- })
- expect(restoreResponse.ok()).toBeTruthy()
- const restored = await readTree()
- expect(restored.flatMap((group) => group.children).some((item) => item.name === '模型制作')).toBeTruthy()
- expect(restored.flatMap((group) => group.children).some((item) => item.name === '模型制作验收')).toBeFalsy()
- }
- }
- } finally {
- await request.post('/api/auth/v1/auth/logout', { headers })
- }
- })
|