# Cypress 登录初始化流程封装 ## 1. 目标和价值 ### 问题背景 在编写 Cypress e2e 测试时,几乎每个测试用例都需要: 1. 清除存储(session storage、local storage) 2. Mock 多个必要的 API(i18n、quota、patient types、body parts 等) 3. 执行登录操作 4. 等待应用初始化完成 这导致每个测试的 `beforeEach` 都包含大量重复代码(17+ 行),不仅冗长,而且难以维护。 **示例(重复的代码):** ```typescript beforeEach(() => { cy.clearAllSessionStorage(); cy.clearAllLocalStorage(); mockAllRequiredAPIs(); // 软件信息、日志 mockI18nSuccess('zh'); // 多语言资源 mockGetQuotaSuccess(); // 配额检查 mockGetPatientTypeHuman(); // 患者类型 - 必需! mockGetBodyPartHuman(); // 身体部位 - 必需! mockLoginSuccess(); // 登录 loginPage.visit(); loginPage.login('admin', '123456'); cy.wait('@loginSuccess'); cy.contains('登录成功').should('be.visible'); }); ``` ### 解决方案 创建自定义 Cypress 命令 `cy.loginAndInitialize()`,将所有登录初始化逻辑封装成一行代码。 ### 带来的好处 1. ✅ **代码复用** - 所有需要登录的测试都可以使用同一个命令 2. ✅ **易于维护** - 登录流程变更只需修改一处 3. ✅ **语义清晰** - 命令名称明确表达意图 4. ✅ **减少错误** - 避免遗漏必要的 Mock 导致测试失败 5. ✅ **类型安全** - 提供完整的 TypeScript 类型定义 6. ✅ **代码简洁** - 17 行代码压缩为 1 行 ## 2. 实现方案 ### 技术选择 使用 Cypress 自定义命令(Custom Commands)机制,通过 `Cypress.Commands.add()` 注册全局命令。 ### 实现步骤 #### 步骤 1:创建自定义命令 在 `cypress/support/commands.js` 中添加: ```javascript Cypress.Commands.add( 'loginAndInitialize', (username = 'admin', password = '123456') => { // 导入所需的 mock handlers const { mockAllRequiredAPIs, mockI18nSuccess, } = require('./mock/handlers/i18n'); const { mockGetQuotaSuccess } = require('./mock/handlers/quota'); const { mockGetPatientTypeHuman, mockGetBodyPartHuman, } = require('./mock/handlers/protocol'); const { mockLoginSuccess } = require('./mock/handlers/user'); const LoginPage = require('./pageObjects/LoginPage').default; // 清除存储 cy.clearAllSessionStorage(); cy.clearAllLocalStorage(); // Mock 所有必要的 API mockAllRequiredAPIs(); // 软件信息、日志 mockI18nSuccess('zh'); // 多语言资源 mockGetQuotaSuccess(); // 配额检查 mockGetPatientTypeHuman(); // 患者类型 - 必需! mockGetBodyPartHuman(); // 身体部位 - 必需! mockLoginSuccess(); // 登录 // 使用 LoginPage Page Object 进行登录 const loginPage = new LoginPage(); loginPage.visit(); loginPage.login(username, password); // 等待登录成功消息 cy.wait('@loginSuccess'); cy.contains('登录成功', { timeout: 5000 }).should('be.visible'); // 等待初始化完成 - 退出按钮可见表示已成功进入主页 cy.get('[data-testid="exit-button"]', { timeout: 10000 }).should( 'be.visible' ); cy.log('✅ 登录并初始化完成'); } ); ``` #### 步骤 2:添加 TypeScript 类型定义 创建 `cypress/support/commands.d.ts`: ```typescript /// declare namespace Cypress { interface Chainable { /** * 自定义命令:登录并完成初始化进入主页 * * 这个命令封装了完整的登录流程: * 1. 清除存储 * 2. Mock 所有必要的 API(i18n、quota、patient types、body parts) * 3. 执行登录 * 4. 等待初始化完成 * * @param username - 用户名,默认 'admin' * @param password - 密码,默认 '123456' * * @example * cy.loginAndInitialize(); * cy.loginAndInitialize('testuser', 'testpass'); */ loginAndInitialize( username?: string, password?: string ): Chainable; } } export {}; ``` #### 步骤 3:重构现有测试 **之前(17 行):** ```typescript import { mockLoginSuccess, mockLogoutSuccess, } from '../../support/mock/handlers/user'; import { mockI18nSuccess, mockAllRequiredAPIs, } from '../../support/mock/handlers/i18n'; import { mockGetQuotaSuccess } from '../../support/mock/handlers/quota'; import { mockGetPatientTypeHuman, mockGetBodyPartHuman, } from '../../support/mock/handlers/protocol'; import LoginPage from '../../support/pageObjects/LoginPage'; describe('User Logout', () => { const loginPage = new LoginPage(); beforeEach(() => { cy.clearAllSessionStorage(); cy.clearAllLocalStorage(); mockAllRequiredAPIs(); mockI18nSuccess('zh'); mockGetQuotaSuccess(); mockGetPatientTypeHuman(); mockGetBodyPartHuman(); mockLoginSuccess(); loginPage.visit(); loginPage.login('admin', '123456'); cy.wait('@loginSuccess'); cy.contains('登录成功').should('be.visible'); }); it('should logout successfully', () => { // 测试代码 }); }); ``` **之后(1 行):** ```typescript import { mockLogoutSuccess } from '../../support/mock/handlers/user'; import LoginPage from '../../support/pageObjects/LoginPage'; describe('User Logout', () => { const loginPage = new LoginPage(); beforeEach(() => { cy.loginAndInitialize(); }); it('should logout successfully', () => { // 测试代码 }); }); ``` ## 3. 使用示例 ### 示例 1:基本用法(使用默认用户名密码) ```typescript describe('Main Page Tests', () => { beforeEach(() => { // 使用默认用户名 'admin' 和密码 '123456' cy.loginAndInitialize(); }); it('should display main page correctly', () => { // 现在已经在主页了,可以直接开始测试 cy.get('[data-testid="exit-button"]').should('be.visible'); }); }); ``` ### 示例 2:使用自定义用户名和密码 ```typescript describe('Different User Tests', () => { beforeEach(() => { // 使用自定义用户名和密码 cy.loginAndInitialize('testuser', 'testpass123'); }); it('should work with different user', () => { // 测试代码 }); }); ``` ### 示例 3:在单个测试中使用 ```typescript describe('Specific Test', () => { it('should handle logout correctly', () => { // 在测试内部登录 cy.loginAndInitialize(); // 执行测试操作 cy.get('[data-testid="exit-button"]').click(); cy.get('[data-testid="exit-modal-logout-button"]').click(); // 验证返回登录页 cy.get('[data-testid="login-username-input"]').should('be.visible'); }); }); ``` ### 示例 4:结合 Page Objects 使用 ```typescript import MainPage from '../../support/pageObjects/MainPage'; import ExitModalPage from '../../support/pageObjects/ExitModalPage'; describe('Logout Flow', () => { const mainPage = new MainPage(); const exitModalPage = new ExitModalPage(); beforeEach(() => { cy.loginAndInitialize(); }); it('should successfully logout', () => { // 使用 Page Object 执行操作 mainPage.clickExitButton(); exitModalPage.shouldBeVisible(); exitModalPage.clickLogout(); // 验证登出成功 cy.contains('已退出登录').should('be.visible'); }); }); ``` ### 示例 5:多次登录场景 ```typescript describe('Multiple Login Tests', () => { it('should support multiple logins in same test', () => { // 第一次登录 cy.loginAndInitialize('user1', 'pass1'); cy.get('[data-testid="exit-button"]').should('be.visible'); // 登出 cy.get('[data-testid="exit-button"]').click(); cy.get('[data-testid="exit-modal-logout-button"]').click(); // 第二次登录 cy.loginAndInitialize('user2', 'pass2'); cy.get('[data-testid="exit-button"]').should('be.visible'); }); }); ``` ## 4. 技术细节 ### 封装的内容 `cy.loginAndInitialize()` 命令封装了以下操作: 1. **清除存储** ```javascript cy.clearAllSessionStorage(); cy.clearAllLocalStorage(); ``` 2. **Mock 所有必要的 API** - `mockAllRequiredAPIs()` - 软件信息、日志 API - `mockI18nSuccess('zh')` - 多语言资源 - `mockGetQuotaSuccess()` - 配额检查 - `mockGetPatientTypeHuman()` - 患者类型(AppInitializer 必需) - `mockGetBodyPartHuman()` - 身体部位(AppInitializer 必需) - `mockLoginSuccess()` - 登录 API 3. **执行登录** ```javascript loginPage.visit(); loginPage.login(username, password); ``` 4. **等待登录成功** ```javascript cy.wait('@loginSuccess'); cy.contains('登录成功', { timeout: 5000 }).should('be.visible'); ``` 5. **等待初始化完成** ```javascript cy.get('[data-testid="exit-button"]', { timeout: 10000 }).should( 'be.visible' ); ``` ### TypeScript 支持 TypeScript 类型定义提供: - ✅ 智能提示(IntelliSense) - ✅ 参数类型检查 - ✅ 返回值类型推断 - ✅ JSDoc 文档提示 ### 为什么需要 Mock Patient Types 和 Body Parts? 应用的 `AppInitializer` 组件在登录后会执行初始化操作: ```typescript // AppInitializer 初始化流程 1. getPatientTypes() // 获取患者类型列表 2. getBodyParts() // 获取身体部位列表 3. initializeProductState() // 初始化产品状态 ``` 只有这三个操作**全部完成**后,才会调用 `onInitialized()` 进入主界面。 如果缺少相应的 Mock: - ❌ 初始化会卡住 - ❌ 显示"正在初始化,请稍候..." - ❌ 测试会超时失败 ### 超时设置 - **登录成功消息**:5 秒超时 - **初始化完成**:10 秒超时(等待退出按钮可见) 这些超时值经过测试验证,足够应对正常的初始化流程。 ### 注意事项 1. **命令必须在 beforeEach 或测试内部调用** ```typescript // ✅ 正确 beforeEach(() => { cy.loginAndInitialize(); }); // ❌ 错误 - 不要在 describe 块外调用 cy.loginAndInitialize(); describe('Tests', () => {}); ``` 2. **确保 Mock Handlers 已正确导入** - 命令内部会自动导入所需的 Mock Handlers - 不需要在测试文件中手动导入 3. **Page Objects 的使用** - 命令内部使用 `LoginPage` Page Object - 确保 `LoginPage` 实现正确 4. **环境变量** - 命令使用当前环境的配置 - 确保测试环境配置正确 ## 5. 维护指南 ### 如果登录流程发生变化 只需修改 `cypress/support/commands.js` 中的 `loginAndInitialize` 命令,所有使用该命令的测试会自动更新。 **示例:添加新的 Mock** ```javascript Cypress.Commands.add( 'loginAndInitialize', (username = 'admin', password = '123456') => { // ... 现有代码 ... // 添加新的 Mock mockNewRequiredAPI(); // ... 现有代码 ... } ); ``` ### 如果需要不同的初始化流程 可以创建变体命令: ```javascript // 快速登录(跳过某些等待) Cypress.Commands.add('loginQuick', (username, password) => { // 简化的登录流程 }); // 登录到特定页面 Cypress.Commands.add('loginToPage', (username, password, pagePath) => { cy.loginAndInitialize(username, password); cy.visit(pagePath); }); ``` ## 6. 总结 ### 封装前后对比 | 方面 | 封装前 | 封装后 | | -------- | -------------------- | ---------------- | | 代码行数 | 17+ 行 | 1 行 | | 导入依赖 | 5+ 个 import | 不需要 | | 可维护性 | 低(每个测试都要改) | 高(只改一处) | | 易用性 | 复杂 | 简单 | | 类型安全 | 无 | 有(TypeScript) | | 错误风险 | 高(容易遗漏 Mock) | 低(一次性配置) | ### 适用场景 - ✅ 所有需要登录的 e2e 测试 - ✅ 需要进入主页的测试 - ✅ 需要完整初始化流程的测试 ### 不适用场景 - ❌ 测试登录页面本身 - ❌ 测试登录失败场景 - ❌ 需要特殊 Mock 配置的场景 对于特殊场景,可以直接手动编写登录逻辑,或创建专门的变体命令。