添加Playwright浏览器UI测试(16个测试,3个模块批量删除+启用停用)并补充mock API端点

main
haoliang 6 days ago
parent 3315e1dec9
commit 5f47164532

@ -0,0 +1,247 @@
/**
* CNC - PlaywrightUI
*
* /
* 使Mock/mockAPI
*
*
* -
* -
* -
* -
* -
* -
*/
import { test, expect, type Page } from '@playwright/test'
// 模拟登录TokenMock模式下路由守卫只检查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.5isEnabled=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()
})
})

@ -1,11 +1,20 @@
import type { MockMethod } from './types' 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: 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: 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 }, { 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[] = [ const mock: MockMethod[] = [
{ url: '/mock-api/admin/collect-address', method: 'get', response: () => ({ code: 0, data: { items: addresses } }) }, { url: '/mock-api/admin/collect-address', method: 'get', response: () => ({ code: 0, data: { items: addresses } }) },
// 参数化路由GET /mock-api/admin/collect-address/:id // 参数化路由GET /mock-api/admin/collect-address/:id
@ -53,6 +62,29 @@ const mock: MockMethod[] = [
] ]
return { code: 0, data: { items: machines } } 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 export default mock

@ -1,6 +1,6 @@
import type { MockMethod } from './types' 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: 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: 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: 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 }, { 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[] = [ const mock: MockMethod[] = [
{ {
url: '/mock-api/admin/machine', url: '/mock-api/admin/machine',
@ -178,6 +189,49 @@ const mock: MockMethod[] = [
method: 'get', method: 'get',
response: () => ({ code: 0, data: { items: [{ id: 1, name: '张三', code: 'W001' }, { id: 2, name: '李四', code: 'W002' }, { id: 3, name: '王五', code: 'W003' }] } }), 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 export default mock

@ -1,11 +1,20 @@
import type { MockMethod } from './types' import type { MockMethod } from './types'
const workers = [ let workers = [
{ id: 1, code: 'W001', name: '张三', isEnabled: 1, machineCount: 2, machineNames: '西-1.8,西-2.0' }, { 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: 2, code: 'W002', name: '李四', isEnabled: 1, machineCount: 1, machineNames: '西-1.10' },
{ id: 3, code: 'W003', name: '王五', isEnabled: 0, machineCount: 0, machineNames: '-' }, { 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[] = [ const mock: MockMethod[] = [
{ url: '/mock-api/admin/worker', method: 'get', response: ({ query }: any) => { { url: '/mock-api/admin/worker', method: 'get', response: ({ query }: any) => {
let items = [...workers] let items = [...workers]
@ -72,6 +81,29 @@ const mock: MockMethod[] = [
{ id: 5, name: '东-2.5', deviceCode: 'siemens_2.5', workshopName: 'B栋' }, { id: 5, name: '东-2.5', deviceCode: 'siemens_2.5', workshopName: 'B栋' },
{ id: 8, name: '北-4.1', deviceCode: 'fanake_4.1', workshopName: 'C栋' }, { 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 export default mock

@ -16,6 +16,7 @@
"vue-router": "^4.6.4" "vue-router": "^4.6.4"
}, },
"devDependencies": { "devDependencies": {
"@playwright/test": "^1.59.1",
"@types/mockjs": "^1.0.10", "@types/mockjs": "^1.0.10",
"@types/node": "^24.12.2", "@types/node": "^24.12.2",
"@vitejs/plugin-vue": "^6.0.6", "@vitejs/plugin-vue": "^6.0.6",
@ -513,6 +514,22 @@
"url": "https://opencollective.com/parcel" "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": { "node_modules/@popperjs/core": {
"name": "@sxzz/popperjs-es", "name": "@sxzz/popperjs-es",
"version": "2.11.8", "version": "2.11.8",
@ -1925,6 +1942,53 @@
"url": "https://github.com/sponsors/jonschlinkert" "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": { "node_modules/postcss": {
"version": "8.5.10", "version": "8.5.10",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz",

@ -17,6 +17,7 @@
"vue-router": "^4.6.4" "vue-router": "^4.6.4"
}, },
"devDependencies": { "devDependencies": {
"@playwright/test": "^1.59.1",
"@types/mockjs": "^1.0.10", "@types/mockjs": "^1.0.10",
"@types/node": "^24.12.2", "@types/node": "^24.12.2",
"@vitejs/plugin-vue": "^6.0.6", "@vitejs/plugin-vue": "^6.0.6",

@ -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,
},
})
Loading…
Cancel
Save