Skip to content

Commit

Permalink
fix: configuration
Browse files Browse the repository at this point in the history
  • Loading branch information
noomorph committed May 20, 2020
1 parent edc5e6a commit 13f99c7
Show file tree
Hide file tree
Showing 11 changed files with 279 additions and 206 deletions.
38 changes: 10 additions & 28 deletions detox/src/configuration/composeDeviceConfig.js
@@ -1,45 +1,27 @@
const _ = require('lodash');
const DetoxConfigError = require('../errors/DetoxConfigError');

function composeDeviceConfig(options, cliConfig) {
const { configurations, selectedConfiguration } = options;

if (_.isEmpty(configurations)) {
throw new Error(`There are no device configurations in the detox config`);
}

const configurationName = selectedConfiguration || cliConfig.configuration;
const deviceOverride = cliConfig.deviceName;

const deviceConfig = (!configurationName && _.size(configurations) === 1)
? _.values(configurations)[0]
: configurations[configurationName];

if (!deviceConfig) {
throw new Error(`Cannot determine which configuration to use. use --configuration to choose one of the following:
${Object.keys(configurations)}`);
}

if (!deviceConfig.type) {
function composeDeviceConfig({ rawDeviceConfig, cliConfig }) {
if (!rawDeviceConfig.type) {
throwOnEmptyType();
}

deviceConfig.device = deviceOverride || deviceConfig.device || deviceConfig.name;
delete deviceConfig.name;
rawDeviceConfig.device = cliConfig.deviceName || rawDeviceConfig.device || rawDeviceConfig.name;
delete rawDeviceConfig.name;

if (_.isEmpty(deviceConfig.device)) {
if (_.isEmpty(rawDeviceConfig.device)) {
throwOnEmptyDevice();
}

return deviceConfig;
}

function throwOnEmptyDevice() {
throw new DetoxConfigError(`'device' property is empty, should hold the device query to run on (e.g. { "type": "iPhone 11 Pro" }, { "avdName": "Nexus_5X_API_29" })`);
return rawDeviceConfig;
}

function throwOnEmptyType() {
throw new DetoxConfigError(`'type' property is missing, should hold the device type to test on (e.g. "ios.simulator" or "android.emulator")`);
}

function throwOnEmptyDevice() {
throw new DetoxConfigError(`'device' property is empty, should hold the device query to run on (e.g. { "type": "iPhone 11 Pro" }, { "avdName": "Nexus_5X_API_29" })`);
}

module.exports = composeDeviceConfig;
118 changes: 38 additions & 80 deletions detox/src/configuration/composeDeviceConfig.test.js
@@ -1,112 +1,70 @@
describe('composeDeviceConfig', () => {
let composeDeviceConfig;
let configs;
let cliConfig;
let composeDeviceConfig, cliConfig, rawDeviceConfig;

beforeEach(() => {
composeDeviceConfig = require('./composeDeviceConfig');

cliConfig = {};
configs = [1, 2].map(i => ({
type: `someDriver${i}`,
device: `someDevice${i}`,
}));
rawDeviceConfig = {
type: 'ios.simulator',
device: {
type: 'iPhone X'
},
};
});

describe('validation', () => {
it('should throw if no configurations are passed', () => {
expect(() => composeDeviceConfig({
configurations: {},
}, cliConfig)).toThrowError(/There are no device configurations/);
});
const compose = () => composeDeviceConfig({ cliConfig, rawDeviceConfig });

describe('validation', () => {
it('should throw if configuration driver (type) is not defined', () => {
expect(() => composeDeviceConfig({
configurations: {
undefinedDriver: {
device: { type: 'iPhone X' },
},
},
}, cliConfig)).toThrowError(/type.*missing.*ios.simulator.*android.emulator/);
delete rawDeviceConfig.type;
expect(compose).toThrowError(/type.*missing.*ios.simulator.*android.emulator/);
});

it('should throw if device query is not defined', () => {
expect(() => composeDeviceConfig({
configurations: {
undefinedDeviceQuery: {
type: 'ios.simulator',
},
},
}, cliConfig)).toThrowError(/device.*empty.*device.*query.*type.*avdName/);
delete rawDeviceConfig.device;
expect(compose).toThrowError(/device.*empty.*device.*query.*type.*avdName/);
});
});

describe('for no specified configuration name', () => {
beforeEach(() => { delete cliConfig.configuration; });

describe('when there is a single config', () => {
it('should return it', () => {
const singleDeviceConfig = configs[0];

expect(composeDeviceConfig({
configurations: {singleDeviceConfig }
}, cliConfig)).toBe(singleDeviceConfig);
});
describe('if a device configuration has the old .name property', () => {
beforeEach(() => {
rawDeviceConfig.name = rawDeviceConfig.device;
delete rawDeviceConfig.device;
});

describe('when there is more than one config', () => {
it('should throw if there is more than one config', () => {
const [config1, config2] = configs;
expect(() => composeDeviceConfig({
configurations: { config1, config2 },
}, cliConfig)).toThrowError(/Cannot determine/);
});

describe('but also selectedConfiguration param is specified', () => {
it('should select that configuration', () => {
const [config1, config2] = configs;
it('should rename it to .device', () => {
const { type, device, name } = compose();

expect(composeDeviceConfig({
selectedConfiguration: 'config1',
configurations: { config1, config2 },
}, cliConfig)).toEqual(config1);
});
});
expect(type).toBe('ios.simulator');
expect(name).toBe(undefined);
expect(device).toEqual({ type: 'iPhone X' });
});
});

describe('for a specified configuration name', () => {
let sampleConfigs;

describe('if a device configuration has the new .device property', () => {
beforeEach(() => {
cliConfig.configuration = 'config2';
rawDeviceConfig.device = 'iPhone SE';
});

const [config1, config2] = [1, 2].map(i => ({
type: `someDriver${i}`,
device: `someDevice${i}`,
}));
it('should be left intact', () => {
const { type, device } = compose();

sampleConfigs = { config1, config2 };
expect(type).toBe('ios.simulator');
expect(device).toBe('iPhone SE');
});

it('should return that config', () => {
expect(composeDeviceConfig({
configurations: sampleConfigs
}, cliConfig)).toEqual(sampleConfigs.config2);
});
describe('and there is a CLI override', () => {
beforeEach(() => {
cliConfig.deviceName = 'iPad Air';
});

describe('if device-name override is present', () => {
beforeEach(() => { cliConfig.deviceName = 'Override'; });
it('should be override .device property', () => {
const { type, device } = compose();

it('should return that config with an overriden device query', () => {
expect(composeDeviceConfig({
configurations: sampleConfigs
}, cliConfig)).toEqual({
...sampleConfigs.config2,
device: 'Override',
});
expect(type).toBe('ios.simulator');
expect(device).toBe('iPad Air');
});
})
});
});
});

9 changes: 8 additions & 1 deletion detox/src/configuration/composeSessionConfig.test.js
@@ -1,5 +1,12 @@
describe('composeSessionConfig', () => {
const composeSessionConfig = (...args) => configuration._internals.composeSessionConfig(...args);
let composeSessionConfig;
let detoxConfig, deviceConfig;

beforeEach(() => {
composeSessionConfig = require('./composeSessionConfig');
detoxConfig = {};
deviceConfig = {};
});

const compose = () => composeSessionConfig({
detoxConfig,
Expand Down
56 changes: 33 additions & 23 deletions detox/src/configuration/index.js
@@ -1,22 +1,29 @@
const _ = require('lodash');
const DetoxConfigError = require('../errors/DetoxConfigError');
const DetoxRuntimeError = require('../errors/DetoxRuntimeError');

const configUtils = require('./utils');
const collectCliConfig = require('./collectCliConfig');
const loadExternalConfig = require('./loadExternalConfig');
const composeArtifactsConfig = require('./composeArtifactsConfig');
const composeBehaviorConfig = require('./composeBehaviorConfig');
const composeDeviceConfig = require('./composeDeviceConfig');
const composeSessionConfig = require('./composeSessionConfig');
const selectConfiguration = require('./selectConfiguration');

async function composeDetoxConfig({
cwd = process.cwd(),
cwd,
argv,
selectedConfiguration,
configuration,
override,
userParams,
}) {
const cliConfig = collectCliConfig(argv);
const cosmiResult = await loadDetoxConfig(cliConfig.configPath, cwd);
const cliConfig = collectCliConfig({ argv });
const cosmiResult = await loadExternalConfig({
configPath: cliConfig.configPath,
cwd,
});

const externalConfig = cosmiResult && cosmiResult.config;
const detoxConfig = _.merge(
externalConfig,
override,
);
const detoxConfig = _.merge({}, externalConfig, override);

if (_.isEmpty(detoxConfig)) {
throw new DetoxRuntimeError({
Expand All @@ -25,26 +32,26 @@ async function composeDetoxConfig({
});
}

if (argv.configuration) {
detoxConfig.selectedConfiguration = argv.configuration;
}

if (selectedConfiguration) {
detoxConfig.selectedConfiguration = selectedConfiguration;
}
const deviceConfigName = selectConfiguration({
detoxConfig,
cliConfig,
selectedConfiguration: configuration,
});

const deviceConfig = composeDeviceConfig(detoxConfig);
const configurationName = _.findKey(detoxConfig.configurations, (config) => {
return config === deviceConfig;
const deviceConfig = composeDeviceConfig({
cliConfig,
rawDeviceConfig: detoxConfig.configurations[deviceConfigName],
});

const artifactsConfig = composeArtifactsConfig({
configurationName,
cliConfig,
configurationName: deviceConfigName,
detoxConfig,
deviceConfig,
});

const behaviorConfig = composeBehaviorConfig({
cliConfig,
detoxConfig,
deviceConfig,
userParams,
Expand All @@ -57,7 +64,7 @@ async function composeDetoxConfig({

return {
meta: {
configuration: configurationName,
configuration: deviceConfigName,
location: externalConfig.filepath,
},

Expand All @@ -69,6 +76,9 @@ async function composeDetoxConfig({
}

module.exports = {
throwOnEmptyBinaryPath: configUtils.throwOnEmptyBinaryPath,
composeDetoxConfig,

throwOnEmptyBinaryPath() {
throw new DetoxConfigError(`'binaryPath' property is missing, should hold the app binary path`);
},
};
55 changes: 10 additions & 45 deletions detox/src/configuration/index.test.js
@@ -1,9 +1,9 @@
const _ = require('lodash');
const path = require('path');

jest.mock('./argparse');
jest.mock('../utils/argparse');

describe('configuration', () => {
describe('composeDetoxConfig', () => {
let args;
let configuration;
let detoxConfig;
Expand All @@ -27,53 +27,11 @@ describe('configuration', () => {
);
});

it('should implicitly use package.json config if it has "detox" section', async () => {
const config = await configuration.composeDetoxConfig({
cwd: path.join(__dirname, '__mocks__/configuration/priority'),
});

expect(config).toMatchObject({
deviceConfig: expect.objectContaining({
device: 'Hello from package.json',
}),
});
});

it('should implicitly use .detoxrc if package.json has no "detox" section', async () => {
const config = await configuration.composeDetoxConfig({
cwd: path.join(__dirname, '__mocks__/configuration/detoxrc')
});

expect(config).toMatchObject({
deviceConfig: expect.objectContaining({
device: 'Hello from .detoxrc',
}),
});
});

it('should explicitly use the specified config (via env-cli args)', async () => {
args['config-path'] = path.join(__dirname, '__mocks__/configuration/priority/detox-config.json');
const config = await configuration.composeDetoxConfig({});

expect(config).toMatchObject({
deviceConfig: expect.objectContaining({
device: 'Hello from detox-config.json',
}),
});
});

it('should throw if explicitly given config is not found', async () => {
args['config-path'] = path.join(__dirname, '__mocks__/configuration/non-existent.json');

await expect(configuration.composeDetoxConfig({})).rejects.toThrowError(
/ENOENT: no such file.*non-existent.json/
);
});

it('should return a complete Detox config merged with the file configuration', async () => {
const config = await configuration.composeDetoxConfig({
cwd: path.join(__dirname, '__mocks__/configuration/detoxrc'),
selectedConfiguration: 'another',
configuration: 'another',
override: {
configurations: {
another: {
Expand All @@ -99,3 +57,10 @@ describe('configuration', () => {
});
});
});

describe('throwOnEmptyBinaryPath', () => {
it('should throw an error', () => {
const { throwOnEmptyBinaryPath } = require('./index');
expect(throwOnEmptyBinaryPath).toThrowError(/binaryPath.*missing/);
});
});

0 comments on commit 13f99c7

Please sign in to comment.