Gaining access to Command-line from Maestro
What’s this?
The command line is quite a powerful tool. It can be used to automate a bunch of things here and there. Obviously, it’s not only about Shell scripts, you can run any other tool from it as well. But using it directly is not always possible.
Recently, I showed how to gain access to the command line from XCTest. This time it’s Maestro’s turn, as it also works inside a sandbox (more precisely, a JavaScript sandbox). Let’s bypass this limitation and learn how to gain access to the command line from Maestro 🎹
So, basically, the same dish is on the menu today, just with a slightly different serving 👨🍳
And this is useful because…?
- This uncovers a whole new world of simulator/emulator-related features. For example:
- This opens up the possibility to create and use a standalone mock server that can completely replace the real backend for the testing purposes
- This allows executing any macOS UI actions through Applescript from your Maestro tests
- This gives the option to automate web-related actions via playwright or selenium instead of opening Safari/Chrome on iOS/Android
- This offers a pretty flexible way to export anything you want from the test to the host machine (screenshots, mocks, test data, etc.)
- And much much more 🤠
Cool, what’s the trick?
Even though Maestro does support a minimal subset of vanilla JavaScript APIs, it does not allow to access the filesystem. But we can still use the HTTP protocol to communicate with the host machine.
So, let’s create an HTTP server that will be listening for requests. Then we can send a request from Maestro to the server and run whatever we want. The server can even return the output back to Maestro if required.
Okay, show me how to do this
Proof of concept
GIVEN we have a server
running on localhost:4567
AND we sent a request to /terminal
endpoint from Maestro
with the desired command
WHEN server
receives the request
AND server
executes the command
THEN server
returns the result back to Maestro
AND Maestro
verifies the result
Walkthrough
-
Install Maestro
brew install facebook/fb/idb-companion curl -Ls "https://get.maestro.mobile.dev" | bash
-
Choose any web framework of your choice (they are pretty much all the same for our use case)
{ "js": "express", // https://github.com/expressjs/express "rb": "sinatra", // https://github.com/sinatra/sinatra "py": "flask", // https://github.com/pallets/flask "go": "gin", // https://github.com/gin-gonic/gin "..": "..." // ... }
-
Create an HTTP server (I picked up
{ "js": "express" }
as an example)-
Command-line:
npm install express touch server.js open server.js
-
Source code:
// server.js const process = require("child_process"); const express = require("express"); const app = express(); const port = 4567; app.use(express.json()); app.listen(port); app.post("/terminal", (req, res) => { const output = exec(req.body.command, req.query.async).toString("utf8").trim(); res.send(output); }); function exec(command, async) { if (async === "true") { return process.exec(command); } else { return process.execSync(command); } }
-
-
Run the server
node server.js
-
Create a tiny layer between the server and the test
// terminal.js var response = http.post(`http://localhost:4567/terminal?async=${async}`, { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ command: exec }) }) output.terminal = response.body;
-
Create the test file
# test.yaml appId: ${app_id} --- - launchApp - runScript: file: terminal.js env: exec: "sw_vers -productName" async: false - assertTrue: ${output.terminal == "macOS"}
- Launch the emulator/simulator
-
Run the test ✅
-
on iOS:
udid="7E0929F4-CFB5-41E7-9943-85CA9370EE51" # This is a unique iOS Simulator id app_id="com.apple.Preferences" # This is a Settings app, just as an example maestro --udid ${udid} test --env app_id=${app_id} test.yaml
-
on Android:
udid="emulator-5554" # This is a default id for the first running Android Emulator app_id="com.android.settings" # This is a Settings app, just as an example maestro --udid ${udid} test --env app_id=${app_id} test.yaml
-
- Adjust the scripts to your needs and enjoy hacking!
Does this work on real devices?
Yup, but not on iOS, at least at the moment.
Alrighty, does this work on CI?
Sure thing, works like a charm!
Just make sure to run the server in the background on CI:
node server.js &
Thanks a mill!
No probs! Check out this GitHub repo for more details 🙂