了解 Cypress 测试框架,给已有项目加上 Cypress 测试

什么是 Cypress

巴拉巴拉,不如看图,执行命令之后,就能自己这样了,

好,如果觉得有点意思,就往下看,然后刚自己项目也来个,一方面能装,一方面能加绩效。

正经文开始。

Cypress 是一个现代化的端到端测试框架,专为现代 Web 应用而设计,其提供了完整的测试解决方案,包括:

  • 浏览器测试:直接在浏览器中运行测试,提供真实的用户交互体验
  • 时间旅行调试:可以回放测试的每个步骤,查看 DOM 状态、网络请求等
  • 自动等待:智能等待元素出现和网络请求完成,减少测试的不稳定性
  • 实时重载:修改测试代码时自动重新运行测试
  • 丰富的断言:提供多种断言方法,支持 DOM、网络、Cookie 等验证
  • 可视化调试:实时查看测试执行过程,快速定位问题
  • 测试生成:通过 Cypress Studio 录制用户操作生成测试代码
  • 自愈能力AI 驱动的智能测试修复和优化

如何使用 Cypress

1. 安装和初始化

bash 复制代码
# 安装 Cypress
npm install cypress --save-dev
# 或
yarn add cypress --dev
# 或 推荐
pnpm add cypress --dev

# 初始化 Cypress,运行之后项目根目录自动生成cypress
npx cypress open

2. 项目结构

bash 复制代码
cypress/
├── e2e/                 # 端到端测试文件
│   ├── home.cy.js
│   └── user.cy.js
├── fixtures/            # 测试数据文件
│   └── users.json
├── support/             # 支持文件
│   ├── commands.js      # 自定义命令
│   └── e2e.js          # 全局配置
├── screenshots/         # 失败截图
└── videos/             # 测试视频

3. 基本测试结构

javascript 复制代码
describe('功能模块测试', () => {
  beforeEach(() => {
    // 每个测试前的准备工作
    cy.visit('/')
  })

  it('应该能够执行某个操作', () => {
    // 测试步骤
    cy.get('[data-testid="button"]').click()
    cy.contains('成功').should('be.visible')
  })
})

常用方法分类

1. 页面导航

javascript 复制代码
// 访问页面
cy.visit('/login')
cy.visit('https://example.com')

// 页面重载
cy.reload()
cy.reload(true) // 强制重载,忽略缓存

// 前进/后退
cy.go('back')
cy.go('forward')
cy.go(-1) // 后退1步
cy.go(1)  // 前进1步

2. 元素查找和操作

javascript 复制代码
// 查找元素
cy.get('#id')                    // 通过ID
cy.get('.class')                 // 通过class
cy.get('[data-testid="button"]') // 通过属性
cy.get('button').contains('登录') // 通过文本内容

// 点击操作
cy.get('button').click()
cy.get('button').click({ force: true }) // 强制点击
cy.get('button').dblclick()            // 双击
cy.get('button').rightclick()          // 右键点击

// 输入操作
cy.get('input').type('文本内容')
cy.get('input').clear().type('新内容')  // 清空后输入
cy.get('input').type('{enter}')         // 按回车键
cy.get('input').type('{selectall}{backspace}') // 全选删除

// 选择操作
cy.get('select').select('选项值')
cy.get('input[type="checkbox"]').check()
cy.get('input[type="radio"]').check()

3. 断言验证

javascript 复制代码
// 可见性断言
cy.get('button').should('be.visible')
cy.get('button').should('not.be.visible')
cy.get('button').should('exist')
cy.get('button').should('not.exist')

// 文本断言
cy.get('h1').should('contain', '标题')
cy.get('h1').should('have.text', '完整标题')
cy.get('input').should('have.value', '输入值')

// 属性断言
cy.get('input').should('have.attr', 'type', 'text')
cy.get('input').should('have.class', 'form-control')
cy.get('input').should('have.css', 'color', 'rgb(0, 0, 0)')

// 数量断言
cy.get('li').should('have.length', 3)
cy.get('li').should('have.length.greaterThan', 2)
cy.get('li').should('have.length.lessThan', 5)

4. 网络请求

javascript 复制代码
// 监听网络请求
cy.intercept('GET', '/api/users').as('getUsers')
cy.intercept('POST', '/api/login').as('login')

// 等待请求完成
cy.wait('@getUsers')
cy.wait('@login').then((interception) => {
  expect(interception.response.statusCode).to.eq(200)
})

// 模拟网络响应
cy.intercept('GET', '/api/users', { fixture: 'users.json' })
cy.intercept('POST', '/api/login', { statusCode: 401, body: { error: 'Unauthorized' } })

5. 文件操作

javascript 复制代码
// 文件上传
cy.get('input[type="file"]').selectFile('cypress/fixtures/image.jpg')
cy.get('input[type="file"]').selectFile(['file1.txt', 'file2.txt'])

// 文件下载
cy.get('a[download]').click()
cy.readFile('cypress/downloads/document.pdf')

6. 窗口和视口

javascript 复制代码
// 视口操作
cy.viewport(1280, 720)
cy.viewport('iphone-6')
cy.viewport('macbook-15')

// 窗口操作
cy.window().its('localStorage').should('exist')
cy.window().then((win) => {
  win.localStorage.setItem('token', 'abc123')
})
javascript 复制代码
// Cookie 操作
cy.setCookie('sessionId', 'abc123')
cy.getCookie('sessionId').should('have.property', 'value', 'abc123')
cy.clearCookie('sessionId')
cy.clearCookies()

// 本地存储
cy.clearLocalStorage()
cy.getAllLocalStorage()
cy.setLocalStorage('key', 'value')

8. 自定义命令

javascript 复制代码
// 在 cypress/support/commands.js 中定义
Cypress.Commands.add('login', (username, password) => {
  cy.visit('/login')
  cy.get('#username').type(username)
  cy.get('#password').type(password)
  cy.get('button[type="submit"]').click()
})

// 使用自定义命令
cy.login('user@example.com', 'password123')

常用配置

1. 基础配置 (cypress.config.js)

javascript 复制代码
import { defineConfig } from 'cypress'

export default defineConfig({
  // 视口设置
  viewportWidth: 1280,
  viewportHeight: 720,
  
  // 超时设置
  defaultCommandTimeout: 10000,
  requestTimeout: 10000,
  responseTimeout: 10000,
  pageLoadTimeout: 30000,
  
  // 视频和截图
  video: true,
  screenshotOnRunFailure: true,
  videosFolder: 'cypress/videos',
  screenshotsFolder: 'cypress/screenshots',
  
  // 浏览器安全 
  chromeWebSecurity: false,
  
  // E2E 配置
  e2e: {
    baseUrl: 'http://localhost:3000',
    supportFile: 'cypress/support/e2e.js',
    specPattern: 'cypress/e2e/**/*.cy.{js,jsx,ts,tsx}',
    setupNodeEvents(on, config) {
      // 插件配置
      on('task', {
        log(message) {
          console.log(message)
          return null
        }
      })
    },
  },
  
  // 组件测试配置
  component: {
    devServer: {
      framework: 'create-react-app',
      bundler: 'webpack',
    },
  },
})

2. 环境变量配置

Cypress 支持多种方式设置环境变量,特别适合多套环境的场景:

2.1 在 cypress.config.js 中设置

javascript 复制代码
// cypress.config.js
export default defineConfig({
  env: {
    apiUrl: 'https://api.example.com',
    username: 'test@example.com',
    password: 'password123'
  }
})

// 使用环境变量
cy.visit(Cypress.env('apiUrl'))
cy.get('#username').type(Cypress.env('username'))

2.2 使用 cypress.env.json 文件(推荐)

json 复制代码
// cypress.env.json
{
  "testUser": {
    "username": "test@example.com",
    "password": "password123",
    "securityCode": "test123"
  },
  "baseUrl": "https://test.example.com",
  "loginUrl": "https://test.example.com/login",
  "apiUrl": "https://api.test.example.com"
}

2.3 多环境配置文件

开发环境配置 (cypress.env.dev.json)

json 复制代码
{
  "testUser": {
    "username": "dev@example.com",
    "password": "dev123",
    "securityCode": "dev123"
  },
  "baseUrl": "https://dev.example.com",
  "loginUrl": "https://dev.example.com/login",
  "apiUrl": "https://api.dev.example.com",
  "environment": "development"
}

测试环境配置 (cypress.env.test.json)

json 复制代码
{
  "testUser": {
    "username": "test@example.com",
    "password": "test123",
    "securityCode": "test123"
  },
  "baseUrl": "https://test.example.com",
  "loginUrl": "https://test.example.com/login",
  "apiUrl": "https://api.test.example.com",
  "environment": "testing"
}

生产环境配置 (cypress.env.prod.json)

json 复制代码
{
  "testUser": {
    "username": "prod@example.com",
    "password": "prod123",
    "securityCode": "prod123"
  },
  "baseUrl": "https://prod.example.com",
  "loginUrl": "https://prod.example.com/login",
  "apiUrl": "https://api.prod.example.com",
  "environment": "production"
}

2.4 动态环境配置

javascript 复制代码
// cypress.config.js
import { defineConfig } from 'cypress'

export default defineConfig({
  e2e: {
    setupNodeEvents(on, config) {
      // 根据环境变量选择配置文件
      const env = config.env.environment || 'test'
      const envFile = `cypress.env.${env}.json`
      
      try {
        const envConfig = require(`./${envFile}`)
        config.env = { ...config.env, ...envConfig }
      } catch (error) {
        console.log(`环境配置文件 ${envFile} 不存在,使用默认配置`)
      }
      
      return config
    },
  },
})

2.5 命令行环境变量

bash 复制代码
# 设置环境变量
CYPRESS_baseUrl=https://test.example.com cypress run

# 设置多个环境变量
CYPRESS_baseUrl=https://test.example.com CYPRESS_username=test@example.com cypress run

# 使用环境文件
cypress run --env-file cypress.env.test.json

2.6 package.json 脚本配置

json 复制代码
{
  "scripts": {
    "cypress:open": "cypress open",
    "cypress:run": "cypress run",
    "cypress:dev": "cypress run --env-file cypress.env.dev.json",
    "cypress:test": "cypress run --env-file cypress.env.test.json",
    "cypress:prod": "cypress run --env-file cypress.env.prod.json",
    "cypress:dev:open": "cypress open --env-file cypress.env.dev.json",
    "cypress:test:open": "cypress open --env-file cypress.env.test.json"
  }
}

2.8 动态环境切换

javascript 复制代码
// cypress/support/e2e.js
const environments = {
  dev: {
    baseUrl: 'https://dev.example.com',
    apiUrl: 'https://api.dev.example.com',
    testUser: {
      username: 'dev@example.com',
      password: 'dev123'
    }
  },
  test: {
    baseUrl: 'https://test.example.com',
    apiUrl: 'https://api.test.example.com',
    testUser: {
      username: 'test@example.com',
      password: 'test123'
    }
  },
  prod: {
    baseUrl: 'https://prod.example.com',
    apiUrl: 'https://api.prod.example.com',
    testUser: {
      username: 'prod@example.com',
      password: 'prod123'
    }
  }
}

// 根据环境变量选择配置
const currentEnv = Cypress.env('environment') || 'test'
const envConfig = environments[currentEnv]

// 设置全局配置
Cypress.config('baseUrl', envConfig.baseUrl)
Cypress.env('apiUrl', envConfig.apiUrl)
Cypress.env('testUser', envConfig.testUser)

2.9 环境变量优先级

Cypress 环境变量的优先级(从高到低):

  1. 命令行参数CYPRESS_baseUrl=https://example.com cypress run
  2. cypress.env.json 文件
  3. cypress.config.js 中的 env 配置

2.10 实际使用示例

javascript 复制代码
// 在测试中使用环境变量
describe('多环境测试', () => {
  beforeEach(() => {
    const baseUrl = Cypress.env('baseUrl')
    const testUser = Cypress.env('testUser')
    
    console.log(`当前环境: ${Cypress.env('environment')}`)
    console.log(`基础URL: ${baseUrl}`)
    console.log(`测试用户: ${testUser.username}`)
    
    cy.visit(baseUrl)
  })

  it('应该能够登录', () => {
    const testUser = Cypress.env('testUser')
    
    cy.get('#username').type(testUser.username)
    cy.get('#password').type(testUser.password)
    cy.get('#securityCode').type(testUser.securityCode)
    cy.get('button[type="submit"]').click()
  })
})

2.11 环境变量验证

javascript 复制代码
// cypress/support/e2e.js
// 验证必需的环境变量
beforeEach(() => {
  const requiredEnvVars = ['baseUrl', 'testUser']
  
  requiredEnvVars.forEach(envVar => {
    if (!Cypress.env(envVar)) {
      throw new Error(`缺少必需的环境变量: ${envVar}`)
    }
  })
  
  // 验证 testUser 对象
  const testUser = Cypress.env('testUser')
  if (!testUser || !testUser.username || !testUser.password) {
    throw new Error('testUser 环境变量配置不完整')
  }
})

2.12 CI/CD 环境变量

yaml 复制代码
# .github/workflows/cypress.yml
name: Cypress Tests
on: [push, pull_request]

jobs:
  cypress-run:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        environment: [dev, test, prod]
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
        with:
          node-version: '18'
      - run: npm ci
      - run: npx cypress run --env-file cypress.env.${{ matrix.environment }}.json
        env:
          CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

2.13 环境变量最佳实践

  1. 敏感信息处理

    javascript 复制代码
    // 不要在代码中硬编码敏感信息
    // 使用环境变量或加密存储
    const apiKey = Cypress.env('apiKey') || 'default-key'
  2. 环境变量文档化

    markdown 复制代码
    ## 环境变量说明
    - `baseUrl`: 应用基础URL
    - `testUser.username`: 测试用户名
    - `testUser.password`: 测试密码
    - `apiUrl`: API接口地址
  3. 环境变量验证

    javascript 复制代码
    // 在测试开始前验证环境变量
    const validateEnv = () => {
      const required = ['baseUrl', 'testUser']
      required.forEach(key => {
        if (!Cypress.env(key)) {
          throw new Error(`Missing required env var: ${key}`)
        }
      })
    }
  4. 环境变量类型安全

    typescript 复制代码
    // cypress/support/index.d.ts
    declare namespace Cypress {
      interface Cypress {
        env(key: 'baseUrl'): string
        env(key: 'testUser'): {
          username: string
          password: string
          securityCode: string
        }
        env(key: 'apiUrl'): string
      }
    }

3. 全局配置 (cypress/support/e2e.js)

javascript 复制代码
// 全局异常处理
Cypress.on('uncaught:exception', (err, runnable) => {
  // 忽略特定错误
  if (err.message.includes('ResizeObserver loop limit exceeded')) {
    return false
  }
  return true
})

// 全局超时设置
Cypress.config('defaultCommandTimeout', 10000)
Cypress.config('requestTimeout', 10000)

// 全局命令
Cypress.Commands.add('login', (username, password) => {
  cy.session([username, password], () => {
    cy.visit('/login')
    cy.get('#username').type(username)
    cy.get('#password').type(password)
    cy.get('button[type="submit"]').click()
    cy.url().should('not.include', '/login')
  })
})

4. 数据驱动测试

javascript 复制代码
// cypress/fixtures/users.json
{
  "users": [
    { "username": "user1", "password": "pass1", "role": "admin" },
    { "username": "user2", "password": "pass2", "role": "user" }
  ]
}

// 使用测试数据
describe('用户登录测试', () => {
  beforeEach(() => {
    cy.fixture('users').as('users')
  })

  it('不同用户登录测试', function() {
    this.users.forEach(user => {
      cy.login(user.username, user.password)
      cy.get('[data-testid="user-role"]').should('contain', user.role)
      cy.logout()
    })
  })
})

5. 并行测试配置

javascript 复制代码
// cypress.config.js
export default defineConfig({
  e2e: {
    setupNodeEvents(on, config) {
      // 并行测试配置
      if (config.env.CI) {
        config.specPattern = [
          'cypress/e2e/**/*.cy.{js,jsx,ts,tsx}',
          'cypress/e2e/**/*.spec.{js,jsx,ts,tsx}'
        ]
      }
    },
  },
})

最佳实践

1. 测试组织

  • 使用 describeit 组织测试结构
  • 每个测试文件专注于一个功能模块
  • 使用 beforeEachafterEach 进行测试准备和清理

2. 元素选择

  • 优先使用 data-testid 属性
  • 避免使用不稳定的 CSS 选择器
  • 使用语义化的选择器

3. 等待策略

  • 使用 cy.get().should() 而不是 cy.wait()
  • 等待元素可见而不是存在
  • 使用网络请求等待而不是固定时间

4. 测试数据

  • 使用 fixtures 管理测试数据
  • 每个测试使用独立的数据
  • 测试后清理数据

5. 错误处理

  • 使用全局异常处理
  • 提供有意义的错误信息
  • 使用截图和视频辅助调试

给已有项目加上 cypress 测试

1. 安装 cypress

这里注意,如果是 node 版本在 20 以下需要手动指定 cypress 版本,否则会因为版本不兼容导入失败,最新的 cypress 是 15.15.0。这里我的 node 版本是 18.12.0,所以安装 13.15.0 版本。

bash 复制代码
pnpm install cypress@13.15.0 -D
pnpm cypress install

2. 配置脚本

这里不加配置的话,自己选择功能和浏览器。如果需要指定功能和浏览器,可以在命令后面加上 --browser 和 --spec 参数,我的项目是后台管理系统,所以指定 e2e 功能和 chrome 浏览器。

在 package.json 中添加以下脚本:

json 复制代码
"scripts": {
  "e2e": "cypress open --e2e --browser chrome"
}

3. 执行脚本,打开 cypress 测试界面

bash 复制代码
pnpm e2e

执行脚本之后,项目根目录会生成 cypress.config.ts 文件,这个文件是 cypress 的配置文件,可以在这里配置一些全局变量和选项。

js 复制代码
import { defineConfig } from 'cypress';

export default defineConfig({
  // 全局配置 设置浏览器窗口大小、视频录制、截图等
  viewportWidth: 1280,
  viewportHeight: 1500,
  video: true,
  screenshotOnRunFailure: true,
  defaultCommandTimeout: 10000,
  requestTimeout: 10000,
  responseTimeout: 10000,
  pageLoadTimeout: 30000,
  chromeWebSecurity: false,
  // 防止会话数据被清除
  // experimentalSessionAndOrigin: true,

  // E2E 测试配置
  e2e: {
    baseUrl: 'https://nppss-app-management-test1.test.xdf.cn',
    supportFile: 'cypress/support/e2e.js',
    specPattern: 'cypress/e2e/**/*.cy.{js,jsx,ts,tsx}',
    setupNodeEvents(on, config) {
      // 可以在这里添加插件配置
      // 例如:on('task', { ... })
    },
  },
});

cypress/support/e2e.js 文件中,可以添加一些全局配置,比如超时时间、异常处理等。

js 复制代码
// 全局配置
Cypress.on('uncaught:exception', (err, runnable) => {
  // 防止未捕获的异常导致测试失败
  // 对于 React 应用,这通常是由于开发环境中的热重载导致的
  if (err.message.includes('ResizeObserver loop limit exceeded')) {
    return false;
  }
  if (err.message.includes('Non-Error promise rejection captured')) {
    return false;
  }
  // 返回 false 防止 Cypress 失败
  return false;
});

// 设置全局超时
Cypress.config('defaultCommandTimeout', 10000);
Cypress.config('requestTimeout', 10000);
Cypress.config('responseTimeout', 10000);

同时,根目录会生成 cypress 目录,这个目录是 cypress 的测试文件目录,可以在这里创建测试文件。

比如创建一个简单的测试文件cypress/e2e/home-page.cy.js:

javascript 复制代码
describe('简单测试', () => {
  it('应该能够访问首页', () => {
    cy.visit('https://www.baidu.com');
    cy.get('body').should('be.visible');
    cy.title().should('not.be.empty');
  });
});

4. 拿一个页面试手

拿一个页面试试手,比如教师任务管理页面,cypress/e2e/teacher-task-page.cy.js:

javascript 复制代码
describe('教师任务管理页面测试', () => {
  const pagePath = '/shuangshi-ai/teacher-manage';

  // 公共函数:获取iframe内容
  const getIframeBody = () => {
    // 等待iframe加载完成
    cy.get('iframe', { timeout: 30000 }).should('be.visible');

    // 等待iframe内容完全加载
    cy.get('iframe').should(($iframe) => {
      const iframe = $iframe[0];
      const iframeDoc = iframe.contentDocument || iframe.contentWindow.document;
      expect(iframeDoc.body).to.exist;
    });

    return cy
      .get('iframe')
      .its('0.contentDocument.body')
      .should('not.be.empty')
      .then(cy.wrap); // 封装为Cypress可操作对象
  };

  beforeEach(() => {
    // 每个测试前清除存储
    cy.clearAllStorage();

    // 统一处理页面访问和登录
    cy.visit(pagePath);

    // 如果被重定向到登录页面,处理登录流程
    cy.url().then((url) => {
      if (url.includes('/e2/login')) {
        console.log('检测到登录重定向,执行登录流程...');

        // 等待登录页面加载完成
        cy.get('#txtUser').should('be.visible');
        cy.get('#txtPwd').should('be.visible');
        cy.get('#txtTestEnvSecCode').should('be.visible');

        // 输入登录信息
        cy.get('#txtUser').type(Cypress.env('testUser').username);
        cy.get('#txtPwd').type(Cypress.env('testUser').password);
        cy.get('#txtTestEnvSecCode').type(Cypress.env('testUser').securityCode);

        // 点击登录按钮
        cy.get('#btnLogin').click();

        // 等待跳转回目标页面
        cy.url({ timeout: 30000 }).should('include', pagePath);
      }
    });

    // 验证页面内容加载
    cy.get('body').should('be.visible');
    cy.wait(2000); // 等待页面完全加载
  });

  it('应该能够处理创建功能', () => {
    // 监听试卷列表API请求
    cy.intercept('GET', '**/npad/teacher/task/paper/list**').as(
      'paperListRequest',
    );

    getIframeBody().within(() => {
      // 点击创建按钮
      cy.get('.ant-pro-table-list-toolbar-right')
        .contains('button', '创建')
        .should('be.visible')
        .click();

      // 验证创建弹框出现
      cy.get('.ant-drawer').should('be.visible');

      // 选择学校 - 点击选择器,然后选择"上海"
      cy.get('.ant-drawer .ant-select').first().should('be.visible').click();
      cy.get('.ant-select-dropdown').should('be.visible');
      cy.contains('北京').click();

      // 选择部门 - 点击选择器,然后选择"智慧学习部"
      cy.get('.ant-drawer .ant-select').eq(1).should('be.visible').click();
      cy.get('.ant-select-dropdown').should('be.visible');
      // 如果已经有默认值,先清除再选择
      cy.get('.ant-select-dropdown .ant-select-item')
        .contains('智慧学习部')
        .click({ force: true });

      // 选择学科 - 点击选择器,然后选择"英语"
      cy.get('.ant-drawer .ant-select').eq(2).should('be.visible').click();
      cy.get('.ant-select-dropdown').should('be.visible');
      cy.contains('英语').click({ force: true });

      // 输入任务名称
      cy.get('.ant-drawer #name')
        .should('be.visible')
        .clear()
        .type('cypress测试任务');

      // 选择日期
      cy.get('.ant-drawer div[id="type"] label:first-child')
        .should('be.visible')
        .click();
      cy.get('.ant-drawer input[placeholder="开始日期"]')
        .should('be.visible')
        .click();
      cy.get('.ant-picker-panel-container').should('be.visible');
      cy.get(
        '.ant-picker-panel-container .ant-picker-footer .ant-picker-preset',
      )
        .eq(0)
        .should('be.visible')
        .click();

      // 去添加试题,打开弹框
      cy.wait(1000);
      cy.get('.ant-drawer .select-add-container button')
        .should('be.visible')
        .click();

      cy.get('.ant-modal').should('be.visible');

      // 定位部门选择器的输入框
      cy.get('.ant-modal .ant-select').eq(2).should('be.visible').click();
      cy.get('.ant-select-dropdown').should('be.visible');
      // 选择本校
      cy.contains('本校').click({ force: true });
      cy.wait(1000);

      // 查询试题
      cy.get('.ant-modal-body .ant-pro-table-search .ant-btn-primary')
        .scrollIntoView()
        .should('be.visible')
        .click({ force: true });

      // 选择第一个试题
      cy.get('.ant-modal-body .ant-radio-wrapper')
        .eq(0)
        .should('be.visible')
        .click();

      // 点击确定按钮,选择试题完成
      cy.get('.ant-modal-body .footer .confirm-btn')
        .should('be.visible')
        .click();
      // 等待弹框关闭
      cy.get('.ant-modal').should('not.exist');

      // 点击确定按钮,创建任务完成
      cy.get('.ant-drawer .button-box .ant-btn-primary')
        .should('be.visible')
        .click();
      // 关闭弹框,创建任务完成
      cy.get('.ant-drawer .ant-drawer-header-title .ant-drawer-close')
        .should('be.visible')
        .click();
      cy.get('.ant-drawer').should('not.exist');
    });
  });
});
相关推荐
weixin_382395233 小时前
为小工厂量身打造:本地部署的物料管理系统带缺料计算
前端·制造
__zRainy__4 小时前
解决pnpm v10+不自动构建
前端·pnpm·工程化
猫猫不是喵喵.4 小时前
Vue3 Props 属性
前端·javascript·vue.js
醉城夜风~5 小时前
CSS元素显示模式(display)
前端·css
AI大模型-小华6 小时前
Codex 三方充值快速入门指南
java·前端·数据库·chatgpt·ai编程·codex·chatgpt pro
做前端的娜娜子8 小时前
同一链接实现 PC Web 与移动 H5 自适应
前端·掘金·金石计划
小帅不太帅8 小时前
架构没变、规模没变,DeepSeek V4 Flash 正式版凭什么暴涨 47 分?
前端·aigc·deepseek
jarvisuni9 小时前
DeepSeekFlash前端依旧拉垮,而且变慢了很多!
前端·javascript·算法
卷福同学10 小时前
AI编程出海第二步:验证关键词能否做站
前端·人工智能·后端