Cypress前端E2E自动化测试记录
近期用Cypress作一个新项目的前端E2E自动化测试,对比TestCafe作前端E2E自动化测试,Cypress有一些不同之处,现记录下来。
所有Command都是异步的
Cypress中的所有Command都是异步的,所以编写自动化脚本时要时刻记住这点。比如:
不能从Command中直接返回,而要用 .then()
来处理。下面例子不工作:
const allProductes = cy.get('@allProductes')
正确的应当用
let allProductes = null;
cy.get('@allProductes').then((values) => {
allProductes = values;
}
还有一例,下面代码也不能正常工作,是因为在一个代码块中,所有command都是异步先入队列,只有代码块结束后才会依次执行:
it('is not using aliases correctly', function () {
cy.fixture('users.json').as('users')
// 此处as还没有执行呢,只是入了队列而已
const user = this.users[0]
})
正确的应当仍然是要用 .then()
范式:
cy.fixture('users.json').then((users) => {
const user = users[0]
// passes
cy.get('header').should('contain', user.name)
})
这样的结果就是,测试代码中难免出现很多的回调函数嵌套。
同样,因为Cypress命令是异步的,所以debugger也要在 then()
里调用,不能象下面这样:
it('let me debug like a fiend', function() {
cy.visit('/my/page/path')
cy.get('.selector-in-question')
debugger // Doesn't work
})
// cy.pause()
上下文中的this不能用在箭头函数中
当用Mocha的共享上下文对象机制时, this
不能用在箭头函数,要用传统的函数形式
beforeEach(function () {
cy.fixture('product').as('allProductes');
cy.gotoIndexPage();
})
...
phonePage.actRootSel.forEach((actSel, index) => {
cy.get(actSel + phonePage.btnExchangeSel, {timeout: Cypress.env('timeoutLevel1')}).click();
cy.get(rechargePage.sidebarSel).should('exist');
cy.get(rechargePage.sidebarSel).within(function ($sidebar) {
productName = this.allProductes[index].product;
cy.get(rechargePage.productNameSel).should('have.text', productName);
gameCount = UTILS.randomPhoneNo('1323434');
cy.get(rechargePage.gameCountSel).type(gameCount);
cy.get(rechargePage.btnRechargeSel).should('have.text', '支付 ' + this.allProductes[index].jfValue + ' 积分');
cy.get(rechargePage.btnRechargeSel).click();
});
...
如上所示,在within的回调函数中用了 function ($sidebar) {
这样形式,而不能用箭头函数。为了避免这样的麻烦,可以考虑用 cy.get('@*')
来获取上下文中变量。但那样又要面对异步的问题(注: cy.get
是异步的,而 this.*
是同步的),还得用 then()
来解决问题,有些两难。
Mocha的共享上下文对象会在所有可用的hook和test之间共享。而且每个测试结束后,会自动全部清除。正是因为有这样的上下文共享机制,可以在test和hook之间共享变量或别名,要么用闭包 this.*
形式,要么用 .as(*)
这样的形式,实际上cy.get(@*)
相当于 cy.wrap(this.*)
。而且还可以在多层级中共享:
describe('parent', function () {
beforeEach(function () {
cy.wrap('one').as('a')
})
context('child', function () {
beforeEach(function () {
cy.wrap('two').as('b')
})
describe('grandchild', function () {
beforeEach(function () {
cy.wrap('three').as('c')
})
it('can access all aliases as properties', function () {
expect(this.a).to.eq('one') // true
expect(this.b).to.eq('two') // true
expect(this.c).to.eq('three') // true
})
})
})
})
强大的重试机制
Cypress有缺省的重试(re-try)机制,会在执行时进行一些内置的assertion,然后才超时,两种超时缺省值都是4秒:
command操作的timeout
assertion的timeout
Cypress一般只在几个查找DOM元素的命令如 cy.get()
、 find()
、 contains()
上重试,但决不会在一些可能改变应用程序状态的command上重试(比如 click()
等)。但是有些command,比如 eq
就算后面没有紧跟assertion,它也会重试。
你可以修改每个command的timeout值,这个timeout时间会影响到本command和其下联接的所有assertion的超时时间,所以不要在command后面的assertion上人工指定timeout。
cy.get(actSel + phonePage.btnExchangeSel, { timeout: Cypress.env('timeoutLevel1') }).click();
...
cy.location({ timeout: Cypress.env('timeoutLevel2') }).should((loc) => {
expect(loc.toString()).to.eq(Cypress.env('gatewayUrl'));
});
紧接着command的assertion失败后,重试时会重新运行command去查询dom元素然后再次assert,直到超时或成功。多个assertion也一样,每次重试时本次失败的assertion都会把之前成功的assertion顺便再次assert。
.and()
实际上是 .should()
的别名,同样可用于传入callback的方式。
cy.get('.todo-list li') // command
.should('have.length', 2) // assertion
.and(($li) => {
// 2 more assertions
expect($li.get(0).textContent, 'first item').to.equal('todo a')
expect($li.get(1).textContent, 'second item').to.equal('todo B')
})
注意:重试机制只会作用在最后一个command上,解决方法一般有下面两种。
解决方法一 :仅用一个命令来选择元素
// not recommended
// only the last "its" will be retried
cy.window()
.its('app') // runs once
.its('model') // runs once
.its('todos') // retried
.should('have.length', 2)
// recommended
cy.window()
.its('app.model.todos') // retried
.should('have.length', 2)
顺便提一下,assertion都最好用最长最准确的定位元素方式,要不然偶尔会出现"detached from the DOM"这样的错误,比如:
cy.get('.list')
.contains('li', 'Hello')
.should('be.visible')
这是因为在 cy.get('.list')
时, .list
被当作了subject存了起来,如果中途DOM发生了变化,就会出现上面的错误了。改为:
cy.contains('.list li', 'Hello')
.should('be.visible')
所以,最好用最精确的定位方式,而不要用方法链的形式。cypress定位元素时不但可以用CSS选择器,还可以用JQuery的方式,比如:
// get first element
cy.get('.something').first()
cy.get('.something:first-child')
// get last element
cy.get('.something').last()
cy.get('.something:last-child')
// get second element
cy.get('.something').eq(1)
cy.get('.something:nth-child(2)')
解决方法二 :在命令后及时再加一个合适的assertion,导致它及时自动重试掉当前元素(即“过程中”元素)
cy
.get('.mobile-nav', { timeout: 10000 })
.should('be.visible')
.and('contain', 'Home')
这样处理后,在判断 .mobile-nav
存在于DOM中、可见、包括Home子串这三种情况下,都会等待最大10秒。
强大的别名机制
用于Fixture
这是最常用的用途,比如:
beforeEach(function () {
// alias the users fixtures
cy.fixture('users.json').as('users')
})
it('utilize users in some way', function () {
// access the users property
const user = this.users[0]
// make sure the header contains the first
// user's name
cy.get('header').should('contain', user.name)
})
数据驱动的自动化测试,就可以考虑用这样的方式读取数据文件。
用于查找DOM
个人很少用这个方式,因为没有带来什么的好处。
// alias all of the tr's found in the table as 'rows'
cy.get('table').find('tr').as('rows')
// Cypress returns the reference to the <tr>'s
// which allows us to continue to chain commands
// finding the 1st row.
cy.get('@rows').first().click()
注意:用alias定议dom时,最好一次精确倒位而不要用命令链的方式,当cy.get参照别名元素时,当参照的元素不存在时,Cypress也会再用生成别名的命令再查询一次。但正如所知的,cypress的re-try机制只会在最近yield的subject上才起作用,所以一定要用一次精确倒位的选择元素方式。
cy.get('#nav header .user').as('user') (good)
cy.get('#nav').find('header').find('.user').as('user') (bad)
用于Router
用来设置桩
cy.server()
// we set the response to be the activites.json fixture
cy.route('GET', 'activities/*', 'fixture:activities.json')
cy.server()
cy.fixture('activities.json').as('activitiesJSON')
cy.route('GET', 'activities/*', '@activitiesJSON')
等待xhr的回应
cy.server()
cy.route('activities/*', 'fixture:activities').as('getActivities')
cy.route('messages/*', 'fixture:messages').as('getMessages')
// visit the dashboard, which should make requests that match
// the two routes above
cy.visit('http://localhost:8888/dashboard')
// pass an array of Route Aliases that forces Cypress to wait
// until it sees a response for each request that matches
// each of these aliases
cy.wait(['@getActivities', '@getMessages'])
cy.server()
cy.route({
method: 'POST',
url: '/myApi',
}).as('apiCheck')
cy.visit('/')
cy.wait('@apiCheck').then((xhr) => {
assert.isNotNull(xhr.response.body.data, '1st API call has data')
})
cy.wait('@apiCheck').then((xhr) => {
assert.isNotNull(xhr.response.body.data, '2nd API call has data')
})
断言HXR的回应内容
// 先侦听topay请求
cy.server();
cy.route({
method: 'POST',
url: Cypress.env('prePaymentURI'),
}).as('toPay');
...
// 从topay请求中获取网关单
cy.wait('@toPay').then((xhr) => {
expect(xhr.responseBody).to.have.property('data');
cy.log(xhr.responseBody.data);
let reg = /name="orderno" value="(.*?)"/;
kpoOrderId = xhr.responseBody.data.match(reg)[1];
cy.log(kpoOrderId);
// 自定义命令去成功支付
cy.paySuccess(kpoOrderId);
// 校验订单情况
validateOrderItem(productName, gameCount);
});
...
环境
Cypress的环境相关机制是分层级、优先级的,后面的会覆盖前面的方式。
如果在cypress.json中有一个 env
key后,它的值可以用 Cypress.env()
获取出来:
// cypress.json
{
"projectId": "128076ed-9868-4e98-9cef-98dd8b705d75",
"env": {
"foo": "bar",
"some": "value"
}
}
Cypress.env() // {foo: 'bar', some: 'value'}
Cypress.env('foo') // 'bar'
如果直接放在cypress.env.json后,会覆盖掉cypress.json中的值。这样可以把cypress.env.json放到 .gitignore
文件中,每个环境都将隔离:
// cypress.env.json
{
"host": "veronica.dev.local",
"api_server": "http://localhost:8888/api/v1/"
}
Cypress.env() // {host: 'veronica.dev.local', api_server: 'http://localhost:8888/api/v1'}
Cypress.env('host') // 'veronica.dev.local'
以 CYPRESS_
或 cypress_
打头的环境变量,Cypress会自动处理:
export CYPRESS_HOST=laura.dev.local
export cypress_api_server=http://localhost:8888/api/v1/
Cypress.env() // {HOST: 'laura.dev.local', api_server: 'http://localhost:8888/api/v1'}
Cypress.env('HOST') // 'laura.dev.local'
Cypress.env('api_server') // 'http://localhost:8888/api/v1/'
最后,还是要以用 --env
环境变量来指定环境变量:
cypress run --env host=kevin.dev.local,api_server=http://localhost:8888/api/v1
Cypress.env() // {host: 'kevin.dev.local', api_server: 'http://localhost:8888/api/v1'}
Cypress.env('host') // 'kevin.dev.local'
Cypress.env('api_server') // 'http://localhost:8888/api/v1/'
环境还能在在 plugins/index.js
中处理:
module.exports = (on, config) => {
// we can grab some process environment variables
// and stick it into config.env before returning the updated config
config.env = config.env || {}
// you could extract only specific variables
// and rename them if necessary
config.env.FOO = process.env.FOO
config.env.BAR = process.env.BAR
console.log('extended config.env with process.env.{FOO, BAR}')
return config
}
// 在spec文件中,都用Cypress.env()来获取
it('has variables FOO and BAR from process.env', () => {
// FOO=42 BAR=baz cypress open
// see how FOO and BAR were copied in "cypress/plugins/index.js"
expect(Cypress.env()).to.contain({
FOO: '42',
BAR: 'baz'
})
})
自定义Command
自定义一些命令可以简化测试脚本,以下举两个例子。
简化选择元素
Cypress.Commands.add('dataCy', (value) => cy.get(`[data-cy=${value}]`))
...
it('finds element using data-cy custom command', () => {
cy.visit('index.html')
// use custom command we have defined above
cy.dataCy('greeting').should('be.visible')
})
模拟登录
Cypress.addParentCommand("login", function(email, password){
var email = email || "joe@example.com"
var password = password || "foobar"
var log = Cypress.Log.command({
name: "login",
message: [email, password],
consoleProps: function(){
return {
email: email,
password: password
}
}
})
cy
.visit("/login", {log: false})
.contains("Log In", {log: false})
.get("#email", {log: false}).type(email, {log: false})
.get("#password", {log: false}).type(password, {log: false})
.get("button", {log: false}).click({log: false}) //this should submit the form
.get("h1", {log: false}).contains("Dashboard", {log: false}) //we should be on the dashboard now
.url({log: false}).should("match", /dashboard/, {log: false})
.then(function(){
log.snapshot().end()
})
})
模拟支付请求
Cypress.Commands.add("paySuccess", (kpoOrderId, overrides = {}) => {
const log = overrides.log || true;
const timeout = overrides.timeout || Cypress.config('defaultCommandTimeout');
Cypress.log({
name: 'paySuccess',
message: 'KPO order id: ' + kpoOrderId
});
const options = {
log: log,
timeout: timeout,
method: 'POST',
url: Cypress.env('paymentUrl'),
form: true,
qs: {
orderNo: kpoOrderId,
notifyUrl: Cypress.env('notifyUrl'),
returnUrl: Cypress.env('returnUrl') + kpoOrderId,
},
};
Cypress._.extend(options, overrides);
cy.request(options);
});
...
// 在spec文件中
// 从页面获取网关单
cy.get('input[name="orderNo"]').then(($id) => {
kpoOrderId = $id.val();
cy.log(kpoOrderId);
// 自定义命令去成功支付
cy.paySuccess(kpoOrderId);
// 校验订单情况
// 因为cypress所有命令都是异步的,所以只能放在这,不能放到then之外!
validateOrderItem(productName, gameCount);
});
Cookie处理
Cookies.debug()
允许在cookie被改变时,会记录日志在console上:
Cypress.Cookies.debug(true)
Cypress会在每个test运行前自动的清掉所有的cookie。但可以用 preserveOnce()
来在多个test之间保留cookie,这在有登录要求的自动化测试方面很方便。
describe('Dashboard', function () {
before(function () {
cy.login()
})
beforeEach(function () {
// before each test, we can automatically preserve the
// 'session_id' and 'remember_token' cookies. this means they
// will not be cleared before the NEXT test starts.
Cypress.Cookies.preserveOnce('session_id', 'remember_token')
})
it('displays stats', function () {
})
it('can do something', function () {
})
})
最后,也可以用全局白名单来让Cypress不在每个test前清cookie。
登录的几种方法
Cypress可以直接处理cookie,所以直接表单登录和模拟POST请求就可以登录了。如果是cookie/session方式,还要留意要在test之间手工保留cookie,请见Cookie处理部分。
// 直接提交表单,凭证入cookie中,登录成功
it('redirects to /dashboard on success', function () {
cy.get('input[name=username]').type(username)
cy.get('input[name=password]').type(password)
cy.get('form').submit()
// we should be redirected to /dashboard
cy.url().should('include', '/dashboard')
cy.get('h1').should('contain', 'jane.lane')
// and our cookie should be set to 'cypress-session-cookie'
cy.getCookie('cypress-session-cookie').should('exist')
})
// 自定义命令发请求,但还有csrf隐藏域
Cypress.Commands.add('loginByCSRF', (csrfToken) => {
cy.request({
method: 'POST',
url: '/login',
failOnStatusCode: false, // dont fail so we can make assertions
form: true, // we are submitting a regular form body
body: {
username,
password,
_csrf: csrfToken // insert this as part of form body
}
})
})
// csrf在返回的html中
it('strategy #1: parse token from HTML', function(){
// if we cannot change our server code to make it easier
// to parse out the CSRF token, we can simply use cy.request
// to fetch the login page, and then parse the HTML contents
// to find the CSRF token embedded in the page
cy.request('/login')
.its('body')
.then((body) => {
// we can use Cypress.$ to parse the string body
// thus enabling us to query into it easily
const $html = Cypress.$(body)
const csrf = $html.find("input[name=_csrf]").val()
cy.loginByCSRF(csrf)
.then((resp) => {
expect(resp.status).to.eq(200)
expect(resp.body).to.include("<h2>dashboard.html</h2>")
})
})
...
})
// 如果csrf在响应头中
it('strategy #2: parse token from response headers', function(){
// if we embed our csrf-token in response headers
// it makes it much easier for us to pluck it out
// without having to dig into the resulting HTML
cy.request('/login')
.its('headers')
.then((headers) => {
const csrf = headers['x-csrf-token']
cy.loginByCSRF(csrf)
.then((resp) => {
expect(resp.status).to.eq(200)
expect(resp.body).to.include("<h2>dashboard.html</h2>")
})
})
...
})
// 登录凭证不自动存入cookie,需手工操作
describe('Logging in when XHR is slow', function(){
const username = 'jane.lane'
const password = 'password123'
const sessionCookieName = 'cypress-session-cookie'
// the XHR endpoint /slow-login takes a couple of seconds
// we so don't want to login before each test
// instead we want to get the session cookie just ONCE before the tests
before(function () {
cy.request({
method: 'POST',
url: '/slow-login',
body: {
username,
password
}
})
// cy.getCookie automatically waits for the previous
// command cy.request to finish
// we ensure we have a valid cookie value and
// save it in the test context object "this.sessionCookie"
// that's why we use "function () { ... }" callback form
cy.getCookie(sessionCookieName)
.should('exist')
.its('value')
.should('be.a', 'string')
.as('sessionCookie')
})
beforeEach(function () {
// before each test we just set the cookie value
// making the login instant. Since we want to access
// the test context "this.sessionCookie" property
// we need to use "function () { ... }" callback form
cy.setCookie(sessionCookieName, this.sessionCookie)
})
it('loads the dashboard as an authenticated user', function(){
cy.visit('/dashboard')
cy.contains('h1', 'jane.lane')
})
it('loads the admin view as an authenticated user', function(){
cy.visit('/admin')
cy.contains('h1', 'Admin')
})
})
如果是jwt,可以手工设置jwt
// login just once using API
let user
before(function fetchUser () {
cy.request('POST', 'http://localhost:4000/users/authenticate', {
username: Cypress.env('username'),
password: Cypress.env('password'),
})
.its('body')
.then((res) => {
user = res
})
})
// but set the user before visiting the page
// so the app thinks it is already authenticated
beforeEach(function setUser () {
cy.visit('/', {
onBeforeLoad (win) {
// and before the page finishes loading
// set the user object in local storage
win.localStorage.setItem('user', JSON.stringify(user))
},
})
// the page should be opened and the user should be logged in
})
...
// use token
it('makes authenticated request', () => {
// we can make authenticated request ourselves
// since we know the token
cy.request({
url: 'http://localhost:4000/users',
auth: {
bearer: user.token,
},
})
.its('body')
.should('deep.equal', [
{
id: 1,
username: 'test',
firstName: 'Test',
lastName: 'User',
},
])
})
拖放处理
Cyprss中处理拖放很容易,在test runner中调试也很方便。
用mouse事件
// A drag and drop action is made up of a mousedown event,
// multiple mousemove events, and a mouseup event
// (we can get by with just one mousemove event for our test,
// even though there would be dozens in a normal interaction)
//
// For the mousedown, we specify { which: 1 } because dragula will
// ignore a mousedown if it's not a left click
//
// On mousemove, we need to specify where we're moving
// with clientX and clientY
function movePiece (number, x, y) {
cy.get(`.piece-${number}`)
.trigger('mousedown', { which: 1 })
.trigger('mousemove', { clientX: x, clientY: y })
.trigger('mouseup', {force: true})
}
用拖放事件
// 定义拖放方法
function dropBallInHoop (index) {
cy.get('.balls img').first()
.trigger('dragstart')
cy.get('.hoop')
.trigger('drop')
}
// 或手工处理
it('highlights hoop when ball is dragged over it', function(){
cy.get('.hoop')
.trigger('dragenter')
.should('have.class', 'over')
})
it('unhighlights hoop when ball is dragged out of it', function(){
cy.get('.hoop')
.trigger('dragenter')
.should('have.class', 'over')
.trigger('dragleave')
.should('not.have.class', 'over')
})
动态生成test
动态生成test,经常出现在数据驱动的场合下
用不同的屏幕测试
// run the same test against different viewport resolution
const sizes = ['iphone-6', 'ipad-2', [1024, 768]]
sizes.forEach((size) => {
it(`displays logo on ${size} screen`, () => {
if (Cypress._.isArray(size)) {
cy.viewport(size[0], size[1])
} else {
cy.viewport(size)
}
cy.visit('https://www.cypress.io')
cy.get(logoSelector).should('be.visible')
})
})
})
数据驱动,数据可以来源于fixture,也可以来源于实时请求或task!
context('dynamic users', () => {
// invoke:Invoke a function on the previously yielded subject.
before(() => {
cy.request('https://jsonplaceholder.cypress.io/users?limit=3')
.its('body')
.should('have.length', 10)
.invoke('slice', 0, 3)
.as('users')
// the above lines "invoke" + "as" are equivalent to
// .then((list) => {
// this.users = list.slice(0, 3)
// })
})
describe('fetched users', () => {
Cypress._.range(0, 3).forEach((k) => {
it(`# ${k}`, function () {
const user = this.users[k]
cy.log(`user ${user.name} ${user.email}`)
cy.wrap(user).should('have.property', 'name')
})
})
})
})
// plugins/index.js中
module.exports = (on, config) => {
on('task', {
getData () {
return ['a', 'b', 'c']
},
})
}
...
context('generated using cy.task', () => {
before(() => {
cy.task('getData').as('letters')
})
describe('dynamic letters', () => {
it('has fetched letters', function () {
expect(this.letters).to.be.an('array')
})
Cypress._.range(0, 3).forEach((k) => {
it(`tests letter #${k}`, function () {
const letter = this.letters[k]
cy.wrap(letter).should('match', /a|b|c/)
})
})
})
})
杂项
- fixture文件不会被test runner 的"live mode" watched到,修改它后,只能手工重新跑测试。
- Cypress只能检查到Ajax请求,不能检查到Fetch和其它比如<script>发出的请求。这是个很大的问题,一直没有解决。要
polyfill
掉Fetch API才行。但这样实际上对被测应用有些改动了。
// 先npm install cypress-unfetch
// 再在supports/index.js中import即可
import '@rckeller/cypress-unfetch'
- Cypress的command返回的不是promise,而是包装过的。但可以用
then
把yield的subject传递下去。而且在then()
中得到的是最近的一个command里yield的subject。可以用.end()
来结束这个yield链,让其后的Command不收到前面yield的subject:
cy
.contains('User: Cheryl').click().end() // yield null
.contains('User: Charles').click() // contains looks for content in document now
其实,完全可以都用 cy.
重新开始一个新的调用链
cy.contains('User: Cheryl').click()
cy.contains('User: Charles').click() // contains looks for content in document now
- 当指定baseUrl配置项后,Cypress会忽略掉
cy.visit()
或cy.request()
中的url。当没有baseUrl配置项设定时,Cypress会用localhost加随机端口的方式来先运行,然后遇一以了cy.visit()
或cy.request()
会再变化请求的url,这样会有一点点闪烁或reload的情况。所以指定的baseUrl后,能节省点启动时间。 - 录制video现在只能被Electron这个浏览器支持。
- 判断元素是否存在,走不同的条件逻辑,用Cypress内置的JQuery:
const $el = Cypress.$('.greeting')
if ($el.length) {
cy.log('Closing greeting')
cy.get('.greeting')
.contains('Close')
.click()
}
cy.get('.greeting')
.should('not.be.visible')
- 测试对象可以用
this.test
访问,测试的的名字可以用this.test.title
获得,但在hook中它却是hook的名字! - 测试脚本是执行在浏览器中的,包括引入的javascript模块。Cypress用babel和browserify预处理它们。Cypress只能和
cy.task()
或cy.exec()
分别执行Node和Shell。但是要小心:只有task
或exec
执行终止后,Cypress才会继续运行! -
contains
断言可以跨多个简单的文本形的元素,比如<span>或<strong>,也能成功
cy.get('.todo-count').contains('3 items left')
-
within
在定位层级多的元素时非常好用
cy.get(orderlistPage.orderItemRootSel, { timeout: Cypress.env('timeoutLevel3') }).first().within(($item) => {
cy.get(orderlistPage.orderItemProductNameSel).should('have.text', productName);
cy.get(orderlistPage.orderItemGameCountSel).first().should('have.text', gameCount);
cy.get(orderlistPage.orderItemStateSel).should('have.text', orderState);
})
- 把
fixture
文档可以当作模块来引入
const allProductes = require('../../fixtures/product');
- 忽略页面未处理异常
// ignore errors from the site itself
Cypress.on('uncaught:exception', () => {
return false
})
- hook可以放到
supports/*.js
中 - 给window加入定制属性,test中可以用
cy.window
得到window对象
it('can modify window._bootstrappedData', function () {
const data = {
...
}
cy.visit('/bootstrap.html', {
onBeforeLoad: (win) => {
win._bootstrappedData = data
},
})
...
- 底层交互元素
it('updates range value when moving slider', function(){
// To interact with a range input (slider), we need to set its value and
// then trigger the appropriate event to signal it has changed
// Here, we invoke jQuery's val() method to set the value
// and trigger the 'change' event
// Note that some implementations may rely on the 'input' event,
// which is fired as a user moves the slider, but is not supported
// by some browsers
cy.get('input[type=range]').as('range')
.invoke('val', 25)
.trigger('change')
cy.get('@range').siblings('p')
.should('have.text', '25')
})
- 处理不可交互的元素时,可以给Command传递
{force: true}
这个选项