UNPKG

cypress-commands

Version:

A collection of Cypress commands to extend and complement the default commands

856 lines (755 loc) 23.3 kB
import path from 'path-browserify'; var repository = { type: "git", url: "https://github.com/Lakitna/cypress-commands" }; const notInProduction = 'This message should never show ' + "if you're a user of cypress-commands. If it does, please open " + `an issue at ${repository.url}.`; const command = { attribute: { disable_strict: 'This behaviour can be disabled by calling ' + "'.attribute()' with the option 'strict: false'.", existence: { single: { negated: (attribute) => { return ( 'Expected element to not have attribute ' + `'${attribute}', but it was continuously found.` ); }, normal: (attribute) => { return ( 'Expected element to have attribute ' + `'${attribute}', but never found it.` ); }, }, multiple: { negated: (attribute, inputCount, outputCount) => { return ( `Expected all ${inputCount} elements to not have ` + `attribute '${attribute}', but it was continuously found on ` + `${outputCount} elements.` ); }, normal: (attribute, inputCount, outputCount) => { return ( `Expected all ${inputCount} elements to have ` + `attribute '${attribute}', but never found it on ` + `${inputCount - outputCount} elements.` ); }, }, }, }, to: { cantCast: (description, target) => { let message = `Can't cast ${description}`; if (target) { message += ` to type ${target}`; } return `${message}.`; }, cantCastType: (type, target) => { return command.to.cantCast(`subject of type ${type}`, target); }, cantCastVal: (val, target) => { return command.to.cantCast(`'${val}'`, target); }, expected: (expected) => { return `Expected one of [${expected.join(', ')}]`; }, }, }; /** * By marking the current command we can retrieve it later in any * context, including retried context. * @param {string} commandName */ function markCurrentCommand(commandName) { const queue = Cypress.cy.queue; const currentCommand = queue .get() .filter((command) => { return command.get('name') === commandName && !command.get('invoked'); }) .shift(); // The mark currentCommand.attributes.invoked = true; } /** * Find out of the last marked command in the command queue has upcoming * assertions that negate existence. * @return {boolean} */ function upcomingAssertionNegatesExistence() { const currentCommand = getLastMarkedCommand(); if (!currentCommand) { return false; } const upcomingAssertions = getUpcomingAssertions(currentCommand); return upcomingAssertions.some((c) => { let args = c.get('args'); if (typeof args[0] === 'string') { args = args[0].split('.'); return args.includes('exist') && args.includes('not'); } return false; }); } /** * @return {Command|false} */ function getLastMarkedCommand() { const queue = Cypress.cy.queue; const cmd = queue .get() .filter((command) => command.get('invoked')) .pop(); if (cmd === undefined) { console.error( 'Could not find any marked commands in the queue. ' + 'Did you forget to mark the command during its invokation?' + `\n\n${notInProduction}` ); return false; } return cmd; } /** * Recursively find all direct upcoming assertions * @param {Command} command * @return {Command[]} */ function getUpcomingAssertions(command) { const next = command.get('next'); let ret = []; if (next && next.get('type') === 'assertion') { ret = [next, ...getUpcomingAssertions(next)]; } return ret; } /** * @param {'simplify'|'keep-newline'|'keep'} mode * @return {function} */ function whitespace(mode) { const zeroWidthWhitespace = /[\u200B-\u200D\uFEFF]/g; if (mode === 'simplify') { return (input) => { return input.replace(zeroWidthWhitespace, '').replace(/\s+/g, ' ').trim(); }; } if (mode === 'keep-newline') { return (input) => { return input .replace(zeroWidthWhitespace, '') .replace(/[^\S\n]+/g, ' ') .replace(/^[^\S\n]/g, '') .replace(/[^\S\n]$/g, '') .replace(/[^\S\n]*\n[^\S\n]*/g, '\n'); }; } return (input) => input; } const _$7 = Cypress._; /** * Error namespace for command related issues */ class CommandError extends Error { /** * @param {string} message The error message */ constructor(message) { if (_$7.isArray(message)) { message = message.join(''); } super(message); this.name = 'CommandError'; } } const _$6 = Cypress._; /** * Validate user set options */ class OptionValidator { /** * @param {string} commandName */ constructor(commandName) { /** * @type {string} */ this.command = commandName; /** * Url to the full documentation of the command * @type {string} */ this.docUrl = `${repository.url}/blob/master/docs/${commandName}.md`; } /** * Validate a user set option * @param {string} option * @param {*} actual * @param {string[]|string} expected * @throws {CommandError} */ check(option, actual, expected) { if (_$6.isUndefined(actual)) { // The option is not set. This is fine. return; } const errMessage = { start: `Bad value for the option "${option}" of the command "${this.command}".\n\n`, received: `Command received the value "${actual}" but `, end: `\n\nFor details refer to the documentation at ${this.docUrl}`, }; if (_$6.isArray(expected)) { if (!expected.includes(actual)) { throw new CommandError([ errMessage.start, errMessage.received, `expected one of ${JSON.stringify(expected)}`, errMessage.end, ]); } } else if (_$6.isString(expected)) { if (!eval(`'${actual}' ${expected}`)) { throw new CommandError([ errMessage.start, errMessage.received, `expected a value ${expected}`, errMessage.end, ]); } } else { throw new CommandError( 'Not sure how to validate ' + `the option "${option}" of the command "${this.command}".\n\n` + 'If you see this message in the wild, please create an issue ' + `so this error can be resolved.\n${repoUrl}` ); } } } const _$5 = Cypress._; const $$2 = Cypress.$; const errMsg$1 = command.attribute; const validator$3 = new OptionValidator('attribute'); /** * Get the value of an attribute of the subject * * @example * cy.get('a').attribute('href'); * * @param {Object} [options] * @param {boolean} [options.log=true] * Log the command to the Cypress command log * * @yields {string|string[]} * @since 0.2.0 */ Cypress.Commands.add( 'attribute', { prevSubject: 'element' }, (subject, attribute, options = {}) => { subject = $$2(subject); // Make sure the order of input can be flipped if (_$5.isObject(attribute)) { [attribute, options] = [options, attribute]; } // Handle options validator$3.check('log', options.log, [true, false]); validator$3.check('whitespace', options.whitespace, ['simplify', 'keep', 'keep-newline']); validator$3.check('strict', options.strict, [true, false]); _$5.defaults(options, { log: true, strict: true, whitespace: 'keep', }); options._whitespace = whitespace(options.whitespace); const consoleProps = { 'Applied to': subject, }; if (options.log) { options._log = Cypress.log({ $el: subject, name: 'attribute', message: attribute, consoleProps: () => consoleProps, }); } // Mark this newly invoked command in the command queue to be able to find it later. markCurrentCommand('attribute'); /** * @param {Array.<string>|string} result */ function updateLog(result) { consoleProps.Yielded = result; } /** * Get the attribute and do the upcoming assertion * @return {Promise} */ function resolveAttribute() { const attr = subject .map((i, element) => { return $$2(element).attr(attribute); }) // Deconstruct jQuery object to normal array .toArray() .map(options._whitespace); let result = attr; if (options.strict && subject.length > result.length) { const negate = upcomingAssertionNegatesExistence(); if (!negate) { // Empty result to we fail on missing attributes result = []; } } if (result.length === 0) { // Empty JQuery object is Cypress' way of saying that something does not exist result = $$2(); } else if (result.length === 1) { // Only one result, so unwrap the array result = result[0]; } if (options.log) { updateLog(result); } return cy.verifyUpcomingAssertions(result, options, { onFail: (err) => onFail(err, subject, attribute, attr), // Retry untill the upcoming assertion passes onRetry: resolveAttribute, }); } return resolveAttribute().then((attribute) => { // The upcoming assertion passed, finish up the log if (options.log) { options._log.snapshot().end(); } return attribute; }); } ); /** * Overwrite the error message of implicit assertions * @param {AssertionError} err * @param {jQuery} subject * @param {string} attribute * @param {string | string[]} result */ function onFail(err, subject, attribute, result) { const negate = err.message.includes(' not '); result = _$5.isArray(result) ? result : [result]; if (err.type === 'existence' && subject.length == 1) { const errorMessage = errMsg$1.existence.single; if (negate) { err.displayMessage = errorMessage.negated(attribute); } else { err.displayMessage = errorMessage.normal(attribute); } } else if (err.type === 'existence' && subject.length > 1) { const errorMessage = errMsg$1.existence.multiple; if (negate) { err.displayMessage = errorMessage.negated(attribute, subject.length, result.length); } else { err.displayMessage = errorMessage.normal(attribute, subject.length, result.length); } err.displayMessage += '\n\n' + errMsg$1.disable_strict; } err.message = err.displayMessage; } const _$4 = Cypress._; /** * Find out if a given value is a jQuery object * @param {*} value * @return {boolean} */ function isJquery(value) { if (_$4.isUndefined(value) || _$4.isNull(value)) { return false; } return !!value.jquery; } const _$3 = Cypress._; const $$1 = Cypress.$; const validator$2 = new OptionValidator('then'); /** * Enables you to work with the subject yielded from the previous command. * * @example * cy.then((subject) => { * // ... * }); * * @param {function} fn * @param {Object} options * @param {boolean} [options.log=false] * Log to Cypress bar * @param {boolean} [options.retry=false] * Retry when an upcomming assertion fails * * @yields {any} * @since 0.0.0 */ Cypress.Commands.overwrite('then', (originalCommand, subject, fn, options = {}) => { if (_$3.isFunction(options)) { // Flip the values of `fn` and `options` [fn, options] = [options, fn]; } validator$2.check('log', options.log, [true, false]); validator$2.check('retry', options.retry, [true, false]); if (options.retry && typeof options.log === 'undefined') { options.log = true; } _$3.defaults(options, { log: false, retry: false, }); // Setup logging const consoleProps = {}; if (options.log) { options._log = Cypress.log({ name: 'then', message: '', consoleProps: () => consoleProps, }); if (isJquery(subject)) { // Link the DOM element to the logger options._log.set('$el', $$1(subject)); consoleProps['Applied to'] = $$1(subject); } else { consoleProps['Applied to'] = String(subject); } if (options.retry) { options._log.set('message', 'retry'); } } /** * This function is recursively called untill timeout or the upcomming * assertion passes. Keep this function as fast as possible. * * @return {Promise} */ async function executeFnAndRetry() { const result = await executeFn(); return cy.verifyUpcomingAssertions(result, options, { // Try again by calling itself onRetry: executeFnAndRetry, }); } /** * Execute the provided callback function * * @return {*} */ async function executeFn() { // Execute using the original `then` to not reinvent the wheel return await originalCommand(subject, options, fn).then((value) => { if (options.log) { consoleProps.Yielded = value; } return value; }); } if (options.retry) { return executeFnAndRetry(); } return executeFn(); }); const _$2 = Cypress._; const $ = Cypress.$; const validator$1 = new OptionValidator('text'); /** * Get the text contents of the subject * * @example * cy.get('footer').text(); * * @param {Object} [options] * @param {boolean} [options.log=true] * Log the command to the Cypress command log * @param {'simplify'|'keep-newline'|'keep'} [options.whitespace='simplify'] * Replace complex whitespace (`&nbsp;`, `\t`, `\n`, multiple spaces and more * obscure whitespace characters) with a single regular space. * @param {number} [options.depth=0] * Include the text contents of child elements up to a depth of `n` * * @yields {string|string[]} * @since 0.1.0 */ Cypress.Commands.add('text', { prevSubject: 'element' }, (element, options = {}) => { validator$1.check('log', options.log, [true, false]); validator$1.check('whitespace', options.whitespace, ['simplify', 'keep', 'keep-newline']); validator$1.check('depth', options.depth, '>= 0'); _$2.defaults(options, { log: true, whitespace: 'simplify', depth: 0, }); options._whitespace = whitespace(options.whitespace); const consoleProps = { 'Applied to': $(element), 'Whitespace': options.whitespace, 'Depth': options.depth, }; if (options.log) { options._log = Cypress.log({ $el: $(element), name: 'text', message: '', consoleProps: () => consoleProps, }); } /** * @param {Array.<string>|string} result */ function updateLog(result) { consoleProps.Yielded = result; if (_$2.isArray(result)) { options._log.set('message', JSON.stringify(result)); } else { options._log.set('message', result); } } /** * Get the text and do the upcoming assertion * @return {Promise} */ function resolveText() { let text = []; element.each((_, elem) => { text.push(getTextOfElement($(elem), options.depth).trim()); }); text = text.map(options._whitespace); if (text.length == 1) { text = text[0]; } if (options.log) updateLog(text); return cy.verifyUpcomingAssertions(text, options, { // Retry untill the upcoming assertion passes onRetry: resolveText, }); } return resolveText().then((text) => { // The upcoming assertion passed, finish up the log if (options.log) { options._log.snapshot().end(); } return text; }); }); /** * @param {JQuery} element * @param {number} depth * @return {string} */ function getTextOfElement(element, depth) { const TAG_REPLACEMENT = { WBR: '\u200B', BR: ' ', }; let text = ''; element.contents().each((i, content) => { if (content.nodeType === Node.TEXT_NODE) { return (text += content.data); } if (content.nodeType === Node.ELEMENT_NODE) { if (_$2.has(TAG_REPLACEMENT, content.nodeName)) { return (text += TAG_REPLACEMENT[content.nodeName]); } if (depth > 0) { return (text += getTextOfElement($(content), depth - 1)); } } }); return text; } const _$1 = Cypress._; const errMsg = command.to; const validator = new OptionValidator('to'); const types = { array: castArray, string: castString, number: castNumber, }; /** * @param {string} type Target type * @param {Object} [options] * @param {boolean} [options.log=true] * Log the command to the Cypress command log * * @yields {any} * @since 0.3.0 */ Cypress.Commands.add('to', { prevSubject: true }, (subject, type, options = {}) => { validator.check('log', options.log, [true, false]); _$1.defaults(options, { log: true, }); if (_$1.isUndefined(subject)) { throw new Error(errMsg.cantCastType('undefined')); } if (_$1.isNull(subject)) { throw new Error(errMsg.cantCastType('null')); } if (_$1.isNaN(subject)) { throw new Error(errMsg.cantCastType('NaN')); } if (!_$1.keys(types).includes(type)) { // We don't know the given type, so we can't cast to it throw new Error(`${errMsg.cantCast('subject', type)} ${errMsg.expected(_$1.keys(types))}`); } const consoleProps = { 'Applied to': subject, }; if (options.log) { options._log = Cypress.log({ name: 'to', message: type, consoleProps: () => consoleProps, }); } /** * Cast the subject and do the upcoming assertion * @return {Promise} */ function castSubject() { try { return cy.verifyUpcomingAssertions(types[type](subject), options, { // Retry untill the upcoming assertion passes onRetry: castSubject, }); } catch (err) { // The casting function threw an error, let's try again options.error = err; return cy.retry(castSubject, options, options._log); } } return castSubject().then((result) => { // Everything passed, finish up the log consoleProps.Yielded = result; return result; }); }); /** * @param {any|any[]} subject * @return {any[]} */ function castArray(subject) { if (_$1.isArrayLikeObject(subject)) { return subject; } return [subject]; } /** * @param {any|any[]} subject * @return {string} */ function castString(subject) { if (_$1.isArrayLikeObject(subject)) { return subject.map(castString); } if (_$1.isObject(subject)) { return JSON.stringify(subject); } return `${subject}`; } /** * @param {any|any[]} subject * @return {number|number[]} */ function castNumber(subject) { if (_$1.isArrayLikeObject(subject)) { return subject.map(castNumber); } else if (_$1.isObject(subject)) { throw new Error(errMsg.cantCastType('object', 'number')); } const casted = Number(subject); if (isNaN(casted)) { throw new Error(errMsg.cantCastVal(subject, 'number')); } return casted; } const _ = Cypress._; const methods = [ 'GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS', 'TRACE', 'COPY', 'LOCK', 'MKCOL', 'MOVE', 'PURGE', 'PROPFIND', 'PROPPATCH', 'UNLOCK', 'REPORT', 'MKACTIVITY', 'CHECKOUT', 'MERGE', 'M-SEARCH', 'NOTIFY', 'SUBSCRIBE', 'UNSUBSCRIBE', 'SEARCH', 'CONNECT', ]; /** * @yields {any} * @since 0.2.0 */ Cypress.Commands.overwrite('request', (originalCommand, ...args) => { const options = {}; if (_.isObject(args[0])) { _.extend(options, args[0]); } else if (args.length === 1) { options.url = args[0]; } else if (args.length === 2) { if (methods.includes(args[0].toUpperCase())) { options.method = args[0]; options.url = args[1]; } else { options.url = args[0]; options.body = args[1]; } } else if (args.length === 3) { options.method = args[0]; options.url = args[1]; options.body = args[2]; } options.url = parseUrl(options.url); return originalCommand(options); }); /** * @param {string} url * @return {string} */ function parseUrl(url) { if ( typeof url === 'string' && !url.includes('://') && !url.startsWith('localhost') && !url.startsWith('www.') ) { // It's a relative url const config = Cypress.config(); const requestBaseUrl = config.requestBaseUrl; if (_.isString(requestBaseUrl) && requestBaseUrl.length > 0) { const split = requestBaseUrl.split('://'); const protocol = split[0] + '://'; const baseUrl = split[1]; url = protocol + path.join(baseUrl, url); } } return url; }