Building Appium Plugins
This is a high-level guide for developing Appium plugins, which is not something most Appium users need to know or care about. If you are not familiar with Appium plugins yet from a user perspective, check out the list of plugins to play around with some and get an idea of the sorts of things that plugins can do. Plugins are a powerful system for augmenting Appium's functionality or changing the way Appium works. They can be distributed to other Appium users and can extend Appium's ecosystem in all kinds of interesting ways! (There is also a significant amount of overlap here with developing Appium drivers, so you may also want to check out the building drivers guide for further inspiration.)
Before you create your plugin
Before creating your plugin, it's good to have a general idea of what you want your plugin to accomplish and whether it will be possible to implement it given the restrictions of the Appium platform. Reading through this guide will help you understand what's possible. In general, Appium's plugin system is extremely powerful and no attempts have been made to artificially limit what's possible with them (which is a main reason that all plugins are opt-in by the system administrator responsible for starting the Appium server---plugins are powerful and should only be used when explicitly trusted!).
Other plugins to reference
There are a wide variety of open source Appium plugins available for perusal. It's definitely recommended to explore the code for some other plugins before embarking on writing your own. The Appium team maintains a set of official plugins in the Appium GitHub repo. Links to other open source plugins can be found in the Plugin list
Basic requirements for plugins
These are the things your plugin must do (or be), if you want it to be a valid Appium plugin.
Node.js package with Appium extension metadata
All Appium plugins are fundamentally Node.js packages, and therefore must have a valid
package.json
. Your driver is not limited to Node.js, but it must provide an adapter written in
Node.js so it can be loaded by Appium.
Your package.json
must include appium
as a peerDependency
. The requirements for the
dependency versions should be as loose as possible (unless you happen to know your plugin will only
work with certain versions of Appium). For Appium 2.0, for example, this would look something like
^2.0.0
, declaring that your plugin works with any version of Appium that starts with 2.x.
Your package.json
must contain an appium
field, like this (we call this the 'Appium extension
metadata'):
```json
{
...,
"appium": {
"pluginName": "fake",
"mainClass": "FakePlugin"
},
...
}
```
The required subfields are:
pluginName
: this should be a short name for your plugin.mainClass
: this is a named export (in CommonJS style) from yourmain
field. It must be a class which extends Appium'sBasePlugin
(see below).
Extend Appium's BasePlugin
class
Ultimately, your plugin is much easier to write because most of the hard work of defining patterns
for overriding commands is done for you. This is
all encoded up as a class which Appium exports for you to use, called BasePlugin
. It is exported
from appium/plugin
, so you can use one of these styles to import it and create your own class
that extends it:
import {BasePlugin} from 'appium/plugin';
// or: const {BasePlugin} = require('appium/plugin');
export class MyPlugin extends BasePlugin {
// class methods here
}
Note
In all the code samples below, whenever we reference an example method, it is assumed that it is defined within the class, though this is not explicitly written, for the sake of clarity and space.
Make your plugin available
That's basically it! With a Node.js package exporting a plugin class and with correct Appium extension metadata, you've got yourself an Appium plugin! Now it doesn't do anything, but you can load it up in Appium, activate it, etc...
To make it available to users, you could publish it via NPM. When you do so, your plugin will be installable via the Appium CLI:
It's a good idea to test your plugin first, of course. One way to see how it works within Appium is to install it locally first:
And of course, plugins must be "activated" during Appium server start, so make sure you direct your users to do so:
Developing your plugin
How you develop your plugin is up to you. It is convenient, however, to run it from within Appium
without having to do lots of publishing and installing. The most straightforward way to do this is
to include the most recent version of Appium as a devDependency
(although its being already
included as a peerDependency
is sufficient in newer versions of NPM), and then also your own plugin,
like this:
Now, you can run Appium locally (npm exec appium
or npx appium
), and because your plugin is
listed as a dependency alongside it, it will be automatically "installed" and available. You can
design your e2e tests this way, or if you're writing them in Node.js, you can simply import
Appium's start server methods to handle starting and stopping the Appium server in Node.
Of course, you can always install it locally as described above as well.
Anytime you make changes to your plugin code, you'll need to restart the Appium server to make sure
it picks up the latest code. As with drivers, you can set the APPIUM_RELOAD_EXTENSIONS
environment variable if you wish Appium to try to re-require your plugin module when a new session
starts.
Standard plugin implementation ideas
These are things you will probably find yourself wanting to do when creating a plugin.
Set up state in a constructor
If you define your own constructor, you'll need to call super
to make sure all the standard state
is set up correctly:
The args
parameter here is the object containing all the CLI args used to start the Appium
server.
Intercept and handle specific Appium commands
This is the most normal behavior for Appium plugins -- to modify or replace the execution of one or
more commands that would normally be handled by the active driver. To override the default command
handling, you need to implement async
methods in your class with the same name as the Appium
commands to be handled (just exactly how drivers themselves are
implemented). Curious what command names there
are? They are defined in the Appium base driver's
routes.js file,
and of course you can add more as defined in the next section.
Each command method is sent the following arguments:
next
: This is a reference to anasync
function which encapsulates the chain of behaviors which would take place if this plugin were not handling the command. You can choose to call the next behavior in the chain at any point in your logic (by making sure to includeawait next()
somewhere), or not. If you don't, it means the default behavior (or any plugins registered after this one) won't be run.driver
: This is the object representing the driver handling the current session. You have access to it for any work you need to do, for example calling other driver methods, checking capabilities or settings, etc......args
: A spread array with any arguments that have been applied to the command by the user.
For example, if we wanted to override the setUrl
command to simply add some extra logging on top,
we would implement as follows:
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;
}
Intercept and handle all Appium commands
You might find yourself in a position where you want to handle all commands, in order to inspect
payloads to determine whether or not to act in some way. If so, you can implement async handle
,
and any command that is not handled by one of your named methods will be handled by this method
instead. It takes the following parameters (with all the same semantics as above):
next
driver
cmdName
- string representing the command being run...args
For example, let's say we want to log timing for all Appium commands as part of a plugin. We could
do this by implementing handle
in our plugin class as follows:
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;
}
Work around driver proxies
There is a bit of a gotcha with handling Appium commands. Appium drivers have the ability to turn
on a special 'proxy' mode, wherein the Appium server process takes a look at incoming URLs, and
decides whether to forward them on to some upstream WebDriver server. It could happen that
a command which a plugin wants to handle is designated as a command which is being proxied to an
upstream server. In this case, we run into a problem, because the plugin never gets a chance to
handle that command! For this reason, plugins can implement a special member function called
shouldAvoidProxy
, which takes the following parameters:
method
- string denoting HTTP method (GET
,POST
, etc...)route
- string denoting the requested resource, for example/session/8b3d9aa8-a0ca-47b9-9ab7-446e818ec4fc/source
body
- optional value of any type representing the WebDriver request body
These parameters define an incoming request. If you want to handle a command in your plugin which
would normally be proxied directly through a driver, you could disable or 'avoid' proxying the
request, and instead have the request fall into the typical Appium command execution flow (and
thereby your own command function). To avoid proxying a request, just return true
from
shouldAvoidProxy
. Some examples of how this method is used are in the Universal XML
plugin (where
we want to avoid proxying the getPageSource
command, or in the Images
plugin (where
we want to conditionally avoid proxying any command if it looks like it contains an image element).
Throw WebDriver-specific errors
The WebDriver spec defines a set of error
codes to accompany command responses if an
error occurred. Appium has created error classes for each of these codes, so you can throw the
appropriate error from inside a command, and it will do the right thing in terms of the protocol
response to the user. To get access to these error classes, import them from appium/driver
:
Log messages to the Appium log
You can always use console.log
, of course, but Appium provides a nice logger for you as
this.logger
(it has .info
, .debug
, .log
, .warn
, .error
methods on it for differing log
levels). If you want to create an Appium logger outside of a plugin context (say in a script or
helper file), you can always construct your own too:
Further possibilities for Appium plugins
These are things your plugin can do to take advantage of extra plugin features or do its job more conveniently.
Add a schema for custom command line arguments
You can add custom CLI args if you want your plugin to receive data from the command line when the Appium server is started (for example, ports that a server administrator should set that should not be passed in as capabilities).
This works largely the same for plugins as it does for drivers, so for more details have a look at the equivalent section in the building drivers doc.
The only difference is that to construct the CLI argument name, you prefix it with
--plugin-<name>
. So for example, if you have a plugin named pluggo
and a CLI arg defined with
the name electro-port
, you can set it when starting Appium via --plugin-pluggo-electro-port
.
Setting args via a configuration file is also supported, as it is for drivers, but under the
plugin
field instead. For example:
Add plugin scripts
Sometimes you might want users of your plugin to be able to run scripts outside the context of
a session (for example, to run a script that pre-builds aspects of your plugin). To support this,
you can add a map of script names and JS files to the scripts
field within your Appium extension
metadata. So let's say you've created a script in your project that lives in a scripts
directory
in your project, named plugin-prebuild.js
. Then you could add a scripts
field like this:
Now, assuming your plugin is named myplugin
, users of your plugin can run appium plugin run
myplugin prebuild
, and your script will execute.
Add new Appium commands
If you want to offer functionality that doesn't map to any of the existing commands supported by drivers, you can create new commands in one of two ways, just as is possible for drivers:
- Extending the WebDriver protocol and creating client-side plugins to access the extensions
- Overloading the Execute Script command by defining Execute Methods
If you want to follow the first path, you can direct Appium to recognize new methods and add them
to its set of allowed HTTP routes and command names. You do this by assigning the newMethodMap
static variable in your driver class to an object of the same form as Appium's routes.js
object.
For example, here is part of the newMethodMap
for the FakePlugin
example driver:
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},
},
};
Note
If you're using TypeScript, static member objects like these should be defined as const
.
In this example we're adding a few new routes and a total of 3 new commands. For more examples of
how to define commands in this way, it's best to have a look through routes.js
. Now all you need
to do is implement the command handlers in the same way you'd implement any other Appium command.
Note also the special neverProxy
key for commands; this is generally a good idea to set to true
for plugins, since your plugin might be active for a driver that is put into proxy mode but hasn't
bothered to decline proxying for these (new and therefore unknown) commands. Setting neverProxy
to true
here will cause Appium to never proxy these routes and therefore ensure your plugin
handles them, even if a driver is in proxy mode.
The downside of adding new commands via newMethodMap
is that people using the standard Appium
clients won't have nice client-side functions designed to target these endpoints. So you would need
to create and release client-side plugins for each language you want to support (directions or
examples can be found at the relevant client docs).
An alternative to this way of doing things is to overload a command which all WebDriver clients
have access to already: Execute Script. Make sure to read the section on adding new
commands in
the Building Drivers guide to understand the way this works in general. The way it works with
plugins is only slightly different. Let's look at an example taken from Appium's 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);
}
We have three important components shown here which make this system work, all of which are defined inside the plugin class:
- The
executeMethodMap
, defined in just the same way as for drivers - The implementation of the command method as defined in
executeMethodMap
(in this case,plugMeIn
) - The overriding/handling of the
execute
command. Just like any plugin command handlers, the first two arguments arenext
anddriver
, followed by the script name and args.BasePlugin
implements a helper method which we can simply call with all of these arguments.
Overriding Execute Methods from drivers works as you'd expect: if your plugin defines an Execute
Method with the same name as that of a driver, your command (in this case plugMeIn
) will be
called first. You can choose to run the driver's original behaviour via next
if you want.
Update the Appium server object
You probably don't normally need to update the Appium server object (which is an
Express server having already been
configured
in a variety of ways). But, for example, you could add new Express middleware to the server to
support your plugin's requirements. To update the server you must implement the static async
updateServer
method in your class. This method takes three parameters:
expressApp
: the Express app objecthttpServer
: the Node HTTP server objectcliArgs
: a map of the CLI args used to start the Appium server
You can do whatever you want with them inside the updateServer
method. You might want to
reference how these objects are created and worked with in the BaseDriver code, so that you know
you're not undoing or overriding anything standard and important. But if you insist, you can, with
results you'll need to test! Warning: this should be considered an advanced feature and requires
knowledge of Express, as well as the care not to do anything that could affect the operation of
other parts of the Appium server!
Handle unexpected session shutdown
When developing a plugin you may want to add some cleanup logic for when a session ends. You would
naturally do this by adding a handler for deleteSession
. This works in most cases, except when
the session does not finish cleanly. Appium sometimes determines that a session has finished
unexpectedly, and in these situations, Appium will look for a method called onUnexpectedShutdown
in your plugin class, which will be called (passing the current session driver as the first
parameter, and the error object representing the cause of the shutdown as the second), giving you
an opportunity to take any steps that might be necessary to clean up from the session. For example,
keeping in mind that the function is not await
ed you could implement something like this: