Skip to content

Commit

Permalink
feat: add mobile:simctl to run listed simctl subcommands (#2461)
Browse files Browse the repository at this point in the history
* add simctl command

* add rough description

* remove timeout

* update example

* add result

* add a note

* add test

* fix lint and update types

* tweak the readme

* check the given command target

* tweak the example

* add tests

* add simulator check

* tweak docs

* revert bad commit

* add timeout as an option

* add tests

* bump node-simctl

* tweak type

* exclude addmedia
  • Loading branch information
KazuCocoa authored Aug 31, 2024
1 parent 2323ec7 commit 6aac043
Show file tree
Hide file tree
Showing 7 changed files with 182 additions and 1 deletion.
20 changes: 20 additions & 0 deletions docs/reference/execute-methods.md
Original file line number Diff line number Diff line change
Expand Up @@ -2086,3 +2086,23 @@ Same as for [mobile: startXCTestScreenRecording](#mobile-startxctestscreenrecord
Name | Type | Description | Example
--- | --- | --- | ---
payload | string | Base64-encoded content of the recorded media file if `remotePath` parameter is empty/null or an empty string otherwise. The resulting media is expected to a be a valid QuickTime movie (.mov). | `YXBwaXVt....`

### mobile: simctl

Runs the given command as a subcommand of `xcrun simctl` against the device under test.
Does not work for real devices.

#### Arguments
Name | Type | Required | Description | Example
--- | --- | --- | --- | ---
command | string | yes | a subcommand for the `simctl`. Available commands are boot, get_app_container, getenv, icloud_sync, install, install_app_data, io, keychain, launch, location, logverbose, openurl, pbcopy, pbpaste, privacy, push, shutdown, spawn, status_bar, terminate, ui, and uninstall. Please check each usage details with `xcrun simctl help`. | `'getenv'`
args | array | no | array of string as arguments for the command after `<device>`. For example `getenv` subcommand accept `simctl getenv <device> <variable name>`. The `<device>` will be filled out automatically. This `args` should be the ` <variable name>` part only. | `['HOME']`
timeout | number | no | Command timeout in milliseconds. If the command blocks for longer than this timeout then an exception is going to be thrown. The default timeout is `600000` ms. | `10000`

#### Returned Result

Name | Type | Description | Example
--- | --- | --- | ---
stdout | string | The standard output of the command. | `'/Users/user/Library/Developer/CoreSimulator/Devices/60EB8FDB-92E0-4895-B466-0153C6DE7BAE/data\n'`
stderr | string | The standard error of the command. | `''` (an empty string)
code | string | The status code of the command. | `0`
2 changes: 2 additions & 0 deletions lib/commands/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import recordAudioExtensions from './record-audio';
import recordScreenExtensions from './recordscreen';
import screenshotExtensions from './screenshots';
import sourceExtensions from './source';
import simctl from './simctl';
import timeoutExtensions from './timeouts';
import webExtensions from './web';
import xctestExtensions from './xctest';
Expand Down Expand Up @@ -80,6 +81,7 @@ export default {
proxyHelperExtensions,
recordAudioExtensions,
recordScreenExtensions,
simctl,
screenshotExtensions,
sourceExtensions,
timeoutExtensions,
Expand Down
75 changes: 75 additions & 0 deletions lib/commands/simctl.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { errors } from 'appium/driver';

/**
* List of subcommands for `simctl` we provide as mobile simctl command.
* They accept 'device' target.
*/
const SUBCOMMANDS_HAS_DEVICE = [
'boot',
'get_app_container',
'getenv',
'icloud_sync',
'install',
'install_app_data',
'io',
'keychain',
'launch',
'location',
'logverbose',
'openurl',
'pbcopy',
'pbpaste',
'privacy',
'push',
'shutdown',
'spawn',
'status_bar',
'terminate',
'ui',
'uninstall'
];

const commands = {
/**
* Run the given command with arguments as `xcrun simctl` subcommand.
* This method works behind the 'simctl' security flag.
* @this {XCUITestDriver}
* @param {string} command Subcommand to run with `xcrun simctl`
* @param {string[]} [args=[]] arguments for the subcommand. The arguments should be after <device> in the help.
* @param {number|undefined} timeout - The maximum number of milliseconds
* @returns {Promise<SimctlExecResponse>}
* @throws {Error} If the simctl subcommand command returns non-zero return code, or the given subcommand was invalid.
*/
async mobileSimctl(command, args = [], timeout = undefined) {
if (!this.isSimulator()) {
throw new errors.UnsupportedOperationError(`Only simulator is supported.`);
};

if (!this.opts.udid) {
throw new errors.InvalidArgumentError(`Unknown device or simulator UDID: '${this.opts.udid}'`);
}

if (!SUBCOMMANDS_HAS_DEVICE.includes(command)) {
throw new errors.InvalidArgumentError(`The given command '${command}' is not supported. ` +
`Available subcommands are ${SUBCOMMANDS_HAS_DEVICE.join(',')}`);
}

return await /** @type {import('./../driver').Simulator} */ (this.device).simctl.exec(
command,
{args: [this.opts.udid, ...args], timeout}
);
}
};

export default {...commands};

/**
* @typedef {Object} SimctlExecResponse
* @property {string} stdout The output of standard out.
* @property {string} stderr The output of standard error.
* @property {number} code Return code.
*/

/**
* @typedef {import('../driver').XCUITestDriver} XCUITestDriver
*/
1 change: 1 addition & 0 deletions lib/driver.js
Original file line number Diff line number Diff line change
Expand Up @@ -1874,6 +1874,7 @@ export class XCUITestDriver extends BaseDriver {
execute = commands.executeExtensions.execute;
executeAsync = commands.executeExtensions.executeAsync;
executeMobile = commands.executeExtensions.executeMobile;
mobileSimctl = commands.simctl.mobileSimctl;

/*--------------+
| FILEMOVEMENT |
Expand Down
7 changes: 7 additions & 0 deletions lib/execute-method-map.ts
Original file line number Diff line number Diff line change
Expand Up @@ -548,4 +548,11 @@ export const executeMethodMap = {
command: 'background',
params: {optional: ['seconds']},
},
'mobile: simctl': {
command: 'mobileSimctl',
params: {
required: ['command'],
optional: ['args', 'timeout'],
},
},
} as const satisfies ExecuteMethodMap<XCUITestDriver>;
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@
"lru-cache": "^10.0.0",
"moment": "^2.29.4",
"moment-timezone": "^0.x",
"node-simctl": "^7.1.17",
"node-simctl": "^7.6.0",
"portscanner": "^2.2.0",
"semver": "^7.5.4",
"source-map-support": "^0.x",
Expand Down
76 changes: 76 additions & 0 deletions test/unit/commands/simctl-specs.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import sinon from 'sinon';
import XCUITestDriver from '../../../lib/driver';
import Simctl from 'node-simctl';


describe('general commands', function () {
const driver = new XCUITestDriver();
const simctl = new Simctl();
driver._device = { simctl };

let chai;
let mockSimctl;

before(async function () {
chai = await import('chai');
chai.should();
});

beforeEach(function () {
mockSimctl = sinon.mock(driver.device.simctl);
});

afterEach(function () {
mockSimctl.verify();
});

describe('simctl', function () {
it('should call xcrun simctl', async function () {
driver.opts.udid = '60EB8FDB-92E0-4895-B466-0153C6DE7BAE';
driver.isSimulator = () => true;
mockSimctl.expects('exec').once().withExactArgs(
'getenv',
{args: ['60EB8FDB-92E0-4895-B466-0153C6DE7BAE', 'HOME'], timeout: undefined}
);
await driver.mobileSimctl('getenv', ['HOME']);
});

it('should call xcrun simctl with timeout', async function () {
driver.opts.udid = '60EB8FDB-92E0-4895-B466-0153C6DE7BAE';
driver.isSimulator = () => true;
mockSimctl.expects('exec').once().withExactArgs(
'getenv',
{args: ['60EB8FDB-92E0-4895-B466-0153C6DE7BAE', 'HOME'], timeout: 10000}
);
await driver.mobileSimctl('getenv', ['HOME'], 10000);
});

it('should raise an error as not supported command', async function () {
driver.opts.udid = '60EB8FDB-92E0-4895-B466-0153C6DE7BAE';
driver.isSimulator = () => true;
mockSimctl.expects('exec').never();
await driver.mobileSimctl(
'list',
['devices', 'booted', '--json']
).should.eventually.be.rejected;
});

it('should raise an error as no udid', async function () {
driver.opts.udid = null;
driver.isSimulator = () => true;
mockSimctl.expects('exec').never();
await driver.mobileSimctl(
'getenv', ['HOME']
).should.eventually.be.rejected;
});

it('should raise an error for non-simulator', async function () {
driver.opts.udid = '60EB8FDB-92E0-4895-B466-0153C6DE7BAE';
driver.isSimulator = () => false;
mockSimctl.expects('exec').never();
await driver.mobileSimctl(
'getenv', ['HOME']
).should.eventually.be.rejected;
});
});
});

0 comments on commit 6aac043

Please sign in to comment.