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。但是要小心:只有 taskexec 執行終止后,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} 這個選項


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM