1. 程式人生 > 其它 >cypress 的錯誤訊息 - the element has become detached or removed from the dom

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 的方式:

  1. Writing an assertion
  2. 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” 錯誤的競爭條件。

  1. 測試點選小部件
  2. Select2 小部件觸發第一個搜尋 Ajax 呼叫。 在 CI 上,此呼叫可能比預期慢。
  3. 測試程式碼輸入“clem”進行搜尋,這會觸發第二個 AJAX 呼叫。
  4. 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的原創文章,盡在:"汪子熙":