'use strict';
/**
* Unit test manager. Manages tests for actions/reducers and components. Usually used with action/component manager.
* @module
**/
const _ = require('lodash');
const utils = require('./utils');
const vio = require('./vio');
const refactor = require('./refactor');
const template = require('./template');
/**
* Add the unit test boilerplate file for a component.
* @param {string} feature - The feature name.
* @param {string} name - The component name.
* @param {Object} [args] - The args passed to rekit-core/template.
* @param {string} [args.templateFile=Component.test.js] - The template file to create test file.
* @alias module:test.add
*
* @example
* const test = require('rekit-core').test;
* test.add('home', 'Hello');
*
* // Result => Create the unit test boilerplate file at: 'tests/features/home/Hello.test.js'.
**/
function add(feature, component, args) {
args = args || {};
template.generate(utils.mapComponentTestFile(feature, component), Object.assign({}, args, {
templateFile: args.templateFile || 'Component.test.js',
context: Object.assign({ feature, component }, args.context || {}),
}));
}
/**
* Remove the unit test file for a component.
* @param {string} feature - The feature name.
* @param {string} name - The component name.
* @alias module:test.remove
*
* @example
* const test = require('rekit-core').test;
* test.remove('home', 'Hello');
*
* // Result => Delete the test file of: 'tests/features/home/Hello.test.js'.
**/
function remove(feature, component) {
vio.del(utils.mapComponentTestFile(feature, component));
}
/**
* Move/rename the unit test file for a component.
* @param {ElementArg} source - Which component test to move.
* @param {ElementArg} target - The target location of the component test.
* @alias module:test.move
**/
function move(source, target) {
source.feature = _.kebabCase(source.feature);
source.name = _.pascalCase(source.name);
target.feature = _.kebabCase(target.feature);
target.name = _.pascalCase(target.name);
const srcPath = utils.mapComponentTestFile(source.feature, source.name);
const targetPath = utils.mapComponentTestFile(target.feature, target.name);
vio.move(srcPath, targetPath);
const oldCssClass = `.${_.kebabCase(source.feature)}-${_.kebabCase(source.name)}`;
const newCssClass = `.${_.kebabCase(target.feature)}-${_.kebabCase(target.name)}`;
// Note: below string pattern binds to the test template, update here if template is changed.
// Two styles of imports for component and high order component like page.
const oldImportPath1 = `src/features/${_.kebabCase(source.feature)}`;
const newImportPath1 = `src/features/${_.kebabCase(target.feature)}`;
const oldImportPath2 = `src/features/${_.kebabCase(source.feature)}/${_.pascalCase(source.name)}`;
const newImportPath2 = `src/features/${_.kebabCase(target.feature)}/${_.pascalCase(target.name)}`;
// Try to update describe('xxx')
const oldDescribe = `${_.kebabCase(source.feature)}/${_.pascalCase(source.name)}`;
const newDescribe = `${_.kebabCase(target.feature)}/${_.pascalCase(target.name)}`;
refactor.updateFile(targetPath, ast => [].concat(
refactor.renameImportSpecifier(ast, source.name, target.name),
refactor.renameStringLiteral(ast, oldImportPath1, newImportPath1),
refactor.renameStringLiteral(ast, oldImportPath2, newImportPath2),
refactor.renameStringLiteral(ast, oldDescribe, newDescribe),
refactor.renameStringLiteral(ast, oldCssClass, newCssClass)
));
}
/**
* Add the unit test boilerplate file for an action (using different templates for sync or async).
* @param {string} feature - The feature name.
* @param {string} name - The action name.
* @param {Object} [args] - The args passed to rekit-core/template.
* @param {string} [args.templateFile=Component.test.js] - The template file to create test file.
* @param {string} [args.isAsync=false] - Whether the action is async.
* @alias module:test.addAction
*
* @example
* const test = require('rekit-core').test;
* test.addAction('home', 'doSomething', { isAsync: true });
*
* // Result => Create the unit test boilerplate file at: 'tests/features/home/redux/doSomething.test.js'.
**/
function addAction(feature, name, args) {
args = args || {};
const context = {
feature,
action: name,
};
if (args.isAsync) {
context.actionTypes = utils.getAsyncActionTypes(feature, name);
} else {
context.actionType = args.actionType || utils.getActionType(feature, name);
}
template.generate(utils.mapReduxTestFile(feature, name), Object.assign({}, args, {
templateFile: args.templateFile || (args.isAsync ? 'redux/async_action.test.js' : 'redux/action.test.js'),
context: Object.assign(context, args.context || {}),
}));
}
/**
* Remove the unit test file for an action (either sync or async).
* @param {string} feature - The feature name.
* @param {string} name - The action name.
* @alias module:test.removeAction
*
**/
function removeAction(feature, name) {
vio.del(utils.mapReduxTestFile(feature, name));
}
/**
* Move/rename the unit test file for an action.
* @param {ElementArg} source - Which action test to move.
* @param {ElementArg} target - The target location of the action test.
* @param {Object} [args] - Only used to indicates if it's async.
* @param {Object} [args.isAsync=false] - Whether it's an async action.
* @alias module:test.moveAction
**/
function moveAction(source, target, args) {
args = args || {};
source.feature = _.kebabCase(source.feature);
source.name = _.camelCase(source.name);
target.feature = _.kebabCase(target.feature);
target.name = _.camelCase(target.name);
const srcPath = utils.mapReduxTestFile(source.feature, source.name);
const targetPath = utils.mapReduxTestFile(target.feature, target.name);
vio.move(srcPath, targetPath);
// Note: below string pattern binds to the test template, update here if template is changed.
// For action/reducer import
const oldImportPath1 = `src/features/${source.feature}/redux/${source.name}`;
const newImportPath1 = `src/features/${target.feature}/redux/${target.name}`;
// For constant import
const oldImportPath2 = `src/features/${source.feature}/redux/constants`;
const newImportPath2 = `src/features/${target.feature}/redux/constants`;
// Try to update describe('xxx')
const oldDescribe = `${source.feature}/redux/${source.name}`;
const newDescribe = `${target.feature}/redux/${target.name}`;
const ast = vio.getAst(targetPath);
let changes = [].concat(
refactor.renameImportSpecifier(ast, source.name, target.name),
refactor.renameStringLiteral(ast, oldImportPath1, newImportPath1),
refactor.renameStringLiteral(ast, oldImportPath2, newImportPath2),
refactor.renameStringLiteral(ast, oldDescribe, newDescribe)
);
if (args.isAsync) {
const oldActionTypes = utils.getAsyncActionTypes(source.feature, source.name);
const newActionTypes = utils.getAsyncActionTypes(target.feature, target.name);
const oldIt1 = `dispatches success action when ${_.camelCase(source.name)} succeeds`;
const newIt1 = `dispatches success action when ${_.camelCase(target.name)} succeeds`;
const oldIt2 = `dispatches failure action when ${_.camelCase(source.name)} fails`;
const newIt2 = `dispatches failure action when ${_.camelCase(target.name)} fails`;
const oldIt3 = `returns correct action by dismiss${_.pascalCase(source.name)}Error`;
const newIt3 = `returns correct action by dismiss${_.pascalCase(target.name)}Error`;
const oldIt4 = `handles action type ${oldActionTypes.begin} correctly`;
const newIt4 = `handles action type ${newActionTypes.begin} correctly`;
const oldIt5 = `handles action type ${oldActionTypes.success} correctly`;
const newIt5 = `handles action type ${newActionTypes.success} correctly`;
const oldIt6 = `handles action type ${oldActionTypes.failure} correctly`;
const newIt6 = `handles action type ${newActionTypes.failure} correctly`;
const oldIt7 = `handles action type ${oldActionTypes.dismissError} correctly`;
const newIt7 = `handles action type ${newActionTypes.dismissError} correctly`;
changes = changes.concat(
refactor.renameImportSpecifier(ast, `dismiss${_.pascalCase(source.name)}Error`, `dismiss${_.pascalCase(target.name)}Error`),
refactor.renameImportSpecifier(ast, `${oldActionTypes.begin}`, `${newActionTypes.begin}`),
refactor.renameImportSpecifier(ast, `${oldActionTypes.success}`, `${newActionTypes.success}`),
refactor.renameImportSpecifier(ast, `${oldActionTypes.failure}`, `${newActionTypes.failure}`),
refactor.renameImportSpecifier(ast, `${oldActionTypes.dismissError}`, `${newActionTypes.dismissError}`),
refactor.renameStringLiteral(ast, oldIt1, newIt1),
refactor.renameStringLiteral(ast, oldIt2, newIt2),
refactor.renameStringLiteral(ast, oldIt3, newIt3),
refactor.renameStringLiteral(ast, oldIt4, newIt4),
refactor.renameStringLiteral(ast, oldIt5, newIt5),
refactor.renameStringLiteral(ast, oldIt6, newIt6),
refactor.renameStringLiteral(ast, oldIt7, newIt7)
);
} else {
// Try to update it('xxx') for sync action, bound to the templates
const oldActionType = utils.getActionType(source.feature, source.name);
const newActionType = utils.getActionType(target.feature, target.name);
const oldIt1 = `returns correct action by ${source.name}`;
const newIt1 = `returns correct action by ${target.name}`;
const oldIt2 = `handles action type ${oldActionType} correctly`;
const newIt2 = `handles action type ${newActionType} correctly`;
changes = changes.concat(
refactor.renameStringLiteral(ast, oldIt1, newIt1),
refactor.renameStringLiteral(ast, oldIt2, newIt2),
refactor.renameImportSpecifier(ast, oldActionType, newActionType)
);
}
let code = vio.getContent(targetPath);
code = refactor.updateSourceCode(code, changes);
vio.save(targetPath, code);
}
module.exports = {
add,
remove,
move,
addAction,
removeAction,
moveAction,
};