├── .gitignore ├── .mocharc.js ├── test ├── empty.js ├── context.js ├── config.js ├── elements.js ├── actions.js └── game.js ├── package.json ├── FAQ.md └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | -------------------------------------------------------------------------------- /.mocharc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | timeout: 120000, 3 | slow: 60000, 4 | } 5 | -------------------------------------------------------------------------------- /test/empty.js: -------------------------------------------------------------------------------- 1 | const { remote } = require('webdriverio') 2 | const WDIO_PARAMS = require('./config') 3 | 4 | describe('Unity Game', function() { 5 | /** @type import('webdriverio').Browser<'async'> **/ 6 | let driver 7 | 8 | before(async function() { 9 | driver = await remote(WDIO_PARAMS) 10 | }) 11 | 12 | it('should open and do nothing', function() { 13 | }) 14 | 15 | after(async function() { 16 | if (driver) { 17 | await driver.deleteSession() 18 | } 19 | }) 20 | }) 21 | -------------------------------------------------------------------------------- /test/context.js: -------------------------------------------------------------------------------- 1 | const { remote } = require('webdriverio') 2 | const WDIO_PARAMS = require('./config') 3 | 4 | describe('Unity Game', function() { 5 | /** @type import('webdriverio').Browser<'async'> **/ 6 | let driver 7 | 8 | before(async function() { 9 | driver = await remote(WDIO_PARAMS) 10 | }) 11 | 12 | it('should switch to the unity context', async function() { 13 | await driver.switchContext('UNITY') 14 | console.log(await driver.getPageSource()) 15 | }) 16 | 17 | after(async function() { 18 | if (driver) { 19 | await driver.deleteSession() 20 | } 21 | }) 22 | }) 23 | -------------------------------------------------------------------------------- /test/config.js: -------------------------------------------------------------------------------- 1 | if (!process.env['UNITY_APP']) { 2 | throw new Error(`You must set the UNITY_APP env var with path to the demo game`) 3 | } 4 | 5 | const capabilities = { 6 | platformName: 'Android', 7 | 'appium:deviceName': 'Android', 8 | 'appium:automationName': 'UiAutomator2', 9 | 'appium:app': process.env['UNITY_APP'], 10 | 'appium:altUnityHost': 'localhost', 11 | 'appium:altUnityPort': 13000, 12 | } 13 | 14 | const WDIO_PARAMS = { 15 | hostname: 'localhost', 16 | port: 4723, 17 | path: '/', 18 | connectionRetryCount: 0, 19 | logLevel: 'silent', 20 | capabilities, 21 | } 22 | 23 | module.exports = WDIO_PARAMS 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "unity-plugin-workshop", 3 | "version": "1.0.0", 4 | "description": "Supporting code for a workshop on the Appium AltUnity Plugin", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/jlipps/unity-plugin-workshop.git" 12 | }, 13 | "author": "Jonathan Lipps", 14 | "license": "ISC", 15 | "bugs": { 16 | "url": "https://github.com/jlipps/unity-plugin-workshop/issues" 17 | }, 18 | "homepage": "https://github.com/jlipps/unity-plugin-workshop#readme", 19 | "dependencies": { 20 | "mocha": "^9.2.1", 21 | "webdriverio": "^7.16.16" 22 | }, 23 | "devDependencies": { 24 | "earljs": "^0.2.1" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /test/elements.js: -------------------------------------------------------------------------------- 1 | const { remote } = require('webdriverio') 2 | const { expect } = require('earljs') 3 | const WDIO_PARAMS = require('./config') 4 | 5 | describe('Unity Game', function() { 6 | /** @type import('webdriverio').Browser<'async'> **/ 7 | let driver 8 | 9 | before(async function() { 10 | driver = await remote(WDIO_PARAMS) 11 | }) 12 | 13 | it('should find elements', async function() { 14 | await driver.switchContext('UNITY') 15 | /** @type import('webdriverio').Element<'async> **/ 16 | const player = await driver.$('#SuperPlayer') 17 | /** @type import('webdriverio').Element<'async> **/ 18 | const enemy10 = await driver.$('//Enemy[10]') 19 | expect(await player.isDisplayed()).toBeTruthy() 20 | expect(await enemy10.isDisplayed()).toBeFalsy() 21 | }) 22 | 23 | after(async function() { 24 | if (driver) { 25 | await driver.deleteSession() 26 | } 27 | }) 28 | }) 29 | -------------------------------------------------------------------------------- /test/actions.js: -------------------------------------------------------------------------------- 1 | const { remote } = require('webdriverio') 2 | const { expect } = require('earljs') 3 | const WDIO_PARAMS = require('./config') 4 | 5 | describe('Unity Game', function() { 6 | /** @type import('webdriverio').Browser<'async'> **/ 7 | let driver 8 | 9 | before(async function() { 10 | driver = await remote(WDIO_PARAMS) 11 | }) 12 | 13 | it('should navigate to the settings menu', async function() { 14 | await driver.switchContext('UNITY') 15 | 16 | const pressEsc = { 17 | type: 'key', 18 | id: 'keyboard', 19 | actions: [ 20 | {type: 'keyDown', value: 'Escape'}, 21 | {type: 'pause', duration: 750}, 22 | {type: 'keyUp', value: 'Escape'}, 23 | ] 24 | } 25 | 26 | await driver.performActions([pressEsc]) 27 | 28 | /** @type import('webdriverio').Element<'async>[] **/ 29 | const menuButtons = await driver.$$('//Button/Text') 30 | for (const button of menuButtons) { 31 | if (await button.getText() === 'Settings') { 32 | await button.click() 33 | break 34 | } 35 | } 36 | 37 | /** @type import('webdriverio').Element<'async> **/ 38 | const settingsHeader = await driver.$('//Settings/Header') 39 | expect(await settingsHeader.getText()).toEqual('Settings') 40 | await driver.performActions([pressEsc]) 41 | }) 42 | 43 | after(async function() { 44 | if (driver) { 45 | await driver.deleteSession() 46 | } 47 | }) 48 | }) 49 | -------------------------------------------------------------------------------- /test/game.js: -------------------------------------------------------------------------------- 1 | const { remote } = require('webdriverio') 2 | const { expect } = require('earljs') 3 | const WDIO_PARAMS = require('./config') 4 | 5 | describe('Unity Game', function() { 6 | /** @type import('webdriverio').Browser<'async'> **/ 7 | let driver 8 | 9 | before(async function() { 10 | driver = await remote(WDIO_PARAMS) 11 | }) 12 | 13 | it('should stomp on enemies', async function() { 14 | await driver.switchContext('UNITY') 15 | let firstEnemy = await driver.$('//Enemy[starts-with(@worldX, "7.7")]') 16 | const initWorldY = parseFloat(await firstEnemy.getAttribute('worldY')) 17 | 18 | const runWithJump = { 19 | type: 'key', 20 | id:'keyboard', 21 | actions: [ 22 | {type: 'keyDown', value: 'RightArrow'}, 23 | {type: 'pause', duration: 1600}, 24 | {type: 'keyDown', value: 'Space'}, 25 | {type: 'pause', duration: 500}, 26 | {type: 'keyUp', value: 'Space'}, 27 | {type: 'pause', duration: 500}, 28 | {type: 'keyUp', value: 'RightArrow'}, 29 | {type: 'pause', duration: 1000}, 30 | ] 31 | } 32 | await driver.performActions([runWithJump]) 33 | 34 | firstEnemy = await driver.$('//Enemy[starts-with(@worldX, "7.7")]') 35 | const finalWorldY = parseFloat(await firstEnemy.getAttribute('worldY')) 36 | expect(finalWorldY).toBeLessThan(initWorldY) 37 | }) 38 | 39 | after(async function() { 40 | if (driver) { 41 | await driver.deleteSession() 42 | } 43 | }) 44 | }) 45 | -------------------------------------------------------------------------------- /FAQ.md: -------------------------------------------------------------------------------- 1 | # Workshop FAQ 2 | 3 | Q: Do I need to already know how to use Appium before this workshop?
4 | A: Yes. This workshop assumes you are already familiar with mobile testing using Appium. Specifically, we will be using Android in this workshop. See the prerequisites document for more info. 5 | 6 | Q: Does the Unity plugin work for more than Android? Is the knowledge learned in this workshop transferable?
7 | A: Yes. The skills learned in the workshop will apply to any Unity platform: iOS, Android, macOS, Windows, Linux, etc... 8 | 9 | Q: Which programming language will the examples be developed in?
10 | A: JavaScript, specifically WebDriverIO. You are free to follow along in the programming language of your choice. The only code we will be writing is WebDriver/Appium code, so if you are more familiar with another Appium client, you can simply translate on the fly into whatever makes sense for you. 11 | 12 | Q: Which game(s) will we test?
13 | A: We'll use one of Unity's templates, for a 2d platformer. We won't spend much time modifying the game itself as our goal is to focus on how to use the Appium plugin, not how to develop games. 14 | 15 | Q: Will the test code we write in the workshop be available afterward?
16 | A: Yes, everything is already available in the workshop repo 17 | 18 | Q: It sounds like we are using AltUnity Tester. Do we need the paid version?
19 | A: No. I developed the Appium plugin to work with the free version. Presumably it also works with the paid version since the server component is the same! 20 | 21 | Q: I already know how to use AltUnity Tester on its own; why should I learn about the Appium plugin?
22 | A: The model of using an AltUnity client on its own is fine, but you're mixing two separate APIs. The Appium plugin allows you to stay completely within the Appium world. The Appium plugin also allows you to connect to AltUnity from any language with an Appium client (not just C# or Python). 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Unity Plugin Workshop 2 | 3 | This is the workbook for a workshop led by Jonathan Lipps on how to use the [Appium AltUnity Plugin](https://github.com/projectxyzio/appium-altunity-lugin). Make sure you have taken care of the prerequisites before following this workbook. It's also a good idea to have the plugin README and docs open for reference. 4 | 5 | * [Prerequisites](#prerequisites) 6 | * [Unity](#unity) 7 | * [Appium](#appium) 8 | * [Version List](#version-list) 9 | * [Test Framework (Optional)](#test-framework-(optional)) 10 | * [Conceptual intro](#conceptual-intro) 11 | * [Building our game](#building-our-game) 12 | * [Project initialization](#project-initialization) 13 | * [Add Android support](#add-android-support) 14 | * [Setting up AltUnity tester](#setting-up-altunity-tester) 15 | * [Add the AltUnity Tester to the project](#add-the-altunity-tester-to-the-project) 16 | * [Build the app with the AltUnity server](#build-the-app-with-the-altunity-server) 17 | * [Add some IDs](#add-some-ids) 18 | * [Configuring the Appium plugin](#configuring-the-appium-plugin) 19 | * [Designing and writing tests](#designing-and-writing-tests) 20 | * [The Unity context](#the-unity-context) 21 | * [Working with elements](#working-with-elements) 22 | * [Keystrokes, clicking, and text](#keystrokes,-clicking,-and-text) 23 | * [Putting it all together](#putting-it-all-together) 24 | * [Conclusion and next steps](#conclusion-and-next-steps) 25 | 26 | ## Prerequisites 27 | 28 | *(To be completed before the workshop)* 29 | 30 | ### Unity 31 | 32 | 1. You have downloaded Unity and Unity Hub for your platform 33 | 2. You have created a Unity account and are logged in with Unity Hub 34 | 3. You have a license for the Unity editor; a "free personal license" is fine 35 | 36 | ### Appium 37 | 38 | 1. You have a working Appium 2.0 install (`npm install -g appium@next`) 39 | 2. You can run Android sessions on an emulator or real device with the UiAutomator2 driver (`appium driver install uiautomator2`) 40 | 3. You are familiar with using `adb` from the command line and have access to the Android SDK 41 | 4. You should have Node.js 14+ 42 | 43 | ### Version List 44 | 45 | Here are the specific versions of the various tools used while developing this workshop. Newer or 46 | older versions may also work fine, but these versions are verified to work correctly. 47 | 48 | | Tool | Version | 49 | |---------------------------------------|---------------| 50 | | macOS | 12.2.1 | 51 | | Unity Hub | 3.0.1 | 52 | | Unity IDE | 2020.3.30f1 | 53 | | Unity-bundled OpenJDK | 1.8.0 | 54 | | Unity-bundled Android SDK Build tools | 30.0.2 | 55 | | Unity-bundled Android Platform | 30 rev 3 | 56 | | Unity asset: AltUnity Tester | 1.7.0 | 57 | | Unity asset: JSON .NET for Unity | 2.0.1 | 58 | | Appium | 2.0.0-beta.25 | 59 | | Appium UiAutomator2 driver | 2.0.3 | 60 | | Appium AltUnity Plugin | 1.3.0 | 61 | 62 | 63 | ### Test Framework (Optional) 64 | 65 | We'll be writing scripts from scratch in WebDriverIO. If you want, you can use another test language or framework, you'll just need to do the setup yourself (and be comfortable transposing WebDriver commands into your preferred API). So if you're not going to follow along, make sure you come along with an empty Java or Python automation project, including the Appium client library, that you can start creating Appium sessions with. 66 | 67 | ## Conceptual intro 68 | 69 | *(10 min)* This portion of the workshop is covered with slides and explains the ideas and architecture underlying our work here. 70 | 71 | ## Building our game 72 | 73 | *(20 min)* The goal in this section is to get our demo game building and running on an Android device. 74 | 75 | ### Project initialization 76 | 77 | 78 | * Open the Unity Hub 79 | * Create a new project using the "2d platformer microgame" template. Name the project "AppiumWorkshop" 80 | * Skip the tutorial and head straight to the scene ("Load Scene") 81 | * Press the "Play" button to make sure the game runs. You can play it for a bit. 82 | 83 | ### Add Android support 84 | 85 | * Make sure you have an Android device or emulator connected 86 | * In Unity, go to Build Settings (File > Build Settings) 87 | * Click on Android in the left hand side 88 | * In "Run Device" option, choose your connected device 89 | * Check "Development Build" 90 | * Click "Switch Platform" to make Android our platform 91 | * Click "Build and Run" to make sure it works 92 | * You can put the game anywhere, name it "game.apk" or similar 93 | 94 | If you get "JDK not found" error: 95 | * Go to Preferences > External Tools 96 | * Find the correct JDK or check to use the one installed with Unity 97 | * If necessary, install via Unity Hub 98 | * Go to "Installs" 99 | * Find your Unity install and click the gear icon 100 | * Click "Add Modules" 101 | * Choose "OpenJDK" under "Android Build Support" and install it 102 | * You may do all of the above with the Android SDK tools as well 103 | 104 | If you get a message about ARM64 vs ARMv7, or x86: 105 | * Click "Player Settings" 106 | * Scroll down inside "Other Settings" to "Scripting Backend" and change from "Mono" to "IL2CPP" 107 | * Change "Target Architectures" to ARM64 or x86 or whatever you need 108 | 109 | * Install the app: `adb install -r /path/to/your/game.apk` 110 | * Make sure it works! 111 | * Can switch to Landscape mode 112 | * Currently, only keyboard controls are supported, no tap-to-move. Might need a keyboard plugged in if you're on a real device. 113 | 114 | ## Setting up AltUnity tester 115 | 116 | *(10 min)* The goal in this section is to instrument the app with the AltUnity Tester server which is necessary for the Appium plugin to communicate with internal game objects. 117 | 118 | ### Add the AltUnity Tester to the project 119 | 120 | * Go to Asset Store (Window > Asset Store), this will open up the Unity asset website 121 | * Ensure you are logged in with your Unity account 122 | * Search for "AltUnity Tester" 123 | * Add it and open it in Unity, this will open the Package Manager. Now select it. 124 | * Make sure you have updated to the latest version (click "Update"). Should be 1.7.0 for this workshop, but later versions might work just as well. 125 | * Go back to the store and search for "JSON .NET for Unity" by parentElement 126 | * Add it as well, and ensure it's also in the Package manager 127 | * With the JSON package selected, click "Import" 128 | * If warned about Package Manager dependencies, click "Install/Upgrade" 129 | * Confirm "Import" when shown the list of files 130 | * With the AltUnity Tester package selected, click "Import" 131 | * If warned about Package Manager dependencies, click "Install/Upgrade" 132 | * Confirm "Import" when shown the list of files 133 | * Confirm installation by checking that a new "AltUnity Tools" menu item is shown 134 | * Open up the "AltUnity Tester Editor" 135 | 136 | ### Build the app with the AltUnity server 137 | 138 | With the AltUnity Tester Editor open: 139 | 140 | * Change "Platform" to "Android" 141 | * Select a build location for the instrumented version of the game APK (can be anywhere) 142 | * Click "Build Only" 143 | * If you get an error, and it looks to do with "sdkmanager", check the permissions on your Unity app folder in `/Applications`. May need to `chown -R $USER:admin /Applications/Unity/Hub/Editor` to fix 144 | * It should create `AppiumWorkshop.apk` in your folder 145 | * Install it to the device using `adb` 146 | * You should see the same game but now with an AltUnity Tester window showing waiting for connections 147 | * Take note of the port it is running on, probably the default of `13000`. This can be adjusted. 148 | 149 | The app is now properly instrumented with AltUnity Tester and ready for testing with the Appium plugin 150 | 151 | ### Add some IDs 152 | 153 | * AltUnity Tools > AltId > Add AltId to Every Object in Active Scene 154 | * Now find the Player object in the Hierarchy, and give it a custom player name as the AltId 155 | * Save and rebuild 156 | 157 | ## Configuring the Appium plugin 158 | 159 | *(10 min)* The goal of this section is to ensure we can have an empty Appium script that successfully connects to the AltUnity process within the game. 160 | 161 | Install the Appium AltUnity plugin: 162 | * `appium plugin install --source=npm appium-altunity-plugin` 163 | * Run the Appium server with the plugin enabled: `appium --use-plugins=altunity` 164 | * Keep this server running 165 | 166 | Ensure the AltUnity service is accessible on your system: 167 | * Use `adb` to forward the appropriate port to a port on your system (can be the same or different) 168 | * `adb forward tcp:13000 tcp:13000` 169 | 170 | Set up the empty script: 171 | * Clone this repo somewhere: `git clone git@github.com:jlipps/unity-plugin-workshop.git` 172 | * Head into that newly directory with a terminal session 173 | * Run `npm install` inside it to get our client dependencies (we'll use WebdriverIO; you could run this workshop in parallel with another language if you prefer) 174 | * Set the absolute path to your app as the UNITY_APP environment variable 175 | * `export UNITY_APP=/path/to/AppiumWorkshop.apk` 176 | * Check out `test/empty.js` and make necessary updates 177 | * update the `appium:altUnityHost` and `appium:altUnityPort` if appropriate 178 | 179 | Run the empty script: 180 | * `npx mocha ./test/empty.js` 181 | * Test should pass 182 | * Should see something like `[AltUnityPlugin] Connection to AltUnity server established` in the Appium logs. This is how we know we were able to successfully connect with the AltUnity server. 183 | 184 | Troubleshooting: 185 | * Did you forward your port correctly to your device? 186 | * Did you use the app with the AltUnity server instrumented into it? 187 | * Did you opt in to using the plugin when you started Appium? 188 | 189 | ## Designing and writing tests 190 | 191 | *(35 min)* The goal of this section is to explore what is possible with the Appium plugin and to write some tests of our game using the special plugin features 192 | 193 | ### The Unity context 194 | 195 | The first thing we need to learn is how to address the Unity game rather than the default mobile automation behaviour. So let's build a new test file: 196 | 197 | * Copy `empty.js` and rename it to `context.js` 198 | * Rename the test definition to something like `should switch to the unity context` 199 | * Make the test method an `async` function 200 | * Fill out the new test method: 201 | ```js 202 | await driver.switchContext('UNITY') 203 | console.log(await driver.getPageSource()) 204 | ``` 205 | * Run the test 206 | `npx mocha ./test/context.js` 207 | 208 | Notice that the page source looks very different from a normal Android source output. This source is generated by the Appium plugin from the Unity object hierarchy. You can use it to explore the game as well as generate element finding queries. 209 | 210 | ### Working with elements 211 | 212 | Let's explore how to find and interact with elements. 213 | 214 | * Copy `context.js` and rename it to `elements.js` 215 | * Rename the test definition to something like `should find elements` 216 | * Add a new import up top (since we are going to make assertions) 217 | * `const { expect } = require('earljs')` 218 | * Replace the test body with 219 | ```js 220 | await driver.switchContext('UNITY') 221 | const player = await driver.$('//Player') 222 | const enemy10 = await driver.$('//Enemy[10]') 223 | expect(await player.isDisplayed()).toBeTruthy() 224 | expect(await enemy10.isDisplayed()).toBeFalsy() 225 | ``` 226 | 227 | What we are doing here is finding game objects via XPath, then asserting whether they are visible. The Appium plugin compares their position in the game to the screen size to determine whether they are displayed. 228 | 229 | Various locator strategies are available. 230 | * `xpath`: finds the element via xpath query on the provided source 231 | * `id`: finds the element via something called the AltId, which can be set on the element using the AltUnity Tester Editor in Unity 232 | * `css selector`: this only works for IDs, it's a convenience method since some clients translate id to css selector now 233 | * `link text`: this finds an element by its text 234 | * `tag name`: this finds an element by its type, e.g., `Enemy` 235 | 236 | Let's add an ID to an element: 237 | * AltUnity Tools > AltId > Add AltId to Every Object in Active Scene 238 | * Find the "Player" object in the hierarchy 239 | * Change the auto-generated AltId to something unique that you want, e.g., "SuperPlayer" 240 | * Save and rebuild the instrumented app from the AltUnity Tester Editor window 241 | 242 | And update our test script: 243 | * Change how we find our player object: 244 | ```js 245 | const player = await driver.$('#SuperPlayer') 246 | ``` 247 | * This translates to an AltId element query. Could also use find by id (this is a css selector version). 248 | * Rerun the test 249 | 250 | Various element commands are available: 251 | * click 252 | * get location 253 | * get attribute (can get anything you can see in the XML) 254 | * get text 255 | * is displayed 256 | * and more (see the [plugin reference](https://github.com/projectxyzio/appium-altunity-plugin/tree/master/docs)) 257 | 258 | ### Keystrokes, clicking, and text 259 | 260 | Games often involve key or button presses. To automate this, use the Actions API. For example we can press the 'ESCAPE' key to bring up the in game menu. Let's do that and also see how we can get the text of menu items at the same time. 261 | 262 | * Copy the `elements.js` file and rename it to `actions.js` 263 | * Rename the test to `should navigate to the settings menu` 264 | * Replace the test body with: 265 | ```js 266 | await driver.switchContext('UNITY') 267 | 268 | const pressEsc = { 269 | type: 'key', 270 | id: 'keyboard', 271 | actions: [ 272 | {type: 'keyDown', value: 'Escape'}, 273 | {type: 'pause', duration: 750}, 274 | {type: 'keyUp', value: 'Escape'}, 275 | ] 276 | } 277 | 278 | await driver.performActions([pressEsc]) 279 | 280 | const menuButtons = await driver.$$('//Button/Text') 281 | for (const button of menuButtons) { 282 | if (await button.getText() === 'Settings') { 283 | await button.click() 284 | break 285 | } 286 | } 287 | 288 | const settingsHeader = await driver.$('//Settings/Header') 289 | expect(await settingsHeader.getText()).toEqual('Settings') 290 | await driver.performActions([pressEsc]) 291 | ``` 292 | * Notice how we can't see the Text of the buttons directly so we have to get all the Text items underneath the Button items and check their text, in order to click on the one we want. So this is how we perform key presses, get text, and click elements. 293 | * To figure out which key strings are available, check the plugin source code for the [AltKeyCode](https://github.com/projectxyzio/appium-altunity-plugin/blob/master/src/client/key-code.ts) enum. 294 | 295 | ### Putting it all together 296 | 297 | Let's test some actual game behaviour! We want to cause the player to run and jump on an enemy, and make an assertion about the state of the enemy. In our game, the enemy is considered "stomped" when its worldY value has gone negative. Enemies don't "die", they just fall off the level. 298 | 299 | * Copy the `actions.js` file and rename it `game.js` 300 | * Update the test description to `should stomp on enemies` 301 | * Replace the test body with: 302 | ```js 303 | await driver.switchContext('UNITY') 304 | let firstEnemy = await driver.$('//Enemy[starts-with(@worldX, "7.7")]') 305 | const initWorldY = parseFloat(await firstEnemy.getAttribute('worldY')) 306 | 307 | const runWithJump = { 308 | type: 'key', 309 | id:'keyboard', 310 | actions: [ 311 | {type: 'keyDown', value: 'RightArrow'}, 312 | {type: 'pause', duration: 1600}, 313 | {type: 'keyDown', value: 'Space'}, 314 | {type: 'pause', duration: 500}, 315 | {type: 'keyUp', value: 'Space'}, 316 | {type: 'pause', duration: 500}, 317 | {type: 'keyUp', value: 'RightArrow'}, 318 | {type: 'pause', duration: 1000}, 319 | ] 320 | } 321 | await driver.performActions([runWithJump]) 322 | 323 | firstEnemy = await driver.$('//Enemy[starts-with(@worldX, "7.7")]') 324 | const finalWorldY = parseFloat(await firstEnemy.getAttribute('worldY')) 325 | expect(finalWorldY).toBeLessThan(initWorldY) 326 | ``` 327 | 328 | Basically the way this test works is we create an action to hold down the right arrow, then while we're holding it, we press space, then release both. If our timing is correct, we will stomp on the first enemy, which we assert by checking its `worldY` attribute. 329 | 330 | You can do a whole lot from here! Try to automate more of the game! 331 | 332 | ## Conclusion and next steps 333 | 334 | *(5 min)* This portion of the workshop is covered with slides. 335 | --------------------------------------------------------------------------------