Skip to content

Commit

Permalink
Depends on #3304 (Detox v20).
Browse files Browse the repository at this point in the history
  • Loading branch information
asafkorem committed Jul 12, 2022
1 parent 45e2103 commit f530a32
Show file tree
Hide file tree
Showing 9 changed files with 117 additions and 67 deletions.
61 changes: 32 additions & 29 deletions detox/src/configuration/composeDeviceConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ function validateDeviceConfig({ deviceConfig, errorComposer, deviceAlias }) {
throw errorComposer.malformedDeviceProperty(deviceAlias, 'headless');
}

if (deviceConfig.type !== 'android.emulator') {
if (deviceConfig.type !== 'ios.simulator' && deviceConfig.type !== 'android.emulator') {
throw errorComposer.unsupportedDeviceProperty(deviceAlias, 'headless');
}
}
Expand Down Expand Up @@ -187,41 +187,44 @@ function validateDeviceConfig({ deviceConfig, errorComposer, deviceAlias }) {
}

function applyCLIOverrides(deviceConfig, cliConfig) {
if (cliConfig.deviceName) {
deviceConfig.device = cliConfig.deviceName;
_assignCLIConfigIfSupported('device-name', cliConfig.deviceName, deviceConfig, 'device');
_assignCLIConfigIfSupported('device-boot-args', cliConfig.deviceBootArgs, deviceConfig, 'bootArgs');
_assignCLIConfigIfSupported('headless', cliConfig.headless, deviceConfig, 'headless');
_assignCLIConfigIfSupported('force-adb-install', cliConfig.forceAdbInstall, deviceConfig, 'forceAdbInstall');
_assignCLIConfigIfSupported('gpu', cliConfig.gpu, deviceConfig, 'gpuMode');
_assignCLIConfigIfSupported('readonly-emu', cliConfig.readonlyEmu, deviceConfig, 'readonly');
}

function _assignCLIConfigIfSupported(argName, argValue, deviceConfig, propertyName) {
if (argValue === undefined) {
return;
}

const deviceType = deviceConfig.type;
if (cliConfig.deviceBootArgs) {
if ((deviceType === 'ios.simulator') || (deviceType === 'android.emulator')) {
deviceConfig.bootArgs = cliConfig.deviceBootArgs;
} else {
log.warn(`--device-boot-args CLI override is not supported by device type = "${deviceType}" and will be ignored`);
}
const supportedDeviceTypesPrefixes = _supportedDeviceTypesPrefixes(argName);
if (!supportedDeviceTypesPrefixes.some((prefix) => deviceType.startsWith(prefix))) {
log.warn(`--${argName} CLI override is not supported by device type = "${deviceType}" and will be ignored`);
return;
}

if (cliConfig.forceAdbInstall !== undefined) {
if (deviceType.startsWith('android.')) {
deviceConfig.forceAdbInstall = cliConfig.forceAdbInstall;
} else {
log.warn(`--force-adb-install CLI override is not supported by device type = "${deviceType}" and will be ignored`);
}
}
deviceConfig[propertyName] = argValue;
}

const emulatorCLIConfig = _.pick(cliConfig, ['headless', 'gpu', 'readonlyEmu']);
const emulatorOverrides = _.omitBy({
headless: cliConfig.headless,
gpuMode: cliConfig.gpu,
readonly: cliConfig.readonlyEmu,
}, _.isUndefined);
function _supportedDeviceTypesPrefixes(argName) {
switch (argName) {
case 'device-name':
return [''];

if (!_.isEmpty(emulatorOverrides)) {
if (deviceType === 'android.emulator') {
Object.assign(deviceConfig, emulatorOverrides);
} else {
const flags = Object.keys(emulatorCLIConfig).map(key => '--' + _.kebabCase(key)).join(', ');
log.warn(`${flags} CLI overriding is not supported by device type = "${deviceType}" and will be ignored`);
}
case 'force-adb-install':
return ['android.'];

case 'gpu':
case 'readonly-emu':
return ['android.emulator'];

case 'device-boot-args':
case 'headless':
return ['ios.simulator', 'android.emulator'];
}
}

Expand Down
35 changes: 22 additions & 13 deletions detox/src/configuration/composeDeviceConfig.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -363,13 +363,16 @@ describe('composeDeviceConfig', () => {
});

describe('--headless', () => {
describe('given android.emulator device', () => {
beforeEach(() => setConfig('android.emulator', configType));
describe.each([
['ios.simulator'],
['android.emulator']
])('given a supported device type (%j)', (deviceType) => {
beforeEach(() => setConfig(deviceType, configType));

it('should override .headless without warnings', () => {
cliConfig.headless = true;
cliConfig.headless = false;
expect(compose()).toEqual(expect.objectContaining({
headless: true,
headless: false,
}));

expect(logger.warn).not.toHaveBeenCalled();
Expand All @@ -378,17 +381,16 @@ describe('composeDeviceConfig', () => {

describe.each([
['ios.none'],
['ios.simulator'],
['android.attached'],
['android.genycloud'],
['./customDriver'],
['./customDriver']
])('given a non-supported device (%j)', (deviceType) => {
beforeEach(() => setConfig(deviceType, configType));

it('should print a warning and refuse to override .headless', () => {
cliConfig.headless = true;
cliConfig.headless = false;
expect(compose()).not.toEqual(expect.objectContaining({
headless: true,
headless: false,
}));

expect(logger.warn).toHaveBeenCalledWith(expect.stringMatching(/--headless.*not supported/));
Expand Down Expand Up @@ -608,22 +610,29 @@ describe('composeDeviceConfig', () => {
describe('.headless validation', () => {
test.each([
'ios.none',
'ios.simulator',
'android.attached',
'android.genycloud',
'android.genycloud'
])('cannot be used for a non-emulator device (%j)', (deviceType) => {
setConfig(deviceType, configType);
deviceConfig.headless = true;
expect(compose).toThrow(errorComposer.unsupportedDeviceProperty(alias(), 'headless'));
});

describe('given android.emulator device', () => {
beforeEach(() => setConfig('android.emulator', configType));
describe.each([
'ios.simulator',
'android.emulator'
])('given supporting device type (%j)', (deviceType) => {
beforeEach(() => setConfig(deviceType, configType));

test(`should throw if value is not a boolean (e.g., string)`, () => {
deviceConfig.headless = `${Math.random() > 0.5}`; // string
deviceConfig.headless = `${Math.random() > 0.5}`;
expect(compose).toThrowError(errorComposer.malformedDeviceProperty(alias(), 'headless'));
});

test('should not throw if value is a boolean', () => {
deviceConfig.headless = false;
expect(compose).not.toThrowError();
});
});

test('should be disabled for custom devices', () => {
Expand Down
6 changes: 3 additions & 3 deletions detox/src/devices/allocation/drivers/ios/SimulatorLauncher.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ class SimulatorLauncher extends DeviceLauncher {
this._applesimutils = applesimutils;
}

async launch(udid, type, bootArgs) {
const coldBoot = await this._applesimutils.boot(udid, bootArgs);
await this._notifyBootEvent(udid, type, coldBoot);
async launch(udid, type, bootArgs, headless) {
const coldBoot = await this._applesimutils.boot(udid, bootArgs, headless);
await this._notifyBootEvent(udid, type, coldBoot, headless);
}

async shutdown(udid) {
Expand Down
21 changes: 14 additions & 7 deletions detox/src/devices/allocation/drivers/ios/SimulatorLauncher.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,14 @@ describe('Simulator launcher (helper)', () => {
describe('launch', () => {
const type = 'mockType';
const bootArgs = { mock: 'boot-args' };
const headless = true;

const givenBootResultCold = () => applesimutils.boot.mockResolvedValue(true);
const givenBootResultWarm = () => applesimutils.boot.mockResolvedValue(false);

it('should boot using apple-sim-utils', async () => {
await uut.launch(udid, '', bootArgs);
expect(applesimutils.boot).toHaveBeenCalledWith(udid, bootArgs);
await uut.launch(udid, '', bootArgs, headless);
expect(applesimutils.boot).toHaveBeenCalledWith(udid, bootArgs, headless);
});

it('should fail if apple-sim-utils fails', async () => {
Expand All @@ -35,20 +36,26 @@ describe('Simulator launcher (helper)', () => {

it('should emit boot event', async () => {
givenBootResultWarm();
await uut.launch(udid, type, {});
expect(eventEmitter.emit).toHaveBeenCalledWith('bootDevice', expect.objectContaining({ deviceId: udid, type, coldBoot: false }));
await uut.launch(udid, type, {}, headless);
expect(eventEmitter.emit).toHaveBeenCalledWith(
'bootDevice',
expect.objectContaining({ deviceId: udid, type, coldBoot: false, headless })
);
});

it('should emit cold-boot status in boot event', async () => {
givenBootResultCold();
await uut.launch(udid, type, {});
expect(eventEmitter.emit).toHaveBeenCalledWith('bootDevice', expect.objectContaining({ deviceId: udid, type, coldBoot: true }));
await uut.launch(udid, type, {}, headless);
expect(eventEmitter.emit).toHaveBeenCalledWith(
'bootDevice',
expect.objectContaining({ deviceId: udid, type, coldBoot: true, headless })
);
});

it('should fail if emission fails', async () => {
const error = new Error('mock error');
eventEmitter.emit.mockRejectedValue(error);
await expect(uut.launch(udid, '', bootArgs)).rejects.toThrowError(error);
await expect(uut.launch(udid, '', bootArgs, headless)).rejects.toThrowError(error);
});
});

Expand Down
4 changes: 2 additions & 2 deletions detox/src/devices/common/drivers/DeviceLauncher.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ class DeviceLauncher {
return this._eventEmitter.emit('shutdownDevice', { deviceId });
}

async _notifyBootEvent(deviceId, type, coldBoot) {
return this._eventEmitter.emit('bootDevice', { deviceId, type, coldBoot });
async _notifyBootEvent(deviceId, type, coldBoot, headless) {
return this._eventEmitter.emit('bootDevice', { deviceId, type, coldBoot, headless });
}
}

Expand Down
43 changes: 34 additions & 9 deletions detox/src/devices/common/drivers/ios/tools/AppleSimUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,20 +42,27 @@ class AppleSimUtils {
/***
* Boots the simulator if it is not booted already.
*
* @param {String} udid - device id
* @returns {Promise<boolean>} true, if device has been booted up from the shutdown state
* @param {String} udid iOS Simulator UDID.
* @param {String} deviceBootArgs simctl boot command arguments.
* @param {Boolean} headless If false, opens the Simulator app after the Simulator has booted.
* @returns {Promise<boolean>} true, if device has been booted up from the shutdown state.
*/
async boot(udid, deviceBootArgs = '') {
async boot(udid, deviceBootArgs = '', headless = false) {
const isBooted = await this.isBooted(udid);

if (!isBooted) {
const statusLogs = { trying: `Booting device ${udid}...` };
await this._execSimctl({ cmd: `boot ${udid} ${deviceBootArgs}`, statusLogs, retries: 10 });
await this._execSimctl({ cmd: `bootstatus ${udid}`, retries: 1 });
return true;
if (isBooted) {
return false;
}

return false;
const statusLogs = { trying: `Booting device ${udid}...` };
await this._execSimctl({ cmd: `boot ${udid} ${deviceBootArgs}`, statusLogs, retries: 10 });
await this._execSimctl({ cmd: `bootstatus ${udid}`, retries: 1 });

if (!headless) {
await this._openSimulatorApp(udid);
}

return true;
}

async isBooted(udid) {
Expand All @@ -72,6 +79,24 @@ class AppleSimUtils {
return device;
}

async _openSimulatorApp(udid) {
try {
await childProcess.execWithRetriesAndLogs(`open -a Simulator --args -CurrentDeviceUDID ${udid}`, { retries: 0 });
} catch (error) {
this._logUnableToOpenSimulator();
}
}

_logUnableToOpenSimulator() {
log.warn(
`Unable to open the Simulator app. Please make sure you have Xcode and iOS Simulator installed ` +
`(https://developer.apple.com/xcode/). In case you already have the latest Xcode version installed, ` +
`try run the command: \`sudo xcode-select -s /Applications/Xcode.app\`. If you are running tests from CI, ` +
`we recommend running them with "--headless" device configuration (see: ` +
`https://wix.github.io/Detox/docs/next/api/configuration/#device-configurations).`
);
}

/***
* @param deviceInfo - an item in output of `applesimutils --list`
* @returns {Promise<string>} UDID of a new device
Expand Down
5 changes: 3 additions & 2 deletions detox/src/devices/runtime/drivers/ios/SimulatorDriver.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,13 @@ class SimulatorDriver extends IosDriver {
* @param deps { SimulatorDriverDeps }
* @param props { SimulatorDriverProps }
*/
constructor(deps, { udid, type, bootArgs }) {
constructor(deps, { udid, type, bootArgs, headless }) {
super(deps);

this.udid = udid;
this._type = type;
this._bootArgs = bootArgs;
this._headless = headless;
this._deviceName = `${udid} (${this._type})`;
this._simulatorLauncher = deps.simulatorLauncher;
this._applesimutils = deps.applesimutils;
Expand Down Expand Up @@ -151,7 +152,7 @@ class SimulatorDriver extends IosDriver {
async resetContentAndSettings() {
await this._simulatorLauncher.shutdown(this.udid);
await this._applesimutils.resetContentAndSettings(this.udid);
await this._simulatorLauncher.launch(this.udid, this._type, this._bootArgs);
await this._simulatorLauncher.launch(this.udid, this._type, this._bootArgs, this._headless);
}

getLogsPaths() {
Expand Down
8 changes: 6 additions & 2 deletions detox/src/devices/runtime/drivers/ios/SimulatorDriver.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ describe('IOS simulator driver', () => {
const type = 'Chika';
const bundleId = 'bundle-id-mock';
const bootArgs = { boot: 'args' };
const headless = true;

let client;
let eventEmitter;
Expand All @@ -27,7 +28,10 @@ describe('IOS simulator driver', () => {
simulatorLauncher = new SimulatorLauncher();

const SimulatorDriver = require('./SimulatorDriver');
uut = new SimulatorDriver({ simulatorLauncher, applesimutils, client, eventEmitter }, { udid, type, bootArgs });
uut = new SimulatorDriver(
{ simulatorLauncher, applesimutils, client, eventEmitter },
{ udid, type, bootArgs, headless }
);
});

it('should return the UDID as the external ID', () => {
Expand Down Expand Up @@ -80,7 +84,7 @@ describe('IOS simulator driver', () => {

it('should relaunch the simulator', async () => {
await uut.resetContentAndSettings();
expect(simulatorLauncher.launch).toHaveBeenCalledWith(udid, type, bootArgs);
expect(simulatorLauncher.launch).toHaveBeenCalledWith(udid, type, bootArgs, true);
});
});

Expand Down
1 change: 1 addition & 0 deletions detox/src/devices/runtime/factories/ios.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ class IosSimulator extends RuntimeDriverFactoryIos {
udid: deviceCookie.udid,
type: deviceConfig.device.type,
bootArgs: deviceConfig.bootArgs,
headless: deviceConfig.headless
};

const { IosSimulatorRuntimeDriver } = require('../drivers');
Expand Down

0 comments on commit f530a32

Please sign in to comment.