I am trying to keep my pageObjects in Protractor as clean as possible, but have run up against some behavior in selecting sub-elements.
This works:
var parent = element(by.css('.parent-class'));
parent.element(by.css('.child-class')).getText();
However this does not:
var parent = element(by.css('.parent-class'));
var child = element(by.css('.child-class'));
parent.child.getText();
Is there someway to do something like the second example? I'd rather not have the element locators spread throughout the methods on my pageObjects, but it seems thats the only way to locate subelements?
In actual application I have a long list of cards, from which I filter down to just the one I am looking for. I then want to do things with subelements of the card.
You could use the locator() function to get the locator of the child element and use it to find a child of the parent. This is similar to the solution you provided in your comment, but allows you to define all properties on your page object as web elements instead of a mix of elements and locators:
var parent = element(by.css('.parent-class'));
var child = element(by.css('.child-class'));
parent.element(child.locator()).getText();
I have a lot of the following code:
var parent = element(by.css('.parent-class'));
var child = parent.element(by.css('.child-class'));
child.getText();
But as far as I understood you have a lot of children and you don't want to list all the variants.
Additionally to Nathan Thompson answer you can have a helper function in page object to find subelement:
function getCard(parent, child) { // Or getChild()
return element(by.css(parent)).element(by.css(child));
}
function getCardText(parent, child) { // Or getChildText
return getCard(parent, child).getText();
}
So then you can just write in spec:
expect(cardPage.getCardText('.parent-class', '.child-class')).toBe('...');
I wanted to mention that using 5.2.2 version this implementation is bit different.
To get actual selector value you must use following code
let selector = child.locator().value
This is because locator returns an object which contains selector and other properties, but in this case, you only need selector.
here is what is returned by method locator()
name(name) {
return By.css('*[name="' + escapeCss(name) + '"]');
}
{ using: 'css selector', value: '.child-class' }
Here is how it should be implemented now.
var parent = element(by.css('.parent-class'));
var child = element(by.css('.child-class'));
parent.element(child.locator().value).getText();
//short hand
var parent = $('.parent-class');
var child = $('.child-class')
parent.$(child.locator().value).getText();
Related
I am trying to keep my pageObjects in Protractor as clean as possible, but have run up against some behavior in selecting sub-elements.
This works:
var parent = element(by.css('.parent-class'));
parent.element(by.css('.child-class')).getText();
However this does not:
var parent = element(by.css('.parent-class'));
var child = element(by.css('.child-class'));
parent.child.getText();
Is there someway to do something like the second example? I'd rather not have the element locators spread throughout the methods on my pageObjects, but it seems thats the only way to locate subelements?
In actual application I have a long list of cards, from which I filter down to just the one I am looking for. I then want to do things with subelements of the card.
You could use the locator() function to get the locator of the child element and use it to find a child of the parent. This is similar to the solution you provided in your comment, but allows you to define all properties on your page object as web elements instead of a mix of elements and locators:
var parent = element(by.css('.parent-class'));
var child = element(by.css('.child-class'));
parent.element(child.locator()).getText();
I have a lot of the following code:
var parent = element(by.css('.parent-class'));
var child = parent.element(by.css('.child-class'));
child.getText();
But as far as I understood you have a lot of children and you don't want to list all the variants.
Additionally to Nathan Thompson answer you can have a helper function in page object to find subelement:
function getCard(parent, child) { // Or getChild()
return element(by.css(parent)).element(by.css(child));
}
function getCardText(parent, child) { // Or getChildText
return getCard(parent, child).getText();
}
So then you can just write in spec:
expect(cardPage.getCardText('.parent-class', '.child-class')).toBe('...');
I wanted to mention that using 5.2.2 version this implementation is bit different.
To get actual selector value you must use following code
let selector = child.locator().value
This is because locator returns an object which contains selector and other properties, but in this case, you only need selector.
here is what is returned by method locator()
name(name) {
return By.css('*[name="' + escapeCss(name) + '"]');
}
{ using: 'css selector', value: '.child-class' }
Here is how it should be implemented now.
var parent = element(by.css('.parent-class'));
var child = element(by.css('.child-class'));
parent.element(child.locator().value).getText();
//short hand
var parent = $('.parent-class');
var child = $('.child-class')
parent.$(child.locator().value).getText();
Restangular offers a feature, extendModel, which lets you add functionality onto objects returned from the server. Is there any way to get these methods added to an empty / new model, that hasn't yet been saved to the server?
I wanted to do the same thing but didn't find an example. Here's how I ended up doing it:
models.factory('User', function(Restangular) {
var route = 'users';
var init = {a:1, b:2}; // custom User properties
Restangular.extendModel(route, function(model) {
// User functions
model.myfunc = function() {...}
return model;
});
var User = Restangular.all(route);
User.create = function(obj) {
// init provides default values which will be overridden by obj
return Restangular.restangularizeElement(null, _.merge({}, init, obj), route);
}
return User;
}
Some things to be aware of:
Use a function like _.merge() instead of angular.extend() because it clones the init variable rather than simply assigning its properties.
There is a known issue with Restangular 1.x that causes the Element's bound data to not be updated when you modify its properties (see #367 and related). The workaround is to call restangularizeElement() again before calling save(). However this call will always set fromServer to false which causes a POST to be sent so I wrote a wrapper function that checks if id is non-null and sets fromServer to true.
Given I have the elmFinder variable:
var elmFinder = element(by.css('.thing'));
What if i need to get back the webdriver.Locator, a.k.a locator strategy? i.e.
elmFinder.??? //=> by.css('.thing')
I'm looking after the function ??? if it exists.
UPDATE:
This feature has been merged and we can now do:
elmFinder.locator();
UPDATE:
This feature has been merged and we can now do:
elmFinder.locator();
Old answer:
You cannot. The element finder does not keep a reference to the locator:
https://github.com/angular/protractor/blob/master/lib/protractor.js#L103
What I typically do is store the selector in it's own var, and then place that string into the selector, so I can use both interchangably:
var cssThingSelector = '.thing';
var elem = $(cssThingSelector);
Something like that.
Edit:
I will also add that you can nest findElement calls from selenium webelement objects.
So, if there is another item in the inner html of the .thing web element (say, a span tag), you could just nest another findElement call:
var spanElem = elem.$('span');
You can do this as much as you'd like.
In a separate Page Object file (not in the actual file with tests) I'm trying to do something like:
this.item0 = element.all(by.repeater('menu items')).get(0);
This won't work because the code is executed before the tests are run. I haven't found another way of doing this except to call get() in the test file (which I don't want to do). Is there a way to do this in the Page Object file?
This behavior often confuses people who are trying to write page objects.
Your locator will not be executed until you call a function on the elementFinder or the elementArrayFinder (see https://github.com/angular/protractor/blob/master/docs/api.md).
I usually use this pattern:
// Page object
MyView = function() {
// This will not find elements until you call count(), get(), first(), etc.
this.itemList = element.all(by.repeater('menu items'));
};
module.exports = new MyView();
// Test
// Require the page object at the top of the test file.
var myView = require('./my-view.js');
// Use the page object in the test.
it('should get first element', function() {
myView.itemList.get(0).then(function(webElement) {
})
});
The following is from source code for ElementArrayFinder.get
/**
* Get an element within the ElementArrayFinder by index. The index starts at 0.
* This does not actually retrieve the underlying element.
*
ElementArrayFinder.prototype.get = function...
So Apparently you should be able to call it from within the page object even before the elements are loaded.
yay, i'm not the only one using page objects ;)
i did the following:
var MyPage = function () {
this.item0 = element.all(by.repeater('menu items')).get(0);
}
in my tests:
describe('MyPage:', function () {
var myPage = new MyPage();
// after this line navigate your browser to your page
// then you can call myPage.item0, myPage.whatEver
}
I have 2 main questions.
Does extending things like Object count?
What is DOM wrapping?
http://perfectionkills.com/whats-wrong-with-extending-the-dom/
After reading that article I couldn't find anything about DOM wrapping, and no specification and what exactly is and isn't DOM extension.
No, Object is specified as part of the Javascript language, while the DOM is an API only relevant in a browser environment and is used to "access and update the content, structure and style of documents" (W3C).
However, one of the reasons provided in that article arguing against the extension of DOM objects still applies to extending native types such as Object - namely the chance of collisions.
Wrapping an object refers to creating a new object that references the original, but providing additional functionality through the new, wrapper object.
For example, rather than extending a DOM Element object with a cross-browser addClass function like this:
var element = document.getElementById('someId');
element.addClass = function (className) {
...
};
You can instead define a wrapper function:
var ElementWrapper = function (element) {
this.element = element;
};
And add the function to its prototype:
ElementWrapper.prototype.addClass = function (className) {
...
};
And "wrap" elements like this:
var element = document.getElementById('someId');
var wrapped = new ElementWrapper(element);
wrapped.addClass('someClass');