[Day27] Vue3 E2E Testing: Cypress 实战之 Todo MVC (下)

前情提要

前两天,我们开始为 Vue.js • TodoMVC 攥写 E2E 测试,并分别在

而今天我们就要把最後的 Case 4(改变 Todo 状态) 以及 Case 5(删除 Todo) 完成,并且还会介绍一个进阶的技巧 - 客制化命令 来优化我们的测试程序。

https://i.imgur.com/w29OiBy.png

大家可以透过连结自由操作一下,会对於接下来的测试例子更佳有感!

由於今天的内容会使用到前两篇的程序码,所以建议还没看过前两篇的朋友先前往阅读,那我们就马上开始今天的内容!

Custom Commands

我们先来回顾一下 Case 2 和 Case 3 的程序码,可以观察到其实有许多测试案例中都会需要新增 Todo 再接下去测试其他操作行为 (红色框的部分),不过又因为每个测试案例的情境有些许的不同,无法一口气使用 beforeEach 来新增 Todo 。

https://i.imgur.com/ti9UZlo.png

面对这样的情况 Cypress 提供了一个 API Cypress.Commands.add(name, callbackFn) ,我们可以用它来客制化一个 createTodo 的命令吧!

Cypress.Commands.add(name, callbackFn) 的使用方法很简单,name 就是命令的名称,callbackFn 则是要执行内容,通常就是那些我们不想要重复一直写的命令。

我们马上来看一下改动後的程序码吧!(是不是变得乾净许多,至少不用一直重复写一样的东西了)

const selectors = {
  main: '.main',
  footer: '.footer',
  todoItems: '.todo-list .todo',
  newTodo: '.new-todo',
  lastOne: '.todo-list .todo:last-child'
}

Cypress.Commands.add('createTodo', (todo) => {
  cy
    .get(selectors.newTodo)
    .type(`${todo}{enter}`)

  cy.get(selectors.lastOne)
    .find('label')
    .contains(todo)
})

describe('Todo MVC', () => {
  // ... case 1

	context('Case 2: New Todo', () => {
    it('Case 2-1: create items', () => {

      // create first item
      cy.createTodo(TODO_ITEM_ONE)

      // create second item
      cy.createTodo(TODO_ITEM_TWO)

      cy.get(selectors.todoItems).should('have.length', 2)
    })
	})

  // ... case 3 ~ 5
})

Case 4: 改变 Todo 状态

  • Case 4-1: 可以一一将 Todo 标示为完成。
  • Case 4-2: 可以一一将完成的 Todo 标示为未完成。
  • Case 4-3: 可以一次将所有 Todo 标示为完成。
  • Case 4-4: 可以一次将所有完成的 Todo 标示为未元成。
context('Case 4: Mark Todo As Completed', () => {
  it('Case 4-1: mark items as completed one by one', () => {
    cy.createTodo(TODO_ITEM_ONE)
    cy.createTodo(TODO_ITEM_TWO)

    cy.get(selectors.todoItems).eq(0).as('firstTodo')
    cy.get(selectors.todoItems).eq(1).as('secondTodo')

    cy.get('@firstTodo')
      .should('not.have.class', 'completed')
      .find('.toggle')
      .check()

    cy.get('@secondTodo')
      .should('not.have.class', 'completed')
      .find('.toggle')
      .check()

    cy.get('@firstTodo').should('have.class', 'completed')
    cy.get('@secondTodo').should('have.class', 'completed')
  })

  it('Case 4-2: clear the complete state of item one by one', () => {
    cy.createTodo(TODO_ITEM_ONE)
    cy.createTodo(TODO_ITEM_TWO)

    cy.get(selectors.todoItems).eq(0).as('firstTodo')
    cy.get(selectors.todoItems).eq(1).as('secondTodo')

    cy.get('@firstTodo')
      .should('not.have.class', 'completed')
      .find('.toggle')
      .check()

    cy.get('@secondTodo')
      .should('not.have.class', 'completed')
      .find('.toggle')
      .check()

    cy.get('@firstTodo')
      .should('have.class', 'completed')
      .find('.toggle')
      .uncheck()

    cy.get('@firstTodo').should('not.have.class', 'completed')
    cy.get('@secondTodo').should('have.class', 'completed')
  })

  it('Case 4-3: mark all items as completed at once', () => {
    const count = 10
    for (let i = 0; i < count; i++) {
      cy.createTodo(`Item ${i}`)
    }
    cy.get(selectors.toggleAll).check({ force: true })

    cy.get(selectors.todoItems)
      .filter('.completed')
      .should('have.length', count)
  })
  it('Case 4-4: clear the complete state of all item at once', () => {
    const count = 10
    for (let i = 0; i < count; i++) {
      cy.createTodo(`Item ${i}`)
    }
    cy.get(selectors.toggleAll).check({ force: true })
    cy.get(selectors.toggleAll).uncheck({ force: true })

    cy.get(selectors.todoItems)
      .filter('.completed')
      .should('have.length', 0)
  })
})

语法说明:

  • as(): 宣告一个别名让之後的 cy.get()cy.wait() 可以直接使用别名来快速查找元素。

  • check() & uncheck(): 选取或取消选取 checkbox 或者是 radio 的事件,而我们这里多传的 { force: true } 是因为 toggle all 的 checkbox 其实被隐藏起来了(opacity: 0) ,渲染在画面中的其实是 label icon,所以 check 或 uncheck 是无法正常发出事件的,因此在这边加上 { force: true } 便可以无视元素的可见度,直接对元素强制进行事件行为。

    https://i.imgur.com/McLDHzp.png

  • filter(): 筛选符合指定选择器的 DOM 元素。

Case 5: 删除 Todo

  • Case 5-1: 可以一一将 Todo 删除。
  • Case 5-2: 可以一次将标示为完成的 Todo 删除。
context('Case 5: Delete Todo', () => {
  it('Case 5-1: delete item one by one', () => {
    cy.createTodo(TODO_ITEM_ONE)
    cy.createTodo(TODO_ITEM_TWO)

    cy.get(selectors.todoItems).eq(0).as('firstTodo')
    cy.get(selectors.todoItems).eq(1).as('secondTodo')

    cy.get('@firstTodo').find('.destroy').click({ force: true })

    cy.get(selectors.todoItems)
      .eq(0)
      .find('label')
      .should('contain', TODO_ITEM_TWO)

    cy.get(selectors.todoItems).should('have.length', 1)

    cy.get('@secondTodo').find('.destroy').click({ force: true })

    cy.get(selectors.todoItems).should('have.length', 0)
  })

  it('Case 5-2: delete all completed items at once', () => {
    const count = 10
    for (let i = 0; i < count; i++) {
      cy.createTodo(`Item ${i}`)
    }
    cy.get(selectors.toggleAll).check({ force: true })

    cy.get(selectors.clearCompleted).click()

    cy.get(selectors.todoItems).should('have.length', 0)
  })
})

参考资料


今天的分享就到这边,如果大家对我分享的内容有兴趣欢迎点击追踪 & 订阅系列文章,如果对内容有任何疑问,或是文章内容有错误,都非常欢迎留言讨论或指教的!

Vue3 E2E Testing 的主题在这边告一个段落了,明天我会分享前端部署网页的方式 (Vercel, Netlify & AWS S3),我们明天见!


<<:  企划实现(27)

>>:  [Day28]程序菜鸟自学C++资料结构演算法 – 基数排序法(Radix sort)

[iT铁人赛Day1]JAVA下载与执行

JAVA是一个大家既熟悉又陌生的程序语言 稍微知道怎麽编写但会写错,也可能写还不知道怎麽储存,还有不...

# JS杂食-06--小实作-1: Star Calculator

参考资料1:MDN — the Mozilla Developer Network 参考资料2:0...

[Day26]ISO 27001 附录 A.14 系统获取、开发及维护

A.14 系统获取、开发及维护 A.14.1 资讯系统之安全要求事项 目标:确保资讯安全系跨越整个生...

Endpoint

我们用到的 API endpoint 只有一个,就是用来取得港铁机场快綫、东涌綫、屯马綫及将军澳綫最...

[铁人赛 Day05] React 中的 Code splitting(代码分离)方法

什麽是 Code splitting?为什麽要做 Code splitting? 如果你的网站是用 ...