CyPress技巧
常规介绍
-
describe(nameDesc,callbackFunc4Desc)callbackFunc4Desc中可以创建多个it(name,callback)- 可以使用
this.beforeAll(cb)添加一个在所有it执行前执行的函数 - 可以使用
this.beforeEach(cb)添加在每个it执行前执行的函数 - 每个
it执行前都会清空cookie,因此如果有多个it那么登录逻辑不能在beforeAll
-
it(name,callbackFunc4It)函数的参数callbackFunc4It可以是异步函数(返回Promise),也可以是一个常规同步函数(无返回值),一般使用常规函数便于理解,不要使用labmda函数。callbackFunc4It的this是执行上下文,当为同步函数时,可以有一个入参Done是一个回调函数,如it('test',function(done){}),调用done会立即完成测试,调用done(Error)则会以异常形式结束测试,此时也必须手动调用done结束测试
-
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');//第六个执行
});
});
- cy中可以自定义函数
-
- 如选择某个特定的iframe可以提取成一个函数
var ibFn=()=>cy.getIFrameBody('iframe[class^=masterAppIframe].components-iframe');,然后通过ibFn().get('.clz')则可以在iframe中选择元素
- 如选择某个特定的iframe可以提取成一个函数
技巧
元素选择
判断一个dom是否存在且在不存在时不使测试失败
直接使用cy.get('.not_fund')或者cy.get('.not_fund').should('exist')时,如果.not_fund在超时后无法选择到一个dom时会使测试失败。
想要不失败,可以用以下两种方式:
- 使用
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;//返回值会作为下一个链的入参
})
- 使用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中拦截请求
- 使用样例
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')
})
});
});
- 比如在获取某个接口结果的情况,建议使用wait单次处理
- 建议在发出动作前intercept,在动作后wait,如点击查询,根据查询情况做某事,保证wait到想要的查询