cypress 的錯誤訊息 - the element has become detached or removed from the dom
這個錯誤訊息的分析和解決方案,可以參考 Cypress 的官方文件。
這個錯誤訊息提示我們,我們編寫的 Cypress 程式碼正在同一個“死去”的 DOM 元素互動。
顯然,在真實的使用場景下,一個使用者也無法同這種型別的 UI 元素互動。
看個實際例子:
<body>
<div id="parent">
<button>delete</button>
</div>
</body>
這個 button 被點選之後,會將自己從 DOM 樹中移除:
$('button').click(function () { // when the <button> is clicked // we remove the button from the DOM $(this).remove() })
下列這行測試程式碼會引起錯誤:
cy.get('button').click().parent()
當 cypress 執行到下一個 parent 命令時,檢測到施加該命令的 button 已經從 DOM 樹中移除了,因此會報錯。
解決方案:
cy.get('button').click()
cy.get('#parent')
解決此類問題的指導方針:
Guard Cypress from running commands until a specific condition is met
兩種實現 guard 的方式:
- Writing an assertion
- Waiting on an XHR
看另一個例子:
輸入 clem,從結果列表裡選擇 User clementine , 即所謂的 type head search 效果。
測試程式碼如下:
it('selects a value by typing and selecting', () => { // spy on the search XHR cy.server() cy.route('https://jsonplaceholder.cypress.io/users?term=clem&_type=query&q=clem').as('user_search') // first open the container, which makes the initial ajax call cy.get('#select2-user-container').click() // then type into the input element to trigger search, and wait for results cy.get('input[aria-controls="select2-user-results"]').type('clem{enter}') // select a value, again by retrying command // https://on.cypress.io/retry-ability cy.contains('.select2-results__option', 'Clementine Bauch').should('be.visible').click() // confirm Select2 widget renders the name cy.get('#select2-user-container').should('have.text', 'Clementine Bauch') })
要點:
使用 cy.route 監控某個 XHR, 這裡可以監控相對路徑嗎?
本地測試通過,在 CI 上執行時會遇到下列錯誤:
如何分析這個問題呢?可以使用 pause 操作,讓 test runner 暫停。
// first open the container, which makes the initial ajax call
cy.get('#select2-user-container').click().pause()
當我們點選了 select2 widget 時,會立即觸發一個 AJAX call. 而測試程式碼並不會等待 clem 搜尋請求的返回。它只是一心查詢 "Clementine Bauch" 的 DOM 元素。
// first open the container, which makes the initial ajax call
cy.get('#select2-user-container').click()
// then type into the input element to trigger search,
// and wait for results
cy.get('input[aria-controls="select2-user-results"]').type('clem{enter}')
cy.contains('.select2-results__option',
'Clementine Bauch').should('be.visible').click()
上面的測試在本地執行時大部分時間可能會通過,但在 CI 上它可能會經常失敗,因為網路呼叫速度較慢,瀏覽器 DOM 更新可能較慢。 以下是測試和應用程式如何進入導致 “detached element” 錯誤的競爭條件。
- 測試點選小部件
- Select2 小部件觸發第一個搜尋 Ajax 呼叫。 在 CI 上,此呼叫可能比預期慢。
- 測試程式碼輸入“clem”進行搜尋,這會觸發第二個 AJAX 呼叫。
- Select2 小部件接收對帶有十個使用者名稱的第一個搜尋 Ajax 呼叫的響應,其中一個是“Clementine Bauch”。 這些名稱被新增到 DOM
然後測試搜尋可見的選擇“Clementine Bauch” - 並在初始使用者列表中找到它。
然後,測試執行器將要單擊找到的元素。注意這裡的竟態條件。當第二個搜尋 Ajax 呼叫“term=clem”從伺服器返回時。 Select2 小部件刪除當前的選項列表,只顯示兩個找到的使用者:“Clementine Bauch”和“Clementina DuBuque”。
然後測試程式碼執行 Clem 元素的點選。
Cypress 丟擲錯誤,因為它要單擊的帶有文字“Clementine Bauch”的 DOM 元素不再連結到 HTML 文件; 它已被應用程式從文件中刪除,而 Cypress 仍然引用了該元素。
這就是問題的根源。
下面這段程式碼可以人為地讓這個竟態條件總是觸發:
cy.contains('.select2-results__option',
'Clementine Bauch').should('be.visible')
.pause()
.then(($clem) => {
// remove the element from the DOM using jQuery method
$clem.remove()
// pass the element to the click
cy.wrap($clem)
})
.click()
既然瞭解了竟態條件觸發的根源,修正起來就有方向了。
我們希望測試在繼續之前始終等待應用程式完成其操作。
解決方案:
cy.get('#select2-user-container').click()
// flake solution: wait for the widget to load the initial set of users
cy.get('.select2-results__option').should('have.length.gt', 3)
// then type into the input element to trigger search
// also avoid typing "enter" as it "locks" the selection
cy.get('input[aria-controls="select2-user-results"]').type('clem')
我們通過 cy.get('XXX').should('') 操作,確保在執行 clem 輸入之前,初始的 user list 對應的 AJAX 一定回覆到伺服器上了,否則 select2-options 的長度必定小於 3.
當測試在搜尋框中鍵入“clem”時,應用程式將觸發 Ajax 呼叫,該呼叫返回使用者的子集。 因此,測試需要等待顯示新集合 - 否則它將從初始列表中找到“Clementine Bauch”並遇到detached 錯誤。我們知道只有兩個使用者匹配“clem”,因此我們可以再次確認顯示的使用者數以等待應用程式。
/ then type into the input element to trigger search, and wait for results
cy.get('input[aria-controls="select2-user-results"]').type('clem')
// flake solution: wait for the search for "clem" to finish
cy.get('.select2-results__option').should('have.length', 2)
cy.contains('.select2-results__option', 'Clementine Bauch')
.should('be.visible').click()
// confirm Select2 widget renders the name
cy.get('#select2-user-container')
.should('have.text', 'Clementine Bauch')
如果盲目的在 click 呼叫裡傳入 force:true
的引數,可能會引入新的問題。
更多Jerry的原創文章,盡在:"汪子熙":