As described in the Appium 2.0 Epic, Appium needs to evolve from being a server which bundles many drivers to one which by default doesn't bundle any drivers, and instead provides an interface for retrieving and using drivers. Essentially, "Appium" will become:
- A set of spec extensions to the WebDriver W3C protocol (eventually upstreaming those extensions to the official protocol)
- A set of code libraries (like
BaseDriver
orjsonwp-proxy
that make it easy to write Appium drivers) - A driver runner (this is what the main Appium package currently does); the difference is that drivers will not come bundled with the runner by default
- A plugin interface based around command-level hooks
- An API and CLI interface for retrieving and managing different versions of Appium-compatible drivers and plugins
It's the last of these that needs more discussion, since it is the only piece that doesn't currently exist. This document attempts to outline a proposal for that.
Appium 2.0 drivers should extend BaseDriver
, as all the current ones do. In addition, in their package.json
, they should have:
- a (required)
automationName
field which declares the automation name by which this driver will be activated. - an (optional)
minAppiumVersion
field which declares the minimum version of Appium required to support a particular version of this driver. - an (optional)
maxAppiumVersion
field which developers can set if they determine that a driver is EOL and will not support future Appium versions
A lighter weight way to add to the Appium ecosystem is via plugins. Plugins are not full-blown drivers, and can exist to modulate only one Appium command, or one aspect of Appium's execution, or the Appium server itself.
Plugins must be Node.js packages (from NPM, GitHub, or local, just as for drivers) whose package.json
file includes the following fields:
- (required)
pluginName
(this will be referred to by the user) - (required)
mainClass
(the name of the plugin class which is exported from the library) - (optional)
minAppiumVersion
(as above) - (optional)
maxAppiumVersion
(as above)
Plugins can be installed using the appium plugin
CLI, which exactly mirrors the appium driver
CLI (see below).
Plugins can be activated using a CLI flag --plugins
: for example appium --plugins=plugin1,plugin2
. Plugins must be activated by the Appium administrator in this way because there is no restriction on their behavior and thus they constitute a security hole by design. Administrators should never enable plugins they don't fully trust.
Plugins must have already been installed in order to be activated. Plugins will not be downloaded silently or in the background when requested--they must be proactively installed by the server administrator.
All plugins must extend BasePlugin
(NPM @appium/base-plugin
). Plugin authors usually don't need to override the constructor. If plugins override constructor()
, be aware the signature is:
constructor (pluginName)
The pluginName
is used, for example, in defining the logger
object. And so super(pluginName)
should be called! Plugin objects have access to a logger constructed by Appium on the plugin's behalf, as this.logger
.
Plugins can modify the Appium server itself, meaning they can run immediately before the Appium server starts listening. Plugins which wish to modify the server must:
- Set the
updatesServer
boolean flag on the plugin object to true - Implement the
updateServer
method:
async function updateServer (expressApp, httpServer)
Objects sent in to this method:
expressApp
: the express app used by Appium to do routing and command handlinghttpServer
: the Node http.Server instance which hosts the express app
Plugins can also modify the behavior of specific within-session commands, by doing the following:
- Setting the
commands
instance field to eithertrue
or an array. If it is set totrue
, then this plugin will be used to handle all Appium commands. If set to an array of strings, it will be used to handle only those commands whose names are listed in the array (names must match the names as given in Appium's protocol routes file). - Implementing the
handle
method:
async function handle (next, driver, cmdName, ...args)
A description of the values passed into this method:
next
- an async function. The idea behind thenext
method passed in is that callingawait next()
will execute the command as it would normally have been executed by Appium (or as wrapped by another plugin), returning the normal result. It does not need to be called at all.driver
- the Appium driver object handling the sessioncmdName
- the name of the command...args
- the list of arguments being applied to the command function
The return value of handle
is what will be wrapped up and returned to the client. For example, using this structure it is possible to wrap every command in external logic:
async function handle (next) {
logger.info("We're doing something custom here");
const result = await next();
logger.info("We're doing something custom here too");
return result;
}
It's important to remember that it is the responsibility of handle
to return the result. There is no way for a command plugin to get "outside" the Appium server's request and response methods.
The handle
command hook will be called for any active plugin (in the order specified in the capability), so plugin authors should be aware that their logic might be executed before or after the logic of other plugins. And they should also be aware that if they fail to call next
(which might be the intentional and desired behavior), then other plugins might not be able to operate, depending on the order in which they are included.
By default Appium comes with a huge mapping of routes to command names and command parameters. Plugins can extend this mapping by defining their own mapping, which would enable them to then handle custom commands that don't exist in generic Appium. This is achieved by setting the newMethodMap
field on the plugin object to an object of the appropriate type (see Appium's METHOD_MAP in protocol/routes for examples). One example is as follows:
newMethodMap = {
'/session/:sessionId/fake_data': {
GET: {command: 'getFakeSessionData'},
POST: {command: 'setFakeSessionData', payloadParams: {required: ['data']}}
},
};
In the above example, the /fake_data
endpoint is added to the session handler, and both GET and POST commands are associated with it.
A basic example of a plugin which uses the various features is available at appium/appium-fake-plugin
The appium
executable will continue to operate as it currently does, with all the appropriate server flags. (TODO
: figure out whether some server flags are driver-dependent and should therefore be relegated to capabilities or responsibility for parsing them passed on to drivers). However, by default no drivers will be installed, so using it to run a test will result in a driver unknown error. Drivers must first be installed using the CLI tool.
A new scope for the CLI executable will be added: driver
, which can be followed by a number of verbs:
This is the default verb. So running appium driver
will get you the same output. This produces a list of installed and available drivers and versions, with appropriate annotations:
> appium driver list
xcuitest
- 1.0.0
- 1.0.2
- 1.1.0 [installed]
uiautomator2
- 1.0.0
- 1.5.0
chromedriver
- 2.5.0 [installed]
These drivers are so-called "official" versions that are tracked in some Appium registry. These names are unique and maintained by the Appium maintainers (even if the drivers themselves are not). Presumably the data for this command and all others comes from NPM, even if it is subsequently cached locally.
Some more options for running this command:
> appium driver list --installed
[email protected]
[email protected]
If a user has unregistered drivers installed, note those too:
> appium driver list
...
myowndriver
- 1.0.1
jlipps/customdriver
To install a driver, refer to it by name and possibly version:
> appium driver install xcuitest
Found driver 'xcuitest', latest version 1.1.0
Installing [email protected] via NPM...
Installation successful. xcuitest now available via automationName 'XCUITest'
> appium driver install [email protected]
Found driver 'xcuitest' at version 1.0.2 (latest version is 1.1.0)
Installing [email protected] via NPM...
Installation successful. xcuitest now available via automationName 'XCUITest'
Install unregistered driver via npm:
> appium driver install --npm myowndriver
Found package 'myowndriver' on NPM, latest version 3.0.1
Installing [email protected] via NPM...
Installation successful. myowndriver now available via automationName 'MyOwnDriver'
(Maybe) install unregistered driver via GitHub:
> appium driver install --github jlipps/customdriver
Found 'jlipps/customdriver' on GitHub
Cloned 'jlipps/customdriver' into local registry
Running 'npm install' in local checkout
Installation successful. jlipps/customdriver now available via automationName 'CustomDriver'
Some error cases:
-
Driver doesn't exist:
> appium driver install foobar Could not find driver 'foobar' in the registry
(similarly for no NPM or no GitHub package, or no version match)
-
Driver already installed
> appium driver install xcuitest Driver 'xcuitest' is already installed. Either specify version or run 'appium driver update xcuitest' if you want to update to latest.
-
Problem installing via NPM/GitHub
> appium driver install [email protected] Found driver 'xcuitest' at version 1.0.2 (latest version is 1.1.0) Installing [email protected] via NPM... Installation failed. Could not install via NPM. Error: <message>
-
automationName is used by another driver
> appium driver install [email protected] Found driver 'foodriver' at version 1.0.0 (latest version is 1.1.0) Installing [email protected] via NPM... Installation failed. 'foodriver' uses automationName 'Foo' but this automationName is already in use by 'bardriver'.
-
Appium version is lower than driver's minAppiumVersion (if set)
-
Appium version is greater than driver's maxAppiumVersion (if set)
Uninstall a driver. Pretty straightforward.
> appium driver uninstall xcuitest
Uninstalling driver 'xcuitest'...
Uninstall successful
Some error cases:
-
Driver not installed:
> appium driver uninstall foobar Driver 'foobar' is not installed, doing nothing
Update drivers to latest versions
> appium driver update --show
The following updates are available:
xcuitest [1.0.3 => 1.1.0]
chromedriver [2.5.0 => 2.5.1]
Updating a specific driver:
> appium driver update xcuitest
Updating driver 'xcuitest'.
You have version 1.0.3; version 1.1.0 is available.
Installing [email protected] via NPM...
Update successful
Updating all drivers:
> appium driver update --all
Updating driver 'xcuitest'.
You have version 1.0.3; version 1.1.0 is available.
Installing [email protected] via NPM...
Update successful
Updating driver 'chromedriver'.
You have version 2.5.0; version 2.5.1 is available.
Installing [email protected] via NPM...
Update successful
(Some drivers might have errors, in which case show those errors as necessary)
Warnings for breaking semver:
> appium driver update xcuitest
Updating driver 'xcuitest'.
You have version 1.0.3; version 2.0.0 is available.
Not updating since new version might contain breaking changes.
If you want to do the upgrade anyway, run with '--force'
> echo $?
1
> appium driver update --force xcuitest
Updating driver 'xcuitest'.
You have version 1.0.3; version 2.0.0 is available.
Updating to new major version despite warnings (--force used)
Installing [email protected] via NPM...
Update successful
All of the above commands also work with plugin
in place of driver
!
Attaching --json
to the end of any command should return the output in a JSON format easily parseable by a machine or other program.
Any command which is completely successful exits with 0. Any command which had one or more errors exits with 1.
Everything you can do with the CLI should also be able to be done by importing JS methods from the appium
package. This way all the functionality can be reimplemented, say in a graphical fashion in Appium Desktop.
As much as possible, I think we should use NPM for everything.
When someone requests a driver install, we can use the NPM API (or shell out to the npm
command) to simply npm install driver@version
with Appium's directory as the current working directory. This will install the driver into Appium's node_modules
dir so Appium can access it via a regular import
.
How do we keep track of which drivers were installed? Using directory searches or NPM for that might be a bit too wonky. We could have a file in the user's home dir (or other system-appropriate resource dir) called .appium-drivers.json
which retains information about which drivers have been installed:
> cat ~/.appium-drivers.json # (or ~/Library/Application Support/appium/drivers.json?)
{
"xcuitest": {
"version": "1.1.0",
"automationName": "XCUITest",
"packageName": "appium-xcuitest-driver"
},
"jlipps/customdriver": {
"version": "2.1.2",
"automationName": "CustomDriver",
"packageName": "customdriver"
},
....
}
This file would be continually updated by Appium. On startup, Appium would be responsible for verifying its accuracy, i.e., ensuring that it can import packages named xcuitest
and customdriver
and that their versions match. If this check fails, the server shouldn't start, or it should emit a warning or similar.
Of course we should think long and hard about the format of this file, because upgrading it with a new version of Appium will be a pain.
There should be some blessed list of drivers we have deemed to be ready for community consumption. These are "registered" drivers that correctly implement the Appium driver interface. How do we maintain this "registry"? The simplest option is to hard-code it in Appium itself (which means we'd need an Appium update if we wanted to include a new driver). Or, it could reside somewhere on the web for Appium to check when requested. Note that driver versions should be discoverable via NPM so we don't need to maintain those. This might be as easy as a simple map:
> cat lib/driver-registry.js
export default const REGISTRY = {
xcuitest: "appium-xcuitest-driver",
uiautomator2: "appium-uiautomator2-driver",
...
};
If we assume that all registered drivers will be NPM packages, it should be as simple as this.
Not much changes on the Appium side. On startup, it should import only the drivers that are currently installed. When a new session request comes in, it should check against the automationNames it knows about via the installed drivers. If one matches, it can instantiate a new instance of the driver class as we currently do. If there is no match, we throw an error. We could maintain a list of automationNames in the driver registry so that we could suggest a helpful solution for someone, like "Didn't have a driver to handle automationName 'XCUITest'. You need the 'xcuitest' driver. Run 'appium driver install xcuitest' and try again."
- StarDriver Enterprise: App to the Future (video of talk introducing the idea of Appium 2.0)
A couple other commands I would like to see in this are:
e.g.)
appium driver install --file /Users/danielgraham/appium-xcuitest-driver
npm has the capability to install packages by filepath (
npm install file://Users/danielgraham/appium-xcuitest-driver
) and I think this would be useful when doing e2e tests for drivers. The driver can install Appium 2 as a dev dependency and then basically install itself.e.g.)
appium bundle
This would take appium and the installed bundle and put it into one zipped directory so that we have Mac bundles (MacDriver, XCUITest, Android), Windows bundles (Android, Windows) and Linux bundles (Android) that can be deployed to cloud providers (SauceLabs, AWS Device Farm, etc..).
Another possible use case is that we could include these platform-specific bundles with releases so that folks can install Appium without having to go through the the individual driver installation process.