こんばんは、Cypressで動的にループさせる実装で小一時間詰まったので記録を残しておきます。
背景
Cypressを使って、画面内の複数要素それぞれに対して一定のテストをしたいとします。例えば、一覧画面の各カード内のリンクを押して詳細画面に遷移し、画面内の文字をアサーションする、これを全てのカードに対して行うイメージです。
僕は、テスト前のbefore()
内で前回のテストで出た副作用を削除するコードを書くときに、このシチュエーションに遭遇しました。(副作用が悩ましいですね、、、
ループさせる部分を適当に書くとこうなりますが、これでは意図した動きになりません
it("awesome e2e test", () => {
cy.visit("https://blog.morifuji-is.ninja/")
const children = cy.get('article').children
for (let index = 0; index < children.length; index++) {
// 詳細画面へ遷移
cy.get('article').eq(index).find("a").click()
cy.get("h1").should('not.be.empty')
// 一覧へ戻る
cy.visit("https://blog.morifuji-is.ninja/")
}
})
it("awesome e2e test", () => {
cy.visit("https://blog.morifuji-is.ninja/")
cy.get('.article').each(($el, index, $list) => {
// 詳細画面へ遷移
cy.wrap($el).find("a").click()
cy.get("h1").should('not.be.empty')
// 一覧へ戻る
cy.visit("https://blog.morifuji-is.ninja/")
})
})
どうすれば良いか
cy.document()
を使いましょう
以下のようにすると意図した動作になります
it("awesome e2e test", () => {
cy.visit("https://blog.morifuji-is.ninja/")
cy.document().then((doc) => {
const articleList = doc.querySelectorAll("article")
articleList.forEach((_, index) => {
cy.get("article").eq(index).find(".post-title").click()
cy.get("h1").should('not.be.empty')
// 一覧へ戻る
cy.visit("https://blog.morifuji-is.ninja/")
})
})
})
うまく動きました😊
なぜこうしないといけないか?
cypressの実行モデル上、そうしなければなりません。 というのもCypressのコマンドは遅延評価されるためです。
なので、間違ったパターンの前者では、cy.get('article').children
は意図した値を返さないため動作しません。
また、間違ったパターンの後者では一見動くように見えますが、cy.each()
を使って要素をループさせる中でcy.visit()
をするとDOMがリセットされてしまいます。なので動作しませんでした。
遅延評価に関しては公式でもわかりやすく詳しく書かれています。ドキュメントはよく読みましょう😫
https://docs.cypress.io/guides/core-concepts/introduction-to-cypress#Mixing-Async-and-Sync-code
検証用に作ったリポジトリです↓