feat(erp-frontend-vue): add Playwright E2E tests and update layout
Add Playwright configuration and E2E specs for key production, purchasing, and warehouse flows, and update layout/login to align with the new testing setup. Made-with: Cursor
This commit is contained in:
@@ -1,5 +1,297 @@
|
|||||||
# Vue 3 + TypeScript + Vite
|
# 铭奕 ERP 前端(erp-frontend-vue)
|
||||||
|
|
||||||
This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
|
基于 **Vue 3 + TypeScript + Vite + Element Plus** 实现的 ERP Web 前端,覆盖主数据、销售、采购、生产计划、仓储、系统管理等业务模块。
|
||||||
|
|
||||||
Learn more about the recommended Project Setup and IDE Support in the [Vue Docs TypeScript Guide](https://vuejs.org/guide/typescript/overview.html#project-setup).
|
本文档用于说明项目整体架构和约定,方便后续功能迭代、Bug 修复以及测试用例编写。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 技术栈与关键依赖
|
||||||
|
|
||||||
|
- **框架**:Vue 3(`<script setup>` 语法)
|
||||||
|
- **语言**:TypeScript(严格类型约束,配合 `vue-tsc` 做构建时类型检查)
|
||||||
|
- **构建工具**:Vite
|
||||||
|
- **UI 组件库**:Element Plus(含图标集 `@element-plus/icons-vue`)
|
||||||
|
- **路由**:Vue Router 4
|
||||||
|
- **HTTP 客户端**:Axios(统一封装在 `src/api/request.ts`)
|
||||||
|
- **测试**:Vitest + @vue/test-utils + jsdom
|
||||||
|
|
||||||
|
主要脚本(见 `package.json`):
|
||||||
|
|
||||||
|
- `npm run dev`:启动开发服务器
|
||||||
|
- `npm run build`:先运行 `vue-tsc -b` 做类型检查,再用 Vite 打生产包
|
||||||
|
- `npm run preview`:预览生产构建
|
||||||
|
- `npm run test`:一次性运行所有 Vitest 用例
|
||||||
|
- `npm run test:watch`:watch 模式运行 Vitest
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 目录结构概览
|
||||||
|
|
||||||
|
仅列出与前端业务开发关系最密切的部分:
|
||||||
|
|
||||||
|
- `src/main.ts`:应用入口,创建 Vue 应用,注册 Element Plus 图标,挂载路由与权限守卫
|
||||||
|
- `src/App.vue`:根组件
|
||||||
|
- `src/router/index.ts`:路由配置(按业务域划分路由树)
|
||||||
|
- `src/permission.ts`:路由前置守卫,处理登录校验、用户信息加载、白名单路由等
|
||||||
|
- `src/layout/`
|
||||||
|
- `index.vue`:主布局(侧边菜单 + 顶部导航 + 面包屑 + 内容区域)
|
||||||
|
- `src/stores/`
|
||||||
|
- `user.ts`:用户状态与权限校验(登录、退出、获取用户信息等)
|
||||||
|
- `src/api/`
|
||||||
|
- `request.ts`:Axios 实例及请求/响应拦截器
|
||||||
|
- 业务 API 模块(每个文件对应一个业务域,内部定义 TypeScript 接口与请求方法),例如:
|
||||||
|
- `auth.ts`:认证登录/获取用户信息
|
||||||
|
- `productionPlan.ts`:生产计划单
|
||||||
|
- `mbom.ts`:物料清单 / MRP
|
||||||
|
- `purchasePlan.ts`:采购计划
|
||||||
|
- `purchaseOrder.ts`:采购订单
|
||||||
|
- `salesOrder.ts`:销售订单
|
||||||
|
- `deliver.ts` / `invoice.ts` / `saleback.ts`:销售相关单据
|
||||||
|
- `masterdata/*`:物料、计量单位、车间、工作站等主数据
|
||||||
|
- `system/*`:组织架构(部门、岗位、角色、用户)
|
||||||
|
- `warehouse/*`:生产领料相关接口
|
||||||
|
- `rd/ebom.ts`:研发 BOM
|
||||||
|
- `src/views/`:按业务域划分的页面组件
|
||||||
|
- `MasterData/`:主数据
|
||||||
|
- `Sales/`:销售管理与销售报表
|
||||||
|
- `Purchasing/`:采购管理与采购报表
|
||||||
|
- `Production/`:生产计划、MBOM/MRP、采购计划、生产报表
|
||||||
|
- `Warehouse/Issue/`:生产领料单
|
||||||
|
- `RD/Ebom/`:研发 EBOM
|
||||||
|
- `System/`:用户、角色、部门、岗位
|
||||||
|
- 每个业务通常使用:
|
||||||
|
- `index.vue`:列表/查询页
|
||||||
|
- `form.vue`:单据编辑/查看页
|
||||||
|
- `src/utils/`:通用工具(如 token 存取等)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 运行与构建
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 安装依赖
|
||||||
|
npm install
|
||||||
|
|
||||||
|
# 开发调试
|
||||||
|
npm run dev
|
||||||
|
|
||||||
|
# 类型检查 + 构建生产包
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
# 本地预览生产包
|
||||||
|
npm run preview
|
||||||
|
|
||||||
|
# 运行单元测试
|
||||||
|
npm run test
|
||||||
|
```
|
||||||
|
|
||||||
|
> 建议在提交代码前至少保证 `npm run build` 通过,以防止 TypeScript 类型错误和明显的构建问题进入主分支。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 前端架构设计
|
||||||
|
|
||||||
|
### 4.1 路由与布局
|
||||||
|
|
||||||
|
- 路由配置集中在 `src/router/index.ts`,采用**按业务域分组**的嵌套路由结构:
|
||||||
|
- `/sales/*`:销售管理模块
|
||||||
|
- `/purchasing/*`:采购管理模块
|
||||||
|
- `/production/*`:生产计划模块
|
||||||
|
- `/masterdata/*`:主数据维护
|
||||||
|
- `/system/*`:系统管理
|
||||||
|
- `/warehouse/*`:仓储/生产领料
|
||||||
|
- `/rd/*`:研发
|
||||||
|
- 所有业务路由均以 `Layout` 作为父路由,实现统一的侧边菜单 + 顶部导航 + 面包屑结构。
|
||||||
|
- 每个路由都带有 `meta.title`,用于:
|
||||||
|
- 页面标题(`permission.ts` 中设置 `document.title`)
|
||||||
|
- 面包屑显示
|
||||||
|
- 侧边菜单高亮匹配
|
||||||
|
|
||||||
|
### 4.2 权限与认证流程
|
||||||
|
|
||||||
|
相关文件:
|
||||||
|
|
||||||
|
- `src/stores/user.ts`:维护用户 token、基本信息、角色、权限等
|
||||||
|
- `src/utils/auth.ts`:token 的本地存储读写
|
||||||
|
- `src/api/auth.ts`:登录、获取用户信息、注销
|
||||||
|
- `src/permission.ts`:全局路由守卫
|
||||||
|
|
||||||
|
核心逻辑:
|
||||||
|
|
||||||
|
1. 路由进入前,`permission.ts` 会检查:
|
||||||
|
- 当前是否在白名单(如 `/login` 等)
|
||||||
|
- 本地是否存在 token
|
||||||
|
2. 若有 token 但 `userStore.state.userInfo.roles` 为空,则会调用 `userStore.getUserInfo()` 拉取用户信息。
|
||||||
|
3. 若认证失败或接口错误,会调用 `fedLogout` 清理本地状态,并跳转登录页。
|
||||||
|
4. 在开发模式下支持 `DEV_SKIP_AUTH`(通过 `import.meta.env` 控制),方便后端未就绪时调试页面。
|
||||||
|
|
||||||
|
### 4.3 API 封装与数据访问
|
||||||
|
|
||||||
|
统一入口:`src/api/request.ts`
|
||||||
|
|
||||||
|
- 使用 Axios 创建单例 `request`,通过请求拦截器自动处理:
|
||||||
|
- 按 URL 前缀自动拼接 `/erp`、`/mes`、`/system` 等前缀
|
||||||
|
- 附加 `Authorization: Bearer <token>` 头
|
||||||
|
- 响应拦截器统一处理:
|
||||||
|
- `code === 401`:弹出重新登录提示,清理 token 并跳转登录
|
||||||
|
- 其他非 200 code:使用 `ElMessage.error` 提示
|
||||||
|
- 成功时统一返回包裹后的 `res` 对象(通常包含 `data`、`rows`、`total` 等)
|
||||||
|
|
||||||
|
业务 API 模块模式(以 `src/api/productionPlan.ts` 为例):
|
||||||
|
|
||||||
|
- 在文件顶部定义接口类型,如 `ProductionPlan`、`PlanLine` 等。
|
||||||
|
- 导出对应的请求方法:
|
||||||
|
- `getProductionPlanList(params: PlanQuery)`
|
||||||
|
- `createProductionPlan(data: Partial<ProductionPlan>)`
|
||||||
|
- `approveProductionPlan(planId: number)` 等。
|
||||||
|
- 列表接口通常约定返回 `{ rows: T[]; total: number }` 结构,方便列表分页展示。
|
||||||
|
|
||||||
|
> **建议**:新增接口时先定义好 TypeScript 接口类型,再在页面组件中通过 `type Xxx` 引用,保持前后端字段的一致性与可维护性。
|
||||||
|
|
||||||
|
### 4.4 状态管理
|
||||||
|
|
||||||
|
当前项目使用 **轻量级的自实现 store**(并未引入 Pinia/Vuex),例如:
|
||||||
|
|
||||||
|
- `src/stores/user.ts` 使用 `reactive` 维护用户状态,并暴露:
|
||||||
|
- `loginAction` / `logoutAction` / `getUserInfo`
|
||||||
|
- `hasRole`、`hasPermission` 做按钮级权限控制
|
||||||
|
|
||||||
|
在组件中通过:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { useUserStore } from '@/stores/user'
|
||||||
|
|
||||||
|
const userStore = useUserStore()
|
||||||
|
```
|
||||||
|
|
||||||
|
来访问和修改全局用户状态。
|
||||||
|
|
||||||
|
### 4.5 业务模块划分
|
||||||
|
|
||||||
|
按典型业务流程进行模块化:
|
||||||
|
|
||||||
|
- **主数据(MasterData)**:物料、分类、计量单位、车间/工作站等基础资料。
|
||||||
|
- **销售(Sales)**:客户、销售订单、发货通知单、开票结算单、退货通知单以及相关报表。
|
||||||
|
- **采购(Purchasing)**:供应商、采购订单、到货单、发票、退货及采购执行报表。
|
||||||
|
- **生产(Production)**:生产计划单、MBOM/MRP 运算、采购计划及生产执行报表。
|
||||||
|
- **仓储/生产领料(Warehouse/Issue)**:工单领料单及出库明细。
|
||||||
|
- **研发(RD/Ebom)**:工程 BOM(EBOM)维护。
|
||||||
|
- **系统管理(System)**:用户、角色、部门、岗位等组织架构。
|
||||||
|
|
||||||
|
各模块页面组件基本遵循:
|
||||||
|
|
||||||
|
- `index.vue`:列表、搜索、分页、批量操作
|
||||||
|
- `form.vue`:单据新增/编辑/查看(通常由路由 `new` / `edit/:id` / `view/:id` 复用)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 开发约定与最佳实践
|
||||||
|
|
||||||
|
1. **类型优先**
|
||||||
|
- 所有接口数据结构优先在 `src/api/*` 中通过 `interface`/`type` 定义。
|
||||||
|
- 组件中通过 `import type Xxx from '@/api/xxx'` 复用类型,避免魔法字符串和隐式 `any`。
|
||||||
|
|
||||||
|
2. **单据模式一致**
|
||||||
|
- 列表页负责查询、分页、批量操作。
|
||||||
|
- 单据表单页负责单据的增删改查、审批、导出、引入等。
|
||||||
|
- 路由命名尽量统一(如 `XxxList`、`XxxNew`、`XxxEdit`、`XxxView`)。
|
||||||
|
|
||||||
|
3. **API 调用约定**
|
||||||
|
- 列表类:`getXxxList(params)` / `getXxxDocList(params)` / `getXxxSummary(params)`。
|
||||||
|
- 单据类:`getXxx(id)` / `createXxx(data)` / `updateXxx(data)` / `deleteXxx(id)` / `approveXxx(id)`。
|
||||||
|
- 导出类:`exportXxx(params)` 返回 `Blob`,前端统一通过 `URL.createObjectURL` 下载。
|
||||||
|
|
||||||
|
4. **错误处理**
|
||||||
|
- 全局拦截器负责大部分 HTTP 错误提示。
|
||||||
|
- 业务逻辑错误(如必填字段缺失、状态不允许操作)应在组件内通过 `ElMessage` 或 `ElMessageBox` 友好提示。
|
||||||
|
|
||||||
|
5. **国际化与文案**
|
||||||
|
- 当前以中文为主,业务文案尽量统一、简洁。
|
||||||
|
- 后续如需多语言,可在布局和组件中收口文案位置,方便替换。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 新功能迭代指南
|
||||||
|
|
||||||
|
以新增一个简单业务模块为例,推荐步骤如下:
|
||||||
|
|
||||||
|
1. **定义后端接口与数据结构**
|
||||||
|
- 与后端约定好 URL、请求方式以及返回结构。
|
||||||
|
- 在 `src/api/<module>.ts` 中新增对应的接口类型定义与请求函数。
|
||||||
|
|
||||||
|
2. **新增页面组件**
|
||||||
|
- 在 `src/views/<Module>/<Feature>/` 下创建:
|
||||||
|
- `index.vue`:列表页
|
||||||
|
- `form.vue`:单据表单页(可根据是否需要拆分为 new/edit/view 来复用)
|
||||||
|
- 使用 `<script setup lang="ts">`,并引入上一步定义的类型。
|
||||||
|
|
||||||
|
3. **注册路由**
|
||||||
|
- 在 `src/router/index.ts` 中对应业务分组下新增子路由:
|
||||||
|
- 列表:`/module/feature`
|
||||||
|
- 新增:`/module/feature/new`
|
||||||
|
- 编辑:`/module/feature/edit/:id`
|
||||||
|
- 查看:`/module/feature/view/:id`
|
||||||
|
- 配置好 `name` 和 `meta.title`,以便面包屑与标题显示。
|
||||||
|
|
||||||
|
4. **加入侧边菜单**
|
||||||
|
- 在 `src/layout/index.vue` 左侧菜单中增加对应菜单项,index 与路由 path 对齐。
|
||||||
|
|
||||||
|
5. **权限控制(如需要)**
|
||||||
|
- 后端返回对应的角色/权限字符串。
|
||||||
|
- 在前端根据 `userStore.hasRole` / `hasPermission` 决定是否展示按钮或入口。
|
||||||
|
|
||||||
|
6. **编写/更新测试**
|
||||||
|
- 对复杂的计算逻辑或数据加工函数,建议抽到独立模块并使用 Vitest 编写单元测试。
|
||||||
|
- 对关键表单组件或流程,使用 `@vue/test-utils` + Vitest 编写基础渲染和交互用例。
|
||||||
|
|
||||||
|
7. **自测与提交**
|
||||||
|
- 运行 `npm run dev` 手动验证主要流程。
|
||||||
|
- 运行 `npm run test` 和 `npm run build` 确认无测试失败与类型错误。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Bug 修复与调试建议
|
||||||
|
|
||||||
|
1. **快速定位问题**
|
||||||
|
- TS 编译错误:优先通过 `npm run build` 或 `vue-tsc -b` 定位。
|
||||||
|
- 运行时错误:查看浏览器控制台和网络请求(Network)日志。
|
||||||
|
|
||||||
|
2. **保持类型一致**
|
||||||
|
- 修复字段名或结构变更时,同步更新:
|
||||||
|
- 对应的 API 接口类型定义(`src/api/*`)
|
||||||
|
- 所有使用该类型的组件
|
||||||
|
|
||||||
|
3. **避免静默失败**
|
||||||
|
- catch 到错误时,优先使用 `ElMessage.error` 告知用户(同时 `console.error` 方便开发排查)。
|
||||||
|
|
||||||
|
4. **回归检查**
|
||||||
|
- 对涉及核心单据(生产计划、采购计划、销售订单、领料单等)的改动,建议:
|
||||||
|
- 至少完成一次完整的“从主数据 → 业务单据 → 报表/导出”的端到端手工验证。
|
||||||
|
- 补充或更新相应的单元测试。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 测试用例编写建议
|
||||||
|
|
||||||
|
项目已集成 Vitest 与 Vue Test Utils,可按以下思路补充测试:
|
||||||
|
|
||||||
|
1. **纯逻辑/工具函数**
|
||||||
|
- 放在独立的 `utils` 或 `helpers` 文件中。
|
||||||
|
- 使用 Vitest 编写函数级单元测试,覆盖数字计算、状态映射等逻辑。
|
||||||
|
|
||||||
|
2. **API 封装**
|
||||||
|
- 使用 Vitest 的 mocking 能力(如 `vi.mock('axios')`)对 API 模块进行单元测试,确保在不同返回结构下都有正确行为。
|
||||||
|
|
||||||
|
3. **组件/页面**
|
||||||
|
- 使用 `@vue/test-utils` 挂载组件,测试:
|
||||||
|
- 基本渲染是否正常(必需字段/按钮是否存在)
|
||||||
|
- 关键交互(点击按钮后是否发出预期事件或调用 API)
|
||||||
|
- 表单校验逻辑(缺失必填项时是否提示)
|
||||||
|
|
||||||
|
4. **回归测试**
|
||||||
|
- 对已修复的 Bug,尽量补一条对应的测试用例,避免同类问题再次出现。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
如需在后续迭代中扩展架构(例如引入 Pinia、国际化、多布局支持等),可以在本 README 继续追加对应的小节,保持文档与实现同步更新。
|
||||||
|
|||||||
3350
erp-frontend-vue/package-lock.json
generated
3350
erp-frontend-vue/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -8,7 +8,8 @@
|
|||||||
"build": "vue-tsc -b && vite build",
|
"build": "vue-tsc -b && vite build",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"test": "vitest run",
|
"test": "vitest run",
|
||||||
"test:watch": "vitest"
|
"test:watch": "vitest",
|
||||||
|
"test:e2e": "playwright test"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@element-plus/icons-vue": "^2.3.2",
|
"@element-plus/icons-vue": "^2.3.2",
|
||||||
@@ -20,6 +21,7 @@
|
|||||||
"vue-router": "^4.6.4"
|
"vue-router": "^4.6.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@playwright/test": "^1.58.2",
|
||||||
"@types/node": "^24.10.1",
|
"@types/node": "^24.10.1",
|
||||||
"@vitejs/plugin-vue": "^6.0.1",
|
"@vitejs/plugin-vue": "^6.0.1",
|
||||||
"@vue/test-utils": "^2.4.6",
|
"@vue/test-utils": "^2.4.6",
|
||||||
|
|||||||
23
erp-frontend-vue/playwright.config.ts
Normal file
23
erp-frontend-vue/playwright.config.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { defineConfig, devices } from '@playwright/test'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
testDir: './tests',
|
||||||
|
timeout: 60 * 1000,
|
||||||
|
expect: {
|
||||||
|
timeout: 5 * 1000
|
||||||
|
},
|
||||||
|
use: {
|
||||||
|
baseURL: process.env.ERP_BASE_URL || 'http://localhost:5173',
|
||||||
|
headless: true,
|
||||||
|
viewport: { width: 1440, height: 900 },
|
||||||
|
ignoreHTTPSErrors: true,
|
||||||
|
trace: 'on-first-retry'
|
||||||
|
},
|
||||||
|
projects: [
|
||||||
|
{
|
||||||
|
name: 'chromium',
|
||||||
|
use: { ...devices['Desktop Chrome'] }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
BIN
erp-frontend-vue/src/assets/logo.png
Normal file
BIN
erp-frontend-vue/src/assets/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
@@ -2,8 +2,8 @@
|
|||||||
<el-container class="layout-container">
|
<el-container class="layout-container">
|
||||||
<el-aside :width="isCollapse ? '64px' : '220px'" class="layout-aside">
|
<el-aside :width="isCollapse ? '64px' : '220px'" class="layout-aside">
|
||||||
<div class="logo">
|
<div class="logo">
|
||||||
<img src="/vite.svg" alt="Logo" />
|
<img src="/src/assets/logo.png" alt="Logo" />
|
||||||
<span v-show="!isCollapse">ERP系统</span>
|
<!-- <span v-show="!isCollapse">铭奕ERP</span> -->
|
||||||
</div>
|
</div>
|
||||||
<el-menu
|
<el-menu
|
||||||
:default-active="activeMenu"
|
:default-active="activeMenu"
|
||||||
@@ -227,8 +227,8 @@ const handleCommand = async (command: string) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.logo img {
|
.logo img {
|
||||||
width: 32px;
|
/* width: 32px; */
|
||||||
height: 32px;
|
height: 48px;
|
||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
:rules="loginRules"
|
:rules="loginRules"
|
||||||
class="login-form"
|
class="login-form"
|
||||||
>
|
>
|
||||||
<h3 class="title">ERP 企业资源管理系统</h3>
|
<h3 class="title">铭奕ERP系统</h3>
|
||||||
|
|
||||||
<el-form-item prop="username">
|
<el-form-item prop="username">
|
||||||
<el-input
|
<el-input
|
||||||
|
|||||||
24
erp-frontend-vue/tests/production-need-report.spec.ts
Normal file
24
erp-frontend-vue/tests/production-need-report.spec.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { test, expect } from '@playwright/test'
|
||||||
|
import { login, clickSubMenu, clickMenuItem, expectBasicList } from './utils/erpTestUtils'
|
||||||
|
|
||||||
|
test.describe('采购计划需求表页面', () => {
|
||||||
|
test('报表查询基础交互', async ({ page }) => {
|
||||||
|
await login(page)
|
||||||
|
|
||||||
|
await clickSubMenu(page, '生产计划')
|
||||||
|
await clickMenuItem(page, '/production/report/need')
|
||||||
|
await expect(page).toHaveURL(/\/production\/report\/need/)
|
||||||
|
await expectBasicList(page)
|
||||||
|
|
||||||
|
const dateRange = page.getByPlaceholder(/开始日期|结束日期|选择日期范围/).first().catch(() => null)
|
||||||
|
if (dateRange) {
|
||||||
|
await dateRange.click()
|
||||||
|
await page.getByRole('button', { name: /今/ }).first().click().catch(() => {})
|
||||||
|
await page.getByRole('button', { name: /确 定|确定/ }).first().click().catch(() => {})
|
||||||
|
}
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: /搜索|查询/ }).click()
|
||||||
|
await expect(page.locator('.el-table').first()).toBeVisible()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
87
erp-frontend-vue/tests/production-plan-order.spec.ts
Normal file
87
erp-frontend-vue/tests/production-plan-order.spec.ts
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
import { test, expect } from '@playwright/test'
|
||||||
|
import { login, clickSubMenu, clickMenuItem, expectBasicList } from './utils/erpTestUtils'
|
||||||
|
|
||||||
|
test.describe('生产计划单页面', () => {
|
||||||
|
test('明细视图与单据视图切换 + 查询', async ({ page }) => {
|
||||||
|
await login(page)
|
||||||
|
|
||||||
|
await clickSubMenu(page, '生产计划')
|
||||||
|
await clickMenuItem(page, '/production/plan-order')
|
||||||
|
await expect(page).toHaveURL(/\/production\/plan-order/)
|
||||||
|
await expectBasicList(page)
|
||||||
|
|
||||||
|
// 明细视图:包含销售订单号/物料编码等字段
|
||||||
|
await expect(page.getByText('销售订单号')).toBeVisible()
|
||||||
|
await expect(page.getByText('物料编码')).toBeVisible()
|
||||||
|
|
||||||
|
// 切换到单据视图
|
||||||
|
await page.getByRole('button', { name: '单据' }).click()
|
||||||
|
await expect(page.getByText('业务状态')).toBeVisible()
|
||||||
|
|
||||||
|
// 在单据视图中按单据编码+状态查询
|
||||||
|
const codeInput = page.getByLabel('单据编码').locator('input')
|
||||||
|
await codeInput.fill('TEST')
|
||||||
|
await page.getByRole('button', { name: /搜索/ }).click()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('新增生产计划单基础表单交互', async ({ page }) => {
|
||||||
|
await login(page)
|
||||||
|
|
||||||
|
await clickSubMenu(page, '生产计划')
|
||||||
|
await clickMenuItem(page, '/production/plan-order')
|
||||||
|
|
||||||
|
// 打开新增页
|
||||||
|
await page.getByRole('button', { name: /新增/ }).click()
|
||||||
|
await expect(page).toHaveURL(/\/production\/plan-order\/(new|edit)/)
|
||||||
|
await expect(page.locator('.el-form').first()).toBeVisible()
|
||||||
|
|
||||||
|
// 表头基本字段存在(生产计划单文档:计划单号/计划日期/业务类型/工作类型等)
|
||||||
|
await expect(page.getByText(/单据编码|计划单号/)).toBeVisible()
|
||||||
|
await expect(page.getByText(/单据日期|计划日期/)).toBeVisible()
|
||||||
|
|
||||||
|
// 引入订单弹窗:根据 PRD,通过「引入」按钮选择销售订单/备货订单
|
||||||
|
const importBtn = page.getByRole('button', { name: /引入/ }).first()
|
||||||
|
if (await importBtn.isVisible().catch(() => false)) {
|
||||||
|
await importBtn.click()
|
||||||
|
await expect(page.getByText('订单信息')).toBeVisible()
|
||||||
|
await expect(page.locator('.el-dialog').locator('.el-table').first()).toBeVisible()
|
||||||
|
await page.getByRole('button', { name: /关 闭|关闭|取 消/ }).click()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 订单 BOM 选择弹窗
|
||||||
|
const bomSelectBtn = page.getByRole('button', { name: /选择BOM|选择/ }).first().catch(() => null)
|
||||||
|
if (bomSelectBtn) {
|
||||||
|
await bomSelectBtn
|
||||||
|
await expect(page.getByText(/选择EBOM|选择BOM/)).toBeVisible()
|
||||||
|
await page.getByRole('button', { name: /关 闭|关闭|取 消/ }).click()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
test('物料清单(BOM运算结果)区域交互', async ({ page }) => {
|
||||||
|
await login(page)
|
||||||
|
|
||||||
|
await clickSubMenu(page, '生产计划')
|
||||||
|
await clickMenuItem(page, '/production/plan-order')
|
||||||
|
|
||||||
|
// 打开一个已存在的计划单(若有)
|
||||||
|
const firstLink = page.locator('.el-table').first().getByRole('link').first()
|
||||||
|
if (!(await firstLink.isVisible().catch(() => false))) test.skip()
|
||||||
|
|
||||||
|
await firstLink.click()
|
||||||
|
await expect(page.locator('.section-title', { hasText: '物料清单' })).toBeVisible()
|
||||||
|
|
||||||
|
// BOM 运算按钮存在且可点(如果该单据允许)
|
||||||
|
const bomBtn = page.getByRole('button', { name: 'BOM运算' }).first()
|
||||||
|
if (await bomBtn.isVisible().catch(() => false)) {
|
||||||
|
await bomBtn.click()
|
||||||
|
// 可能出现 loading 或消息提示,这里只校验不会报错
|
||||||
|
}
|
||||||
|
|
||||||
|
// 物料清单表格中的下发车间、供应方式、齐套检查/补料列存在
|
||||||
|
const mbomTable = page.locator('.material-section').nth(0).locator('.el-table').first()
|
||||||
|
await expect(mbomTable).toBeVisible()
|
||||||
|
await expect(page.getByText(/供应方式/)).toBeVisible()
|
||||||
|
await expect(page.getByText(/下发车间/)).toBeVisible()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
32
erp-frontend-vue/tests/production-purchase-plan.spec.ts
Normal file
32
erp-frontend-vue/tests/production-purchase-plan.spec.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { test, expect } from '@playwright/test'
|
||||||
|
import { login, clickSubMenu, clickMenuItem, expectBasicList } from './utils/erpTestUtils'
|
||||||
|
|
||||||
|
test.describe('采购计划单页面', () => {
|
||||||
|
test('列表搜索 & 新增采购计划单 & 导出', async ({ page }) => {
|
||||||
|
await login(page)
|
||||||
|
|
||||||
|
await clickSubMenu(page, '生产计划')
|
||||||
|
await clickMenuItem(page, '/production/purchase-plan')
|
||||||
|
await expect(page).toHaveURL(/\/production\/purchase-plan/)
|
||||||
|
await expectBasicList(page)
|
||||||
|
|
||||||
|
const planCodeInput = page.getByPlaceholder(/计划单号|采购计划/).first().catch(() => null)
|
||||||
|
if (planCodeInput) {
|
||||||
|
await planCodeInput.fill('TEST')
|
||||||
|
await page.getByRole('button', { name: /搜索|查询/ }).click()
|
||||||
|
}
|
||||||
|
|
||||||
|
const addBtn = page.getByRole('button', { name: /新增|新建/ }).first()
|
||||||
|
await addBtn.click()
|
||||||
|
await expect(page).toHaveURL(/\/production\/purchase-plan\/(new|edit|form)/)
|
||||||
|
await expect(page.locator('.el-form').first()).toBeVisible()
|
||||||
|
await page.goBack().catch(() => {})
|
||||||
|
|
||||||
|
const exportBtn = page.getByRole('button', { name: /导出/ }).first()
|
||||||
|
if (await exportBtn.isVisible().catch(() => false)) {
|
||||||
|
await exportBtn.click()
|
||||||
|
// 此处主要校验点击不报错
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
52
erp-frontend-vue/tests/production-work-order.spec.ts
Normal file
52
erp-frontend-vue/tests/production-work-order.spec.ts
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import { test, expect } from '@playwright/test'
|
||||||
|
import { login, clickSubMenu, clickMenuItem, expectBasicList } from './utils/erpTestUtils'
|
||||||
|
|
||||||
|
test.describe('生产订单页面', () => {
|
||||||
|
test('列表搜索与基本操作按钮存在', async ({ page }) => {
|
||||||
|
await login(page)
|
||||||
|
|
||||||
|
await clickSubMenu(page, '生产管理')
|
||||||
|
await clickMenuItem(page, '/production/work-order')
|
||||||
|
await expect(page).toHaveURL(/\/production\/work-order/)
|
||||||
|
await expectBasicList(page)
|
||||||
|
|
||||||
|
// 搜索区字段
|
||||||
|
await expect(page.getByLabel('工单编码')).toBeVisible()
|
||||||
|
await expect(page.getByLabel('计划单号')).toBeVisible()
|
||||||
|
await page.getByRole('button', { name: /搜索/ }).click()
|
||||||
|
await page.getByRole('button', { name: '重置' }).click()
|
||||||
|
|
||||||
|
// 工具栏按钮:新增/修改/删除/导出
|
||||||
|
await expect(page.getByRole('button', { name: /新增/ })).toBeVisible()
|
||||||
|
await expect(page.getByRole('button', { name: /修改/ })).toBeVisible()
|
||||||
|
await expect(page.getByRole('button', { name: /删除/ })).toBeVisible()
|
||||||
|
await expect(page.getByRole('button', { name: /导出/ })).toBeVisible()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('查看工单详情与状态相关操作按钮', async ({ page }) => {
|
||||||
|
await login(page)
|
||||||
|
|
||||||
|
await clickSubMenu(page, '生产管理')
|
||||||
|
await clickMenuItem(page, '/production/work-order')
|
||||||
|
|
||||||
|
const firstRow = page.locator('.el-table__row').first()
|
||||||
|
if (!(await firstRow.isVisible().catch(() => false))) test.skip()
|
||||||
|
|
||||||
|
// 查看按钮
|
||||||
|
const viewBtn = firstRow.getByRole('button', { name: '查看' }).first()
|
||||||
|
await viewBtn.click()
|
||||||
|
await expect(page.locator('.el-dialog, .el-drawer, .page-container').first()).toBeVisible()
|
||||||
|
await page.goBack().catch(() => {})
|
||||||
|
|
||||||
|
// 根据不同状态可能出现:审核、一键领料、完工、取消等按钮
|
||||||
|
// 这里只做存在性和可点击性检查(不强制业务成功)
|
||||||
|
const quickIssueBtn = firstRow.getByRole('button', { name: /一键领料/ }).first()
|
||||||
|
if (await quickIssueBtn.isVisible().catch(() => false)) {
|
||||||
|
await quickIssueBtn.click()
|
||||||
|
// 可能弹确认框,选择取消
|
||||||
|
const confirm = page.getByRole('button', { name: /取 消|取消/ }).first()
|
||||||
|
await confirm.click().catch(() => {})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
30
erp-frontend-vue/tests/purchasing-checkin.spec.ts
Normal file
30
erp-frontend-vue/tests/purchasing-checkin.spec.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { test, expect } from '@playwright/test'
|
||||||
|
import { login, clickSubMenu, clickMenuItem, expectBasicList } from './utils/erpTestUtils'
|
||||||
|
|
||||||
|
test.describe('采购到货单页面', () => {
|
||||||
|
test('列表页加载 & 查询 & 查看详情', async ({ page }) => {
|
||||||
|
await login(page)
|
||||||
|
|
||||||
|
await clickSubMenu(page, '采购管理')
|
||||||
|
await clickMenuItem(page, '/purchasing/checkin')
|
||||||
|
await expect(page).toHaveURL(/\/purchasing\/checkin/)
|
||||||
|
await expectBasicList(page)
|
||||||
|
|
||||||
|
const supplierInput = page.getByPlaceholder(/供应商/).first().catch(() => null)
|
||||||
|
if (supplierInput) {
|
||||||
|
await supplierInput.fill('测试供应商')
|
||||||
|
await page.getByRole('button', { name: /搜索|查询/ }).click()
|
||||||
|
}
|
||||||
|
|
||||||
|
const firstRow = page.locator('.el-table__row').first()
|
||||||
|
if (await firstRow.isVisible().catch(() => false)) {
|
||||||
|
const viewBtn = firstRow.getByRole('button', { name: /查看/ }).first()
|
||||||
|
if (await viewBtn.isVisible().catch(() => false)) {
|
||||||
|
await viewBtn.click()
|
||||||
|
await expect(page.locator('.el-dialog__body')).toBeVisible()
|
||||||
|
await page.getByRole('button', { name: /关 闭|关闭|取 消/ }).click().catch(() => {})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
28
erp-frontend-vue/tests/purchasing-order.spec.ts
Normal file
28
erp-frontend-vue/tests/purchasing-order.spec.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import { test, expect } from '@playwright/test'
|
||||||
|
import { login, clickSubMenu, clickMenuItem, expectBasicList } from './utils/erpTestUtils'
|
||||||
|
|
||||||
|
test.describe('采购订单页面', () => {
|
||||||
|
test('列表搜索与新增采购订单表单', async ({ page }) => {
|
||||||
|
await login(page)
|
||||||
|
|
||||||
|
await clickSubMenu(page, '采购管理')
|
||||||
|
await clickMenuItem(page, '/purchasing/order')
|
||||||
|
await expect(page).toHaveURL(/\/purchasing\/order/)
|
||||||
|
await expectBasicList(page)
|
||||||
|
|
||||||
|
// 按订单编号/供应商搜索
|
||||||
|
const codeInput = page.getByPlaceholder(/订单编号|采购订单/).first().catch(() => null)
|
||||||
|
if (codeInput) {
|
||||||
|
await codeInput.fill('TEST')
|
||||||
|
}
|
||||||
|
await page.getByRole('button', { name: /搜索|查询/ }).click()
|
||||||
|
|
||||||
|
// 新增采购订单
|
||||||
|
const addBtn = page.getByRole('button', { name: /新增|新建/ }).first()
|
||||||
|
await addBtn.click()
|
||||||
|
await expect(page).toHaveURL(/\/purchasing\/order\/(new|edit|form)/)
|
||||||
|
await expect(page.locator('.el-form').first()).toBeVisible()
|
||||||
|
await expect(page.locator('.el-table').first()).toBeVisible() // 明细
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
49
erp-frontend-vue/tests/rd-ebom.spec.ts
Normal file
49
erp-frontend-vue/tests/rd-ebom.spec.ts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import { test, expect } from '@playwright/test'
|
||||||
|
import { login, clickSubMenu, clickMenuItem, expectBasicList } from './utils/erpTestUtils'
|
||||||
|
|
||||||
|
test.describe('产品 BOM(EBOM)页面', () => {
|
||||||
|
test('明细/单据视图切换与搜索', async ({ page }) => {
|
||||||
|
await login(page)
|
||||||
|
|
||||||
|
await clickSubMenu(page, '研发管理')
|
||||||
|
await clickMenuItem(page, '/rd/ebom')
|
||||||
|
await expect(page).toHaveURL(/\/rd\/ebom/)
|
||||||
|
await expectBasicList(page)
|
||||||
|
|
||||||
|
// 明细视图:物料分类筛选存在
|
||||||
|
await expect(page.getByText('物料分类')).toBeVisible()
|
||||||
|
|
||||||
|
// 切换到单据视图
|
||||||
|
await page.getByRole('button', { name: '单据' }).click()
|
||||||
|
await expect(page.getByText('业务状态')).toBeVisible()
|
||||||
|
|
||||||
|
// 单据视图按单据状态/业务状态查询
|
||||||
|
const statusSelect = page.getByLabel('单据状态')
|
||||||
|
await statusSelect.click()
|
||||||
|
await page.getByRole('option').first().click()
|
||||||
|
await page.getByRole('button', { name: /搜索/ }).click()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('新增 BOM 表单基本交互', async ({ page }) => {
|
||||||
|
await login(page)
|
||||||
|
|
||||||
|
await clickSubMenu(page, '研发管理')
|
||||||
|
await clickMenuItem(page, '/rd/ebom')
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: /新增/ }).click()
|
||||||
|
await expect(page).toHaveURL(/\/ebom\/(new|edit)/)
|
||||||
|
await expect(page.locator('.el-form').first()).toBeVisible()
|
||||||
|
|
||||||
|
// 母件物料选择弹窗
|
||||||
|
const itemSelectBtn = page.getByRole('button', { name: /选择物料|选择/ }).first().catch(() => null)
|
||||||
|
if (itemSelectBtn) {
|
||||||
|
await itemSelectBtn
|
||||||
|
await expect(page.locator('.el-dialog').filter({ hasText: /选择物料/ })).toBeVisible()
|
||||||
|
await page.getByRole('button', { name: /关 闭|关闭|取 消/ }).click().catch(() => {})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 明细表格存在
|
||||||
|
await expect(page.locator('.el-table').first()).toBeVisible()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
33
erp-frontend-vue/tests/utils/erpTestUtils.ts
Normal file
33
erp-frontend-vue/tests/utils/erpTestUtils.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { expect, Page } from '@playwright/test'
|
||||||
|
|
||||||
|
export const USERNAME = process.env.ERP_USER || 'admin'
|
||||||
|
export const PASSWORD = process.env.ERP_PASS || 'admin123'
|
||||||
|
|
||||||
|
export async function login(page: Page) {
|
||||||
|
await page.goto('/login')
|
||||||
|
|
||||||
|
await page.getByPlaceholder(/用户名|账号/).fill(USERNAME)
|
||||||
|
await page.getByPlaceholder(/密码/).fill(PASSWORD)
|
||||||
|
|
||||||
|
const codeInput = page.getByPlaceholder(/验证码/).first()
|
||||||
|
if (await codeInput.isVisible().catch(() => false)) {
|
||||||
|
await codeInput.fill('0000')
|
||||||
|
}
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: /登录|登 录/ }).click()
|
||||||
|
await expect(page).toHaveURL(/dashboard/)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function clickSubMenu(page: Page, title: string) {
|
||||||
|
await page.click(`.el-sub-menu__title:has-text("${title}")`)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function clickMenuItem(page: Page, path: string) {
|
||||||
|
await page.click(`.el-menu-item[index="${path}"]`)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function expectBasicList(page: Page) {
|
||||||
|
await expect(page.locator('.el-form').first()).toBeVisible()
|
||||||
|
await expect(page.locator('.el-table').first()).toBeVisible()
|
||||||
|
}
|
||||||
|
|
||||||
28
erp-frontend-vue/tests/warehouse-issue.spec.ts
Normal file
28
erp-frontend-vue/tests/warehouse-issue.spec.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import { test, expect } from '@playwright/test'
|
||||||
|
import { login, clickSubMenu, clickMenuItem, expectBasicList } from './utils/erpTestUtils'
|
||||||
|
|
||||||
|
test.describe('生产领料单页面', () => {
|
||||||
|
test('列表搜索 & 打开新建领料单', async ({ page }) => {
|
||||||
|
await login(page)
|
||||||
|
|
||||||
|
await clickSubMenu(page, '生产管理')
|
||||||
|
await clickMenuItem(page, '/warehouse/issue')
|
||||||
|
await expect(page).toHaveURL(/\/warehouse\/issue/)
|
||||||
|
await expectBasicList(page)
|
||||||
|
|
||||||
|
const issueCodeInput = page.getByPlaceholder(/领料单号|单据编号/).first().catch(() => null)
|
||||||
|
if (issueCodeInput) {
|
||||||
|
await issueCodeInput.fill('TEST')
|
||||||
|
await page.getByRole('button', { name: /搜索|查询/ }).click()
|
||||||
|
}
|
||||||
|
|
||||||
|
const addBtn = page.getByRole('button', { name: /新建|新增/ }).first()
|
||||||
|
if (await addBtn.isVisible().catch(() => false)) {
|
||||||
|
await addBtn.click()
|
||||||
|
await expect(page).toHaveURL(/\/warehouse\/issue\/(new|edit|form)/)
|
||||||
|
await expect(page.locator('.el-form').first()).toBeVisible()
|
||||||
|
await expect(page.locator('.el-table').first()).toBeVisible()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
Reference in New Issue
Block a user