Skip to content

Commit

Permalink
code: improve error messages and project examples
Browse files Browse the repository at this point in the history
  • Loading branch information
noomorph committed May 22, 2020
1 parent bb636e3 commit d221153
Show file tree
Hide file tree
Showing 32 changed files with 160 additions and 150 deletions.
8 changes: 7 additions & 1 deletion detox/local-cli/__snapshots__/build.test.js.snap
@@ -1,3 +1,9 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`build fails with an error if a build script has not been found 1`] = `"Could not find build script for \\"testConfig\\" in Detox config at: /etc/detox/config"`;
exports[`build fails with an error if a build script has not been found 1`] = `
"Could not find a build script inside \\"testConfig\\" configuration.
HINT: Check contents of your Detox config at: /etc/detox/config
{}"
`;
7 changes: 6 additions & 1 deletion detox/local-cli/build.js
Expand Up @@ -2,6 +2,7 @@ const _ = require('lodash');
const cp = require('child_process');
const log = require('../src/utils/logger').child({ __filename });
const {composeDetoxConfig} = require('../src/configuration');
const DetoxConfigError = require('../src/errors/DetoxConfigError');

module.exports.command = 'build';
module.exports.desc = "Convenience method. Run the command defined in 'build' property of the specified configuration.";
Expand All @@ -28,6 +29,10 @@ module.exports.handler = async function build(argv) {
log.info(buildScript);
cp.execSync(buildScript, { stdio: 'inherit' });
} else {
throw new Error(`Could not find build script for "${meta.configuration}" in Detox config at: ${meta.location}`);
throw new DetoxConfigError({
message: `Could not find a build script inside "${meta.configuration}" configuration.`,
hint: meta.location && `Check contents of your Detox config at: ${meta.location}`,
debugInfo: deviceConfig,
});
}
};
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
4 changes: 2 additions & 2 deletions detox/local-cli/test.test.js
Expand Up @@ -60,13 +60,13 @@ describe('test', () => {

it('changes --opts to --config, when given non ".opts" file extension', async () => {
mockAndroidMochaConfiguration({
'runner-config': 'e2e/.mocharc'
'runner-config': 'e2e/.mocharc.json'
});

await callCli('./test', 'test');

expect(execSync).toHaveBeenCalledWith(
expect.stringContaining(`${normalize('node_modules/.bin/mocha')} --config e2e/.mocharc `),
expect.stringContaining(`${normalize('node_modules/.bin/mocha')} --config e2e/.mocharc.json `),
expect.anything()
);
});
Expand Down
19 changes: 5 additions & 14 deletions detox/src/configuration/composeDeviceConfig.js
@@ -1,23 +1,22 @@
const _ = require('lodash');
const util = require('util');
const DetoxRuntimeError = require('../errors/DetoxRuntimeError');
const DetoxConfigError = require('../errors/DetoxConfigError');

function composeDeviceConfig({ configurationName, rawDeviceConfig, cliConfig }) {
if (!rawDeviceConfig.type) {
throw new DetoxRuntimeError({
throw new DetoxConfigError({
message: `Missing "type" inside detox.configurations["${configurationName}"]`,
hint: `Usually, 'type' property should hold the device type to test on (e.g. "ios.simulator" or "android.emulator").\nCheck again:`,
debugInfo: inspectObj(rawDeviceConfig),
debugInfo: rawDeviceConfig,
});
}

const device = cliConfig.deviceName || rawDeviceConfig.device || rawDeviceConfig.name;

if (_.isEmpty(device)) {
throw new DetoxRuntimeError({
throw new DetoxConfigError({
message: `'device' property is empty in detox.configurations["${configurationName}"]`,
hint: `It should hold the device query to run on (e.g. { "type": "iPhone 11 Pro" }, { "avdName": "Nexus_5X_API_29" }).\nCheck again:`,
debugInfo: inspectObj(rawDeviceConfig),
debugInfo: rawDeviceConfig,
});
}

Expand All @@ -27,13 +26,5 @@ function composeDeviceConfig({ configurationName, rawDeviceConfig, cliConfig })
return rawDeviceConfig;
}

function inspectObj(obj) {
return util.inspect(obj, {
colors: false,
compact: false,
depth: 0,
showHidden: false,
});
}

module.exports = composeDeviceConfig;
7 changes: 4 additions & 3 deletions detox/src/configuration/index.js
@@ -1,6 +1,5 @@
const _ = require('lodash');
const DetoxConfigError = require('../errors/DetoxConfigError');
const DetoxRuntimeError = require('../errors/DetoxRuntimeError');
const collectCliConfig = require('./collectCliConfig');
const loadExternalConfig = require('./loadExternalConfig');
const composeArtifactsConfig = require('./composeArtifactsConfig');
Expand All @@ -26,7 +25,7 @@ async function composeDetoxConfig({
const detoxConfig = _.merge({}, externalConfig, override);

if (_.isEmpty(detoxConfig)) {
throw new DetoxRuntimeError({
throw new DetoxConfigError({
message: 'Cannot start Detox without a configuration',
hint: 'Make sure your package.json has "detox" section, or there\'s .detoxrc file in the working directory',
});
Expand Down Expand Up @@ -85,6 +84,8 @@ module.exports = {
composeDetoxConfig,

throwOnEmptyBinaryPath() {
throw new DetoxConfigError(`'binaryPath' property is missing, should hold the app binary path`);
throw new DetoxConfigError({
message: `'binaryPath' property is missing, should hold the app binary path`,
});
},
};
19 changes: 7 additions & 12 deletions detox/src/configuration/selectConfiguration.js
@@ -1,7 +1,6 @@
const _ = require('lodash');
const util = require('util');

const DetoxRuntimeError = require('../errors/DetoxRuntimeError');
const DetoxConfigError = require('../errors/DetoxConfigError');

function hintConfigurations(configurations) {
return _.keys(configurations).map(c => `* ${c}`).join('\n')
Expand All @@ -11,14 +10,10 @@ function selectConfiguration({ detoxConfig, cliConfig }) {
const { configurations } = detoxConfig;

if (_.isEmpty(configurations)) {
throw new DetoxRuntimeError({
throw new DetoxConfigError({
message: `There are no device configurations in the given Detox config:`,
debugInfo: util.inspect(detoxConfig, {
colors: false,
compact: false,
depth: 1,
showHidden: false,
}),
debugInfo: detoxConfig,
inspectOptions: { depth: 1 },
});
}

Expand All @@ -28,14 +23,14 @@ function selectConfiguration({ detoxConfig, cliConfig }) {
}

if (!configurationName) {
throw new DetoxRuntimeError({
throw new DetoxConfigError({
message: 'Cannot determine which configuration to use.',
hint: 'Use --configuration to choose one of the following:\n' + hintConfigurations(configurations),
});
}

if (!configurations.hasOwnProperty(configurationName)) {
throw new DetoxRuntimeError({
throw new DetoxConfigError({
message: `Failed to find a configuration named "${configurationName}" in Detox config`,
hint: 'Below are the configurations Detox has been able to find:\n' + hintConfigurations(configurations),
});
Expand All @@ -44,4 +39,4 @@ function selectConfiguration({ detoxConfig, cliConfig }) {
return configurationName;
}

module.exports = selectConfiguration;
module.exports = selectConfiguration;
9 changes: 7 additions & 2 deletions detox/src/errors/DetoxConfigError.js
@@ -1,5 +1,10 @@
const CustomError = require('./CustomError');
const DetoxRuntimeError = require('./DetoxRuntimeError');

class DetoxConfigError extends CustomError {}
class DetoxConfigError extends DetoxRuntimeError {
constructor(opts) {
super(opts);
this.name = 'DetoxConfigError';
}
}

module.exports = DetoxConfigError;
21 changes: 19 additions & 2 deletions detox/src/errors/DetoxRuntimeError.js
@@ -1,16 +1,33 @@
const _ = require('lodash');
const util = require('util');

class DetoxRuntimeError extends Error {
constructor({ message = '', hint = '', debugInfo = '' } = {}) {
constructor({
message = '',
hint = '',
debugInfo = '',
inspectOptions,
} = {}) {
const formattedMessage = _.compact([
message,
hint && `HINT: ${hint}`,
debugInfo
_.isObject(debugInfo) ? inspectObj(debugInfo, inspectOptions) : debugInfo,
]).join('\n\n');

super(formattedMessage);
this.name = 'DetoxRuntimeError';
}
}

function inspectObj(obj, options) {
return util.inspect(obj, {
colors: false,
compact: false,
depth: 0,
showHidden: false,

...options,
});
}

module.exports = DetoxRuntimeError;
8 changes: 8 additions & 0 deletions detox/src/errors/DetoxRuntimeError.test.js
Expand Up @@ -30,6 +30,14 @@ describe(DetoxRuntimeError, () => {
suffix: undefined,
}, null, 2),
}),
'message with debug info object': new DetoxRuntimeError({
message: 'no filename was given to constructSafeFilename()',
debugInfo: {
prefix: 'detox - ',
trimmable: undefined,
suffix: undefined,
},
}),
'message with hint and debug info': new DetoxRuntimeError({
message: `Invalid test summary was passed to detox.beforeEach(testSummary)` +
'\nExpected to get an object of type: { title: string; fullName: string; status: "running" | "passed" | "failed"; }',
Expand Down
3 changes: 1 addition & 2 deletions detox/test/e2e/init-circus.js
@@ -1,5 +1,4 @@
const detox = require('detox');
const config = require('../package.json').detox;
const adapter = require('detox/runners/jest/adapter');
const specReporter = require('detox/runners/jest/specReporter');
const assignReporter = require('detox/runners/jest/assignReporter');
Expand All @@ -13,7 +12,7 @@ detoxCircus.getEnv().addEventsListener(specReporter);
jest.setTimeout(timeoutUtils.testTimeout);

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

beforeEach(async () => {
Expand Down

0 comments on commit d221153

Please sign in to comment.