跳至内容

构建驱动程序

Appium 希望让任何人都能轻松地开发自己的自动化驱动程序,作为 Appium 生态系统的一部分。本指南将解释其中涉及的内容以及如何使用 Appium 提供的工具完成各种驱动程序开发任务。本指南假设您 (1) 是 Appium 的熟练用户,(2) 是熟练的 Node.js 开发人员,以及 (3) 您已阅读并理解 驱动程序简介

如果符合您的情况,太好了!本指南将帮助您入门。

在您创建驱动程序之前

在开始实施驱动程序之前,务必先解决一些问题。例如,您需要知道您的驱动程序将执行什么操作。它试图为哪个平台公开 WebDriver 自动化?

Appium 不会神奇地赋予您自动化任何平台的能力。它所做的只是为您提供一套方便的工具来实现 WebDriver 协议。因此,如果您想创建例如针对新应用程序平台的驱动程序,则需要了解如何在没有 Appium 的情况下自动化该平台上的应用程序。

这通常意味着您需要非常熟悉特定平台的应用程序开发。而且通常意味着您将依赖平台供应商提供的工具或 SDK。

基本上,如果您无法回答问题“如何在该平台上启动、远程触发行为并读取应用程序状态?”,那么您还没有准备好编写 Appium 驱动程序。确保您进行研究,以便确信有前进的道路。一旦有了道路,将它编码并将其作为 Appium 驱动程序提供应该很容易!

其他可供参考的驱动程序

构建 Appium 驱动程序最棒的一点是,已经存在许多开源 Appium 驱动程序,您可以参考它们。有一个 fake-driver 示例驱动程序,它基本上什么也不做,只是展示了本指南中描述的一些内容。

当然,所有 Appium 的官方驱动程序都是开源的,可以在项目 GitHub 组织的存储库中找到。因此,如果您发现自己问“驱动程序如何执行 X?”,请阅读这些驱动程序的代码!如果您遇到困难,也不要害怕向 Appium 开发人员提问;我们始终乐于帮助确保驱动程序开发体验良好!

Appium 驱动程序的基本要求

如果您希望驱动程序成为有效的 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": {
    "driverName": "fake",
    "automationName": "Fake",
    "platformNames": [
      "Fake"
    ],
    "mainClass": "FakeDriver"
  },
  ...
}
```

必需的子字段是

  • driverName:这应该是您的驱动程序的简短名称。
  • automationName:这应该是用户将用于其 appium:automationName 功能的字符串,以告诉 Appium 使用您的驱动程序。
  • platformNames:这是一个或多个平台名称的数组,这些平台名称被认为对您的驱动程序有效。当用户发送 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 中如何工作的一种方法是先在本地安装它

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

开发您的驱动程序

如何开发您的驱动程序由您决定。但是,从 Appium 内部运行它很方便,这样就不必进行大量发布和安装。最直接的方法是将最新版本的 Appium 作为 devDependency 包含在内,然后还包含您自己的驱动程序,如下所示

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

现在,您可以在本地运行 Appium(npm exec appiumnpx appium),并且由于您的驱动程序与它一起列为依赖项,因此它将自动“安装”并可用。您可以通过这种方式设计您的 e2e 测试,或者如果您使用 Node.js 编写它们,则可以简单地导入 Appium 的启动服务器方法来处理在 Node 中启动和停止 Appium 服务器(待办事项:在准备好时,参考开源驱动程序之一中的此实现)。

在现有 Appium 服务器安装中进行本地开发的另一种方法是简单地在本地安装您的驱动程序

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

在开发过程中刷新您的驱动程序

当 Appium 服务器启动时,它会将您的驱动程序加载到内存中。对驱动程序代码的更改将不会生效,直到下次 Appium 服务器启动。仅仅启动一个新会话不足以导致驱动程序的代码重新加载。

但是,您可以将 APPIUM_RELOAD_EXTENSIONS 环境变量设置为 1,以请求 Appium 在每次请求新会话时清除其模块缓存并重新加载扩展。这可能可以避免在对驱动程序进行代码更改时重新启动服务器。

标准驱动程序实现思路

这些是在创建驱动程序时您可能会想要做的事情。

在构造函数中设置状态

如果您定义了自己的构造函数,则需要调用 super 以确保所有标准状态都正确设置。

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

此处的 args 参数是包含用于启动 Appium 服务器的所有 CLI 参数的对象。

定义和验证接受的 capabilities

您可以定义自己的 capabilities 以及对其进行基本验证。用户始终可以发送您未定义的 capabilities,但如果他们发送了您明确定义的 capabilities,则 Appium 会验证它们是否为正确类型(并检查是否存在必需的 capabilities)。

如果您想完全关闭 capability 验证,请在构造函数中将 this.shouldValidateCaps 设置为 false

要向 Appium 提供您的验证约束,请在构造函数中将 this.desiredCapConstraints 设置为验证对象。验证对象可能比较复杂。以下来自 UiAutomator2 驱动的示例

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

启动会话并读取 capabilities

Appium 的 BaseDriver 已经实现了 createSession 命令,因此您无需这样做。但是,通常需要执行自己的启动操作(启动应用程序、运行一些平台代码或根据为驱动程序定义的 capabilities 执行不同的操作)。因此,您最终可能会覆盖 createSession。您可以在驱动程序中定义该方法来实现这一点

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

出于遗留原因,您的函数将接收旧式 JSON Wire Protocol 作为前两个参数的 desired 和 required caps。鉴于旧协议不再受支持,并且所有客户端都已更新,您现在可以只依赖 w3cCaps 参数。(有关 otherDriverData 的讨论,请参阅下面关于并发驱动程序的部分)。

您需要确保调用 super.createSession 以获取会话 ID 以及处理后的 capabilities(请注意,capabilities 也设置在 this.caps 上;在此处本地修改 caps 不会产生任何影响,除了更改用户在创建会话响应中看到的内容)。

就是这样!您可以在中间部分填写驱动程序所需的任何启动逻辑。

结束会话

如果您的驱动程序需要任何清理或关闭逻辑,最好将其作为覆盖 deleteSession 实现的一部分来完成。

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

如果可能,不要在此处抛出任何错误,以便会话清理的所有部分都能成功!

访问 capabilities 和 CLI 参数

您通常需要读取用户为会话设置的参数,无论是作为 CLI 参数还是作为 capabilities。最简单的方法是访问 this.opts,它是来自 CLI 或 capabilities 的所有选项的合并。例如,要访问 appium:app capability,您可以简单地获取 this.opts.app 的值。

如果您关心知道某件事是作为 CLI 参数还是 capability 发送的,您可以显式访问 this.cliArgsthis.caps 对象。

在所有情况下,appium: capability 前缀在您在此处访问值时都会被剥离,以方便起见。

实现 WebDriver 经典命令

您可以通过在驱动程序类中实现函数来处理 WebDriver 命令。WebDriver 协议的每个成员,加上各种 Appium 扩展,都有一个相应的函数,如果您想在驱动程序中支持该命令,则需要实现该函数。查看 Appium 的 routes.js 是了解 Appium 支持哪些命令以及您需要为每个命令实现哪些方法的最佳方法。此文件中的每个路由对象都告诉您命令名称以及您期望为该命令接收的参数。

以这个块为例

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

在这里,我们看到路由 /session/:sessionId/url 映射到两个命令,一个用于 GET 请求,另一个用于 POST 请求。如果我们想允许我们的驱动程序更改“url”(或它对我们的驱动程序意味着什么),那么我们可以实现 setUrl 命令,知道它将接受 url 参数

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

一些注意事项: - 所有命令方法都应该是 async 函数,或者返回 Promise - 您无需担心协议编码/解码。您将获得 JS 对象作为参数,并且可以返回 JSON 可序列化对象作为响应。Appium 将负责将其包装在 WebDriver 协议响应格式中,将其转换为 JSON 等... - 所有基于会话的命令都将 sessionId 参数作为最后一个参数接收 - 所有基于元素的命令都将 elementId 参数作为倒数第二个参数接收 - 如果您的驱动程序没有实现命令,用户仍然可以尝试访问该命令,并且会收到 501 Not Yet Implemented 响应错误。

实现 WebDriver BiDi 命令

WebDriver BiDi 是 WebDriver 规范的较新版本,它是在 Websockets 而不是 HTTP 上实现的。作为 Appium 驱动程序作者,您可以利用 Appium 的 BiDi 支持,而无需了解任何有关 BiDi 协议或 Websockets 的知识。实现 BiDi 命令的处理程序与实现 WebDriver 经典命令的处理程序(在上一节中描述)的工作方式相同。您只需在驱动程序上定义一个具有适当名称的方法,它将在客户端请求 BiDi 命令时被调用。要查看应使用哪些特定名称来表示 BiDi 命令,请查看 bidi-commands.js

目前,您还需要在驱动程序实例上定义一个 doesSupportBidi 字段,并确保将其设置为 true。除非您的驱动程序以这种方式表示它支持 BiDi,否则 Appium 不会为您的驱动程序打开其 Websocket 服务器并设置任何处理程序。

实现元素查找

元素查找是特殊的命令实现情况。您实际上并不想覆盖 findElementfindElements,即使它们是在 routes.js 中列出的。如果您实现此函数,Appium 会为您做很多工作

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

以下是传入的内容

  • strategy - 字符串,正在使用的定位器策略
  • selector - 字符串,选择器
  • mult - 布尔值,用户是否请求了一个元素或与选择器匹配的所有元素
  • context - (可选)如果定义,将是 W3C 元素(即,一个 JS 对象,其 W3C 元素标识符作为键,元素 ID 作为值)

您需要返回以下内容之一

  • 单个 W3C 元素(如上所述的对象)
  • W3C 元素数组

请注意,您可以从 appium/support 中导入该 W3C 网页元素标识符

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

您对元素的处理方式由您决定!通常,您最终会保留一个 ID 到实际元素“对象”的缓存映射,或者您的平台的等效项。

定义有效的定位器策略

您的驱动程序可能只支持标准 WebDriver 定位器策略的子集,或者它可能添加自己的自定义定位器策略。要告诉 Appium 哪些策略被认为对您的驱动程序有效,请创建一个策略数组并将其分配给 this.locatorStrategies

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

如果用户尝试使用任何非允许策略,Appium 将抛出错误,这使您可以保持元素查找代码的整洁,并且只处理您知道的策略。

默认情况下,有效策略列表为空,因此如果您的驱动程序不是简单地代理到另一个 WebDriver 端点,则需要定义一些策略。协议标准定位器策略定义 here

抛出 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 服务器时从命令行接收数据(例如,服务器管理员应设置的端口,这些端口不应作为 capabilities 传入),则可以添加自定义 CLI 参数。

要为 Appium 服务器定义 CLI 参数(或配置属性),您的扩展必须提供一个模式。在扩展的 package.json 中的 appium 属性中,添加一个 schema 属性。这将是 a) 模式本身,或 b) 模式文件的路径。

这些模式的规则

  • 模式必须符合 JSON Schema Draft-07
  • 如果 schema 属性是模式文件的路径,则该文件必须采用 JSON 或 JS(CommonJS)格式。
  • 不支持自定义 $id 值。要使用 $ref,请提供相对于模式根的相对值,例如 /properties/foo
  • 已知 format 关键字的值可能受支持,但各种其他关键字可能不受支持。如果您发现需要使用的不受支持的关键字,请 请求支持 或发送 PR!
  • 模式必须是 object 类型({"type": "object"}),在 properties 关键字中包含参数。不支持嵌套属性。

示例

{
  "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 参数或配置文件设置。如果此扩展是一个驱动程序,并且其名称为“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"
      }
    }
  }
}

添加驱动程序脚本

有时您可能希望驱动程序的用户能够在会话上下文之外运行脚本(例如,运行预构建驱动程序方面的脚本)。为了支持这一点,您可以将脚本名称和 JS 文件的映射添加到 Appium 扩展元数据中的 scripts 字段中。假设您在项目中创建了一个脚本,该脚本位于项目中的 scripts 目录中,名为 driver-prebuild.js。然后,您可以添加一个 scripts 字段,如下所示

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

现在,假设您的驱动程序名为 mydriver,驱动程序的用户可以运行 appium driver run mydriver prebuild,您的脚本将执行。

将命令代理到另一个 WebDriver 实现

Appium 驱动程序的一种非常常见的架构设计是,使用某种平台特定的 WebDriver 实现来与 Appium 驱动程序交互。例如,Appium UiAutomator2 驱动程序与运行在 Android 设备上的特殊(基于 Java)服务器交互。在 webview 模式下,它还与 Chromedriver 交互。

如果您发现自己处于这种情况,告诉 Appium 您的驱动程序只是将 WebDriver 命令直接代理到另一个端点非常容易。

首先,通过实现 canProxy 方法让 Appium 知道您的驱动程序可以代理。

canProxy() {
    return true;
}

接下来,告诉 Appium 它不应该尝试代理哪些 WebDriver 路由(通常最终会有一些您不想转发的路由)。

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

代理避免列表应是一个数组数组,其中每个内部数组的第一个成员是 HTTP 方法,第二个成员是正则表达式。如果正则表达式与路由匹配,则该路由将不会被代理,而是由您的驱动程序处理。在本例中,我们避免代理所有具有 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);

这里我们创建了一个代理对象,并将它的某些方法分配给 this,名称分别为 proxyReqResproxyCommand。这是 Appium 使用代理所必需的,所以不要忘记这一步!JWProxy 还有许多其他选项,您可以在源代码中查看。(待办事项:将选项发布为 API 文档并在此处链接)。

最后,我们需要一种方法来告诉 Appium 代理何时处于活动状态。对于您的驱动程序,它可能始终处于活动状态,或者它可能仅在特定上下文中处于活动状态。您可以将逻辑定义为 proxyActive 的实现。

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

有了这些部分,您就不必重新实现您正在代理到的远程端点已经实现的任何内容。Appium 会为您处理所有代理。

将 BiDi 命令代理到另一个 BiDi 实现

上面关于代理 WebDriver 命令的所有内容在概念上也适用于专门代理 BiDi 命令。为了启用 BiDi 代理,您需要

  1. 将驱动程序实例上的 doesSupportBidi 字段设置为 true
  2. 在您的驱动程序上实现 get bidiProxyUrl。这应该返回一个 Websocket URL,即您希望将 BiDi 命令代理到的上游套接字的地址。

这里预期的模式是,您在上游实现上启动一个会话,检查它是否在返回的 capabilities 中具有活动的 BiDi 套接字(例如,webSocketUrl capability),然后将内部字段设置为该值,以便它可以由 get bidiProxyUrl 返回。一旦所有这些都到位,Appium 将从客户端将 BiDi 命令直接代理到上游连接。

使用新命令扩展现有协议

您可能会发现现有的命令不足以满足您的驱动程序。如果您想公开不映射到任何现有命令的行为,您可以通过以下两种方式之一创建新命令

  1. 扩展 WebDriver 协议并创建客户端插件来访问扩展
  2. 通过定义 执行方法 来重载执行脚本命令

如果您想遵循第一条路径,您可以指示 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 客户端都可以访问的命令:执行脚本。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。该实现可以调用单个辅助函数 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. 通过调用执行脚本发送的参数数组必须包含零个或一个元素。列表中的第一个项目被认为是您方法的参数对象。这些参数将被解析、验证,然后按照 executeMethodMap 中指定的顺序应用于您的重载方法(在 required 参数列表中指定的顺序,然后是 optional 参数列表)。即,此框架假设通过执行脚本发送的只有一个实际参数(并且此参数应该是一个对象,其键/值表示您的执行方法期望的参数)。1. Appium 不会自动为您实现 execute(执行脚本处理程序)。例如,您可能希望仅在您不处于代理模式时调用 executeMethod 辅助函数!1. 如果脚本名称与 executeMethodMap 中定义的命令之一不匹配,或者缺少参数,则 executeMethod 辅助函数将拒绝并返回错误。

构建 Appium Doctor 检查

您的用户可以运行 appium driver doctor <driverName> 来运行安装和运行状况检查。访问 构建 Doctor 检查 指南以获取有关此功能的更多信息。

实现对 Appium 设置的处理

Appium 用户可以通过 CLI 参数和 capabilities 将参数发送到您的驱动程序。但这些参数在测试过程中无法更改,有时用户希望在测试过程中调整参数。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 的事件,以及一个类似于 {load: 0.97} 的有效负载,表示 97% 的 CPU 使用率。每当我们想要时,我们的驱动程序只需调用以下代码(假设我们在 this.currentCpuLoad 中有当前负载)

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

现在,如果客户端已订阅 system.cpu 事件,它将在驱动程序发出该事件时收到负载通知。

使其了解其他并发驱动程序正在使用的资源

假设您的驱动程序使用了一些系统资源,例如端口。有几种方法可以确保多个同时会话不使用相同的资源

  1. 让您的用户通过 capabilities 指定资源 ID(appium:driverPort 等)
  2. 始终使用空闲资源(为每个会话找到一个新的随机端口)
  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应该是一个接收错误对象的函数(表示意外关机的原因)。