nightwatch
Version:
Easy to use Node.js based end-to-end testing solution for web applications using the W3C WebDriver API.
399 lines (342 loc) • 10.2 kB
JavaScript
const path = require('path');
const fs = require('fs');
const Utils = require('../utils');
const StaticApis = require('./_loaders/static.js');
const CommandLoader = require('./_loaders/command.js');
const ElementCommandLoader = require('./_loaders/element-command.js');
const AssertionLoader = require('./_loaders/assertion.js');
const ExpectLoader = require('./_loaders/expect.js');
const PageObjectLoader = require('./_loaders/page-object.js');
const PluginLoader = require('./_loaders/plugin.js');
const __elementCommands = [];
class ApiLoader {
static isElementCommand(commandName) {
return __elementCommands.includes(commandName);
}
static get CommandFiles() {
return {
protocolActions: {
dirPath: 'protocol',
protocol: true
},
clientCommands: 'client-commands',
elementCommands: {
dirPath: 'element-commands',
element: true,
loader: ElementCommandLoader
},
assertions: {
loader: AssertionLoader,
dirPath: 'assertions',
element: true,
namespaces: {
assert: {
abortOnFailure: true
},
verify: {
abortOnFailure: false
}
}
},
expect: {
loader: ExpectLoader,
dirPath: 'expect',
element: true,
namespaces: {
expect: {
abortOnFailure: true
},
should: {
abortOnFailure: false
}
}
}
};
}
/**
* @param {*} customPath
* @return {Array}
*/
static adaptCustomPath(customPath) {
return customPath.map(location => {
return path.resolve(location);
});
}
static init(nightwatchInstance) {
const api = new ApiLoader(nightwatchInstance);
const staticApis = new StaticApis(nightwatchInstance);
staticApis.loadStaticAssertions();
staticApis.loadStaticExpect();
api.loadApiCommandsSync();
api.loadCustomCommandsSync();
api.loadCustomAssertionsSync();
api.loadPlugins();
api.loadPageObjectsSync();
if (!nightwatchInstance.unitTestingMode) {
const EnsureApi = require('./_loaders/ensure.js');
const ensureApi = new EnsureApi(nightwatchInstance);
ensureApi.loadAssertions();
const ChromeApi = require('./_loaders/chrome.js');
const chromeApis = new ChromeApi(nightwatchInstance);
chromeApis.loadCommands();
const FirefoxApi = require('./_loaders/firefox.js');
const firefoxApis = new FirefoxApi(nightwatchInstance);
firefoxApis.loadCommands();
}
}
constructor(nightwatchInstance) {
this.nightwatchInstance = nightwatchInstance;
}
get api() {
return this.nightwatchInstance.api;
}
get reporter() {
return this.nightwatchInstance.reporter;
}
get settings() {
return this.nightwatchInstance.settings;
}
get commandQueue() {
return this.nightwatchInstance.queue;
}
static makeAssertProxy(api) {
return new Proxy(api, {
get(target, name) {
if (name === 'not') {
return new Proxy(api, {
get(target, name) {
return function (...args) {
if (!Utils.isFunction(target[name])) {
throw new Error(`Unknown api method .not."${name}".`);
}
return target[name]({negate: true, args});
};
}
});
}
return function (...args) {
if (typeof name != 'string') {
return null;
}
if (!Utils.isFunction(target[name])) {
throw new Error(`Unknown api method "${name}".`);
}
return target[name]({negate: false, args});
};
}
});
}
loadPlugins(parent = null) {
const plugins = this.nightwatchInstance.options.plugins || [];
plugins.forEach(pluginName => {
const instance = new PluginLoader(this.nightwatchInstance);
const plugin = instance.load(pluginName);
if (plugin.commands) {
this.__loadCustomObjectsSync({
parent,
sourcePath: plugin.commands,
isUserDefined: true
});
}
if (plugin.assertions) {
this.__loadCustomObjectsSync({
parent,
sourcePath: plugin.assertions,
namespaces: ApiLoader.CommandFiles.assertions.namespaces,
Loader: AssertionLoader
});
}
});
}
/**
* Loads the page objects, if defined
* @param parent
*/
loadPageObjectsSync(parent = null) {
this.__loadCustomObjectsSync({
parent,
sourcePath: this.nightwatchInstance.options.page_objects_path,
isUserDefined: true,
Loader: PageObjectLoader
});
}
/**
* Loads custom assertions, if defined
*
* @param parent
*/
loadCustomAssertionsSync(parent = null) {
this.__loadCustomObjectsSync({
parent,
sourcePath: this.nightwatchInstance.options.custom_assertions_path,
namespaces: ApiLoader.CommandFiles.assertions.namespaces,
Loader: AssertionLoader
});
}
/**
* Loads custom commands, if defined
* @param parent
*/
loadCustomCommandsSync(parent = null) {
this.__loadCustomObjectsSync({
parent,
sourcePath: this.nightwatchInstance.options.custom_commands_path,
isUserDefined: true
});
}
/**
* Loads built-in api commands, assertions, and expect definitions
* @param parent
*/
loadApiCommandsSync(parent = null) {
Object.keys(ApiLoader.CommandFiles)
.forEach((key) => {
const commands = ApiLoader.CommandFiles[key];
const {namespaces} = commands;
let dirPath;
let Loader = CommandLoader;
let isProtocolCommand = false;
let isElementCommand = false;
if (Utils.isString(commands)) {
dirPath = path.join(__dirname, commands);
} else {
dirPath = path.join(__dirname, commands.dirPath);
isProtocolCommand = commands.protocol;
isElementCommand = commands.element;
if (commands.loader) {
Loader = commands.loader;
}
}
const opts = {
dirPath,
parent,
Loader,
isProtocolCommand,
isElementCommand,
namespacesObj: namespaces
};
// protocol commands are not loaded onto the main page object
if (parent && isProtocolCommand) {
return;
}
this.__loadCommandsSync(opts);
});
}
addCommandDefinitionSync({
dirPath,
isUserDefined = false,
fileName,
parent,
namespace = [],
abortOnFailure = false,
isProtocolCommand = false,
isElementCommand = false,
Loader = CommandLoader
}) {
const loader = new Loader(this.nightwatchInstance);
loader.isUserDefined = isUserDefined;
if (namespace && namespace.length) {
loader.setNamespace(namespace);
}
loader
.loadModule(dirPath, fileName)
.createWrapper(abortOnFailure)
.define(parent);
if (isElementCommand && loader.commandName && !__elementCommands.includes(loader.commandName)) {
__elementCommands.push(loader.commandName);
}
}
__loadCustomObjectsSync({parent = null, Loader = CommandLoader, namespaces, sourcePath}) {
if (!sourcePath) {
sourcePath = [];
}
if (!Array.isArray(sourcePath)) {
sourcePath = [sourcePath];
}
const dirPathArr = ApiLoader.adaptCustomPath(sourcePath);
const commandFiles = [];
dirPathArr.forEach((dirPath, index) => this.__loadCommandsSync({
parent,
dirPath,
originalSourcePath: sourcePath[index],
isUserDefined: true,
namespacesObj: namespaces,
Loader,
loadAction(opts) {
commandFiles.push(opts);
}
}));
const hasCorrespondingJsFile = (parsedResource) => {
const jsFile = path.join(parsedResource.dir, `${parsedResource.name}${Utils.jsFileExt}`);
const entry = commandFiles.find(({dirPath, fileName}) => {
return path.join(dirPath, fileName) === jsFile;
});
return !!entry;
};
const commandFilesProcessed = commandFiles.reduce((prev, opts) => {
const {dirPath, fileName} = opts;
const fullPath = path.join(dirPath, fileName);
const parsedResource = path.parse(fullPath);
const isTypeScriptFile = parsedResource.ext === Utils.tsFileExt;
// ignore `.ts` files if a `.js` has already been loaded
if (isTypeScriptFile && hasCorrespondingJsFile(parsedResource)) {
return prev;
}
prev.push(opts);
return prev;
}, []);
commandFilesProcessed.forEach(opts => this.addCommandDefinitionSync(opts));
}
__loadCommandsSync({
parent = null,
namespacesObj = null,
ns = null,
dirPath,
originalSourcePath,
abortOnFailure = false,
isProtocolCommand = false,
isElementCommand = false,
isUserDefined = false,
Loader = CommandLoader,
loadAction = this.addCommandDefinitionSync.bind(this)
}) {
if (namespacesObj) {
Object.keys(namespacesObj).forEach((ns) => this.__loadCommandsSync({
ns,
parent,
dirPath,
isUserDefined,
isElementCommand,
Loader,
abortOnFailure: namespacesObj[ns].abortOnFailure
}));
} else {
Utils.readFolderRecursively(dirPath, [], (dirPath, fileName, namespace) => {
loadAction.call(this, {
dirPath,
fileName,
parent,
isUserDefined,
namespace: ns || namespace,
isProtocolCommand,
isElementCommand,
Loader,
abortOnFailure
});
}, function readDirFn(sourcePath) {
try {
return {
resources: fs.readdirSync(sourcePath),
sourcePath
};
} catch (err) {
if (err.code === 'ENOENT' && originalSourcePath && originalSourcePath.startsWith('examples/')) {
return readDirFn(path.join('node_modules/nightwatch/', originalSourcePath));
}
throw err;
}
});
}
}
}
module.exports = ApiLoader;