TestMu AI(LambdaTest)のダッシュボードを見やすく整理する

PlaywrightのテストをTestMu AI(LambdaTest)で実行したあと、ダッシュボードでテスト結果を見ると、どれが何のテストなのか判別しづらい状態でした。同じ日に実行したテストが1つの画面に混在して表示されていたり、全てのテストファイル・個々のテストコマンドが1列に繋がって並んでいたり・・・。この記事では、「いつ・どの環境での実行か」「どのテストでエラーが起きているか」がすぐわかるダッシュボードを目指して、表示を整えます。

PlaywrightのテストをTestMu AI(LambdaTest)で実行したあと、ダッシュボードでテスト結果を見ると、どれが何のテストなのか判別しづらい状態でした。同じ日に実行したテストが1つの画面に混在して表示されていたり、全てのテストファイル・個々のテストコマンドが1列に繋がって並んでいたり・・・。この記事では、「いつ・どの環境での実行か」「どのテストでエラーが起きているか」がすぐわかるダッシュボードを目指して、表示を整えます。

改修前のダッシュボード

表示を改修する前のダッシュボードは、以下のスクリーンショットのような感じです。

同日に2回実行したテストがすべてこの中に集約され、また全てのテストコマンドが1列に並んでおりどこに何のテスト結果が表示されているのかが非常に見づらくなっています。

テストを並列実行するとある程度表示は分散されるのですが、テスト間で共有するDBデータの都合で、ワーカー数は1(直列実行)にしています。

ダッシュボードの表示単位と現在の設定

改修前に、まずダッシュボードの表示単位を確認します。LambdaTestのダッシュボードは「ビルド」の中に「セッション」、さらにその中に「コマンド」が並ぶ構造になっています。

【現状】
LambdaTest ダッシュボード
└── ビルド一覧      ← 今は「Playwright Demo」と固定値
    └── セッション    ← 今は全テストが1つに混在
        └── コマンド群  ← 全テストのコマンドが並ぶ

そして以下は、改修前のconfigファイルの設定の抜粋です。

playwright.lambdatest.config.ts
・・・・・
function ltEndpoint(sessionName: string) {
  return `wss://cdp.lambdatest.com/playwright?capabilities=${encodeURIComponent(
    JSON.stringify({
      browserName: ltBrowser,
      browserVersion,
      'LT:Options': {
・・・・・
        build: 'Playwright Demo',              // 固定値
        name: `${sessionName} [${ltPlatform}]`, // 目的が分かりにくいセッション名
        platform: ltPlatform,
・・・・・
     },
    })
  )}`;

export default defineConfig({
  projects: [
    // DB リセット&シード(ローカル実行)
    { name: 'db-setup', testMatch: '**/db.setup.ts' },

    // 全テスト
    {
      name: `${deviceKey}`,
      testIgnore: ['**/db.setup.ts'],
      use: {
        ...selectedDevice,
        connectOptions: { wsEndpoint: ltEndpoint(`${deviceKey}`) },
      },
      dependencies: ['db-setup'],
    },
  ],
});
・・・・・

ここからbuildnameを中心に変更して、次のような形となるように整理していきます。

【整理後】
LambdaTest ダッシュボード
└── ビルド一覧      ← 動的なビルド名で、いつ・何の実行かがわかる名前に
    ├── セッション    ← test()名を表示し、何のテストかを明確に
    │   └── コマンド群  ← 1つのtest()に紐づくコマンドのみ
    ├── セッション
    │   └── コマンド群
    └── セッション
        └── コマンド群

① ビルドを動的に生成する

LambdaTestの「ビルド」は、テスト結果をまとめる箱のようなもので、同じビルド名なら同じ箱に入り、違うビルド名なら別の箱となります。

ビルド名が固定値の場合、ダッシュボードには全く同じビルド名が並びます。さらに同一日に実行したテストも全て1つのビルドに含まれるため、1日に複数回実行した場合は何回目の実行かがわかりにくくなっていました。

そこで、以下のようにデバイス・OS・日時を含めた動的な名前に変更します。

playwright.lambdatest.config.ts
・・・・・
// ビルド名は「1回の実行=1ビルド」とする
process.env.LT_BUILD ??= `食事管理アプリ [${deviceKey} / ${ltPlatform} / ${new Date().toLocaleString('ja-JP')}]`;
const buildName = process.env.LT_BUILD;
・・・・・
'LT:Options': {
  build: buildName,
・・・・・
}
・・・・・

LT_BUILDという環境変数に一旦代入しています。これは実行中にワーカーが変わっても同じビルド名を使うようにするためです。

Playwrightは仕様上、テストが失敗するとワーカーを破棄し、新しいワーカーでテストの続きを開始します。その際にビルドが作り直されるため、一回のテスト実行でもLambdaTestのダッシュボード上では別のビルドという扱いになってしまいます。

これを避けるため、テスト実行の最初にLT_BUILDにビルド名を保持しておきます。そうすることでワーカーが変わっても、同じビルド名のままテストを続けることができます。

この設定で、一度テストを実行してダッシュボードの表示を確認してみます。

固定値ではなく、“食事管理アプリ [chrome / Windows 11 / 2026/5/17・・・]“のような動的な名前が表示されるようになりました。

② test() ごとにセッション化する

次は、セッション名にテストタイトルを反映させます。

ここではlt-capabilities.tslt-fixture.tsという2つのファイルを新しく作ります。それぞれのファイルの役割は以下のようになります。

  • lt-capabilities.ts:LambdaTestに「使用するブラウザ・OS、どのアカウントで動かすか」という接続情報を設定する
  • lt-fixture.ts:lt-capabilities.tsの情報を使って、実際にLambdaTestのブラウザへ接続する

まずはlt-capabilities.tsから。

e2e/support/lt-capabilities.ts
import { devices } from '@playwright/test';
import dotenv from 'dotenv';
import { fileURLToPath } from 'url';
import path from 'path';

const __dirname = path.dirname(fileURLToPath(import.meta.url));

dotenv.config({ path: path.resolve(__dirname, '..', '..', '.env') });
dotenv.config({ path: path.resolve(__dirname, '..', '..', '.env.playwright'), override: true });

const LT_USERNAME = process.env.LT_USERNAME ?? '';
const LT_ACCESS_KEY = process.env.LT_ACCESS_KEY ?? '';

// ---- デバイスマッピング ----
// DEVICE 変数1つで「LTブラウザ名・デフォルトOS・Playwrightデバイス」が連動して決まる。
// 不正な組み合わせ(Windows上のSafariなど)を防ぐため、OSはここで固定する。
// LT_PLATFORM を指定した場合のみ上書き可能(例: Windows 10 vs 11の比較)。
type DeviceConfig = {
  ltBrowser: string;  // LambdaTest に渡す browserName
  browserVersion: string;  // ブラウザバージョン(LambdaTest capability generator の値に合わせる)
  platform: string;  // デフォルトOS(LT_PLATFORM で上書き可)
  device: string;  // Playwright devices キー(画面サイズ・UA用)
};

const DEVICE_MAP: Record<string, DeviceConfig> = {
  chrome: { ltBrowser: 'Chrome', browserVersion: 'latest', platform: 'Windows 11', device: 'Desktop Chrome' },
  firefox: { ltBrowser: 'pw-firefox', browserVersion: 'latest', platform: 'Windows 11', device: 'Desktop Firefox' },
  edge: { ltBrowser: 'Microsoft Edge', browserVersion: 'latest', platform: 'Windows 11', device: 'Desktop Edge' },
  safari: { ltBrowser: 'pw-webkit', browserVersion: 'latest', platform: 'MacOS Tahoe', device: 'Desktop Safari' },
  ios: { ltBrowser: 'pw-webkit', browserVersion: 'latest', platform: 'MacOS Tahoe', device: 'iPhone 15' },
};

const deviceKeyEnv = process.env.DEVICE ?? 'chrome';

if (!(deviceKeyEnv in DEVICE_MAP)) {
  console.error(`エラー: DEVICE="${deviceKeyEnv}" は未定義です。有効な値: ${Object.keys(DEVICE_MAP).join(', ')}`);
  process.exit(1);
}

const { ltBrowser, browserVersion, platform: defaultPlatform, device: deviceName } = DEVICE_MAP[deviceKeyEnv];
const ltPlatform = process.env.LT_PLATFORM ?? defaultPlatform;

// config 側で project 名・use に使う
export const deviceKey = deviceKeyEnv;
export const selectedDevice = devices[deviceName];

process.env.LT_BUILD ??= `食事管理アプリ [${deviceKey} / ${ltPlatform} / ${new Date().toLocaleString('ja-JP')}]`;
const buildName = process.env.LT_BUILD;

// ---- LambdaTest CDP エンドポイント ----
// 「整理前」の状態
export function ltEndpoint(sessionName: string) {
  return `wss://cdp.lambdatest.com/playwright?capabilities=${encodeURIComponent(
    JSON.stringify({
      browserName: ltBrowser,
      browserVersion,
      'LT:Options': {
        user: LT_USERNAME,
        accessKey: LT_ACCESS_KEY,
        build: buildName,
        name: sessionName,
        platform: ltPlatform,
        tunnel: true,
        network: true,
        console: true,
        video: true,
        useSpecificBundleVersion: false,
      },
    })
  )}`;
}

次にlt-fixture.tsです。冒頭でlt-capabilities.tsをインポートします。

e2e/support/lt-fixture.ts
import { test as base, chromium } from '@playwright/test';
import { ltEndpoint } from './lt-capabilities';

export const test = base.extend({
  page: async ({}, use, testInfo) => {
    // testInfo.title = test('・・・') の第一引数(テスト関数名)を渡す
    const wsEndpoint = ltEndpoint(testInfo.title);
    const browser = await chromium.connect(wsEndpoint);
    const context = await browser.newContext();
    const page = await context.newPage();

    await use(page);

    await context.close();
    await browser.close();
  },
});

export { expect } from '@playwright/test';

testInfo.titleを使用し、実行中のtest()の第一引数であるテスト名をLT:Options.nameに渡すことで、テストごとに別セッションとしてダッシュボードに記録されるようになります。

また、上記のファイルを作成したので、元のconfigファイルから不要となったenv読み込み処理・デバイスマッピング・ltEndpoint()などがごっそり無くなります。①でconfigに書いたビルド名の処理もこのlt-capabilities.tsに移り、playwright.lambdatest.config.tsは以下のようにdefineConfig()に関する記述のみなります。

playwright.lambdatest.config.ts
import { defineConfig } from '@playwright/test';
// 新しく作成したlt-capabilities.tsをimport
import { deviceKey, selectedDevice } from './e2e/support/lt-capabilities';

// ---- Playwright 設定 ----
export default defineConfig({
・・・・・
  projects: [
    // DB リセット&シード(ローカル実行)
    { name: 'db-setup', testMatch: '**/db.setup.ts' },

    // 全テスト → LambdaTest クラウドブラウザ
    {
      name: `${deviceKey}`,
      testIgnore: ['**/db.setup.ts'],
      use: {
        ...selectedDevice,
        // connectOptions 方式は廃止。接続はlt-fixture.tsがltEndpoint()で行う。
      },
      dependencies: ['db-setup'],
    },
  ],
  webServer: {
・・・・・
  },
});

configファイルでは、先程作成したlt-capabilities.tsをimportすることと、projectsからconnectOptionsの記述削除が必要です。connectOptionsの役割はlt-fixture.tsに移行したのでしたね。

ここまで設定できたら、最後に各specファイルのimport元を@playwright/testから新しいfixtureに切り替えます。

e2e/それぞれのspecファイル
import { test, expect } from './support/lt-fixture';  // ← fixtureに変更

test('ログインページが表示される', async ({ page }) => {
  // ...
});

新しいファイルが増えて少しややこしく感じますが、テスト実行時には以下のような流れになります。

  1. まず playwright.lambdatest.config.tsが読み込まれる
  2. 1のファイルがimportしているlt-capabilities.tsも読み込まれ、ビルド名の準備などが行われる
  3. 各specファイルが読み込まれる
  4. 各specがimportしているlt-fixture.tsが読み込まれる
  5. テスト実行時、test()ごとにfixtureが呼ばれ、LambdaTestへ接続される

さてここまでの設定でテストを実行してみると、セッションが"テスト名"で表示されるようになりました!

セッションが分かれたことで、コマンド群もそのテストのコマンドのみが表示されるようになります。これでかなり見やすくなりました。

補足:セッションを分けると実行時間は伸びる

test()ごとにセッションを分けると、クラウドブラウザの起動〜トンネル経由の接続、終了時の動画・ログ保存といった処理が、テストの数だけ繰り返されます。改修前は1セッションで全テストを流していたためこれらのコストは1回で済みましたが、この変更後、直列実行ではダッシュボードの見やすさと引き換えに実行時間が少し増えてしまいます。

テスト時間をできるだけ短くしたい場合は、すべてのtest()でセッションを分けるのではなく一部のテストだけを対象にしたり、capabilityのvideonetworkconsoleを必要なテストだけtrueにして、それ以外はfalseにする、といった調整が有効です。

③ tagsでフィルタリングできるようにする

ここまででかなり見やすくなりました。ですが、今後テストの数が増えてくると「たくさんのセッションの中から目的のテストを探すのが大変」という問題が出てきそうです。

そこで、各セッションにtagsを付けてダッシュボード上でフィルタリングできるようにします。tags個々のセッションに付けるラベルです。「認証」「決済」などの機能ごとや、「regression」「critical」などのテストのタイプで分類しておくと、重要テストだけ、認証関連だけ、といった絞り込みができるようになります。

タグを使うには、Playwright標準機能を使ってspec側で宣言し、それをfixtureでLambdaTestのtagsに変換して渡す必要があります。以下のようにspec側でtest()の第2引数にtagを付けます。tagは配列として複数渡せます。

test('ログインページが表示される', { tag: ['@認証', '@smoke'] }, async ({ page }) => {
  // ...
});

test('ダッシュボードが表示される', { tag: ['@ダッシュボード', '@smoke'] }, async ({ page }) => {
  // ...
});

それをfixture側で、testInfo.tagsとして受け取り、先頭の@を外してLambdaTestのtagsとして渡す、という流れです。

e2e/support/lt-fixture.ts
export const test = base.extend({
  page: async ({}, use, testInfo) => {
    // @付きタグをLambdaTest用(@なし)に変換
    const tags = testInfo.tags.map(t => t.replace(/^@/, ''));

    // セッション名とタグを渡す
    const wsEndpoint = ltEndpoint(testInfo.title, tags);
    const browser = await chromium.connect(wsEndpoint);
    ・・・・・

最後に、タグを受け取れるようltEndpoint()に引数を1つ追加し、LT:Options.tagsに渡します。

e2e/support/lt-capabilities.ts
export function ltEndpoint(sessionName: string, tags: string[] = []) {  // ← 追加1:引数にタグを追加
  return `wss://cdp.lambdatest.com/playwright?capabilities=${encodeURIComponent(
    JSON.stringify({
      ・・・・・
      'LT:Options': {
        build: buildName,
        name: sessionName,
        tags,     // ← 追加2:セッション単位のタグ
        ・・・・・
      },
    })
  )}`;
}

タグを付与したあとでテストを実行すると、ダッシュボードの絞り込み機能を使って任意のテストだけ抽出できるようになります。

“認証"のタグを選択して、Applyをクリックしてみます。

以下のように、認証のタグに紐づくテスト結果だけを抽出できました!

④ Passed/Failedを明示的にする

セッションがテスト名ごとに分かれ、タグで絞り込めるようになりました。しかし、よく見るとテスト結果が"Passed"でも"Failed"でもなく”Completed“となっています。

これはLambdaTestのデフォルトの挙動で、クラウドブラウザのセッションが正常に開始・終了したかどうかは分かるものの、Playwright側のアサーションが成功したのか失敗したのかまでは知らないことが原因です。そのため、テスト結果にかかわらずブラウザセッションが正常に閉じられたら、Completedとして記録されてしまいます。

これではテスト結果としてはとても見づらいので、Playwrightが持っているテスト結果testInfo.statusをチェックし、テスト終了時にLambdaTestへ"Passed"または"Failed"を明示的に送信するように修正します。

lt-fixture.tsの後半部分に以下のように追記します。

e2e/support/lt-fixture.ts
export const test = base.extend({
  page: async ({}, use, testInfo) => {
    ・・・・・
    await use(page);  // ← ここでテスト本体(assert を含む)が実行される

    // ▼ テスト結果を LambdaTest に明示送信
    // testInfo.status は use() の後でないと確定しないため、必ずここで送る
    const status = testInfo.status === 'passed' ? 'passed' : 'failed';
    await page.evaluate(
      _ => {},
      `lambdatest_action: ${JSON.stringify({
        action: 'setTestStatus',
        arguments: {
          status,
          remark: testInfo.error?.message ?? '',  // 失敗理由をダッシュボードに表示
        },
      })}`
    );
    ・・・・・

use(page)の箇所でテスト本体が実行され、その結果がtestInfo.statusに入ります。このtestInfo.statusを元にステータスを成功・失敗に振り分け、LambdaTestに送信しています。

テスト失敗時はtestInfo.error.messageremarkに渡しておくことで、LambdaTestのダッシュボード上に失敗理由が表示されるのでよりデバッグしやすくなります。

これでテストを実行します。

成功・失敗が色分けではっきりし、画面上部の水色背景の箇所にエラーメッセージも表示されるようになりました!

最終的なlt-fixture.ts

②〜④で段階的に追記してきたlt-fixture.tsのコード全体は、以下のようになります。

e2e/support/lt-fixture.ts
import { test as base, chromium } from '@playwright/test';
import { ltEndpoint } from './lt-capabilities';

export const test = base.extend({
  page: async ({}, use, testInfo) => {
    // @付きタグを LambdaTest 用(@なし)に変換
    const tags = testInfo.tags.map(t => t.replace(/^@/, ''));

    // テスト名とタグを渡して接続
    const wsEndpoint = ltEndpoint(testInfo.title, tags);
    const browser = await chromium.connect(wsEndpoint);
    const context = await browser.newContext();
    const page = await context.newPage();

    await use(page);  // ← ここでテスト本体(assert を含む)が実行される

    // テスト結果(Passed/Failed)を LambdaTest に明示送信
    const status = testInfo.status === 'passed' ? 'passed' : 'failed';
    await page.evaluate(
      _ => {},
      `lambdatest_action: ${JSON.stringify({
        action: 'setTestStatus',
        arguments: { status, remark: testInfo.error?.message ?? '' },
      })}`
    );

    await context.close();
    await browser.close();
  },
});

export { expect } from '@playwright/test';
Hugo で構築されています。
テーマ StackJimmy によって設計されています。