How to click in a select element with several options with Cypress? - javascript

I have a problem in clicking in a select element using cypress.
This is the element to click:
And i receive this error.
cy.click() cannot be called on a <select> element. Use the cy.select() command instead to change the value.
This is my code:
cy.get('[name^=shopveg]').click()
After searching some people advise using the trigger command.
Changed the code to:
cy.get('[name^=shopveg]').trigger('click')
The step pass in the cypress execution but the click was not executed.

As the message says, .select() is the command to use
cy.get('[name^=shopveg]').select('Bananas')
That's because <select> behavior is implemented by a web component (see slot tag), not javascript, so there's no click handler available.

Given the select looks like a "special" implementation, i.e a web component with slots, you may need to click it open to see the option values (lazy loading).
If so, try with cypress-real-events plugin
Install
npm install cypress-real-events
// or
yarn add cypress-real-events
In /cypress/suport/e2e.js
import "cypress-real-events";
Test
cy.get('[name^=shopveg]')
.should('have.value', '1') // confirm initial value
.realClick() // click open
cy.contains('option', 'Batatas') // this option exists
cy.contains('option', 'Arroz') // this option exists
cy.contains('option', 'Bananas') // this option exists
// etc
cy.get('[name^=shopveg]')
.select('Bananas') // select new option
.should('have.value', '3') // confirm new value

To validate the values in the select menu, you can iterate through them with cy.each().
const options = [ "Batatas", "Arroz", "Bananas", "Tomates", "Macaz" ];
cy.get('[name^=shopveg]').find('option').each(($el, index) => {
cy.wrap($el).should('have.text', options[index]);
});
As #Grainger answered, you can then use cy.select() to pick a value.

Related

Unable to select item in cypress inside a frame

I have managed to log into a laboratory results system but I can't progress any further. My long term intention is to grab lab results for local incorporation into a medical record.
const getIframeBody = () => {
// get the iframe > document > body
// and retry until the body element is not empty
return cy
.get('frame[id="EclairMainFrame"]')
.its('0.contentDocument.body').should('not.be.empty')
// wraps "body" DOM element to allow
// chaining more Cypress commands, like ".find(...)"
// https://on.cypress.io/wrap
.then(cy.wrap)
}
describe('Medlab Test', () => {
it('Visits Eclair', () => {
cy.visit('https://cdr.medlabcentral.co.nz/Eclair/mvc')
cy.get('.login-field').eq(0).type('userid')
cy.get('.login-field').eq(1).type('passphrase{enter}')
cy.location('pathname').should('eq', '/Eclair/ClinicalWorkstation.aspx')
getIframeBody().find('.search-form-form-textbox')
})
})
This gets me logged in, but Cypress fails to find the input field to enter the Patient ID. I see the following error message:
Timed out retrying after 6000ms: Expected to find element:
.search-form-form-textbox, but never found it. Queried from element: <body>
The DOM at this point for that input field shows:
<input class="search-form-form-textbox Mandatory" id="PatAliasId" name="PatAliasId" type="text" value="" tabindex="4">
I've tried cy.get on the class cy.get('.search-form-form-textbox'), on the id cy.get('#PatAliasId'), and on class attribute cy.get('name=["PatAliasId"]') but all these attempts time out at 6 seconds.
What's the correct syntax for finding a dom element inside a frame?
To work with frames in Cypress you have to install a cypress plugin with the following command, then you can interact with elements inside the frame
npm install -D cypress-iframe
Reference URL
Cypress Frames
If you have done the above already, then load the frame first to find the required element
cy.frameLoaded(#frameId)
cy.iframe.find('#PatAliasId').type('abc')
In previous version of cypress, don't have any support for the iFrame. But in latest version cypress provided the support for iFrames.
Do work on cypress , we need to install cypress-iframe by using the below commands.
npm install -D cypress-iframe
Use this below code:
describe('Medlab Test', () => {
it('Visits Eclair', () => {
cy.visit('https://cdr.medlabcentral.co.nz/Eclair/mvc')
cy.frameLoaded('#frameId')
cy.iframe.find('#PatAliasId').type('Enter Your Input')
})
})

Cypress testing a Material-UI datepicker is not working on Github actions

We have a strange behavior when running our cypress tests in a github action. MUI datepicker the datepicker is in readonly mode and we can't enter any date (it's fine in other environments).
Error in Cypress
CypressError: Timed out retrying after 4000ms: cy.clear() failed because this element is readonly:
<input aria-invalid="false" readonly="" type="text" aria-readonly="true" aria-label="Choose date" class="MuiOutlinedInput-input MuiInputBase-input css-1x5jdmq" value="">
Visually looks the date picker does not have any button (something is going on) :
The console logs show no error or warning
On other environments, windows/linux, the tests work fine, even though we launch the test in headless mode (all desktops with a UI). The MUI datepicker looks as nice as in MUI documentation (link).
Github action looks like :
on:
workflow_dispatch:
defaults:
run:
working-directory: ic3-test
jobs:
build:
runs-on: ubuntu-latest
container: cypress/included:8.6.0
steps:
- uses: actions/checkout#v2
- uses: actions/setup-node#v2
with:
node-version: '16'
- name: Install dependencies
run: npm install
working-directory: ic3-test
- name: Cypress run with env
uses: cypress-io/github-action#v2
with:
# headless: true
browser: chrome
record: true
working-directory: ic3-test
The Cypress line that generates the error :
cy.getWidget(widgetId). // this is getting a div with wid = widgetId , works fine
.find("input.MuiInputBase-input")
.clear()
.type(date). // date is a string
Some hints are welcomed
We had the same error when running cypress test in azure devops pipelines. And I think its the same reason, looking at your screenshot of the date picker without any button.
The input which was giving us the error was #mui/lab/DatePicker.
We found that this component is rendering as #mui/lab/MobileDatePicker when we ran the cypress tests in azure devops pipelines. It's explained here: docs.
That version not accept direct text input, but opens a dialog to pick/input date, therefore cypress test fails when trying to type into the input.
Our solution was to use #mui/lab/DesktopDatePicker directly.
The reason behind this is that Material UI renders the MobileDatePicker component, since the query #media (pointer: fine) doesn't match on the headless Chrome used by our Github Actions Workflow. The mobile component only has readonly inputs, therefore it can't be cleared or typed into with .type() and .clear() (as opposed to the DesktopDatePicker component, which has typable and clearable inputs).
Since we don't want to remove the MobileDatePicker component, we've created a custom command so it checks if the mobile date picker is currently being rendered. If on mobile, the test opens the date picker, clicks on the edit button, so the mobile input view is opened.
And in that input view, the input field is not readonly anymore. Using that command, no matter if desktop or mobile, the input field can be cleared and typed into.
Cypress.Commands.add(
'chooseDatePicker',
(selector: string, value: string) => {
cy.get('body')
.then(($body) => {
const mobilePickerSelector = `${selector} input[readonly]`;
const isMobile = $body.find(mobilePickerSelector).length > 0;
if (isMobile) {
// The MobileDatePicker component has readonly inputs and needs to
// be opened and clicked on edit so its inputs can be edited
cy.get(mobilePickerSelector)
.click();
cy.get('[role="dialog"] [aria-label="calendar view is open, go to text input view"]')
.click();
cy.get(`[role="dialog"] ${selector}`)
.find('input')
.clear()
.type(value);
cy.contains('[role="dialog"] button', 'OK')
.click();
} else {
cy.get(selector)
.find('input')
.clear()
.type(value);
}
});
},
);
// Usage:
const datePickerValue = '2021-01-03';
cy.chooseDatePicker('[data-testid="my-datepicker"]', datePickerValue);
Since I haven't seen all the code, I will try to comment as specific as I can, I would like you to review few topics.
1- Examine the properties in the transformed code. Make sure a Property such as helperText={null} is should not set.
2- You might need to install polyfills. For instance for the popper.js (transitive dependency). Although MUI claims that it is (polly) not needed. Technology is advancing every day and there may be changes that they cannot catch.
3- Be sure working right mode. Even test it with Jest. Such window.matchMedia.
Cypress.Commands.add('setDateField', ({ date, label }) => {
//open the datepicker dialog if is mobile
cy.contains(label).siblings('div').click()
cy.get('body').then(($body) => {
if ($body.find('[role=dialog]').length) {
cy.get('[data-testid=PenIcon]').click()
cy.get('[role=dialog]').contains(label).type(date)
cy.get('[role=dialog]').contains('Confirmar').click()
return
}
cy.contains(label).siblings('div').type(date)
})
})

How to click on each payment method in cypress using iteration?

My requirement is to click on each and every payment method (pay-safe, visa, bit-pay etc.)
and then validate using assert method by comparing URL.
Problem : Unable to click on element. I'm getting null value in variable. Tried using val() as well as html() method.
I tried below code.
//cy.get('.real-money--providers-list') = allPaymentMethods
depositFiat.allPaymentMethods().find('[src*="providers/logo"]').each(($element, index, $list) => {
var namePaymentProvider = $element.find('[alt*="safe"]').text()
cy.log(namePaymentProvider)
cy.wait(1000)
if(namePaymentProvider.includes('class')){
$element.find('.provider-content--choice').click()
//cy.get('.provider-content').invoke('removeAttr','src').click()
//depositFiat.secureCheckout().click()
//cy.back()
}
})
As cypress unable to handle child windows I tried to use invoke method but no luck.
Find HTML here
<div class="provider-img"><img alt="safecharge_paysafecard" class="style__Logo-a3ugi5-2 fAwRoV visible" src="https://static.xyz.com/1234123463/img/providers/logo_safecharge_paysafecard.svg"></div>
As per your HTML fiddle, I could see that for every payment provider you can use the css selector img[class*="style__Logo"]
For one payment method you can use:
cy.get('img[class*="style__Logo"]').eq(0).invoke('attr', 'src').should('contain', 'https: //static.xyz.com/')
You are finding an image, then trying to click on it.
Most likely the click-event sits on the button
Instead try to click on the button:
cy.get('.provider-content').each($element => {
cy.wrap($element).click()
// Assert something here
})
If the click action opens up a new tab/window, and you want to assert that it moved you do this new link, then Cypress does not support this directly.
Instead, you would either get the url that should be opened by the click and verify that.
Or
Stub the browser window so that the new tab opens up in the same tab you are currently in.
You can use Recursion and Jquery .removeAttr :
cy.get('[src*="providers/logo"]') //You need to make sure here is the correct selector that covers all methods here
.then(methods => {
checkPaymentMethod
function checkPaymentMethod(methodNumber = 0) {
if(methodNumber < methods.length) {
Cypress.$(methods[methodNumber]).removeAttr("target");
cy.get(methods[methodNumber])
.click()
.should('not.exist')
cy.url().should('eq', 'targetUrl')
cy.visit('yourPageUrl')
cy.url().should('eq', 'yourPageUrl')
methodNumber ++
checkPaymentMethod(methodNumber)
}
}
})

Select elements in HTML via cypress.get()

I'm using cypress to write some tests against an html site..
The following selects me correctly a single tr elements from a table on my HTML site.
The site contents looks like this:
<tr data-recordid="theId">
<td...><div ..>Text 1</div></td>
<td...><div ..>Text 2</div></td>
<td...><div ..>Text 3</div></td>
</tr>
The following test script snippet selects me correctly the single <tr..> part.
cy.get('tr[data-recordid="theId"]').contains('Text')
Now I want to select the text within the <div>..</div> tags..The first thing I have tried to chain a single call for the first <div>..</div> tag like this:
cy.get('tr[data-recordid="theId"]').get('div').contains('Text')
which does not work as I expected. The get() calls a chained jQuery calls (Based on the Docs of cypress). So it looks like I misunderstand how things work in JQuery.
What I'm expecting is how I can check all div elements like this (Not working):
cy.get('tr[data-recordid="theId"]')..SomeHowMagic
.get('td[alt="xyz"]".get('div').contains('Text 1')
.get('td...').get('div').contains('Text 2')
.get('td...').get('div').contains('Text 3')
Any idea how to get forward a step? Missing any information just make a comment.
Let's clarify a few things:
1) If you are just wanting to ASSERT that the div's contain the given text then this is the best possible and most precise way to do this:
cy.get('tr[data-recordid="theId"]').should(($tr) => {
const $divs = $tr.find('div') // find all the divs
expect($divs.eq(0)).to.contain('Text 1')
expect($divs.eq(1)).to.contain('Text 2')
expect($divs.eq(2)).to.contain('Text 2')
})
I can't tell if things need to be this specific. If you only want to ensure that the $tr contains text you could simplify it down to be:
cy.get('tr[data-recordid="theId"]').should(($tr) => {
expect($tr).to.contain('Text 1')
expect($tr).to.contain('Text 2')
expect($tr).to.contain('Text 2')
})
Why do it this way?
Using a .should() function will not change the subject. Your $tr will continue to be the subject going forward.
Cypress will wait until all of the assertions in the .should() callback pass, and continually retry until they do. That guarantees you the state of multiple elements is correct before proceeding.
2) However if you just care that Cypress finds the text and you don't mind the subject being changed you could do this:
cy.get('tr[data-recordid="theId"]').within(() => {
cy.contains('Text 1') // changes the subject to the <div>
cy.contains('Text 2') // changes the subject to the <div>
cy.contains('Text 3') // changes the subject to the <div>
})
This is different than the first example because instead of an explicit assertion you are simply changing the subject to whatever element the text is found in. Cypress's default assertion on cy.contains() is to retry so ultimately the behavior is the same, except you are additionally changing the subject.
If even this is too complicated you could also just do this:
cy.get('tr[data-recordid="theId"] div').contains('Text 1')
cy.get('tr[data-recordid="theId"] div').contains('Text 2')
cy.get('tr[data-recordid="theId"] div').contains('Text 3')
Your original question was also using chained cy.get() which does not drill into subjects. For that to happen use .find()
cy.get('a').get('span') // each cy.get() queries from the root
cy.get('a').find('span') // the .find() queries from the <a>
One final note: you suggested solution does not work. cy.get() does not accept a callback function, and if you look at your Command Log you will not see those 3 cy.contains from ever being invoked. In other words, they are not running. That's why its passing.
So after more experimenting I found a solution:
cy.get('tr[data-recordid="TheId"]>td> div', function() {
cy.contains('Text 1').end()
cy.contains('Text 2').end()
cy.contains('Text 3').end()
})
If someone else has a better solution please post it here.

Programmatically set the value of a Select2 ajax

I have a Select2 auto-complete input (built via SonataAdmin), but cannot for the life of me figure out how to programmatically set it to a known key/value pair.
There's a JS Fiddle here that shows roughly what I have. What I want to know is what function I can attach to the button so that
the Select2 field shows the text "NEW VALUE" to the user, and
the Select2 field will submit a value of "1" when the form is sent to the server
I have tried all sorts of combinations of jQuery and Select2 data and val methods, called against various inputs on the page, but nothing seems to work... surely there's some way to do this?
-- Edit --
The accepted answer below is very useful, helps shed some light on the right way to initialise the selection and explains what initSelection is for.
Having said that, it seems that my biggest mistake here was the way I was trying to trigger the change.
I was using:
$(element).select2('data', newObject).trigger('change');
But this results in an empty add object inside select2's change event.
If, instead, you use:
$(element).select2('data', newObject, true);
then the code works as it should, with the newObject available in select2's change event and the values being set correctly.
I hope this extra information helps somebody else!
Note this was tested with version 4+
I was finally able to make progress after finding this discussion: https://groups.google.com/forum/#!topic/select2/TOh3T0yqYr4
The last comment notes a method that I was able to use successfully.
Example:
$("#selectelement").select2("trigger", "select", {
data: { id: "5" }
});
This seems to be enough information for it to match the ajax data, and set the value correctly. This helped immensely with Custom Data Adapters.
Note: For multi select, execute the above code for each item, like this :
// for each initially selected ids, execute the above code to add the id to the selection.
[{id: 5, text: 'op5'}, {id: 10, text: 'op10'}].forEach(option => {
$("#selectelement").select2("trigger", "select", {data: { id: option.id, text: option.text }});
})
Note: The Question and this Answer are for Select2 v3. Select2 v4 has a very different API than v3.
I think the problem is the initSelection function. Are you using that function to set the initial value? I know the Select2 documentation makes it sound like that is it's purpose, but it also says "Essentially this is an id->object mapping function," and that is not how you have implemented it.
For some reason the call to .trigger('change') causes the initSelection function to get called, which changes the selected value back to "ENABLED_FROM_JS".
Try getting rid of the initSelection function and instead set the initial value using:
autocompleteInput.select2('data', {id:103, label:'ENABLED_FROM_JS'});
jsfiddle
Note: The OP has supplied the formatResult and formatSelection options. As supplied, those callback functions expect the items to have a "label" property, rather than a "text" property. For most users, it should be:
autocompleteInput.select2('data', {id:103, text:'ENABLED_FROM_JS'});
More info on the initSelection function:
If you search through the Select2 documentation for "initSelection", you will see that it is used when the element has an initial value and when the element's .val() function is called. That is because those values consist of only an id and Select2 needs the entire data object (partly so it can display the correct label).
If the Select2 control was displaying a static list, the initSelection function would be easy to write (and it seems like Select2 could supply it for you). In that case, the initSelection function would just have to look up the id in the data list and return the corresponding data object. (I say "return" here, but it doesn't really return the data object; it passes it to a callback function.)
In your case, you probably don't need to supply the initSelection function since your element does not have an initial value (in the html) and you are not going to call its .val() method. Just keep using the .select2('data', ...) method to set values programmatically.
If you were to supply an initSelection function for an autocomplete (that uses ajax), it would probably need to make an ajax call to build the data object.
To set initial values you need to add the necessary options tag to the select element with jQuery, then define these options as selected with select2's val method and finally trigger select2's 'change' event.
1.-$('#selectElement').append('<option value=someID>optionText</option>');
2.-$('#selectElement').select2('val', someID, true);
The third boolean argument tells select2 to trigger the change event.
For more info, see https://github.com/select2/select2/issues/3057
Be carreful, there is a mistake in "validated" comment.
autocompleteInput.select2('data', {id:103, label:'ENABLED_FROM_JS'});
The correct way is
autocompleteInput.select2('data', {id:103, text:'ENABLED_FROM_JS'});
Use text instead of label
With Select2 version 4+, there is actually nothing special you need to do. Standard jQuery with a 'change' event trigger at the end will work.
var $select = $("#field");
var items = {id: 1, text: "Name"}; // From AJAX etc
var data = $select.val() || []; // If you want to merge with existing
$(items).each(function () {
if(!$select.find("option[value='" + this.id + "']").length) {
$select.append(new Option(this.text, this.id, true, true));
}
data.push(this.id);
});
$select.val(data).trigger('change'); // Standard event notifies select2
There is a basic example in the Select2 documentation:
https://select2.org/programmatic-control/add-select-clear-items
from their examples
https://select2.github.io/examples.html
Programmatic access:
var $example = $(".js-example-programmatic").select2();
var $exampleMulti = $(".js-example-programmatic-multi").select2();
$(".js-programmatic-set-val").on("click", function () { $example.val("CA").trigger("change"); });
$(".js-programmatic-open").on("click", function () { $example.select2("open"); });
$(".js-programmatic-close").on("click", function () { $example.select2("close"); });
$(".js-programmatic-init").on("click", function () { $example.select2(); });
$(".js-programmatic-destroy").on("click", function () { $example.select2("destroy"); });
$(".js-programmatic-multi-set-val").on("click", function () { $exampleMulti.val(["CA", "AL"]).trigger("change"); });
$(".js-programmatic-multi-clear").on("click", function () { $exampleMulti.val(null).trigger("change"); });
All you have to do is set the value and then execute: $ ('#myselect').select2 (); or $ ('select').select2 ();.
And everything is updated very well.
If you remove the .trigger('change') from your fiddle it logs Object {id: 1, label: "NEW VALUE"} (need to click twice since the logging is before the value change). Is that what you're looking for?
When using select2 with multiple option, use this construction:
$(element).select2("data", $(element).select2("data").concat(newObject), true);
jqueryselect2multiplesetconcatenation
this is it:
$("#tag").select2('data', { id:8, title: "Hello!"});
FOR VERSION 3.5.3
$(".select2").select2('data',{id:taskid,text:taskname}).trigger('change');
Based on John S' answer . Just the the above will work however only if while initializing the select2 the initSelection option is not initialized.
$(".select2").select2({
//some setup
})
For those still using version 3.5 or even higher ones. Please be sure how you reference select2.js to your page. If you are using async or defer load. This plug-in might behave differently.
Thought to mention.
In my situation I was able to render the preselected option into the HTML server side with PHP.
During my page load, I already knew the option value, so my <select name="team_search"></select> became the following;
<select name="team_search">
<?php echo !empty($preselected_team)
? '<option selected="selected" value="'. $preselected_team->ID .'">' . $preselected_team->team_name . '</option>'
: null ?>
</select>';
As you can see, when I have a $preselected_team available I render in an option with the selected attribute, value and label set. And, if I don't have a value then not option is rendered.
This approach may not always be possible (and in the case of the OP is not mentioned), but it does come with the added benefit of being ready on page load ahead of JavaScript execution.
Append a new option with id and text
let $newOption = $("<option selected='selected'></option>").val(1).text('New Text goes here');
$("#selector").append($newOption).trigger('change');

Categories