UNPKG

nightwatch

Version:

Easy to use Node.js based end-to-end testing solution for web applications using the W3C WebDriver API.

561 lines (477 loc) 14.7 kB
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 WithinContextLoader = require('./_loaders/within-context.js'); const PluginLoader = require('./_loaders/plugin.js'); const __elementCommandsStrict = []; const __elementCommands = []; const DEFAULT_PLUGINS = ['nightwatch-axe-verbose']; 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 getElementsCommandsStrict() { return __elementCommandsStrict; } static init(nightwatchInstance) { const api = new ApiLoader(nightwatchInstance); const staticApis = new StaticApis(nightwatchInstance); staticApis.loadStaticAssertions(); staticApis.loadStaticExpect(); api.loadApiCommandsSync(); return WithinContextLoader.loadCommandCache(nightwatchInstance) .then(_ => api.loadCustomCommands()) .then(_ => api.loadCustomAssertions()) .then(_ => api.initPluginTransforms()) .then(_ => api.loadPlugins()) .then(_ => api.defineWithinContext()) .then(_ => api.loadPageObjects()) .then(_ => { 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 === '__isProxy') { return true; } 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}); }; } }); } initPluginTransforms() { this.nightwatchInstance.transforms = Promise.resolve([]); return Promise.resolve(); } loadPlugins(parent = null) { let plugins = DEFAULT_PLUGINS.concat(this.nightwatchInstance.options.plugins || []); plugins = [... new Set(plugins)]; const promises = []; plugins.forEach(pluginName => { const plugin = PluginLoader.load(pluginName); if (plugin.commands) { promises.push(this.__loadCustomObjects({ parent, sourcePath: plugin.commands, isUserDefined: true })); } if (plugin.assertions) { promises.push(this.__loadCustomObjects({ parent, sourcePath: plugin.assertions, namespaces: ApiLoader.CommandFiles.assertions.namespaces, Loader: AssertionLoader })); } if (plugin.transforms) { promises.push(Promise.resolve(this.__loadPluginTransforms(plugin.transforms))); } }); return Promise.all(promises); } async defineWithinContext(parent) { const loader = new WithinContextLoader(this.nightwatchInstance); loader.define(parent); return Promise.resolve(); } /** * Loads the page objects, if defined * @param parent */ async loadPageObjects(parent = null) { await PageObjectLoader.loadApiCommands(this.nightwatchInstance); return this.__loadCustomObjects({ parent, sourcePath: this.nightwatchInstance.options.page_objects_path, isUserDefined: true, Loader: PageObjectLoader }); } /** * Loads custom assertions, if defined * * @param parent */ loadCustomAssertions(parent = null) { return this.__loadCustomObjects({ parent, sourcePath: this.nightwatchInstance.options.custom_assertions_path, namespaces: ApiLoader.CommandFiles.assertions.namespaces, Loader: AssertionLoader }); } /** * Loads custom commands, if defined * @param parent */ loadCustomCommands(parent = null) { return this.__loadCustomObjects({ parent, sourcePath: this.nightwatchInstance.options.custom_commands_path, isUserDefined: true }); } /** * Loads built-in api commands, assertions, and expect definitions * @param parent * @param {object} options */ loadApiCommandsSync(parent = null, {loadAssertions = true, loadClientCommands = true} = {}) { Object.keys(ApiLoader.CommandFiles) .forEach((key) => { const commands = ApiLoader.CommandFiles[key]; const {namespaces} = commands; if (!loadAssertions && key === 'assertions') { return; } if (!loadClientCommands && key === 'clientCommands') { return; } let elementCommandStrict = false; if (key === 'elementCommands') { elementCommandStrict = true; } 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, elementCommandStrict, 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, elementCommandStrict = 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); } if (elementCommandStrict && loader.commandName && !__elementCommandsStrict.includes(loader.commandName)) { __elementCommandsStrict.push(loader.commandName); } } async addCommandDefinitionAsync({ dirPath, isUserDefined = false, fileName, parent, namespace = [], abortOnFailure = false, isElementCommand = false, Loader = CommandLoader }) { const loader = new Loader(this.nightwatchInstance); loader.isUserDefined = isUserDefined; if (namespace && namespace.length) { loader.setNamespace(namespace); } await loader.loadModuleAsync(dirPath, fileName); loader.createWrapper(abortOnFailure).define(parent); if (isElementCommand && loader.commandName && !__elementCommands.includes(loader.commandName)) { __elementCommands.push(loader.commandName); } } __loadCustomObjects({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; }, []); return Promise.all(commandFilesProcessed.map(opts => this.addCommandDefinitionAsync(opts))); //commandFilesProcessed.forEach(opts => this.addCommandDefinitionSync(opts)); } __loadCommandsSync({ parent = null, namespacesObj = null, ns = null, dirPath, originalSourcePath, abortOnFailure = false, isProtocolCommand = false, isElementCommand = false, isUserDefined = false, elementCommandStrict = false, Loader = CommandLoader, loadAction = this.addCommandDefinitionSync.bind(this) }) { if (namespacesObj) { Object.keys(namespacesObj).forEach((ns) => this.__loadCommandsSync({ ns, parent, dirPath, isUserDefined, isElementCommand, elementCommandStrict, Loader, loadAction: isUserDefined ? loadAction : this.addCommandDefinitionSync.bind(this), abortOnFailure: namespacesObj[ns].abortOnFailure })); } else { Utils.readFolderRecursively(dirPath, [], (dirPath, fileName, namespace) => { loadAction.call(this, { dirPath, fileName, parent, isUserDefined, namespace: ns || namespace, isProtocolCommand, isElementCommand, elementCommandStrict, 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; } }); } } __loadCommands({ parent = null, namespacesObj = null, ns = null, dirPath, abortOnFailure = false, isProtocolCommand = false, isElementCommand = false, isUserDefined = false, Loader = CommandLoader, loadAction = this.addCommandDefinitionAsync.bind(this) }) { if (namespacesObj) { return Promise.all(Object.keys(namespacesObj).map((ns) => { return this.__loadCommands({ ns, parent, dirPath, isUserDefined, isElementCommand, Loader, abortOnFailure: namespacesObj[ns].abortOnFailure }); })); } const promises = []; Utils.readFolderRecursively(dirPath, [], (dirPath, fileName, namespace) => { const result = Promise.resolve(loadAction.call(this, { dirPath, fileName, parent, isUserDefined, namespace: ns || namespace, isProtocolCommand, isElementCommand, Loader, abortOnFailure })); promises.push(result); }, function readDirFn(sourcePath) { return { resources: fs.readdirSync(sourcePath), sourcePath }; }); return Promise.all(promises); } /** * @param {function|Array} transforms * @returns {Promise} */ __normalizeTransforms(transforms) { if (typeof transforms === 'function') { return transforms(); } return Promise.resolve(transforms || []); } /** * @param {function|Array} nextTransforms */ __loadPluginTransforms(nextTransforms) { this.nightwatchInstance.transforms = this.nightwatchInstance.transforms.then(existingTransforms => this.__normalizeTransforms(nextTransforms).then(items => existingTransforms.concat(items))); } } module.exports = ApiLoader;