Devtoolsサービス
テストでChrome DevToolsコマンドを実行できるようにするWebdriverIOサービス
Chrome v63以降、ブラウザはマルチクライアントのサポートを開始し、任意のクライアントがChrome DevTools Protocolにアクセスできるようになりました。これにより、WebDriverプロトコルを超えてChromeを自動化する興味深い機会が生まれます。このサービスを使用すると、wdio browserオブジェクトを拡張してそのアクセスを活用し、テスト内でChrome DevToolsコマンドを呼び出して、リクエストをインターセプトしたり、ネットワーク機能を調整したり、CSS/JSカバレッジを取得したりできます。
Firefox 86以降、"moz:debuggerAddress": trueという機能を追加することで、Chrome DevTools Protocolのサブセットが実装されました。
注:このサービスは現在、Chrome v63以降、Chromium、およびFirefox 86以降のみをサポートしています。クラウドベンダーがChrome DevTools Protocolへのアクセスを公開していないため、このサービスは通常、ローカルでテストを実行するか、Selenium Grid v4以上でテストを実行する場合にのみ機能します。
インストール
最も簡単な方法は、package.jsonで@wdio/devtools-serviceを開発依存関係として保持することです。次のコマンドを実行してください。
npm install @wdio/devtools-service --save-dev
WebdriverIOのインストール方法については、こちらをご覧ください。
設定
サービスを使用するには、wdio.conf.jsのサービスリストにサービスを追加するだけです。次に例を示します。
// wdio.conf.js
export const config = {
    // ...
    services: ['devtools'],
    // ...
};
使い方
@wdio/devtools-serviceは、WebDriverプロトコルを超えてChromeを自動化するのに役立つさまざまな機能を提供します。Chrome DevToolsプロトコルへのアクセスと、Puppeteerインスタンスへのアクセスを提供し、Puppeteer自動化インターフェースを使用してChromeを自動化できます。
パフォーマンステスト
DevToolsサービスを使用すると、クリックによって発生したすべてのページロードまたはページ遷移からパフォーマンスデータをキャプチャできます。有効にするには、browser.enablePerformanceAudits(<options>)を呼び出します。必要なすべてのパフォーマンスデータのキャプチャが完了したら、スロットル設定を元に戻すために無効にします。次に例を示します。
import assert from 'node:assert'
describe('JSON.org page', () => {
    before(async () => {
        await browser.enablePerformanceAudits()
    })
    it('should load within performance budget', async () => {
        /**
         * this page load will take a bit longer as the DevTools service will
         * capture all metrics in the background
         */
        await browser.url('http://json.org')
        let metrics = await browser.getMetrics()
        assert.ok(metrics.speedIndex < 1500) // check that speedIndex is below 1.5ms
        let score = await browser.getPerformanceScore() // get Lighthouse Performance score
        assert.ok(score >= .99) // Lighthouse Performance score is at 99% or higher
        $('=Esperanto').click()
        metrics = await browser.getMetrics()
        assert.ok(metrics.speedIndex < 1500)
        score = await browser.getPerformanceScore()
        assert.ok(score >= .99)
    })
    after(async () => {
        await browser.disablePerformanceAudits()
    })
})
emulateDeviceコマンドを使用すると、モバイルデバイスをエミュレートしたり、CPUとネットワークを調整したり、フォームファクタとしてmobileを設定したりできます。
await browser.emulateDevice('iPhone X')
await browser.enablePerformanceAudits({
    networkThrottling: 'Good 3G',
    cpuThrottling: 4,
    formFactor: 'mobile'
})
次のコマンドと結果を利用できます。
getMetrics
最も一般的に使用されるパフォーマンスメトリクスを取得します。
console.log(await browser.getMetrics())
/**
 * { timeToFirstByte: 566,
 *   serverResponseTime: 566,
 *   domContentLoaded: 3397,
 *   firstVisualChange: 2610,
 *   firstPaint: 2822,
 *   firstContentfulPaint: 2822,
 *   firstMeaningfulPaint: 2822,
 *   largestContentfulPaint: 2822,
 *   lastVisualChange: 15572,
 *   interactive: 6135,
 *   load: 8429,
 *   speedIndex: 3259,
 *   totalBlockingTime: 31,
 *   maxPotentialFID: 161,
 *   cumulativeLayoutShift: 2822 }
 */
getDiagnostics
ページロードに関する有用な診断を取得します。
console.log(await browser.getDiagnostics())
/**
 * { numRequests: 8,
 *   numScripts: 0,
 *   numStylesheets: 0,
 *   numFonts: 0,
 *   numTasks: 237,
 *   numTasksOver10ms: 5,
 *   numTasksOver25ms: 2,
 *   numTasksOver50ms: 2,
 *   numTasksOver100ms: 0,
 *   numTasksOver500ms: 0,
 *   rtt: 147.20600000000002,
 *   throughput: 47729.68474448835,
 *   maxRtt: 176.085,
 *   maxServerLatency: 1016.813,
 *   totalByteWeight: 62929,
 *   totalTaskTime: 254.07899999999978,
 *   mainDocumentTransferSize: 8023 }
 */
getMainThreadWorkBreakdown
すべてのメインスレッドタスクとその合計期間の内訳を含むリストを返します。
console.log(await browser.getMainThreadWorkBreakdown())
/**
 * [ { group: 'styleLayout', duration: 130.59099999999998 },
 *   { group: 'other', duration: 44.819 },
 *   { group: 'paintCompositeRender', duration: 13.732000000000005 },
 *   { group: 'parseHTML', duration: 3.9080000000000004 },
 *   { group: 'scriptEvaluation', duration: 2.437999999999999 },
 *   { group: 'scriptParseCompile', duration: 0.20800000000000002 } ]
 */
getPerformanceScore
次のメトリクスの加重平均であるLighthouse Performance Scoreを返します:firstContentfulPaint、speedIndex、largestContentfulPaint、cumulativeLayoutShift、totalBlockingTime、interactive、maxPotentialFIDまたはcumulativeLayoutShift。
console.log(await browser.getPerformanceScore())
/**
 * 0.897826278457836
 */
enablePerformanceAudits
urlコマンドを呼び出すか、リンクをクリックするか、ページロードを引き起こすすべてのページロードに対して、自動パフォーマンス監査を有効にします。設定オブジェクトを渡して、一部のスロットルオプションを決定できます。デフォルトのスロットルプロファイルは、CPUスロットルが4倍のGood 3Gネットワークです。
await browser.enablePerformanceAudits({
    networkThrottling: 'Good 3G',
    cpuThrottling: 4,
    cacheEnabled: true,
    formFactor: 'mobile'
})
次のネットワークスロットルプロファイルを使用できます:offline、GPRS、Regular 2G、Good 2G、Regular 3G、Good 3G、Regular 4G、DSL、Wifi、online(スロットルなし)。
デバイスエミュレーション
このサービスを使用すると、特定のデバイスタイプをエミュレートできます。設定すると、ブラウザのビューポートはデバイスの機能に合うように変更され、ユーザーエージェントはデバイスのユーザーエージェントに従って設定されます。事前定義されたデバイスプロファイルを設定するには、次を実行します。
await browser.emulateDevice('iPhone X')
// or `browser.emulateDevice('iPhone X', { inLandscape: true })` if you want to be in landscape mode
// or `browser.emulateDevice('iPhone X', { osVersion: "15.0" })` if you want to use emulated device with custom OS version
使用可能な事前定義されたデバイスプロファイルは、Blackberry PlayBook、BlackBerry Z30、Galaxy Note 3、Galaxy Note II、Galaxy S III、Galaxy S5、iPad、iPad Mini、iPad Pro、iPhone 4、iPhone 5、iPhone 6、iPhone 6 Plus、iPhone 7、iPhone 7 Plus、iPhone 8、iPhone 8 Plus、iPhone SE、iPhone X、JioPhone 2、Kindle Fire HDX、LG Optimus L70、Microsoft Lumia 550、Microsoft Lumia 950、Nexus 10、Nexus 4、Nexus 5、Nexus 5X、Nexus 6、Nexus 6P、Nexus 7、Nokia Lumia 520、Nokia N9、Pixel 2、Pixel 2 XLです。
次の例のように、オブジェクトをパラメータとして提供することで、独自のデバイスプロファイルを定義することもできます。
await browser.emulateDevice({
    viewport: {
        width: 550, // <number> page width in pixels.
        height: 300, // <number> page height in pixels.
        deviceScaleFactor: 1, //  <number> Specify device scale factor (can be thought of as dpr). Defaults to 1
        isMobile: true, // <boolean> Whether the meta viewport tag is taken into account. Defaults to false
        hasTouch: true, // <boolean> Specifies if viewport supports touch events. Defaults to false
        isLandscape: true // <boolean> Specifies if viewport is in landscape mode. Defaults to false
    },
    userAgent: 'my custom user agent'
})
注
これは、capabilities['goog:chromeOptions']内でmobileEmulationを使用しない場合にのみ機能します。mobileEmulationが存在する場合、browser.emulateDevice()の呼び出しは何も行いません。
PWAテスト
checkPWAコマンドを使用すると、プログレッシブWebアプリに関する最新のWeb標準にWebアプリが準拠しているかどうかを検証できます。次をチェックします。
- アプリがインストール可能かどうか
- サービスワーカーを提供しているか
- スプラッシュスクリーンがあるか
- Apple touchとマスク可能なアイコンを提供しているか
- モバイルデバイスで提供できるか
これらのチェックのいずれかに興味がない場合は、実行するチェックのリストを渡すことができます。passedプロパティは、すべてのチェックが合格した場合にtrueを返します。失敗した場合は、detailsプロパティを使用して、失敗の詳細を含む失敗メッセージを充実させることができます。
// open page first
await browser.url('https://webdriverio.dokyumento.jp')
// validate PWA
const result = await browser.checkPWA()
expect(result.passed).toBe(true)
コードカバレッジのキャプチャ
このサービスを使用すると、テスト対象のアプリケーションのコードカバレッジをキャプチャできます。これを行うには、サービス設定の一部としてこの機能を有効にする必要があります。
// wdio.conf.js
services: [
    ['devtools', {
        coverageReporter: {
            enable: true,
            type: 'html', // lcov, json, text
            logDir: __dirname + '/coverage',
            exclude: [/resources/]
        }
    }]
]
次に、テスト内でアサートするために、カバーされたコード行とブランチの比率を計算するコマンドにアクセスできます。
const coverage = await browser.getCoverageReport()
expect(coverage.lines.total).toBeAbove(0.9)
expect(coverage.statements.total).toBeAbove(0.9)
expect(coverage.functions.total).toBeAbove(0.9)
expect(coverage.branches.total).toBeAbove(0.9)
Chrome DevToolsアクセス
現在、このサービスでは、Chrome DevTools Protocolにアクセスする方法が2つあります。
cdp コマンド
cdpコマンドは、ブラウザスコープに追加されたカスタムコマンドで、プロトコルに直接コマンドを呼び出すことができます。
browser.cdp(<domain>, <command>, <arguments>)
たとえば、ページのJavaScriptカバレッジを取得したい場合は、次のように実行できます。
it('should take JS coverage', async () => {
    /**
     * enable necessary domains
     */
    await browser.cdp('Profiler', 'enable')
    await browser.cdp('Debugger', 'enable')
    /**
     * start test coverage profiler
     */
    await browser.cdp('Profiler', 'startPreciseCoverage', {
        callCount: true,
        detailed: true
    })
    await browser.url('http://google.com')
    /**
     * capture test coverage
     */
    const { result } = await browser.cdp('Profiler', 'takePreciseCoverage')
    const coverage = result.filter((res) => res.url !== '')
    console.log(coverage)
})
getNodeId(selector) および getNodeIds(selector) コマンド
ページ内の要素のnodeIdを取得するためのヘルパーメソッドです。NodeIdはWebDriverのノードIDのようなもので、ノードの識別子です。これは、DOM.focusなどの他のChrome DevToolsメソッドのパラメータとして使用できます。
const nodeId = await browser.getNodeId('body')
console.log(nodeId) // outputs: 4
const nodeId = await browser.getNodeIds('img')
console.log(nodeId) // outputs: [ 40, 41, 42, 43, 44, 45 ]
startTracing(categories, samplingFrequency) コマンド
ブラウザのトレースを開始します。オプションで、カスタムのトレースカテゴリ(デフォルトはこのリスト)とサンプリング頻度(デフォルトは10000)を渡すことができます。
await browser.startTracing()
endTracing コマンド
ブラウザのトレースを停止します。
await browser.endTracing()
getTraceLogs コマンド
トレース期間中にキャプチャされたトレースログを返します。このコマンドを使用して、トレースログをファイルシステムに保存し、Chrome DevToolsインターフェースを介してトレースを分析できます。
import fs from 'node:fs/promises'
await browser.startTracing()
await browser.url('http://json.org')
await browser.endTracing()
await fs.writeFile('/path/to/tracelog.json', JSON.stringify(browser.getTraceLogs()))
getPageWeight コマンド
最後のページロードのページウェイト情報を返します。
await browser.startTracing()
await browser.url('https://webdriverio.dokyumento.jp')
await browser.endTracing()
console.log(await browser.getPageWeight())
// outputs:
// { pageWeight: 2438485,
//   transferred: 1139136,
//   requestCount: 72,
//   details: {
//       Document: { size: 221705, encoded: 85386, count: 11 },
//       Stylesheet: { size: 52712, encoded: 50130, count: 2 },
//       Image: { size: 495023, encoded: 482433, count: 36 },
//       Script: { size: 1073597, encoded: 322854, count: 15 },
//       Font: { size: 84412, encoded: 84412, count: 5 },
//       Other: { size: 1790, encoded: 1790, count: 2 },
//       XHR: { size: 509246, encoded: 112131, count: 1 } }
// }
ブラウザのダウンロードパスの設定
cdpコマンドを使用して、Devtools ProtocolのPage.setDownloadBehaviorコマンドを呼び出し、ファイルをダウンロードする際の動作を設定できます。downloadPathが絶対パスであり、ファイルのダウンロード前にbrowser.cdp()呼び出しが行われていることを確認してください。
await browser.cdp('Page', 'setDownloadBehavior', {
    behavior: 'allow',
    downloadPath: '/home/root/webdriverio-project/',
});
Puppeteerインスタンスへのアクセス
このサービスは、内部で自動化にPuppeteerを使用しています。使用されているインスタンスには、getPuppeteerコマンドを呼び出すことでアクセスできます。注意: Puppeteerコマンドは非同期であり、callコマンド内で呼び出すか、async/awaitで処理する必要があります。
describe('use Puppeteer', () => {
    it('by wrapping commands with call', () => {
        await browser.url('http://json.org')
        const puppeteer = await browser.getPuppeteer()
        const page = await browser.call(() => puppeteer.pages())[0]
        console.log(await browser.call(() => page.title()))
    })
})
イベントリスナー
ブラウザでネットワークイベントをキャプチャするには、Chrome DevToolsにイベントリスナーを登録できます。利用可能なCDPネットワークイベントの完全なリスト。
it('should listen on network events', () => {
    await browser.cdp('Network', 'enable')
    await browser.on('Network.requestWillBeSent', (event) => {
        console.log(`Request: ${event.request.method} ${event.request.url}`);
    });
    await browser.on('Network.responseReceived', (event) => {
        console.log(`Response: ${event.response.status} ${event.response.url}`);
    });
    await browser.on('Network.loadingFailed', (event) => {
        console.log(`Request failed: ${event.errorText}`);
    });
    await browser.url('https://www.google.com')
})
WebdriverIOの詳細については、ホームページをご覧ください。