# 變數和別名 :::info * 如何處理非同步命令 * 什麼是別名? 以及別名如何能簡化您的代碼 * 為什麼在 Cypress 中很少需要用到變數 * 如何為物件、元素和路由使用別名 ::: --- ## 返回值 Cypress 的新用戶一開始可能會發現使用我們 API 的異步特性很有挑戰性。 :::success **別擔心!** 有多種方法可以引用、比較和利用 Cypress 指令所生成的物件。 一旦你掌握了異步代碼的竅門,你就會意識到你可以同步做所有你能做的事情。 本指南探討了許多常見模式,用於編寫良好的 Cypress 代碼,甚至可以處理最複雜的情況。 ::: 非同步 API 將保留在 JavaScript 中。它們在現在的代碼中隨處可見。 事實上,大多數新瀏覽器的 API 都是非同步的,許多核心 Node 模組也是非同步的。 我們將在下面探討的模式在 Cypress 內外都很有用。 您應該認識到的第一個也是最重要的概念是...... :::danger **返回值** 您不能分配或使用任何 Cypress 命令的返回值。命令被非同步運行。 ::: ```javascript= // 下列程式碼是錯誤的 const button = cy.get('button') const form = cy.get('form') button.click() ``` ## 關閉 想使用 cypress 命令所產生的每的內容,你可以使用 then(). ```javascript= cy.get('button').then(($btn) => { // $btn is the object that the previous // command yielded us }) ``` 如果您熟悉JS的原生 Promise ,Cypress 的.then()使用方式與之相同。您可以在尾段繼續接著.then(). 每個鏈式命令都可以接受到前一命令中的值。 這在可讀性上非常的好。 ```javascript= cy.get('button').then(($btn) => { // store the button's text const txt = $btn.text() // submit a form cy.get('form').submit() // compare the two buttons' text // and make sure they are different cy.get('button').should(($btn2) => { expect($btn2.text()).not.to.eq(txt) }) }) // these commands run after all of the // other previous commands have finished cy.get(...).find(...).should(...) ``` 在.then()所有鏈式命令完成之前,在外部的命令是不會執行的。 :::info 通過使用回調函數,我們創建了一個 閉包。閉包使我們能夠保留引用以引用在先前命令中完成的工作。 ::: ## 除錯 (debugger) 在使用.then()函數時,是使用 debugger 的良好時機. 這可以幫助您了解命令的運行順序。這也使您能夠檢查 Cypress 在每個命令中生成的物件 ```javascript= cy.get('button').then(($btn) => { // inspect $btn <object> debugger cy.get('#countries') .select('USA') .then(($select) => { // inspect $select <object> debugger cy.url().should((url) => { // inspect the url <string> debugger $btn // is still available $select // is still available too }) }) }) ``` ## 變數 在 Cypress 中,您幾乎不需要用到const, let, 或var。使用閉包時您可以訪問物件生產給您的,而無需分配它們。 此規則的一個例外是當您處理可變物件(更改狀態)時。當事情改變狀態時,您通常希望將物件的前一個值與下一個值進行比較。 以下是一個 const 比較的範例. ```javascript= <button>increment</button> you clicked button <span id="num">0</span> times ``` ```javascript= // app code let count = 0 $('button').on('click', () => { $('#num').text((count += 1)) }) ``` ```java= // cypress test code cy.get('#num').then(($span) => { // capture what num is right now const num1 = parseFloat($span.text()) cy.get('button') .click() .then(() => { // now capture it again const num2 = parseFloat($span.text()) // make sure it's what we expected expect(num2).to.eq(num1 + 1) }) }) ``` 使用const的原因是因為對 $span 物件是可變的。 每當您擁有可變的物件,並且嘗試比較它們時,您都需要存儲它們的值。使用const 是一個完美的方法來做到這一點。 ## 別名 使用.then()回調函數來處理前一鍊式中的值是對的, 但當你在 hooks 中執行像是 before 或 beforeEachc 會發生什麼式呢? ```javascript= beforeEach(() => { cy.button().then(($btn) => { const text = $btn.text() }) }) it('does not have access to text', () => { // 我們該如何拿到text ?!?! }) ``` 我們該如何拿到 text ? 我們可以透過 let 寫一些髒code來訪問text :::danger 千萬別這樣做!! 以下程式法只用於示範 ::: ```javascript= describe('a suite', () => { // this creates a closure around // 'text' so we can access it let text beforeEach(() => { cy.button().then(($btn) => { // redefine text reference text = $btn.text() }) }) it('does have access to text', () => { // now text is available to us // but this is not a great solution :( text }) }) ``` 很幸運的是我們無需再使用一些花禮胡俏的寫法, 透過 Cypress 我們可以更好的處理此況 :::success **別介紹名** 別名是 Cypress 中的一個強大的結構,有很多用途。 我們將在下面探討它們的每一項功能。 首先,我們使用別名在 hooks 和 tests 之間共享 objects 。 ::: ## 共享 Context 共享 Context 是別名中最簡單的方法。 使用別名中的.as()命令 來共享您想要的的東西。 讓我們看一下别名的使用範例。 ```javascript= beforeEach(() => { // alias the $btn.text() as 'text' cy.get('button').invoke('text').as('text') }) it('has access to text', function () { this.text // is now available }) ``` 在底層,別名的機本物件和語法是利用 Mocha的context物件, 也就是說,別名也可以利用 this.*. Mocha 自動為我們在每個測試中的所有 hooks 中共享 Context。此外,這些別名和屬性會在每次測試後自動清除。 ```javascript= describe('parent', () => { beforeEach(() => { cy.wrap('one').as('a') }) context('child', () => { beforeEach(() => { cy.wrap('two').as('b') }) describe('grandchild', () => { beforeEach(() => { 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 }) }) }) }) ``` ## 訪問 fixture 共享 Context 中最常見用例子, 是在處理 cy.fixture() 時. 很多時候你可能會在 beforeEach hooks 中加載一個 fixture ,但希望利用測試中的值。 ```javascript= beforeEach(() => { // 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) }) ``` :::danger 注意非同不命令 不要忘記 Cypress 是非同步的! 你不能用 this.*,直到 .as() 命令執行。 ::: ```javascript= it('is not using aliases correctly', function () { cy.fixture('users.json').as('users') // nope this won't work // // this.users is not defined // because the 'as' command has only // been enqueued - it has not run yet const user = this.users[0] }) ``` 我們以前介紹過很多次的原則也適用於這種情況。 如果你想要訪問一個命令產生的結果,你必須在閉包中使用.then()。 ```javascript= // yup all good cy.fixture('users.json').then((users) => { // now we can avoid the alias altogether // and use a callback function const user = users[0] // passes cy.get('header').should('contain', user.name) }) ``` ## 避免使用 :::warning 箭頭涵式 使用此属性訪問别名。如果您在測試中使用箭頭函式或 hook, 那 this.* 將不起作用。 這就是什麼我們的範例都是使用常規的 function () {} 語法, 而不是 () => {} 箭頭涵式 ::: 除了 this.* 語法, 還有另一種方式可以讓你訪問到別名(aliases) cy.get() 是一個能夠使用 @ 特殊語法,來訪問到别名的命令 ```javascript= beforeEach(() => { // alias the users fixtures cy.fixture('users.json').as('users') }) it('utilize users in some way', function () { // use the special '@' syntax to access aliases // which avoids the use of 'this' cy.get('@users').then((users) => { // access the users argument const user = users[0] // make sure the header contains the first // user's name cy.get('header').should('contain', user.name) }) }) ``` 藉由 cy.get()語法, 我們可以避免使用 this 請記住,這兩中方式都有作用,因為它們有不同的情境用法。 當使用 this.users 時, 我們是以同步的方式訪問 然而,當使用cy.get('@users')時,它就變成了一個非同步命令 您可以將cy.get('@users') 視為與cy.wrap(this.users) 相同的操作。 ## 元素 在與DOM元素一起使用時,別名還有其他特殊特徵。 在給DOM元素起了別名之後,可以在以後訪問它們以便重用。 ```javascript= // alias all of the tr's found in the table as 'rows' cy.get('table').find('tr').as('rows') ``` 在内部,Cypress引用<tr>集合作为别名“rows”返回。要在以后引用这些相同的“行”,可以使用cy.get()命令。 ```javascript= // 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() ``` 因為我們在cy.get()中使用了@字符,而不是在DOM中查詢元素,cy.get()尋找一個名為rows的現有別名並返回引用(如果找到它)。 ## 舊元素 在許多單頁面JavaScript應用程序中,DOM不斷地重新呈現應用程序的某些部分。如果在使用別名調用cy.get()時為已從DOM中刪除的DOM元素添加別名,Cypress將自動重新查詢DOM,以再次查找這些元素。 ```javascript= <ul id="todos"> <li> Walk the dog <button class="edit">edit</button> </li> <li> Feed the cat <button class="edit">edit</button> </li> </ul> ``` 讓我們想像一下,當我們單擊.edit按鈕時,我們的<li>將在DOM中重新呈現。它沒有顯示編輯按鈕,而是顯示了一個<input />文本字段,允許您編輯todo。前面的<li>已經從DOM中完全刪除,取而代之的是一個新的<li>。 ```javascript= cy.get('#todos li').first().as('firstTodo') cy.get('@firstTodo').find('.edit').click() cy.get('@firstTodo') .should('have.class', 'editing') .find('input') .type('Clean the kitchen') ``` 當我們引用@firstTodo時,Cypress會檢查它所引用的所有元素是否仍然在DOM中。如果是,則返回那些現有的元素。如果不是,Cypress將會重新執行命令,直到別名被定義。 在我們的例子中,它將重新發出命令:cy.get('#todos li').first()。一切正常,因為找到了新的<li>。 :::warning 通常,重新執行之前的命令會返回您所期望的結果,但並不總是如此。建議您盡快為元素設置別名,而不是進一步使用命令鏈。 - cy.get('#nav header .user').as('user') :heavy_check_mark: (good) - cy.get('#nav').find('header').find('.user').as('user') :warning: (bad) 當有疑問時,您隨時可以發出常規的cy.get()來再次查詢元素。 ::: ## 攔截 別名也可以與cy.intercept()一起使用。將攔截路由別名,你可以: - 確保應用程序發出預期的請求 - 等待服務器發送響應 - 訪問斷言的實際請求物件 - ![](https://docs.cypress.io/_nuxt/img/aliasing-routes.af26c33.jpg) 這裡有一個別名攔截路由的例子,並等待它完成。 ```javascript= cy.intercept('POST', '/users', { id: 123 }).as('postUser') cy.get('form').submit() cy.wait('@postUser').then(({ request }) => { expect(request.body).to.have.property('name', 'Brian') }) cy.contains('Successfully created user: Brian') ``` :::info 新 Cypress? 我們有一個更詳細和全面的路由網路請求指南。 ::: ## Requests 別名也可以用於Requests。 下面是一個將 Requests 別名 並稍後訪問其屬性的範例。 ```javascript= cy.request('https://jsonplaceholder.cypress.io/comments').as('comments') // other test code here cy.get('@comments').should((response) => { if (response.status === 200) { expect(response).to.have.property('duration') } else { // whatever you want to check here } }) }) ``` ## 別名會在每次測試之前重制 注意:所有別名在每次測試前都被重置。一個常見的用戶錯誤是使用before鉤子創建別名。這樣的別名只在第一次測試中起作用! ```javascript= // 🚨 THIS EXAMPLE DOES NOT WORK before(() => { // notice this alias is created just once using "before" hook cy.wrap('some value').as('exampleValue') }) it('works in the first test', () => { cy.get('@exampleValue').should('equal', 'some value') }) // NOTE the second test is failing because the alias is reset it('does not exist in the second test', () => { // there is not alias because it is created once before // the first test, and is reset before the second test cy.get('@exampleValue').should('equal', 'some value') }) ``` 解決方案是在每次測試之前使用 beforeEach hook 創建別名 ```javascript= // ✅ THE CORRECT EXAMPLE beforeEach(() => { // we will create a new alias before each test cy.wrap('some value').as('exampleValue') }) it('works in the first test', () => { cy.get('@exampleValue').should('equal', 'some value') }) it('works in the second test', () => { cy.get('@exampleValue').should('equal', 'some value') }) ``` ## 另請參閱 - 從Cypress自定義命令加載裝置解釋瞭如何加載或導入裝置以在Cypress自定義命令中使用。 (https://glebbahmutov.com/blog/fixtures-in-custom-commands/)