上次博客讲解了如何模拟键盘输入和鼠标操作以及拖动页面元素,此次课程将介绍如何处理页面弹框,保证测试案例继续运行。另外,还会介绍如果测试案例跨多个tab页,如何实现多tab页间切换测试。最后介绍cypress框架提供的插件和自定义命令功能,为了完成此次课程目标,拆分了2个task。
接下来就从第一个task开始吧。
实际cypress在浏览器中运行时发出了很多事件,其中就包括监听alert或者confirm弹框,并允许控制弹框行为。默认情况下,会对弹框进行确认处理,也提供了入口让用户自定义如何处理弹框。例如下面web页面,当点击populate按钮时,会弹出一个confirm弹框。
编写自动化脚本,Test Runner中选择“handleAlertDialog_spec.js”运行下面的脚本。运行过程中并没有看见confirm弹框,因为cypress默认进行了确认处理。Test Runner上查看脚本运行日志,可以在console中看到对alert弹框做了确认处理。
it("should cypress confirm alert dialog successfully", () => {
cy.visit("https://devexpress.github.io/testcafe/example/");
cy.get('#populate').click();
});
//可以看到,上面脚本只是打开被测页面完成点击按钮操作,没有任何脚本处理alert弹框。
查看Test Runner左侧的运行日志,可以看到对alert框做了确认处理,下图是处理日志记录。
除默认确认操作外,还可以根据alert弹框信息,用户自定义弹框处理结果。下面脚本对弹框做拒绝处理,等同于点击confirm弹框的cancel按钮。
it("should change alert dialog behavior successfully", () => {
cy.on('window:confirm', (str) => {
//cy.on('window:confirm')监听页面弹出的alert、confirm弹框
if (expect(str).to.eq('Reset information before proceeding?')) {
return false
//return fasle表示对弹框做拒绝处理,等于点击弹框上的cancel按钮
} else {
return true
//return true或者不返回任何值表示对弹框做确认处理,等于点击弹框上的confirm按钮
}
});
cy.visit("https://devexpress.github.io/testcafe/example/");
cy.get('#populate').click();
});
查看上面脚本运行日志,可以看到console中对弹框做了拒绝处理。日志记录如下图所示。
Cypress框架在浏览器中运行时,除监听confirm、aler弹框外,还监听应用运行的异常信息,这个在上次课程中也提到过。例如下面的脚本,默认情况下,cypress监听到被测应用存在未捕获的异常信息就会终止测试脚本运行,并显示错误信息。如果运行过程中,不期望被测应用的异常阻止测试脚本运行,那么可以return false。
it("should handle dialog successfully",()=> {
Cypress.on('uncaught:exception', (err, runnable) => {
//监听被测应用未捕获的异常信息
return false
//如果发现异常信息,不阻断测试脚本运行,默认情况下会阻断测试脚本运行,并显示错误信息
});
cy.visit("https://chercher.tech/practice/popups");
//打开被测应用
cy.get('input[name="alert"]').click();
//打开一个alert弹框,cypress默认进行了确认处理。
cy.get('input[name="confirmation"]').click();
//打开一个confirm弹框,cypress默认进行了确认处理
})
上面只介绍了cypress运行过程中发出的三个事件:window:confirm,window:alert,uncaught:exception。实际,Cypress运行过程中还发出了其他类型的事件,Cypress官网可以查询所有发出的事件信息。“ Catalog of Events | Cypress Documentation ”。上面讲解了如何处理弹框,接下来看看如果测试案例涉及多个tab页,如何进行处理。
这里的多tab页切换指当前页面上点击某个链接或者button后,浏览器上打开了一个新的tab页面,测试脚本需要切换到新的页面定位和操作页面元素。实际,Cyress框架并不支持多tab页间切换操作,因为cypress是运行在浏览器内部非node环境下,那么如果测试场景中涉及多tab页间切换,如何处理呢?Cypress官网给了一些workaround的解决办法,下面列举了官网建议的workround方法。
下面是对于上面四种workaround方式的示例代码,同样,Test Runner中选择“checkAttribute_spec.js”即可运行下面的脚本。
describe("multiple tab demo", () => {
Cypress.on('uncaught:exception', (err, runnable) => {
return false
});
//添加cypress.on(....)是因为找的这个demo页面自身会报一些错误,忽略被测页面的异常信息。
it("should check href and target attribute successfully", () => {
cy.visit("https://www.w3schools.com/tags/tryit.asp?filename=tryhtml_a_target");
cy.getIframe('iframe[name="iframeResult"]').find('p a')
//找的被测页面包含了iframe,故这里调用了前面封装的getIframe()自定义命令定位操作iframe下的页面元素
.should('have.attr', 'target')
.and('include', '_blank')
//校验超链接的属性值,除了校验target属性,还可以校验href属性值是否和期望的一致
})
});
下面是第二种workaround方式,Test Runner上选择“updateAttribute_spec.js”即可运行下面的脚本。
describe("multiple tab demo", () => {
Cypress.on('uncaught:exception', (err, runnable) => {
return false
});
it("should remove target attribute successfully",()=> {
cy.visit("https://www.w3schools.com/tags/tryit.asp?filename=tryhtml_a_target");
cy.getIframe('iframe[name="iframeResult"]').find('p a[href="https://www.w3schools.com"]')
.invoke('removeAttr', 'target').click();
//点击click前删除target属性,此时点击页面链接,就会在当前窗口打开新的tab页
cy.getIframe('iframe[name="iframeResult"]').find('a').contains('Try it Yourself').first().click();
//点击新tab页上的元素,以此验证可以定位操作新tab页上面的页面元素
})
});
剩余的两种workaround方式演示脚本如下所示,对应的脚本文件名称是“vistNewTabUrl_spec.js” 和 “checkBody_spec.js”。
describe("multiple tab demo", () => {
Cypress.on('uncaught:exception', (err, runnable) => {
return false
});
it("should visit new tab url successfully",()=> {
cy.visit("https://www.w3schools.com/tags/tryit.asp?filename=tryhtml_a_target");
cy.getIframe('iframe[name="iframeResult"]').find('p a[href="https://www.w3schools.com"]')
.should('have.attr', 'target')
.and('include', '_blank');
cy.visit('https://www.w3schools.com');
//通过调用cy.visit(new tab page url),那么新的tab页就会在目前页面上打开。
cy.getIframe('iframe[name="iframeResult"]').find('a').contains('Try it Yourself').first().click();
})
});
describe("multiple tab demo",()=> {
it("should check body successfully",()=> {
cy.visit("https://www.w3schools.com/tags/tryit.asp?filename=tryhtml_a_target");
cy.getIframe('iframe[name="iframeResult"]').find('p a[href="https://www.w3schools.com"]')
.then($el => {
const href = $el.prop("href");
cy.request(href)
.its('body')
.should('include' ,'a[href="/html/tryit.asp?filename=tryhtml_default"]')
//获取新tab的html body,校验body中是否存在期望查找的页面元素,此种方法有一定局限性,因为此种方法无法实现点击新tab页上的页面元素
})
})
});
总结而言,如果测试场景只是想校验点击某个链接后应该在新的tab页上显示内容,那么可以通过方法一完成;如果测试场景是点击某个链接后,还需操作新tab页上的页面元素,那么可以通过方法二和方法三完成。如果测试场景需要新旧两个tab页上来回切换操作,那么上面的四种方式可能就无法满足测试场景需要。这也是选择cypress框架的局限性之一,如果你的被测应用有较多的多tab页切换场景,那么就要考虑是否选择此框架了。
Cypress框架除在多tab页间切换场景支持有限外,也不支持跨域页面请求。例如下面的脚本运行会出错,因为点击主页面链接后,打开的新tab页和主页不属于相同的域。同样,Test Runner中选择“vistDiffDomainPage_spec.js”即可运行下面的脚本。
describe("visit different domain page",()=> {
Cypress.on('uncaught:exception', (err, runnable) => {
return false
});
it("can't visit different domain page",()=> {
cy.visit("https://freshdesignweb.com/jquery-html5-file-upload/");
cy.get('a[href="https://codepen.io/bi11johnston/pen/bsGDf"]')
.invoke('removeAttr', 'target').click();
cy.get('a[href="https://codepen.io/bi11johnston/pen/bsGDf"]').click();
//当点击该链接后,会在当前页面打开新域名的页面,因为cypress不支持跨域请求,故测试案例会失败
})
it("can't visit diff domain url page", () => {
cy.visit("http://www.lorenzostanco.com/lab/demos/CrossDomainFragment/Demo.html");
cy.get('p a').first().click();
})
//第二个脚本也是个跨域请求的例子,当运行此脚本时,cypress也会报错。
});
运行结果如下图所示,可以看到案例运行失败,并提示cypress不支持跨域请求。
总结而言,cypress对于多tab页间切换操作场景支持有限,另外,不支持测试场景中访问跨域的页面。后面介绍的puppeteer和testcafe框架在这两个点上支持就比较好,故在选择框架时需根据被测项目情况选择合适的框架。
执行“npm install cypress”命令时,除完成cypress安装外,该命令还会初始化一些目录和文件。对于插件而言,生成plugin目录,并在该目录下默认生成index.js文件;对于自定义命令而言,生成support目录,并在该目录下默认生成index.js和command.js文件。接下来就看看如何使用cypress提供的插件功能。
插件功能分为两类,一类是使用已经写好的插件,一类是在index.js文件中编写脚本。对于第一类,使用现有的插件,使用方法很简单,执行安装命令安装插件后,调用插件提供的功能即可。例如前面已经使用过的“cypress-file-upload”或者“cypress-shadow-dom”。可查看这里“ Plugins | Cypress Documentation ”获取cypress提供的所有插件信息。这里重点介绍如何在index.js编写自定义脚本。
index.js文件中默认必须按如下格式导出方法,这里的config信息是cypress.json中配置的信息,即通过插件脚本可以轻松获取cypress.json中所有的配置信息。on()方法中第一个参数是eventName,cypress支持4中类型的event,每个task的作用可查看此处资料 “ Writing a Plugin | Cypress Documentation ”。
module.exports = (on, config) => {
on('eventName',(arg1,arg2)=> {..... }))
}
下面脚本演示了通过调用task event完成数据库链接操作。当index.js中定义了数据库链接脚本后,当需要连接数据库进行查询操作时,调用cy.task('taskName')即可。
const mysql = require('mysql');
module.exports = (on, config) => {
const con = mysql.createConnection({
host: 'localhost',
user: 'root',
password: 'root123456',
database: 'test'
});
on('task', {queryDBWithParameter(sql) {
return new Promise((resolve, reject) => {
con.query(sql,function (err, result) {
if (err) {
reject(err)
} else {
return resolve(result)
}
})
})
}
});
};
上面就是关于插件的介绍,接着看看自定义命令功能,在前面的课程中实际已使用过自定义命令功能。只要是一些通用的处理,实际都可以封装成自定义命令,这样可以在脚本任意位置轻松调用自定义命令完成相关逻辑处理。例如调用任意接口前都需要先获取登陆生成的token,那么可以把登陆生成token封装成自定义命令,脚本如下所示。
Cypress.Commands.add("login", (email, password) => {
//定义command的名称是“login”,后面调用时输入Cypress.log(email,password)即可
return cy.request({ //调用cy.request()方法完成接口调用
url: "https://conduit.productionready.io/api/users/login",
method: "POST",
body: {
"user": {
"email": email,
"password": password
}
}
}).then(response => {
return response.body.user.token
})
});
调用自定义的login命令获取token后调用创建blog接口,代码详情如下所示。可以看到该文件并没有引入command.js文件,但却可以调用command.js中自定义的命令,因为Cypress在启动时会在所有脚本前加载command中的内容,无需手动引入。Test Runner上选择“createArticleWithCommand_spec.js”运行下面的案例,可以看到接口返回200,说明通过自定义命令正确获取到了token信息。
function createArticleThree() {
cy.login('[email protected]','12345678').then(token => {
//cy.login()方式调用
cy.request({
method: 'POST',
url: 'https://conduit.productionready.io/api/articles/',
headers: {
'authorization': 'Token '+token
},
body: {
"article": {
"tagList": [],
"title": "testTitle",
"description": "test",
"body": "test"
}
}
}).then(response => {
expect(response.status).to.eq(200);
})
})
}