mcpField
M
mobilewright

mobilewright

github.com/mobile-nextテスト・品質389 回インストール29 週間前に更新
リポジトリ

mobilewright について

mobilewright は、iOS と Android のアプリテスト、デバイス自動化、UI 検証に使う Playwright 風のフレームワークとスキルです。TypeScript と JavaScript に対応し、ビュー階層の確認やロケーター操作を備えています。

主な機能

  1. 01iOS と Android のデバイス自動化
  2. 02E2E、回帰、スモークテスト
  3. 03ビュー階層とスクリーンショットの確認
  4. 04ロケーターによる UI 操作
  5. 05テストフィクスチャと再試行アサーション
  6. 06設定、デバイス、テスト用 CLI

活用シーン

  1. 01モバイルアプリの回帰テスト実行
  2. 02デバイスでの反復操作の自動化
  3. 03実装した UI の実機確認
  4. 04スクリーンショットとアクセシビリティ階層の取得

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/mobile-next/mobilewright-skill --skill mobilewright

インストール後、Claude Code で/mobilewrightを入力して読み込まれたか確認します。

詳細

Mobilewright スキル

iOS と Android 向けの Playwright に着想を得たフレームワーク、mobilewright を使用したモバイルデバイスの自動化、テスト、開発時の動作検証です。

次のような場合にこのスキルを使用します。

  • モバイルアプリのテスト(E2E、回帰、スモークテスト)を作成する
  • モバイルデバイスの操作を自動化する(スクレイピング、スクリーンショット、反復タスク)
  • モバイルアプリの開発中に UI の実装を検証する — スクリーンショットを撮影し、ビュー階層をダンプして、記述したコードが期待どおりの結果を生成することを確認する

最初のステップ

コードを書く前に、コンテキストを確認します。

  1. 何をしていますか? ユーザーに、テスト、自動化スクリプト、または作成した UI の検証のどれかを尋ねます。
  2. 対象プラットフォームは何ですか? ユーザーに iOS、Android、または両方のどれかを尋ねます。
  3. TypeScript と JavaScript のどちらですか? ユーザーに尋ねます。どちらもサポートされています。
  4. バンドル ID を自動的に見つけます。 尋ねずに探します。
    • iOS:*.pbxproj ファイル内の PRODUCT_BUNDLE_IDENTIFIER、または Expo の app.json/app.config.js 内の bundleIdentifier を検索します
    • Android:build.gradle または build.gradle.kts 内の applicationId、または AndroidManifest.xml 内の package を検索します
    • React Native/Expo:app.json 内の expo.ios.bundleIdentifier と expo.android.package を確認します
    • 見つからない場合は、bundleId が設定された既存の mobilewright.config.ts があるか確認します
    • 自動的に見つけられない場合に限り、ユーザーに尋ねます
  5. 既存の mobilewright.config.ts はありますか? なければ作成します。

プロジェクトのセットアップ

新しいプロジェクトをスキャフォールドします。

npx mobilewright init

これにより、mobilewright.config.ts と example.test.ts が作成されます。環境を確認します。

npx mobilewright doctor
CLI コマンド
npx mobilewright init                              # scaffold config + example test
npx mobilewright doctor                            # verify environment setup
npx mobilewright devices                           # list connected devices/simulators
npx mobilewright test                              # run tests
npx mobilewright screenshot                        # take a screenshot of a connected device
npx mobilewright screenshot -o login.png           # save to specific file
npx mobilewright screenshot -d <device-id>         # target a specific device
設定
import { defineConfig } from 'mobilewright';

export default defineConfig({
  platform: 'ios',                // 'ios' or 'android'
  bundleId: 'com.example.myapp',  // app bundle ID
  deviceName: 'iPhone 16',        // regex to match device name (optional)
  timeout: 10_000,                // global timeout in ms (optional)
});

UI の確認 — 最初に行うこと

ロケーターを書く前に、必ず UI ツリーを確認してください。 要素名、ラベル、種類を推測してはいけません。screen.viewTree() を使って実際のアクセシビリティ階層を取得し、確認した内容に基づいて最適なロケーター戦略を選びます。

const tree = await screen.viewTree();
console.log(JSON.stringify(tree, null, 2));

これはビュー階層全体を JSON として返します。次の項目を確認します。

  • アクセシビリティ識別子(testId)
  • アクセシビリティラベル(label)
  • 要素の種類(type)
  • テキストコンテンツ(text/value)
  • プレースホルダーテキスト

次に、以下の優先順位に従ってロケーターを選択します。

ロケーターの優先順位(高い順)
優先度ロケーター理由
1getByTestId('login-btn')最も安定しています。開発者が設定するため、文言や i18n が変わっても影響を受けません。
2getByRole('button', { name: 'Sign In' })セマンティックで、クロスプラットフォームです。iOS と Android の両方でネイティブの種類にマッピングされます。
3getByLabel('Sign In')アクセシビリティラベルです。アクセシビリティが適切に維持されていれば安定しています。
4getByPlaceholder('Search...')テキストフィールドに適しています。Locator でのみ利用でき、Screen では利用できません。
5getByText('Sign In')不安定です。文言の更新、i18n、またはわずかな言い換えで変わります。
6getByType('Button')プラットフォーム固有です。コードが iOS または Android の要素の種類に依存します。

テストファイルの構造

モバイル用フィクスチャを拡張する Playwright Test の @mobilewright/test を使用します。

import { test, expect } from '@mobilewright/test';

test.use({ bundleId: 'com.example.myapp', video: 'on' });

test.beforeEach(async ({ device, bundleId }) => {
  // Fresh app state for every test
  await device.terminateApp(bundleId).catch(() => {});
  await device.launchApp(bundleId);
});

test.describe('login flow', () => {
  test('user can sign in with valid credentials', async ({ screen }) => {
    await screen.getByLabel('Email').fill('[email protected]');
    await screen.getByLabel('Password').fill('password123');
    await screen.getByRole('button', { name: 'Sign In' }).tap();

    await expect(screen.getByText('Welcome back')).toBeVisible();
  });
});

テストを実行します。

npx mobilewright test
npx mobilewright test login.test.ts              # specific file
npx mobilewright test --grep "sign in"           # run a single test by name
npx mobilewright test --reporter list            # line-by-line output, good for CI/agents
npx mobilewright test --reporter json            # machine-readable JSON output
利用可能なフィクスチャ
フィクスチャスコープ説明
screentestデバイスの画面です。すべての要素操作に使用します
deviceworkerデバイス接続です。アプリのライフサイクル、向き、URL に使用します
bundleIdtest設定済みのバンドル ID 文字列です
test.use() によるテストオプション
test.use({
  bundleId: 'com.example.app',    // override app bundle ID
  platform: 'android',            // override platform
  deviceId: '5A5FCFCA-...',       // target specific device
  video: 'on',                    // record video ('on' | 'retain-on-failure')
});

自動化スクリプトの構造

テストランナーを必要としない単独スクリプトの場合:

import { ios, android } from 'mobilewright';

const device = await ios.launch({ bundleId: 'com.example.myapp' });
const { screen } = device;

// Do your work
const tree = await screen.viewTree();
const screenshot = await screen.screenshot();
await screen.getByRole('button', { name: 'Start' }).tap();

// Always close when done
await device.close();
起動オプション
// Auto-discover first booted simulator
const device = await ios.launch();

// Launch a specific app
const device = await ios.launch({ bundleId: 'com.example.app' });

// Match device by name (regex)
const device = await ios.launch({ deviceName: 'My.*iPhone' });

// Target exact device
const device = await ios.launch({ deviceId: '5A5FCFCA-...' });

// List available devices
const devices = await ios.devices();
const devices = await android.devices();

開発時の動作検証

モバイルアプリの開発時には、mobilewright を使って UI の実装を検証します。

import { ios } from 'mobilewright';

const device = await ios.launch({ bundleId: 'com.example.myapp' });
const { screen } = device;

// Take a screenshot to verify the UI looks correct
const screenshot = await screen.screenshot();

// Dump the view tree to verify elements exist and have correct properties
const tree = await screen.viewTree();

// Verify specific elements are present and have expected content
const title = await screen.getByRole('text', { name: 'Welcome' }).getText();
const isButtonEnabled = await screen.getByTestId('submit-btn').isEnabled();

await device.close();

API クイックリファレンス

Screen — ロケーターファクトリー
screen.getByLabel('Email')                          // accessibility label
screen.getByTestId('login-button')                  // accessibility identifier
screen.getByText('Welcome')                         // exact text match
screen.getByText(/welcome/i)                        // regex match
screen.getByText('welcome', { exact: false })       // substring match
screen.getByType('TextField')                       // native element type
screen.getByRole('button', { name: 'Sign In' })     // semantic role + name
Screen — 直接操作
await screen.screenshot()                            // PNG buffer
await screen.screenshot({ format: 'jpeg', quality: 80 })
await screen.swipe('up')                             // scroll down
await screen.swipe('down', { distance: 300, duration: 500 })
await screen.pressButton('HOME')
await screen.goBack()                                // press BACK (Android only, no-op on iOS)
await screen.viewTree()                              // full view hierarchy as JSON
Locator — 操作

すべての操作で、要素が表示され、有効で、安定するまで自動的に待機します。

await locator.tap()
await locator.doubleTap()
await locator.longPress({ duration: 1000 })
await locator.fill('[email protected]')              // tap to focus + type
Locator — クエリ
await locator.getText()       // visible text / label
await locator.getValue()      // input value
await locator.isVisible()     // boolean
await locator.isEnabled()     // boolean
await locator.isSelected()    // boolean
await locator.isFocused()     // boolean
await locator.isChecked()     // boolean
Locator — コレクション

一致する複数の要素を操作します。

await locator.count()         // number of matching elements
await locator.all()           // array of Locator, one per match
locator.first()               // first matching element
locator.last()                // last matching element
locator.nth(2)                // element at index (0-based, negative counts from end)

例 — リストの3番目の項目をタップします。

await screen.getByType('Cell').nth(2).tap();

例 — 検索結果の件数を確認します。

const count = await screen.getByType('Cell').count();
expect(count).toBe(5);
Locator — チェーン

親要素の範囲内にクエリを限定します。

const row = screen.getByType('Cell');
await row.getByRole('button', { name: 'Delete' }).tap();
await row.getByPlaceholder('Enter name').fill('Arthur');

注:getByPlaceholder は Locator(チェーン)でのみ使用でき、Screen から直接は使用できません。

Locator — 待機
await locator.waitFor({ state: 'visible' })
await locator.waitFor({ state: 'hidden' })
await locator.waitFor({ state: 'enabled' })
await locator.waitFor({ state: 'disabled', timeout: 10_000 })
アサーション

すべてのアサーションは、条件が満たされるかタイムアウトするまで自動で再試行されます。否定には .not を使用します。

await expect(locator).toBeVisible();
await expect(locator).not.toBeVisible();
await expect(locator).toBeEnabled();
await expect(locator).toBeDisabled();
await expect(locator).toHaveText('Welcome back!');
await expect(locator).toHaveText(/welcome/i);
await expect(locator).toContainText('back');
await expect(locator).toHaveValue('[email protected]');
await expect(locator).toBeHidden();
await expect(locator).toBeChecked();
await expect(locator).toBeSelected();
await expect(locator).toBeFocused();
await expect(locator).toBeVisible({ timeout: 10_000 });
デバイス
// App lifecycle
await device.launchApp('com.example.app');
await device.launchApp('com.example.app', { locale: 'fr_FR' });
await device.terminateApp('com.example.app');
await device.installApp('/path/to/app.ipa');
await device.uninstallApp('com.example.app');
const apps = await device.listApps();
const foreground = await device.getForegroundApp();

// Navigation / deep links
await device.goto('myapp://settings');
await device.openUrl('https://example.com');

// Orientation
await device.setOrientation('landscape');
const orientation = await device.getOrientation();

// Recording
await device.startRecording({ output: 'recording.mp4' });
await device.stopRecording();

// Cleanup
await device.close();

ロールのマッピング

getByRole はセマンティックロールをプラットフォーム固有の型にマッピングします。

ロールiOSAndroid
buttonButton, ImageButtonButton, ImageButton, ReactViewGroup*
textfieldTextField, SecureTextField, SearchFieldEditText, ReactEditText
textStaticTextTextView, Text, ReactTextView
imageImageImageView, ReactImageView
switchSwitchSwitch, Toggle
checkbox--Checkbox
sliderSliderSeekBar
listTable, CollectionView, ScrollViewListView, RecyclerView, ReactScrollView
headerNavigationBarToolbar
linkLinkLink

* ReactViewGroup は、clickable="true" または accessible="true" の場合にのみ button に一致します。

マッピングが存在しない場合は、型を直接照合します。

よくあるパターン

テストごとにアプリの状態をリセットする
test.beforeEach(async ({ device, bundleId }) => {
  await device.terminateApp(bundleId).catch(() => {});
  await device.launchApp(bundleId);
});
失敗時のスクリーンショット(自動)

@mobilewright/test フィクスチャは、テストが失敗すると自動的にスクリーンショットをキャプチャします。手動でスクリーンショットを撮る場合:

test.afterEach(async ({ screen }, testInfo) => {
  const screenshot = await screen.screenshot();
  await testInfo.attach('screenshot', { body: screenshot, contentType: 'image/png' });
});
画面外の要素を見つけるためにスクロールする
await screen.swipe('up');  // scroll down to reveal content below
await expect(screen.getByText('Footer Content')).toBeVisible();
繰り返し使用するフローをヘルパーとして抽出する
async function login(screen: any, email: string, password: string) {
  await screen.getByLabel('Email').fill(email);
  await screen.getByLabel('Password').fill(password);
  await screen.getByRole('button', { name: 'Sign In' }).tap();
}

test('logged in user sees dashboard', async ({ screen }) => {
  await login(screen, '[email protected]', 'pass123');
  await expect(screen.getByText('Dashboard')).toBeVisible();
});

ルール

必ず行うこと
  • まず UI ツリーを確認します。 ロケーターを書く前に screen.viewTree() を実行してください。推測で決めないでください。
  • 再試行するアサーションを使用します。 await expect(locator).toBeVisible() は自動的に再試行します。await locator.isVisible() は再試行しません。
  • 座標ではなくロケーターを使用します。 ロケーターはレイアウトの変更に強いです。
  • テストの間にアプリを再起動します。 terminateApp + launchApp で状態をクリーンにできます。
  • 正確な文言が変わる可能性がある場合は、柔軟なテキスト照合に { exact: false } を使用します。
  • テストを読みやすく保つため、繰り返し使用するナビゲーションはヘルパー関数にまとめます。
絶対に行わないこと
  • 要素の操作に screen.tap(x, y) を絶対に使用しないでください。 生の座標はデバイス、画面サイズ、OS バージョンによって異なります。必ずロケーターで要素を見つけてください。
  • タイムアウトをハードコードしないでください。 要素が表示されることを期待して await sleep(2000) を実行しないでください。自動待機アクションと再試行アサーションを使用すれば、タイミングは自動的に処理されます。
  • アクションの前に waitFor を使用しないでください。 tap() や fill() などのアクションには、すでに自動待機が組み込まれています。await locator.waitFor({ state: 'visible' }); await locator.tap(); は冗長なので、await locator.tap() だけを使用してください。
  • テストのアサーションに isVisible() を使用しないでください。 これはその時点でのブール値を返します。アサーションが満たされるまで再試行する expect(locator).toBeVisible() を使用してください。
  • 要素を見つけるためにスクリーンショットを撮って画像を解析しないでください。 screen.viewTree() を使ってアクセシビリティツリーをプログラムから確認してください。
  • ロールが存在する場合に、プラットフォーム固有の型をハードコードしないでください。 getByType('UIButton') ではなく、getByRole('button') を使用してください。

コメント