10日で覚えるCypressDay 3: セレクタとDOM操作
books.chapter 310日で覚えるCypress

Day 3: セレクタとDOM操作

今日学ぶこと

  • CSSセレクタの基本(ID, class, tag, attribute)
  • cy.get() の詳細な使い方
  • data-cy / data-testid 属性によるベストプラクティス
  • DOM走査コマンド(find, parent, children, siblings)
  • 要素の絞り込み(first, last, eq)
  • cy.within() によるスコープ限定
  • クリック・テキスト入力などのDOM操作

CSSセレクタの基本

Cypressでは、CSSセレクタを使って要素を取得します。まずはCSSセレクタの基本を確認しましょう。

flowchart TB
    subgraph Selectors["CSSセレクタの種類"]
        ID["#id\nID セレクタ"]
        CLASS[".class\nクラスセレクタ"]
        TAG["tag\nタグセレクタ"]
        ATTR["[attr=val]\n属性セレクタ"]
    end
    style Selectors fill:#3b82f6,color:#fff
セレクタ 書き方 説明
ID #id #login-btn 一意のIDで要素を取得
クラス .class .btn-primary クラス名で要素を取得
タグ tag button HTMLタグ名で要素を取得
属性 [attr=value] [type="submit"] 属性値で要素を取得
子孫 parent child form input 親の中にある子孫要素
直下の子 parent > child ul > li 直下の子要素のみ
複合 .class[attr] .btn[disabled] 複数条件を組み合わせ
// ID セレクタ
cy.get('#username')

// クラスセレクタ
cy.get('.submit-button')

// タグセレクタ
cy.get('h1')

// 属性セレクタ
cy.get('[type="email"]')
cy.get('[name="password"]')

// 複合セレクタ
cy.get('input[type="text"]')
cy.get('button.primary[type="submit"]')

// 子孫セレクタ
cy.get('.form-group input')
cy.get('nav > ul > li')

cy.get() の詳細な使い方

cy.get() はCypressで最も使用頻度の高いコマンドです。CSSセレクタを受け取り、マッチする要素を返します。

// 基本的な使い方
cy.get('button')           // 全ての<button>要素
cy.get('.error-message')   // class="error-message" の要素
cy.get('#submit')          // id="submit" の要素

// タイムアウトの指定
cy.get('.loading', { timeout: 10000 }) // 最大10秒待機

// 複数要素が返る場合
cy.get('li')               // 全ての<li>要素を取得
cy.get('li').should('have.length', 5)  // 5つの<li>があることを確認

cy.get() と cy.contains() の使い分け

// cy.get() - CSSセレクタで要素を取得
cy.get('.nav-link')

// cy.contains() - テキスト内容で要素を取得
cy.contains('ログイン')
cy.contains('button', '送信')  // <button>の中から「送信」を含むものを取得

// 正規表現も使える
cy.contains(/^合計: \d+$/)
コマンド 用途 特徴
cy.get() CSSセレクタで取得 構造ベースの取得
cy.contains() テキスト内容で取得 ユーザー視点の取得

data-cy / data-testid によるベストプラクティス

CSSクラスやIDは、デザイン変更やリファクタリングで変わる可能性があります。テスト専用の属性を使うことで、テストの安定性が大幅に向上します。

flowchart TB
    subgraph Bad["避けるべきセレクタ"]
        B1["タグ名\n(button)"]
        B2["CSSクラス\n(.btn-primary)"]
        B3["ID\n(#main-btn)"]
    end
    subgraph Good["推奨セレクタ"]
        G1["data-cy\n[data-cy=submit]"]
        G2["data-testid\n[data-testid=submit]"]
    end
    Bad -->|"変更に弱い"| FRAGILE["テストが壊れやすい"]
    Good -->|"変更に強い"| STABLE["テストが安定する"]
    style Bad fill:#ef4444,color:#fff
    style Good fill:#22c55e,color:#fff
    style FRAGILE fill:#ef4444,color:#fff
    style STABLE fill:#22c55e,color:#fff

HTMLにdata-cy属性を追加

<!-- 推奨: テスト専用属性を使う -->
<button data-cy="submit-btn" class="btn btn-primary">送信</button>
<input data-cy="email-input" type="email" class="form-control" />
<div data-cy="error-message" class="alert alert-danger">エラーです</div>

Cypressでの使い方

// data-cy属性で要素を取得
cy.get('[data-cy="submit-btn"]').click()
cy.get('[data-cy="email-input"]').type('user@example.com')
cy.get('[data-cy="error-message"]').should('be.visible')

セレクタ戦略の優先順位

優先度 セレクタ 理由
1(最推奨) [data-cy="..."] テスト専用。他の変更の影響を受けない
2 [data-testid="..."] Testing Libraryとの互換性あり
3 #id 一意だが、JSやCSSで使われることがある
4 .class デザイン変更で壊れる可能性あり
5(非推奨) tag 対象が広すぎて不安定

Cypress公式は data-cy 属性の使用を推奨しています。テストコードと本番コードの関心を明確に分離できます。


DOM走査コマンド

cy.get() で取得した要素を起点に、DOMツリーを走査するコマンドを見ていきましょう。

flowchart TB
    subgraph DOM["DOM ツリー"]
        PARENT["parent()\n親要素"]
        CURRENT["現在の要素"]
        CHILD1["children()\n子要素1"]
        CHILD2["children()\n子要素2"]
        SIBLING["siblings()\n兄弟要素"]
        FIND["find()\n子孫要素"]
    end
    PARENT --> CURRENT
    CURRENT --> CHILD1
    CURRENT --> CHILD2
    CURRENT --- SIBLING
    CHILD1 --> FIND
    style CURRENT fill:#3b82f6,color:#fff
    style PARENT fill:#8b5cf6,color:#fff
    style CHILD1 fill:#22c55e,color:#fff
    style CHILD2 fill:#22c55e,color:#fff
    style SIBLING fill:#f59e0b,color:#fff
    style FIND fill:#22c55e,color:#fff

cy.find() - 子孫要素を検索

// cy.get() との違い: find() は現在の要素内から検索する
cy.get('.user-card').find('.username')    // .user-card 内の .username
cy.get('form').find('input[type="text"]') // form 内の text input

// cy.get() はドキュメント全体から検索する
cy.get('.username')  // ページ全体から .username を検索

cy.parent() と cy.parents() - 親要素へ移動

// 直接の親要素
cy.get('.error-text').parent()

// 条件に合う祖先要素
cy.get('.error-text').parents('.form-group')

// closest() のように最も近い祖先を取得
cy.get('.error-text').closest('.card')

cy.children() - 子要素を取得

// 全ての子要素
cy.get('ul.menu').children()

// 条件に合う子要素
cy.get('ul.menu').children('.active')

cy.siblings() - 兄弟要素を取得

// 全ての兄弟要素
cy.get('.active-tab').siblings()

// 条件に合う兄弟要素
cy.get('.active-tab').siblings('.disabled')

要素の絞り込み

複数の要素が返される場合に、特定の要素を選択するコマンドです。

// 最初の要素
cy.get('li').first()

// 最後の要素
cy.get('li').last()

// N番目の要素(0始まり)
cy.get('li').eq(0)    // 1番目
cy.get('li').eq(2)    // 3番目
cy.get('li').eq(-1)   // 最後の要素

// フィルタリング
cy.get('li').filter('.active')      // .active クラスを持つ li のみ
cy.get('li').not('.disabled')       // .disabled クラスを持たない li のみ
コマンド 説明
first() 最初の要素 cy.get('li').first()
last() 最後の要素 cy.get('li').last()
eq(index) N番目の要素 cy.get('li').eq(2)
filter(selector) 条件に合う要素のみ cy.get('li').filter('.done')
not(selector) 条件に合わない要素 cy.get('li').not('.skip')

cy.within() でスコープを限定

cy.within() を使うと、特定の要素をスコープとしてその中だけでコマンドを実行できます。同じ構造が繰り返されるページで特に便利です。

<div data-cy="login-form">
  <input name="email" />
  <input name="password" />
  <button>ログイン</button>
</div>

<div data-cy="register-form">
  <input name="email" />
  <input name="password" />
  <button>登録</button>
</div>
// within() を使わない場合 - どちらの email かわからない
cy.get('[name="email"]')  // 2つマッチしてしまう

// within() でスコープを限定
cy.get('[data-cy="login-form"]').within(() => {
  cy.get('[name="email"]').type('user@example.com')
  cy.get('[name="password"]').type('secret123')
  cy.get('button').click()
})

// 別のフォームに対する操作
cy.get('[data-cy="register-form"]').within(() => {
  cy.get('[name="email"]').type('newuser@example.com')
  cy.get('[name="password"]').type('newpassword')
  cy.get('button').click()
})
flowchart TB
    subgraph Page["ページ全体"]
        subgraph Login["login-form スコープ"]
            LE["email input"]
            LP["password input"]
            LB["ログインボタン"]
        end
        subgraph Register["register-form スコープ"]
            RE["email input"]
            RP["password input"]
            RB["登録ボタン"]
        end
    end
    style Login fill:#3b82f6,color:#fff
    style Register fill:#8b5cf6,color:#fff

要素のクリック操作

Cypressでは3種類のクリック操作が用意されています。

// 通常のクリック
cy.get('[data-cy="submit-btn"]').click()

// ダブルクリック
cy.get('[data-cy="item"]').dblclick()

// 右クリック(コンテキストメニュー)
cy.get('[data-cy="file"]').rightclick()

click() のオプション

// 要素の特定の位置をクリック
cy.get('.map').click('topLeft')
cy.get('.map').click('center')       // デフォルト
cy.get('.map').click('bottomRight')

// 座標を指定してクリック
cy.get('.canvas').click(100, 200)

// 複数要素を順番にクリック
cy.get('.checkbox').click({ multiple: true })

// 要素が覆われていてもクリック(注意して使用)
cy.get('.hidden-btn').click({ force: true })
オプション 説明 使用例
position クリック位置 click('topLeft')
x, y 座標指定 click(100, 200)
multiple 複数要素を全てクリック click({ multiple: true })
force 強制クリック click({ force: true })

force: true はテストが通らないときの安易な解決策として使わないでください。本当にUIが隠れている場合のみ使用しましょう。


テキスト入力操作

cy.type() - テキストを入力

// 基本的な入力
cy.get('[data-cy="email"]').type('user@example.com')

// 特殊キーの入力
cy.get('[data-cy="search"]').type('Cypress{enter}')  // Enter キー
cy.get('[data-cy="name"]').type('{selectall}{backspace}')  // 全選択して削除
cy.get('[data-cy="input"]').type('{ctrl+a}')  // Ctrl+A

// 入力速度の調整(デフォルトは10ms)
cy.get('[data-cy="input"]').type('slow typing', { delay: 100 })

特殊キー一覧

キー 記法 説明
Enter {enter} Enterキー
Tab {tab} Tabキー(プラグイン必要な場合あり)
Escape {esc} Escapeキー
Backspace {backspace} 1文字削除
Delete {del} Deleteキー
全選択 {selectall} テキスト全選択
上矢印 {uparrow} 上矢印キー
下矢印 {downarrow} 下矢印キー

cy.clear() - 入力をクリア

// 入力フィールドをクリア
cy.get('[data-cy="email"]').clear()

// クリアしてから新しい値を入力
cy.get('[data-cy="email"]').clear().type('new@example.com')

その他の入力操作

// セレクトボックス
cy.get('[data-cy="country"]').select('Japan')
cy.get('[data-cy="country"]').select('jp')       // value で選択

// チェックボックス
cy.get('[data-cy="agree"]').check()
cy.get('[data-cy="agree"]').uncheck()

// ラジオボタン
cy.get('[data-cy="plan"]').check('premium')       // value を指定

実践: ユーザー登録フォームのテスト

ここまで学んだ内容を組み合わせて、実践的なテストを書いてみましょう。

describe('ユーザー登録フォーム', () => {
  beforeEach(() => {
    cy.visit('/register')
  })

  it('全てのフィールドを入力して登録できる', () => {
    cy.get('[data-cy="register-form"]').within(() => {
      // テキスト入力
      cy.get('[data-cy="username"]').type('testuser')
      cy.get('[data-cy="email"]').type('test@example.com')
      cy.get('[data-cy="password"]').type('SecurePass123!')
      cy.get('[data-cy="password-confirm"]').type('SecurePass123!')

      // セレクトボックス
      cy.get('[data-cy="country"]').select('Japan')

      // チェックボックス
      cy.get('[data-cy="terms"]').check()

      // 送信ボタンをクリック
      cy.get('[data-cy="submit"]').click()
    })

    // 登録成功メッセージの確認
    cy.get('[data-cy="success-message"]')
      .should('be.visible')
      .and('contain', '登録が完了しました')
  })

  it('必須フィールドが空の場合エラーが表示される', () => {
    cy.get('[data-cy="register-form"]').within(() => {
      // 何も入力せずに送信
      cy.get('[data-cy="submit"]').click()

      // エラーメッセージの確認
      cy.get('[data-cy="error-username"]')
        .should('be.visible')
        .and('have.text', 'ユーザー名は必須です')

      cy.get('[data-cy="error-email"]')
        .should('be.visible')
    })
  })

  it('メールアドレスの形式が正しくない場合エラー', () => {
    cy.get('[data-cy="register-form"]').within(() => {
      cy.get('[data-cy="email"]').type('invalid-email')
      cy.get('[data-cy="submit"]').click()

      cy.get('[data-cy="error-email"]')
        .should('contain', '有効なメールアドレスを入力してください')
    })
  })
})

まとめ

概念 説明
CSSセレクタ ID, クラス, タグ, 属性を組み合わせて要素を特定する
data-cy属性 テスト専用属性で安定したセレクタを実現する
cy.get() CSSセレクタでDOM要素を取得する基本コマンド
DOM走査 find, parent, children, siblings で要素間を移動する
要素の絞り込み first, last, eq, filter, not で対象を限定する
cy.within() 特定の要素をスコープとして操作を限定する
クリック操作 click, dblclick, rightclick の3種類
テキスト入力 type, clear, select, check/uncheck で入力操作を行う

重要ポイント

  1. data-cy属性を使おう - CSSクラスやIDに依存するとデザイン変更でテストが壊れる。テスト専用属性を使うことで保守性が大幅に向上する
  2. cy.within() で明確なスコープを - 同じ構造が繰り返されるページでは、within()でスコープを限定して意図を明確にする
  3. force: true は最終手段 - テストが通らない場合、まずUIの問題を疑う。force オプションは本当に必要な場合だけ使う

練習問題

問題1: 基本

以下のHTMLに対して、Cypressでログインフォームのテストを書いてください。

<form id="login-form">
  <input data-cy="login-email" type="email" />
  <input data-cy="login-password" type="password" />
  <button data-cy="login-submit" type="submit">ログイン</button>
</form>

問題2: 応用

商品一覧ページで、3番目の商品カードの「カートに追加」ボタンをクリックするテストを書いてください。各商品カードは .product-card クラスを持ち、内部に [data-cy="add-to-cart"] ボタンがあります。

チャレンジ問題

以下の要件を満たすテストスイートを作成してください。

  • 検索フォームにキーワードを入力してEnterキーで検索
  • 検索結果が5件表示されることを確認
  • 最初の結果をクリックして詳細ページに遷移
  • 詳細ページの「お気に入り」ボタンをクリック
  • 「お気に入りに追加しました」メッセージが表示されることを確認

参考リンク


次回予告: Day 4では「アサーションをマスターする」について学びます。should() と expect() の使い分け、チェーンアサーション、リトライメカニズムを理解して、信頼性の高いテストを書けるようになりましょう。