Cypress: cannot find elements in calendar popup - angularjs

I cannot interact with the date picker (popup) in my Cypress tests.
I tried .find() and .get() on every div class but each time it says
Timed out retrying after 4000ms: Expected to find element: .cdk-overlay-container, but never found it
This is my test code:
cy.get('#signup-step-pers-details').within(() => {
cy.get('[name="firstName"]').type(user.firstName)
.get('[name="surname"]').type(user.lastName)
.get('#select-gender').click()
.get('.ng-dropdown-panel-items').contains(user.gender, {matchCase: false}).click()
.get('#input-dateOfBirth').click()
.find('.owl-dt-popup').click()
.get('.owl-calendar-year').contains(2002).click()
I tried adding some wait time but that didn't help either.

#KKhan is correct, the Angular Date Time Picker opens in a cdk-overlay-container at the foot of the document.
More detail on the layout:
<body>
...
<div id="signup-step-pers-details>
...
<div id="input-dateOfBirth"></div>
</div>
...
<div class="cdk-overlay-container">
<owl-date-time-container>
...
</owl-date-time-container>
</div>
</body>
Using cy.get('#signup-step-pers-details').within(() => { restricts commands inside to that section of the DOM, but the owl-date-time-container is outside of that.
You can use this approach Cypress how to temporarily escape from a cy.within()
cy.get('#signup-step-pers-details').within(() => {
// cy.root() === #signup-step-pers-details
cy.get('[name="firstName"]').type(user.firstName)
.get('[name="surname"]').type(user.lastName)
.get('#select-gender').click()
.get('.ng-dropdown-panel-items').contains(user.gender, {matchCase: false}).click()
.get('#input-dateOfBirth').click()
// shift cy.root() to date-time-picker
cy.document().its('body').find('owl-date-time-container').within(() => {
cy.get('button.owl-dt-control-period-button').click()
cy.contains('2002').click()
cy.contains('Aug').click()
cy.contains('23').click()
})
// back to cy.root() === #signup-step-pers-details
cy.get('#select-nationality').click()
})
Note I've used .owl-dt-control-period-button which is correct for the current version of Angular Date Time Picker, but perhaps you have an older version that requires .owl-calendar-year.

This sequence
.get('#input-dateOfBirth').click()
.find('.owl-dt-popup').click()
is expecting to find the date popup within the dateOfBirth control.
You may simply need
.get('#input-dateOfBirth').click()
.get('.owl-dt-popup').click()
Generally you see cy.get() for each item, not chaining all the gets as you have done. That still works because .get() always starts it's search at cy.root(), which is set to #signup-step-pers-details by the .within().
But the .find() is different, it starts it's search at the previous subject, which is the DOB control.
I should add, in case you were expecting the date popup to actually be inside the DOB input, that cdk-overlay-container is added when the popup is made visible, at the bottom of the <body></body> tag (take a look in devtools).
<div class="cdk-overlay-container">...</div>
</body>

Your approach of using contains is good but there is another syntax you may use for the purpose of selecting a date:
cy.get('#signup-step-pers-details').within(() => {
cy.get('[name="firstName"]').type(user.firstName)
.get('[name="surname"]').type(user.lastName)
.get('#select-gender').click()
.get('.ng-dropdown-panel-items').contains(user.gender, {matchCase: false}).click()
.get('#input-dateOfBirth').click()
.find('.owl-dt-popup').click()
.contains('.owl-calendar-year', '2002').click()

Related

How to get immediate text with webdriverIO

I have html DOM like this:
<div class="alert alert-danger">
<button class="close" aria-hidden="true" data-dismiss="alert" type="button">×</button>
Your username or password was incorrect.
</div>
I would like to get Your username or password was incorrect. text.
If I do:
$('.global-alerts div.alert-danger').getText()
Then I get this ×.
Is there a way to get the only text part inside that div element?
I managed to do something like this:
getErrorMessageText() {
return browser.execute(function () {
const selector = '.global-alerts div.alert-danger';
// #ts-ignore
return $(selector).contents().not($(selector).children()).text().trim();
});
}
And it works.
But does anybody have better idea? Or more of like webdriverIO approach here?
Does it work if you use something like this?
var innerHTML = $('.global-alerts div.alert-danger').getHTML(false);
the false argument tells indicates whether or not to include the selector element tag in the output.
Serious solution
I do not quite see any other way but to use execute in order to "grab" that information from the page.
I would however place it in a browser command (either do it in the config "before" hook, or add a service that adds the command in the before hook)
This is what I ended up with considering typescript as main language, ignoring the use of jQuery, and considering that you use the before hook:
/**
* Gets executed before test execution begins. At this point you can access to all global
* variables like `browser`. It is the perfect place to define custom commands.
* #param {Array.<Object>} capabilities list of capabilities details
* #param {Array.<String>} specs List of spec file paths that are to be run
* #param {Object} browser instance of created browser/device session
*/
before: function (_capabilities: any, _specs: any, browser: WebdriverIO.Browser) {
browser.addCommand(
'getNodeText',
async function() {
return this.execute(
(selector: string) =>
Array.from( document.querySelector(selector).childNodes || [])
.filter((n: HTMLElement) => n.nodeType === n.TEXT_NODE)
.map(n => n.textContent)
.reduce(function(f, c) {return f+c;}, '')
.replace('\n', '')
.trim(),
this.selector
);
},
true
);
},
With this approach, typescript might complain about the function that passed to webdriver to get executed, so you can either write it properly, or just move it to a .js file and be done with it.
Just watch for document.querySelector(selector), in theory, it should not be null since the command is executed on an already found by webdriver element.
The way you grab the text there is just await (await $('.alert.alert-danger').getNodeText());
This should return the full string from within the node itself, but not any subchild.
Note: If you end up with an element like: <div id="mytxt">my text style is <strong>strong</strong> and <italic> italic </italic>. - html fan</div> and you do this getNodeText(), you probably end up with the value my text style is and . - html fan.
The "don't get bothered to much" solution
This approach will also sort of check that the "x" button is still there.
await expect($('.global-alerts div.alert-danger')).toHaveText('xYour username or password was incorrect.')
in latest version of WebDriverIO (v8), you can use this selector: aria/YourContent. For example:
With the DOM like this:
<h1>Hello World!</h1>
You can use this selector
console.log(await $('aria/Hello World!').getText()) // outputs: "Hello World!"
Ref: https://webdriver.io/docs/selectors/#fetch-by-content

Ask to confirm when changing tabs in angular bootstrap

I have tabs with forms and I want ask the user to confirm or discard their changes when changing tabs. My current code works
<uib-tab heading="..." index="3" deselect="main.onDeselect($event)" ... >
this.onDeselect = function($event) {
if(...isDirty...) {
if($window.confirm("Do you want to discard?")) {
... discard (and go to new tab) ...
} else {
$event.preventDefault(); //stays on current tab
}
}
}
The problem is I want to change confirm to javascript dialog and I will get result in callback.
I planed to preventDefault() all and then switch manually, but I cannot figure out where to get new tab id.
Any solution is appreciated. Even if it is easier in other tab implementations.
I use AngularJS v1.4.7, ui-bootstrap-tpls-1.3.3.min.js
You can make use of $selectedIndex and the active property for that purpose. See this Plunk
One thing to be noted here is that when we manually change the active property, it again fires the deselect event which needed to be handled. Otherwise it seems to do what you wanted.
Edit
Indeed as noted in the comments, the deselect carries the HTML index rather than what is passed in in the tab index property. A workaround could be in this: Another Plunk. Here I'm pulling the actual index from the HTML index.
And a little research indicates that this issue might as well be fixed already with 3.0 bootstrap tpl See this.
I spent some time with different approaches and this one is stable for some time. What I do is to prevent deselect at the beginning and set the new tab in callback if confirmed to loose changes...
this.onDeselect = function($event, $selectedIndex) {
var me = this;
if(this.tabs.eventDirty || this.tabs.locationDirty || this.tabs.contractDirty) {
$event.preventDefault();
var alert = $mdDialog.confirm({
title: 'Upozornění',
textContent: 'Na záložce jsou neuložené změny. Přejete si tyto změny zrušit a přejít na novou záložku?',
ok: 'Přijít o změny',
cancel: 'Zůstat na záložce'
});
$mdDialog
.show( alert )
.then(function() {
$rootScope.$emit("discardChanges");
me.tabs.activeTab = $selectedIndex;
})
}
};

AngularJS UI-calendar not updating events on Calendar

I am using Angular UI-Calendar to show some events on the Calendar. The events are showing fine on the Calendar. But when I update any event's details, the event's detail is actually modified, but not modified on the Calendar display(eg: start).
Initially, after I modified the event's details, I did a page reload to display modified changes and it worked too.In that method, I had empty $scope.events = []; array, which I filled after retrieving entries from DB.
But now, I want to avoid that page reload. So, once the event's details are modified from modal window, I clear the contents of $scope.events array using $scope.events = []; and then using API call, I fill the new events again in $scope.events array. This is working fine as the List view shows the modified events details. But the Calendar itself shows old entries. e.g if I change start from 11 April to 13 April, the calendar shows event on 11 April whereas List views shows the modified data i.e 13 April. Using Page reload, corrects this i.e event is shown on modified date(13 April).
How can I ensure that the event is modified on Calendar too without a Page reload ?
I tried calendar.fullCalendar('render'); after fetching new entries from DB, but it does not solve the Problem.
Here are some codes :
Initially I did this to send data to DB and then did a page reload to retrieve updated data.
$http.put(url,senddata).success(function(data){$window.location.reload();});
Now I want to avoid the page reload, so I did
$http.put(url,senddata).success(function(data){//$window.location.reload();//sends modified data to server
$scope.events = []; //clear events array
$http.get(url2).success(function(data){ //fetch new events from server, and push in array
$scope.schedule = data;
for(var i=0;i<data.length;i++)
{
$scope.events.push({
idx: data[i].idx,
title: data[i].title,
description : data[i].description,
allDay: false,
start: new Date(data[i].start),
end: new Date(data[i].end),
});
calendar.fullCalendar('render'); //Tried even this, didn't work
}
});
Above code pushes modified event in event array as visible in List view, but calendar still shows old event until page is reloaded.
Try maintaining the same array instance.
Instead of doing:
$scope.events = []
Try:
$scope.events.slice(0, $scope.events.length);
Then when your server request comes back, add each item individually to the existing array:
for(var i = 0; i < newEvents.length; ++i) {
$scope.events.push(newEvents[i]);
}
The reason I suggest this is because what you're describing sounds like the calendar might be holding onto the old list of events. The calendar might not know to update its reference, so instead, let it keep the same reference but change the contents of the array.
Just a quick correction to Studio4Development's answer. You should use "splice" not "slice". Slice returns the trimmed array. What we want to do is actually alter the original array. So you can use:
$scope.events.splice(0, $scope.events.length)
and to add new events:
$scope.events.push(newEvent)
Don't know if you found the solution to your problem, but what worked for me was:
calendar.fullCalendar('refetchEvents');
Here is how I fixed a similar problem on my page.
view (simplified, note using jade)
div#calendarNugget(ng-show="activeCalendar == 'Nugget'" ui-calendar="uiConfig.calendarNugget" ng-model="eventSources")
div#calendarWillow(ng-show="activeCalendar == 'Willow'" ui-calendar="uiConfig.calendarWillow" ng-model="eventSources2")
controller:
As per ui-calendar docs, I start with an empty array for my event sources. Ignore that I should probably rename these eventSources to something about the shop names (willow, nugget)
$scope.eventSources = [];
$scope.eventSources2 = [];
I then call a factory function that makes an http request and pulls a series of events from the DB. The events all have "title", "start", "end" properties (make sure your Date format is correct). They also have a "shop" property, which tells me which calendar to add the event to.
So after receiving the data I make two local arrays, loop through the received http data, and assign the events to those local arrays by shop. Finally, I can re-render the calendars with the proper event data by calling addEventSource, which automatically re-renders as per the fullCalendar docs
It looks something along the lines of this iirc:
function splitShiftsByShop(shifts) {
var nuggetShifts = [];
var willowShifts = [];
for (var i=0; i<shifts.length; i++) {
if (shifts[i].shop === "Nugget") {
var nshift = {
title : shifts[i].employee,
start : new Date(shifts[i].start),
end : new Date(shifts[i].end),
allDay: false
}
nuggetShifts.push(nshift);
} else if (shifts[i].shop === "Willow") {
var wshift = {
title : shifts[i].employee,
start : new Date(shifts[i].start),
end : new Date(shifts[i].end),
allDay: false
}
willowShifts.push(wshift);
}
}
/*render the calendars again*/
$('#calendarNugget').fullCalendar('addEventSource', nuggetShifts);
$('#calendarWillow').fullCalendar('addEventSource', willowShifts);
}
I was having some similar issues where events weren't being refetched after emptying my "eventSource" array ($scope.eventSources = [$scope.completedEvents]) and repopulating it with new events. I was able to overcome this at first by calling 'removeEvents',
uiCalendarConfig.calendars.calendar.fullcalendar('removeEvents')
This is hackish, so after further tinkering I found that events were refetched when my child array is modified,
$scope.completedEvents.splice(0, $scope.completedEvents.length)
After reviewing the source, I can see that the eventSource array is being watched, but the 'refetch' is never occurring. furthermore, I was never able to get the following to work,
uiCalendarConfig.calendars.calendar.fullcalendar('refetchEvents')
According to ui-calendar code, it does actually watch eventSources but never the actual individual sources. So doing a push to, let's say $scope.events ($scope.eventSources = [$scope.events]), will never trigger anything.
So push events to $scope.eventSources[0]
$scope.eventSources[0].splice(0, $scope.eventSources[0].length);
$scope.eventSources[0].push({
title : title,
start : start,
end : end
});
As you already know Full Calendar is dependant on JQuery. Therefore, all you need to do is put an ID on your calendar like this:
<div id="calendar" ui-calendar="uiConfig.calendar" class="span8 calendar" ng-model="eventSources"></div>
and then when ever you want to update the calendar just call:
$('#calendar').fullCalendar('refetchEvents')
I struggled with this for some time as well and this fixed all my problems. I hope this will help you too!
Goodluck!
Although it's late response but hope it may help some. I am putting it forth step by step.
You will need to maintain the same event source as Studio4Development mentioned earlier.
$scope.events //no need to reinitialize it
You will remove the updated event from events array like this (for last event in event array):
$scope.events.splice($scope.events.length - 1, 1);
For event that may exist anywhere in the events array you'll need to find its index using:
var eventIndex = $scope.events.map(function (x) { return x.Id; }).indexOf(eventId);
and then you'll remove it from events array like this:
$scope.events.splice(eventIndex, 1);
Next you'll fetch this event from the API again like this:
getEvent(data.Id);
and in the callback method you'll again add the event in events array:
var getSingleEventSuccessCallback = function (event) {
$scope.events.push(event);
};
Was stuck on this for a while.
What seems to be working for me turned out to be quite simple
I fetch my calendar from google and run an event formatter function. In this I define a new array and at the end of the function I set my eventSource equal to the new array. Then I call $scope.$apply
x = []
for e in events
x.push(
title: e.summary
etc...
)
$scope.eventSource = x
$scope.$apply()
Try this. It worked for me.
function flushEvents()
{
$scope.events.splice(0,$scope.events.length);
for(var i=0; i<$scope.events.length; i++)
{
$scope.events.splice(i,1);
}
}
I found this and it helped me:
$(id_your_div).fullCalendar('removeEvents');
$(id_your_div).fullCalendar('refetchEvents');

Filtering spcific with checkboxes

My question is that I want a filtering system that will filter by checked checkboxes.
The tool is to my comparison website where I compare TV packages.
my visitors should filter the packages by the tv-channels they want to se.
example;
Checkbox 1: Discovery
Checkbox 2: Animal PLanet
Checkbox 3: Disney Channel
Output should be the matching TV-package
Package 1: (contains Discovery and Disney channel)
Package 2: (contains Animal Planet, Disney channel)
Package 3: (contains Animal Planet)
So if checkbox 1 is checked it should only show package 1.
if checkbox 1 + checkbox 2 is checked it should say "No match found, but this package was was closest to your choice"
if checkbox 2 + checkbox 3 is checked it should only show package 2 which match the visitors choice exactly.
I hope your can help me out. I have been searching a lot after this specific solution without any success.
I think it should be in Jquery. i have seen some simular filtering examples, but no one there are like my wish above.
This is an old question, but... I'll take a shot. Working jsfiddle: http://jsfiddle.net/a0nnrfua/
I think a lot really depends on how you intend to define "closest", but presuming a jQuery solution and hopefully your browser requirements aren't TOO far in the past, you could use the data- attributes and jQuery to come up with some relatively simple functions. Or even use the value portions of the checkboxes really.
Psuedocode, it would look like:
Define a click or change handler to detect whenever a checkbox has been touched/changed.
Define a function that will scan all checked items and pass the values into your "closest package" function.
Based on the results of that function, filter your package selection so that your choices are highlighted or marked in some way.
So let's presume the following HTML markup:
<h3>TV Channels</h3>
<div id="TVChannelSelections">
<input type="checkbox" class="tvchannel" name="tvchannel" id="tvchannel_Discovery" value="Discovery" />
<label for="tvchannel_Discovery">Discovery Channel</label>
<br/>
<input type="checkbox" class="tvchannel" name="tvchannel" id="tvchannel_AnimalPlanet" value="Animal Planet" />
<label for="tvchannel_AnimalPlanet">Animal Planet</label>
<br/>
<input type="checkbox" class="tvchannel" name="tvchannel" id="tvchannel_DisneyChannel" value="Disney Channel" />
<label for="tvchannel_Disney">Disney Channel</label>
<br/>
</div>
<div id="message"></div>
<h3>Packages</h3>
<div id="FilteredPackages">
<div class="package deselected" id="Package1" data-channels="Discovery,Disney Channel">Package #1</div>
<div class="package deselected" id="Package2" data-channels="Animal Planet,Disney Channel">Package #2</div>
<div class="package deselected" id="Package3" data-channels="Animal Planet">Package #3</div>
</div>
So in jQuery, your generic change or click handler would be defined in code: Note that I'm saying, any element on your page that has the class "tvchannel" defined, if there's ever a change that occurs, run my filter function.
<script type="text/javascript" src="../path/to/jQuery/library"></script>
<script type="text/javascript">
$(document).ready(function() {
$(".tvchannel").on("change", function() {
FilterMySelectedChannels();
});
});
</script>
Now we can define your Filter function. We're going to assume two things. #1, that we want to find all the selected checkboxes and their values. Then we're going to iterate through the data-channels property of all of our packages (defined as elements with class = "package"). We'll use some form of string comparison and boolean logic to define what a complete match is vs. a close but no cigar match vs. a complete fail.
In order to keep track of things I'm using 3 classes, selected, deselected, and close.
In css, you can decide whether you want notselected to mean "hide the package completely" (i.e. display: none;) or maybe you want it to be visible but greyed out and "struck out" (i.e. text-decoration: strikethrough; color: grey;}
I'm going to use kind of a brute force way of doing the comparison. There are better array functions and comparison functions in javascript, but this should be relatively clear for most people and I trust the good folks at stackoverflow to chime in with better solutions. But this should get you started. :)
<script type="text/javascript">
function FilterMySelectedChannels() {
$checkedboxes = $(".tvchannel:checked");
$packages = $(".package");
var bAnyFound = false;
$packages.each(function () {
var bCloseButNoCigar = false;
var bCompleteMatch = true;
var packagearray = $(this).data("channels").split(",");
var $currentPackage = $(this);
$checkedboxes.each(function () {
if ($.inArray($(this).val(), packagearray) != -1) {
bCloseButNoCigar = true;
} else {
bCompleteMatch = false;
}
});
if (bCompleteMatch) {
$currentPackage.removeClass("selected").removeClass("deselected").removeClass("close").addClass("selected");
bAnyFound = true;
} else if (bCloseButNoCigar) {
$currentPackage.removeClass("selected").removeClass("deselected").removeClass("close").addClass("close");
} else {
$currentPackage.removeClass("selected").removeClass("deselected").removeClass("close").addClass("deselected");
}
});
if (bAnyFound) {
$("#message").html("The following matches were found");
} else {
$("#message").html("No actual matches were found, but here are some close matches based on your selections");
$(".package.close").removeClass("deselected").removeClass("close").removeClass("selected").addClass("selected");
}
}
</script>
<style type="text/css">
.selected {
color: red;
background-color: yellow !important;
}
.deselected {
color: grey;
text-decoration: strike-through !important;
background-color: white !important;
}
</style>
There are obvious optimizations that could probably work here, but it's a start for those trying to do something similar. Note that it assumes that your markup is dynamically generated or properly coded. If you need to guard against human typos, converting your text using .toLowerCase/UpperCase and using the .Trim functions to eliminate extra space will assist. But you still have to choose your data values wisely so there's no overlap. And if you choose them well enough you can use better techniques such as regular expressions and wildcard searches to make the code a bit shorter.
Hope this helps someone!

Filtering data using ajax observefield

I ve tried to implemented filtering data (list base on selected category) using dropdown with observefield and ajax pagination. I use session to remember selected category in order to keep pagination.
Here is my code (Cakephp 1.2)
In view :
echo $form->select('Category.id', $categories, null, array('id' => 'categories'),'Filter by Categories')
echo $ajax->observeField('categories' ,array('url' =>'update_category','update' => 'collectionDiv'));
In Controller:
if(!empty($this->data['Category']['id']))
{
$cat_id=$this->data['Category']['id'];
$filters=array('Collection.category_id' => $cat_id);
$this->set('collections', $this->paginate('Collection', $filters));
$this->Session->write($this->name.'.$cat_id', $category_id);
}
else
{
$cat_id=$this->Session->read($this->name.'.cat_id');
$filters=array('Collection.category_id' => $cat_id);
$this->set('collections', $this->paginate('Collection'));
}
The filter work as I wanted but the problem is when I select empty value('Filter by Category) it still remember last category session so I can't back to the default list (All record list without filter).
I've tried to make some condition but still not success. Is there another way? Please I appreciate your help. thank
hermawan
Perhaps I don't understand the question, but it looks to me like it might be worth changing:
else
{
$cat_id=$this->Session->read($this->name.'.cat_id');
$filters=array('Collection.category_id' => $cat_id);
$this->set('collections', $this->paginate('Collection'));
}
to:
else
{
$this->set('collections', $this->paginate('Collection',array()));
}
In effect your code appears to be doing this anyway. Check what the URL is at the top of the browser window after it has returned. Does it still contain pagination directives from the previous query?
You might want to review http://book.cakephp.org/view/167/AJAX-Pagination and make sure you've 'ticked all the boxes'.
I got it, It work as I hope now. I my self explain the condition and solution.
When I select category from combobox, then it render the page the code is :
If ($this->data['Category']['id']) {
$cat_id=$this->data['Category']['id'];
$this->Session->write($this->name.'.category_id', $category_id);
// I will use this session next when I click page number
$filters=array('Collection.art_type_id' => $category_id);
$this->set('collections', $this->paginate('Collection', $filters));
}else{
//if clik page number, next or whatever, $this->data['Category']['id'] will empty so
// use session to remember the category so the page will display next page or prev
// page with the proper category, But the problem is, when I set category fillter to
// "All Category" it will dislpay last category taked from the session. To prevent it
// I used $this->passedArgs['page']. When I clik the page paginator it will return
// current number page, but it will be empty if we click dropdown.
if (!empty($this->passedArgs['page']) {
$cat_id=$this->Session->read($this->name.'.category_id');
$filters=array('Collection.category_id' => $cat_id);
$this->set('collections', $this->paginate('Collection',$filters));
}else{
$this->set('collections', $this->paginate('Collection'));
}
}
From this case, I think that observefield will not send passedArg as we get from url such from ajax link or html->link. I hope this will usefull to anybody

Resources