コンテンツにスキップ

Webとモバイルを横断するテスト

ユースケース: プロダクトの本当のユーザージャーニーがデバイスをまたぐ——Webでサインアップしてスマホで認証する。デスクトップやTVの画面のQRコードをアプリでスキャンしてログインする。キャンペーンページのディープリンクをタップするとネイティブアプリに着地する。それぞれ単体では動くのに、バグは受け渡しの瞬間に住んでいます。

▶ 動くサンプル: test-across-web-and-mobile.spec.ts / デモを触る(実行方法は examples/README)

  • 2つのエコシステムに共通のランナーがない。 Playwrightはネイティブアプリを操作できず、Appiumはデスクトップブラウザが苦手。どちらもジャーニー全体を所有できません。
  • 受け渡しの成果物——QRコード、マジックリンク、ワンタイムコード——は片側で生成され、もう片側で消費されるため、単体ではどちらのツールも検証できません。
  • セッションと状態はバックエンドにあり、2つのUIはそれを覗く2つの窓にすぎません。

オーケストレーターは1つ、ドライバーは2つ

Section titled “オーケストレーターは1つ、ドライバーは2つ”

Playwrightをテストランナーとして維持し、テスト内からWebdriverIOクライアント経由でAppiumセッションを接続します。テストファイル1つ、レポート1つで両デバイスをカバー:

import { test, expect } from '@playwright/test';
import { remote, type Browser as Wdio } from 'webdriverio';
let app: Wdio;
test.beforeAll(async () => {
app = await remote({
hostname: 'localhost', port: 4723, // Appiumサーバー
capabilities: {
platformName: 'Android',
'appium:automationName': 'UiAutomator2',
'appium:app': process.env.APP_APK, // iOSなら XCUITest
},
});
});
test.afterAll(async () => { await app?.deleteSession(); });
test('Webで始めたサインアップがアプリで完結する', async ({ page }) => {
// Web側
const email = `user-${Date.now()}@example.test`;
await page.goto('/signup');
await page.getByLabel('Email').fill(email);
await page.getByRole('button', { name: 'Sign up' }).click();
await expect(page.getByText('Check your phone')).toBeVisible();
// 受け渡し: 継続用リンクを取得(テスト用インボックスやテストAPIから)
const link = await fetchMagicLink(email); // メールテストのレシピ参照
// アプリ側: ディープリンクから入る
await app.execute('mobile: deepLink', { url: link, package: 'com.example.app' });
const welcome = app.$('~welcome-banner'); // accessibility id
await welcome.waitForDisplayed({ timeout: 15_000 });
await expect(page.getByText('Connected to your phone')).toBeVisible(); // Web側も反応する
});

受け渡しがビジュアルなら、目視ではなくデコードします。要素をスクリーンショット→Nodeでデコード→ペイロードをデバイスに渡す:

import jsQR from 'jsqr';
import { PNG } from 'pngjs';
const shot = await page.locator('#login-qr').screenshot();
const png = PNG.sync.read(shot);
const qr = jsQR(new Uint8ClampedArray(png.data), png.width, png.height);
await app.execute('mobile: deepLink', { url: qr!.data, package: 'com.example.app' });

これは同時にQRが読み取り可能であることの証明にもなります。コントラスト低下やクワイエットゾーンの欠けといった描画リグレッションは、デコードの段階で落ちます。

受け渡しの検証はUIではなくバックエンドで

Section titled “受け渡しの検証はUIではなくバックエンドで”

最も強いアサーションは「両側が1つのセッションを共有している」ことの確認です:

await expect
.poll(() => api.get(`/test/sessions?email=${email}`).then((r) => r.json()))
.toMatchObject({ devices: ['web', 'android'], linked: true });
  • これはあなたが持つ最も高価なテストです。デバイス横断ジャーニーは1〜2本に絞り、残りはプラットフォーム別のテストに押し下げてください。
  • CIではブラウザの隣にエミュレータが必要です(GitHub Actionsなら reactivecircus/android-emulator-runner、またはデバイスクラウド)。起動はジョブごとに1回、テストごとにはしない。
  • アプリ側の操作が単純なら、同じテストからCLIで起動するMaestroフローが、フルのAppiumセッションより軽い代替になります。
  • モバイルWebは別の(ずっと安い)問題です。Playwrightのデバイスエミュレーションはビューポートとタッチを再現しますが、アプリへの受け渡しは実行できません。