diff --git a/frontend/e2e/simulator.spec.ts b/frontend/e2e/simulator.spec.ts new file mode 100644 index 0000000..7055548 --- /dev/null +++ b/frontend/e2e/simulator.spec.ts @@ -0,0 +1,106 @@ +/** + * CNC管理后台 - 模拟采集模块 Playwright E2E测试 + * + * 测试范围(IIS模式,真实后端,模拟器未启动): + * - 侧边栏导航:模拟采集菜单项可见可点击 + * - 总览页(/simulator):断连状态UI——未连接提示、按钮禁用 + * + * 运行方式:`npx playwright test e2e/simulator.spec.ts --project=chromium` + * 前提:IIS站点已部署最新前后端代码,无需启动CncSimulator + */ +import { test, expect, type Page } from '@playwright/test' + +// === 登录辅助函数 === +// 使用完整URL避免baseURL(127.0.0.1)与IIS绑定(192.168.1.202)不匹配 +const BASE = 'http://192.168.1.202/admin' + +async function login(page: Page) { + await page.goto(`${BASE}/login`) + await page.waitForLoadState('networkidle') + await page.waitForSelector('input', { timeout: 10000 }) + const inputs = page.locator('input') + await inputs.nth(0).fill('admin') + await inputs.nth(1).fill('admin123') + await page.locator('button').last().click() + await page.waitForURL(/\/(dashboard|admin\/?$)/, { timeout: 15000 }) +} + +// ============================================================ +// 套件1:侧边栏导航 +// ============================================================ +test.describe('模拟采集侧边栏导航', () => { + + test.beforeEach(async ({ page }) => { + await login(page) + }) + + test('侧边栏有"模拟采集"菜单项', async ({ page }) => { + const menuItems = page.locator('.el-menu-item') + const simulatorItem = menuItems.filter({ hasText: '模拟采集' }) + await expect(simulatorItem).toBeVisible() + }) + + test('点击"模拟采集"菜单跳转到总览页', async ({ page }) => { + const menuItems = page.locator('.el-menu-item') + const simulatorItem = menuItems.filter({ hasText: '模拟采集' }) + await simulatorItem.click() + await page.waitForURL(/\/simulator$/, { timeout: 10000 }) + expect(page.url()).toContain('/simulator') + }) +}) + +// ============================================================ +// 套件2:总览页——模拟器未连接状态 +// ============================================================ +test.describe('模拟采集总览页(模拟器未连接)', () => { + + test.beforeEach(async ({ page }) => { + await login(page) + // 通过侧边栏菜单导航到模拟采集页面 + const menuItems = page.locator('.el-menu-item') + const simulatorItem = menuItems.filter({ hasText: '模拟采集' }) + await simulatorItem.click() + await page.waitForURL(/\/simulator$/, { timeout: 10000 }) + // 等待ping API调用完成 + await page.waitForTimeout(2000) + }) + + test('页面显示"模拟器未连接"', async ({ page }) => { + await expect(page.locator('text=模拟器未连接')).toBeVisible() + }) + + test('页面显示友好提示信息', async ({ page }) => { + await expect(page.locator('text=模拟器未启动')).toBeVisible() + }) + + test('三个操作按钮正确禁用', async ({ page }) => { + await expect(page.locator('button:has-text("全部启动")')).toBeDisabled() + await expect(page.locator('button:has-text("全部停止")')).toBeDisabled() + await expect(page.locator('button:has-text("刷新配置")')).toBeDisabled() + }) + + test('面包屑导航显示"首页 / 模拟采集"', async ({ page }) => { + const breadcrumb = page.locator('.el-breadcrumb') + await expect(breadcrumb).toBeVisible() + await expect(breadcrumb.locator('text=首页')).toBeVisible() + await expect(breadcrumb.locator('text=模拟采集')).toBeVisible() + }) + + test('页面无JavaScript异常(排除预期的网络错误)', async ({ page }) => { + const errors: string[] = [] + page.on('console', (msg) => { + if (msg.type() === 'error') errors.push(msg.text()) + }) + await page.reload({ waitUntil: 'networkidle' }) + await page.waitForTimeout(3000) + // 过滤掉模拟器不可达的预期错误 + const realErrors = errors.filter(e => + !e.includes('simulator') && + !e.includes('50001') && + !e.includes('Network Error') && + !e.includes('ERR_CONNECTION_REFUSED') && + !e.includes('404') + ) + expect(realErrors).toEqual([]) + }) +}) diff --git a/frontend/mock/simulator.ts b/frontend/mock/simulator.ts index d950d8d..c513110 100644 --- a/frontend/mock/simulator.ts +++ b/frontend/mock/simulator.ts @@ -87,8 +87,8 @@ const mocks: MockMethod[] = [ // 重新加载 { url: '/api/admin/simulator/reload', method: 'post', response: () => ({ code: 0, message: 'success', data: { ok: true, count: 2 } }) }, - // 单地址状态(匹配 /address/{port}/status) - { url: /\/api\/admin\/simulator\/address\/\d+\/status$/, method: 'get', response: () => ({ + // 单地址状态(参数化路由 :port 匹配动态端口) + { url: '/api/admin/simulator/address/:port/status', method: 'get', response: () => ({ code: 0, message: 'success', data: { name: 'FANUC-1号模拟', port: 9001, isRunning: true, requestCount: 1560, successCount: 1540, failCount: 20, @@ -100,20 +100,20 @@ const mocks: MockMethod[] = [ })}, // 单地址启动/停止/事件/设置(POST类,统返回ok) - { url: /\/api\/admin\/simulator\/address\/\d+\/start$/, method: 'post', response: () => ({ code: 0, message: 'success', data: { ok: true } }) }, - { url: /\/api\/admin\/simulator\/address\/\d+\/stop$/, method: 'post', response: () => ({ code: 0, message: 'success', data: { ok: true } }) }, - { url: /\/api\/admin\/simulator\/address\/\d+\/event$/, method: 'post', response: () => ({ code: 0, message: 'success', data: { ok: true } }) }, - { url: /\/api\/admin\/simulator\/address\/\d+\/interval$/, method: 'post', response: () => ({ code: 0, message: 'success', data: { ok: true } }) }, - { url: /\/api\/admin\/simulator\/address\/\d+\/network$/, method: 'post', response: () => ({ code: 0, message: 'success', data: { ok: true } }) }, - { url: /\/api\/admin\/simulator\/address\/\d+\/mode$/, method: 'post', response: () => ({ code: 0, message: 'success', data: { ok: true } }) }, - { url: /\/api\/admin\/simulator\/address\/\d+\/add-device$/, method: 'post', response: () => ({ code: 0, message: 'success', data: { ok: true } }) }, - { url: /\/api\/admin\/simulator\/address\/\d+\/remove-device$/, method: 'post', response: () => ({ code: 0, message: 'success', data: { ok: true } }) }, + { url: '/api/admin/simulator/address/:port/start', method: 'post', response: () => ({ code: 0, message: 'success', data: { ok: true } }) }, + { url: '/api/admin/simulator/address/:port/stop', method: 'post', response: () => ({ code: 0, message: 'success', data: { ok: true } }) }, + { url: '/api/admin/simulator/address/:port/event', method: 'post', response: () => ({ code: 0, message: 'success', data: { ok: true } }) }, + { url: '/api/admin/simulator/address/:port/interval', method: 'post', response: () => ({ code: 0, message: 'success', data: { ok: true } }) }, + { url: '/api/admin/simulator/address/:port/network', method: 'post', response: () => ({ code: 0, message: 'success', data: { ok: true } }) }, + { url: '/api/admin/simulator/address/:port/mode', method: 'post', response: () => ({ code: 0, message: 'success', data: { ok: true } }) }, + { url: '/api/admin/simulator/address/:port/add-device', method: 'post', response: () => ({ code: 0, message: 'success', data: { ok: true } }) }, + { url: '/api/admin/simulator/address/:port/remove-device', method: 'post', response: () => ({ code: 0, message: 'success', data: { ok: true } }) }, // 日志 - { url: /\/api\/admin\/simulator\/address\/\d+\/logs$/, method: 'get', response: () => ({ code: 0, message: 'success', data: mockLogs }) }, + { url: '/api/admin/simulator/address/:port/logs', method: 'get', response: () => ({ code: 0, message: 'success', data: mockLogs }) }, // 统计 - { url: /\/api\/admin\/simulator\/address\/\d+\/stats$/, method: 'get', response: () => ({ + { url: '/api/admin/simulator/address/:port/stats', method: 'get', response: () => ({ code: 0, message: 'success', data: { totalDevices: 32, onlineDevices: 28, totalParts: 128, partsByDevice: { @@ -124,16 +124,16 @@ const mocks: MockMethod[] = [ })}, // 事件历史 - { url: /\/api\/admin\/simulator\/address\/\d+\/event-history$/, method: 'get', response: () => ({ code: 0, message: 'success', data: [ + { url: '/api/admin/simulator/address/:port/event-history', method: 'get', response: () => ({ code: 0, message: 'success', data: [ { timestamp: '2026-05-06 14:30:00', deviceCode: 'fanake_1.2', eventType: 'change_program', oldProgram: 'O200', newProgram: 'O504', partCountBefore: 10, partCountAfter: 14, detail: '程序切换' }, { timestamp: '2026-05-06 14:25:00', deviceCode: 'fanake_1.3', eventType: 'part_count_increase', oldProgram: 'O1', newProgram: 'O1', partCountBefore: 52, partCountAfter: 53, detail: '零件数+1' } ] })}, // 完整汇总 - { url: /\/api\/admin\/simulator\/address\/\d+\/full-summary$/, method: 'get', response: () => ({ code: 0, message: 'success', data: { exportTime: '2026-05-06 14:35:00', addressName: 'FANUC-1号模拟', port: 9001, totalDevices: 32, onlineDevices: 28, totalParts: 128 } }) }, + { url: '/api/admin/simulator/address/:port/full-summary', method: 'get', response: () => ({ code: 0, message: 'success', data: { exportTime: '2026-05-06 14:35:00', addressName: 'FANUC-1号模拟', port: 9001, totalDevices: 32, onlineDevices: 28, totalParts: 128 } }) }, // 异常日志 - { url: /\/api\/admin\/simulator\/address\/\d+\/error-log$/, method: 'get', response: () => ({ code: 0, message: 'success', data: [] }) }, + { url: '/api/admin/simulator/address/:port/error-log', method: 'get', response: () => ({ code: 0, message: 'success', data: [] }) }, ] export default mocks