logout-implementation-summary.md 8.6 KB

注销功能实现总结

问题描述

原先的"注销用户"功能执行了系统级的注销操作(shutdown /l),导致注销了整个操作系统用户,而不是应用程序内的用户。

解决方案

将系统级注销改为应用级注销,即清除应用内的用户状态并返回登录页面。

修改内容

1. 核心功能实现

src/components/ExitModal.tsx

  • ✅ 引入 useDispatchclearUserInfo
  • ✅ 在 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() - 模拟登出成功(可选,当前未使用)

测试初始化流程:

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 测试命令:

# 运行所有安全相关测试
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() 函数:
   export function logout(): Promise<{ data: { code: string } }> {
     return axiosInstance.post('/pub/logout');
   }
  1. ExitModal.tsx 中调用后端 API(可选):
   case 'logout':
     actionName = '注销用户';
     try {
       // 调用后端 logout API
       await logout();
     } catch (error) {
       console.error('后端登出失败:', error);
       // 即使后端失败也继续清除前端状态
     }
     dispatch(clearUserInfo());
     message.success('已退出登录');
     onClose();
     return;
  1. 更新 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.tscypress/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 文字修改而失效
  • ✅ 最佳实践:符合测试自动化的业界标准

测试用例示例

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 开发