From 13f99c72798dff3119c73b59732a82b5471d91b5 Mon Sep 17 00:00:00 2001 From: Yaroslav Serhieiev Date: Wed, 20 May 2020 12:29:50 +0300 Subject: [PATCH] fix: configuration --- .../src/configuration/composeDeviceConfig.js | 38 ++---- .../configuration/composeDeviceConfig.test.js | 118 ++++++------------ .../composeSessionConfig.test.js | 9 +- detox/src/configuration/index.js | 56 +++++---- detox/src/configuration/index.test.js | 55 ++------ detox/src/configuration/loadExternalConfig.js | 9 +- .../configuration/loadExternalConfig.test.js | 40 ++++++ .../src/configuration/selectConfiguration.js | 33 +++++ .../configuration/selectConfiguration.test.js | 103 +++++++++++++++ detox/src/configuration/utils.js | 18 --- detox/src/configuration/utils.test.js | 6 - 11 files changed, 279 insertions(+), 206 deletions(-) create mode 100644 detox/src/configuration/selectConfiguration.js create mode 100644 detox/src/configuration/selectConfiguration.test.js delete mode 100644 detox/src/configuration/utils.js delete mode 100644 detox/src/configuration/utils.test.js diff --git a/detox/src/configuration/composeDeviceConfig.js b/detox/src/configuration/composeDeviceConfig.js index e9207cfe2a..8a35402753 100644 --- a/detox/src/configuration/composeDeviceConfig.js +++ b/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; diff --git a/detox/src/configuration/composeDeviceConfig.test.js b/detox/src/configuration/composeDeviceConfig.test.js index 2652e94c2a..1eb49dd993 100644 --- a/detox/src/configuration/composeDeviceConfig.test.js +++ b/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'); }); - }) + }); }); }); diff --git a/detox/src/configuration/composeSessionConfig.test.js b/detox/src/configuration/composeSessionConfig.test.js index 19fda1d80c..28694fea35 100644 --- a/detox/src/configuration/composeSessionConfig.test.js +++ b/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, diff --git a/detox/src/configuration/index.js b/detox/src/configuration/index.js index 07e4cc70fa..0ab9e26955 100644 --- a/detox/src/configuration/index.js +++ b/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({ @@ -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, @@ -57,7 +64,7 @@ async function composeDetoxConfig({ return { meta: { - configuration: configurationName, + configuration: deviceConfigName, location: externalConfig.filepath, }, @@ -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`); + }, }; diff --git a/detox/src/configuration/index.test.js b/detox/src/configuration/index.test.js index 9b1fcbc099..2bc2ba9fb2 100644 --- a/detox/src/configuration/index.test.js +++ b/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; @@ -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: { @@ -99,3 +57,10 @@ describe('configuration', () => { }); }); }); + +describe('throwOnEmptyBinaryPath', () => { + it('should throw an error', () => { + const { throwOnEmptyBinaryPath } = require('./index'); + expect(throwOnEmptyBinaryPath).toThrowError(/binaryPath.*missing/); + }); +}); diff --git a/detox/src/configuration/loadExternalConfig.js b/detox/src/configuration/loadExternalConfig.js index 61692b9e26..b569b4bd1a 100644 --- a/detox/src/configuration/loadExternalConfig.js +++ b/detox/src/configuration/loadExternalConfig.js @@ -1,10 +1,9 @@ const { cosmiconfig } = require('cosmiconfig'); +const explorer = cosmiconfig('detox') -async function loadExternalConfig(detoxConfigPath, cwd) { - const explorer = cosmiconfig('detox') - - return detoxConfigPath - ? await explorer.load(detoxConfigPath) +async function loadExternalConfig({ configPath, cwd }) { + return configPath + ? await explorer.load(configPath) : await explorer.search(cwd); } diff --git a/detox/src/configuration/loadExternalConfig.test.js b/detox/src/configuration/loadExternalConfig.test.js index 6057de9779..ff572d3c4d 100644 --- a/detox/src/configuration/loadExternalConfig.test.js +++ b/detox/src/configuration/loadExternalConfig.test.js @@ -1,8 +1,48 @@ +const path = require('path'); +const os = require('os'); + describe('loadExternalConfig', () => { + const DIR_DETOXRC = path.join(__dirname, '__mocks__/configuration/detoxrc'); + const DIR_PRIORITY = path.join(__dirname, '__mocks__/configuration/priority'); + let loadExternalConfig; beforeEach(() => { loadExternalConfig = require('./loadExternalConfig'); }); + it('should implicitly use package.json config if it has "detox" section', async () => { + const { filepath, config } = await loadExternalConfig({ cwd: DIR_PRIORITY }); + + expect(filepath).toBe(path.join(DIR_PRIORITY, 'package.json')) + expect(config).toMatchObject({ configurations: expect.anything() }); + }); + + it('should implicitly use .detoxrc if package.json has no "detox" section', async () => { + const { filepath, config } = await loadExternalConfig({ cwd: DIR_DETOXRC }); + + expect(filepath).toBe(path.join(DIR_DETOXRC, '.detoxrc.yml')) + expect(config).toMatchObject({ configurations: expect.anything() }); + }); + + it('should return an empty result if a config cannot be implicitly found', async () => { + const result = await loadExternalConfig({ cwd: os.homedir() }); + expect(result).toBe(null); + }); + + it('should explicitly use the specified config (via env-cli args)', async () => { + const configPath = path.join(DIR_PRIORITY, 'detox-config.json'); + const { filepath, config } = await loadExternalConfig({ configPath }); + + expect(filepath).toBe(configPath) + expect(config).toMatchObject({ configurations: expect.anything() }); + }); + + it('should throw if the explicitly given config is not found', async () => { + const configPath = path.join(DIR_PRIORITY, 'non-existent.json'); + + await expect(loadExternalConfig({ configPath })).rejects.toThrowError( + /ENOENT: no such file.*non-existent.json/ + ); + }); }); diff --git a/detox/src/configuration/selectConfiguration.js b/detox/src/configuration/selectConfiguration.js new file mode 100644 index 0000000000..00bdd0ade8 --- /dev/null +++ b/detox/src/configuration/selectConfiguration.js @@ -0,0 +1,33 @@ +const _ = require('lodash'); +const DetoxRuntimeError = require('../errors/DetoxRuntimeError'); + +function hintConfigurations(configurations) { + return _.keys(configurations).map(c => `* ${c}`).join('\n') +} + +function selectConfiguration({ detoxConfig, cliConfig, selectedConfiguration }) { + const { configurations } = detoxConfig; + + if (_.isEmpty(configurations)) { + throw new DetoxRuntimeError({ + message: `There are no device configurations in the given Detox config:`, + debugInfo: util.inspect(detoxConfig, false, 1, false), + }); + } + + let configurationName = selectedConfiguration || cliConfig.configuration || detoxConfig.selectedConfiguration; + if (!configurationName && _.size(configurations) === 1) { + configurationName = _.keys(configurations)[0]; + } + + if (!configurationName) { + throw new DetoxRuntimeError({ + message: 'Cannot determine which configuration to use.', + hint: 'Use --configuration to choose one of the following:\n' + hintConfigurations(configurations), + }); + } + + return configurationName; +} + +module.exports = selectConfiguration; \ No newline at end of file diff --git a/detox/src/configuration/selectConfiguration.test.js b/detox/src/configuration/selectConfiguration.test.js new file mode 100644 index 0000000000..0f621b76e3 --- /dev/null +++ b/detox/src/configuration/selectConfiguration.test.js @@ -0,0 +1,103 @@ +describe('selectConfiguration', () => { + let selectConfiguration; + + beforeEach(() => { + selectConfiguration = require('./selectConfiguration'); + }); + + // describe('validation', () => { + // it('should throw if no configurations are passed', () => { + // expect(() => composeDeviceConfig({ + // configurations: {}, + // }, cliConfig)).toThrowError(/There are no device configurations/); + // }); + // + // 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/); + // }); + // + // it('should throw if device query is not defined', () => { + // expect(() => composeDeviceConfig({ + // configurations: { + // undefinedDeviceQuery: { + // type: 'ios.simulator', + // }, + // }, + // }, cliConfig)).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('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; + // + // expect(composeDeviceConfig({ + // selectedConfiguration: 'config1', + // configurations: { config1, config2 }, + // }, cliConfig)).toEqual(config1); + // }); + // }); + // }); + // }); + // + // describe('for a specified configuration name', () => { + // let sampleConfigs; + // + // beforeEach(() => { + // cliConfig.configuration = 'config2'; + // + // const [config1, config2] = [1, 2].map(i => ({ + // type: `someDriver${i}`, + // device: `someDevice${i}`, + // })); + // + // sampleConfigs = { config1, config2 }; + // }); + // + // it('should return that config', () => { + // expect(composeDeviceConfig({ + // configurations: sampleConfigs + // }, cliConfig)).toEqual(sampleConfigs.config2); + // }); + // + // describe('if device-name override is present', () => { + // beforeEach(() => { cliConfig.deviceName = 'Override'; }); + // + // it('should return that config with an overriden device query', () => { + // expect(composeDeviceConfig({ + // configurations: sampleConfigs + // }, cliConfig)).toEqual({ + // ...sampleConfigs.config2, + // device: 'Override', + // }); + // }); + // }) + // }); +}); diff --git a/detox/src/configuration/utils.js b/detox/src/configuration/utils.js deleted file mode 100644 index ab13759a26..0000000000 --- a/detox/src/configuration/utils.js +++ /dev/null @@ -1,18 +0,0 @@ -const _ = require('lodash'); -const DetoxConfigError = require('../errors/DetoxConfigError'); - -function hintConfigurations(configurations) { - return Object.keys(configurations).map(c => `* ${c}`).join('\n') -} - -function throwOnEmptyBinaryPath() { - throw new DetoxConfigError(`'binaryPath' property is missing, should hold the app binary path`); -} - -module.exports = { - negateDefined, - hintConfigurations, - throwOnEmptyBinaryPath, - throwOnEmptyDevice, - throwOnEmptyType, -}; diff --git a/detox/src/configuration/utils.test.js b/detox/src/configuration/utils.test.js deleted file mode 100644 index a8b3cf11c7..0000000000 --- a/detox/src/configuration/utils.test.js +++ /dev/null @@ -1,6 +0,0 @@ -const { throwOnEmptyBinaryPath } = require('./utils'); - -describe('throwOnEmptyBinaryPath', () => { - it('should throw an error', () => - expect(throwOnEmptyBinaryPath).toThrowError(/binaryPath.*missing/)) -})