Cypress登录初始化流程封装.md 12 KB

Cypress 登录初始化流程封装

1. 目标和价值

问题背景

在编写 Cypress e2e 测试时,几乎每个测试用例都需要:

  1. 清除存储(session storage、local storage)
  2. Mock 多个必要的 API(i18n、quota、patient types、body parts 等)
  3. 执行登录操作
  4. 等待应用初始化完成

这导致每个测试的 beforeEach 都包含大量重复代码(17+ 行),不仅冗长,而且难以维护。

示例(重复的代码):

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 中添加:

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

/// <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 行):

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 行):

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:基本用法(使用默认用户名密码)

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:使用自定义用户名和密码

describe('Different User Tests', () => {
  beforeEach(() => {
    // 使用自定义用户名和密码
    cy.loginAndInitialize('testuser', 'testpass123');
  });

  it('should work with different user', () => {
    // 测试代码
  });
});

示例 3:在单个测试中使用

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 使用

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:多次登录场景

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. 清除存储
   cy.clearAllSessionStorage();
   cy.clearAllLocalStorage();
  1. Mock 所有必要的 API

    • mockAllRequiredAPIs() - 软件信息、日志 API
    • mockI18nSuccess('zh') - 多语言资源
    • mockGetQuotaSuccess() - 配额检查
    • mockGetPatientTypeHuman() - 患者类型(AppInitializer 必需)
    • mockGetBodyPartHuman() - 身体部位(AppInitializer 必需)
    • mockLoginSuccess() - 登录 API
  2. 执行登录

   loginPage.visit();
   loginPage.login(username, password);
  1. 等待登录成功
   cy.wait('@loginSuccess');
   cy.contains('登录成功', { timeout: 5000 }).should('be.visible');
  1. 等待初始化完成

    cy.get('[data-testid="exit-button"]', { timeout: 10000 }).should(
     'be.visible'
    );
    

    TypeScript 支持

    TypeScript 类型定义提供:

    • ✅ 智能提示(IntelliSense)
    • ✅ 参数类型检查
    • ✅ 返回值类型推断
    • ✅ JSDoc 文档提示

    为什么需要 Mock Patient Types 和 Body Parts?

    应用的 AppInitializer 组件在登录后会执行初始化操作:

    // AppInitializer 初始化流程
    1. getPatientTypes()  // 获取患者类型列表
    2. getBodyParts()     // 获取身体部位列表
    3. initializeProductState()  // 初始化产品状态
    

只有这三个操作全部完成后,才会调用 onInitialized() 进入主界面。

如果缺少相应的 Mock:

  • ❌ 初始化会卡住
  • ❌ 显示"正在初始化,请稍候..."
  • ❌ 测试会超时失败

超时设置

  • 登录成功消息:5 秒超时
  • 初始化完成:10 秒超时(等待退出按钮可见)

这些超时值经过测试验证,足够应对正常的初始化流程。

注意事项

  1. 命令必须在 beforeEach 或测试内部调用
   // ✅ 正确
   beforeEach(() => {
     cy.loginAndInitialize();
   });

   // ❌ 错误 - 不要在 describe 块外调用
   cy.loginAndInitialize();
   describe('Tests', () => {});
  1. 确保 Mock Handlers 已正确导入

    • 命令内部会自动导入所需的 Mock Handlers
    • 不需要在测试文件中手动导入
  2. Page Objects 的使用

    • 命令内部使用 LoginPage Page Object
    • 确保 LoginPage 实现正确
  3. 环境变量

    • 命令使用当前环境的配置
    • 确保测试环境配置正确

5. 维护指南

如果登录流程发生变化

只需修改 cypress/support/commands.js 中的 loginAndInitialize 命令,所有使用该命令的测试会自动更新。

示例:添加新的 Mock

Cypress.Commands.add(
  'loginAndInitialize',
  (username = 'admin', password = '123456') => {
    // ... 现有代码 ...

    // 添加新的 Mock
    mockNewRequiredAPI();

    // ... 现有代码 ...
  }
);

如果需要不同的初始化流程

可以创建变体命令:

// 快速登录(跳过某些等待)
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 配置的场景

对于特殊场景,可以直接手动编写登录逻辑,或创建专门的变体命令。