nightwatch
Version:
Easy to use Node.js based end-to-end testing solution for web applications using the W3C WebDriver API.
694 lines (555 loc) • 17.4 kB
JavaScript
const path = require('path');
const EventEmitter = require('events');
const Utils = require('../utils');
const ExportsInterface = require('./interfaces/exports.js');
const DescribeInterface = require('./interfaces/describe.js');
class Context extends EventEmitter {
static get REPORT_KEY_SEPARATOR() {
return path.sep;
}
get isTestHook() {
return false;
}
constructor({modulePath, settings, argv = {}, attributes = {}}) {
super();
this.settings = settings;
this.argv = argv;
this.__module = null;
this.__testsuite = {};
this.__currentRunnable = null;
this.__retries = {
testcase: null,
suite: null
};
this.attributes = attributes;
this.groupName = '';
if (modulePath) {
this.setModulePath(modulePath);
}
}
setModulePath(file) {
this.modulePath = file;
this.__moduleName = path.parse(this.modulePath).name;
this.moduleKey = this.moduleName || '';
}
loadTags({usingMocha = false} = {}) {
if (!usingMocha) {
return;
}
const context = this;
const {Suite} = require('mocha');
class BasicSuite extends Suite {
get tags() {
return context.attributes['@tags'];
}
set tags(value) {
context.attributes['@tags'] = value;
}
}
global.describe = function(title, definitionFn) {
const instance = new BasicSuite(title);
definitionFn.call(instance);
};
try {
this.__module = this.requireModule(this.modulePath); // eslint-disable-next-line no-empty
} catch (err) {}
}
async init({usingMocha = false, suiteTitle = null, client = null} = {}) {
this.__currentTestName = this.argv.testcase;
this.__testSuiteName = suiteTitle;
this.__hooks = [];
this.__testcases = [];
this.__skippedTestCases = [];
this.__allScreenedTests = [];
this.__contextBinding = {};
this.__transforms = client ? await client.transforms : [];
this.source = this.argv._source || [];
if (!usingMocha) {
this.createInterface(client);
await this.loadModule();
}
this.createTestSuite();
return this;
}
get testSuiteName() {
return this.__testSuiteName;
}
get currentTest() {
return this.__currentTestName;
}
get contextBinding() {
return this.__contextBinding;
}
/**
* @returns {boolean}
*/
get unitTestingMode() {
return this.settings.unit_testing_mode || this.isUnitTest();
}
/**
* @deprecated
* @returns {boolean}
*/
get unitTestsMode() {
return this.settings.unit_tests_mode || this.isUnitTest();
}
get moduleName() {
return this.__moduleName;
}
get module() {
return this.__module;
}
get testsuite() {
return this.__testsuite;
}
get tests() {
return this.__testcases;
}
set tests(value) {
this.__testcases = value;
}
get skippedTests() {
return this.__skippedTestCases;
}
set skippedTests(value) {
this.__skippedTestCases = value;
}
get allScreenedTests() {
return this.__allScreenedTests;
}
set allScreenedTests(value) {
this.__allScreenedTests = value;
}
get hooks() {
return this.__hooks;
}
get currentRunnable() {
return this.__currentRunnable;
}
get queue() {
return this.currentRunnable && this.currentRunnable.queue;
}
get queueStarted() {
return this.queue && this.queue.started;
}
get retries() {
return this.__retries;
}
setReloadModuleCache(val = true) {
this.__reloadModuleCache = val;
return this;
}
shouldReloadModuleCache() {
return this.__reloadModuleCache;
}
get usingBddDescribe() {
return this.bddInterface && Utils.isFunction(this.bddInterface.describeFn);
}
getName() {
return this.moduleName;
}
getSuiteName() {
return this.testSuiteName || Utils.getTestSuiteName(this.moduleKey);
}
setCurrentRunnable(runnable) {
this.__currentRunnable = runnable;
return this;
}
createInterface(client) {
this.exportsInterface = new ExportsInterface(this);
this.bddInterface = new DescribeInterface(this, client);
return this;
}
async loadModule() {
this.emit('pre-require', global);
this.__module = await this.requireModule();
if (!this.module && !this.bddInterface.describeFn) {
throw new Error(`Empty module provided in: "${this.modulePath}".`);
}
if (this.module) {
this.__testsuite = Object.assign(this.__testsuite, this.module);
}
this.emit('post-require');
this.emit('module-loaded');
return this;
}
async requireModule(loadJsWithPlugins = false) {
const pluginDescriptor = this.__transforms.find(transform => {
const {filter} = transform;
if (Utils.isFunction(filter)) {
return filter(this.modulePath, loadJsWithPlugins);
}
return filter.test(this.modulePath);
});
if (pluginDescriptor) {
try {
const testContext = await pluginDescriptor.requireTest(this.modulePath, pluginDescriptor, {
argv: this.argv,
nightwatch_settings: this.settings
});
if (testContext && testContext.initialize) {
return testContext;
}
return {};
} catch (err) {
const error = new Error(`Error while trying to load ${this.modulePath}`);
error.detailedErr = err.message;
error.stack = err.stack;
throw error;
}
}
try {
return Utils.requireModule(this.modulePath);
} catch (err) {
if ((err instanceof SyntaxError) && !loadJsWithPlugins) {
return await this.requireModule(true);
}
throw err;
}
}
createTestSuite() {
if (this.currentTest && this.tests.length === 0) {
throw new Error(`"${this.currentTest}" is not a valid testcase in the current test suite.`);
}
if (this.currentTest && this.tests.length > 1) {
this.tests = [this.currentTest];
}
this.__moduleKeysCopy = this.tests.slice(0);
}
addTestSuiteMethod(testName, testFn, context) {
this.testsuite[testName] = testFn;
this.contextBinding[testName] = context || this.module;
}
/**
* Add test hooks created by describe interface
*
* @param {string} hookName
* @param {Function} hookFn
* @param {Object} [context]
*/
addTestHook(hookName, hookFn, context) {
// TODO: warn if hook name already exists
this.hooks.push(hookName);
this.addTestSuiteMethod(hookName, hookFn, context);
}
/**
* Add testcases created by describe interface
*
* @param {string} testName
* @param {function} testFn
* @param {Object} [describeInstance] The instance of the describe function declaration
* @param {Boolean=false} [runOnly] If the runner should run only this testcase
* @param {Boolean=false} [skipTest] If the testcase should be skipped
*/
addTestCase({testName, testFn, describeInstance, runOnly, skipTest}) {
if (!Utils.isFunction(testFn)) {
throw new Error(`The "${testName}" test script must be a function. "${typeof testFn}" given.`);
}
if (this.allScreenedTests.includes(testName)) {
const {Logger} = Utils;
const err = new Error(
'An error occurred while loading the testsuite:\n' +
`A testcase with name "${testName}" already exists. Testcases must have unique names inside the test suite, ` +
'otherwise testcases with duplicate names might not run at all.\n\n' +
'This testsuite has been disabled, please fix the error to run it again properly.'
);
Logger.error(err);
this.setAttribute('@disabled', true);
}
if (!skipTest) {
this.tests.push(testName);
} else {
this.skippedTests.push(testName);
}
this.addTestSuiteMethod(testName, testFn, describeInstance);
if (runOnly) {
this.__currentTestName = testName;
this.skippedTests = [...this.allScreenedTests];
this.runOnly = true;
} else if (this.runOnly) {
this.skippedTests.push(testName);
}
this.allScreenedTests.push(testName);
}
/**
* Create a testsuite using describe interface
*
* @param {string} describeTitle
* @param {Object} describeInstance The instance of the describe function declaration
* @param {Boolean=false} [runOnly] If the runner should run only this testsuite
*/
setDescribeContext({describeTitle, describeInstance, runOnly}) {
// if `setDescribeContext` is called twice for the same test suite,
// that would mean that there are two `describe()`s in same test suite.
if (this.__testSuiteName && describeInstance) {
// eslint-disable-next-line no-console
console.warn(
'Nightwatch does not support more than one "describe" declarations in a single testsuite.' +
' Using this might give unexpected results.'
);
}
if (runOnly) {
// eslint-disable-next-line no-console
console.warn('describe.only() is not supported at the moment.');
}
this.__testSuiteName = describeTitle;
}
setTestcaseRetries(n) {
this.retries.testcase = n;
}
setSuiteRetries(n) {
this.retries.suite = n;
}
////////////////////////////////////////////////////////////////
// Attributes
////////////////////////////////////////////////////////////////
isES6Async(testName) {
return Utils.isES6AsyncFn(this.testsuite[testName]);
}
addAttributes(attributes = {}) {
Object.assign(this.attributes, attributes);
}
setAttribute(name, value) {
this.attributes[name] = value;
}
getAttribute(name) {
return this.attributes[name];
}
isDisabled() {
return this.attributes['@disabled'] === true;
}
isUnitTest() {
return this.attributes['@unitTest'] === true;
}
getSkipTestcasesOnFail() {
return this.attributes['@skipTestcasesOnFail'];
}
getEndSessionOnFail() {
return this.attributes['@endSessionOnFail'];
}
getNameAttr() {
return this.attributes['@name'];
}
getTags() {
return this.attributes['@tags'];
}
getDesiredCapabilities() {
return this.attributes['@desiredCapabilities'];
}
////////////////////////////////////////////////////////////////
// Module calls
////////////////////////////////////////////////////////////////
/**
*
* @param {function} done
* @param {Object} api
* @param {Number} expectedArgs
* @return []
*/
getHookFnArgs(done, api, expectedArgs) {
if (this.unitTestsMode) {
return [done];
}
const args = [api];
if (expectedArgs === 2) {
args.push(done);
}
return args;
}
extendContextWithApi(context, api) {
if (('client' in context) && this.modulePath &&
context.client && !('currentTest' in context.client)) {
throw new Error('There is already a .client property defined in: ' + this.modulePath);
}
context.client = api;
}
/**
*
* @param {string} fnName
* @param {Object} client
* @param {Number} expectedArgs
* @param {function} done
* @return {*}
*/
invokeMethod(fnName, client, expectedArgs, done) {
const isES6Async = this.isES6Async(fnName);
const isTestHook = this.isTestHook;
if (client) {
client.isES6AsyncTestcase = isES6Async;
client.isES6AsyncTestHook = isTestHook ? isES6Async : undefined;
}
const api = client && client.api || null;
let context;
if (this.contextBinding && this.contextBinding[fnName]) {
context = this.contextBinding[fnName];
} else {
context = this.__module;
}
this.extendContextWithApi(context, api);
switch (expectedArgs) {
case 2:
case 1:
return this.callAsync({fnName, api, expectedArgs, done, context, isES6Async, isTestHook});
case 0: {
try {
const result = this.call(fnName);
if (!(result instanceof Promise)) {
return done();
}
result.then(() => {
done();
}).catch(err => {
done(err);
});
} catch (err) {
done(err);
}
}
}
}
/**
* @param {string} fnName
* @param {Array} args
*/
call(fnName, ...args) {
const context = this.contextBinding[fnName];
const client = args[0];
if (client) {
const isES6Async = this.isES6Async(fnName);
client.isES6AsyncTestcase = isES6Async;
client.isES6AsyncTestHook = this.isTestHook ? isES6Async : undefined;
args[0] = client.api;
}
const result = this.testsuite[fnName].apply(context, args);
if (this.currentRunnable) {
this.currentRunnable.currentTestCaseResult = result;
}
return result;
}
/**
*
* @param {string} fnName
* @param {Object} api
* @param {Number} expectedArgs
* @param {function} done
* @param {Object} context
* @return {*}
*/
callAsync({fnName, api, expectedArgs = 2, done = function() {}, context}) {
const fnAsync = Utils.makeFnAsync(expectedArgs, this.testsuite[fnName], context);
const args = this.getHookFnArgs(done, api, expectedArgs);
const result = fnAsync.apply(context, args);
if (this.currentRunnable) {
this.currentRunnable.currentTestCaseResult = result;
}
return result;
}
////////////////////////////////////////////////////////////////
// Module keys
////////////////////////////////////////////////////////////////
hasHook(key) {
return this.hooks.indexOf(key) > -1;
}
getKey(key) {
return this.testsuite[key];
}
getNextKey() {
if (this.tests.length) {
return this.tests.shift();
}
return null;
}
/**
* When using retries, the testcases are reset
*
* @return {Context}
*/
reset() {
this.tests = this.__moduleKeysCopy.slice();
return this;
}
////////////////////////////////////////////////////////////////
// Reporting
////////////////////////////////////////////////////////////////
setReportKey(allModulePaths = []) {
if (!this.modulePath) {
return;
}
let parentFolder = this.modulePath.substring(0, this.modulePath.lastIndexOf(path.sep));
const parentFolderName = parentFolder.split(path.sep).pop();
const srcFolders = this.settings.src_folders || this.source || [];
let diffInFolder = '';
if (srcFolders.length > 0) {
for (let i = 0; i < srcFolders.length; i++) {
const srcPathResolved = path.resolve(srcFolders[i]);
diffInFolder = this.getDiffFromSourceFolder(srcPathResolved, parentFolder, srcFolders);
if (diffInFolder) {
this.moduleKey = [diffInFolder, this.moduleKey].join(Context.REPORT_KEY_SEPARATOR);
this.groupName = parentFolderName;
parentFolder = parentFolder.substring(0, parentFolder.lastIndexOf(path.sep + diffInFolder)); // removing the diffInFolder string from the parent folder
break;
}
}
}
// in case we're using src_folders and there are more than one, prepend the parent folder name to the report key
if (diffInFolder === '' && Array.isArray(this.settings.src_folders) && this.settings.src_folders.length > 1) {
this.moduleKey = [parentFolderName, this.moduleKey].join(Context.REPORT_KEY_SEPARATOR);
}
// in case there are several test files, make sure the report key is unique
if (allModulePaths.length > 1) {
this.moduleKey = this.checkKeyForUniqueness(allModulePaths, parentFolder);
}
}
shouldCheckIfDirectory() {
return !this.settings.src_folders && this.source.length > 1;
}
checkKeyForUniqueness(allModulePaths, parentFolder) {
// removing the current module
const modulePathsCopy = allModulePaths.slice(0);
const index = modulePathsCopy.indexOf(this.modulePath);
if (index > -1) {
modulePathsCopy.splice(index, 1);
}
const modulePathParts = parentFolder.split(path.sep);
return this.getUniqueModuleKey(modulePathsCopy, modulePathParts, this.moduleKey);
}
/**
*
* @param {string} srcPathResolved
* @param {string} moduleParentFolder
* @param {Array} source
*/
getDiffFromSourceFolder(srcPathResolved, moduleParentFolder, source) {
const isDirectory = !this.shouldCheckIfDirectory() || Utils.dirExistsSync(srcPathResolved);
if (!isDirectory) {
return '';
}
if (moduleParentFolder.startsWith(srcPathResolved)) {
return moduleParentFolder.substring(srcPathResolved.length + 1).split(path.sep).join(Context.REPORT_KEY_SEPARATOR);
}
return '';
}
/**
* In case there are multiple sources, compute the moduleKey uniquely
*
* @param {Array} modulePathsCopy
* @param {Array} [modulePathParts]
* @param {string} [moduleKey]
* @return {string}
*/
getUniqueModuleKey(modulePathsCopy, modulePathParts = null, moduleKey = '') {
if (modulePathParts && modulePathParts.length < 2) {
return moduleKey;
}
const isKeyUnique = !modulePathsCopy.some(item => {
const modulePath = path.sep + moduleKey;
return item.endsWith(modulePath + Utils.jsFileExt) || item.endsWith(modulePath + Utils.tsFileExt);
});
if (isKeyUnique) {
return moduleKey;
}
moduleKey = [modulePathParts.pop(), moduleKey].join(Context.REPORT_KEY_SEPARATOR);
return this.getUniqueModuleKey(modulePathsCopy, modulePathParts, moduleKey);
}
}
module.exports = Context;