mobilewright について
mobilewright は、iOS と Android のアプリテスト、デバイス自動化、UI 検証に使う Playwright 風のフレームワークとスキルです。TypeScript と JavaScript に対応し、ビュー階層の確認やロケーター操作を備えています。
主な機能
- 01iOS と Android のデバイス自動化
- 02E2E、回帰、スモークテスト
- 03ビュー階層とスクリーンショットの確認
- 04ロケーターによる UI 操作
- 05テストフィクスチャと再試行アサーション
- 06設定、デバイス、テスト用 CLI
活用シーン
- 01モバイルアプリの回帰テスト実行
- 02デバイスでの反復操作の自動化
- 03実装した UI の実機確認
- 04スクリーンショットとアクセシビリティ階層の取得
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ mobile-next/ mobilewright-skill --skill mobilewrightインストール後、Claude Code で/mobilewrightを入力して読み込まれたか確認します。
詳細
Mobilewright スキル
iOS と Android 向けの Playwright に着想を得たフレームワーク、mobilewright を使用したモバイルデバイスの自動化、テスト、開発時の動作検証です。
次のような場合にこのスキルを使用します。
- モバイルアプリのテスト(E2E、回帰、スモークテスト)を作成する
- モバイルデバイスの操作を自動化する(スクレイピング、スクリーンショット、反復タスク)
- モバイルアプリの開発中に UI の実装を検証する — スクリーンショットを撮影し、ビュー階層をダンプして、記述したコードが期待どおりの結果を生成することを確認する
最初のステップ
コードを書く前に、コンテキストを確認します。
- 何をしていますか? ユーザーに、テスト、自動化スクリプト、または作成した UI の検証のどれかを尋ねます。
- 対象プラットフォームは何ですか? ユーザーに iOS、Android、または両方のどれかを尋ねます。
- TypeScript と JavaScript のどちらですか? ユーザーに尋ねます。どちらもサポートされています。
- バンドル 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があるか確認します - 自動的に見つけられない場合に限り、ユーザーに尋ねます
- iOS:
- 既存の
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) - プレースホルダーテキスト
次に、以下の優先順位に従ってロケーターを選択します。
ロケーターの優先順位(高い順)
| 優先度 | ロケーター | 理由 |
|---|---|---|
| 1 | getByTestId('login-btn') | 最も安定しています。開発者が設定するため、文言や i18n が変わっても影響を受けません。 |
| 2 | getByRole('button', { name: 'Sign In' }) | セマンティックで、クロスプラットフォームです。iOS と Android の両方でネイティブの種類にマッピングされます。 |
| 3 | getByLabel('Sign In') | アクセシビリティラベルです。アクセシビリティが適切に維持されていれば安定しています。 |
| 4 | getByPlaceholder('Search...') | テキストフィールドに適しています。Locator でのみ利用でき、Screen では利用できません。 |
| 5 | getByText('Sign In') | 不安定です。文言の更新、i18n、またはわずかな言い換えで変わります。 |
| 6 | getByType('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
利用可能なフィクスチャ
| フィクスチャ | スコープ | 説明 |
|---|---|---|
screen | test | デバイスの画面です。すべての要素操作に使用します |
device | worker | デバイス接続です。アプリのライフサイクル、向き、URL に使用します |
bundleId | test | 設定済みのバンドル 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 はセマンティックロールをプラットフォーム固有の型にマッピングします。
| ロール | iOS | Android |
|---|---|---|
button | Button, ImageButton | Button, ImageButton, ReactViewGroup* |
textfield | TextField, SecureTextField, SearchField | EditText, ReactEditText |
text | StaticText | TextView, Text, ReactTextView |
image | Image | ImageView, ReactImageView |
switch | Switch | Switch, Toggle |
checkbox | -- | Checkbox |
slider | Slider | SeekBar |
list | Table, CollectionView, ScrollView | ListView, RecyclerView, ReactScrollView |
header | NavigationBar | Toolbar |
link | Link | Link |
* 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')を使用してください。