/** * 管理后台冒烟测试 — IIS部署环境 * * 运行方式: * cd frontend * npx playwright test e2e/smoke-iis.spec.ts --project=chromium * * 断言标准:严格检查数据内容,禁止只检查元素存在 */ import { test, expect, type Page } from '@playwright/test' // === 登录辅助函数 === async function login(page: Page) { // 直接用完整URL,不依赖baseURL await page.goto('http://127.0.0.1/admin/login') await page.waitForLoadState('networkidle') // 等待登录表单渲染 await page.waitForSelector('input', { timeout: 10000 }) // 用CSS选择器定位:第一个input是用户名,第二个是密码 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 }) } // === 辅助:断言文本不为空且非占位符 === function assertValidValue(text: string | null, label: string) { expect(text, `${label} 不应为null`).toBeTruthy() const trimmed = text!.trim() expect(trimmed, `${label} 不应为空字符串`).not.toBe('') expect(trimmed, `${label} 不应为占位符"-"`).not.toBe('-') expect(trimmed, `${label} 不应为占位符"--"`).not.toBe('--') expect(trimmed, `${label} 不应为"undefined"`).not.toBe('undefined') expect(trimmed, `${label} 不应为"null"`).not.toBe('null') } // ======================== // 登录测试 // ======================== test.describe('登录', () => { test('登录成功跳转到仪表盘', async ({ page }) => { await login(page) // 断言:URL包含dashboard expect(page.url()).toMatch(/dashboard/) // 断言:侧边栏有"仪表盘"菜单项 await expect(page.getByRole('menuitem', { name: '仪表盘' })).toBeVisible() }) }) // ======================== // 仪表盘 // ======================== test.describe('仪表盘', () => { test.beforeEach(async ({ page }) => { await login(page) // 确保在仪表盘页面 if (!page.url().includes('dashboard')) { await page.goto('http://127.0.0.1/admin/dashboard') } // 等待数据加载 await page.waitForTimeout(2000) }) test('统计卡片有数值', async ({ page }) => { // 找到所有卡片区域,检查数值部分不为空 const cards = page.locator('.el-card') const cardCount = await cards.count() expect(cardCount, '仪表盘应该有统计卡片').toBeGreaterThan(0) // 检查每个卡片内的数值区域 for (let i = 0; i < cardCount; i++) { const card = cards.nth(i) // 卡片内的数值通常在较大的字体div中 const valueEl = card.locator('div').filter({ hasText: /^\d/ }).first() if (await valueEl.isVisible()) { const text = await valueEl.textContent() // 数值应该包含数字 expect(text, `第${i + 1}个卡片应该有数值`).toMatch(/\d/) } } }) test('采集服务状态显示正常', async ({ page }) => { // 检查采集服务状态区域 const statusArea = page.locator('text=采集服务') if (await statusArea.isVisible()) { // 状态文本不应为空 const statusText = await page.locator('.el-tag').first().textContent() assertValidValue(statusText, '采集服务状态') } }) }) // ======================== // 产量报表 // ======================== test.describe('产量报表', () => { test.beforeEach(async ({ page }) => { await login(page) await page.goto('http://127.0.0.1/admin/production') // 等待页面数据加载 await page.waitForTimeout(2000) }) test('默认加载今天数据', async ({ page }) => { // 检查日期选择器显示今天的日期 const today = new Date() const dateStr = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}` const dateInput = page.locator('input[placeholder]').first() const inputValue = await dateInput.inputValue() expect(inputValue, `日期选择器应显示今天 ${dateStr}`).toContain(dateStr.slice(0, 7)) // 至少年月匹配 // 检查表格有数据 const rows = page.locator('.el-table__body-wrapper .el-table__row') const rowCount = await rows.count() expect(rowCount, '产量报表应默认有数据').toBeGreaterThan(0) }) test('汇总卡片有数值', async ({ page }) => { // 检查4个卡片:总产量、运行机床、切削总时、平均产量 const cardLabels = ['总产量', '运行机床', '切削总时', '平均产量'] for (const label of cardLabels) { // 找到标签文本所在的卡片 const labelEl = page.locator(`text=${label}`).first() await expect(labelEl, `"${label}"标签应可见`).toBeVisible() // 同一个卡片内的数值(紧跟标签的大字div) const card = labelEl.locator('..').locator('..') const valueDiv = card.locator('div').last() const valueText = await valueDiv.textContent() assertValidValue(valueText, `"${label}"的数值`) } }) test('表格数据列有值', async ({ page }) => { const rows = page.locator('.el-table__body-wrapper .el-table__row') const rowCount = await rows.count() expect(rowCount, '表格应有数据行').toBeGreaterThan(0) // 检查第一行的关键列 const firstRow = rows.first() const cells = firstRow.locator('td .cell') // 日期列(第1列) const dateText = await cells.nth(0).textContent() assertValidValue(dateText, '表格第1行日期列') // 机床名列(第2列) const machineText = await cells.nth(1).textContent() assertValidValue(machineText, '表格第1行机床名列') // 产量列(第4列) const qtyText = await cells.nth(3).textContent() assertValidValue(qtyText, '表格第1行产量列') }) test('车间下拉框有选项且筛选生效', async ({ page }) => { // 1. 记录当前表格行数 const rowsBefore = page.locator('.el-table__body-wrapper .el-table__row') const countBefore = await rowsBefore.count() // 2. 点击车间下拉框展开(点击外层wrapper) const workshopSelect = page.locator('.el-form-item').filter({ hasText: '车间' }).locator('.el-select') await workshopSelect.click() await page.waitForTimeout(500) // 3. 断言:下拉列表中有选项 const options = page.locator('.el-select-dropdown__item:visible') await expect(options.first(), '车间下拉框应有选项').toBeVisible() const optionCount = await options.count() expect(optionCount, '车间下拉框选项数量应>=1').toBeGreaterThanOrEqual(1) // 4. 选择第一个选项 await options.first().click() await page.waitForTimeout(300) // 5. 点击查询按钮 await page.getByRole('button', { name: '查询' }).click() await page.waitForTimeout(2000) // 6. 断言:表格有数据(筛选后仍应有数据) const rowsAfter = page.locator('.el-table__body-wrapper .el-table__row') const countAfter = await rowsAfter.count() expect(countAfter, '筛选后表格应有数据').toBeGreaterThan(0) }) test('机床下拉框有选项', async ({ page }) => { const machineSelect = page.locator('.el-form-item').filter({ hasText: '机床' }).locator('.el-select') await machineSelect.click() await page.waitForTimeout(500) const options = page.locator('.el-select-dropdown__item:visible') await expect(options.first(), '机床下拉框应有选项').toBeVisible() const count = await options.count() expect(count, '机床下拉框选项数量应>=1').toBeGreaterThanOrEqual(1) // 关闭下拉框 await page.keyboard.press('Escape') }) test('工人下拉框有选项', async ({ page }) => { const workerSelect = page.locator('.el-form-item').filter({ hasText: '工人' }).locator('.el-select') await workerSelect.click() await page.waitForTimeout(500) const options = page.locator('.el-select-dropdown__item:visible') await expect(options.first(), '工人下拉框应有选项').toBeVisible() const count = await options.count() expect(count, '工人下拉框选项数量应>=1').toBeGreaterThanOrEqual(1) // 关闭下拉框 await page.keyboard.press('Escape') }) test('重置按钮清空筛选', async ({ page }) => { // 先选择一个车间 const workshopSelect = page.locator('.el-form-item').filter({ hasText: '车间' }).locator('.el-select') await workshopSelect.click() await page.waitForTimeout(500) await page.locator('.el-select-dropdown__item:visible').first().click() await page.waitForTimeout(300) // 点重置 await page.getByRole('button', { name: '重置' }).click() await page.waitForTimeout(1000) // 断言:车间下拉框恢复占位符 const placeholder = workshopSelect.locator('.el-select__placeholder') const placeholderText = await placeholder.textContent() expect(placeholderText, '重置后车间下拉框应显示占位符').toContain('请选择') }) test('修正弹窗能打开', async ({ page }) => { // 点击第一行的"修正"按钮 const adjustBtn = page.locator('.el-table__body-wrapper .el-table__row').first().getByRole('button', { name: '修正' }) await adjustBtn.click() await page.waitForTimeout(500) // 断言:弹窗出现,标题包含"修正" const dialog = page.locator('.el-dialog:visible') await expect(dialog, '修正弹窗应可见').toBeVisible() const title = await dialog.locator('.el-dialog__title').textContent() expect(title, '弹窗标题应包含"修正"').toContain('修正') // 断言:当前产量输入框有值 const input = dialog.locator('input').first() const inputValue = await input.inputValue() expect(inputValue, '当前产量输入框应有初始值').toBeTruthy() // 关闭弹窗 await dialog.locator('.el-dialog__headerbtn').click() }) test('修正历史弹窗能打开', async ({ page }) => { const historyBtn = page.locator('.el-table__body-wrapper .el-table__row').first().getByRole('button', { name: '修正历史' }) await historyBtn.click() await page.waitForTimeout(500) // 断言:弹窗出现 const dialog = page.locator('.el-dialog:visible') await expect(dialog, '修正历史弹窗应可见').toBeVisible() const title = await dialog.locator('.el-dialog__title').textContent() expect(title, '弹窗标题应包含"修正历史"').toContain('修正历史') // 关闭弹窗 await dialog.locator('.el-dialog__headerbtn').click() }) // === 新增:表格所有列验证 === test('表格所有列有数据(运行时间、切削时间、日状态、修正标记)', async ({ page }) => { const rows = page.locator('.el-table__body-wrapper .el-table__row') expect(await rows.count(), '表格应有数据行').toBeGreaterThan(0) const cells = rows.first().locator('td .cell') // 运行时间列(第5列)— 应有数值,不为空 const runTime = await cells.nth(4).textContent() assertValidValue(runTime, '运行时间列') // 切削时间列(第6列)— 应有数值,不为空 const cutTime = await cells.nth(5).textContent() assertValidValue(cutTime, '切削时间列') // 日状态列(第7列)— 应为"正常"或"缺失"或"离线" const status = await cells.nth(6).textContent() expect(status, '日状态列应有值').toBeTruthy() const validStatuses = ['正常', '缺失', '离线'] expect(validStatuses, `日状态应为已知值,实际: ${status?.trim()}`).toContain(status?.trim()) // 修正列(第8列)— 应为"✓"或"-" const adjusted = await cells.nth(7).textContent() expect(adjusted, '修正列应有值').toBeTruthy() }) // === 新增:分页切换 === test('分页切换有效', async ({ page }) => { // 检查分页组件 const pagination = page.locator('.el-pagination') await expect(pagination, '分页组件应可见').toBeVisible() // 检查总数 const totalText = await pagination.locator('.el-pagination__total').textContent() expect(totalText, '分页应显示总数').toContain('条') // 如果有多页,切换到第2页 const page2Btn = pagination.locator('.el-pager > li').nth(1) if (await page2Btn.isVisible()) { // 记录第一页第一行机床名 const row1Before = await page.locator('.el-table__body-wrapper .el-table__row').first().locator('td .cell').nth(1).textContent() // 点击第2页 await page2Btn.click() await page.waitForTimeout(1500) // 验证:第2页高亮 await expect(page2Btn, '第2页应高亮').toHaveClass(/is-active/) // 验证:表格第一行机床名变化了(不同页数据不同) const row1After = await page.locator('.el-table__body-wrapper .el-table__row').first().locator('td .cell').nth(1).textContent() expect(row1After, '切页后第一行数据应不同').toBeTruthy() } }) // === 新增:日期范围筛选 === test('日期范围筛选生效', async ({ page }) => { // 记录当前总数 const totalBefore = await page.locator('.el-pagination__total').textContent() // 用JS直接修改日期范围(选一个未来日期确保无数据) await page.evaluate(() => { // 找到Vue实例并修改日期 const inputs = document.querySelectorAll('.el-date-editor input') if (inputs.length >= 2) { // 模拟选2099-01-01,一个不可能有数据的日期 const evt = new Event('input', { bubbles: true }) ;(inputs[0] as HTMLInputElement).value = '2099-01-01' inputs[0].dispatchEvent(evt) ;(inputs[1] as HTMLInputElement).value = '2099-01-01' inputs[1].dispatchEvent(evt) } }) // 点查询 await page.locator('button:has-text("查询")').click() await page.waitForTimeout(2000) // 验证:2099年不应该有数据,表格行数应为0或显示空状态 const rows = page.locator('.el-table__body-wrapper .el-table__row') const rowCount = await rows.count() expect(rowCount, '2099年不应该有产量数据').toBe(0) }) // === 新增:程序名文本筛选 === test('程序名筛选生效', async ({ page }) => { // 先看第一行的程序名 const firstProgramCell = page.locator('.el-table__body-wrapper .el-table__row').first().locator('td .cell').nth(2) const programName = await firstProgramCell.textContent() assertValidValue(programName, '第一行程序名列') // 输入程序名筛选 const programInput = page.locator('input[placeholder="输入程序名"]') await programInput.fill(programName!.trim()) await page.locator('button:has-text("查询")').click() await page.waitForTimeout(2000) // 验证:表格数据应包含该程序名 const rows = page.locator('.el-table__body-wrapper .el-table__row') const rowCount = await rows.count() expect(rowCount, `筛选"${programName?.trim()}"后应有数据`).toBeGreaterThan(0) // 验证:每行的程序名列都包含筛选关键词 for (let i = 0; i < Math.min(rowCount, 5); i++) { const cell = rows.nth(i).locator('td .cell').nth(2) const text = await cell.textContent() expect(text?.trim(), `第${i + 1}行程序名应包含筛选关键词`).toContain(programName!.trim()) } }) // === 新增:API返回值 vs 页面显示值对账 === test('汇总卡片数值与API返回一致', async ({ page }) => { // 通过浏览器fetch调API,拿到实际返回值 const apiData = await page.evaluate(async () => { const token = localStorage.getItem('token') const today = new Date().toISOString().slice(0, 10) const res = await fetch(`/api/admin/production/daily-summary?date=${today}`, { headers: { Authorization: `Bearer ${token}` } }) const json = await res.json() return json.data }) expect(apiData, 'API应返回数据').toBeTruthy() // 验证总产量卡片 const totalQtyLabel = page.locator('text=总产量').first() if (await totalQtyLabel.isVisible()) { const card = totalQtyLabel.locator('..').locator('..') const valueDiv = card.locator('div').last() const displayText = await valueDiv.textContent() const displayNum = parseInt(displayText!.trim().replace(/,/g, ''), 10) const apiNum = apiData.totalQuantity expect(displayNum, `页面总产量${displayNum}应与API返回${apiNum}一致`).toBe(apiNum) } // 验证运行机床卡片 const machineLabel = page.locator('text=运行机床').first() if (await machineLabel.isVisible()) { const card = machineLabel.locator('..').locator('..') const valueDiv = card.locator('div').last() const displayText = await valueDiv.textContent() const displayNum = parseInt(displayText!.trim(), 10) const apiNum = apiData.activeMachineCount expect(displayNum, `页面运行机床${displayNum}应与API返回${apiNum}一致`).toBe(apiNum) } }) // === 新增:下拉框选项值验证(前后端字段映射) === test('下拉框选项文本和值都有效', async ({ page }) => { // 展开车间下拉框 const workshopSelect = page.locator('.el-form-item').filter({ hasText: '车间' }).locator('.el-select') await workshopSelect.click() await page.waitForTimeout(500) const options = page.locator('.el-select-dropdown__item:visible') const count = await options.count() expect(count, '车间下拉框应有选项').toBeGreaterThan(0) // 验证每个选项的文本不是空/undefined/null for (let i = 0; i < Math.min(count, 5); i++) { const text = await options.nth(i).textContent() assertValidValue(text, `车间第${i + 1}个选项`) } // 关闭下拉框 await page.keyboard.press('Escape') }) // === 新增:修正弹窗提交闭环 === test('修正弹窗提交后列表刷新', async ({ page }) => { // 获取第一行当前产量 const firstRow = page.locator('.el-table__body-wrapper .el-table__row').first() const qtyCell = firstRow.locator('td .cell').nth(3) const qtyBefore = await qtyCell.textContent() // 点击修正按钮 await firstRow.getByRole('button', { name: '修正' }).click() await page.waitForTimeout(500) const dialog = page.locator('.el-dialog:visible') await expect(dialog).toBeVisible() // 读取当前产量 const currentInput = dialog.locator('input').first() const currentValue = await currentInput.inputValue() expect(currentValue, '当前产量应有值').toBeTruthy() // 修改为新值(原值+1) const newQty = parseInt(currentValue, 10) + 1 const newQtyInput = dialog.locator('input').nth(1) // 清空并输入新值 await newQtyInput.fill(String(newQty)) // 填写修正原因 const reasonTextarea = dialog.locator('textarea') await reasonTextarea.fill('E2E自动化测试修正') // 监听confirm弹窗并自动确认 page.on('dialog', async dialog => { await dialog.accept() }) // 点确认修正 await dialog.locator('button:has-text("确认修正")').click() await page.waitForTimeout(2000) // 验证:弹窗关闭 await expect(page.locator('.el-dialog:visible'), '修正后弹窗应关闭').toHaveCount(0) // 验证:表格刷新了(行数>0说明没崩溃) const rows = page.locator('.el-table__body-wrapper .el-table__row') expect(await rows.count(), '修正后表格应有数据').toBeGreaterThan(0) }) }) // ======================== // 设备管理 // ======================== test.describe('设备管理', () => { test.beforeEach(async ({ page }) => { await login(page) await page.goto('http://127.0.0.1/admin/machine') await page.waitForTimeout(2000) }) test('机床列表有数据', async ({ page }) => { const rows = page.locator('.el-table__body-wrapper .el-table__row') const rowCount = await rows.count() expect(rowCount, '设备管理表格应有数据行').toBeGreaterThan(0) }) })