# 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 配置的场景
对于特殊场景,可以直接手动编写登录逻辑,或创建专门的变体命令。