Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(config): external configuration files #2050

Merged
merged 10 commits into from May 25, 2020
40 changes: 21 additions & 19 deletions detox/__tests__/setupJest.js
Expand Up @@ -5,27 +5,29 @@ const path = require('path');

function callCli(modulePath, cmd) {
return new Promise((resolve, reject) => {
try {
yargs
.scriptName('detox')
.command(require(path.join(__dirname, "../local-cli", modulePath)))
.exitProcess(false)
.fail((msg, err) => reject(err || msg))
.parse(cmd, (err, argv, output) => {
err ? reject(err) : setImmediate(() => resolve(output));
});
} catch (e) {
reject(e);
}
});
}
const originalModule = require(path.join(__dirname, "../local-cli", modulePath));
const originalHandler = originalModule.handler;
const spiedModule = {
...originalModule,
handler: async program => {
try {
return await originalHandler(program);
} catch (e) {
reject(e);
} finally {
resolve();
}
}
};

function mockPackageJson(mockContent) {
jest.mock(path.join(process.cwd(), 'package.json'), () => ({
detox: mockContent
}));
return yargs
.scriptName('detox')
.command(spiedModule)
.exitProcess(false)
.fail((msg, err) => reject(err || msg))
.parse(cmd, (err) => err && reject(err));
});
}

global.mockPackageJson = mockPackageJson;
global.callCli = callCli;
global.IS_RUNNING_DETOX_UNIT_TESTS = true;
10 changes: 10 additions & 0 deletions detox/local-cli/__mocks__/detox.config.js
@@ -0,0 +1,10 @@
module.exports = {
configurations: {
only: {
type: 'ios.simulator',
device: {
type: 'iPhone 11'
}
},
},
};
13 changes: 0 additions & 13 deletions detox/local-cli/__snapshots__/build.test.js.snap

This file was deleted.

6 changes: 5 additions & 1 deletion detox/local-cli/__snapshots__/test.test.js.snap
@@ -1,3 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`test fails with a different runner 1`] = `"ava is not supported in detox cli tools. You can still run your tests with the runner's own cli tool"`;
exports[`test fails with a different runner 1`] = `
"\\"ava\\" is not supported in Detox CLI tools.

HINT: You can still run your tests with the runner's own CLI tool"
`;
16 changes: 11 additions & 5 deletions detox/local-cli/build.js
@@ -1,27 +1,33 @@
const _ = require('lodash');
const cp = require('child_process');
const log = require('../src/utils/logger').child({ __filename });
const {getDefaultConfiguration, getConfigurationByKey} = require('./utils/configurationUtils');
const {composeDetoxConfig} = require('../src/configuration');

module.exports.command = 'build';
module.exports.desc = "Convenience method. Run the command defined in 'build' property of the specified configuration.";
module.exports.builder = {
C: {
alias: 'config-path',
group: 'Configuration:',
describe: 'Specify Detox config file path. If not supplied, detox searches for .detoxrc[.js] or "detox" section in package.json',
},
c: {
alias: 'configuration',
group: 'Configuration:',
describe:
"Select a device configuration from your defined configurations, if not supplied, and there's only one configuration, detox will default to it",
default: getDefaultConfiguration(),
}
},
};

module.exports.handler = async function build(argv) {
const buildScript = getConfigurationByKey(argv.configuration).build;
const { errorBuilder, deviceConfig } = await composeDetoxConfig({ argv });

const buildScript = deviceConfig.build;

if (buildScript) {
log.info(buildScript);
cp.execSync(buildScript, { stdio: 'inherit' });
} else {
throw new Error(`Could not find build script in detox.configurations["${argv.configuration}"].build`);
throw errorBuilder.missingBuildScript();
}
};
101 changes: 31 additions & 70 deletions detox/local-cli/build.test.js
@@ -1,85 +1,46 @@
jest.mock('child_process');
jest.mock('../src/utils/logger');
jest.mock('../src/configuration');

describe('build', () => {
let mockExec;
beforeEach(() => {
mockExec = jest.fn();
jest.mock('child_process', () => ({
execSync: mockExec
}));
});
const DetoxConfigErrorBuilder = require('../src/errors/DetoxConfigErrorBuilder');

it('runs the build script if there is only one config', async () => {
mockPackageJson({
configurations: {
only: {
build: 'echo "only"'
}
}
});
describe('build', () => {
let execSync, composeDetoxConfig, detoxConfig;

await callCli('./build', 'build');
expect(mockExec).toHaveBeenCalledWith(expect.stringContaining('only'), expect.anything());
beforeEach(() => {
detoxConfig = {
artifactsConfig: {},
behaviorConfig: {},
deviceConfig: {},
sessionConfig: {},
errorBuilder: new DetoxConfigErrorBuilder(),
};

execSync = require('child_process').execSync;
composeDetoxConfig = require('../src/configuration').composeDetoxConfig;
composeDetoxConfig.mockReturnValue(Promise.resolve(detoxConfig));
});

it('runs the build script of selected config', async () => {
mockPackageJson({
configurations: {
only: {
build: 'echo "only"'
},
myconf: {
build: 'echo "myconf"'
}
}
});

await callCli('./build', 'build -c myconf');
expect(mockExec).toHaveBeenCalledWith(expect.stringContaining('myconf'), expect.anything());
});
it('passes argv to composeConfig', async () => {
await callCli('./build', 'build -C /etc/.detoxrc.js -c myconf').catch(() => {});

it('fails with multiple configs if none is selected', async () => {
mockPackageJson({
configurations: {
only: {
build: 'echo "only"'
},
myconf: {
build: 'echo "myconf"'
}
}
expect(composeDetoxConfig).toHaveBeenCalledWith({
argv: expect.objectContaining({
'config-path': '/etc/.detoxrc.js',
'configuration': 'myconf',
}),
});

await expect(callCli('./build', 'build')).rejects.toThrowErrorMatchingSnapshot();
expect(mockExec).not.toHaveBeenCalled();
});

it('fails without configurations', async () => {
mockPackageJson({});
it('runs the build script from the composed device config', async () => {
detoxConfig.deviceConfig.build = 'yet another command';

await expect(callCli('./build', 'build')).rejects.toThrowErrorMatchingSnapshot();
expect(mockExec).not.toHaveBeenCalled();
});

it('fails without build script', async () => {
mockPackageJson({
configurations: {
only: {}
}
});

await expect(callCli('./build', 'build -c only')).rejects.toThrowErrorMatchingSnapshot();
expect(mockExec).not.toHaveBeenCalled();
await callCli('./build', 'build');
expect(execSync).toHaveBeenCalledWith('yet another command', expect.anything());
});

it('fails without build script and configuration', async () => {
mockPackageJson({
configurations: {
only: {}
}
});

await expect(callCli('./build', 'build')).rejects.toThrowErrorMatchingSnapshot();
expect(mockExec).not.toHaveBeenCalled();
it('fails with an error if a build script has not been found', async () => {
delete detoxConfig.deviceConfig.build;
await expect(callCli('./build', 'build')).rejects.toThrowError(/Could not find a build script/);
});
});
84 changes: 36 additions & 48 deletions detox/local-cli/init.js
Expand Up @@ -24,16 +24,9 @@ module.exports.handler = async function init(argv) {
switch (runner) {
case 'mocha':
createMochaFolderE2E();
patchDetoxConfigInPackageJSON({
runner: 'mocha',
runnerConfig: 'e2e/.mocharc'
});
break;
case 'jest':
createJestFolderE2E();
patchDetoxConfigInPackageJSON({
runner: 'jest',
});
break;
default:
throw new Error([
Expand Down Expand Up @@ -67,6 +60,13 @@ function createFolder(dir, files) {
}

function createFile(filename, content) {
if (fs.existsSync(filename)) {
return reportError(
`Failed to create ${filename} file, ` +
`because it already exists at path: ${path.resolve(filename)}`
);
}

try {
fs.writeFileSync(filename, content);
log.info(`Created a file at path: ${filename}`);
Expand All @@ -77,10 +77,16 @@ function createFile(filename, content) {

function createMochaFolderE2E() {
createFolder('e2e', {
'.mocharc': mochaTemplates.runnerConfig,
'.mocharc.json': mochaTemplates.runnerConfig,
'init.js': mochaTemplates.initjs,
'firstTest.spec.js': mochaTemplates.firstTest
});

createFile('.detoxrc.json', JSON.stringify({
testRunner: 'mocha',
runnerConfig: 'e2e/.mocharc.json',
configurations: createDefaultConfigurations(),
}, null, 2))
}

function createJestFolderE2E() {
Expand All @@ -89,49 +95,31 @@ function createJestFolderE2E() {
'init.js': jestTemplates.initjs,
'firstTest.spec.js': jestTemplates.firstTest
});
}

function parsePackageJson(filepath) {
try {
return require(filepath);
} catch (err) {
reportError(`Failed to parse package.json due to an error:\n${err.message}`);
}
}

function loggedSet(obj, path, value) {
_.set(obj, path, value);

const pathString = path.map(segment => `[${JSON.stringify(segment)}]`).join('');
log.info(` json${pathString} = ${JSON.stringify(value)};`);
createFile('.detoxrc.json', JSON.stringify({
testRunner: 'jest',
runnerConfig: 'e2e/config.json',
configurations: createDefaultConfigurations(),
}, null, 2))
}

function savePackageJson(filepath, json) {
try {
fs.writeFileSync(filepath, JSON.stringify(json, null, 2) + '\n');
} catch (err) {
reportError(`Failed to write changes back into package.json due to an error:\n${err.message}`);
}
}

function patchDetoxConfigInPackageJSON({ runner, runnerConfig }) {
const packageJsonPath = path.join(process.cwd(), 'package.json');

if (fs.existsSync(packageJsonPath)) {
log.info(`Patching package.json at path: ${packageJsonPath}`);

const packageJson = parsePackageJson(packageJsonPath);
if (packageJson) {
loggedSet(packageJson, ['detox', 'test-runner'], runner);
if (runnerConfig) {
loggedSet(packageJson, ['detox', 'runner-config'], runnerConfig);
}

savePackageJson(packageJsonPath, packageJson);
}
} else {
reportError(`Failed to find package.json at path: ${packageJsonPath}`);
}
function createDefaultConfigurations() {
return {
ios: {
type: 'ios.simulator',
binaryPath: 'SPECIFY_PATH_TO_YOUR_APP_BINARY',
device: {
type: 'iPhone 11',
},
},
android: {
type: 'android.emulator',
binaryPath: 'SPECIFY_PATH_TO_YOUR_APP_BINARY',
device: {
avdName: 'Pixel_2_API_29',
},
},
};
}

function reportError(...args) {
Expand Down
3 changes: 1 addition & 2 deletions detox/local-cli/templates/jest.js
Expand Up @@ -8,7 +8,6 @@ const runnerConfig = `{
`;

const initjsContent = `const detox = require('detox');
const config = require('../package.json').detox;
const adapter = require('detox/runners/jest/adapter');
const specReporter = require('detox/runners/jest/specReporter');

Expand All @@ -22,7 +21,7 @@ jasmine.getEnv().addReporter(adapter);
jasmine.getEnv().addReporter(specReporter);

beforeAll(async () => {
await detox.init(config);
await detox.init();
}, 300000);

beforeEach(async () => {
Expand Down
3 changes: 1 addition & 2 deletions detox/local-cli/templates/mocha.js
Expand Up @@ -8,11 +8,10 @@ const mochaRcContent = JSON.stringify({
}, null, 4);

const initjsContent = `const detox = require('detox');
const config = require('../package.json').detox;
const adapter = require('detox/runners/mocha/adapter');

before(async () => {
await detox.init(config);
await detox.init();
});

beforeEach(async function () {
Expand Down