コンテンツへスキップ

ドライバーの構築

Appium は、誰でも Appium エコシステムの一部として独自の自動化ドライバーを簡単に開発できるようにしたいと考えています。このガイドでは、Appium が提供するツールを使用して、さまざまなドライバー開発タスクを行う方法と、関連する内容について説明します。このガイドは、(1) Appium の熟練したユーザーであり、(2) 熟練した Node.js 開発者であり、(3) ドライバー入門を読解し理解していることを前提としています。

もしあなたがそうであれば、素晴らしいです!このガイドはあなたの出発点となるでしょう。

ドライバーを作成する前に

ドライバーの実装作業に取り掛かる前に、いくつかのことを整理しておくことが重要です。たとえば、ドライバーが何をするのかを知る必要があります。どのプラットフォームで WebDriver の自動化を公開しようとしていますか?

Appium は、あらゆるプラットフォームを自動化する力を魔法のように与えてくれるわけではありません。Appium が行うのは、WebDriver プロトコルを実装するための便利なツールセットを提供することだけです。したがって、たとえば、新しいアプリプラットフォーム用のドライバーを作成する場合は、*Appium なしで*そのプラットフォーム上のアプリを自動化する方法を知る必要があります。

これは通常、特定のプラットフォームのアプリ開発に非常に精通している必要があることを意味します。また、通常、プラットフォームベンダーが提供するツールまたは SDK に依存することを意味します。

基本的に、「このプラットフォームでアプリを起動し、リモートで動作をトリガーし、状態を読み取るにはどうすればよいか?」という質問に答えられない場合は、Appium ドライバーを作成する準備はまだできていません。確実に進むべき道があることを確信するために調査を行ってください。それができたら、それをコーディングして Appium ドライバーとして利用できるようにするのは簡単なはずです!

参照すべき他のドライバー

Appium ドライバーを構築する最大の利点の 1 つは、参照できるオープンソースの Appium ドライバーがすでに多数存在することです。このガイドで説明されている内容の一部を実演する以外のことを基本的には何も行わないfake-driver サンプルドライバーがあります。

もちろん、Appium の公式ドライバーはすべてオープンソースであり、プロジェクトの GitHub 組織のリポジトリで入手できます。したがって、「ドライバーは X をどのように行うのか?」という疑問が浮かんだら、これらのドライバーのコードを読んでください。また、行き詰まった場合は、Appium 開発者に質問することを恐れないでください。ドライバー開発の経験を良いものにするために、常に喜んでお手伝いします!

Appium ドライバーの基本要件

これらは、有効な Appium ドライバーにするために、ドライバーが *必ず* 行う (またはそうである) 必要があることです。

Appium 拡張メタデータを持つ Node.js パッケージ

すべての Appium ドライバーは基本的に Node.js パッケージであるため、有効な package.json が必要です。ドライバーは Node.js に *限定* されていませんが、Appium でロードできるように、Node.js で記述されたアダプターを提供する必要があります。

package.json には、peerDependency として appium を含める必要があります。依存関係のバージョンの要件は、できるだけ緩くする必要があります (ドライバーが特定のバージョンの Appium でしか動作しないことがわかっている場合を除く)。たとえば、Appium 2 の場合、これは ^2.0.0 のようになり、ドライバーが 2.x で始まるすべてのバージョンの Appium で動作することを宣言します。

package.json には、次のような appium フィールドを含める必要があります (これを「Appium 拡張メタデータ」と呼びます)。

```json
{
  ...,
  "appium": {
    "driverName": "fake",
    "automationName": "Fake",
    "platformNames": [
      "Fake"
    ],
    "mainClass": "FakeDriver"
  },
  ...
}
```

必須のサブフィールドは次のとおりです。

  • driverName: これはドライバーの短い名前にする必要があります。
  • automationName: これは、ユーザーが appium:automationName 機能に使用して、Appium に *あなたの* ドライバーを使用するように指示する文字列である必要があります。
  • platformNames: これは、ドライバーに対して有効と見なされる 1 つ以上のプラットフォーム名の配列です。ユーザーがセッションを開始するために platformName 機能で送信する場合、セッションを処理するためにこのリストに含まれている必要があります。既知のプラットフォーム名の文字列には、iOStvOSmacOSWindowsAndroid が含まれます。
  • mainClass: これは、main フィールドからの名前付きエクスポート (CommonJS スタイル) です。これは、Appium の BaseDriver (下記参照) を拡張するクラスである必要があります。

Appium の BaseDriver クラスを拡張する

最終的に、ドライバーを記述するのがはるかに簡単なのは、WebDriver プロトコルの実装と特定の共通ロジックの処理という難しい作業のほとんどが、Appium によって既に行われているためです。これはすべて、Appium が使用するためにエクスポートする BaseDriver というクラスとしてエンコードされています。これは appium/driver からエクスポートされるため、これらのスタイルのいずれかを使用してインポートし、拡張する *独自の* クラスを作成できます。

import {BaseDriver} from 'appium/driver';
// or: const {BaseDriver} = require('appium/driver');

export class MyDriver extends BaseDriver {
}

ドライバーを利用可能にする

基本的にはそれだけです!ドライバークラスをエクスポートする Node.js パッケージと、正しい Appium 拡張メタデータがあれば、Appium ドライバーが完成しました! 今は何も *しません* が、Appium にロードして、セッションを開始および停止することができます...

ユーザーが利用できるようにするには、NPM 経由で公開できます。公開すると、ドライバーは Appium CLI を介してインストール可能になります。

appium driver install --source=npm <driver-package-on-npm>

もちろん、最初にドライバーをテストすることをお勧めします。Appium 内でどのように動作するかを確認する 1 つの方法は、最初にローカルにインストールすることです。

appium driver install --source=local /path/to/your/driver

ドライバーの開発

ドライバーをどのように開発するかは、あなた次第です。ただし、公開とインストールを何度も行うことなく、Appium 内から実行すると便利です。これを行う最も簡単な方法は、最新バージョンの Appium を devDependency として含め、さらに自分のドライバーを次のように含めることです。

{
    "devDependencies": {
        ...,
        "appium": "^2.0.0",
        "your-driver": "file:.",
        ...
    }
}

これで、Appium をローカルで (npm exec appium または npx appium) で実行できます。また、ドライバーが依存関係としてリストされているため、自動的に「インストール」され、利用できるようになります。この方法で e2e テストを設計したり、Node.js で記述している場合は、Appium の開始サーバーメソッドをインポートして、Node.js で Appium サーバーの開始と停止を処理できます。(TODO: 準備ができたら、オープンソースドライバーのいずれかでこの実装を参照してください)。

既存の Appium サーバーインストールでローカル開発を行う別の方法は、ドライバーをローカルにインストールするだけです。

appium driver install --source=local /path/to/your/driver/dev/dir

開発中のドライバーのリフレッシュ

Appium サーバーが起動すると、ドライバーがメモリにロードされます。ドライバーコードへの変更は、Appium サーバーが次回起動するまで有効になりません。新しいセッションを開始するだけでは、ドライバーのコードが再ロードされることはありません。

ただし、新しいセッションがリクエストされるたびに、Appiumがモジュールキャッシュをクリアし、拡張機能をリロードするように要求するには、APPIUM_RELOAD_EXTENSIONS 環境変数を 1 に設定できます。これにより、ドライバーにコード変更を加えたときにサーバーを再起動する必要がなくなる場合があります。

標準的なドライバー実装のアイデア

これらは、ドライバーを作成する際に、おそらく実行することになると思われるものです。

コンストラクターで状態を設定する

独自のコンストラクターを定義する場合は、すべての標準状態が正しく設定されるように、super を呼び出す必要があります。

constructor(...args) {
    super(...args);
    // now do your own thing
}

ここでの args パラメータは、Appiumサーバーの起動に使用されるすべてのCLI引数を含むオブジェクトです。

受け入れられる機能の定義と検証

独自の機能とそれに対する基本的な検証を定義できます。ユーザーは常に、定義していない機能を送信できますが、明示的に定義した機能を送信した場合、Appiumはそれらが正しい型であることを検証します(および必須機能の存在をチェックします)。

機能の検証を完全にオフにしたい場合は、コンストラクターで this.shouldValidateCapsfalse に設定します。

Appiumに検証制約を与えるには、コンストラクターで this.desiredCapConstraints を検証オブジェクトに設定します。検証オブジェクトはやや複雑になる可能性があります。以下は、UiAutomator2ドライバーの例です。

{
  app: {
    presence: true,
    isString: true
  },
  automationName: {
    isString: true
  },
  browserName: {
    isString: true
  },
  launchTimeout: {
    isNumber: true
  },
}

セッションを開始し、機能を取得する

Appiumの BaseDriver は、すでに createSession コマンドを実装しているため、実装する必要はありません。ただし、独自の起動アクション(アプリの起動、プラットフォームコードの実行、またはドライバーに定義した機能に基づいて異なることを行うなど)を実行する必要があることは非常に一般的です。したがって、createSession をオーバーライドすることになるでしょう。ドライバーでメソッドを定義することで、これを行うことができます。

async createSession(jwpCaps, reqCaps, w3cCaps, otherDriverData) {
    const [sessionId, caps] = super.createSession(w3cCaps);
    // do your own stuff here
    return [sessionId, caps];
}

従来の理由から、関数は最初の2つの引数として、古いスタイルのJSON Wire Protocolのdesireとrequired capsを受け取ります。古いプロトコルはもうサポートされておらず、クライアントはすべて更新されているため、代わりに w3cCaps パラメータのみに依存できます。(otherDriverData が何であるかについては、後述の同時ドライバーに関するセクションを参照してください)。

セッションIDと処理された機能を取得するために、必ず super.createSession を呼び出すようにしてください(機能は this.caps にも設定されることに注意してください。ここでローカルに caps を変更しても、ユーザーがセッション作成応答で確認するもの以外には影響はありません)。

これで終わりです!真ん中のセクションに、ドライバーに必要な起動ロジックを入力できます。

セッションを終了する

ドライバーにクリーンアップまたはシャットダウンロジックが必要な場合は、deleteSession の実装をオーバーライドする一部として行うのが最善です。

async deleteSession() {
    // do your own cleanup here
    // don't forget to call super!
    await super.deleteSession();
}

セッションのクリーンアップのすべての部分が成功できるように、可能な限りここでエラーをスローしないことが非常に重要です!

機能とCLI引数にアクセスする

CLI引数として設定されたか、機能として設定されたかに関わらず、ユーザーがセッションに設定したパラメータを読み取りたいことがよくあります。これを行う最も簡単な方法は、CLIまたは機能からのすべてのオプションをマージした this.opts にアクセスすることです。たとえば、appium:app 機能にアクセスするには、単純に this.opts.app の値を取得できます。

何かがCLI引数または機能として送信されたかどうかを知りたい場合は、this.cliArgs オブジェクトと this.caps オブジェクトに明示的にアクセスできます。

すべての場合において、便宜上、ここで値にアクセスするまでには appium: 機能プレフィックスは削除されます。

WebDriverのクラシックコマンドを実装する

WebDriverコマンドは、ドライバークラスに関数を実装することで処理します。WebDriverプロトコルの各メンバーと、さまざまなAppium拡張機能には、ドライバーでそのコマンドをサポートしたい場合に実装する対応する関数があります。Appiumがサポートするコマンドと、各コマンドに対して実装する必要があるメソッドを確認する最良の方法は、Appiumのroutes.jsを参照することです。このファイルの各ルートオブジェクトは、コマンド名と、そのコマンドで受け取ることが予想されるパラメータを示します。

たとえば、このブロックを見てみましょう。

'/session/:sessionId/url': {
    GET: {command: 'getUrl'},
    POST: {command: 'setUrl', payloadParams: {required: ['url']}},
}

ここでは、ルート /session/:sessionId/url が2つのコマンドにマッピングされていることがわかります。1つは GET リクエスト用で、もう1つは POST リクエスト用です。ドライバーで「url」を変更できるようにしたい場合(またはドライバーにとってそれが何を意味するにしても)、url パラメータを受け取ることを知って、setUrl コマンドを実装できます。

async setUrl(url) {
    // your implementation here
}

いくつかの注意点:- すべてのコマンドメソッドは async 関数であるか、Promise を返す必要があります。- プロトコルのエンコード/デコードについて心配する必要はありません。JSオブジェクトをパラメータとして取得し、応答でJSONシリアル化可能なオブジェクトを返すことができます。Appiumは、WebDriverプロトコルの応答形式でラップし、JSONに変換するなどを行います。- すべてのセッションベースのコマンドは、最後のパラメータとして sessionId パラメータを受け取ります。- すべての要素ベースのコマンドは、最後から2番目のパラメータとして elementId パラメータを受け取ります。- ドライバーがコマンドを実装していない場合でも、ユーザーはコマンドにアクセスしようとすることができ、501 Not Yet Implemented 応答エラーが表示されます。

WebDriver BiDiコマンドを実装する

WebDriver BiDiは、HTTPではなくWebsocketを介して実装されるWebDriver仕様の新しいバージョンです。Appiumドライバーの作成者は、BiDiプロトコルやWebsocketについて何も知らなくても、AppiumのBiDiサポートを活用できます。BiDiコマンドのハンドラーの実装は、WebDriverクラシックコマンドのハンドラーの実装(前のセクションで説明)と同じように機能します。適切な名前のメソッドをドライバーで定義するだけで、クライアントからBiDiコマンドがリクエストされたときに呼び出されます。BiDiコマンドに使用する必要がある特定の名前を確認するには、bidi-commands.jsを参照してください。

現在、ドライバーインスタンスで doesSupportBidi フィールドを定義し、true に設定する必要もあります。ドライバーがこの方法でBiDiをサポートすると言わない限り、AppiumはドライバーのWebsocketサーバーをオンにして、ハンドラーをセットアップしません。

要素の検索を実装する

要素の検索は、特別なコマンド実装のケースです。routes.js にリストされているにもかかわらず、実際には findElementfindElements をオーバーライドする必要はありません。代わりにこの関数を実装すると、Appiumは多くの処理を行います。

async findElOrEls(strategy, selector, mult, context) {
    // find your element here
}

ここに渡されるものを示します。

  • strategy - 使用されるロケータ戦略である文字列
  • selector - セレクターである文字列
  • mult - ユーザーがセレクターに一致する1つの要素をリクエストしたか、すべての要素をリクエストしたかを示すブール値
  • context - (オプション) 定義されている場合は、W3C要素になります(つまり、W3C要素識別子をキーとし、要素IDを値とするJSオブジェクト)。

次のいずれかを返す必要があります。

  • 単一のW3C要素(上記のようなオブジェクト)
  • W3C要素の配列

appium/support からそのW3C Web要素識別子をインポートできることに注意してください。

import {util} from 'appium/support';
const { W3C_WEB_ELEMENT_IDENTIFIER } = util;

要素で何をするかはあなた次第です!通常、IDから実際の要素「オブジェクト」、またはプラットフォームに相当するもののキャッシュマップを保持することになります。

有効なロケータ戦略を定義する

ドライバーは、標準のWebDriverロケータ戦略のサブセットのみをサポートするか、独自のカスタムロケータ戦略を追加する場合があります。ドライバーに有効と見なされる戦略をAppiumに伝えるには、戦略の配列を作成して this.locatorStrategies に割り当てます。

this.locatorStrategies = ['xpath', 'custom-strategy'];

Appiumは、ユーザーが許可された戦略以外の戦略を使用しようとするとエラーをスローするため、要素検索コードをクリーンに保ち、認識している戦略のみを処理できます。

デフォルトでは、有効な戦略のリストは空であるため、ドライバーが別のWebDriverエンドポイントにプロキシしていない場合は、いくつか定義する必要があります。プロトコル標準のロケータ戦略はこちらで定義されています。

WebDriver固有のエラーをスローする

WebDriver仕様では、エラーが発生した場合にコマンド応答に付随するエラーコードのセットを定義しています。Appiumはこれらの各コードに対してエラークラスを作成しているため、コマンド内から適切なエラーをスローでき、ユーザーへのプロトコル応答に関して適切な処理を行います。これらのエラークラスにアクセスするには、appium/driver からインポートします。

import {errors} from 'appium/driver';

throw new errors.NoSuchElementError();

Appiumログにメッセージをログ記録する

もちろん、常に console.log を使用できますが、Appiumは this.log として優れたロガーを提供します(異なるログレベルに対して .info.debug.log.warn.error メソッドがあります)。ドライバーのコンテキスト外で(スクリプトまたはヘルパーファイルでなど)Appiumロガーを作成する場合は、常に独自に構築することもできます。

import {logging} from 'appium/support';
const log = logging.getLogger('MyDriver');

Appiumドライバーのさらなる可能性

これらは、ドライバーが追加のドライバー機能を利用したり、より便利にジョブを実行したりするためにできることです。

カスタムコマンドライン引数のスキーマを追加する

Appiumサーバーの起動時にコマンドラインからデータを受信したい場合(たとえば、機能として渡されるべきではないサーバー管理者が設定する必要があるポートなど)、カスタムCLI引数を追加できます。

AppiumサーバーのCLI引数(または構成プロパティ)を定義するには、拡張機能がスキーマを提供する必要があります。拡張機能の package.jsonappium プロパティで、schema プロパティを追加します。これは、a)スキーマ自体であるか、b)スキーマファイルへのパスになります。

これらのスキーマのルール

  • スキーマは、JSON Schema Draft-07に準拠する必要があります。
  • schema プロパティがスキーマファイルへのパスである場合、ファイルはJSONまたはJS(CommonJS)形式である必要があります。
  • カスタムの $id 値はサポートされていません。$ref を使用するには、スキーマルートからの相対値を指定します。例:/properties/foo
  • format キーワードの既知の値はサポートされる可能性がありますが、他のさまざまなキーワードはサポートされない場合があります。もし、使用する必要があるにもかかわらずサポートされていないキーワードを発見した場合は、サポートを依頼するか、PRを送ってください。
  • スキーマは、properties キーワードに引数を含む object 型 ({"type": "object"}) である必要があります。ネストされたプロパティはサポートされていません。

{
  "type": "object",
  "properties": {
    "test-web-server-port": {
      "type": "integer",
      "minimum": 1,
      "maximum": 65535,
      "description": "The port to use for the test web server"
    },
    "test-web-server-host": {
      "type": "string",
      "description": "The host to use for the test web server",
      "default": "sillyhost"
    }
  }
}

上記のスキーマは、CLI引数または構成ファイルを使用して設定できる2つのプロパティを定義しています。この拡張機能がドライバであり、その名前が「horace」の場合、CLI引数はそれぞれ--driver-horace-test-web-server-port--driver-horace-test-web-server-hostになります。あるいは、ユーザーは次の構成ファイルを提供できます。

{
  "server": {
    "driver": {
      "horace": {
        "test-web-server-port": 1234,
        "test-web-server-host": "localhorse"
      }
    }
  }
}

ドライバスクリプトの追加

ドライバのユーザーがセッションのコンテキスト外でスクリプトを実行できるようにしたい場合があります(たとえば、ドライバの側面を事前に構築するスクリプトを実行する場合など)。これをサポートするには、Appium拡張機能のメタデータ内のscriptsフィールドにスクリプト名とJSファイルのマップを追加できます。たとえば、プロジェクトのscriptsディレクトリにdriver-prebuild.jsという名前のスクリプトを作成したとします。次に、次のようなscriptsフィールドを追加できます。

{
    "scripts": {
        "prebuild": "./scripts/driver-prebuild.js"
    }
}

これで、ドライバの名前がmydriverであるとすると、ドライバのユーザーはappium driver run mydriver prebuildを実行でき、スクリプトが実行されます。

別のWebDriver実装へのプロキシコマンド

Appiumドライバの非常に一般的な設計アーキテクチャは、Appiumドライバがインターフェイスする何らかのプラットフォーム固有のWebDriver実装を持つことです。たとえば、Appium UiAutomator2ドライバは、Androidデバイスで実行されている特別な(Javaベースの)サーバーとインターフェイスします。webviewモードでは、Chromedriverともインターフェイスします。

このような状況になった場合、ドライバがWebDriverコマンドを別のエンドポイントに直接プロキシすることをAppiumに伝えるのは非常に簡単です。

まず、canProxyメソッドを実装して、ドライバがプロキシできることをAppiumに知らせます。

canProxy() {
    return true;
}

次に、Appiumに、プロキシを試みないWebDriverルートを伝えます(転送したくない特定のルートが最終的に発生することがよくあります)。

getProxyAvoidList() {
    return [
        ['POST', new RegExp('^/session/[^/]+/appium')]
    ];
}

プロキシ回避リストは配列の配列である必要があります。各内部配列には、最初のメンバーとしてHTTPメソッド、2番目のメンバーとして正規表現が含まれます。正規表現がルートと一致する場合、ルートはプロキシされず、代わりにドライバによって処理されます。この例では、appiumプレフィックスを持つすべてのPOSTルートのプロキシを回避しています。

次に、プロキシ自体をセットアップする必要があります。これを行う方法は、AppiumのJWProxyという特別なクラスを使用することです。(名前は「JSON Wire Proxy」を意味し、プロトコルのレガシー実装に関連しています)。リモートサーバーへの接続に必要な詳細を使用して、JWProxyオブジェクトを作成する必要があります。

// import {JWProxy} from 'appium/driver';

const proxy = new JWProxy({
    server: 'remote.server',
    port: 1234,
    base: '/',
});

this.proxyReqRes = proxy.proxyReqRes.bind(proxy);
this.proxyCommand = proxy.command.bind(proxy);

ここでは、プロキシオブジェクトを作成し、そのメソッドの一部をproxyReqResおよびproxyCommandという名前でthisに割り当てています。これは、Appiumがプロキシを使用するために必要であるため、このステップを忘れないでください!JWProxyには、ソースコードで確認できるさまざまなオプションもあります。(TODO:オプションをAPIドキュメントとして公開し、ここにリンクします)。

最後に、プロキシがアクティブなときにAppiumに伝える方法が必要です。ドライバでは常にアクティブであるか、特定のコンテキストでのみアクティブになる場合があります。proxyActiveの実装としてロジックを定義できます。

proxyActive() {
    return true; // or use custom logic
}

これらの要素が揃っていれば、プロキシ先のリモートエンドポイントによってすでに実装されているものを再実装する必要はありません。Appiumがすべてのプロキシを処理します。

別のBiDi実装へのプロキシBiDiコマンド

上記のようなWebDriverコマンドのプロキシに関する説明は、概念的にはBiDiコマンドのプロキシにも当てはまります。BiDiプロキシを有効にするには、次のものが必要です。

  1. ドライバインスタンスのdoesSupportBidiフィールドをtrueに設定します。
  2. ドライバにget bidiProxyUrlを実装します。これは、BiDiコマンドのプロキシ先となるアップストリームソケットのアドレスであるWebsocket URLを返す必要があります。

ここでの意図されたパターンは、アップストリーム実装でセッションを開始し、返されたケイパビリティ(たとえば、webSocketUrlケイパビリティ)にアクティブなBiDiソケットがあるかどうかを確認し、その値を内部フィールドに設定して、get bidiProxyUrlで返せるようにすることです。これがすべて完了すると、AppiumはクライアントからのBiDiコマンドをアップストリーム接続に直接プロキシします。

新しいコマンドで既存のプロトコルを拡張する

既存のコマンドではドライバに不十分な場合があります。既存のコマンドにマッピングされない動作を公開する場合は、次の2つの方法のいずれかで新しいコマンドを作成できます。

  1. WebDriverプロトコルを拡張し、拡張機能にアクセスするためのクライアント側プラグインを作成する
  2. 実行メソッドを定義して、Execute Scriptコマンドをオーバーロードする

最初のパスに従う場合は、Appiumに新しいメソッドを認識させ、許可されたHTTPルートとコマンド名のセットに追加するように指示できます。これは、ドライバクラスのnewMethodMap静的変数を、Appiumのroutes.jsオブジェクトと同じ形式のオブジェクトに割り当てることで行います。たとえば、次にFakeDriverサンプルドライバのnewMethodMapを示します。

static newMethodMap = {
  '/session/:sessionId/fakedriver': {
    GET: {command: 'getFakeThing'},
    POST: {command: 'setFakeThing', payloadParams: {required: ['thing']}},
  },
  '/session/:sessionId/fakedriverargs': {
    GET: {command: 'getFakeDriverArgs'},
  },
};

この例では、いくつかの新しいルートと合計3つの新しいコマンドを追加しています。この方法でコマンドを定義する方法の例については、routes.jsを参照するのが最適です。これで、他のAppiumコマンドを実装する場合と同じ方法でコマンドハンドラーを実装するだけです。

この方法で新しいコマンドを追加することの欠点は、標準のAppiumクライアントを使用している人が、これらのエンドポイントをターゲットとするように設計された優れたクライアント側関数を持っていないことです。したがって、サポートする各言語に対してクライアント側プラグインを作成およびリリースする必要があります(手順または例は、関連するクライアントドキュメントにあります)。

これを行う別の方法は、すべてのWebDriverクライアントがすでにアクセスできるコマンドであるExecute Scriptをオーバーロードすることです。Appiumは、これを簡単にするための便利なツールを提供します。soundzと呼ばれるステレオシステムのドライバを構築していて、名前で曲を再生するためのコマンドを作成したいとします。次のようなものを呼び出すように、これをユーザーに公開できます。

// webdriverio example. Calling webdriverio's `executeScript` command is what trigger's Appium's
// Execute Script command handler
driver.executeScript('soundz: playSong', [{song: 'Stairway to Heaven', artist: 'Led Zeppelin'}]);

次に、ドライバコードで、スクリプト名からドライバのメソッドへのマッピングとして静的プロパティexecuteMethodMapを定義できます。これは、上記で説明したnewMethodMapと同じ基本形式を持っています。executeMethodMapが定義されたら、Appiumのルートマッピングに従ってexecuteと呼ばれるExecute Scriptコマンドハンドラーも実装する必要があります。実装では、ユーザーが送信したスクリプトと引数を調べて、定義した正しいカスタムハンドラーにルーティングする単一のヘルパー関数this.executeMethodを呼び出すことができます。次に例を示します。

static executeMethodMap = {
  'soundz: playSong', {
    command: 'soundzPlaySong',
    params: {required: ['song', 'artist'], optional: []},
  }
}

async soundzPlaySong(song, artist) {
  // play the song based on song and artist details
}

async execute(script, args) {
  return await this.executeMethod(script, args);
}

このシステムに関する注意点がいくつかあります。1. Execute Scriptの呼び出しを介して送信される引数配列には、0個または1個の要素のみを含める必要があります。リストの最初の項目は、メソッドのパラメーターオブジェクトと見なされます。これらのパラメーターは解析、検証され、executeMethodMapで指定された順序(requiredパラメーターリストで指定された順序、その後にoptionalパラメーターリスト)でオーバーロードメソッドに適用されます。つまり、このフレームワークは、Execute Scriptを介して送信される実際の引数が1つだけであることを前提としています(そして、この引数は、実行メソッドが予期するパラメーターを表すキー/値を持つオブジェクトである必要があります)。1. Appiumは、execute(Execute Scriptハンドラー)を自動的に実装しません。たとえば、プロキシモードでない場合にのみexecuteMethodヘルパー関数を呼び出すことをお勧めします。1. スクリプト名がexecuteMethodMapでコマンドとして定義されたスクリプト名と一致しない場合、またはパラメーターが不足している場合、executeMethodヘルパーはエラーで拒否します。

Appium Doctorチェックの構築

ユーザーはappium driver doctor <driverName>を実行して、インストールとヘルスチェックを実行できます。この機能の詳細については、Doctorチェックの構築ガイドをご覧ください。

Appium設定の処理の実装

Appiumユーザーは、CLI引数だけでなく、ケイパビリティを介してドライバにパラメーターを送信できます。ただし、これらはテスト中に変更することはできず、テスト中にパラメーターを調整したい場合があります。Appiumには、この目的のための設定APIがあります。

独自のドライバで設定をサポートするには、まずコンストラクターで、this.settingsを適切なクラスのインスタンスとして定義します。

// import {DeviceSettings} from 'appium/driver';

this.settings = new DeviceSettings();

これで、this.settings.getSettings()を呼び出すだけでいつでもユーザー設定を読み取ることができます。これにより、設定名がキーとなり、対応する値を持つJSオブジェクトが返されます。

デフォルト設定を割り当てたり、設定が更新されるたびにエンドで何らかのコードを実行したい場合は、これら両方のことも実行できます。

constructor() {
  const defaults = {setting1: 'value1'};
  this.settings = new DeviceSettings(defaults, this.onSettingsUpdate.bind(this));
}

async onSettingsUpdate(key, value) {
  // do anything you want here with key and value
}

BiDiイベントの発行

WebDriver BiDiプロトコルを使用すると、クライアントはBiDiソケット接続を介してクライアントに非同期で送信できる任意のイベントをサブスクライブできます。Appiumドライバの作成者は、イベントのサブスクリプションについて心配する必要はありません。特定のメソッド名とペイロードでイベントを発行する場合は、bidiEventイベントで組み込みのイベントエミッターを使用するのと同じくらい簡単です。

例として、ドライバが定期的にCPU負荷情報を発行したいとします。system.cpuと呼ばれるイベントと、CPU使用率97%を示す{load: 0.97}のようなペイロードを定義できます。必要なときにいつでも、ドライバは次のコードを呼び出すことができます(現在の負荷がthis.currentCpuLoadにあると仮定します)。

this.eventEmitter.emit('bidiEvent', {
  method: 'system.cpu',
  params: {load: this.currentCpuLoad},
})

これで、クライアントがsystem.cpuイベントをサブスクライブしている場合、ドライバが発行するたびに負荷が通知されます。

他の同時実行ドライバが使用しているリソースを認識させる

ドライバがポートなどの一部のシステムリソースを消費するとします。複数の同時セッションが同じリソースを使用しないようにする方法はいくつかあります。

  1. ユーザーにケイパビリティ(appium:driverPortなど)を介してリソースIDを指定させる
  2. 常に空きリソースを使用する(各セッションに新しいランダムポートを見つける)
  3. 各ドライバに使用しているリソースを表明させ、新しいセッションが開始されるときに他のドライバから現在使用されているリソースを調べる。

この3番目の戦略をサポートするには、ドライバのget driverDataを実装して、ドライバが現在使用しているリソースの種類を返すことができます。たとえば、

get driverData() {
  return {specialPort: 1234, specialFile: /path/to/file}
}

これで、ドライバで新しいセッションが開始されると、同時に実行されている他のドライバ(同じタイプ)からのdriverData応答も、createSessionメソッドの最後のパラメーターとして含まれます。

async createSession(jwpCaps, reqCaps, w3cCaps, driverData)

このdriverData配列を掘り下げて、他のドライバがどのリソースを使用しているかを確認し、この特定のセッションに使用するリソースを決定するのに役立ちます。

警告

ここでは注意してください。driverDataは、単一の実行中のAppiumサーバーのセッション間でのみ渡されるためです。ユーザーが複数のAppiumサーバーを実行し、それらのサーバーで同時にドライバを要求することを妨げるものはありません。この場合、driverDataを介してリソースの独立性を確保することはできないため、ファイルベースのロックメカニズムまたは同様のものを使用することを検討する場合があります。

警告

また、driverDataを受け取れるのは、あなた自身のドライバーの別のインスタンスに限られることに注意が必要です。したがって、関係のない別のドライバーが実行されている場合でも、システムリソースを使用している可能性があります。一般的に、Appiumは、関係のないドライバー同士が互いに干渉しないようにするための機能を提供していないため、ユーザーが競合を避けるためにリソースの場所やアドレスを指定できるようにするのはドライバーの責任となります。

Appiumイベントタイムラインにイベントを記録する

Appiumには、イベントタイミングAPIがあり、ユーザーが特定のサーバー側イベント(コマンド、起動マイルストーンなど)のタイムスタンプを取得し、タイムラインに表示できるようにします。この機能は基本的に、Appiumドライバーの内部のデバッグや分析の実行に役立つように、内部イベントのタイミングを調べるために存在します。イベントログに独自のイベントを追加することができます。

this.logEvent(name);

イベントの名前を指定するだけで、現在の時刻に追加され、ユーザーがイベントログの一部としてアクセスできるようになります。

セキュリティフラグの背後に動作を隠す

Appiumには、機能フラグに基づいたセキュリティモデルがあり、ドライバーの作成者が特定の機能をセキュリティフラグの背後に隠すことができます。つまり、セキュリティ上の懸念があり、サーバー管理者に明示的に有効にしてもらいたい機能がある場合、--allow-insecureリストに追加するか、サーバーセキュリティを完全にオフにすることで機能を有効にすることを要求できます。

自身のドライバー内でチェックをサポートするには、this.isFeatureEnabled(featureName)を呼び出して、指定された名前の機能が有効になっているかどうかを判断できます。または、機能が有効になっていない場合にエラーを発生させて処理を中断したい場合は、this.assertFeatureEnabled(featureName)を呼び出すことができます。

ファイルに一時ディレクトリを使用する

ドライバーが作成したファイルのうち、コンピューターやサーバーの再起動間で保持する必要のない一時ディレクトリを使用したい場合は、this.opts.tmpDirから読み込むだけで済みます。これは、@appium/supportから一時ディレクトリの場所を読み込み、CLIフラグで上書きされる可能性があります。つまり、ユーザーの設定に合わせて調整されるため、独自の一時ディレクトリに書き込むよりも安全です。this.opts.tmpDirは、ディレクトリへのパスを表す文字列です。

予期しないシャットダウンまたはクラッシュに対処する

ドライバーは、正常な動作を継続できない状況に陥る可能性があります。たとえば、外部サービスがクラッシュし、何も機能しなくなったことを検出する場合があります。この場合、詳細を含むエラーオブジェクトとともにthis.startUnexpectedShutdown(err)を呼び出すことができます。Appiumは、セッションをシャットダウンする前に残りのリクエストを適切に処理しようとします。

この状態が発生したときに独自のクリーンアップロジックを実行したい場合は、this.startUnexpectedShutdownを呼び出す直前に実行するか、予期しないシャットダウンイベントにハンドラーをアタッチし、クリーンアップロジックを「アウトオブバンド」で実行することができます。

this.onUnexpectedShutdown(handler)

handlerは、予期しないシャットダウンの理由を表すエラーオブジェクトを受け取る関数である必要があります。