diff --git a/frontend/e2e/admin-ui.spec.ts b/frontend/e2e/admin-ui.spec.ts new file mode 100644 index 0000000..86e724f --- /dev/null +++ b/frontend/e2e/admin-ui.spec.ts @@ -0,0 +1,247 @@ +/** + * CNC管理后台 - Playwright浏览器UI测试 + * + * 测试范围:设备管理、员工管理、采集地址三个模块的批量删除和启用/停用功能 + * 使用Mock模式(/mock前缀路由),不依赖后端API + * + * 测试在真实浏览器中执行,验证: + * - 页面加载和表格数据渲染 + * - 勾选行后批量操作按钮的显示 + * - 批量删除操作(勾选→点击→确认弹窗→验证行数减少) + * - 批量停用操作(勾选已启用行→确认→验证状态变为停用) + * - 批量启用操作(勾选已停用行→确认→验证状态变为启用) + * - 单个删除操作(点击行内删除→确认→验证行数减少) + */ +import { test, expect, type Page } from '@playwright/test' + +// 模拟登录Token(Mock模式下路由守卫只检查token存在性) +const MOCK_TOKEN = 'mock-test-token' + +// 全局beforeEach:在每个测试前预设token到localStorage,避免路由守卫拦截 +test.beforeEach(async ({ page }) => { + await page.addInitScript((token) => { + localStorage.setItem('token', token) + }, MOCK_TOKEN) +}) + +// 辅助函数:重置指定模块的mock数据 +async function resetMockData(page: Page, endpoint: string) { + await page.evaluate(async (url) => { + await fetch(url, { method: 'POST' }) + }, endpoint) +} + +// 辅助函数:导航到页面并等待表格渲染完成 +async function gotoPage(page: Page, path: string) { + await page.goto(path, { waitUntil: 'networkidle' }) + await page.waitForSelector('.el-table', { timeout: 15000 }) +} + +// 辅助函数:勾选表格中指定行的checkbox +async function selectRowsByIndex(page: Page, indices: number[]) { + const checkboxes = page.locator('.el-table__body .el-checkbox') + for (const idx of indices) { + await checkboxes.nth(idx).waitFor({ state: 'visible' }) + await checkboxes.nth(idx).click() + } +} + +// 辅助函数:处理Element Plus的ElMessageBox确认弹窗 +async function confirmDialog(page: Page) { + const dialog = page.locator('.el-message-box') + await dialog.waitFor({ state: 'visible', timeout: 5000 }) + await dialog.locator('.el-button--primary').click() + await dialog.waitFor({ state: 'hidden', timeout: 5000 }) +} + +// 辅助函数:等待ElMessage消息提示并验证文本 +async function expectMessage(page: Page, expectedText: string) { + const msg = page.locator('.el-message') + await msg.waitFor({ state: 'visible', timeout: 10000 }) + await expect(msg).toContainText(expectedText) +} + +// ============================================================ +// 套件1:设备管理(机床)页面测试 +// ============================================================ +test.describe('设备管理页面', () => { + + test.beforeEach(async ({ page }) => { + await gotoPage(page, '/mock/machine') + await resetMockData(page, '/mock-api/test/reset-machines') + await page.reload({ waitUntil: 'networkidle' }) + await page.waitForSelector('.el-table', { timeout: 15000 }) + }) + + test('页面加载 - 表格显示5台机床', async ({ page }) => { + const rows = page.locator('.el-table__body-wrapper .el-table__row') + await expect(rows).toHaveCount(5) + const checkboxes = page.locator('.el-table__body .el-checkbox') + await expect(checkboxes).toHaveCount(5) + }) + + test('勾选行后 - 批量操作按钮显示', async ({ page }) => { + await expect(page.locator('button:has-text("批量删除")')).not.toBeVisible() + await selectRowsByIndex(page, [0]) + await expect(page.locator('button:has-text("批量停用")')).toBeVisible() + await expect(page.locator('button:has-text("批量启用")')).toBeVisible() + await expect(page.locator('button:has-text("批量删除")')).toBeVisible() + await expect(page.locator('button:has-text("批量删除(1)")')).toBeVisible() + }) + + test('批量删除 - 勾选2行后删除,表格行数减少', async ({ page }) => { + const rows = page.locator('.el-table__body-wrapper .el-table__row') + await expect(rows).toHaveCount(5) + await selectRowsByIndex(page, [0, 1]) + await page.locator('button:has-text("批量删除")').click() + await confirmDialog(page) + await expectMessage(page, '批量删除成功') + await page.waitForTimeout(1000) + await expect(rows).toHaveCount(3) + }) + + test('批量停用 - 勾选已启用行后停用,状态标签变更', async ({ page }) => { + await selectRowsByIndex(page, [0]) + await page.locator('button:has-text("批量停用")').click() + await confirmDialog(page) + await expectMessage(page, '操作成功') + await page.waitForTimeout(1000) + const statusTag = page.locator('.el-table__body-wrapper .el-table__row').first().locator('.el-tag:has-text("停用")') + await expect(statusTag).toBeVisible() + }) + + test('批量启用 - 勾选已停用行后启用,状态标签变更', async ({ page }) => { + // 第4行(东-2.5,isEnabled=0) + await selectRowsByIndex(page, [3]) + await page.locator('button:has-text("批量启用")').click() + await confirmDialog(page) + await expectMessage(page, '操作成功') + await page.waitForTimeout(1000) + const statusTag = page.locator('.el-table__body-wrapper .el-table__row').nth(3).locator('.el-tag:has-text("启用")') + await expect(statusTag).toBeVisible() + }) + + test('单个删除 - 点击行内删除按钮,确认后行消失', async ({ page }) => { + const rows = page.locator('.el-table__body-wrapper .el-table__row') + await expect(rows).toHaveCount(5) + await rows.first().locator('button:has-text("删除")').click() + await confirmDialog(page) + await expectMessage(page, '已删除') + await page.waitForTimeout(1000) + await expect(rows).toHaveCount(4) + }) +}) + +// ============================================================ +// 套件2:员工管理页面测试 +// ============================================================ +test.describe('员工管理页面', () => { + + test.beforeEach(async ({ page }) => { + await gotoPage(page, '/mock/worker') + await resetMockData(page, '/mock-api/test/reset-workers') + await page.reload({ waitUntil: 'networkidle' }) + await page.waitForSelector('.el-table', { timeout: 15000 }) + }) + + test('页面加载 - 表格显示3名员工', async ({ page }) => { + const rows = page.locator('.el-table__body-wrapper .el-table__row') + await expect(rows).toHaveCount(3) + }) + + test('勾选行后 - 批量操作按钮显示', async ({ page }) => { + await expect(page.locator('button:has-text("批量删除")')).not.toBeVisible() + await selectRowsByIndex(page, [0]) + await expect(page.locator('button:has-text("批量启用")')).toBeVisible() + await expect(page.locator('button:has-text("批量停用")')).toBeVisible() + await expect(page.locator('button:has-text("批量删除")')).toBeVisible() + }) + + test('批量删除 - 勾选1行后删除,表格行数减少', async ({ page }) => { + const rows = page.locator('.el-table__body-wrapper .el-table__row') + await expect(rows).toHaveCount(3) + await selectRowsByIndex(page, [2]) + await page.locator('button:has-text("批量删除")').click() + await confirmDialog(page) + await expectMessage(page, '批量删除成功') + await page.waitForTimeout(1000) + await expect(rows).toHaveCount(2) + }) + + test('批量停用 - 勾选已启用员工后停用', async ({ page }) => { + await selectRowsByIndex(page, [0]) + await page.locator('button:has-text("批量停用")').click() + await confirmDialog(page) + await expectMessage(page, '操作成功') + await page.waitForTimeout(1000) + const statusTag = page.locator('.el-table__body-wrapper .el-table__row').first().locator('.el-tag:has-text("停用")') + await expect(statusTag).toBeVisible() + }) + + test('批量启用 - 勾选已停用员工后启用', async ({ page }) => { + await selectRowsByIndex(page, [2]) + await page.locator('button:has-text("批量启用")').click() + await confirmDialog(page) + await expectMessage(page, '操作成功') + await page.waitForTimeout(1000) + const statusTag = page.locator('.el-table__body-wrapper .el-table__row').nth(2).locator('.el-tag:has-text("启用")') + await expect(statusTag).toBeVisible() + }) +}) + +// ============================================================ +// 套件3:采集地址页面测试 +// ============================================================ +test.describe('采集地址页面', () => { + + test.beforeEach(async ({ page }) => { + await gotoPage(page, '/mock/collect-address') + await resetMockData(page, '/mock-api/test/reset-addresses') + await page.reload({ waitUntil: 'networkidle' }) + await page.waitForSelector('.el-table', { timeout: 15000 }) + }) + + test('页面加载 - 表格显示3条采集地址', async ({ page }) => { + const rows = page.locator('.el-table__body-wrapper .el-table__row') + await expect(rows).toHaveCount(3) + }) + + test('勾选行后 - 批量操作按钮显示', async ({ page }) => { + await expect(page.locator('button:has-text("批量删除")')).not.toBeVisible() + await selectRowsByIndex(page, [0]) + await expect(page.locator('button:has-text("批量启用")')).toBeVisible() + await expect(page.locator('button:has-text("批量停用")')).toBeVisible() + await expect(page.locator('button:has-text("批量删除")')).toBeVisible() + }) + + test('批量删除 - 勾选1行后删除,表格行数减少', async ({ page }) => { + const rows = page.locator('.el-table__body-wrapper .el-table__row') + await expect(rows).toHaveCount(3) + await selectRowsByIndex(page, [2]) + await page.locator('button:has-text("批量删除")').click() + await confirmDialog(page) + await expectMessage(page, '批量删除成功') + await page.waitForTimeout(1000) + await expect(rows).toHaveCount(2) + }) + + test('批量停用 - 勾选已启用地址后停用', async ({ page }) => { + await selectRowsByIndex(page, [0]) + await page.locator('button:has-text("批量停用")').click() + await confirmDialog(page) + await expectMessage(page, '操作成功') + await page.waitForTimeout(1000) + const statusTag = page.locator('.el-table__body-wrapper .el-table__row').first().locator('.el-tag:has-text("停用")') + await expect(statusTag).toBeVisible() + }) + + test('批量启用 - 勾选已停用地址后启用', async ({ page }) => { + await selectRowsByIndex(page, [2]) + await page.locator('button:has-text("批量启用")').click() + await confirmDialog(page) + await expectMessage(page, '操作成功') + await page.waitForTimeout(1000) + const statusTag = page.locator('.el-table__body-wrapper .el-table__row').nth(2).locator('.el-tag:has-text("启用")') + await expect(statusTag).toBeVisible() + }) +}) diff --git a/frontend/mock/collect-address.ts b/frontend/mock/collect-address.ts index bfcae9c..8a0e21e 100644 --- a/frontend/mock/collect-address.ts +++ b/frontend/mock/collect-address.ts @@ -1,11 +1,20 @@ import type { MockMethod } from './types' -const addresses = [ +let addresses = [ { id: 1, name: 'FANUC-A栋', url: 'http://10.1.1.1/', brandId: 1, brandName: 'FANUC', collectInterval: 30, isEnabled: 1, lastCollectTime: '2026-04-25T17:36:38', failCount: 0, machineCount: 32 }, { id: 2, name: 'FANUC-B栋', url: 'http://10.1.2.1/', brandId: 1, brandName: 'FANUC', collectInterval: 60, isEnabled: 1, lastCollectTime: '2026-04-25T17:35:38', failCount: 0, machineCount: 28 }, { id: 3, name: 'SIEMENS-C栋', url: 'http://10.1.3.1/', brandId: 2, brandName: 'SIEMENS', collectInterval: 30, isEnabled: 0, lastCollectTime: null, failCount: 3, machineCount: 0 }, ] +/** 重置mock数据为初始状态(供测试使用) */ +export function resetAddressMockData() { + addresses = [ + { id: 1, name: 'FANUC-A栋', url: 'http://10.1.1.1/', brandId: 1, brandName: 'FANUC', collectInterval: 30, isEnabled: 1, lastCollectTime: '2026-04-25T17:36:38', failCount: 0, machineCount: 32 }, + { id: 2, name: 'FANUC-B栋', url: 'http://10.1.2.1/', brandId: 1, brandName: 'FANUC', collectInterval: 60, isEnabled: 1, lastCollectTime: '2026-04-25T17:35:38', failCount: 0, machineCount: 28 }, + { id: 3, name: 'SIEMENS-C栋', url: 'http://10.1.3.1/', brandId: 2, brandName: 'SIEMENS', collectInterval: 30, isEnabled: 0, lastCollectTime: null, failCount: 3, machineCount: 0 }, + ] +} + const mock: MockMethod[] = [ { url: '/mock-api/admin/collect-address', method: 'get', response: () => ({ code: 0, data: { items: addresses } }) }, // 参数化路由:GET /mock-api/admin/collect-address/:id @@ -53,6 +62,29 @@ const mock: MockMethod[] = [ ] return { code: 0, data: { items: machines } } } }, + // 批量删除采集地址 + { url: '/mock-api/admin/collect-address/batch-delete', method: 'post', response: ({ body }: any) => { + const ids: number[] = body?.ids || [] + addresses = addresses.filter((a: any) => !ids.includes(a.id)) + return { code: 0, message: 'success', data: null } + }}, + // 参数化路由:PUT /mock-api/admin/collect-address/:id/toggle(切换启用/停用) + { url: '/mock-api/admin/collect-address/:id/toggle', method: 'put', response: ({ params }: any) => { + const id = Number(params.id) + const a = addresses.find((a: any) => a.id === id) + if (a) (a as any).isEnabled = (a as any).isEnabled ? 0 : 1 + return { code: 0, message: 'success', data: null } + }}, + // 参数化路由:DELETE /mock-api/admin/collect-address/:id(单个删除) + { url: '/mock-api/admin/collect-address/:id', method: 'delete', response: ({ params }: any) => { + const id = Number(params.id) + addresses = addresses.filter((a: any) => a.id !== id) + return { code: 0, message: 'success', data: null } + }}, + // 参数化路由:PUT /mock-api/admin/collect-address/:id(编辑更新) + { url: '/mock-api/admin/collect-address/:id', method: 'put', response: () => ({ code: 0, message: 'success', data: null }) }, + // 测试专用:重置mock数据 + { url: '/mock-api/test/reset-addresses', method: 'post', response: () => { resetAddressMockData(); return { code: 0, message: 'reset ok' } } }, ] export default mock diff --git a/frontend/mock/machine.ts b/frontend/mock/machine.ts index c07df5c..d0afb7a 100644 --- a/frontend/mock/machine.ts +++ b/frontend/mock/machine.ts @@ -1,6 +1,6 @@ import type { MockMethod } from './types' -const machines = [ +let machines = [ { id: 1, name: '西-1.8', deviceCode: 'fanake_1.8', workshopId: 1, workshopName: 'A栋', brandId: 1, brandName: 'FANUC', collectAddressId: 1, ipAddress: '10.1.1.8', isOnline: 1, workerId: 1, workerName: '张三', isEnabled: 1 }, { id: 2, name: '西-1.10', deviceCode: 'fanake_1.10', workshopId: 1, workshopName: 'A栋', brandId: 1, brandName: 'FANUC', collectAddressId: 1, ipAddress: '10.1.1.10', isOnline: 1, workerId: 2, workerName: '李四', isEnabled: 1 }, { id: 3, name: '东-2.0', deviceCode: 'fanake_2.0', workshopId: 2, workshopName: 'B栋', brandId: 1, brandName: 'FANUC', collectAddressId: 2, ipAddress: '10.1.2.0', isOnline: 0, workerId: 3, workerName: '王五', isEnabled: 1 }, @@ -8,6 +8,17 @@ const machines = [ { id: 5, name: '南-3.1', deviceCode: 'fanake_3.1', workshopId: 3, workshopName: 'C栋', brandId: 1, brandName: 'FANUC', collectAddressId: 1, ipAddress: '10.1.3.1', isOnline: 1, workerId: 4, workerName: '赵六', isEnabled: 1 }, ] +/** 重置mock数据为初始状态(供测试使用) */ +export function resetMachineMockData() { + machines = [ + { id: 1, name: '西-1.8', deviceCode: 'fanake_1.8', workshopId: 1, workshopName: 'A栋', brandId: 1, brandName: 'FANUC', collectAddressId: 1, ipAddress: '10.1.1.8', isOnline: 1, workerId: 1, workerName: '张三', isEnabled: 1 }, + { id: 2, name: '西-1.10', deviceCode: 'fanake_1.10', workshopId: 1, workshopName: 'A栋', brandId: 1, brandName: 'FANUC', collectAddressId: 1, ipAddress: '10.1.1.10', isOnline: 1, workerId: 2, workerName: '李四', isEnabled: 1 }, + { id: 3, name: '东-2.0', deviceCode: 'fanake_2.0', workshopId: 2, workshopName: 'B栋', brandId: 1, brandName: 'FANUC', collectAddressId: 2, ipAddress: '10.1.2.0', isOnline: 0, workerId: 3, workerName: '王五', isEnabled: 1 }, + { id: 4, name: '东-2.5', deviceCode: 'siemens_2.5', workshopId: 2, workshopName: 'B栋', brandId: 2, brandName: 'SIEMENS', collectAddressId: 2, ipAddress: '10.1.2.5', isOnline: 0, workerId: null, workerName: null, isEnabled: 0 }, + { id: 5, name: '南-3.1', deviceCode: 'fanake_3.1', workshopId: 3, workshopName: 'C栋', brandId: 1, brandName: 'FANUC', collectAddressId: 1, ipAddress: '10.1.3.1', isOnline: 1, workerId: 4, workerName: '赵六', isEnabled: 1 }, + ] +} + const mock: MockMethod[] = [ { url: '/mock-api/admin/machine', @@ -178,6 +189,49 @@ const mock: MockMethod[] = [ method: 'get', response: () => ({ code: 0, data: { items: [{ id: 1, name: '张三', code: 'W001' }, { id: 2, name: '李四', code: 'W002' }, { id: 3, name: '王五', code: 'W003' }] } }), }, + // 批量删除机床 + { + url: '/mock-api/admin/machine/batch-delete', + method: 'post', + response: ({ body }: any) => { + const ids: number[] = body?.ids || [] + machines = machines.filter((m: any) => !ids.includes(m.id)) + return { code: 0, message: 'success', data: null } + }, + }, + // 参数化路由:PUT /mock-api/admin/machine/:id/toggle(切换启用/停用) + { + url: '/mock-api/admin/machine/:id/toggle', + method: 'put', + response: ({ params }: any) => { + const id = Number(params.id) + const m = machines.find((m: any) => m.id === id) + if (m) (m as any).isEnabled = (m as any).isEnabled ? 0 : 1 + return { code: 0, message: 'success', data: null } + }, + }, + // 参数化路由:DELETE /mock-api/admin/machine/:id(单个删除) + { + url: '/mock-api/admin/machine/:id', + method: 'delete', + response: ({ params }: any) => { + const id = Number(params.id) + machines = machines.filter((m: any) => m.id !== id) + return { code: 0, message: 'success', data: null } + }, + }, + // 参数化路由:PUT /mock-api/admin/machine/:id(编辑更新) + { + url: '/mock-api/admin/machine/:id', + method: 'put', + response: () => ({ code: 0, message: 'success', data: null }), + }, + // 测试专用:重置mock数据 + { + url: '/mock-api/test/reset-machines', + method: 'post', + response: () => { resetMachineMockData(); return { code: 0, message: 'reset ok' } }, + }, ] export default mock diff --git a/frontend/mock/worker.ts b/frontend/mock/worker.ts index 2c9cc54..1fedf3c 100644 --- a/frontend/mock/worker.ts +++ b/frontend/mock/worker.ts @@ -1,11 +1,20 @@ import type { MockMethod } from './types' -const workers = [ +let workers = [ { id: 1, code: 'W001', name: '张三', isEnabled: 1, machineCount: 2, machineNames: '西-1.8,西-2.0' }, { id: 2, code: 'W002', name: '李四', isEnabled: 1, machineCount: 1, machineNames: '西-1.10' }, { id: 3, code: 'W003', name: '王五', isEnabled: 0, machineCount: 0, machineNames: '-' }, ] +/** 重置mock数据为初始状态(供测试使用) */ +export function resetWorkerMockData() { + workers = [ + { id: 1, code: 'W001', name: '张三', isEnabled: 1, machineCount: 2, machineNames: '西-1.8,西-2.0' }, + { id: 2, code: 'W002', name: '李四', isEnabled: 1, machineCount: 1, machineNames: '西-1.10' }, + { id: 3, code: 'W003', name: '王五', isEnabled: 0, machineCount: 0, machineNames: '-' }, + ] +} + const mock: MockMethod[] = [ { url: '/mock-api/admin/worker', method: 'get', response: ({ query }: any) => { let items = [...workers] @@ -72,6 +81,29 @@ const mock: MockMethod[] = [ { id: 5, name: '东-2.5', deviceCode: 'siemens_2.5', workshopName: 'B栋' }, { id: 8, name: '北-4.1', deviceCode: 'fanake_4.1', workshopName: 'C栋' }, ] } }) }, + // 批量删除工人 + { url: '/mock-api/admin/worker/batch-delete', method: 'post', response: ({ body }: any) => { + const ids: number[] = body?.ids || [] + workers = workers.filter((w: any) => !ids.includes(w.id)) + return { code: 0, message: 'success', data: null } + }}, + // 参数化路由:PUT /mock-api/admin/worker/:id/toggle(切换启用/停用) + { url: '/mock-api/admin/worker/:id/toggle', method: 'put', response: ({ params }: any) => { + const id = Number(params.id) + const w = workers.find((w: any) => w.id === id) + if (w) (w as any).isEnabled = (w as any).isEnabled ? 0 : 1 + return { code: 0, message: 'success', data: null } + }}, + // 参数化路由:DELETE /mock-api/admin/worker/:id(单个删除) + { url: '/mock-api/admin/worker/:id', method: 'delete', response: ({ params }: any) => { + const id = Number(params.id) + workers = workers.filter((w: any) => w.id !== id) + return { code: 0, message: 'success', data: null } + }}, + // 参数化路由:PUT /mock-api/admin/worker/:id(编辑更新) + { url: '/mock-api/admin/worker/:id', method: 'put', response: () => ({ code: 0, message: 'success', data: null }) }, + // 测试专用:重置mock数据 + { url: '/mock-api/test/reset-workers', method: 'post', response: () => { resetWorkerMockData(); return { code: 0, message: 'reset ok' } } }, ] export default mock diff --git a/frontend/package-lock.json b/frontend/package-lock.json index e1a3186..dafd32a 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -16,6 +16,7 @@ "vue-router": "^4.6.4" }, "devDependencies": { + "@playwright/test": "^1.59.1", "@types/mockjs": "^1.0.10", "@types/node": "^24.12.2", "@vitejs/plugin-vue": "^6.0.6", @@ -513,6 +514,22 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/@playwright/test": { + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.59.1.tgz", + "integrity": "sha512-PG6q63nQg5c9rIi4/Z5lR5IVF7yU5MqmKaPOe0HSc0O2cX1fPi96sUQu5j7eo4gKCkB2AnNGoWt7y4/Xx3Kcqg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.59.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@popperjs/core": { "name": "@sxzz/popperjs-es", "version": "2.11.8", @@ -1925,6 +1942,53 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/playwright": { + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.59.1.tgz", + "integrity": "sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.59.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.59.1.tgz", + "integrity": "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/postcss": { "version": "8.5.10", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz", diff --git a/frontend/package.json b/frontend/package.json index f6c927f..401d128 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -17,6 +17,7 @@ "vue-router": "^4.6.4" }, "devDependencies": { + "@playwright/test": "^1.59.1", "@types/mockjs": "^1.0.10", "@types/node": "^24.12.2", "@vitejs/plugin-vue": "^6.0.6", diff --git a/frontend/playwright.config.ts b/frontend/playwright.config.ts new file mode 100644 index 0000000..8c9a944 --- /dev/null +++ b/frontend/playwright.config.ts @@ -0,0 +1,25 @@ +import { defineConfig } from '@playwright/test' + +export default defineConfig({ + testDir: './e2e', + timeout: 60000, + expect: { timeout: 10000 }, + fullyParallel: false, + retries: 0, + use: { + baseURL: 'http://localhost:5173', + headless: true, + screenshot: 'only-on-failure', + trace: 'retain-on-failure', + actionTimeout: 10000, + }, + projects: [ + { name: 'chromium', use: { browserName: 'chromium' } }, + ], + webServer: { + command: 'npm run dev', + port: 5173, + reuseExistingServer: true, + timeout: 30000, + }, +})