How to extend ElementFinder object in ProtractorJS? - javascript

Durring some experiments with protractorJS i noticed that there is no easy way to extend (inherit) ElementFinder object from protractor to add own functions.
For example, i want to create object Checkbox, that would have additional method - check() - should switch checkbox depending on result of isSelected().
I come up with the code -
var ElementFinder = require('protractor/lib/element.js').ElementFinder;
var ElementArrayFinder = require('protractor/lib/element.js').ElementArrayFinder;
class CheckBox extends ElementFinder {
constructor(loc) {
var getWebElements = function () {
var ptor = browser;
var locator = loc;
return ptor.waitForAngular().then(function() {
if (locator.findElementsOverride) {
return locator.findElementsOverride(ptor.driver, null, ptor.rootEl);
} else {
return ptor.driver.findElements(locator);
}
});
}
var ArrayFinderFull = new ElementArrayFinder(browser, getWebElements, loc);
super(browser, ArrayFinderFull);
}
check() {
return this.isSelected().then(selected => selected? this.click() : null)
}
}
But getWebElements is copy-paste from protractor/element.js -
https://github.com/angular/protractor/blob/3.1.0/lib/element.js#L131
This copy-paste flustrating me. I think there should be more proper way to extend ElementFinder.
Does anyone inherited ElementFinder in protractorJS?

I'm not sure this would help, but here is something we did recently to have a takewhile() method available on an ElementArrayFinder. We've put the following into onPrepare():
protractor.ElementArrayFinder.prototype.takewhile = function(whileFn) {
var self = this;
var getWebElements = function() {
return self.getWebElements().then(function(parentWebElements) {
var list = [];
parentWebElements.forEach(function(parentWebElement, index) {
var elementFinder =
protractor.ElementFinder.fromWebElement_(self.ptor_, parentWebElement, self.locator_);
list.push(whileFn(elementFinder, index));
});
return protractor.promise.all(list).then(function(resolvedList) {
var filteredElementList = [];
for (var index = 0; index < resolvedList.length; index++) {
if (!resolvedList[index]) {
break;
}
filteredElementList.push(parentWebElements[index])
}
return filteredElementList;
});
});
};
return new protractor.ElementArrayFinder(this.ptor_, getWebElements, this.locator_);
};
And now we can use takewhile on the result of element.all():
element.all(by.repeater("row in rows")).takewhile(function (elm) {
return elm.getText().then(function (text) {
return some_condition_to_be_true;
});
});

Now it is much simplier to extend ElementFinder, i calling this - page fragments.
I even created lib to solve this issue (PRs welcome!) - https://github.com/Xotabu4/protractor-element-extend
For now it only works with ElementFinder, but i want to be able to extend
ElementArrayFinders as well (planned for 2.0.0 version)
UPDATE
Support for ElementArrayFinder inheritance is added.

Related

Javascript & knockoutjs: how to refactor the following code to be able to access the properties outside the function

Im struggling to find a way to get the properties Override & Justification available outside of the function. The code is:
self.CasOverridesViewModel = ko.observable(self.CasOverridesViewModel);
var hasOverrides = typeof self.CasOverridesViewModel === typeof(Function);
if (hasOverrides) {
self.setupOverrides = function() {
var extendViewModel = function(obj, extend) {
for (var property in obj) {
if (obj.hasOwnProperty(property)) {
extend(obj[property]);
}
}
};
extendViewModel(self.CasOverridesViewModel(), function(item) {
item.isOverrideFilledIn = ko.computed( function() {
var result = false;
if (!!item.Override()) {
result = true;
}
return result;
});
if (item) {
item.isJustificationMissing = ko.computed(function() {
var override = item.Override();
var result = false;
if (!!override) {
result = !item.hasAtleastNineWords();
}
return result;
});
item.hasAtleastNineWords = ko.computed(function() {
var justification = item.Justification(),
moreThanNineWords = false;
if (justification != null) {
moreThanNineWords = justification.trim().split(/\s+/).length > 9;
}
return moreThanNineWords;
});
item.isValid = ko.computed(function() {
return (!item.isJustificationMissing());
});
}
});
}();
}
I've tried it by setting up a global variable like:
var item;
or
var obj;
if(hasOverrides) {...
So the thing that gets me the most that im not able to grasp how the connection is made
between the underlying model CasOverridesviewModel. As i assumed that self.CasOverridesViewModel.Override() would be able to fetch the data that is written on the screen.
Another try i did was var override = ko.observable(self.CasOverridesViewModel.Override()), which led to js typeError as you cannot read from an undefined object.
So if anyone is able to give me some guidance on how to get the fields from an input field available outside of this function. It would be deeply appreciated.
If I need to clarify some aspects do not hesitate to ask.
The upmost gratitude!
not sure how far outside you wanted to go with your variable but if you just define your global var at root level but only add to it at the moment your inner variable gets a value, you won't get the error of setting undefined.
var root = {
override: ko.observable()
};
root.override.subscribe((val) => console.log(val));
var ViewModel = function () {
var self = this;
self.override = ko.observable();
self.override.subscribe((val) => root.override(val));
self.load = function () {
self.override(true);
};
self.load();
};
ko.applyBindings(new ViewModel());
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>

TypeError: .currentPizzaPlace.getPoint is not a function

TypeError: this.pizzaPlaceEditService.currentPizzaPlace.getPoint is not a function
I found TypeError: is not a function typescript class but I don't think it applies to this situation. Found several more that didn't seem to apply. Here's my redacted class:
export class PizzaPlace {
deliveryArea = [];
getPoint(seq: number): Point {
let result: Point = null;
this.deliveryArea.forEach(function(point:Point) {
if(seq == point.sequence) {
result = point;
}
});
return result;
}
}
Here is my working unit test:
it('should retrieve a point by sequence', () => {
var point1 = new Point();
point1.sequence = 0;
point1.latitude = 5.12;
point1.longitude = 5.12;
var point2 = new Point();
point2.sequence = 1;
point2.latitude = 6.13;
point2.longitude = 6.13;
var point3 = new Point();
point3.sequence = 2;
point3.latitude = 5.25;
point3.longitude = 5.25;
pizzaPlace.deliveryArea = [point1, point2, point3];
expect(pizzaPlace.getPoint(0)).toBe(point1);
expect(pizzaPlace.getPoint(1)).toBe(point2);
expect(pizzaPlace.getPoint(2)).toBe(point3);
expect(pizzaPlace.getPoint(3)).toBe(null);
});
Here is the code generating the error:
onDownClick(): void {
if(this.selectedPoint) {
let currSeq = this.selectedPoint.sequence;
let nextSeq = currSeq + 1;
console.log("Current pizza place:" + JSON.stringify(this.pizzaPlaceEditService.currentPizzaPlace));
console.log("nextSeq:" + nextSeq);
let next = this.pizzaPlaceEditService.currentPizzaPlace.getPoint(nextSeq);
if(next) {
this.pizzaPlaceEditService.currentPizzaPlace.swapPoints(this.selectedPoint, next);
}
}
}
And here is the error:
Current pizza place:{"contactName":"Zaphod Beeblebrox","customerId":"TPGup2lt","deliveryArea":[{"errorMsg":"","sequence":0,"latitude":34.552,"longitude":-84.556},{"errorMsg":"","sequence":1,"latitude":34.711,"longitude":-84.665}],"deliveryZips":[],"paypalAccount":"HB17","settings":null,"shopName":"Donato's Hartland","shopStreet":"1531 Kenesaw Dr","shopCity":"Lexington","shopState":"KY","shopZip":"40515","shopPhone":"(859)555-2637","billStreet":"1531 Kenesaw Dr","billCity":"Lexington","billState":"KY","billZip":"40515","billPhone":"(859)555-2637"}
pizza-place-delivery.component.ts:63:6
nextSeq:1
pizza-place-delivery.component.ts:64:6
TypeError: this.pizzaPlaceEditService.currentPizzaPlace.getPoint is not a function
I have run out of things to try. Any advice appreciated!
Thanks to Artem's excellent input, I was able to figure out that the problem was caused by the fact that I was creating an object from the JSON instead of the object that I wanted and so it was a different type. Evidently, Classes in Typescript are compile time only and are discarded at runtime.
So, based on: How do I initialize a TypeScript object with a JSON object option 4 of the selected answer, I created a serializable.ts.
export interface Serializable<T> {
deserialize(input: Object): T;
}
And then modified my class with the implements:
export class PizzaPlace implements Serializable<PizzaPlace> {
and then added:
deserialize(input): PizzaPlace {
this.paypalAccount = input.paypalAccount;
this.customerId = input.customerId;
...
this.settings = input.settings;
return this;
}
Then, since my web service returns an array of these, I changed my service call:
.subscribe(places => this.deserializePlaces(places));
and added a new function:
deserializePlaces(places: Object[]){
this.pizzaPlaces = [];
places.forEach(obj=> {
let place = new PizzaPlace().deserialize(obj);
this.pizzaPlaces.push(place);
});
}
And this seems to work just fine. Thanks for the input.

Take elements while a condition evaluates to true (extending ElementArrayFinder)

We have a menu represented as a ul->li list (simplified):
<ul class="dropdown-menu" role="menu">
<li ng-repeat="filterItem in filterCtrl.filterPanelCfg track by filterItem.name"
ng-class="{'divider': filterItem.isDivider}" class="ng-scope">
Menu Item 1
</li>
...
<li ng-repeat="filterItem in filterCtrl.filterPanelCfg track by filterItem.name"
ng-class="{'divider': filterItem.isDivider}" class="ng-scope">
Menu Item 2
</li>
</ul>
Where somewhere at position N, there is a divider, which can be identified by evaluating filterItem.isDivider or by checking the text of the a link (in case of a divider, it's empty).
Now, the goal is to get all of the menu items that are located before the divider. How would you approach the problem?
My current approach is rather generic - to extend ElementArrayFinder and add takewhile() function (inspired by Python's itertools.takewhile()). Here is how I've implemented it (based on filter()):
protractor.ElementArrayFinder.prototype.takewhile = function(whileFn) {
var self = this;
var getWebElements = function() {
return self.getWebElements().then(function(parentWebElements) {
var list = [];
parentWebElements.forEach(function(parentWebElement, index) {
var elementFinder =
protractor.ElementFinder.fromWebElement_(self.ptor_, parentWebElement, self.locator_);
list.push(whileFn(elementFinder, index));
});
return protractor.promise.all(list).then(function(resolvedList) {
var filteredElementList = [];
for (var index = 0; index < resolvedList.length; index++) {
if (!resolvedList[index]) {
break;
}
filteredElementList.push(parentWebElements[index])
}
return filteredElementList;
});
});
};
return new protractor.ElementArrayFinder(this.ptor_, getWebElements, this.locator_);
};
And, here is how I'm using it:
this.getInclusionFilters = function () {
return element.all(by.css("ul.dropdown-menu li")).takewhile(function (inclusionFilter) {
return inclusionFilter.evaluate("!filterItem.isDivider");
});
};
But, the test is just hanging until jasmine.DEFAULT_TIMEOUT_INTERVAL is reached on the takewhile() call.
If I put console.logs into the loop and after, I can see that it correctly pushes the elements before the divider and stops when it reaches it. I might be missing something here.
Using protractor 2.2.0.
Also, let me know if I'm overcomplicating the problem.
Maybe I'm missing something, but couldn't you just go through ul li a elements while they gave you something from getText(), and store them to some array, or do something with them directly in that loop?
var i = 0;
var el = element.all(by.css('ul li a'));
var tableItems = [];
(function loop() {
el.get(i).getText().then(function(text){
if(text){
tableItems.push(el.get(i));
i+=1;
loop();
}
});
}());
takewhile() actually worked for me once I removed the protractor.promise = require("q"); from onPrepare() - this was there to replace protractor.promise with q on the fly to be able to use the syntactic sugar like spread() function. Apparently, it is not safe to use q in place of protractor.promise.
All I have to do now is to add this to onPrepare():
protractor.ElementArrayFinder.prototype.takewhile = function(whileFn) {
var self = this;
var getWebElements = function() {
return self.getWebElements().then(function(parentWebElements) {
var list = [];
parentWebElements.forEach(function(parentWebElement, index) {
var elementFinder =
protractor.ElementFinder.fromWebElement_(self.ptor_, parentWebElement, self.locator_);
list.push(whileFn(elementFinder, index));
});
return protractor.promise.all(list).then(function(resolvedList) {
var filteredElementList = [];
for (var index = 0; index < resolvedList.length; index++) {
if (!resolvedList[index]) {
break;
}
filteredElementList.push(parentWebElements[index])
}
return filteredElementList;
});
});
};
return new protractor.ElementArrayFinder(this.ptor_, getWebElements, this.locator_);
};
The usage is very similar to filter():
element.all(by.css("ul li a")).takewhile(function (elm) {
return elm.getText().then(function (text) {
return text;
});
});
FYI, proposed to make takewhile() built-in.

JavaScript: Accessing Nested Objects

The code looks like this
function Scripts() {this.FindById = function (id) {
this.FindById.constructor.prototype.value = function () {
return document.getElementById(id).value;
}}}
var Control = new Scripts();
Now when i say Control.FindById("T1").value(). I am not able to get the textInput("T1")'s value.
It seems that your code is a bit more complicated then it should be ;-)
Personally I would write it this way (not tested):
function Scripts() {
this.findById = function(id) {
var el = document.getElementById(id);
return {
value: function() {
return el.value;
}
}
}
}
The findById() now closes over a node and returns an interface that can return its value.
Also, your idea sounds a lot like Singleton, so you wouldn't even need the extra Scripts constructor:
var Control = {
findById: function(id) {
var el = document.getElementById(id);
return {
value: function() {
return el.value;
}
}
}
}
Working example: http://jsfiddle.net/YYkD7/
Try this:
function Scripts() {this.FindById = function (id) {
this.FindById.constructor.prototype.value = function () {
return document.getElementById(id).value
}}}
You didn't close the last "}" :-)

javascript object composition syntax

In the following code, I want to be able to call bindClickEvents() like so:
App.Utils.Modal.bindClickEvents();
However, I don't understand the syntax necessary to do this.
Current code:
var App = new Object;
App.Modal = {
bindClickEvents: function() {
return $('a.alert-modal').click(function(e) {
return console.log('Alert Callback');
});
}
};
$(document).ready(function() {
return App.Modal.bindClickEvents();
});
You can do it in one go:
var App = {
Modal : {
bindClickEvents : function () {/* ... */}
}
}
or if you want to break that up to separate steps:
var App = {};
App.Modal = {};
Modal.bindClickEvents = function () {/* ... */};
BTW, in reference to your original question title, this is not object chaining. This is object composition. Object chaining is being able to call methods in an object multiple times in a single statement.
Is this what you're trying to do?
var App = {};
App.Utils = {};
App.Utils.Modal = {
bindClickEvents: function() {
return $('a.alert-modal').click(function(e) {
return console.log('Alert Callback');
});
}
};
$(document).ready(function() {
return App.Utils.Modal.bindClickEvents();
});
Prefer the object literal syntax to the Object constructor; some authors go so far as to call the latter an anti-pattern
Here's the simplest way to set up App.Utils.Modal.bindClickEvents();
var App = {
Utils: {
Modal: {
bindClickEvents: function() {
return $('a.alert-modal').click(function(e) {
return console.log('Alert Callback');
});
}
}
}
};
Or you can piece it together one step at a time:
var App = {};
App.Utils = {};
App.Utils.Modal = {};
App.Utils.Modal.bindClickEvents = function() {
return $('a.alert-modal').click(function(e) {
return console.log('Alert Callback');
});
};

Categories