CyPress技巧

324 阅读5分钟

CyPress技巧

常规介绍

  1. describe(nameDesc,callbackFunc4Desc)

    • callbackFunc4Desc中可以创建多个it(name,callback)
    • 可以使用this.beforeAll(cb)添加一个在所有it执行前执行的函数
    • 可以使用this.beforeEach(cb)添加在每个it执行前执行的函数
    • 每个it执行前都会清空cookie,因此如果有多个it那么登录逻辑不能在beforeAll
  2. it(name,callbackFunc4It)函数的参数callbackFunc4It可以是异步函数(返回Promise),也可以是一个常规同步函数(无返回值),一般使用常规函数便于理解,不要使用labmda函数。

    • callbackFunc4It的this是执行上下文,当为同步函数时,可以有一个入参Done是一个回调函数,如it('test',function(done){}),调用done会立即完成测试,调用done(Error)则会以异常形式结束测试,此时也必须手动调用done结束测试
  3. cy函数cy.xxxfunction()不是立即执行的,而是加入链路(Chainable),这意味着非cy函数和cy函数的执行顺序不一致的,每个cy函数都返回Promise可以调用其then以在该函数后执行某个动作,这在需要根据某个结果后根据不同情况做不同动作时非常有用

describe('my', function () {
this.beforeEach(function(){
  //在it前执行
});
it('my', function () {
    cy.visit("https://baidu.com")//进入页面
    console.log('1');//第一个执行
    cy.wait(1000);//第三个执行
    console.log('2');//第二个执行
    cy.xpath('//div[contains(@class,"come_class")]')//第四个执行
    .then((doms)=>{
        //回调函数中的非cy函数可以正常顺序执行,相当于是一个子链。
        //即在四之后执行,下面代码根据不同情况执行下面的分支
        if(doms[0].classList.includes('my_css')){
            cy.xpath('//div/li[@id="xxx"]').click();//第五个执行
        }else{
            cy.xpath('//div/li[@id="yyy"]').click();//第五个执行(如果上面分支没进)
        }
    });
    cy.get('.my_css2').contains('my text');//第六个执行
});
});
  1. cy中可以自定义函数
    1. 如选择某个特定的iframe可以提取成一个函数var ibFn=()=>cy.getIFrameBody('iframe[class^=masterAppIframe].components-iframe');,然后通过ibFn().get('.clz')则可以在iframe中选择元素

技巧

元素选择

判断一个dom是否存在且在不存在时不使测试失败

直接使用cy.get('.not_fund')或者cy.get('.not_fund').should('exist')时,如果.not_fund在超时后无法选择到一个dom时会使测试失败。

想要不失败,可以用以下两种方式:

  1. 使用should(callback),expect常见使用
cy.get('.not_fund').should($jQueryEl=>{
    //如果想在某种情况下使测试失败则可以通过expect包装参数并判断,否则本次should不会失败
    //expect($jQueryEl).to.have.length(1)
    if($jQueryEl.length===0){
        // do something
    }
    return $jQueryEl;//返回值会作为下一个链的入参
})
  1. 使用jQuery选择器Cypress.$(cssSelector),注意Cypress.$是一个常规同步函数,不会加入Chainable
cy.wait(1000).then(()=>{
    //Cypress.$是一个普通同步函数,因此需要显式放在链中使用,以保证顺序,
    //如在cy.funcXXX().then(callback)的callback中使用,即本例
    var $el = Cypress.$('.not_fund')
    if($el.length===0){
        // do something
        //此处也可以使用expect($el)在需要的时候使测试失败,或者直接抛出异常
    }
})

xpath选择器

选择有某个class的节点

建议使用contains(@class,"clz")或者starts-with(@class,"clz"),而不是@class="clz"

使用starts-with情况:节点就一个class,为什么使用starts-with呢?因为经过编译后class名称可能带有hash,如my_class__aqw123hash其中my_class是源代码中命名,而__aqw123hash则是编译后添加的hash

使用contains情况:节点有多个class,建议使用contains,因为xpath取的是字符串

案例://div[starts-with(@class,"header")]``//div[contains(@class,"header")]

选择有某个子元素的元素

如以下情况,需要在someIn输入内容,然后点击submit

<form>
    <div class="in">
        <label for="someIn">someIn</label>
        <div class="ii">
            <input id="someIn" type="text" name="someIn">
        </div>
    </div>
    <div>
        <label for="someOut">someOut</label>
        <div class="ii">
            <input id="someOut" type="text" name="someOut">
        </div>
    </div>
    <div class="btn">
        <button>
            <span>
                <span>submit</span>
            </span>
        </button>
        <button>
            <span>reset</span>
        </button>
    </div>
</form>

那么cy脚本可以这样

cy.xpath(`//div[contains(@class,"in")]//div[@class="ii" and (./span[text()="someIn"])]//input[@id="someIn"]`).type('xxx');
cy.xpath(`//div[contains(@class,"in")]//button[(.//span[text()="submit"])]//input[@id="someIn"]`).click();

其中div[@class="ii" and (./span[text()="someIn"])]表示选择子节点span的文本是someIn的div节点,and表示多个条件组合;

其中button[(.//span[text()="submit"])]表示选择后代节点span的文本是submit的button节点;

xpath中也允许以..表示选择父节点,因此其实也可以这样选择someIn:

//div[contains(@class,"in")]//div/span[text()="someIn"]/..//input[@id="someIn"]

即语法为节点A[ root是节点A的常规xpath选择器 ]

判断某个菜单是否展开

比如下面这个,判断父菜单是否展开,如果展开了就直接点击子菜单,否则就先点击父菜单展开后再点击子菜单。

系统中如果菜单的右边展开按钮,有classmenuGroupToggle,如果已经展开则其还拥有classmenuGroupToggle_close,那么就可以根据这种情况来判断。

cy.xpath('//div[starts-with(@class,"menuGroupItem")]//div[text()="父菜单"]/../i[contains(@class,"menuGroupToggle")]').then((doms)=>{
  if(doms&&doms.length>=1){
      var classList = doms[0].classList;
      for(var i=0;i<classList.length;++i){
          if(classList[i].includes('menuGroupToggle_close')){
              return;
          }
      }
  }
  //父菜单没展开的情况下展开
  cy.xpath('//div[starts-with(@class,"menuGroupItem")]//div[text()="父菜单"]').click();
  cy.wait(350);
});
cy.xpath('//div[starts-with(@class,"menuGroupList")]//span[text()="子菜单"]').click();

xhr&fetch请求拦截

在cy中使用cy.intercept拦截请求,并根据情况作出反应

cy中拦截请求

  1. 使用样例
describe('my', function () {
  it('test', function () {
    //进入了某个页面,这个页面点击一个按钮会发出请求
    cy.visit('http://localhost:8080')
    // 在触发请求前拦截请求,持续监听,即如果发出了多个请求都匹配,则都会拦截
    cy.intercept({
      hostname: 'localhost', // 域名
      pathname: '/s/**', // 匹配pat,不包括query参数,一个*表示匹配最多一个子路由,即/s/,/s/1;两个*表示匹配任意多个子路由,如 /s/,/s/1,/s/1/2
      //query:'',// 匹配query参数,json或字符串,如果不需要根据query匹配则无需配置,其他条件同理
      method: 'GET'
    },
    // 每个匹配的请求都会被拦截并处理,这个routeHandler回调是可选的,如果处理单个查询的情况,建议在wait处理
    function routeHandler(request) {
      // request参数结构
      // {body:json?,headers:{},method:'GET|POST',query:"str",url:"完整URL"}
      console.log('request: ', request)
    }).as('doXhr')
    // 假设点击这个按钮触发查询,请求/s/123
    cy.get('button.btn_query').click()
    // 等待首个响应,根据实际情况估计超时时间,即指处理最早匹配的请求
    cy.wait('@doXhr',{requestTimeout:18000,responseTimeout:45000}).then(function respHander(reqRespObj) {
      // reqRespObj参数结构
      // {request:{body:json?,headers:{},method:'GET|POST',query:"str",url:"str"},response:{body:json?,headers:{},statusCode:int,statusMessage:"str"}}
      console.log('reqRespObj: ', reqRespObj)
      cy.wait(200)
      // 假设如果正常情况会根据响应绘制一个table
      cy.get('table.list').should('exist')
    })
  });
});
  1. 比如在获取某个接口结果的情况,建议使用wait单次处理
  2. 建议在发出动作前intercept,在动作后wait,如点击查询,根据查询情况做某事,保证wait到想要的查询