You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
haoliang-net/frontend/e2e/smoke-iis.spec.ts

499 lines
20 KiB
TypeScript

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

/**
* 管理后台冒烟测试 — 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)
})
})