Cypressテストを効率化!.then()の落とし穴とベストプラクティス

2025-05-21

簡単に言うと、.then()は「前のCypressコマンドの結果を受け取って、その結果を使って次の処理を行う」ために使われます。

.then()の主な機能と使い方

  1. 前のコマンドの「Subject」(主題)にアクセスする Cypressのコマンドは、それぞれ何らかの「Subject」を次のコマンドに渡します。例えば、cy.get('button')はボタンのDOM要素をSubjectとして渡します。.then()を使うと、このSubjectを引数として受け取るコールバック関数を定義できます。

    cy.get('button') // ボタン要素がSubjectとなる
      .then(($button) => { // $buttonでボタン要素にアクセスできる
        // ここで$buttonを使った処理を行う
        const buttonText = $button.text();
        cy.log(`ボタンのテキストは: ${buttonText}`);
      });
    

    慣例として、jQueryのDOM要素をラップしたものは$を頭につけた変数名(例: $button)で受け取ることが多いです。

  2. 非同期処理を同期的に制御する Cypressのコマンドは非同期にキューに追加され、順番に実行されます。.then()は、前のコマンドが完了するまで待機し、その結果が利用可能になってから次の処理を実行するように、コマンドの実行順序を制御するのに役立ちます。

  3. 新しいSubjectを次のコマンドに渡す .then()のコールバック関数から値をreturnすると、その値が新しいSubjectとなり、コマンドチェーンの次のコマンドに渡されます。

    cy.get('input[name="username"]')
      .type('testuser')
      .then(() => {
        // ここでは前のcy.typeの結果は不要なので引数は省略
        return '新しいユーザー名'; // 新しいSubjectとして文字列を返す
      })
      .then((newSubject) => { // newSubjectには'新しいユーザー名'が入る
        cy.log(newSubject); // '新しいユーザー名' がログに出力される
      });
    

    もし、コールバック関数からCypressコマンド(例: cy.get('...'))を返すと、Cypressはそのコマンドが解決するのを待ち、そのコマンドがyieldする値を新しいSubjectとします。Promiseを返す場合も同様です。

  4. Promiseのように使う Cypressの.then()は、JavaScriptのネイティブなPromiseの.then()と非常に似た動作をします。非同期操作の結果を処理するのに適しています。

Cypressでは、.then().should()がよく似た状況で使われることがありますが、重要な違いがあります。

  • .then(): 主に値の操作や、前のコマンドの結果を使った複雑なロジックの実行のために使われます。アサーションもできますが、リトライ機能はありません。つまり、.then()内のアサーションは一度しか実行されません。

    cy.get('.item-count')
      .then(($el) => {
        const count = parseInt($el.text());
        if (count > 0) {
          cy.get('.clear-button').click(); // カウントが0より大きければクリアボタンをクリック
        }
      });
    
  • .should(): 主に**アサーション(検証)**のために使われます。.should()は、条件が満たされるまで自動的に再実行を試みます(リトライ機能)。DOM要素の存在確認やテキストの内容確認など、状態の変化を待ってから検証したい場合に最適です。

    cy.get('.message').should('contain', '成功しました'); // メッセージが表示されるまで待機して検証
    

Cypressの.then()は、以下のような場合に非常に役立ちます。

  • コールバック関数内でカスタムロジックを実行し、その結果を次のコマンドに渡したい場合。
  • 非同期処理の実行順序を制御し、前のステップが完了してから次のステップに進みたい場合。
  • 前のCypressコマンドによって得られた値(Subject)にアクセスし、その値を使って追加の処理を行いたい場合。


Cypress detected that you invoked one or more cy commands in a custom command but returned a different value. または Cypress detected that you returned a promise from a command while also invoking one or more cy commands in that promise.

エラーの状況:** .then()のコールバック関数内でcyコマンドを呼び出しつつ、同時に別の値(Promiseを含む)をreturnしようとすると発生します。Cypressは、cyコマンドはそれ自体がCypressコマンドチェーンの一部として処理されるべきだと考えており、.then()のコールバックからcyコマンドを返す場合は、そのコマンドが解決するのを待ち、そのコマンドが生成するSubjectを次のチェーンに渡します。しかし、cyコマンドと他の値を同時に返そうとすると、CypressがどちらをSubjectとして扱うべきか判断できなくなり、このエラーが発生します。

例 (間違ったコード)

cy.get('button')
  .then(($button) => {
    cy.log($button.text()); // cyコマンド
    return 'some string';    // 別の値を返す
  });

解決策

  • .then()のコールバックから値を返したい場合は、cyコマンドを呼び出さない、またはcyコマンドの結果をreturnする。
  • .then()のコールバック内でcyコマンドを呼び出した場合は、そのコマンドをreturnしない。 Cypressは、cyコマンドが呼び出されたことを検出し、自動的にコマンドキューに追加します。

例 (正しいコード)

  1. cy.get('button')
      .then(($button) => {
        cy.log($button.text()); // これでOK
      });
    
  2. cyコマンドの結果をreturnし、それが新しいSubjectとなる場合

    cy.get('button')
      .then(($button) => {
        // 例として、別の要素を取得するcyコマンドを返す
        return cy.get('.some-other-element'); 
      })
      .then(($otherElement) => {
        // $otherElementには、上記で取得した要素が入る
        cy.log($otherElement.text());
      });
    
  3. 純粋なJavaScriptの値を返したい場合 (その中でcyコマンドを呼び出さない)

    cy.get('input').type('hello')
      .then(() => {
        return 'typed text'; // 新しいSubjectとなる
      })
      .then((subject) => {
        cy.log(subject); // 'typed text' が表示される
      });
    

Cypress commands cannot be chained off of a value that was not yielded by a previous Cypress command. または Cannot call "cy.get()" outside a running test.

エラーの状況
cyコマンドは、Cypressのコマンドチェーンのコンテキスト内でのみ実行される必要があります。.then()のコールバック内で取得したDOM要素などを、直接cyコマンドの引数として渡したり、Cypressのコマンドチェーンの外でcyコマンドを呼び出そうとすると、このエラーが発生します。

例 (間違ったコード)

let myElement;
cy.get('.my-div')
  .then(($el) => {
    myElement = $el; // DOM要素を変数に格納
  });

// これはCypressコマンドチェーンの外なのでエラーになる可能性が高い
cy.get(myElement).click(); 

この例では、myElementにDOM要素が格納された時点ではCypressのコマンドキューはまだ終わっていない可能性があり、またcy.get(myElement)自体が新しいコマンドチェーンの始まりとしてCypressのコンテキスト外で実行されるため、エラーになります。

解決策
.then()のコールバック内で取得した値を使って、さらにCypressコマンドを実行したい場合は、そのコールバック内ですべてのCypressコマンドをチェーンとして実行するか、cy.wrap()を使用してCypressのSubjectとしてラップします。

例 (正しいコード)

  1. .then()内で全てを処理する

    cy.get('.my-div')
      .then(($el) => {
        // $elはjQueryオブジェクトなので、そのまま操作可能
        $el.click(); 
        // または、その要素から更にCypressコマンドで要素を探す
        cy.wrap($el).find('button').click(); 
      });
    
  2. cy.wrap()を使用してSubjectを次のチェーンに渡す

    cy.get('.my-div')
      .then(($el) => {
        // $elをCypressのSubjectとしてラップし、次のコマンドに渡す
        return cy.wrap($el); 
      })
      .then(($wrappedEl) => {
        // $wrappedElはCypressのSubjectなので、さらにCypressコマンドをチェーンできる
        $wrappedEl.find('button').click();
      });
    

    多くの場合、cy.wrap($el)の代わりに $el.find('button').click() と直接記述する方が簡潔です。cy.wrap()は、JavaScriptの変数やPromiseなどをCypressのコマンドチェーンに乗せたい場合に特に有用です。

非同期処理の誤解 (特にJavaScriptのasync/awaitと混同)

エラーの状況
JavaScriptのasync/awaitに慣れている開発者は、Cypressのコマンドも同様に動作すると誤解しがちです。しかし、CypressのコマンドはPromiseのように見えても、実際には内部的なコマンドキューに追加されるため、通常のJavaScriptの非同期処理とは少し異なります。

例 (間違ったコードの意図)

// これはCypressでは動きません
async function doSomething() {
  const element = await cy.get('div'); // awaitはcyコマンドでは使えない
  element.click();
}

解決策
Cypressでは、.then()を使ってコマンドの実行順序を制御します。async/awaitはCypressコマンドに対して直接は使用できません。

// Cypressの正しい書き方
cy.get('div')
  .then(($div) => {
    $div.click();
  });

もし、async/awaitをどうしても使いたい場合は、.then()のコールバック内でネイティブのPromiseを返すことで実現できますが、これはCypressのテストコードを複雑にする可能性があり、推奨されない場合が多いです。

DOM要素がデタッチされる問題 (element detached from DOM)

エラーの状況
.then()内でDOM要素にアクセスし、その要素に対して後続のCypressコマンドを実行しようとした際に、その要素がDOMから消えてしまう(再レンダリングなどにより)ことがあります。これは特に、SPA (Single Page Application) などでページの一部が動的に更新される場合に起こりやすいです。


cy.get('.item-list li:first') // 最初のリストアイテムを取得
  .then(($firstItem) => {
    // ここで何らかの操作(例: データをロードするボタンをクリック)
    cy.get('.load-more-button').click(); 

    // 問題: ロード後にリストが再レンダリングされ、$firstItemがDOMから消える可能性がある
    cy.wrap($firstItem).click(); // これがエラーになるかも
  });

解決策

  • .should()を使って、要素の再アタッチや可視性を待つ。
  • .then()内で取得したDOM要素を直接使用する代わりに、必要な時点で再度cy.get()で要素を取得する。 Cypressのcy.get()は、デフォルトで要素が存在し、インタラクト可能になるまでリトライします。

例 (正しいコード)

  1. 必要な時点で再取得

    cy.get('.item-list li:first') 
      .then(($firstItem) => {
        cy.get('.load-more-button').click(); 
      })
      .then(() => {
        // リロード後に再度最初のリストアイテムを取得
        cy.get('.item-list li:first').click(); 
      });
    

    この場合、$firstItemthenの内部でしか使えないので、このような書き方になります。

  2. cy.wrap()とshould()でDOMへの再アタッチを待つ
    cy.wrap()に要素を渡した場合、Cypressはその要素を内部的に再クエリしようとします。.should('be.visible')などのアサーションと組み合わせることで、要素が再びDOMに存在し、可視になるまで待機させることができます。

    cy.get('.item-list li:first')
      .then(($firstItem) => {
        cy.get('.load-more-button').click(); 
        // $firstItemがDOMに再アタッチされ、可視になるまで待つ
        cy.wrap($firstItem).should('exist').and('be.visible').click(); 
      });
    

エラーの状況
.then()のコールバック関数内でMochaやChaiのアサーション(例: expect(value).to.equal(expected))を使用した場合、そのアサーションは一度しか実行されません。もし、アサーションの対象となる値が非同期的に変化するもので、その変化を待たずにアサーションが実行されると、テストが失敗します。これは.should()のリトライ機能と混同されがちです。

例 (間違ったコード)

cy.get('.dynamic-value')
  .then(($el) => {
    // 値がまだ更新されていない可能性がある
    expect($el.text()).to.equal('Expected Value'); 
  });

解決策
値の検証や、非同期的な変化を待つ必要がある場合は、必ず.should()を使用するか、またはカスタムコマンドでリトライロジックを実装します。

例 (正しいコード)

cy.get('.dynamic-value')
  .should('have.text', 'Expected Value'); // Cypressが自動的にリトライしてくれる

もし.then()内で複雑なロジックを処理しつつ、その中で得られた値に対してアサーションを行いたい場合は、以下のように.then()の最後に.should()をチェーンします。

cy.get('.some-element')
  .invoke('text') // 要素のテキストを取得
  .then((text) => {
    // テキストに対して何らかの処理を行う(例: 数値に変換)
    const parsedNumber = parseInt(text);
    return parsedNumber * 2; // 新しいSubjectとして返す
  })
  .should('equal', 10); // 上記の計算結果が10であることを検証(リトライあり)


前のコマンドのSubjectにアクセスする

describe('Then の基本的な使い方', () => {
  beforeEach(() => {
    // テストページにアクセス(例として簡単なHTMLページを想定)
    // index.html: <body><h1 id="page-title">Welcome</h1><button id="my-button">Click Me</button></body>
    cy.visit('index.html'); 
  });

  it('h1要素のテキストを取得し、ログに出力する', () => {
    cy.get('#page-title') // h1要素がSubjectとなる
      .then(($h1) => {   // $h1 でh1要素(jQueryラッパー)にアクセス
        const titleText = $h1.text(); // jQueryの.text()メソッドでテキストを取得
        cy.log(`ページタイトル: ${titleText}`); // Cypressのログに出力
        expect(titleText).to.equal('Welcome'); // Mocha/Chaiでアサート
      });
  });

  it('ボタンのテキストを取得し、条件によってクリックする', () => {
    cy.get('#my-button') // ボタン要素がSubjectとなる
      .then(($button) => { // $button でボタン要素にアクセス
        const buttonText = $button.text();
        cy.log(`ボタンのテキスト: ${buttonText}`);

        if (buttonText === 'Click Me') {
          cy.wrap($button).click(); // jQueryオブジェクトをCypressコマンドに乗せるためにcy.wrap()を使用
          // または、単に $button.click(); でも良い場合が多い
        }
      });
  });
});

ポイント

  • 受け取ったjQueryオブジェクトをCypressコマンドに引き渡したい場合は、cy.wrap($element)を使用すると便利です。
  • .then()のコールバック内で、受け取ったSubjectに対してjQueryのメソッド(.text(), .click(), .find()など)を直接呼び出すことができます。
  • $h1$button のように$をつけた変数名で受け取るのは、それがjQueryオブジェクト(CypressがDOM要素をラップしたもの)であることを示す慣例です。

.then()から新しいSubjectを返す

.then()のコールバック関数から値をreturnすると、その値が次のCypressコマンドのSubjectとして渡されます。これにより、コマンドチェーンを継続できます。

describe('Then から新しいSubjectを返す', () => {
  beforeEach(() => {
    // index.html: <body><input id="user-input" value="initial value"><p id="display"></p></body>
    cy.visit('index.html');
  });

  it('inputの現在の値を取得し、それを加工して次のthenに渡す', () => {
    cy.get('#user-input')
      .invoke('val') // inputの現在の値を取得(Subjectは文字列になる)
      .then((currentValue) => { // currentValue は 'initial value'
        cy.log(`現在の入力値: ${currentValue}`);
        const newValue = currentValue.toUpperCase(); // 値を大文字に変換
        return newValue; // 新しいSubjectとして大文字の文字列を返す
      })
      .then((uppercaseValue) => { // uppercaseValue は 'INITIAL VALUE'
        cy.log(`変換後の入力値: ${uppercaseValue}`);
        expect(uppercaseValue).to.equal('INITIAL VALUE');
      });
  });

  it('要素の数を取得し、その数を表示要素にセットする', () => {
    // index.html に例えば <ul><li>Item 1</li><li>Item 2</li></ul> を追加
    cy.get('li') // 複数のli要素がSubjectとなる(jQueryオブジェクトの集合)
      .then(($lis) => {
        const itemCount = $lis.length; // li要素の数を取得
        return itemCount; // 数値を新しいSubjectとして返す
      })
      .then((count) => { // count は li要素の数
        cy.get('#display').invoke('text', `アイテム数: ${count}`); // p要素にテキストを設定
        cy.get('#display').should('contain.text', `アイテム数: ${count}`);
      });
  });
});

ポイント

  • returnする値は、文字列、数値、オブジェクト、配列など何でも構いません。
  • invoke('val')invoke('text')のように、Cypressのコマンドで特定のプロパティやメソッドの結果をSubjectとして取得すると、それを直接.then()で受け取って処理できます。

.then()内でCypressコマンドをチェーンする

.then()のコールバック内でさらにCypressコマンドを呼び出すことができます。この場合、新しいcyコマンドは既存のコマンドキューに追加され、前のcyコマンドが解決した後に実行されます。

describe('Then 内でCypressコマンドをチェーンする', () => {
  beforeEach(() => {
    // index.html: <body><div id="container"><button id="inner-button">Inner Button</button></div></body>
    cy.visit('index.html');
  });

  it('コンテナ要素を取得し、その中のボタンをクリックする', () => {
    cy.get('#container') // コンテナ要素がSubject
      .then(($container) => {
        // $container(jQueryオブジェクト)を使って、その中の要素を探す
        // ここでcy.wrap()を使ってCypressコマンドとして扱うか、
        // 直接jQueryメソッドを使ってからcy.get()に繋げるなどする
        cy.wrap($container).find('#inner-button').click(); // コンテナ内のボタンをクリック
      });
    
    // ボタンがクリックされた後の状態を検証
    // 例: ボタンがクリックされたら非表示になるなど
    cy.get('#inner-button').should('not.be.visible');
  });

  it('ユーザー情報を取得し、そのIDを使って別のページにアクセスする', () => {
    // ダミーのユーザーAPIをモックする(Cypressの機能)
    cy.intercept('GET', '/api/users/me', { fixture: 'user.json' }).as('getUser');
    // user.json: { "id": "123", "name": "John Doe" }

    cy.visit('/dashboard'); // ダッシュボードページにアクセス(ユーザー情報が必要)

    cy.wait('@getUser') // APIリクエストが完了するのを待つ
      .then((interception) => {
        const userId = interception.response.body.id; // APIレスポンスからIDを取得
        cy.visit(`/users/${userId}`); // そのIDを使って新しいURLにアクセス
      });
    
    cy.url().should('include', '/users/123'); // URLが正しいことを検証
  });
});

ポイント

  • 前のコマンドの結果($containerinterception)を使って、次に実行するコマンドの引数を動的に決定できます。
  • .then()のコールバック内でcy.get(), cy.click(), cy.visit()などの他のCypressコマンドを呼び出すと、それらのコマンドは現在のCypressコマンドチェーンの次のステップとしてキューに追加されます。

.then()内でPromiseを扱う

.then()のコールバック関数からJavaScriptのネイティブなPromiseをreturnすることもできます。CypressはPromiseが解決するのを待ち、解決された値が新しいSubjectとして次のCypressコマンドに渡されます。

describe('Then 内でPromiseを扱う', () => {
  beforeEach(() => {
    cy.visit('index.html'); // シンプルなページを想定
  });

  it('非同期処理の結果を待ってから次のCypressコマンドに進む', () => {
    let someData = '初期データ';

    cy.wrap(null) // ダミーのCypressコマンド(Promiseを返すために必要)
      .then(() => {
        // 外部の非同期処理(例: setTimeoutを使った模擬的な遅延)
        return new Promise((resolve) => {
          setTimeout(() => {
            someData = 'ロードされたデータ';
            cy.log('非同期処理が完了しました');
            resolve(someData); // Promiseを解決し、値を返す
          }, 1000); // 1秒待機
        });
      })
      .then((resolvedData) => { // resolvedData は 'ロードされたデータ'
        cy.log(`Promiseから解決されたデータ: ${resolvedData}`);
        expect(resolvedData).to.equal('ロードされたデータ');
        cy.get('body').should('contain.text', 'ロードされたデータ'); // ページ内にそのデータがあるか確認
      });
  });
});

ポイント

  • cy.wrap(null)は、.then()をチェーンするために必要なダミーのCypressコマンドです。他のCypressコマンドの結果が不要で、すぐにPromiseを返したい場合に便利です。
  • .then()からPromiseを返すことで、ネイティブの非同期処理(APIコール、ファイル読み込みなど)をCypressのコマンドチェーンに組み込むことができます。

.then()は値の操作やロジックの実行に使い、.should()はアサーション(検証)とリトライのために使います。

describe('then と should の違い', () => {
  beforeEach(() => {
    // 動的に更新される要素があるページを想定
    // index.html: <body><p id="status-message">Loading...</p></body>
    cy.visit('index.html');
    // サーバーサイドで1秒後にメッセージが 'Loaded!' に変わると仮定
  });

  it('should を使って要素のテキストが変化するのを待つ', () => {
    // should は条件が満たされるまで自動的にリトライする
    cy.get('#status-message').should('contain.text', 'Loaded!');
    cy.log('メッセージがLoaded!になりました');
  });

  it('then でアサーションを行うとリトライされない(非推奨)', () => {
    cy.get('#status-message')
      .then(($message) => {
        // ここでのexpectは一度しか実行されないため、
        // 読み込みが遅いと 'Loading...' の時点でアサートされ、失敗する
        // したがって、非同期的な値の検証には should を使うべき
        expect($message.text()).to.equal('Loaded!'); // ほとんどの場合、テストは失敗する
      });
  });

  it('then で値を取得し、それをshouldで検証する', () => {
    cy.get('#status-message')
      .invoke('text') // テキストを取得(Subjectは文字列)
      .then((text) => {
        // ここでテキストに対して何らかの処理が可能
        cy.log(`現在のメッセージ: ${text}`);
        return text; // 新しいSubjectとして返す
      })
      .should('equal', 'Loaded!'); // should がリトライしてくれるので、このアサーションは成功する
  });
});
  • 非同期的な値の検証や、DOM要素の状態変化を待つ必要がある場合は、必ず.should()を使用してください。


ここでは、Cypressの.then()の代替手段や、特定の状況で.then()よりも適している可能性のある方法を説明します。

.should() コマンド

これは.then()の最も一般的な代替手段であり、アサーション(検証)とリトライの目的で非常によく使われます。

  • .should()の利点: .should()は、指定された条件が満たされるまでコマンドを自動的に再試行します。これにより、DOMの変化や非同期データの読み込みを待つことができます。
  • .then()の課題: .then()のコールバック関数内で実行されるアサーション(例: expect($el.text()).to.equal('...'))は一度しか実行されません。要素のテキストや状態が非同期的に変化するのを待つ必要がある場合、テストが失敗します。


// then でのリトライなしアサーション(BAD)
cy.get('.status-message')
  .then(($el) => {
    // 要素のテキストが非同期的に変わる場合、ほとんどのケースで失敗する
    expect($el.text()).to.equal('Completed');
  });

// should でのリトライありアサーション(GOOD)
cy.get('.status-message')
  .should('have.text', 'Completed'); // テキストが'Completed'になるまで待機し、リトライする

いつ使うか: 要素の属性、プロパティ、状態、可視性、テキスト内容などを検証したい場合。

.invoke() コマンド

DOM要素のメソッドを呼び出したり、プロパティの値を取得したりする場合に.then()の代わりに使うことができます。

  • .invoke()の利点: invoke()を使うと、jQuery要素やDOM要素のメソッドを直接呼び出し、その戻り値を次のCypressコマンドのSubjectとして渡すことができます。これにより、コードがより簡潔になります。
  • .then()での課題: 特定のDOMメソッド(例: text(), val(), css())の結果を取得するために.then()を使うことがあります。


// then を使った例
cy.get('input[name="username"]')
  .then(($input) => {
    const value = $input.val();
    cy.log(`入力値: ${value}`);
    return value;
  })
  .should('equal', 'testuser');

// invoke を使った代替(より簡潔)
cy.get('input[name="username"]')
  .invoke('val') // input要素の .val() メソッドを呼び出し、その結果が次のSubjectになる
  .then((value) => { // value は入力値の文字列
    cy.log(`入力値: ${value}`);
  })
  .should('equal', 'testuser'); // そのままチェーンして検証も可能

いつ使うか: DOM要素の特定のメソッド(val, text, html, cssなど)の結果を取得したい場合や、要素に対して直接メソッドを呼び出したい場合(例: show(), hide(), focus())。

.its() コマンド

オブジェクトの特定のプロパティの値を取得したい場合に.then()の代わりに使えます。

  • .its()の利点: its()を使うと、Subjectとして渡されたオブジェクトの指定されたプロパティを直接取得し、それを次のCypressコマンドのSubjectとして渡します。
  • .then()での課題: オブジェクト(特にAPIレスポンスやCypressのオブジェクト)の特定のプロパティにアクセスするために.then()を使うことがあります。


// then を使った例
cy.request('/api/data')
  .then((response) => {
    const status = response.status;
    cy.log(`ステータスコード: ${status}`);
    return status;
  })
  .should('equal', 200);

// its を使った代替(より簡潔)
cy.request('/api/data')
  .its('status') // レスポンスオブジェクトの 'status' プロパティを取得し、それが次のSubjectになる
  .then((status) => { // status はステータスコードの数値
    cy.log(`ステータスコード: ${status}`);
  })
  .should('equal', 200);

いつ使うか: APIレスポンス、フィクスチャ、またはCypressコマンドがyieldしたオブジェクトの特定のプロパティにアクセスしたい場合。

カスタムコマンド

特定の.then()のロジックが繰り返し出現する場合、それをカスタムコマンドとしてカプセル化することで、コードの再利用性と可読性を高めることができます。

  • .then()の課題: 複雑な.then()のロジックが複数のテストケースで繰り返されると、コードが冗長になり、保守が難しくなります。


// commands.js に追加
Cypress.Commands.add('getAndLogText', { prevSubject: true }, (subject) => {
  const text = subject.text();
  cy.log(`要素のテキスト: ${text}`);
  return text; // 次のコマンドにテキストを渡す
});

// テストファイルで使う
cy.get('#my-element')
  .getAndLogText() // カスタムコマンドとして呼び出す
  .should('include', 'Expected');

いつ使うか: 複数のテストケースで共通して行われる一連の操作やロジックがある場合。

.then()で取得したjQueryオブジェクトをCypressのSubjectとして再度ラップし、その後にshould()でDOMの状態変化を待つ場合に有効です。特にDOMが再レンダリングされる可能性がある場合に役立ちます。

  • cy.wrap().should()の利点: cy.wrap()は、jQuery要素をCypressのSubjectとして「再活性化」させ、その後のshould()は、その要素がDOMに再アタッチされ、指定された状態になるまでリトライします。
  • .then()の課題: .then()内で取得したDOM要素が、その後の操作(例: API呼び出し、別の要素のクリック)によってDOMからデタッチされると、その要素に対する後続の操作が失敗します。


cy.get('.item-list li:first') // 最初のリストアイテムを取得
  .then(($firstItem) => {
    // 別の操作(例: データを再ロードするボタンをクリック)
    cy.get('.reload-button').click(); 

    // $firstItemがDOMからデタッチされる可能性がある
    // cy.wrap()で再ラップし、shouldで再アタッチと可視性を待つ
    cy.wrap($firstItem)
      .should('exist') // DOMに存在することを確認
      .and('be.visible') // 可視であることを確認
      .click(); // その要素をクリック
  });

いつ使うか: DOM要素が非同期的に再レンダリングされ、以前に取得した要素への参照が失われる可能性がある場合。

.then()は非常に汎用的なコマンドであり、多くの状況で使用できますが、上記のような代替手段は、特定のシナリオにおいてコードをより簡潔に、読みやすく、そして堅牢にするのに役立ちます。

  • DOMのデタッチ問題: cy.wrap().should()の組み合わせを検討する。
  • 繰り返し発生するロジック: カスタムコマンドを作成する。
  • DOMメソッドやプロパティの取得: .invoke().its()を検討する。
  • アサーションとリトライ: 常に.should()を優先する。