Mock document.activeElement in Jasmine test - angularjs

I have the following function:
function focusIsNotInInput() {
// If the element currently in focus is of a certain type, then the key handler shouldn't run
var currentlyInFocus = $window.document.activeElement;
var blacklist = ['INPUT', 'TEXTAREA', 'BUTTON', 'SELECT', 'IFRAME', 'MD-OPTION'];
return !blacklist.some(function (nodeName) {
return nodeName === currentlyInFocus.nodeName;
});
}
And I need to mock that the element currently in focus is of one of the specified types, but can't get it to work.
I've tried injecting window, like this:
beforeEach(function() {
var $windowMock;
inject(function(_$window_) {
$windowMock = _$window_;
$windowMock.document.activeElement.nodeName = 'INPUT';
});
});
But when the code above runs, the active element is always still body. It's getting overwritten. I have also tried creating an element and setting focus on it:
var elementInFocus = $('<input>');
this.elem.append(elementInFocus);
elementInFocus.triggerHandler('focus');
elementInFocus.focus();
But it's the same, body is always in focus, what ever I do.

I had some trouble with this too, a possible solution (worked for me) is to add a spyOn(element, 'focus') -- here's a reference: How do I check if my element has been focussed in a unit test
My successful solution:
const htmlItem = fixture.nativeElement;
const searchBar = htmlItem.querySelector('.search-box');
let focusSpy = spyOn(searchBar, 'focus');
searchBar.focus();
expect(focusSpy).toHaveBeenCalled();

Related

Drag and Drop with Protractor in AngularJS

I appreciate there's quite a bit of stuff already been said about automating drag and drop as part of E2E testing. However after many, many hours of fiddling around, I cannot get any of the methods described to work...that is using Functions, coordinates etc etc. Oddly enough, console.log maintains the tests have passed, but the screenshots clearly show nothing has happened.
Screenshots shows a portion of the application
The user selects a paper and drags onto the image. As the drag 'starts' the grey overlay on the image clears and the paper is rendered on the room.
The code snippet shows one of the more simple ideas I've tried and I would be very pleased to receive any help going!
const JS_HTML5_DND = 'function e(e,t,n,i){var r=a.createEvent("DragEvent");r.initMouseEvent(t,!0,!0,o,0,0,0,c,g,!1,!1,!1,!1,0,null),Object.defineProperty(r,"dataTransfer",{get:function(){return d}}),e.dispatchEvent(r),o.setTimeout(i,n)}var t=arguments[0],n=arguments[1],i=arguments[2]||0,r=arguments[3]||0;if(!t.draggable)throw new Error("Source element is not draggable.");var a=t.ownerDocument,o=a.defaultView,l=t.getBoundingClientRect(),u=n?n.getBoundingClientRect():l,c=l.left+(l.width>>1),g=l.top+(l.height>>1),s=u.left+(u.width>>1)+i,f=u.top+(u.height>>1)+r,d=Object.create(Object.prototype,{_items:{value:{}},effectAllowed:{value:"all",writable:!0},dropEffect:{value:"move",writable:!0},files:{get:function(){return this._items.Files}},types:{get:function(){return Object.keys(this._items)}},setData:{value:function(e,t){this._items[e]=t}},getData:{value:function(e){return this._items[e]}},clearData:{value:function(e){delete this._items[e]}},setDragImage:{value:function(e){}}});if(n=a.elementFromPoint(s,f),!n)throw new Error("The target element is not interactable and need to be scrolled into the view.");u=n.getBoundingClientRect(),e(t,"dragstart",101,function(){var i=n.getBoundingClientRect();c=i.left+s-u.left,g=i.top+f-u.top,e(n,"dragenter",1,function(){e(n,"dragover",101,function(){n=a.elementFromPoint(c,g),e(n,"drop",1,function(){e(t,"dragend",1,callback)})})})})';
describe('Drag and Drop Test', function() {
it('should drag', function () {
var e1 = element(by.xpath('html/body/webapp-app/div/div/webapp-johnlewis-visualiser/div/div[2]/div/digitalbridge-shortlist/div/div/ul/li[1]/a/img'));
var e2 = element(by.css('.db-project-designer'));
element(by.xpath('html/body/webapp-app/div/div/webapp-johnlewis-visualiser/div/div[2]/div/digitalbridge-shortlist/div/div/ul/li[1]/a/img')).click();
//element(by.xpath('html/body/webapp-app/div/div/webapp-johnlewis-visualiser/div/div[1]/div/div/digitalbridge-project/div/digitalbridge-project-designer/canvas')).click();
browser.driver.actions().dragAndDrop(e1.getWebElement(),e2.getWebElement()).perform();
browser.sleep(2000);
});
});
The constant is showing an error 'const' is available in ES6 (use 'esversion: 6') or Mozilla JS extensions (use moz). (W104) - I do have ES6 installed in Node_Modules.
I inserted the click line to see if pre-selecting the item made any difference...it didn't!
Thank you
David
Try this library https://github.com/SunGard-Labs/sg-protractor-tools
The library also includes functions that simplify common tasks like
Scrolling to an element
Drag and drop
Waiting for DOM elements to become visible or hidden
module.exports = function simulateDragAndDrop(sourceNode, destinationNode) {
const EVENT_TYPES = {
DRAG_END: 'dragend',
DRAG_START: 'dragstart',
DROP: 'drop'
};
function createCustomEvent(type) {
const event = new CustomEvent('CustomEvent');
event.initCustomEvent(type, true, true, null);
event.dataTransfer = {
data: {
},
setData: function(type, val) {
this.data[type] = val;
},
getData: function(type) {
return this.data[type];
}
};
return event;
}
function dispatchEvent(node, type, event) {
if (node.dispatchEvent) {
return node.dispatchEvent(event);
}
if (node.fireEvent) {
return node.fireEvent('on' + type, event);
}
}
const event = createCustomEvent(EVENT_TYPES.DRAG_START);
dispatchEvent(sourceNode, EVENT_TYPES.DRAG_START, event);
const dropEvent = createCustomEvent(EVENT_TYPES.DROP);
dropEvent.dataTransfer = event.dataTransfer;
dispatchEvent(destinationNode, EVENT_TYPES.DROP, dropEvent);
const dragEndEvent = createCustomEvent(EVENT_TYPES.DRAG_END);
dragEndEvent.dataTransfer = event.dataTransfer;
dispatchEvent(sourceNode, EVENT_TYPES.DRAG_END, dragEndEvent);
}
You can call it from you code like this
browser.executeScript(dragAndDrop, element, targetArea);

I am losing my definition of a form controller when the controller is set in an ng-repeat

I have code that uses AngularJS v1.5.0 and creates multiple forms with an ng-repeat like this. Note that inside the form I show the form details between xx and xx:
<div ng-click="wos.wordFormRowClicked(wf)"
ng-form="wos.wordFormNgForm_{{wf.wordFormId}}"
ng-repeat="wf in wos.word.wordForms">
xx {{ wos['wordFormNgForm_1465657579'] }} xx
When the runs I am able to see the form details appear between the xx and xx and I am able to query the state of the form like this:
wordFormCheckAndUpdate = (): ng.IPromise<any> => {
var self = this;
var wordFormNgForm = 'wordFormNgForm_' + wf.wordFormId;
self[wordFormNgForm].$setDirty();
However in my code after calling this procedure the form becomes undefined and also nothing shows between the xx and xx. As I step through this procedure with the debugger the last line I see is the line setting the value of a and then as soon as the function finishes the information between the xx and xx disappears and the form becomes undefined:
wordEditSubmit = (): ng.IPromise<any> => {
var self = this;
return this.wordFormCheckAndUpdate().then(
() => {
return self.$http({
url: self.ac.dataServer + "/api/word/Put",
method: "PUT",
data: self.word
})
.then(
(response: ng.IHttpPromiseCallbackArg<IWordRow>): any => {
self.word = angular.copy(response.data);
self['wordNgForm'].$setPristine();
self.uts.remove(self.words, 'wordId', self.word.wordId);
response.data.current = true;
self.words.push(response.data);
var a = 99;
},
(error: ng.IHttpPromiseCallbackArg<any>): any => {
self.ers.error(error);
return self.$q.reject(error);
});
}
);
}
My problem is that if I then try to repeat this:
setDirty = (): ng.IPromise<any> => {
var self = this;
var wordFormNgForm = 'wordFormNgForm_' + wf.wordFormId;
self[wordFormNgForm].$setDirty();
}
then the controller object self[wordFormNgForm] is no longer defined.
For reference. Here is how new wordForms are created:
wordFormAdd = () => {
this.wordFormId = Math.floor(Date.now() / 1000);
var emptyWordForm: IWordForm = <IWordForm>{
wordId: this.word.wordId,
wordFormId: this.wordFormId,
posId: 1,
statusId: Status.New
};
this.word.wordForms.push(emptyWordForm);
this.wordNgForm.$setDirty();
}
Here is the remove function:
remove = (arr, property, num) => {
arr.forEach((elem, index) => {
if (elem[property] === num)
arr.splice(index, 1);
})
};
Does anyone have any advice as to how I could solve this problem
Your problem could have been explained well with a demo reproducing the issue. Nevertheless, I've partially reproduced your problem in a fiddle here (not with Typescript though, it's just vanilla JS).
What I suspect happens when you first add a wordForm object with the wordFormAdd() method, is that, if you try to reference the FormController object associated with the ng-form in the same method immediately, then it might be too early to do so, because the $digest loop might not have been completed.
This is because as soon as you click and trigger the wordFormAdd() function, a new emptyWordForm object is added to the word.wordForms array and immediately ng-repeated in your view. But, the controller hasn't had enough time to associate the newly created ng-form object with itself, so you may end up with referencing an undefined object.
wordFormAdd = () => {
this.wordFormId = Math.floor(Date.now() / 1000);
var emptyWordForm: IWordForm = <IWordForm>{
wordId: this.word.wordId,
wordFormId: this.wordFormId,
posId: 1,
statusId: Status.New
};
this.word.wordForms.push(emptyWordForm);
this.wordNgForm.$setDirty(); //<== too early to do so
}
To overcome this, you should wrap that portion of the code within a $timeout wrapper. This ensures that Angular's so-called dirty check (or simply the digest loop) is finished.
Also note that keeping a single wordNgForm or wordFormId reference doesn't make sense, because you might dynamically add other forms, each of which may be associated with a new wordNgForm key and wordFormId.
I would suggest doing the above like so:
wordFormAdd = () => {
this.wordFormId = Math.floor(Date.now() / 1000);
...
this.word.wordForms.push(emptyWordForm);
this._timeout(function(){ // $timeout injected and assigned to this._timeout in controller definition
var wordFormNgForm = 'wordFormNgForm_' + this.wordFormId;
this[wordFormNgForm].$setDirty(); // <==
});
}
However in my code after calling this procedure the form becomes undefined and also nothing shows between the xx and xx. As I step through this procedure with the debugger the last line I see is the line setting the value of a and then as soon as the function finishes the information between the xx and xx disappears and the form becomes undefined:
A possible reason where the watched value in your view ({{ wos['wordFormNgForm_1465657579'] }}) becomes undefined, is that you are fetching new values and storing a copy of them in the controller's self.word property:
...
.then((response: ng.IHttpPromiseCallbackArg<IWordRow>): any => {
self.word = angular.copy(response.data); // <==
...
},
By doing so, the collection under word.wordForms that was previously ng-repeated in the view is changed and the watched value is no longer a valid reference to an item of this collection.
Meanwhile, self['wordNgForm'] in the wordEditSubmit certainly isn't associated with a FormController object as far as the ng-repeat in your view is concerned. This is because the FormController object keys associated with an ng-form must have a format (as imposed by you) similar to something like wordFormNgForm_1465657579. Therefore, here too, you are referencing an undefined property under self['wordNgForm']:
...
.then((response: ng.IHttpPromiseCallbackArg<IWordRow>): any => {
self.word = angular.copy(response.data);
self['wordNgForm'].$setPristine(); // <==
...
},
This looks like standard issue with java script that this means different things depending what called the function.
What I would advise is to generate form names and place them in wos.word.wordForms collection and bind them from there. Doing gymnastics like ng-form="wos.wordFormNgForm_{{wf.wordFormId}}" and var wordFormNgForm = 'wordFormNgForm_' + wf.wordFormId; feel quite awkward.
If there is a reason you are not using this approach please tell me, there might be a different solution :)

Can you chain properties?

I have an automation test that I have switched out most variables for properties which has been working fantastic for me unless i need to chain something. Here is an example of what I'd like it to look like:
var test = module.exports = {
outerElement: element(by.cssContainingText('some.div' 'A name'),
innerElement: $('something.else'),
clickOnaName: function () {
this.outerElement.this.innerElement.click();
},
However I have to use this code because chaining doesn't work the way I am using it:
var outerElement = element(by.cssContainingText('some.div'
'A name');
var innerElement = $('something.else');
var test = module.exports = {
clickOnaName: function() {
outerElement.innerElement.click();
},
Is there a way for me to do chain or should i just leave those elements as variables
please let me know if this helps!
You can use .element(el.locator()) to extend the elements. You can use multiple selectors at once. You can also hit arrays of elements.
Keep in mind that this kind of chaining of protractor selectors is the same as a css space child selector, and not as a > selector.
I.e. $('.parent').$('.child') will select the same elements as in a css file .parent .child, getting ALL children and not just direct children.
module.exports = function(){
this.parent = $('.parent');
this.child = $('.child');
this.childOfParent = parent.element(child.locator());
this.directParentChild = $('.parent').$('.child');
this.parentArray = $$('.parents');
this.child = $('.child');
this.children = parentArray.get(2).element(child.locator());
}
Adding in clicks and such should be pretty straightforward from there, page.childOfParent.click for example.

I'm not calling $apply explicitly but still get Error: [$rootScope:inprog] $apply already in progress

In a angular factory I have a method to create a new item, which has a connection to a user and a price to add to that users "items" array (like a shopping cart). So I have to see if the user is present in my the local users array if not then on the server and if not then create the user.
Code looks like this:
var saveItem = function (item) {
var user = filterUserById(item.ownerId);
if (user) {
user.createItem(item);
} else {
repository.getUserById(item.ownerId).then(
function (serverUser) {
var userViewModel = repository.getUserViewModel(serverUser);
userViewModel.createItem(item);
users.push(userViewModel);
}
, function () {
user = {
id: item.ownerId,
items: [
createItemDto(item)
]
};
repository.createUser({ id: user.id }, user);
users.push(repository.getUserViewModel(user));
});
}
};
No matter which of the "cases" occurs (user was found localy, on the server or was created and added) I get an error:
Error: [$rootScope:inprog] $apply already in progress
http://errors.angularjs.org/1.3.0-beta.18/$rootScope/inprog?p0=%24apply
I recon this may have to do with the fact that I'm using resources in my repository, but I don't think resource should (since it's a part of angular..). Here's the user.createItem method, code:
user.createItem = function (item) {
var resource = userResource
, itemDto = createItemDto(item)
, command = [{
Type: 'add',
Name: 'items',
Value: itemDto
}];
resource.createItem({ id: item.ownerId }, command);
this.items.push(itemDto);
};
Y U NO WERK!? PLS HLP! :'(
P.S. I don't have any explicit calls to apply, compile or digest anywhere in my code.
Found the problem! I had put a small code line to set focus on the correct input after the item was added and form was emptied. This consisted of a
$('selector').focus();
This was colliding with digest cycle... Solution:
$timeout($('selector').focus());
Try wrapping your call to user.createItem(item) in a $timeout function:
$timeout(function() {
user.createItem(item);
}, 0);
It's possible you could be triggering some other call to $scope.$apply() some other way.
Alternatively, try using $scope.$evalAsync(function())
Here's some good info: inprog

How to use protractor to check if an element is visible?

I'm trying to test if an element is visible using protractor. Here's what the element looks like:
<i class="icon-spinner icon-spin ng-hide" ng-show="saving"></i>
When in the chrome console, I can use this jQuery selector to test if the element is visible:
$('[ng-show=saving].icon-spin')
[
<i class=​"icon-spinner icon-spin ng-hide" ng-show=​"saving">​</i>​
]
> $('[ng-show=saving].icon-spin:visible')
[]
However, when I try to do the same in protractor, I get this error at runtime:
InvalidElementStateError:
invalid element state: Failed to execute 'querySelectorAll' on 'Document':
'[ng-show=saving].icon-spin:visible' is not a valid selector.
Why is this not valid? How can I check for visibility using protractor?
This should do it:
expect($('[ng-show=saving].icon-spin').isDisplayed()).toBe(true);
Remember protractor's $ isn't jQuery and :visible is not yet a part of available CSS selectors + pseudo-selectors
More info at https://stackoverflow.com/a/13388700/511069
The correct way for checking the visibility of an element with Protractor is to call the isDisplayed method. You should be careful though since isDisplayed does not return a boolean, but rather a promise providing the evaluated visibility. I've seen lots of code examples that use this method wrongly and therefore don't evaluate its actual visibility.
Example for getting the visibility of an element:
element(by.className('your-class-name')).isDisplayed().then(function (isVisible) {
if (isVisible) {
// element is visible
} else {
// element is not visible
}
});
However, you don't need this if you are just checking the visibility of the element (as opposed to getting it) because protractor patches Jasmine expect() so it always waits for promises to be resolved. See github.com/angular/jasminewd
So you can just do:
expect(element(by.className('your-class-name')).isDisplayed()).toBeTruthy();
Since you're using AngularJS to control the visibility of that element, you could also check its class attribute for ng-hide like this:
var spinner = element.by.css('i.icon-spin');
expect(spinner.getAttribute('class')).not.toMatch('ng-hide'); // expect element to be visible
I had a similar issue, in that I only wanted return elements that were visible in a page object. I found that I'm able to use the css :not. In the case of this issue, this should do you...
expect($('i.icon-spinner:not(.ng-hide)').isDisplayed()).toBeTruthy();
In the context of a page object, you can get ONLY those elements that are visible in this way as well. Eg. given a page with multiple items, where only some are visible, you can use:
this.visibileIcons = $$('i.icon:not(.ng-hide)');
This will return you all visible i.icons
If there are multiple elements in DOM with same class name. But only one of element is visible.
element.all(by.css('.text-input-input')).filter(function(ele){
return ele.isDisplayed();
}).then(function(filteredElement){
filteredElement[0].click();
});
In this example filter takes a collection of elements and returns a single visible element using isDisplayed().
This answer will be robust enough to work for elements that aren't on the page, therefore failing gracefully (not throwing an exception) if the selector failed to find the element.
const nameSelector = '[data-automation="name-input"]';
const nameInputIsDisplayed = () => {
return $$(nameSelector).count()
.then(count => count !== 0)
}
it('should be displayed', () => {
nameInputIsDisplayed().then(isDisplayed => {
expect(isDisplayed).toBeTruthy()
})
})
To wait for visibility
const EC = protractor.ExpectedConditions;
browser.wait(EC.visibilityOf(element(by.css('.icon-spinner icon-spin ng-hide')))).then(function() {
//do stuff
})
Xpath trick to only find visible elements
element(by.xpath('//i[not(contains(#style,"display:none")) and #class="icon-spinner icon-spin ng-hide"]))
element(by.className('your-class-name'))
.isDisplayed()
.then(function (isVisible) {
if (isVisible) { // element is visible
} else { // element is not visible
}
})
.catch(function(err){
console.error("Element is not found! ", err);
})
Here are the few code snippet which can be used for framework which use Typescript, protractor, jasmine
browser.wait(until.visibilityOf(OversightAutomationOR.lblContentModal), 3000, "Modal text is present");
// Asserting a text
OversightAutomationOR.lblContentModal.getText().then(text => {
this.assertEquals(text.toString().trim(), AdminPanelData.lblContentModal);
});
// Asserting an element
expect(OnboardingFormsOR.masterFormActionCloneBtn.isDisplayed()).to.eventually.equal(true
);
OnboardingFormsOR.customFormActionViewBtn.isDisplayed().then((isDisplayed) => {
expect(isDisplayed).to.equal(true);
});
// Asserting a form
formInfoSection.getText().then((text) => {
const vendorInformationCount = text[0].split("\n");
let found = false;
for (let i = 0; i < vendorInformationCount.length; i++) {
if (vendorInformationCount[i] === customLabel) {
found = true;
};
};
expect(found).to.equal(true);
});
Something to consider
.isDisplayed() assumes the element is present (exists in the DOM)
so if you do
expect($('[ng-show=saving]').isDisplayed()).toBe(true);
but the element is not present, then instead of graceful failed expectation, $('[ng-show=saving]').isDisplayed() will throw an error causing the rest of it block not executed
Solution
If you assume, the element you're checking may not be present for any reason on the page, then go with a safe way below
/**
* element is Present and is Displayed
* #param {ElementFinder} $element Locator of element
* #return {boolean}
*/
let isDisplayed = function ($element) {
return (await $element.isPresent()) && (await $element.isDisplayed())
}
and use
expect(await isDisplayed( $('[ng-show=saving]') )).toBe(true);
waitTillElementIsPresent(locator: Locator): promise.Promise<boolean>
{
const EC = protractor.ExpectedConditions;
return browser.wait(EC.visibilityOf(element(by.id('xyz')), browser.params.explicitWaitTime, 'Element taking too long to appear in the DOM');
}
const isDisplayed = await $('div').isDisplayed().then(null, err => false)

Resources