Skip to content

Instantly share code, notes, and snippets.

@jlipps
Last active March 1, 2021 08:07
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jlipps/651b62316603400cabc95ff0f9faf70f to your computer and use it in GitHub Desktop.
Save jlipps/651b62316603400cabc95ff0f9faf70f to your computer and use it in GitHub Desktop.
Appium 2.0 Proposal

Appium 2.0

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 or jsonwp-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 Drivers

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

Appium Plugins

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)

Installing Plugins

Plugins can be installed using the appium plugin CLI, which exactly mirrors the appium driver CLI (see below).

Activating Plugins

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.

Plugin Behavior

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.

Server Updates

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:

  1. Set the updatesServer boolean flag on the plugin object to true
  2. 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 handling
  • httpServer: the Node http.Server instance which hosts the express app

Command Handling

Plugins can also modify the behavior of specific within-session commands, by doing the following:

  1. Setting the commands instance field to either true or an array. If it is set to true, 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).
  2. Implementing the handle method:
async function handle (next, driver, cmdName, ...args)

A description of the values passed into this method:

  1. next - an async function. The idea behind the next method passed in is that calling await 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.
  2. driver - the Appium driver object handling the session
  3. cmdName - the name of the command
  4. ...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.

Additional Methods

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.

Plugin Example

A basic example of a plugin which uses the various features is available at appium/appium-fake-plugin

The CLI

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:

appium driver list

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
xcuitest@1.1.0
chromedriver@2.5.0

If a user has unregistered drivers installed, note those too:

> appium driver list
...
myowndriver
- 1.0.1
jlipps/customdriver

appium driver install

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 xcuitest@1.1.0 via NPM...
Installation successful. xcuitest now available via automationName 'XCUITest'
> appium driver install xcuitest@1.0.2
Found driver 'xcuitest' at version 1.0.2 (latest version is 1.1.0)
Installing xcuitest@1.0.2 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 myowndriver@3.0.1 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 xcuitest@1.0.2
    Found driver 'xcuitest' at version 1.0.2 (latest version is 1.1.0)
    Installing xcuitest@1.0.2 via NPM...
    Installation failed. Could not install via NPM. Error: <message>
    
  • automationName is used by another driver

    > appium driver install foodriver@1.0.0
    Found driver 'foodriver' at version 1.0.0 (latest version is 1.1.0)
    Installing foodriver@1.0.0 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)

appium driver uninstall

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
    

appium driver update

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 xcuitest@1.1.0 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 xcuitest@1.1.0 via NPM...
Update successful
Updating driver 'chromedriver'. 
You have version 2.5.0; version 2.5.1 is available.
Installing chromedriver@2.5.1 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 xcuitest@2.0.0 via NPM...
Update successful

Plugin CLI

All of the above commands also work with plugin in place of driver!

json output

Attaching --json to the end of any command should return the output in a JSON format easily parseable by a machine or other program.

exit codes

Any command which is completely successful exits with 0. Any command which had one or more errors exits with 1.

API

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.

Architecture

As much as possible, I think we should use NPM for everything.

Installation

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.

Discovery

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.

Running tests

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."

References

@dpgraham
Copy link

A couple other commands I would like to see in this are:

  1. Installing by file

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.

  1. Bundling

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.

@jlipps
Copy link
Author

jlipps commented May 15, 2018

Like those thoughts @dpgraham. Another idea: npm install flags to auto-install drivers:

npm install -g appium --drivers=xcuitest,uiautomator2@2.1.0

@Jonahss
Copy link

Jonahss commented Jun 15, 2019

pluginName could be optional. If missing, Appium could just use the package name. This is one less extra requirement for making a plugin.

Rather than having two types of plugins, how about just one. Where if used as a server arg, it needs to implement onServer. This way, a server plugin can also override commands for all sessions, and the users don't need to specify (or even know about) the added functionality.

@KazuCocoa
Copy link

nice 👍 > CLI commands

btw, the automationName should be case insensitive. (Current some clients and servers handle it as case insensitive. It would be good to keep current behaviour to reduce possible bugs in clients etc.)

@debadarshy
Copy link

For a Browser Hub & Node, GRID let user define custom servlets and then plug in the same to either hub or node, this helps the user to do customization at Hub/node side.
The same doesn't available when it comes to Appium, as of now appium doesn't have the ability to let the user add servlets at the node level.Is it possible to have this feature enabled w.r.t appium 2.0 with this plugin approach?

@Higgens75
Copy link

Like those thoughts @dpgraham. Another idea: npm install flags to auto-install drivers:

npm install -g appium --drivers=xcuitest,uiautomator2@2.1.0

also like to see this 👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment