UNPKG

@revoloo/cypress6

Version:

Cypress.io end to end testing tool

275 lines (226 loc) 9.07 kB
const _ = require('lodash') const Promise = require('bluebird') const $dom = require('../../../dom') const $utils = require('../../../cypress/utils') const $errUtils = require('../../../cypress/error_utils') const $elements = require('../../../dom/elements') const newLineRe = /\n/g module.exports = (Commands, Cypress, cy) => { Commands.addAll({ prevSubject: 'element' }, { select (subject, valueOrText, options = {}) { const userOptions = options options = _.defaults({}, userOptions, { $el: subject, log: true, force: false, }) const consoleProps = {} if (options.log) { // figure out the options which actually change the behavior of clicks const deltaOptions = $utils.filterOutOptions(options) options._log = Cypress.log({ message: deltaOptions, $el: options.$el, timeout: options.timeout, consoleProps () { // merge into consoleProps without mutating it return _.extend({}, consoleProps, { 'Applied To': $dom.getElements(options.$el), 'Options': deltaOptions, }) }, }) options._log.snapshot('before', { next: 'after' }) } let node // if subject is a <select> el assume we are filtering down its // options to a specific option first by value and then by text // we'll throw if more than one is found AND the select // element is multiple=multiple // if the subject isn't a <select> then we'll check to make sure // this is an option // if this is multiple=multiple then we'll accept an array of values // or texts and clear the previous selections which matches jQuery's // behavior if (!options.$el.is('select')) { node = $dom.stringify(options.$el) $errUtils.throwErrByPath('select.invalid_element', { args: { node } }) } if (options.$el.length && options.$el.length > 1) { $errUtils.throwErrByPath('select.multiple_elements', { args: { num: options.$el.length } }) } // normalize valueOrText if its not an array valueOrText = [].concat(valueOrText) const multiple = options.$el.prop('multiple') // throw if we're not a multiple select and we've // passed an array of values if (!multiple && valueOrText.length > 1) { $errUtils.throwErrByPath('select.invalid_multiple') } const getOptions = () => { let notAllUniqueValues // throw if <select> is disabled if (!options.force && options.$el.prop('disabled')) { node = $dom.stringify(options.$el) $errUtils.throwErrByPath('select.disabled', { args: { node } }) } const values = [] const optionEls = [] const optionsObjects = options.$el.find('option').map((index, el) => { // push the value in values array if its // found within the valueOrText const value = $elements.getNativeProp(el, 'value') const optEl = $dom.wrap(el) if (valueOrText.includes(value)) { optionEls.push(optEl) values.push(value) } // replace new line chars, then trim spaces const trimmedText = optEl.text().replace(newLineRe, '').trim() // return the elements text + value return { value, originalText: optEl.text(), text: trimmedText, $el: optEl, } }).get() // if we couldn't find anything by value then attempt // to find it by text and insert its value into values arr if (!values.length) { // if any of the values are the same and the user is trying to // select based on the text, setting the value won't work // `notAllUniqueValues` is used later to do the right thing const uniqueValues = _.chain(optionsObjects).map('value').uniq().value() notAllUniqueValues = uniqueValues.length !== optionsObjects.length _.each(optionsObjects, (obj) => { if (valueOrText.includes(obj.text)) { optionEls.push(obj.$el) const objValue = obj.value values.push(objValue) } }) } // if we didnt set multiple to true and // we have more than 1 option to set then blow up if (!multiple && (values.length > 1)) { $errUtils.throwErrByPath('select.multiple_matches', { args: { value: valueOrText.join(', ') }, }) } if (!values.length) { $errUtils.throwErrByPath('select.no_matches', { args: { value: valueOrText.join(', ') }, }) } _.each(optionEls, ($el) => { if ($el.prop('disabled')) { node = $dom.stringify($el) $errUtils.throwErrByPath('select.option_disabled', { args: { node }, }) } }) _.each(optionEls, ($el) => { if ($el.closest('optgroup').prop('disabled')) { node = $dom.stringify($el) $errUtils.throwErrByPath('select.optgroup_disabled', { args: { node }, }) } }) return { values, optionEls, optionsObjects, notAllUniqueValues } } const retryOptions = () => { return Promise .try(getOptions) .catch((err) => { options.error = err return cy.retry(retryOptions, options) }) } return Promise .try(retryOptions) .then((obj = {}) => { const { values, optionEls, optionsObjects, notAllUniqueValues } = obj // preserve the selected values consoleProps.Selected = values return cy.now('click', options.$el, { $el: options.$el, log: false, verify: false, errorOnSelect: false, // prevent click errors since we want the select to be clicked _log: options._log, force: options.force, timeout: options.timeout, interval: options.interval, }).then(() => { // TODO: // 1. test cancelation // 2. test passing optionEls to each directly // 3. update other tests using this Promise.each pattern // 4. test that force is always true // 5. test that command is not provided (undefined / null) // 6. test that option actually receives click event // 7. test that select still has focus (i think it already does have a test) // 8. test that multiple=true selects receive option event for each selected option return Promise .resolve(optionEls) // why cant we just pass these directly to .each? .each((optEl) => { return cy.now('click', optEl, { $el: optEl, log: false, verify: false, force: true, // always force the click to happen on the <option> timeout: options.timeout, interval: options.interval, }) }).then(() => { // reset the selects value after we've // fired all the proper click events // for the options // TODO: shouldn't we be updating the values // as we click the <option> instead of // all afterwards? options.$el.val(values) if (notAllUniqueValues) { // if all the values are the same and the user is trying to // select based on the text, setting the val() will just // select the first one let selectedIndex = 0 _.each(optionEls, ($el) => { const index = _.findIndex(optionsObjects, (optionObject) => { return $el.text() === optionObject.originalText }) selectedIndex = index return $el.prop('selected', 'selected') }) options.$el[0].selectedIndex = selectedIndex options.$el[0].selectedOptions = _.map(optionEls, ($el) => { return $el.get() }) } const input = new Event('input', { bubbles: true, cancelable: false, }) options.$el.get(0).dispatchEvent(input) // yup manually create this change event // 1.6.5. HTML event types // scroll down to 'change' const change = document.createEvent('HTMLEvents') change.initEvent('change', true, false) options.$el.get(0).dispatchEvent(change) }) }).then(() => { const verifyAssertions = () => { return cy.verifyUpcomingAssertions(options.$el, options, { onRetry: verifyAssertions, }) } return verifyAssertions() }) }) }, }) }