建立外掛程式
這是一份開發 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,但它必須提供以 Node.js 編寫的轉接器,以便 Appium 可以載入它。
您的 package.json
必須將 appium
納入為 peerDependency
。相依性版本的需求應盡可能寬鬆 (除非您知道您的外掛程式只會與特定版本的 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 中如何運作的方法之一,就是先在本地安裝它
當然,外掛程式必須在 Appium 伺服器啟動期間「啟用」,因此請務必指示您的使用者這麼做
開發您的外掛程式¶
如何開發外掛程式由您決定。不過,從 Appium 內部執行外掛程式很方便,無需進行大量發布和安裝。最直接的方式是將最新版本的 Appium 納入為 devDependency
(儘管在較新版本的 NPM 中,將其納入為 peerDependency
就已足夠),然後再納入您自己的外掛程式,如下所示
現在,您可以本地執行 Appium(npm exec appium
或 npx appium
),由於您的外掛程式與之一起列為相依性,因此會自動「安裝」並可以使用。您可以這樣設計 e2e 測試,或者如果您使用 Node.js 編寫它們,則可以簡單地匯入 Appium 的啟動伺服器方法,以處理在 Node 中啟動和停止 Appium 伺服器。
當然,您也可以隨時如上所述在本地安裝它。
每當您對外掛程式程式碼進行變更時,您都需要重新啟動 Appium 伺服器,以確保它採用最新的程式碼。與驅動程式一樣,如果您希望 Appium 在新工作階段開始時嘗試重新需要您的外掛程式模組,則可以設定 APPIUM_RELOAD_EXTENSIONS
環境變數。
標準外掛程式實作構想¶
這些是您在建立外掛程式時可能會想要執行的操作。
在建構函式中設定狀態¶
如果您定義自己的建構函式,您需要呼叫 super
以確保所有標準狀態都正確設定
此處的 args
參數是包含用於啟動 Appium 伺服器的所有 CLI 引數的物件。
攔截並處理特定 Appium 指令¶
這是 Appium 外掛程式的最常見行為,即修改或取代由活動驅動程式通常會處理的一個或多個指令的執行。若要覆寫預設指令處理,您需要在您的類別中實作具有與要處理的 Appium 指令相同名稱的 async
方法(就像驅動程式本身的實作方式一樣)。好奇有哪些指令名稱嗎?它們定義在 Appium 基本驅動程式的 routes.js 檔案中,當然您也可以新增更多,如下一節所定義。
每個命令方法會傳送下列引數
next
:這是對async
函式的參考,它封裝了如果這個外掛程式沒有處理命令時會發生的行為鏈。您可以在邏輯中的任何一點選擇呼叫鏈中的下一個行為(透過確保在某處包含await next()
),也可以不呼叫。如果您沒有呼叫,表示預設行為(或此行為之後註冊的任何外掛程式)不會執行。driver
:這是代表處理目前階段的驅動程式的物件。您可以存取它來執行任何您需要執行的作業,例如呼叫其他驅動程式方法、檢查功能或設定等......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
,而未由您的其中一個命名方法處理的任何命令將改由這個方法處理。它會採用下列參數(與上述所有語意相同)
next
driver
cmdName
- 字串,代表正在執行的命令...args
例如,假設我們想要記錄所有 Appium 命令的時間,作為外掛程式的一部分。我們可以透過在我們的 plugin 類別中實作 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
的特殊成員函式,它會採用下列參數
method
- 表示 HTTP 方法(GET
、POST
等)的字串route
- 表示所請求資源的字串,例如/session/8b3d9aa8-a0ca-47b9-9ab7-446e818ec4fc/source
body
- 表示 WebDriver 請求主體的任何型別的選用值
這些參數定義一個輸入請求。如果您想在您的外掛程式中處理一個指令,而這個指令通常會透過驅動程式直接代理,您可以停用或「避免」代理請求,並讓請求進入典型的 Appium 指令執行流程(從而進入您自己的指令函式)。若要避免代理請求,只需從 shouldAvoidProxy
傳回 true
。此方法的一些使用範例在 通用 XML 外掛程式(我們希望避免代理 getPageSource
指令)或 影像外掛程式(我們希望有條件地避免代理任何指令,如果它看起來包含一個影像元素)。
擲出 WebDriver 特定的錯誤¶
WebDriver 規格定義了一組 錯誤碼,用於在發生錯誤時伴隨指令回應。Appium 已為每個這些碼建立錯誤類別,因此您可以從指令內部擲出適當的錯誤,它將針對使用者協定回應採取正確的動作。若要取得這些錯誤類別,請從 appium/driver
匯入它們
記錄訊息至 Appium 記錄¶
當然,您隨時可以使用 console.log
,但 Appium 為您提供了一個不錯的記錄器,稱為 this.logger
(它有 .info
、.debug
、.log
、.warn
、.error
方法,用於不同的記錄層級)。如果您想在插件內容之外建立一個 Appium 記錄器(例如在腳本或輔助檔案中),您也可以隨時建立自己的記錄器
Appium 外掛程式的更多可能性¶
這些是您的外掛程式可以執行的動作,以利用額外的外掛程式功能或更方便地完成其工作。
新增自訂命令列引數的架構¶
如果您希望您的外掛程式在 Appium 伺服器啟動時從命令列接收資料(例如,伺服器管理員應設定但不應傳遞為功能的埠),您可以新增自訂 CLI 引數。
這在很大程度上與驅動程式的外掛程式相同,因此有關更多詳細資訊,請參閱 建立驅動程式文件中的等效區段。
唯一的不同是,要建構 CLI 參數名稱,您必須加上 --plugin-<name>
前綴。因此,例如,如果您有一個名為 pluggo
的外掛程式和一個定義為 electro-port
名稱的 CLI 參數,您可以在透過 --plugin-pluggo-electro-port
啟動 Appium 時設定它。
也支援透過設定檔設定參數,就像驅動程式一樣,但改在 plugin
欄位中。例如
新增外掛程式腳本¶
有時您可能希望外掛程式的使用者能夠在會話的內容之外執行腳本(例如,執行預先建置外掛程式面向的腳本)。為支援此功能,您可以在 Appium 擴充功能的元資料中,將腳本名稱和 JS 檔案的對應新增到 scripts
欄位。因此,假設您已在專案中建立一個腳本,該腳本位於專案中的 scripts
目錄中,名稱為 plugin-prebuild.js
。然後,您可以新增一個類似這樣的 scripts
欄位
現在,假設您的外掛程式名稱為 myplugin
,外掛程式的使用者可以執行 appium plugin run myplugin prebuild
,您的腳本就會執行。
新增新的 Appium 指令¶
如果您想要提供不對應到驅動程式支援的任何現有指令的功能,您可以使用兩種方式之一建立新的指令,就像驅動程式一樣
- 擴充 WebDriver 協定並建立用戶端外掛程式來存取擴充功能
- 透過定義執行方法來覆寫執行腳本指令
如果您想要遵循第一個路徑,您可以指示 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
鍵;對於外掛程式而言,通常建議將其設為 true
,因為您的外掛程式可能對已設為代理模式但未拒絕為這些(新的且未知的)命令代理的驅動程式處於作用中。在此將 neverProxy
設為 true
將導致 Appium 永不代理這些路由,並因此確保您的外掛程式處理它們,即使驅動程式處於代理模式。
透過 newMethodMap
新增命令的缺點在於,使用標準 Appium 用戶端的人員不會有針對這些端點設計的良好用戶端端函式。因此,您需要為您想支援的每種語言建立並釋出用戶端端外掛程式(相關用戶端文件中有說明或範例)。
執行此操作的另一種方式是覆寫所有 WebDriver 用戶端已存取的命令:執行腳本。請務必閱讀建立驅動程式指南中的 新增命令 章節,以了解此操作的一般運作方式。它與外掛程式配合運作的方式僅略有不同。讓我們來看一個取自 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);
}
我們在此展示了三個重要的元件,讓此系統運作,所有元件都定義在 plugin 類別中
executeMethodMap
,定義方式與驅動程式相同executeMethodMap
中定義的命令方法實作(在本例中為plugMeIn
)execute
命令的覆寫/處理。就像任何外掛程式命令處理常式一樣,前兩個參數為next
和driver
,後接腳本名稱和參數。BasePlugin
實作一個輔助方法,我們可以用所有這些參數呼叫它。
覆寫驅動程式的執行方法運作方式符合您的預期:如果您的外掛程式定義一個與驅動程式同名的執行方法,您的命令(在本例中為 plugMeIn
)將會優先被呼叫。如果您想的話,您可以透過 next
執行驅動程式的原始行為。
建立 Appium Doctor 檢查¶
您的使用者可以執行 appium plugin doctor <pluginName>
來執行安裝和健全檢查。請參閱 建立 Doctor 檢查 指南,以取得關於此功能的更多資訊。
更新 Appium 伺服器物件¶
您通常可能不需要更新 Appium 伺服器物件(它是一個 Express 伺服器,已經在 各種方式 中進行了設定)。但是,例如,您可以將新的 Express 中介軟體新增到伺服器,以支援您的外掛程式的需求。若要更新伺服器,您必須在類別中實作 static async updateServer
方法。此方法採用三個參數
expressApp
:Express 應用程式物件httpServer
:Node HTTP 伺服器物件cliArgs
:用於啟動 Appium 伺服器的 CLI 參數的對應
您可以在 updateServer
方法中對它們執行任何您想做的事。您可能想要參考這些物件在 BaseDriver 程式碼中建立和運作的方式,這樣您就知道您不會取消或覆寫任何標準且重要的內容。但如果您堅持的話,您可以這麼做,但結果您需要測試!警告:這應視為進階功能,且需要具備 Express 的知識,以及小心不要執行任何可能影響 Appium 伺服器其他部分運作的事項!
處理意外的會話關閉¶
在開發外掛程式時,您可能會想要在會話結束時加入一些清理邏輯。您會自然而然地透過加入 deleteSession
處理常式來執行此動作。這在大部分情況下都能運作,除非會話並未乾淨地結束。Appium 有時會判定會話已意外結束,在這些情況下,Appium 會在您的外掛程式類別中尋找名為 onUnexpectedShutdown
的方法,並呼叫該方法(傳遞目前的會話驅動程式作為第一個參數,以及表示關閉原因的錯誤物件作為第二個參數),讓您有機會採取任何必要的步驟來清理會話。例如,請記住該函式並未 await
,您可以實作類似下列內容