コンテンツにスキップ

プラグインの構築

これは、Appiumプラグインを開発するためのハイレベルなガイドであり、ほとんどのAppiumユーザーが知る必要も関心を持つ必要もないものです。ユーザーの視点からAppiumプラグインにまだ馴染みがない場合は、プラグインリストをチェックして、いくつか試してみて、プラグインがどのようなことができるのかを理解してください。プラグインは、Appiumの機能を拡張したり、Appiumの動作方法を変更したりするための強力なシステムです。他のAppiumユーザーに配布したり、あらゆる興味深い方法でAppiumのエコシステムを拡張したりできます!(ここにはAppiumドライバーの開発との重複も多くありますので、さらにインスピレーションを得るためにドライバーの構築ガイドも確認することをお勧めします。)

プラグインを作成する前に

プラグインを作成する前に、プラグインで何を達成したいのか、およびAppiumプラットフォームの制限を考慮してそれを実装することが可能かどうかを大まかに把握しておくと良いでしょう。このガイドを読むことで、何が可能になるかを理解するのに役立ちます。一般的に、Appiumのプラグインシステムは非常に強力であり、プラグインで可能なことを人為的に制限する試みは行われていません(これが、すべてのプラグインがAppiumサーバーの起動を担当するシステム管理者によってオプトインされている主な理由です。プラグインは強力であり、明示的に信頼されている場合にのみ使用する必要があります!)。

参考になる他のプラグイン

幅広い種類のオープンソースのAppiumプラグインが閲覧可能です。自分のプラグインの作成に着手する前に、他のいくつかのプラグインのコードを調べてみることを強くお勧めします。Appiumチームは、Appium GitHubリポジトリで一連の公式プラグインを管理しています。他のオープンソースプラグインへのリンクは、プラグインリストにあります。

プラグインの基本要件

有効な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": {
    "pluginName": "fake",
    "mainClass": "FakePlugin"
  },
  ...
}
```

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

  • pluginName:これはプラグインの短い名前である必要があります。
  • mainClass:これは、mainフィールドからの名前付きエクスポート(CommonJSスタイル)です。これは、AppiumのBasePluginを拡張するクラスである必要があります(下記参照)。

AppiumのBasePluginクラスの拡張

最終的に、プラグインの記述は非常に簡単になります。これは、コマンドをオーバーライドするためのパターンの定義のほとんどの難しい作業がすでに行われているためです。これはすべて、Appiumが使用するためにエクスポートするBasePluginというクラスとしてエンコードされています。これはappium/pluginからエクスポートされるため、次のいずれかのスタイルを使用してインポートし、拡張する独自のクラスを作成できます。

import {BasePlugin} from 'appium/plugin';
// or: const {BasePlugin} = require('appium/plugin');

export class MyPlugin extends BasePlugin {
  // class methods here
}

以下のすべてのコードサンプルでは、例のメソッドを参照するときは、明確さとスペースのために、明示的には記述されていませんが、そのメソッドがクラスで定義されていると仮定しています。

プラグインを利用可能にする

基本的にそれだけです!プラグインクラスをエクスポートするNode.jsパッケージと正しいAppium拡張機能メタデータがあれば、Appiumプラグインができました!今はまだ何も実行しませんが、Appiumでロードしたり、アクティブ化したりできます。

ユーザーが利用できるようにするには、NPM経由で公開できます。そうすると、プラグインはAppium CLI経由でインストールできるようになります。

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

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

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

そしてもちろん、プラグインはAppiumサーバーの起動時に「アクティブ化」する必要があるため、ユーザーにそのように指示するようにしてください。

appium --use-plugins=plugin-name

プラグインの開発

プラグインの開発方法はユーザー次第です。ただし、多くの公開とインストールを行うことなく、Appium内から実行すると便利です。これを行う最も簡単な方法は、最新バージョンのAppiumをdevDependencyとして含めることです(ただし、peerDependencyとしてすでに含まれている場合は、新しいバージョンのNPMでは十分です)。次に、次のように独自のプラグインも含めます。

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

これで、Appiumをローカルで(npm exec appiumまたはnpx appium)実行できます。また、プラグインが依存関係として一緒にリストされているため、自動的に「インストール」され、利用可能になります。この方法でe2eテストを設計するか、Node.jsで記述している場合は、Appiumのサーバー起動メソッドをインポートして、NodeでAppiumサーバーの起動と停止を処理できます。

もちろん、上記のようにローカルにインストールすることもできます。

プラグインコードに変更を加えるたびに、Appiumサーバーを再起動して、最新のコードが取得されるようにする必要があります。ドライバーと同様に、新しいセッションが開始されたときにAppiumがプラグインモジュールを再requireしようとする場合は、APPIUM_RELOAD_EXTENSIONS環境変数を設定できます。

標準的なプラグインの実装アイデア

プラグインを作成するときに、おそらく実行したいと思う項目を次に示します。

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

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

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

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

特定のAppiumコマンドをインターセプトして処理する

これは Appium プラグインの最も一般的な動作です。つまり、通常はアクティブなドライバによって処理される 1 つまたは複数のコマンドの実行を修正または置き換えることです。デフォルトのコマンド処理をオーバーライドするには、処理対象の Appium コマンドと同じ名前でクラス内に async メソッドを実装する必要があります(ドライバ自体が実装される方法とまったく同じです)。コマンド名が何であるか気になりますか?それらは Appium ベースドライバの routes.js ファイルで定義されています。もちろん、次のセクションで定義されているように、さらに追加することもできます。

各コマンドメソッドには、次の引数が送信されます。

  1. next: これは、このプラグインがコマンドを処理していない場合に発生する一連の動作をカプセル化する async 関数への参照です。ロジック内の任意の時点でチェーン内の次の動作を呼び出すことを選択できます(どこかに await next() を含めるようにすることで)。または、呼び出さないこともできます。呼び出さない場合、デフォルトの動作(またはこれの後に登録されたプラグイン)は実行されないことを意味します。
  2. driver: これは、現在のセッションを処理するドライバを表すオブジェクトです。他のドライバメソッドの呼び出し、ケーパビリティや設定の確認など、必要な作業のためにアクセスできます。
  3. ...args: ユーザーによってコマンドに適用されたすべての引数を含むスプレッド配列。

たとえば、setUrl コマンドをオーバーライドして、その上にいくつかの追加ロギングを追加したい場合、次のように実装します。

async setUrl(next, driver, url) {
  this.log(`Let's get the page source for some reason before navigating to '${url}'!`);
  await driver.getPageSource();
  const result = await next();
  this.log(`We can also log after the original behaviour`);
  return result;
}

すべての Appium コマンドをインターセプトして処理する

ペイロードを検査して何らかの方法で動作するかどうかを判断するために、すべての コマンドを処理したい状況になる場合があります。その場合、async handle を実装できます。名前付きメソッドのいずれかによって処理されないコマンドは、代わりにこのメソッドによって処理されます。次のパラメータを受け取ります(上記と同じセマンティクスを使用します)。

  1. next
  2. driver
  3. cmdName - 実行中のコマンドを表す文字列
  4. ...args

たとえば、プラグインの一部としてすべての Appium コマンドのタイミングを記録したいとしましょう。プラグインクラスで handle を次のように実装することで、これを行うことができます。

async handle(next, driver, cmdName, ...args) {
  const start = Date.now();
  try {
    const result = await next();
  } finally {
    const elapsedMs = Date.now() - start;
    this.log(`Command '${cmdName}' took ${elapsedMs}`);
  }
  return result;
}

ドライバプロキシを回避する

Appium コマンドの処理には少し注意が必要です。Appium ドライバには、特別な「プロキシ」モードをオンにする機能があります。このモードでは、Appium サーバープロセスは着信する URL を調べて、それらをアップストリームの WebDriver サーバーに転送するかどうかを決定します。プラグインが処理したいコマンドが、アップストリームサーバーにプロキシされるコマンドとして指定されている可能性があります。この場合、問題が発生します。プラグインがそのコマンドを処理する機会がないからです!このため、プラグインは、次のパラメータを受け取る shouldAvoidProxy という特別なメンバー関数を実装できます。

  1. method - HTTP メソッドを示す文字列(GETPOST など)
  2. route - たとえば /session/8b3d9aa8-a0ca-47b9-9ab7-446e818ec4fc/source のように、要求されたリソースを示す文字列
  3. body - WebDriver リクエストボディを表すオプションの任意の値

これらのパラメータは、着信リクエストを定義します。通常はドライバを介して直接プロキシされるコマンドをプラグインで処理する場合は、リクエストのプロキシを無効にするか「回避」して、代わりにリクエストを通常の Appium コマンド実行フロー(したがって、独自のコマンド関数)にフォールバックさせることができます。リクエストのプロキシを回避するには、shouldAvoidProxy から true を返すだけです。このメソッドの使い方の例としては、ユニバーサル XML プラグインgetPageSource コマンドのプロキシを回避する場合)や、画像プラグイン(画像要素が含まれているように見える場合、条件付きですべてのコマンドのプロキシを回避する場合)などがあります。

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

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

import {errors} from 'appium/driver';

throw new errors.NoSuchElementError();

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

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

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

Appium プラグインのさらなる可能性

これらは、プラグインが追加のプラグイン機能を利用したり、より便利に作業を行うためにできることです。

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

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

これはプラグインの場合とほぼ同じように機能するため、詳細については、ドライバの構築ドキュメントの同等のセクションを参照してください。

唯一の違いは、CLI 引数名を構築するには、--plugin-<name> をプレフィックスとして付けることです。たとえば、pluggo という名前のプラグインと、名前 electro-port で定義された CLI 引数がある場合、--plugin-pluggo-electro-port を介して Appium の起動時に設定できます。

構成ファイルによる引数の設定も、ドライバの場合と同様にサポートされていますが、plugin フィールドの下にあります。たとえば

{
  "server": {
    "plugin": {
      "pluggo": {
        "electro-port": 1234
      }
    }
  }
}

プラグインスクリプトを追加する

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

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

これで、プラグインの名前が myplugin であると仮定すると、プラグインのユーザーは appium plugin run myplugin prebuild を実行でき、スクリプトが実行されます。

新しい Appium コマンドを追加する

ドライバでサポートされている既存のコマンドのいずれにもマッピングされない機能を提供したい場合は、ドライバの場合と同様に、2 つの方法のいずれかで新しいコマンドを作成できます。

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

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

static newMethodMap = {
  '/session/:sessionId/fake_data': {
    GET: {command: 'getFakeSessionData', neverProxy: true},
    POST: {
      command: 'setFakeSessionData',
      payloadParams: {required: ['data']},
      neverProxy: true,
    },
  },
  '/session/:sessionId/fakepluginargs': {
    GET: {command: 'getFakePluginArgs', neverProxy: true},
  },
};

TypeScript を使用している場合、このような静的メンバーオブジェクトは as const として定義する必要があります。

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

コマンドの特別な neverProxy キーにも注意してください。プラグインがプロキシモードになっているドライバでアクティブになる可能性があるが、これらの(新しいしたがって不明な)コマンドのプロキシを拒否していない可能性があるため、これは通常、プラグインに設定することをお勧めします。ここで neverProxytrue に設定すると、Appium はこれらのルートをプロキシせず、ドライバがプロキシモードの場合でも、プラグインがそれらを処理することを保証します。

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

この方法の代わりに、すべての WebDriver クライアントがすでにアクセスできるコマンドをオーバーロードする方法があります。それは、Execute Script です。この仕組みを理解するには、ビルドドライバガイドの新しいコマンドの追加に関するセクションを必ず読んでください。プラグインでの動作はわずかに異なるだけです。Appium の fake-plugin からの例を見てみましょう。

static executeMethodMap = {
  'fake: plugMeIn': {
    command: 'plugMeIn',
    params: {required: ['socket']},
  },
};

async plugMeIn(next, driver, socket) {
  return `Plugged in to ${socket}`;
}

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

このシステムが機能するために、ここで 3 つの重要なコンポーネントを示します。これらはすべて、プラグインクラス内で定義されています。

  1. ドライバの場合と同じように定義された executeMethodMap
  2. executeMethodMap で定義されたコマンドメソッドの実装(この場合は plugMeIn
  3. execute コマンドのオーバーライド/処理。他のプラグインコマンドハンドラーと同様に、最初の 2 つの引数は nextdriver で、その後にスクリプト名と引数が続きます。BasePlugin は、これらの引数をすべて使用して呼び出すことができるヘルパーメソッドを実装しています。

ドライバからの実行メソッドのオーバーライドは、予想どおりに機能します。プラグインがドライバと同じ名前の実行メソッドを定義している場合、最初にプラグインのコマンド(この場合は plugMeIn)が呼び出されます。必要に応じて、next を介してドライバの元の動作を実行することを選択できます。

Appium Doctor チェックを構築する

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

Appium サーバーオブジェクトを更新する

通常、Appium サーバーオブジェクト(Express サーバーで、すでにさまざまな方法で構成済み)を更新する必要はないでしょう。ただし、たとえば、プラグインの要件をサポートするために、新しい Express ミドルウェアをサーバーに追加できます。サーバーを更新するには、クラスで static async updateServer メソッドを実装する必要があります。このメソッドは、次の 3 つのパラメータを受け取ります。

  • expressApp: Express アプリオブジェクト
  • httpServer: Node HTTP サーバーオブジェクト
  • cliArgs: Appium サーバーの起動に使用された CLI 引数のマップ

updateServer メソッド内でそれらを使用して、必要な操作を実行できます。これらのオブジェクトがどのように作成され、BaseDriver コードでどのように使用されるかを参照して、標準的で重要なものを元に戻したり、オーバーライドしたりしていないことを確認する必要があります。ただし、必要に応じて、テストする必要のある結果を使って実行できます。警告:これは高度な機能と見なされるべきであり、Express の知識が必要であり、Appium サーバーの他の部分の動作に影響を与える可能性のあることをしないように注意する必要があります!

予期しないセッションのシャットダウンを処理する

プラグインを開発する際、セッションが終了したときにいくつかのクリーンアップロジックを追加したい場合があります。通常、これはdeleteSessionのハンドラーを追加することで実現します。ほとんどの場合、これでうまくいきますが、セッションが正常に終了しない場合は例外です。Appiumは、セッションが予期せず終了したと判断することがあります。このような場合、Appiumはプラグインクラス内でonUnexpectedShutdownというメソッドを探し、それを呼び出します(現在のセッションドライバを最初のパラメータとして、シャットダウンの原因を表すエラーオブジェクトを2番目のパラメータとして渡します)。これにより、セッションからクリーンアップするために必要な手順を実行する機会が与えられます。たとえば、この関数はawaitされないことを念頭に置いて、次のような実装が可能です。

async onUnexpectedShutdown(driver, cause) {
  try {
    // do some cleanup
  } catch (e) {
    // log any errors; don't allow anything to be thrown as they will be unhandled rejections
  }
}