cypress-io / cypress

Fast, easy and reliable testing for anything that runs in a browser.
https://cypress.io
MIT License
46.73k stars 3.16k forks source link

Ability to select by index within `.select()` command #757

Closed dhoko closed 2 years ago

dhoko commented 6 years ago

4239

screenshot from 2017-10-18 10-02-19

Desired behavior:

I can select this option, via an index inside the argument option.

How to reproduce:

<select id="country">
   <option label="United States" value="object:44" selected="selected">United States</option>
   <option label="United Kingdom" value="object:45">United Kingdom</option>
   <option label="Switzerland" value="object:46">Switzerland</option>
   <option label="France" value="object:47">France</option>
   <option label="Germany" value="object:48">Germany</option>
   <option label="Canada" value="object:49">Canada</option>
   <option disabled="" label="------------------" value="object:50">------------------</option>
   <option label="Estonia" value="object:117">Estonia</option>
   <option label="Ethiopia" value="object:118">Ethiopia</option>
   <option label="Falkland Islands (Malvinas)" value="object:119">Falkland Islands (Malvinas)</option>
   <option label="Faroe Islands" value="object:120">Faroe Islands</option>
   <option label="Fiji" value="object:121">Fiji</option>
   <option label="Finland" value="object:122">Finland</option>
   <option label="France" value="object:123">France</option>
   <option label="French Guiana" value="object:124">French Guiana</option>
   <option label="French Polynesia" value="object:125">French Polynesia</option>
   <option label="French Southern Territories" value="object:126">French Southern Territories</option>
</select>

Test code:

cy.get('#country').select('France');

Additional Info (images, stack traces, etc)

I tried to create the patch myself (it seems the error is here -> https://github.com/cypress-io/cypress/blob/0e0f75cfe90d24d9b0ed52b3c6a5a986baefcf8a/packages/driver/src/cy/commands/actions/select.coffee#L50) but I cannot find the file inside node_modules :/

PS: Cypress is awesome :heart:

avanslaars commented 6 years ago

Your options may have duplicate labels, but they have different values so you can use the value, rather than the label text in select() and you should get your expected option.

You could select the first instance of "France" with cy.get('#country').select('object:47') or the second with cy.get('#country').select('object:123').

Using the value here to be more specific about which match you want makes your test deterministic.

Hope this helps!

dhoko commented 6 years ago

Of course but the list is generated via Angular. The value might change.

avanslaars commented 6 years ago

That does present a bit of a problem. Here is a workaround you can use for now:

cy.get('#country')
    .find('option[label="France"]') // this will return both elements
    .then($els => $els.get(1).setAttribute('selected', "selected")) // set the selected attribute on the desired option
    .parent() // change the subject back to the select
    .trigger('change') // trigger change event so event handlers will pick it up

I did a quick test where the drop down selection lead to another UI change with a handler for the select's change event and this did the trick. You may also wish to remove the selected attribute beforehand just so your DOM represents a more realistic scenario before the change even it triggered (though it made no difference in my sample code).

I did that with:

cy.get('#country')
    .find('option[selected="selected"]')
    .then($el => $el.get(0).removeAttribute('selected'))

So everything together looks like:

// remove current selection
cy.get('#country')
    .find('option[selected="selected"]')
    .then($el => $el.get(0).removeAttribute('selected'))

// Manually set desired selection
cy.get('#country')
    .find('option[label="France"]')
    .then($els => $els.get(1).setAttribute('selected', "selected"))
    .parent()
    .trigger('change')
brian-mann commented 6 years ago

Haven't tried this but you could probably clean this up by doing...

// Manually set desired selection
cy.get('#country').then(($select) => {
  const opt = $select.find('option[label="France"]')
  $select.val(opt.attr('value'))
  return $select
}).trigger('change')
jennifer-shehane commented 6 years ago

Hey @dhoko, did the suggestions above solve your issue?

dhoko commented 6 years ago

I didn't debug my tests yet, I will try monday. I think it should solve it yes. I can create a custom command from the snippet.

dhoko commented 6 years ago

I tried this one -> https://github.com/cypress-io/cypress/issues/757#issuecomment-337676399 it worked. Thx.

benvds commented 6 years ago

Even though this isssue is closed. I still think a select by index is required. Double list entries (both in value as label) is quite common as for long lists people tend to add the most used options as preferred option on top as well (same as in the example).

johndatserakis commented 6 years ago

@brian-mann your answer is an absolute lifesaver. I'm using Vue with Cypress and it's been nearly impossible to figure out how to use my selects - it's due to my selects having objects as their values in the code. Works great for everything but testing - until your answer.

Sorta wish Cypress just let us tell it to act like a normal user and "click here" and then "click here". It doesn't let you do that on selects, and it makes things pretty tough.

Thanks for your answer.

jennifer-shehane commented 6 years ago

Will reopen as feature to add 'select by index' to .select() command.

jennifer-shehane commented 6 years ago

Select command code: https://github.com/cypress-io/cypress/blob/develop/packages/driver/src/cy/commands/actions/select.coffee#L12

mattvb91 commented 6 years ago

:+1: for visibility

grazielegs commented 5 years ago

That does present a bit of a problem. Here is a workaround you can use for now:

cy.get('#country')
    .find('option[label="France"]') // this will return both elements
    .then($els => $els.get(1).setAttribute('selected', "selected")) // set the selected attribute on the desired option
    .parent() // change the subject back to the select
    .trigger('change') // trigger change event so event handlers will pick it up

I did a quick test where the drop down selection lead to another UI change with a handler for the select's change event and this did the trick. You may also wish to remove the selected attribute beforehand just so your DOM represents a more realistic scenario before the change even it triggered (though it made no difference in my sample code).

I did that with:

cy.get('#country')
    .find('option[selected="selected"]')
    .then($el => $el.get(0).removeAttribute('selected'))

So everything together looks like:

// remove current selection
cy.get('#country')
    .find('option[selected="selected"]')
    .then($el => $el.get(0).removeAttribute('selected'))

// Manually set desired selection
cy.get('#country')
    .find('option[label="France"]')
    .then($els => $els.get(1).setAttribute('selected', "selected"))
    .parent()
    .trigger('change')

That workaround helped me, thanks!

Undistraction commented 5 years ago

This definitely feels like it shouldn't need a workaround. There is nothing wrong with having options with duplicate values and labels.

azaeng04 commented 5 years ago
    cy.get('#country')
      .find('option[label="France"]').then(elements => {
        const france = elements[0].getAttribute('value');
        cy.get('#country')
          .select(france);
      });    
tylerlinquata commented 4 years ago

Hey @jennifer-shehane, the lack of select by index has been annoying me so I'd like to take this issue. Would we want to add an option for index, and then default to 0? It looks like the check at line 109 could be changed to handle this pretty easily.

jbool24 commented 4 years ago

@jennifer-shehane Some select tags fields on closed platforms are auto generated and out of developer control. The platform I am testing list the same values twice in select fields causing an error below. Whats the status on index based selection? I believe @tylerlinquata is willing to fix. @tylerlinquata have you opened a pull request yet?

Screen Shot 2020-03-16 at 11 26 06 AM

RamiroPinol commented 4 years ago

+1 to the select by index feature. I'm testing a select that is populated with data from an API that changes every call, so I can't select by value.

jennifer-shehane commented 4 years ago

I think this would basically accept a new argument index, so that the arguments accepted would be:

Syntax

.select(value)
.select(values)
.select(index)
.select(value, options)
.select(values, options)
.select(index, options)

Argument

index (Number)

A number indicating the index to find the <option> at within the <select>.


We would be open to a PR (that is fully tested). The code can be found here: https://github.com/cypress-io/cypress/blob/develop/packages/driver/src/cy/commands/actions/select.coffee#L12

Check out our contributing doc.

davidcordoba4 commented 4 years ago

I share implemented solution

selectDropDownList(cy.get('#selectedRoomNo'),4)  
function selectDropDownList(dropDownList, value) {
    dropDownList.then(($select) => {
      debugger
      $select.find('option[value="' + $select.val() + '"]')[0].removeAttribute('selected')
      const opt = $select.find('option[value="' + value + '"]')
      $select.val(opt[0].value)
      opt[0].setAttribute('selected', '')
      return $select
    }).trigger('change')
  }
dillingham commented 3 years ago

This is especially useful for my current usecase:

a component has select options with the same value but when selected it uses the option's data-name attribute

bahmutov commented 3 years ago

Take a look at https://glebbahmutov.com/cypress-examples/7.4.0/recipes/selected-value.html#selecting-option-by-index where I use the following to select by index

// every child of <select> is an <option> element
cy.get('select')
  .children()
  .eq(1)
  .then(($option) => {
    const value = $option.attr('value')
    // if we want to select the oranges,
    // let's use the value we got
    cy.get('select').select(value)
  })
dillingham commented 3 years ago

This worked for me

cy.get('select').find('option[data-name="sort"]').then($el =>
    $el.get(0).setAttribute("selected", "selected")
).parent().trigger("change")
sbaechler commented 3 years ago

What is the reason that cy.select() does not support duplicate options? If text and value are the same, then Cypress should just select the first instance.

As mentioned before, duplicating entries is a common pattern. E.g. in a countries list where the most used countries appear on top and in the full list below.

Selecting by index would then be the workaround but it would be flaky because the order or number of options might be different.

The workaround with the jQuery selectors did not work for me in a React project. Maybe because React uses its own event system.

Loren-Johnson commented 3 years ago

I agree with sbaechler. I think the solution is not to add index selection, but rather allow select() to specify if it is expecting duplicates and which duplicate should be chosen.

mjustin commented 3 years ago

I'm writing against a select box where the selected option is shown as the default selected element, but is disabled. The same value is shown selectable lower down in the list. In order to select the option, I need to somehow specify that I want the non-disabled one, otherwise I get an error.

cy.select() failed because this <option> you are trying to select is currently disabled

cypress-bot[bot] commented 2 years ago

Released in 8.5.0.

This comment thread has been locked. If you are still experiencing this issue after upgrading to Cypress v8.5.0, please open a new issue.