How can I check until an element is clickable using nightwatchjs? - nightwatch.js

How can I check until an element is clickable using nightwatch js? I want to click on an element but when I run nightwatch, selenium does not click on the element because it is not clickable yet.

Something like this should work. Let me know if you have questions
var util = require('util');
var events = require('events');
/*
* This custom command allows us to locate an HTML element on the page and then wait until the element is both visible
* and does not have a "disabled" state. It rechecks the element state every 500ms until either it evaluates to true or
* it reaches maxTimeInMilliseconds (which fails the test). Nightwatch uses the Node.js EventEmitter pattern to handle
* asynchronous code so this command is also an EventEmitter.
*/
function WaitUntilElementIsClickable() {
events.EventEmitter.call(this);
this.startTimeInMilliseconds = null;
}
util.inherits(WaitUntilElementIsClickable, events.EventEmitter);
WaitUntilElementIsClickable.prototype.command = function (element, timeoutInMilliseconds) {
this.startTimeInMilliseconds = new Date().getTime();
var self = this;
var message;
if (typeof timeoutInMilliseconds !== 'number') {
timeoutInMilliseconds = this.api.globals.waitForConditionTimeout;
}
this.check(element, function (result, loadedTimeInMilliseconds) {
if (result) {
message = '#' + element + ' was clickable after ' + (loadedTimeInMilliseconds - self.startTimeInMilliseconds) + ' ms.';
} else {
message = '#' + element + ' was still not clickable after ' + timeoutInMilliseconds + ' ms.';
}
self.client.assertion(result, 'not visible or disabled', 'visible and not disabled', message, true);
self.emit('complete');
}, timeoutInMilliseconds);
return this;
};
WaitUntilElementIsClickable.prototype.check = function (element, callback, maxTimeInMilliseconds) {
var self = this;
var promises =[];
promises.push(new Promise(function(resolve) {
self.api.isVisible(element, function(result) {
resolve(result.status === 0 && result.value === true);
});
}));
promises.push(new Promise(function(resolve) {
self.api.getAttribute(element, 'disabled', function (result) {
resolve(result.status === 0 && result.value === null);
});
}));
Promise.all(promises)
.then(function(results) {
var now = new Date().getTime();
const visibleAndNotDisabled = !!results[0] && !!results[1];
if (visibleAndNotDisabled) {
callback(true, now);
} else if (now - self.startTimeInMilliseconds < maxTimeInMilliseconds) {
setTimeout(function () {
self.check(element, callback, maxTimeInMilliseconds);
}, 500);
} else {
callback(false);
}
})
.catch(function(error) {
setTimeout(function () {
self.check(element, callback, maxTimeInMilliseconds);
}, 500);
});
};
module.exports = WaitUntilElementIsClickable;
Add this code as a file to your commands folder. It should be called waitUntilElementIsClickable.js or whatever you want your command to be.
Usage is:
browser.waitUntilElementIsClickable('.some.css');
You can also use page elements:
var page = browser.page.somePage();
page.waitUntilElementIsClickable('#someElement');

You can use waitForElementVisible() combined with the :enabled CSS pseudo-class.
For example, the following will wait up to 10 seconds for #element to become enabled, then click it (note that the test will fail if the element doesn't become enabled after 10 seconds):
browser
.waitForElementVisible('#element:enabled', 10000)
.click('#element');

Can you show an example element,usually there should be an attribute name "disabled" if the button is not clickable, this should work.
browser.assert.attributeEquals(yourCSS, 'disabled', true)

I'm unable to comment but there are a couple of issues with the code suggested by Alex R.
First, the code will not work with Firefox as geckodriver does not return a 'status'. So this:
resolve(result.status === 0 && result.value === true)
needs to be changed to this:
resolve(result.value === true).
Second, the line:
self.client.assertion(result, 'not visible or disabled', 'visible and not disabled', message, true);
doesn't work and needs to be commented out in
order to get the code to run.

Related

Protractor IF condition was executed first before IT

Is there a way to ensure that it will be executed first before the if condition located below it?
Sample Code:
it('should click ' + strName + ' from the list', function() {
exports.waitForElementDisp(10, global.elmGravityPanel, false);
browser.sleep(2000).then(function() {
element(by.css('[cellvalue="' + strName + '"]')).click();
element(by.id('uniqueid')).getText().then(function(tmpText) {
global.tempObject.isPresent = false;
if (tmpText == strName) {
global.tempObject.isPresent = true;
}
});
});
});
if (global.tempObject.isPresent == true) {
it('should click the settings icon', function() {
global.elmSettingBtn.click();
});
it...
}
Currently, global.tempObject.isPresent was set to null and protractor did not go inside the IF even though it will be set as true inside the first IT.
Because this 'if' is executed on file parsing.
I would suggest to try something like this:
it(`should click ${strName} from the list`, function () {
exports.waitForElementDisp(10, global.elmGravityPanel, false);
browser.sleep(2000)
$(`[cellvalue="${strName}"]`).click();
$('#uniqueid').getText().then(function (tmpText) {
global.tempObject.isPresent = false;
if (tmpText == strName) {
global.tempObject.isPresent = true;
}
});
});
/**
*
* #param condition Any boolean condition to check
* #param suiteOrTest function, that contains scheduling of tests.
*/
function runIf(condition, suiteOrTest) {
if (condition) {
return suiteOrTest
} else {
console.log('Skipping execution')
}
}
describe('Conditional execution', () => {
it('1', function () {
global.elmSettingBtn.click();
});
it('This test is with condition', runIf(process.env.IS_LINUX, ()=> {
// Some other test
global.elmSettingBtn.click();
}))
}))
I'm not a fan of wrapping the it block in a conditional. So below are my thoughts of how you would change your code to avoid using the conditional around the it block.
If you are expecting the unique text to appear on the element for the following it block, you could increase the browser wait for the expected conditions that element appears instead of having an arbitrary sleep time. After the element is available we could then do some checks against the text.
If you have a mixture of workflows that sometimes test to see if a unique text is present and other times where the unique tests is not present, I would then split that out into two different workflows.
If the element is not there, we could fail the tests after that based on the uniqueText not being present. This could be overkill.
let EC = ExpectedConditions;
let button = element(by.css('[cellvalue="' + strName + '"]'));
let uniqueText = element(by.id('uniqueid'));
// your locator strategy here.
let elemSettingBtn = element(by.css('something'));
it('should click ' + strName + ' from the list', function() {
// Leaving this line alone. Not sure what this is doing.
exports.waitForElementDisp(10, global.elmGravityPanel, false);
browser.wait(EC.elementToBeClickable(button), 3000);
button.click();
browser.wait(EC.presenceOf(uniqueText), 3000);
expect(uniqueText.getText()).toEqual(strName);
});
it('should click the settings icon', function() {
uniqueText.isPresent().then(present => {
if (present) {
// click the settings button because the unique text is there.
return elmSettingBtn.click();
} else {
// Maybe fail the test when the button is not present?
fail('unique text is not present.');
}
});
});

Firefox addon error in scratchpad

I am trying to test this code here. This code "blocks" some URL if I try to join them.
//const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
Cu.import('resource://gre/modules/Services.jsm');
var urls_block = [
//If URLs contain any of these elements they will be blocked or redirected,
// your choice based on code in observer line 17
'www.facebook.com'
];
var redir_obj = {
'www.facebook.com': 'data:text,'+ escape('url_blocked would have went to facebook')
}
var observers = {
'http-on-modify-request': {
observe: function (aSubject, aTopic, aData) {
console.info('http-on-modify-request: aSubject = '
+ aSubject + ' | aTopic = ' + aTopic + ' | aData = ' + aData);
var httpChannel = aSubject.QueryInterface(Ci.nsIHttpChannel);
var requestUrl = httpChannel.URI.spec.toLowerCase();
for (var i=0; i<urls_block.length; i++) {
if (requestUrl.indexOf(urls_block[i]) > -1) {
//httpChannel.cancel(Cr.NS_BINDING_ABORTED); //this aborts the load
//Can redirect with this next line, if don't want to redirect and
// just block, then comment this line and uncomment the line above:
httpChannel.redirectTo(Services.io.newURI(redir_obj[urls_block[i]],
null, null));
break;
}
}
},
reg: function () {
Services.obs.addObserver(observers['http-on-modify-request'],
'http-on-modify-request', false);
},
unreg: function () {
Services.obs.removeObserver(observers['http-on-modify-request'],
'http-on-modify-request');
}
}
};
function install() {}
function uninstall() {}
function startup() {
for (var o in observers) {
observers[o].reg();
}
}
function shutdown(aData, aReason) {
if (aReason == APP_SHUTDOWN) return;
for (var o in observers) {
observers[o].unreg();
}
}
startup()
In scratchpad of Firefox and I get this error:
/*
Exception: ReferenceError: Cu is not defined
#Scratchpad/1:2:1
*/
NO ERROR ON CONSOLE.
Does anyone have any idea what is this error??
I've read that Firefox doesn't go well with constants but this code sometimes works sometimes not.
Also can someone help me fixing it, so it can work all the time?
There are multiple problems. First, in order for Components to be defined, you need to have the scratchpad running in the browser context. To do this go to the Developer Tool Settings and check the box that is "Enable chrome and add-on debugging". Restart Firefox, just to be sure that the scratchpad is in the browser context.
Once that is done, the following code will work:
// -sp-context: browser
//The above line tells scratchpad to run this in the browser context.
//In the scratchpad browser context some of these are already defined, so we check first:
if(typeof Cc === "undefined") {
const Cc = Components.classes;
}
if(typeof Ci === "undefined") {
const Ci = Components.interfaces;
}
if(typeof Cu === "undefined") {
const Cu = Components.utils;
}
if(typeof Cr === "undefined") {
const Cr = Components.results;
}
//In your own add-on you will need to define them all:
//const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
Cu.import('resource://gre/modules/Services.jsm');
var urlsBlock = [
//If URLs contain any of these elements they will be blocked or redirected,
// your choice based on code in observer line 17
'www.facebook.com'
];
var redirObj = {
'www.facebook.com': 'data:text,'+ escape('url_blocked would have went to facebook')
}
var observers = {
'http-on-modify-request': {
observe: function (aSubject, aTopic, aData) {
var httpChannel = aSubject.QueryInterface(Ci.nsIHttpChannel);
var requestUrl = httpChannel.URI.spec.toLowerCase();
console.info('http-on-modify-request: aSubject = '
+ aSubject + ' | aTopic = ' + aTopic + ' | aData = ' + aData
+ ' | httpChannel = ' + httpChannel
+ ' | requestUrl = ' + requestUrl);
for (let urlsBlockLength=urlsBlock.length, i=0; i<urlsBlockLength; i++) {
if (requestUrl.indexOf(urlsBlock[i]) > -1) {
//httpChannel.cancel(Cr.NS_BINDING_ABORTED); //this aborts the load
//Can redirect with this next line, if don't want to redirect and
// just block, then comment this line and uncomment the line above:
httpChannel.redirectTo(Services.io.newURI(redirObj[urlsBlock[i]],
null, null));
break;
}
}
},
reg: function () {
Services.obs.addObserver(observers['http-on-modify-request'],
'http-on-modify-request', false);
},
unreg: function () {
Services.obs.removeObserver(observers['http-on-modify-request'],
'http-on-modify-request');
}
}
};
function install() {}
function uninstall() {}
function startup() {
for (var o in observers) {
observers[o].reg();
}
}
function shutdown(aData, aReason) {
if (aReason == APP_SHUTDOWN) return;
for (var o in observers) {
observers[o].unreg();
}
}
startup();
I had this problem, It is so easy, it is enough to change your environment of scratchpad from Content to Browser
The code works as is except for shutdown but thats obvious because its from scratchpad scope. This code is designed ot run form bootstrap scope. But to make it run form scratchpad you comment out first line and just run startup. then to shutdown just run the loop from within shutdown as APP_SHUTDOWN is not defined. <<< for scrathcpad scope, which is for testing purposes only. Once you want to deploy it use the uncommented line 1 code and no need to run startup or shutodwn as they are bootstrap entry and exit functions so they automatically get called. See the youtube video:
https://www.youtube.com/watch?v=pxt8lJ64nVw

Synchronizing jQuery idle timeout in multiple tabs

I am using jQuery idle timeout plugin by Eric Hynds
My question is simple but I know there won't be any simple answer to this.
The plugin works great when the website is opened in only one tab. What I want to do is when user opens the website in any number of tabs it should there should only one background timer but the info message should be shown on all tabs.
Consider for example user opens a website in 3 different tabs but actively uses only one tab(obviously) so currently the plugin senses that user is inactive on that tab for specified time and logs him out which is not correct as user still actively using other tab.
I know I have to put some hacks somewhere but really dont understand where and how. If anyone had already done this it would really help me alot. Also any suggestions are most welcome. Pls help guys.
I had the same issue! An idle timer can communicate (stay in sync) across multiple windows & tabs using localStorage variables. Most modern browsers support this feature. On github, marcuswestin/store.js provides good functionality with 'fallback' behavior for older browsers.
Here is the 'testing' code for an idleTimer plugin which provides synchronized windows/tabs (must all be within the same domain). It sets 2 localStorage variables to track the state of the user's session.
You can see a demo of this code here. Open multiple windows/tabs and observe.
http://jillelaine.github.io/jquery-idleTimeout/
/**
* This work is licensed under the MIT License
*
* Configurable idle (no activity) timer and logout redirect for jQuery.
* Works across multiple windows, tabs and iframes from the same domain.
*
* Dependencies: JQuery v1.7+, JQuery UI, store.js from https://github.com/marcuswestin/store.js - v1.3.4+
*
* Commented and console logged for debugging with Firefox & Firebug or similar
* version 1.0.6
**/
/*global jQuery: false, document: false, store: false, clearInterval: false, setInterval: false, setTimeout: false, window: false, alert: false, console: false*/
/*jslint indent: 2, sloppy: true, plusplus: true*/
(function ($) {
$.fn.idleTimeout = function (options) {
console.log('start');
//##############################
//## Configuration Variables
//##############################
var defaults = {
idleTimeLimit: 30000, // 30 seconds for testing. 'No activity' time limit in milliseconds. 1200000 = 20 Minutes
dialogDisplayLimit: 20000, // 20 seconds for testing. Time to display the warning dialog before redirect (and optional callback) in milliseconds. 180000 = 3 Minutes
redirectUrl: '/logout', // redirect to this url on timeout logout. Set to "redirectUrl: false" to disable redirect
// optional custom callback to perform before redirect
customCallback: false, // set to false for no customCallback
// customCallback: function () { // define optional custom js function
// perform custom action before logout
// },
// configure which activity events to detect
// http://www.quirksmode.org/dom/events/
// https://developer.mozilla.org/en-US/docs/Web/Reference/Events
activityEvents: 'click keypress scroll wheel mousewheel', // separate each event with a space
//dialog box configuration
dialogTitle: 'Session Expiration Warning',
dialogText: 'Because you have been inactive, your session is about to expire.',
// server-side session keep-alive timer
sessionKeepAliveTimer: 600000 // Ping the server at this interval in milliseconds. 600000 = 10 Minutes
// sessionKeepAliveTimer: false // Set to false to disable pings
},
//##############################
//## Private Variables
//##############################
opts = $.extend(defaults, options),
checkHeartbeat = 2000, // frequency to check for timeouts - 2000 = 2 seconds
origTitle = document.title, // save original browser title
sessionKeepAliveUrl = window.location.href, // set URL to ping to user's current window
keepSessionAlive, activityDetector,
idleTimer, remainingTimer, checkIdleTimeout, idleTimerLastActivity, startIdleTimer, stopIdleTimer,
openWarningDialog, dialogTimer, checkDialogTimeout, startDialogTimer, stopDialogTimer, isDialogOpen, destroyWarningDialog,
countdownDisplay, logoutUser,
checkForIframes, includeIframes, attachEventIframe; // iframe functionality
//##############################
//## Private Functions
//##############################
keepSessionAlive = function () {
if (opts.sessionKeepAliveTimer) {
var keepSession = function () {
if (idleTimerLastActivity === store.get('idleTimerLastActivity')) {
console.log('keep session alive function');
$.get(sessionKeepAliveUrl);
}
};
setInterval(keepSession, opts.sessionKeepAliveTimer);
}
};
activityDetector = function () {
$('body').on(opts.activityEvents, function () {
if (isDialogOpen() !== true) {
console.log('activity detected');
startIdleTimer();
} else {
console.log('dialog open. activity ignored');
}
});
};
checkIdleTimeout = function () {
var timeNow = $.now(), timeIdleTimeout = (store.get('idleTimerLastActivity') + opts.idleTimeLimit);
if (timeNow > timeIdleTimeout) {
console.log('timeNow: ' + timeNow + ' > idle ' + timeIdleTimeout);
if (isDialogOpen() !== true) {
console.log('dialog is not open & will be opened');
openWarningDialog();
startDialogTimer();
}
} else if (store.get('idleTimerLoggedOut') === true) { //a 'manual' user logout?
logoutUser();
} else {
console.log('idle not yet timed out');
if (isDialogOpen() === true) {
console.log('dialog is open & will be closed');
destroyWarningDialog();
stopDialogTimer();
}
}
};
startIdleTimer = function () {
stopIdleTimer();
idleTimerLastActivity = $.now();
store.set('idleTimerLastActivity', idleTimerLastActivity);
console.log('start idle timer: ' + idleTimerLastActivity);
idleTimer = setInterval(checkIdleTimeout, checkHeartbeat);
};
stopIdleTimer = function () {
clearInterval(idleTimer);
};
openWarningDialog = function () {
var dialogContent = "<div id='idletimer_warning_dialog'><p>" + opts.dialogText + "</p><p style='display:inline'>Time remaining: <div style='display:inline' id='countdownDisplay'></div></p></div>";
$(dialogContent).dialog({
buttons: {
"Stay Logged In": function () {
console.log('Stay Logged In button clicked');
destroyWarningDialog();
stopDialogTimer();
startIdleTimer();
},
"Log Out Now": function () {
console.log('Log Out Now button clicked');
logoutUser();
}
},
closeOnEscape: false,
modal: true,
title: opts.dialogTitle
});
// hide the dialog's upper right corner "x" close button
$('.ui-dialog-titlebar-close').css('display', 'none');
// start the countdown display
countdownDisplay();
// change title bar to warning message
document.title = opts.dialogTitle;
};
checkDialogTimeout = function () {
var timeNow = $.now(), timeDialogTimeout = (store.get('idleTimerLastActivity') + opts.idleTimeLimit + opts.dialogDisplayLimit);
if ((timeNow > timeDialogTimeout) || (store.get('idleTimerLoggedOut') === true)) {
console.log('timeNow: ' + timeNow + ' > dialog' + timeDialogTimeout);
logoutUser();
} else {
console.log('dialog not yet timed out');
}
};
startDialogTimer = function () {
dialogTimer = setInterval(checkDialogTimeout, checkHeartbeat);
};
stopDialogTimer = function () {
clearInterval(dialogTimer);
clearInterval(remainingTimer);
};
isDialogOpen = function () {
var dialogOpen = $("#idletimer_warning_dialog").is(":visible");
if (dialogOpen === true) {
return true;
}
return false;
};
destroyWarningDialog = function () {
console.log('dialog destroyed');
$(".ui-dialog-content").dialog('destroy').remove();
document.title = origTitle;
};
// display remaining time on warning dialog
countdownDisplay = function () {
var dialogDisplaySeconds = opts.dialogDisplayLimit / 1000, mins, secs;
remainingTimer = setInterval(function () {
mins = Math.floor(dialogDisplaySeconds / 60); // minutes
if (mins < 10) { mins = '0' + mins; }
secs = dialogDisplaySeconds - (mins * 60); // seconds
if (secs < 10) { secs = '0' + secs; }
$('#countdownDisplay').html(mins + ':' + secs);
dialogDisplaySeconds -= 1;
}, 1000);
};
logoutUser = function () {
console.log('logout function');
store.set('idleTimerLoggedOut', true);
if (opts.customCallback) {
console.log('logout function custom callback');
opts.customCallback();
}
if (opts.redirectUrl) {
console.log('logout function redirect to URL');
window.location.href = opts.redirectUrl;
}
};
// document must be in readyState 'complete' before looking for iframes
checkForIframes = function () {
var docReadyCheck, isDocReady;
docReadyCheck = function () {
if (document.readyState === "complete") {
console.log('check for iframes, now that the document is complete');
clearInterval(isDocReady);
includeIframes();
}
};
isDocReady = setInterval(docReadyCheck, 1000);
};
// look for iframes
includeIframes = function () {
console.log('include iframes start');
var foundIframes = document.getElementsByTagName('iframe'), index, iframeItem;
if (foundIframes.length > 0) { //at least one iframe found
console.log('iframes found: ' + foundIframes.length);
// attach events to each iframe found
for (index = 0; index < foundIframes.length; index++) {
iframeItem = foundIframes.item(index);
if (iframeItem.attachEvent) { // IE < 11. Returns a boolean true/false
console.log('attach event to iframe. Browser IE < 11');
iframeItem.attachEvent('onload', attachEventIframe(index));
} else { // IE >= 11 and FF, etc.
console.log('attach event to iframe. Browser NOT IE < 11');
iframeItem.addEventListener('load', attachEventIframe(index), false);
}
} // end for loop
} // end if any iframes
};
// attach events to each iframe
attachEventIframe = function (index) {
var iframe = $('iframe:eq(' + index + ')').contents().find('html');
iframe.on(opts.activityEvents, function (event) {
console.log('bubbling iframe activity event to body of page');
$('body').trigger(event);
});
};
//###############################
// Build & Return the instance of the item as a plugin
// This is your construct.
//###############################
return this.each(function () {
if (store.enabled) {
idleTimerLastActivity = $.now();
store.set('idleTimerLastActivity', idleTimerLastActivity);
store.set('idleTimerLoggedOut', false);
} else {
alert('Please disable "Private Mode", or upgrade to a modern browser. Or perhaps a dependent file missing. Please see: https://github.com/marcuswestin/store.js');
}
activityDetector();
keepSessionAlive();
startIdleTimer();
checkForIframes();
});
};
}(jQuery));

Firefox sending multiple XHR requests when binding a controller function to the DOM

I have the following function in my controller:
$scope.model.listApplicantStatuses = function(){
var statusChoices = jobsService.getApplicantStatuses();
if(statusChoices !== null)
return statusChoices;
//if($scope.model.listApplicantStatuses.inProgress)
// return;
//$scope.model.listApplicantStatuses.inProgress = true;
jobsService.fetchApplicantStatuses().then(function(data){
jobsService.setApplicantStatuses(data.data);
return data.data;
},
function(data){
$scope.layout.showNotification('error', 10 * 1000, 'we are is experiencing technical difficulties Contact Support');
});
}
corresponding service code:
jobsServ.fetchApplicantStatuses = function(){
return $http.get(utils.getBaseUrl() + '/applications/status_choices', utils.getConfig());
}
jobsServ.getApplicantStatuses = function(){
return that.applicantStatusChoices;
},
jobsServ.setApplicantStatuses = function(choices){
that.applicantStatusChoices = choices;
},
Example DOM usage:
<select class="statusSelect" data-ng-model="model.editedApplicant[applicant.id].status" data-ng-show="layout.statusVisible(applicant.id) && !layout.statusLoader[applicant.id]" data-ng-options="key as val for (key, val) in model.listApplicantStatuses()" data-ng-change="model.updateStatus(applicant.id)"></select>
Now, the problem that I am having is that while chrome waits until the first function call completes, and then gives me the data that I get from the AJAX call, and returns undefined in the meanwhile, Firefox calls the function over and over again, creating a whole lot of uneeded XHR requests.
I commented out the code that was setting the inProgress $scope variable, a because it seems to much of a jQurish solution to me, and because that would force me to change my code in many places, and create another Boolean flag such as this per every request to the server.
I would expect the Firefox behaviour. Perhaps you should change the logic as:
// a variable to keep statuses, or a promise
$scope.applicantStatusList = $scope.model.listApplicantStatuses();
Change listApplicantStatuses() to return the promise:
$scope.model.listApplicantStatuses = function() {
var statusChoices = jobsService.getApplicantStatuses();
if(statusChoices !== null)
return statusChoices;
return jobsService.fetchApplicantStatuses().then(function(data){
jobsService.setApplicantStatuses(data.data);
return data.data;
},
function(data){
$scope.layout.showNotification(...);
});
};
And use the applicantStatusList in the <select>:
<select ... data-ng-options="key as val for (key, val) in applicantStatusList" ...></select>
I solved this by allocating a local variable named _root at the function that sends the xhr, setting _root.ingProgress to true once the request is send, and switching it to false once an answer is received.
Works like a charm.
code:
$scope.model.listApplicantStatuses = function(){
var statusChoices = jobsService.getApplicantStatuses();
if(statusChoices !== null)
return statusChoices;
var _root = this;
if(_root.inProgress)
return;
_root.inProgress = true;
jobsService.fetchApplicantStatuses().then(function(data){
jobsService.setApplicantStatuses(data.data);
_root.inProgress = false;
return data.data;
},
function(data){
_root.inProgress = false;
$scope.layout.showNotification('error', 10 * 1000, 'We are currently experiencing technical difficulties Contact Support');
});
}

AngularJs 2 promises inside a watch the second one never works

I have 2 lists in my application and the user is supposed to drag and drop items from one list to another.
When the user drops an element from one of the lists to the other list a request has to be made to the server side code to update a field in the database (SelectedForDiscussion).
This is the code in my controller:
$scope.$watch("questionsDiscuss", function (value) {
var question = $.Enumerable.From($scope.questionsDiscuss).Where(function (item) { return !item.SelectedForDiscussion }).FirstOrDefault()
if (question != undefined) {
questionSelectionService.UpdateQuestionSelectionStatus(question.Id, true)
.then(function (output) {
var question = $.Enumerable.From($scope.questionsDiscuss)
.Where(function (item) { return item.Id == output.data.questionId })
.FirstOrDefault();
var index = $.Enumerable.From($scope.questionsDiscuss).IndexOf(question);
if (question != undefined)
if (output.data.result != "success") {
$scope.questionsDiscuss.splice(index, 1);
$scope.questionsReceived.splice(0, 0, question);
}
else {
question.SelectedForDiscussion = true;
$scope.questionsDiscuss[index] = question;
}
});
}
else {
var question = $.Enumerable.From($scope.questionsReceived).Where(function (item) { return item.SelectedForDiscussion }).FirstOrDefault();
if (question != undefined) {
questionSelectionService.UpdateQuestionSelectionStatus(question.Id, false)
.then(function (output) {
var question = $.Enumerable.From($scope.questionsReceived)
.Where(function (item) { return item.Id == output.data.questionId })
.FirstOrDefault();
var index = $.Enumerable.From($scope.questionsReceived).IndexOf(question);
if (question != undefined)
if (output.data.result != "success") {
$scope.questionsReceived.splice(index, 1);
$scope.questionsDiscuss.splice(0, 0, question);
}
else {
question.SelectedForDiscussion = false;
$scope.questionsReceived[index] = question;
}
});
}
}
}, true);
I have 4 javascript breakpoint placed at the following lines within Firebug:
2 of them at the following lines:
if (question != undefined) {
One at:
var question = $.Enumerable.From($scope.questionsDiscuss)
.Where(function (item) {
return item.Id == output.data.questionId
})
.FirstOrDefault();
And the other at:
var question = $.Enumerable.From($scope.questionsReceived)
.Where(function (item) {
return item.Id == output.data.questionId
})
.FirstOrDefault();
The following happens:
The breakpoints at:
if (question != undefined) {
are always reached.
The breakpoint at
var question = $.Enumerable.From($scope.questionsDiscuss)
.Where(function (item) {
return item.Id == output.data.questionId
})
.FirstOrDefault();
is also reached.
The other is never reached.
Both responses are OK(response code 200).
Everything should work perfectly but the then clause in the second promise is never reached.
Can anyone tell me what I am doing wrong?
The serverside appplication is an ASP.NET MVC application written in C#.
Edit 1:
I figured out why this was happening and I have a work around for it. I am stil interested in an actual solution.
The problem is angularjs throws an error then swallows it when calling $http for the second time. The error is:
digest alredy in progress
I think this is because in my directive I have this code:
dndfunc = function (scope, element, attrs) {
// contains the args for this component
var args = attrs.dndBetweenList.split(',');
// contains the args for the target
var targetArgs = $('#' + args[1]).attr('dnd-between-list').split(',');
// variables used for dnd
var toUpdate;
var target;
var startIndex = -1;
// watch the model, so we always know what element
// is at a specific position
scope.$watch(args[0], function (value) {
toUpdate = value;
}, true);
// also watch for changes in the target list
scope.$watch(targetArgs[0], function (value) {
target = value;
}, true);
// use jquery to make the element sortable (dnd). This is called
// when the element is rendered
$(element[0]).sortable({
items: 'div',
start: function (event, ui) {
// on start we define where the item is dragged from
startIndex = ($(ui.item).index());
},
stop: function (event, ui) {
var newParent = ui.item[0].parentNode.id;
// on stop we determine the new index of the
// item and store it there
var newIndex = ($(ui.item).index());
var toMove = toUpdate[startIndex];
// we need to remove him from the configured model
toUpdate.splice(startIndex, 1);
if (newParent == args[1]) {
// and add it to the linked list
target.splice(newIndex, 0, toMove);
} else {
toUpdate.splice(newIndex, 0, toMove);
}
// we move items in the array, if we want
// to trigger an update in angular use $apply()
// since we're outside angulars lifecycle
scope.$apply(targetArgs[0]);
scope.$apply(args[0]);
},
connectWith: '#' + args[1]
})
}
And there are 2 calls to apply at the end which trigger a new digest cycle I think.
Anyway I fixed it by adding this call before the calls to apply:
if (scope.updateLists != undefined)
scope.updateLists();
And moved all the code from the watch into the updateLists function.
Also because people have mentioned the service as having something to do with it I am pasting the relevant code within it:
GetQuestionsReceived: function (eid, criteria, page, rows) {
var promise = this.GetQuestionsReceivedInternal(eid,criteria, page, rows).then(function (response) {
// The return value gets picked up by the then in the controller.
return response;
});
// Return the promise to the controller
return promise;
},
GetQuestionsReceivedInternal: function (eid, criteria, page, rows) {
return $http({ method: 'GET',
url: '../QuestionManagement/GetQuestions?eventId='+eid+'&page=1&rows=5&'+serialize(criteria)
}).
success(function (data, status, headers, config) {
// this callback will be called asynchronously
// when the response is available
results = data;
}).
error(function (data, status, headers, config) {
// called asynchronously if an error occurs
// or server returns response with an error status.
if (window.console && console.log) {
console.log("Could not obtain questions received. Error:" + data + "Status:" + status + "Headers:" + headers + "Config:" + config);
}
});
},
GetQuestionsDiscuss: function (eid,criteria, page, rows) {
var promise = this.GetQuestionsDiscussInternal(eid,criteria, page, rows).then(function (response) {
// The return value gets picked up by the then in the controller.
return response;
});
// Return the promise to the controller
return promise;
},
GetQuestionsDiscussInternal: function (eid,criteria, page, rows) {
return $http({ method: 'GET',
url: '../QuestionManagement/GetQuestions?eventId=' + eid + '&page=1&rows=5&' + serialize(criteria)
}).
success(function (data, status, headers, config) {
// this callback will be called asynchronously
// when the response is available
response = data;
}).
error(function (data, status, headers, config) {
// called asynchronously if an error occurs
// or server returns response with an error status.
if (window.console && console.log) {
console.log("Could not obtain questions received. Error:" + data + "Status:" + status + "Headers:" + headers + "Config:" + config);
}
});
},
You have two very similar blocks of code, which could be generalized and placed in a function wrapper, leaving behind a very simple calling function.
If you can get everything into that form, then I think you will find it easier to debug.
Here is an attempt to do so :
function updateSelectionStatus(qA, qB, bool) {
var en = $.Enumerable.From(qA);
var question = en.Where(function (item) {
return bool ? !item.SelectedForDiscussion : item.SelectedForDiscussion;
}).FirstOrDefault();
if(question) {
questionSelectionService.UpdateQuestionSelectionStatus(question.Id, bool).then(function (output) {
if (output.data.result == "success") {
question.SelectedForDiscussion = bool;
}
else {
qA.splice(en.IndexOf(question), 1);
qB.unshift(question);
}
});
}
return question;
}
$scope.$watch("questionsDiscuss", function (value) {
if (!updateSelectionStatus($scope.questionsDiscuss, $scope.questionsReceived, true) {
updateSelectionStatus($scope.questionsReceived, $scope.questionsDiscuss, false);
}
}, true);
I may have made some false assumptions and simplified too much (eg. purging the inner $.Enumerable.From, which appears to reselect the same question as the outer), so you may well need to rework my code.
I'm advocating a principle here, rather than offering a solution.

Resources