cypress-commands
Version:
A collection of Cypress commands to extend and complement the default commands
856 lines (755 loc) • 23.3 kB
JavaScript
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 (` `, `\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;
}