فهرست منبع

docs: add Cypress login initialization encapsulation guide

- Document the custom cy.loginAndInitialize() command
- Explain problem background and solution benefits
- Provide implementation steps and code examples
- Include 5 detailed usage examples
- Add technical details and maintenance guide
dengdx 1 هفته پیش
والد
کامیت
c63bc84ca6
1فایلهای تغییر یافته به همراه478 افزوده شده و 0 حذف شده
  1. 478 0
      docs/Cypress登录初始化流程封装.md

+ 478 - 0
docs/Cypress登录初始化流程封装.md

@@ -0,0 +1,478 @@
+# 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
+/// <reference types="cypress" />
+
+declare namespace Cypress {
+  interface Chainable<Subject = any> {
+    /**
+     * 自定义命令:登录并完成初始化进入主页
+     *
+     * 这个命令封装了完整的登录流程:
+     * 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<Subject>;
+  }
+}
+
+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 配置的场景
+
+对于特殊场景,可以直接手动编写登录逻辑,或创建专门的变体命令。