|
@@ -0,0 +1,304 @@
|
|
|
+# 注销功能实现总结
|
|
|
+
|
|
|
+## 问题描述
|
|
|
+
|
|
|
+原先的"注销用户"功能执行了系统级的注销操作(`shutdown /l`),导致注销了整个操作系统用户,而不是应用程序内的用户。
|
|
|
+
|
|
|
+## 解决方案
|
|
|
+
|
|
|
+将系统级注销改为应用级注销,即清除应用内的用户状态并返回登录页面。
|
|
|
+
|
|
|
+## 修改内容
|
|
|
+
|
|
|
+### 1. 核心功能实现
|
|
|
+
|
|
|
+#### `src/components/ExitModal.tsx`
|
|
|
+
|
|
|
+- ✅ 引入 `useDispatch` 和 `clearUserInfo`
|
|
|
+- ✅ 在 `logout` case 中调用 `dispatch(clearUserInfo())` 清除 Redux 用户状态
|
|
|
+- ✅ 显示成功消息并关闭 modal
|
|
|
+- ✅ 移除了 `window.electronAPI.logoutUser()` 调用
|
|
|
+- ✅ 更新 TypeScript 类型定义,移除 `logoutUser` API
|
|
|
+
|
|
|
+### 2. Electron 主进程清理
|
|
|
+
|
|
|
+#### `preload.js`
|
|
|
+
|
|
|
+- ✅ 移除 `logoutUser` API 暴露
|
|
|
+
|
|
|
+#### `main.js`
|
|
|
+
|
|
|
+- ✅ 移除 `exit-logout` IPC handler
|
|
|
+- ✅ 移除 `getSystemCommands()` 中的 `logout` 命令配置
|
|
|
+
|
|
|
+### 3. 后端 API 占位
|
|
|
+
|
|
|
+#### `src/API/security/userActions.ts`
|
|
|
+
|
|
|
+- ✅ 添加 `logout()` 函数占位
|
|
|
+- ✅ 当前返回 Promise.resolve,待后端 API 开发后实现
|
|
|
+
|
|
|
+### 4. Cypress e2e 测试
|
|
|
+
|
|
|
+#### `cypress/support/mock/handlers/user.ts`
|
|
|
+
|
|
|
+- ✅ 添加 `mockLogoutSuccess()` mock handler
|
|
|
+
|
|
|
+#### `cypress/e2e/security/logout.cy.ts`(新增)
|
|
|
+
|
|
|
+- ✅ 测试注销流程:点击注销 → 清除状态 → 返回登录页
|
|
|
+- ✅ 测试 Redux 状态清除
|
|
|
+- ✅ 测试 modal 取消操作
|
|
|
+- ✅ 测试占位 API(不调用后端)
|
|
|
+
|
|
|
+#### 文件组织
|
|
|
+
|
|
|
+- ✅ 将 `login.cy.ts` 移动到 `cypress/e2e/security/` 目录
|
|
|
+
|
|
|
+## 工作流程
|
|
|
+
|
|
|
+### 用户点击"注销用户"后的流程:
|
|
|
+
|
|
|
+1. 用户在退出 modal 中点击"注销用户"按钮
|
|
|
+2. `ExitModal` 组件调用 `dispatch(clearUserInfo())`
|
|
|
+3. Redux store 清除用户信息:
|
|
|
+ - token → ''
|
|
|
+ - expire → 0
|
|
|
+ - uid → 0
|
|
|
+ - name → ''
|
|
|
+ - avatar → ''
|
|
|
+4. 显示成功消息:"已退出登录"
|
|
|
+5. 关闭退出 modal
|
|
|
+6. `src/pages/index/index.tsx` 检测到 `loggedIn = false`
|
|
|
+7. 自动渲染 `<Login />` 组件,返回登录页面
|
|
|
+
|
|
|
+## 测试验证
|
|
|
+
|
|
|
+### 手动测试步骤:
|
|
|
+
|
|
|
+1. 启动应用并登录
|
|
|
+2. 点击退出按钮
|
|
|
+3. 在弹出的 modal 中点击"注销用户"
|
|
|
+4. 验证:
|
|
|
+ - ✅ 显示"已退出登录"消息
|
|
|
+ - ✅ Modal 关闭
|
|
|
+ - ✅ 返回到登录页面
|
|
|
+ - ✅ **不会**注销操作系统用户
|
|
|
+
|
|
|
+### 测试依赖的 Mock Handlers:
|
|
|
+
|
|
|
+为确保测试能够正常运行,logout 测试依赖以下 mock handlers:
|
|
|
+
|
|
|
+1. **i18n Mocks** (`cypress/support/mock/handlers/i18n.ts`)
|
|
|
+
|
|
|
+ - `mockAllRequiredAPIs()` - 模拟软件信息和日志 API
|
|
|
+ - `mockI18nSuccess('zh')` - 模拟多语言资源加载成功
|
|
|
+
|
|
|
+2. **Quota Mocks** (`cypress/support/mock/handlers/quota.ts`)
|
|
|
+
|
|
|
+ - `mockGetQuotaSuccess()` - 模拟配额检查成功
|
|
|
+
|
|
|
+3. **Protocol Mocks** (`cypress/support/mock/handlers/protocol.ts`) - **重要!**
|
|
|
+
|
|
|
+ - `mockGetPatientTypeHuman()` - 模拟患者类型 API
|
|
|
+ - `mockGetBodyPartHuman()` - 模拟身体部位 API
|
|
|
+ - **注意**:这两个 mock 是必需的,否则登录后会卡在"正在初始化,请稍候..."
|
|
|
+
|
|
|
+4. **User Mocks** (`cypress/support/mock/handlers/user.ts`)
|
|
|
+ - `mockLoginSuccess()` - 模拟登录成功
|
|
|
+ - `mockLogoutSuccess()` - 模拟登出成功(可选,当前未使用)
|
|
|
+
|
|
|
+### 测试初始化流程:
|
|
|
+
|
|
|
+```typescript
|
|
|
+beforeEach(() => {
|
|
|
+ cy.clearAllSessionStorage();
|
|
|
+ cy.clearAllLocalStorage();
|
|
|
+
|
|
|
+ mockAllRequiredAPIs(); // 软件信息、日志
|
|
|
+ mockI18nSuccess('zh'); // 多语言资源
|
|
|
+ mockGetQuotaSuccess(); // 配额检查
|
|
|
+ mockGetPatientTypeHuman(); // 患者类型 ← 必需!
|
|
|
+ mockGetBodyPartHuman(); // 身体部位 ← 必需!
|
|
|
+ mockLoginSuccess(); // 登录
|
|
|
+
|
|
|
+ loginPage.visit();
|
|
|
+ loginPage.login('admin', '123456');
|
|
|
+});
|
|
|
+```
|
|
|
+
|
|
|
+### AppInitializer 初始化流程
|
|
|
+
|
|
|
+登录成功后,`AppInitializer` 会执行以下初始化操作:
|
|
|
+
|
|
|
+1. `getPatientTypes()` - 获取患者类型列表
|
|
|
+2. `getBodyParts()` - 获取身体部位列表
|
|
|
+3. `initializeProductState()` - 初始化产品状态
|
|
|
+
|
|
|
+**重要**:只有这三个操作全部完成后,才会调用 `onInitialized()` 进入主界面。如果缺少相应的 mock,测试会卡在"正在初始化,请稍候..."
|
|
|
+
|
|
|
+### Cypress 测试命令:
|
|
|
+
|
|
|
+```bash
|
|
|
+# 运行所有安全相关测试
|
|
|
+npm run cypress:run -- --spec "cypress/e2e/security/**/*.cy.ts"
|
|
|
+
|
|
|
+# 只运行 logout 测试
|
|
|
+npm run cypress:run -- --spec "cypress/e2e/security/logout.cy.ts"
|
|
|
+
|
|
|
+# 以交互模式运行
|
|
|
+npm run cypress:open
|
|
|
+```
|
|
|
+
|
|
|
+## 后续待办
|
|
|
+
|
|
|
+### 当后端 logout API 开发完成后:
|
|
|
+
|
|
|
+1. 更新 `src/API/security/userActions.ts` 中的 `logout()` 函数:
|
|
|
+
|
|
|
+ ```typescript
|
|
|
+ export function logout(): Promise<{ data: { code: string } }> {
|
|
|
+ return axiosInstance.post('/pub/logout');
|
|
|
+ }
|
|
|
+ ```
|
|
|
+
|
|
|
+2. 在 `ExitModal.tsx` 中调用后端 API(可选):
|
|
|
+
|
|
|
+ ```typescript
|
|
|
+ case 'logout':
|
|
|
+ actionName = '注销用户';
|
|
|
+ try {
|
|
|
+ // 调用后端 logout API
|
|
|
+ await logout();
|
|
|
+ } catch (error) {
|
|
|
+ console.error('后端登出失败:', error);
|
|
|
+ // 即使后端失败也继续清除前端状态
|
|
|
+ }
|
|
|
+ dispatch(clearUserInfo());
|
|
|
+ message.success('已退出登录');
|
|
|
+ onClose();
|
|
|
+ return;
|
|
|
+ ```
|
|
|
+
|
|
|
+3. 更新 Cypress 测试以验证 API 调用
|
|
|
+
|
|
|
+## 注意事项
|
|
|
+
|
|
|
+- ✅ 此修改**不影响**"关闭程序"功能
|
|
|
+- ✅ 此修改**不影响**"关机"功能
|
|
|
+- ✅ "关机"功能仍然调用系统命令(需要管理员权限)
|
|
|
+- ✅ 应用级注销是纯前端操作,无需管理员权限
|
|
|
+- ✅ 登录状态检查逻辑在 `src/pages/index/index.tsx` 中,无需修改
|
|
|
+
|
|
|
+## 影响范围
|
|
|
+
|
|
|
+### 修改的文件:
|
|
|
+
|
|
|
+1. `src/components/ExitModal.tsx`
|
|
|
+2. `preload.js`
|
|
|
+3. `main.js`
|
|
|
+4. `src/API/security/userActions.ts`
|
|
|
+5. `src/layouts/SystemZone.tsx` - 添加 `data-testid="exit-button"`
|
|
|
+6. `cypress/support/mock/handlers/user.ts`
|
|
|
+7. `cypress/support/pageObjects/MainPage.ts` - 添加退出按钮方法
|
|
|
+8. `cypress/support/pageObjects/ExitModalPage.ts`(新增)
|
|
|
+9. `cypress/e2e/security/logout.cy.ts`(新增)
|
|
|
+10. `cypress/e2e/login.cy.ts` → `cypress/e2e/security/login.cy.ts`(移动)
|
|
|
+
|
|
|
+### 未修改的文件:
|
|
|
+
|
|
|
+- ✅ `src/states/user_info/index.ts`(已有 `clearUserInfo` action)
|
|
|
+- ✅ `src/pages/index/index.tsx`(已有登录状态检查逻辑)
|
|
|
+- ✅ 其他组件和页面
|
|
|
+
|
|
|
+## 测试改进
|
|
|
+
|
|
|
+### Page Object 模式
|
|
|
+
|
|
|
+为提高测试可维护性和可读性,使用了 Page Object 模式:
|
|
|
+
|
|
|
+#### 1. MainPage (`cypress/support/pageObjects/MainPage.ts`)
|
|
|
+
|
|
|
+- `clickExitButton()` - 点击退出按钮(带 10 秒超时等待)
|
|
|
+- `getExitButton()` - 获取退出按钮元素
|
|
|
+
|
|
|
+#### 2. ExitModalPage (`cypress/support/pageObjects/ExitModalPage.ts`)
|
|
|
+
|
|
|
+新增专门的退出 Modal Page Object:
|
|
|
+
|
|
|
+- `shouldBeVisible()` - 验证 modal 可见
|
|
|
+- `shouldNotBeVisible()` - 验证 modal 已关闭
|
|
|
+- `clickLogout()` - 点击"注销用户"
|
|
|
+- `clickClose()` - 点击"关闭程序"
|
|
|
+- `clickShutdown()` - 点击"关机"
|
|
|
+- `clickCancel()` - 点击"取消"
|
|
|
+
|
|
|
+#### 3. 退出按钮和 Modal 按钮增强
|
|
|
+
|
|
|
+为所有测试相关的按钮添加了 `data-testid` 属性,便于测试定位:
|
|
|
+
|
|
|
+**SystemZone.tsx** - 退出按钮:
|
|
|
+
|
|
|
+- `data-testid="exit-button"`
|
|
|
+
|
|
|
+**ExitModal.tsx** - Modal 内的四个按钮:
|
|
|
+
|
|
|
+- `data-testid="exit-modal-close-button"` - 关闭程序
|
|
|
+- `data-testid="exit-modal-logout-button"` - 注销用户
|
|
|
+- `data-testid="exit-modal-shutdown-button"` - 关机
|
|
|
+- `data-testid="exit-modal-cancel-button"` - 取消
|
|
|
+
|
|
|
+**为什么使用 data-testid?**
|
|
|
+
|
|
|
+- ✅ 更稳定:不依赖于 UI 文本(文本可能因多语言而改变)
|
|
|
+- ✅ 更明确:清晰地表明这是测试用的标识符
|
|
|
+- ✅ 更易维护:测试代码不会因为 UI 文字修改而失效
|
|
|
+- ✅ 最佳实践:符合测试自动化的业界标准
|
|
|
+
|
|
|
+### 测试用例示例
|
|
|
+
|
|
|
+```typescript
|
|
|
+it('should successfully logout and return to login page', () => {
|
|
|
+ // 使用 Page Object 点击退出按钮(带超时等待)
|
|
|
+ mainPage.clickExitButton();
|
|
|
+
|
|
|
+ // 验证 modal 可见
|
|
|
+ exitModalPage.shouldBeVisible();
|
|
|
+
|
|
|
+ // 点击"注销用户"按钮
|
|
|
+ exitModalPage.clickLogout();
|
|
|
+
|
|
|
+ // 验证成功消息和返回登录页
|
|
|
+ cy.contains('已退出登录').should('be.visible');
|
|
|
+ loginPage.getUsernameInput().should('be.visible');
|
|
|
+});
|
|
|
+```
|
|
|
+
|
|
|
+### 等待时间优化
|
|
|
+
|
|
|
+- 退出按钮等待时间:10 秒(`clickExitButton` 方法中)
|
|
|
+- Modal 显示等待:5 秒(`shouldBeVisible` 方法中)
|
|
|
+
|
|
|
+## 测试覆盖
|
|
|
+
|
|
|
+### 已覆盖的测试场景:
|
|
|
+
|
|
|
+- ✅ 注销成功并返回登录页
|
|
|
+- ✅ Redux 状态清除验证
|
|
|
+- ✅ Modal 取消操作
|
|
|
+- ✅ 占位 API 验证
|
|
|
+- ✅ 使用 Page Object 模式提高可维护性
|
|
|
+- ✅ 增加等待时间确保元素加载
|
|
|
+
|
|
|
+### 建议的额外测试:
|
|
|
+
|
|
|
+- 手动测试在实际 Electron 环境中的完整流程
|
|
|
+- 测试连续登录登出操作
|
|
|
+- 测试登出后的权限验证
|
|
|
+
|
|
|
+## 完成状态
|
|
|
+
|
|
|
+✅ 所有计划的修改已完成
|
|
|
+✅ 所有测试代码已编写
|
|
|
+⏳ 等待手动测试验证
|
|
|
+⏳ 等待后端 API 开发
|