Cypress(e2eテストツール)でDLリンクを開く際のエラーについて

現場でCypressからDLリンクを開くとエラーとなる事象が起きました。今回はそのエラーの発生原因の調査や回避法について記載します。

Cypressの導入

このページを見ている人は既にCypressの導入済みかとは思いますが、導入方法についてもまとめました。

現象

今回の調査の発端になったエラーは以下のようなものになります。

Timed out after waiting 60000ms for your remote page to load.

Your page did not fire its load event within 60000ms.

You can try increasing the pageLoadTimeout value in cypress.json to wait longer.

Browsers will not fire the load event until all stylesheets and scripts are done downloading.

When this load event occurs, Cypress will continue running commands.

上記がCypressでファイルのダウンロードをする際に発生します。今回はこの事象の発生するパターンの整理と回避策について記載していきます。

また、色々と調査した結果、エラーメッセージなどは違いますがダウンロード時に発生するエラー(テスト失敗)があったので、そちらの回避策ついても記載します。

Timed out retrying after 4000ms: expected xxxx to include yyyy

前提

今回の調査や対策はcypressのバージョン9.5.0で実施しています。

調査

ケース1:download属性がない

ダウンロードリンクは通常download属性がついています。つまり以下のような形です。

<a download href="/c/sample.jpeg">img link</a>

これに対して、download属性がない場合、通常ファイルがブラウザ上で表示されます。

しかし、download属性が無くともダウンロードになるケースがあります。ファイルの内容が画面に表示できない場合です。例えばzipファイルなどです。つまり以下のような形です。

<a href="/c/sample.zip">zip link</a>

これはブラウザの設定などにもよりますが、ファイルの拡張子やレスポンスヘッダから判断されます。

Cypressでエラーになるのは後者の「download属性が無くファイルの拡張子などからダウンロードと判断されている」ケースです。

このケースでも、ファイルのダウンロード自体はされるのですが、同時にCypressがページのロード待ちの状態になってしまいます。そしてロード待ち状態が続き、最終的にタイムアウト時間を超えてしまいCypressがエラーと判断してしまいます。

ケース2:リダイレクトが発生している

ブラウザでアクセスしている場合、download属性が設定されていれば、リンク先がリダイレクトしていてもファイルはダウンロードされます。

しかし今回調査した結果、Cypress上で動かしている場合、リダイレクトが発生するとダウンロードではなく、画面遷移してしまうことが分かりました。

そのためダウンロード前提でその後のテストを書いているとテストが失敗してしまいます。

挙動的にはリダイレクト後のリクエストの発生タイミングにズレがあるので、おそらくCypressが元々のリクエストとの対応が取れていないのだと思います。

なお、リダイレクトには301、302、307などの種類がありますが、調べた範囲ではどの場合でも同様の事象となりました。

解決策・回避策

download属性がない場合の対策

システムを改修してdownload属性を付与する

まず、大前提としてCypressのテスト対象は自分たちの構築システムです。

なので基本的にはシステムを改修してdownload属性をつけてしまいましょう。それが正解です。

Cypressでdownload属性を付与する

システム改修が正解ではありますが、都合によりそれができないこともあると思います。ここではCypress上での対処方法を記載します。

方法としては、Cypressでのテスト中にHTMLドキュメント(DOM)を書き換える方法があるので、それを使います。以下のような形です。

  cy.get(id).then(($link) => {
    $link.attr("download", "");
  });

Cypressの公式サイトにも載っていますが、cy.getで要素を取得した後、thenを使うと取得した要素に対して操作ができます。それを使って、download属性を付与しています。

Cypressでloadイベントを強制的に発生させる

一つ前の方法は、そもそもページのロード待ちの状態にさせない方法ですが、この方法は待ち状態になった後にその状態を解除する方法です。

それはloadイベントを強制的に発生させるというものです。以下のような形になります。

cy.intercept(downloadUrl, () => {
  Cypress.$(cy.state("$autIframe")).trigger("load");
});

これはgithubのissue(本ページ最後に記載のもの)の中で記載があった方法に少し手を加えたものです。

cy.interceptでダウンロードのリクエスト発生を監視し、リクエストが発生したらloadイベントをtriggerしています。これでページのロード待ちが解除されます。

なおloadイベントのtrigger部分はissueの議論者の一人が見つけた方法で、公式の情報じゃない点にご注意ください。Cypressのバージョンアップなどで使えなくなる可能性も十分にあります。もしこの方法を使う場合はCypressのバージョンを固定するようにした方が良いと思います。

リダイレクトが発生している場合の対策

システムを改修してリダイレクトが発生しないようにする

download属性の方にも書きましたが、Cypressのテスト対象は自分たちの構築システムです。

なので基本的にはシステムを改修してリダイレクトが発生しないようにするのが良いです。

ただ、そもそもリダイレクトが発生している時点で何かしらの理由があると思うので、この方法での対応が難しいケースも多々あるかなと思います。

リダイレクトのリクエストを破棄する(+擬似的なDLを行う)

リダイレクトが発生し画面遷移が行われてしまうと期待の動きにならないので、リダイレクトが発生するリクエスト自体を破棄してしまうことを考えます。以下のような方法です。

cy.intercept(downloadUrl, (req) => {
  req.destroy();
});

これにより画面遷移が発生しなくなるので、ダウンロード後の操作のテストができるようになります。

ただし、リクエストを破棄しているのでダウンロード内容のテストなどはできなくなります。ダウンロード内容の確認もしたい場合は別途リクエストする必要があります。以下のような形です。

cy.request({
  url: imgLink,
  encoding: "binary",
}).then((response) => {
  const folder = Cypress.config("downloadsFolder");
  cy.writeFile(folder + "/" + fileName, response.body, "binary");
});

上記ではファイルを保存していますが、テスト内容によっては直接レスポンスボディなどをチェックすれば良いと思います。

また、先ほどは分けましたがまとめて記載することもできます。以下のような形です。

cy.get(id).then(($link) => {
  const dlLink = $link.attr("href");
  cy.intercept(dlLink, (req) => {
    req.destroy();
  });

  cy.request({
    url: dlLink,
    encoding: "binary",
  }).then((response) => {
    const folder = Cypress.config("downloadsFolder");
    cy.writeFile(folder + "/" + fileName, response.body, "binary");
  });
});

cy.get(id).click();

以上がリダイレクトのリクエストを破棄するという回避策です。

なお、この方法はリクエストのタイミングが若干ずれるので、リクエストのタイミングをシビアに判断するようなシステムでは使えないかもしれません。ただ、そこまでシビアなものでなければ、基本的には元の動作と近い動きになっているので、問題ないかなと思います。

その他の場合の対策

実は最初に書いた「現場で発生した事象」の原因はこのページで触れたものとは別のものでした。かなり複雑な理由だったのと外部に出せない情報が少し含まれるため、ここでは省いています。

なので、このページを見ている方でも、自分の事象の原因が載っていないという人もいると思います。

ただ、対策としてはこの記事に記載のものをうまく組み合わせて対処ができました。原因が別でもこのページの内容で解決できるかもしれませんので、皆さんの参考になればと思います。

作成物

今回の調査で作成した試作のWebサーバやテストプログラムは以下に置いています。このページでは大きく省いている部分もあるので、実際に動かして見たい方は以下を使ってみてもらえればと思います。

https://github.com/masaki-code/testing/tree/main/cypresse-dl-link

補足

本ページの内容の一部は以下のissueでも取り上げられています。
(本ページ執筆時点ではopenな状態)

https://github.com/cypress-io/cypress/issues/14857

そのため、将来的なCypressのバージョンアップで自然と解決するようになるかもしれません。あるいはissue中の議論にも記載の「ページロードの待機をスキップするオプション」が追加されるかもしれません。

何か進展があったらまた追記したいと思います。

最後に

今回Cypressのダウンロードで発生するエラーについて、「こういうパターンで発生しそう」と思ったものを試して調べてみました。この記事を見ている方で、ここで記載したもの以外で見つけたエラーがあれば調べてみたいと思いますので、コメントいただければと思います。

コメント

タイトルとURLをコピーしました