コンテンツにスキップ

English · 日本語

シナリオクックブック

「〇〇をしたい」という目的別のレシピ集です。scenarios が文法のリファレンス (すべてのステップ種別、待機、アサーション)であるのに対し、このページは実例です。以下のレシピは どれも、リポジトリ自身の CI が実際に走らせているファイルから抜き出したもので、説明のためだけに 作った架空の例ではありません。各レシピの下のリンクから、ここでは省いた部分も含む全文を見られ ます。

関連: scenarios · selectors · network · Getting started

showcase アプリをビルドするか web デモを配信すれば(Getting started 参照)、 以下のどれでも自分で実行できます。

uv run bajutsu run --scenario <path-to-file> --target showcase-swiftui --backend idb --udid booted --no-erase

画面を移動して値の変化を検証する

もっとも単純で有用な形です。画面へ移動し、1 つの要素を操作し、結果をアサーションで確かめます。 これは showcase 自身のガイドツアーで使われているシナリオそのもので、 demos/tour/demo.sh がこれを実行したあと、わざと壊してみせます (まずアサーションを壊して機械アサーションが検知する様子を、続いてセレクタを壊して triage が 原因を突き止める様子を見せます)。

- name: favorite a horse
  preconditions:
    launchEnv: { SHOWCASE_UITEST: "1" }
  steps:
    - wait: { for: { id: stable.row.3 }, timeout: 10 }
    - tap: { id: stable.row.3 }
    - wait: { for: { id: horse.favorite }, timeout: 5 }
    - tap: { id: horse.favorite }
  expect:
    - value: { sel: { id: horse.favorite.value }, equals: "on" }

tap の前の wait はどれも条件待ちであり、固定の sleep ではありません。Bajutsu は対象の id が 現れるまでポーリングし、現れなければタイムアウトしてはっきり失敗します。全文は demos/showcase/scenarios/menu/tour.yaml にあります。

一覧を検索して絞り込む

フィールドに入力し、結果件数をアサーションで確かめます。あわせて、ハッピーパスと同じくらい大切な「該当なし」のケースも載せます。

- name: filter narrows the catalog
  preconditions:
    launchEnv: { SHOWCASE_UITEST: "1" }
  steps:
    - tap: { label: "Search", traits: [button] }
    - wait: { for: { id: search.field }, timeout: 10 }
    - type: { text: "Horse 3", into: { id: search.field } }
    - wait: { for: { id: search.row.3 }, timeout: 5 }
  expect:
    - count: { sel: { idMatches: "search.row.*" }, equals: 1 }
    - value: { sel: { id: search.count }, equals: "1" }
    - exists: { id: search.results-empty, negate: true }

- name: no match shows the empty state
  preconditions:
    launchEnv: { SHOWCASE_UITEST: "1" }
  steps:
    - tap: { label: "Search", traits: [button] }
    - wait: { for: { id: search.field }, timeout: 10 }
    - type: { text: "zzz", into: { id: search.field } }
    - wait: { for: { id: search.results-empty }, timeout: 5 }
  expect:
    - exists: { id: search.results-empty }
    - count: { sel: { idMatches: "search.row.*" }, equals: 0 }

idMatches は id に対する glob マッチで、個々の行を名指しせずに「行が何件あるか」を検証するのに 使えます。全文(プラットフォームをまたぐ id 候補のリスト形式も含む)は demos/showcase/scenarios/search.yaml にあります。

システムの権限ダイアログを許可する

実行時の権限プロンプト(通知、位置情報など)は、アプリ自身の UI ではなくプロセス外のシステム アラートです。idb はこれを直接タップできません。dismissAlerts は、そのタップだけを AI の アラートガードに任せます。ガードがプロンプトを見張って「Allow」をタップする一方で、その前後の アサーションはすべて機械チェックのままです。

- name: grant notification permission
  tags: [permission, system]
  dismissAlerts: { instruction: "tap Allow" }
  preconditions:
    launchEnv: { SHOWCASE_UITEST: "1" }
  steps:
    - tap: { label: "Permissions", traits: [button] }
    - wait: { for: { id: perm.requestNotif }, timeout: 10 }
    - assert:
        - value: { sel: { id: perm.notif.value }, equals: "notDetermined" }
    - tap: { id: perm.requestNotif }
    - wait: { for: { id: perm.notif.authorized }, timeout: 10 }
  expect:
    - value: { sel: { id: perm.notif.value }, equals: "authorized" }

dismissAlerts はアラートのハンドラであって、アサーションではありません。合否の判定には 一切関与せず、idb からは見えないアラートでステップが止まってしまうのを防ぐだけです。Android では 同じシナリオがプロンプトなしで走ります(target の config が権限を事前付与しているため)。その場合 ガードは何もしないまま待機するだけです。1 つのシナリオが 2 つのプラットフォームで分岐なく動きます。 プラットフォーム間の整合性についての注記を含む全文は demos/showcase/scenarios/permission.yaml にあります。

ネットワークをモックする

mocks はリクエストをプロトコル内で捕まえ、決定的に応答します。実サーバは不要で、ネットワークの 不安定さもありません。request アサーションで、そのモックされた呼び出しが実際に起きたことを 確認できます。

- name: log submit answered by a mock, toast appears and clears
  preconditions:
    launchEnv: { SHOWCASE_UITEST: "1" }
  mocks:
    - match: { method: POST, pathMatches: "/post$" }
      respond: { status: 201, body: "{\"ok\":true}" }
  steps:
    - tap: { label: "Log", traits: [button] }
    - wait: { for: { id: log.submit }, timeout: 10 }
    - tap: { id: log.submit }
    - wait: { until: { request: { method: POST, path: /post, status: 201 } }, timeout: 6 }
    - wait: { for: { id: log.toast }, timeout: 4 }
    - wait: { until: { gone: { id: log.toast } }, timeout: 5 }
  expect:
    - request: { method: POST, path: /post, status: 201 }
    - value: { sel: { id: log.status }, equals: "done" }

wait: { until: { gone: … } } は要素が消えるまでポーリングします。こういう一時的なトースト 通知の検証に使えます。全文(このリクエストが運ぶ Authorization ヘッダと password フィールドを redact ポリシーが証跡上でマスクする様子も含む)は demos/showcase/scenarios/network_mock.yaml に あります。

同じシナリオをデータ表に沿って繰り返す

data は 1 つのシナリオ本体を行ごとに 1 回ずつ、それぞれ独立したクリーンな環境で実行し、 ${row.*} トークンを置換します。ここでは入力するクエリとアサーションする id の両方で使っており、 各行が「検索した馬をちょうど見つけた」ことを証明します。

- name: search finds the seeded horse
  data:
    - { q: "Horse 1", n: "1" }
    - { q: "Horse 3", n: "3" }
    - { q: "Horse 5", n: "5" }
  preconditions:
    launchEnv: { SHOWCASE_UITEST: "1" }
  steps:
    - tap: { label: "Search", traits: [button] }
    - wait: { for: { id: search.field }, timeout: 10 }
    - type: { into: { id: search.field }, text: "${row.q}" }
    - wait: { for: { id: "search.row.${row.n}" }, timeout: 5 }
  expect:
    - value: { sel: { id: search.count }, equals: "1" }
    - exists: { id: "search.row.${row.n}" }

各トークンのプラットフォームをまたぐ id 候補のリスト形式を含む全文は demos/showcase/scenarios/data_driven.yaml に あります。

ステップ列をコンポーネントとして再利用する

コンポーネントはパラメータ付きの再利用可能なステップ列で、シナリオ DSL のマクロです。読み込み 時に展開され、run の結果には自分自身のステップとしては現れません。まず定義します。

# _components/search_for.yaml
params: [query]
steps:
  - tap: { label: "Search", traits: [button] }
  - wait: { for: { id: search.field }, timeout: 10 }
  - type: { into: { id: search.field }, text: "${params.query}" }

これをどのシナリオからでも use / with で呼び出せます。

- name: search finds a horse by name
  preconditions:
    launchEnv: { SHOWCASE_UITEST: "1" }
  steps:
    - use: { component: _components/search_for.yaml, with: { query: "Horse 3" } }
    - wait: { for: { id: search.row.3 }, timeout: 5 }
  expect:
    - value: { sel: { id: search.count }, equals: "1" }

コンポーネントファイル: demos/showcase/scenarios/menu/_components/search_for.yaml。 呼び出し側の例: demos/showcase/scenarios/menu/features.yaml

web backend でも同じシナリオの形

ここまでのレシピはどれも iOS の showcase を対象に書きましたが、step/expect の文法自体に iOS 固有の ものは何もありません。変わるのはセレクタの背後にある属性だけです(accessibilityIdentifier が web の data-testid に変わりますが、同じセレクタ解決コアを通ります)。web デモの 2 つ目のシナリオも、 同じ形をしています。画面を移動し、操作を繰り返し、最終的な値をアサーションで確かめ、その上で証跡を 取得します。

scenarios:
  - name: onboard, log in, and increment the counter three times
    steps:
      - tap: { id: onboarding.start }
      - type: { text: "a@b.com", into: { id: auth.email } }
      - type: { text: "pw", into: { id: auth.password } }
      - tap: { id: auth.submit }
      - wait: { for: { id: home.title }, timeout: 5 }
        capture: [deviceLog, video]
      - tap: { id: counter.increment }
      - tap: { id: counter.increment }
      - tap: { id: counter.increment }
    expect:
      - exists: { id: home.title }
      - value: { sel: { id: counter.value }, equals: "3" }

実行するには uv run bajutsu run --scenario demos/web/scenarios/counter.yaml --target web --backend web --config demos/web/demo.config.yamlweb トラック 参照)。全文は demos/web/scenarios/counter.yaml にあります。


これらの出どころ

showcase 一式 (demos/showcase/scenarios/)には、ジェスチャ、マルチタッチ、 デバイス制御、ビジュアルリグレッション、relaunch と状態の永続化などを扱うシナリオがさらに 25 本 ほどあります。各画面が公開する識別子は showcase にカタログがあります。web デモ (demos/web/scenarios/)と、web UI 自身のドッグフーディング一式 (demos/serve-ui/scenarios/)も見る価値があります。後者は ある程度複雑な単一ページアプリをテストする実例としても参考になります。ここに出てきたすべての レシピの背後にある step / wait / アサーションの文法全体は scenarios に、形式的な EBNF は dsl-grammar にあります。