English · 日本語
ドライバ抽象、バックエンド、環境管理¶
ひとつの
Driverインターフェースの裏にバックエンド(idb(iOS Simulator)、adb(Android エミュレータ)、playwright(web ブラウザ)、それにテスト用のインメモリfake)を置き、能力差を 抽象側で吸収します。プラットフォーム対応のレジストリがbackendリストから actuator を選びます。 iOS ではアプリの起動(boot/launch)をsimctlラッパが担い、Android ではその双子であるadbラッパが担います。実装:
bajutsu/drivers/(base.py/idb.py/adb.py/playwright.py/fake.py)・bajutsu/backends.py・bajutsu/simctl.py・bajutsu/adb.py。
関連: selectors(解決) · concepts の安定度順ラダー · run-loop
Driver Protocol¶
すべてのバックエンドが満たす共通インターフェースです(base.py、runtime_checkable な Protocol)。
操作(tap/type/swipe/wait/query)は actuator のみが行います。
class Driver(Protocol):
def query(self) -> list[Element]: ... # 画面の要素ツリー
def tap(self, sel: Selector) -> None: ...
def tap_point(self, p: Point) -> None: ... # 生座標 tap(システムアラート等)
def long_press(self, sel: Selector, duration: float) -> None: ...
def swipe(self, frm: Point, to: Point) -> None: ... # 素のポインタドラッグ(座標形)
def scroll(self, frm: Point, to: Point) -> None: ... # 方向指定のスクロール(BE-0227)
def type_text(self, text: str) -> None: ...
def wait_for(self, sel: Selector) -> bool: ... # 単発チェック:現在の画面に一致するか
def screenshot(self, path: str) -> None: ...
def capabilities(self) -> set[str]: ... # 提供能力(actuator / フォールバック解決用)
wait_forについて: 契約上、単発チェックです(BE-0118)。現在の画面を一度だけ確認して返し、ループはしません。締め切りまでのポーリングは共有ヘルパbase.wait_untilに集約してあり、呼び出し側が渡すtimeoutは、どの backend でも同じ実時間を意味します。各ドライバがそれぞれのループを持つことはありません。run ループ自身の条件待機は、orchestrator がquery()を直接ポーリングして行います(_wait、run-loop)。したがってwait_untilを使うのは、このループの外側の呼び出し側(たとえばgolden_assert)だけです。
能力(Capability)¶
capabilities() が返すトークン集合で、actuator 選択、証跡のフォールバック解決、プリフライト能力検査(後述)に使います。
| 能力 | 意味 | idb | adb | playwright | fake |
|---|---|---|---|---|---|
query |
要素ツリー取得 | ✅ | ✅ | ✅ | ✅ |
elements |
要素ダンプ証跡 | ✅ | ✅ | ✅ | ✅ |
screenshot |
スクリーンショット | ✅ | ✅ | ✅ | ✅ |
semanticTap |
id/label で直接タップ(座標不要) | — | — | ✅ | ✅ |
conditionWait |
ネイティブ条件待機 | — | — | ✅ | ✅ |
network |
ネイティブネットワーク監視 | — | — | ✅ | — |
multiTouch |
2 本指ジェスチャ(pinch / rotate) | — | ✅ | ✅ | ✅ |
textSelection |
フォーカス中フィールドの全選択とクリップボードへのコピー | — | ✅ | ✅ | ✅ |
deviceControl.setLocation |
疑似 GPS 位置の設定 | ✅ | ✅ | — | — |
deviceControl.clipboard |
クリップボードの読み取り / 書き込み / クリア | ✅ | ✅ | — | — |
deviceControl.push |
プッシュ通知の配信 | ✅ | — | — | — |
deviceControl.clearKeychain |
キーチェーンのクリア | ✅ | — | — | — |
deviceControl.appLifecycle |
アプリのバックグラウンド化 / フォアグラウンド化 | ✅ | — | — | — |
deviceControl.statusBar |
ステータスバーの上書き / クリア | ✅ | — | — | — |
deviceControl.*トークンは、DeviceControl一族を操作ごとに分割したものです(BE-0212、BE-0128 の粗いdeviceControlから分割しました)。これにより、バックエンドは自分が忠実に実現できる操作だけを公開でき、部分的にしか実現しないバックエンドも残りを誤って公開せずに済みます。idb は一族全体を実現しますが、Android エミュレータはsetLocationとclipboardだけを実現します(そのpush/ キーチェーン / ステータスバー / アプリライフサイクルの各操作には忠実な等価物がありません)。idb と adb はどちらもリーンな端に位置し、frame 中心の座標で操作します。semantic tap を持たないため、run ループは
query()で要素を一意に確定しその中心をタップします。idb ではpinch/rotateはUnsupportedAction(単一タッチ)を返し、iOS ではこれらを codegen → XCUITest 経由で扱います。adb はquery/elements/screenshot、multiTouch(rooted device でのsendevent2 本指スイープ、BE-0232)に加えて、エミュレータが実現できるデバイス制御のサブセットdeviceControl.setLocationとdeviceControl.clipboardを公開します(BE-0211)。デバイス制御一族の残りにはエミュレータでの忠実な等価物がないため、公開しません。fakeドライバはテストでそれらのコードパスを動かすためだけに、より広い能力集合(semanticTap / conditionWait / multiTouch)を公開します。playwright(web)ドライバはsemanticTap/conditionWait(Playwright がネイティブに持つ)に加えてnetwork(アプリ側の協力なしに通信を観測しスタブできる初めてのネイティブネットワーク対応バックエンド)とmultiTouch(Chromium DevTools プロトコルのInput.dispatchTouchEventで pinch / rotate を合成)を公開します(BE-0054)。
プリフライト能力検査(BE-0082)¶
バックエンドの能力集合は静的なので、選んだ actuator が持たない能力をシナリオが必要とするかどうかは、デバイス作業の前に分かります。run の開始時(actuator を選んだ後、最初のデバイスを lease する前)に、runner は各シナリオを actuator の能力と照合し(bajutsu/capability_preflight.py)、未対応のシナリオを即座に失敗させます。集約した 1 つの理由(UnsupportedAction 相当)を付けて、デバイスを起動して途中で失敗するのを避けます(prime directive #2:速く明確に失敗する)。検査は (シナリオ, 能力集合) の純粋関数で、デバイスも時計も使いません。シナリオ単位なので、未対応のシナリオだけが失敗し、残りは実行されます。
検査は、能力集合で明確に判定できる真の hard requirement だけを門にします。pinch / rotate は multiTouch、select / copy は textSelection(全選択とクリップボードへのコピー。idb は座標専用でどちらも拒否します。delete / clear はどのバックエンドも delete_text を実現できるので門にしません)、visual アサーションは screenshot、そして各デバイス制御ステップは自分の操作に対応するトークンを必要とします。setLocation は deviceControl.setLocation、クリップボードのステップは deviceControl.clipboard、push は deviceControl.push、という具合です(この操作ごとのトークンへの分割は BE-0212、前掲)。すべての run は query と elements を必要とします。relaunch はここに含みません。relaunch は DeviceControl ではなく、注入される relauncher で門を通すからです。一方、conditionWait は門にしません(run ループはすべての待機を polling で実装するので、どのバックエンドもこのトークンを必要としません)。network も門にしません(idb は network を公開しませんが、アプリ側の collector で通信を捕捉するため、request / event / requestSequence / responseSchema アサーションや until: { request } 待機は idb でも動きます)。gestures.py の _require_multi_touch は、ジェスチャ実行時の多層防御の検査として残します。デバイス制御ステップについても同様に _need_control を残し、その run で DeviceControl がまったく配線されていない場合(デバイスを固定しない並行 run など)を捕捉します。トークンが操作ごとになったので、一族の一部だけを実現するバックエンド(Android エミュレータの setLocation と clipboard)は、公開した操作については preflight を通し、残りについては速く失敗します。未対応のステップは一つずつ名指しされ、一族が全か無かで扱われることはありません。
idb¶
ヘッドレスで座標ベースのバックエンドです。CI(継続的インテグレーション)向きで、semantic tap を持たないため、抽象側で id → frame 中心 → 座標 tap に解決します。実装: drivers/idb.py。
query():idb ui describe-all --udid <udid> --jsonをparse_describe_allで正規化します(JSON 配列 / 改行区切り JSON の両対応、AXLabel/AXValue/AXUniqueId等を吸収)。tap(sel):_resolveで一意確定します(not-found はリトライ、ambiguity は即失敗: 実機ツリーは遷移中に一時的に空になり得るため)。確定後、frame 中心へidb ui tap(整数座標)を送ります。screenshot: idb 自身のフレームキャプチャが不安定なためsimctl io screenshotを使います。swipe:--duration 0.2を付けて実ドラッグ化します(瞬間スワイプは SwiftUI に pan として認識されません)。
describe-all の JSON キー名は fb-idb の出力に従い、
make -C demos/showcase run-swiftui+ios-e2e.ymlCI ワークフローで実機検証済みです(iPhone 17 Pro、最近の iOS)。インストール済み idb がスキーマを変えたときだけ再確認すれば十分です(idb.py冒頭の注記参照)。idb クライアントはuv sync --extra idb、idb_companionはbrew install facebook/fb/idb-companionでインストールします。
idb のバージョン追跡(BE-0005)¶
idb は唯一の on-device backend なので、新しい Simulator ランタイムを古い idb_companion が駆動できない、あるいは companion のアップグレードが describe-all の JSON を変える、といった事態が Bajutsu 側の変更なしに run を壊します。そこで idb を走らせるバージョンを、たまたまインストールされているものではなく、記録して比較できる入力として扱います。
- 設定で範囲を固定する。
defaults.idbVersionに">=1.1.8"や">=1.1.0,<2.0.0"のような制約を置きます(環境レベル=どの target を駆動しても同じ pin)。bajutsu doctorがインストール済みのidb_companionをこれと突き合わせて報告します(例:✓ idb_companion version: 1.1.8 (expected >=1.1.8))。不一致が、分かりにくい後続の失敗としてではなく、起動前チェックの一覧に現れます。不正な pin は設定の読み込み時に弾きます。pin を宣言しなければ、doctorはバージョン行を出しません。 - manifest に記録する。 idb で実行した run はすべて
idb_companionと idb クライアントのバージョンをmanifest.jsonに書き込みます("idb": { "companion": …, "client": … })。これでどの成果物がどの idb で生成されたかが正確に分かります。これは来歴(provenance)であって、pass/fail には一切関与しません。run/CI の判定は決定論的なまま保たれます。 - 定期的な互換性監視。
idb-monitor.ymlが、最新のidb_companionに対して smoke シナリオを idb 経由で週次(per-PR ゲートとは分離)実行します。smoke の実行はparse_describe_all→ Element 正規化を通るので、スキーマや挙動の drift があればそこで明確に失敗します。場当たり的に発見されるのではなく、こちらが制御する周期で捕まえられます。
adb(Android)¶
ヘッドレスで座標ベースの、idb の双子にあたるバックエンドです。semantic tap を持たないため、iOS とまったく同じく抽象側で id → frame 中心 → 座標 tap に解決します。実装: drivers/adb.py + bajutsu/adb.py(ロードマップ BE-0007)。
query(): ウィンドウの UI Automator XML を読み取り、純粋なパーサ(parse_hierarchy)が各<node>をElementに写します。読み取りは、常駐 UI Automator サーバがビルド済み(make -C BajutsuAndroidUIAutomatorServer build)のときはそのサーバ経由で行います。温めた 1 つのUiAutomationセッションがadb forward越しにGET /sourceへ答えるので、1 回の読み取りは約 0.1〜0.3 秒で済み、呼び出しのたびに約 2.4 秒かかるadb -s <serial> exec-out uiautomator dump /dev/ttyを都度起動せずに済みます(ロードマップ BE-0245)。常駐サーバの全画面ダンプはアクティブウィンドウへ絞り込むので、ダンプ経路と同じElementを返します。サーバが未ビルドのとき、またはチャネルに失敗したときはuiautomator dumpにフォールバックし、BAJUTSU_ADB_RESIDENT(0/1)でどちらの経路にも固定できます。- セレクタの写像:
resource-id→identifier(<package>:id/接頭辞を剥がしてローカル名にするので、testTagsAsResourceIdで表出した Compose のtestTagはそのまま、ネイティブのandroid:idは接頭辞が落ちます)、text→label(content-descへフォールバック)、content-desc→value(アプリは状態値をここにミラーします。SPEC §2.1)、ウィジェットのclass(と enabled / selected / checked の状態)→traitsです。 - id 照合ポリシー: ローカル名は厳密一致で照合します。ドライバは
.↔_の書き換えをしません。書き換えは別々の id を取り違え、決定性を損なうためです。プラットフォーム本来の id 構文が SPEC の id をそのまま再現できない場合(Android Views のandroid:idは.も-も許さず、stable.refreshはstable_refreshとして現れます)は、シナリオが 1 つのセレクタに id を両方の形で持ち(id: [stable.refresh, stable_refresh])、候補の OR として照合します(BE-0221)。scenarios を参照してください。 tap(sel):_resolveで一意確定します(not-found はリトライ、ambiguity は即失敗。idb と同じく、遷移中のダンプは一時的な null-root としてリトライし、2 件以上の一致は即座に失敗させます)。確定後、frame 中心へadb shell input tapを送ります。swipeは実際のドラッグにするため有限の duration を付け、long_pressは同じ点を duration だけ保持する swipe、type_textはinput text(空白はその%sエスケープで送ります)です。- 実機アクチュエーションの忠実度(ロードマップ BE-0210):
backステップは真のシステムバック(input keyevent 4、KEYCODE_BACK)です。Android にはタップできる画面上の戻る要素がなく、この点が iOS の OS 戻るボタンと異なります。double_tapは 2 回のタップを単一のadb shell往復(input tap … ; input tap …)で発行するので、adb の転送往復がタップの間に挟まって double-tap ウィンドウを超えることがありません。タップの対象が現在のビューポートにないときは、そちらへスクロール(既定は上方向のスワイプ)して再クエリし、回数で区切ります。固定 sleep ではなく条件待機なので、決して現れないセレクタはそれでも決定論的に失敗します。
[!NOTE] scroll-into-view は現状 adb 専用の回復です。
idb/XCUITest/Playwright は対象が初期ビューポートに無いとtapを即座に失敗させます。そのため、同じシナリオでも fold より下の要素へのtapが Android では(数回のスワイプののち)通り、iOS/web では失敗しうる非対称が生じます。移植可能な書き方は依然として明示的なswipeステップです(demos/showcase/scenarios/notices.yamlを参照)。adb の自動スクロールはその代替ではなく堅牢化のための安全網です。他バックエンドへの拡張は後続作業です(BE-0210 は adb に限定しました)。 - マルチタッチ(BE-0232):pinch/rotateは 2 スロットの protocol-Bsendeventスイープで駆動します(pinch_contacts/rotate_contactsが 2 接点の座標を計算し、rotateは両端点を結ぶ直線の弦を掃きます。円弧の線形近似で、web バックエンドの rotate と同じです)。rooted device でタッチスクリーンを検出できることが前提で、_two_finger_gestureはそれ以外ではUnsupportedActionで明確に失敗します。下の double-tap 経路と異なり、単一タッチへのフォールバックはありません。MULTI_TOUCHは root の有無にかかわらず能力集合で静的に宣言するので、preflight は adb 上のgestures_multitouchを通します。root のチェックは能力集合ではなく実行時に課します。 -screenshotはadb exec-out screencap -pの PNG バイト列(バイナリを崩さない stdout)を書き出します。 - ライフサイクル(AndroidEnvironment、iOS のsimctlシーケンスの双子): 起動完了待ち(getprop sys.boot_completedを有界の期限まで polling する条件待機で、固定 sleep も、無期限にブロックするadb wait-for-deviceもありません)→ 必要に応じて APK インストール →pm clearによるクリーン状態(erase相当)→am force-stop→ ランタイム権限の事前付与(pm grant、後述)→am start(起動アクティビティはパッケージマネージャで解決し、launch env は intent extras として渡します)→ deeplink(am start -a android.intent.action.VIEW)。run の manifest はbackend: "adb"を記録するので、選ばれた actuator が開示されます。 - ランタイム権限(BE-0210): ターゲットの configgrantPermissionsに列挙した権限を、lease 時にadb shell pm grant <package> <permission>で事前付与します。pm clear(付与をリセットします)のあと、起動の前に付与するので、ランタイムの権限プロンプトがシナリオを止めることがありません。ダイアログが現れてからタップするのではなく、事前に決定論的に付与することで、タイミングを run の経路に持ち込みません。列挙する権限はアプリごとに異なるので、ドライバではなく config に置きます。 - 区間証跡(BE-0007 の Unit 4):videoはadb shell screenrecordで録画し、deviceLogはadb logcatをストリームします。simctl の provider の双子です。screenrecordはデバイス側に書き込む(ホストのファイルへは流せない)ので、録画は停止時に SIGINT で確定させてからadb pullで回収し、logcatはファイルへストリームして SIGTERM で停止します。どちらも web バックエンドと同じ driver のdriver_intervalseam から供給するので、バックエンド非依存のcaptureポリシーがそのまま両方を運びます(evidenceを参照)。 - ネットワークはネイティブには観測しません(NETWORK能力を持ちません)。iOS と同じモックで対応し、アプリ側の collector の URL を launch env 経由で intent extra として渡すので、新しいコードパスなしにmocksが動きます。デバイス制御は、エミュレータが実現できるサブセットとしてsetLocation(emu geo fix、BE-0211)とクリップボード操作に対応します。一族の残りは未対応のままです。クリップボードはcmd clipboardではなくアプリ内のレシーバ(BajutsuAndroid、BE-0233)を経由します。このコマンドは実機では黙って何もせず、Android 10 以降はフォアグラウンドのアプリと既定の IME しかクリップボードを触れないためです。そこで bajutsu は順序付きam broadcastを送り、アプリの内側のレシーバがアプリプロセスからこれを処理します(両方向を base64 で運ぶので argv に引用符付けは要らず、レシーバがなければ空のクリップを読むのではなく明確に失敗します)。idb がクリップボードを simctl 経由で実現するのと同じく、協調するアプリがあればバックエンドが駆動できるので、adb はclipboardを公開し続けます。BajutsuAndroidを参照してください。XML の属性名は UI Automator の
uiautomator dumpスキーマに従います。Views のandroid:idにおける.↔_の扱いはシナリオ側で解決します。セレクタが id を両方の形で持ち、どちらにもマッチします(BE-0221)。そのため共有の showcase シナリオが両 Android toolkit でそのまま走り、android-e2e.ymlがshowcase-composeとshowcase-viewsを同じセットで駆動して push/PR ごとに検証します。fast ゲートでは、取得済みの XML フィクスチャに対してパーサ、frame 中心タップ、transient-empty のリトライ、ambiguity 即失敗を検証します。adb はbrew install android-platform-toolsでインストールします。
Playwright(web)¶
Playwright(Python)によるヘッドレス Chromium です。Mac も Simulator も要らず Linux で動くため、make check と同じツールチェーンに収まります。実装: drivers/playwright.py(ロードマップ BE-0041)。
query(): 1 本のpage.evaluate()が、可視、操作可能、アクセシビリティ関連の DOM ノードを走査し、純粋なパーサ(parse_dom)が各ノードをElementに写像します。id 規約は iOS の accessibilityIdentifier の web 版です。data-testid→Selector.id、ARIArole(またはタグ)→traits、accessible name /aria-label/ テキスト →label、input のvalue→value。tap(sel): idb と同様、query()のスナップショットに対し共有のresolve_unique/find_allで要素を一意に確定し、frame 中心を座標クリック(page.mouse.click)します。Playwright 自身のget_by_test_id().click()はあえて使いません。これによりセレクタの意味が他のどの backend ともバイト単位で一致します。type_textはpage.keyboardで入力します(オーケストレータが先にintoをタップしてフィールドにフォーカスします)。screenshotはpage.screenshot、wait_forはfind_allによる単発です(どの backend も同様で、締め切りまでのポーリングは共有ヘルパbase.wait_untilが担います)。- ライフサイクルは driver が所有します。新しい
BrowserContextがerase相当、navigate()(page.goto(baseUrl))がlaunch、close()でブラウザを破棄します。simctl のデバイスは無いので、run はダミーのリースを使い、device control は持ちません。 - デバイスモード(BE-0228): web ターゲットの
deviceMode設定が、各BrowserContextの生成のしかたを選びます。desktop(既定で、素のデスクトップコンテキスト。従来と変わりません)か、Playwright のデバイスプリセット名(例iPhone 13)です。プリセットはplaywright.devicesに対して解決し、その記述子(viewport /device_scale_factor/is_mobile/has_touch/user_agent)をreduced_motion="reduce"と並べてnew_context(**kwargs)にマージするので、ターゲットをそのモバイル端末として駆動します。記述子は遅延して解決し(config 読み込みが Playwright を import することはありません)、記憶するので、reset_context(クロールのクリーンな起点)とrelaunch(BE-0077)は同一のコンテキストを組み直します。モードはブラウザのライフサイクル全体を通して安定し、エンジンやreduced_motionが既に守っているのと同じ不変条件です。不明なプリセットはドライバー起動時にValueErrorで明示的に失敗します。これはデスクトップ級ブラウザでのエミュレーションであり(デスクトップ級ブラウザの中でモバイルの viewport とタッチ入力を用いる、Chrome DevTools のデバイスツールバーが行うのと同じもの)、実機のモバイルブラウザやデバイスクラウドではありません。本物のモバイル OS が必要なら Android バックエンドが経路です。 - 方向指定の
swipeはスクロールになる(BE-0227): 方向指定形式swipe: { on, direction }の意味は「スクロール」であり、マウスドラッグは web ページをスクロールしません。そこで web バックエンドは、実際にスクロールを起こす入力プリミティブへ、コンテキストの入力モード(上記のdeviceMode)に応じて振り分けます。デスクトップ(ポインター)コンテキストでは、ジェスチャーの起点でpage.mouse.wheel(...)を発火します。wheel の移動量は travel の符号を反転したものなので、upの swipe はページを下へスクロールさせ、トラックパッドやホイールとまったく同じ挙動になります。タッチコンテキスト(モバイルのdeviceMode)では、CDP による 1 本指の本物のタッチドラッグ(pinch/rotateと同じ経路)を使い、ページのタッチリスナとスクロールリスナが発火します。座標形式swipe: { from, to }は変わりません。canvas やマップのパン、ドラッグハンドルのための素のドラッグの最終手段として、page.mouseのドラッグのままです。codegenも方向指定形式にはデスクトップの wheel スクロールを出力するので、生成された Playwright テストは、従来の何も動かないドラッグではなく、物理的に正しい向きへスクロールします(codegen にはamountを掛ける viewport がないため、距離は既定の固定値です)。別途用意したdragアクション(要素アンカーのポインタドラッグ。リサイズ用の仕切りやスライダーなど)はドライバーのswipeに振り分けられるので、web では掴んだ要素を実際に動かすpage.mouseのドラッグになります。スクロールするだけの方向指定swipeとは対照的です。 - マルチタッチ(BE-0054):
pinch/rotateは Chromium DevTools プロトコル(Input.dispatchTouchEvent)で 2 本指のドラッグとして合成します。mouseは単一ポインタなので、ジェスチャは CDP 経由(実際のタッチと同じ経路)で送り、ページのタッチリスナが発火します。要素の中心を 2 本指の基準点とし、scaleが指の間隔を広げ/狭め、radiansがその中心まわりに回転させます。 - ネイティブネットワーク(BE-0054): Playwright はページが出すすべてのリクエストを見られるので、
--networkはアプリ側の協力なしに web でも動きます。network_collector()がページのrequestfinishedイベントを iOS と同じNetworkExchangeに変換するため、requestアサーションもnetwork.json証跡もそのまま使えます。シナリオのmocksはpage.routeでその場で fulfill します。一致したリクエストには既定のレスポンスを返し、mocked: trueを立てて記録します。一致判定は決定論的なrequestマッチャを再利用し、モデルは一切使いません。 - コンソール / ページエラー、動画の証跡(BE-0054):
deviceLogキャプチャ種別はブラウザのコンソールと未捕捉のページエラーを<scenario>/device.logにストリームし、videoはシナリオ全体を録画します。どちらも simctl ではなく Playwright ネイティブで、iOS の os_log / simctl 動画に相当します。録画はシナリオのcaptureにvideoがある時だけ有効化し(BrowserContextをrecord_video_dir付きで生成)、videoインターバルが context クローズ時に<scenario>/scenario.mp4(中身は webm)へ確定させます。プールがドライバのdriver_interval(adb バックエンドと共有する、driver 供給の区間証跡 seam)をFileSinkに注入するので、バックエンド非依存の同じcaptureポリシーが両方を運びます。
playwrightは遅延 import されます(実際にブラウザを起動するときだけ読み込む)。そのため既定の CLI パスには決して載りません(tests/serve/test_import_guard.pyで固定)。インストールはuv sync --extra web+uv run playwright install chromium。demos/webのデモ(make -C demos/web e2e)が小さな静的 web アプリを端から端まで駆動します。
FakeDriver¶
実機なしで orchestrator / runner / record をテストするためのインメモリ実装です。実装: drivers/fake.py。
screen(Elementのリスト)を保持し、query()で返します。tap/long_pressは本物同様resolve_uniqueを通します(曖昧 / 不在はSelectorError)。reactコールバックで「操作に応じて画面が変わる」動作をスクリプトできます。actionsに実行した操作を記録します(検証用)。
def react(driver, kind, arg):
if kind == "tap":
driver.screen = [...] # タップ後の画面に差し替える
FakeDriver(screen=[...], react=react)
バックエンド選択と actuator¶
実装: bajutsu/backends.py。
PLATFORMS = { # プラットフォームトークンは actuator 列へ展開(安定度順)
"ios": ("xcuitest", "idb"), # 最も高機能なものから(BE-0019)
"android": ("adb",), # 計画中
"web": ("playwright",), # 実装済み(BE-0041)
"fake": ("fake",), # メモリ上のテスト/デモ用ドライバ
}
COST_ORDER = {"ios": ("idb", "xcuitest")} # 安いものから(BE-0240)。idb はツールチェーンも runner も不要
IMPLEMENTED = {"idb", "fake", "playwright", "xcuitest"} # 今日ドライバがある actuator
def default_available(actuator) -> bool: # 実装済みかつ裏のツールがあるか(playwright はパッケージ import、fake は常に可)
def resolve_actuators(backends) -> list: # 各トークン(プラットフォーム/actuator)を actuator 列へ展開
def select_actuator(backends, available) -> str: # 安定度順で最初の「実装済み かつ 利用可能」
def select_actuator_cost_first(backends, available) -> str: # シナリオ無しで、最も安い利用可能な actuator(BE-0267)
def select_actuator_for_scenario(backends, scenario, available, caps) -> str: # 利用可能かつ十分な、最も安い actuator(BE-0240)
def make_driver(actuator, udid, *, base_url=None, runner_port=None) -> Driver: # "xcuitest"→XcuitestDriver, "idb"→IdbDriver, "playwright"→PlaywrightDriver, "fake"→FakeDriver
- バックエンドトークンは、プラットフォーム(
ios/android/web/fake)か、具体的な actuator(例:idb)のどちらかです。actuator を複数持つプラットフォームはシナリオごとに解決します(BE-0240)。--backend ios(またはbackend: [ios])は、各シナリオを、そのステップが使える最も安い actuator で走らせます。既定はidbで、シナリオの構文が idb に無い能力を要求するとき(例:pinch/rotateはmultiTouch)だけ XCUITest へ昇格します。idb の能力集合は XCUITest の真部分集合なので、idb を特に必要とするシナリオはありません。idb が優先されるのはコストのためだけです。 - 二つの順序が二つの問いに答えます。安定度順(
PLATFORMS、最も高機能なものから。concepts)はselect_actuatorを駆動します。これは、まだシナリオが手元に無くコストも問わない場面(doctor、プールの起動時セットアップ、明示的な単一 actuator の固定)で使う、可用性だけの選択です。コスト順(COST_ORDER、最も安いものから)はselect_actuator_for_scenarioとselect_actuator_cost_firstの両方を駆動し、両者は候補解決の前段(_cost_ordered_available)を共有します。select_actuator_for_scenarioはさらにcapability_preflight.unsupported(BE-0082)を各候補の能力集合に対して再利用し、利用可能かつそのシナリオのステップに十分な最初の候補を返します。select_actuator_cost_firstは同じコスト優先の選択をシナリオ無しで行うもので、能力の昇格判定なしに「立ち上げられる中で最も安い actuator」だけが必要な場面(serve の Author タブの Capture と Enrich。BE-0267 — それ以前は安定度順の選択を使っており、serve は XCUITest の runner を起動しないため[ios]設定のターゲットで失敗していました)で使います。どちらも、解決した候補が 1 つに収まる場合はselect_actuatorに委譲し(その診断メッセージを保ちます)、明示的な単一 actuator の要求は決して昇格しません(--udidと同じ、固定の指定です)。利用可能なものが無ければRuntimeError(CLI は終了コード 2)。 webはplaywrightに、androidはadbに解決され、どちらも実装済みです (vision → reach)。本当に未知のトークンはスキップされます(前方互換: 古いビルドでも、将来のバックエンドを列挙した config を実行できます)。- 可用性判定
availableは注入可能です(テストで差し替え可)。既定はshutil.which(fakeは実行ファイル不要で常に利用可能)。 - actuator はシナリオごとに 1 つ確定し、そのシナリオの実行のあいだ固定です(BE-0240)。どの瞬間もリースしたデバイスを操作する actuator はちょうど 1 つで、実行の途中で切り替わりません。これは従来の「run ごとに固定」という単位をシナリオ単位へ狭めたもので、単一 actuator の規則を緩めるものではありません。
操作は単一の actuator にとどまります。リスト内の非 actuator バックエンドは、read-only な証跡フォールバックとして機能します(DESIGN §9、BE-0020)。その種別の provider には、actuator が欠く能力(例: Capability.NETWORK)を capabilities() で表明する同一プラットフォームのバックエンドを充てます。フォールバックには狭い EvidenceProvider Protocol 経由でのみアクセスし、tap/type/swipe は型レベルで不可能です。gap を埋めるバックエンドが無い場合は、理由を記録して skip します(SkippedCapture)。なだらかな劣化であり、run の失敗にはなりません。来歴の詳細は証跡の providerを参照してください。
環境管理(simctl)¶
実装: bajutsu/simctl.py。コマンドビルダは純関数(単体テスト済み)で、実行は注入可能な RunFn 経由です。
| メソッド | コマンド | 備考 |
|---|---|---|
erase() |
simctl erase <udid> |
クリーン環境 |
boot() |
simctl boot <udid> |
既に boot 済みなら冪等(エラーを握りつぶす) |
launch(bundle, args, env) |
simctl launch --terminate-running-process <udid> <bundle> <args> |
env は SIMCTL_CHILD_* で注入 |
terminate(bundle) |
simctl terminate <udid> <bundle> |
未起動でも無視 |
openurl(url) |
simctl openurl <udid> <url> |
deeplink |
screenshot(path) |
simctl io <udid> screenshot <path> |
— |
launch env の注入: アプリへ渡す env 変数は、親プロセスに
SIMCTL_CHILD_<NAME>として設定すると子(アプリ)に<NAME>で渡ります。child_env()がこの変換を行います。showcase アプリのSHOWCASE_UITEST等の launch hook はこの仕組みを使います(showcase)。
video / deviceLog の区間録りも simctl io recordVideo / simctl spawn log stream を使いますが、これらは証跡サブシステム側(evidence/intervals.py)に置かれています(evidence)。