# 變數和別名
:::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()一起使用。將攔截路由別名,你可以:
- 確保應用程序發出預期的請求
- 等待服務器發送響應
- 訪問斷言的實際請求物件
-

這裡有一個別名攔截路由的例子,並等待它完成。
```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/)