How to get immediate text with webdriverIO - css-selectors

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

Related

Cypress: cannot find elements in calendar popup

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()

how to type in multiple input instances with same name using cypress

I have to do an end to end testing on angularjs application using cypress.
I have two instances of the same input element. They have the same ng-model, class and name. We have got the unique id which is dynamically generated by the application which cannot be same everytime the page loads or if it's tested on a different machine.
As an example below, there are two input elements with the same name, but I would need the same text to appear on both the input elements. When I use the following commands, cypress is complaining about two instances of the same name. How can I type the same text'Hello world' on both the input elements with same name?
cy.get('input[name=description]').type('Hello World')
One way to try (may not be optimal) is
cy.get('input[name=description]').then(els => {
[...els].forEach(el => cy.wrap(el).type('Hello World'));
});
Some notes,
Cypress has a first() command, so you could do
cy.get('input[name=description]').first().type('Hello World');
but I there's no command second().
[...els] converts Cypress array to normal array, so you can forEach().
Update - use eq() command
If this seems too unweildy, add the following custom command to \cypress\support\command.js
Cypress.Commands.add('nth', { prevSubject: 'element' }, (els, index) => {
return cy.wrap([...els][index])
})
From comment from Jennifer Shehane, could do this more simply with
cy.get('input[name=description]').eq(0).type('Hello World');
cy.get('input[name=description]').eq(1).type('Hello World');
From the docs:
cy.get('ul>li').each(($el, index, $list) => {
// $el is a wrapped jQuery element
if ($el.someMethod() === 'something') {
// wrap this element so we can
// use cypress commands on it
cy.wrap($el).click()
} else {
// do something else
}
})
You can pick any unique identifier of your input. It could be id, placeholder etc.

Display content of an Array of Objects with Interpolation in Angular2 Typescript

Application
A simple Search bar and a button where user enters a keyword and the response returned is from a RESTful server (HTTP GET requests)
simplesearch.ts
export class SimpleSearch {
kw: string; // keyword
resp: string; // response from Server
}
simplesearch.service.ts
Has a simple method called searchData which does a HTTP GET request with the user's keyword as a query search. (Code not included for brevity)
simplesearch.component.ts
/*All respective headers and #Component removed from brevity*/
const OUTPUT: SimpleSearch[] = []; // create an array for storing Objects
export class SimpleSearchComponent {
Output = OUTPUT; // define variable for array
constructor(private httpServ: SimpleSearchService, private temp: SimpleSearch) {}
/*Search button on HTML file does this*/
Search(inputVal: string) {
this.temp.kw = inputVal; // store the value of user's input
this.httpServ.searchData(inputVal)
.then(res => this.temp.resp = res); // store the response in temp.resp
// push the Object on the Output Array
this.Output.push({kw: this.temp.kw, resp: this.temp.resp});
}
}
Interpolation Variable
I use Output as an Interpolation Variable for my HTML template. I show the data in an unordered list
<ul>
<li *ngFor="let keyword of Output">
<span>{{keyword.kw}}</span>
</li>
</ul>
Response:
<ul>
<li *ngFor="let answer of Output">
<span>{{answer.resp}}</span> <!-- WHAT TO DO HERE for Array Index-->
</li>
</ul>
Result
I can see the keywords in a list every time a user inputs new keywords but
the responses in the wrong way
How do I pass Indexing with the Interpolation? Or am I thinking wrong?
The easy way out was to create two separate Array<String> for keywords and responses and this works great since I can use the index to delete the contents on the page too but with an Object in an Array I am confused with the key: value representation and the index of the Array (OUTPUT) itself.
The problem lies exactly where developer noticed, this.temp.resp is outside the async function. So when you are pushing items in your Output array, it's always pushing the previous search with the new keyword, therefore you are getting the behavior that the resp is always "one step behind". You can check this to understand this async behavior: How do I return the response from an Observable/http/async call in angular2?
So let's look at the code and explain what is happening. I assume you have initialized 'temp' since it isn't throwing an error on first search, where temp.resp would be undefined unless temp is initialized.
this.httpServ.searchData(inputVal)
// this takes some time to execute, so the code below this is executed before 'this.temp.resp' has received a (new) value.
.then(res => this.temp.resp = res);
// old value of 'temp.resp' will be pushed, or if it's a first search, empty value will be pushed
this.Output.push({kw: this.temp.kw, resp: this.temp.resp});
So how to solve this, would be to move the this.Output.push(... line inside the callback (then), so that the correct values will be pushed to the array.
Also I'd change your model to be an Interface instead of Class. But as to how to change the function and do the assignment inside the callback, I'd also shorten the code a bit and do:
Search(inputVal: string) {
this.httpServ.searchData(inputVal)
.then(res => {
// we are pushing values inside callback now, so we have correct values!
// and 'SimpleSearch' stands for the interface
this.Output.push(<SimpleSearch>{kw: inputVal, resp: res});
});
}
}
This should take care of it that the corresponding keyword will have the corresponding response! :)
PS. Worth noticing here, is that you'd maybe want to display the keyword while we are waiting for the response for that keyword, I ignored it here though and applied the same as you are currently using.

Make part of the text as link.(react - localization)

I need to mark part of the text as a link. Something like:
"Please log in with your email...". This text must be localized later.
I need that "log in" part to be the link.
When I do something like this in the render method:
var link = React.DOM.a({
href: this.makeHref('login')
},
'log in'
);// or React.createElement or
//var link = <a href={this.makeHref('login')}>
// 'log in'</a>;
<div>{'Please '+ link + ' with your email...'}</div>
It will output:
Please `[object Object]` with your email...
Without surround text, I receive the expected result. In other words: How to make react render HTML not object.
This is a simplified example - I need to insert link text with format marker {0} like in C# - or any other working solution.
Thank you for help!
If you want to use an element within another element, just use curly braces like so:
var Component = React.createClass({
render: function() {
var link = <a href={this.makeHref('login')}>log in</a>;
return <div>Please {link} with your email.</div>;
}
};
You can see a working fiddle here: https://jsfiddle.net/jrunning/fencjn4x/
If you're going to be internationalizing your app at some point in the future I recommend a) crossing that bridge when you come to it, and b) using a solution like React Intl instead of trying to build your own solution with string concatenation.

How to use Backbone.Marionette.ItemView with Mustache

The following code works fine using Backbone.Marionette.ItemView but not Mustache.
Backbone.Marionette.ItemView - no Mustache
I would like to use the same code but loading the template varaible using Mustache.
Here is my code:
Backbone.Marionette.ItemView - with Mustache
Any idea why my code does not work and why?
Thanks
I'd like to update the answer here a bit as I was just struggling with this, and I was using this answer as a reference.
Here are my findings:
The answer here is a bit out of date with the current version of Mustache (which is understandable as it's pretty old)
Mustache.to_html is now deprecated, but still exists as a simple wrapper around Mustache.render for backwards compat. Check out this link.
Additionally, I found overriding Marionette.Renderer.render, as in the accepted answer above, completely bypasses the Marionette.TemplateCache layer which may not be the desired behavior.
Here's the source for the Marionette.Renderer.render method:
render: function(template, data){
if (!template) {
var error = new Error("Cannot render the template since it's false, null or undefined.");
error.name = "TemplateNotFoundError";
throw error;
}
var templateFunc;
if (typeof template === "function"){
templateFunc = template;
} else {
templateFunc = Marionette.TemplateCache.get(template);
}
return templateFunc(data);
}
Source
As you can see it accesses the Marionette.TemplateCache.get method and the above answer does nothing to maintain that functionality.
Now to get to my solve (note: the above answer is not wrong necessarily; this is just my approach to maintain the Marionette.TemplateCache layer):
As the comments suggest above, override compileTemplate instead:
Marionette.TemplateCache.prototype.compileTemplate = function(rawTemplate) {
// Mustache.parse will not return anything useful (returns an array)
// The render function from Marionette.Renderer.render expects a function
// so instead pass a partial of Mustache.render
// with rawTemplate as the initial parameter.
// Additionally Mustache.compile no longer exists so we must use parse.
Mustache.parse(rawTemplate);
return _.partial(Mustache.render, rawTemplate);
};
Here's a working JSFiddle as proof.
In the fiddle, I've also overridden Marionette.TemplateCache.loadTemplate to demonstrate that it's only called once. The body of the function only adds some debug output and then re-implements most of the original functionality (minus error handling).
Marionette assumes the use of UnderscoreJS templates by default. Simply replacing the template configuration for a view isn't enough. You also need to replace how the rendering process works.
In your simple example, you only need to override the Marionette.Renderer.render function to call Mustache, and then set the template of your views to the string template that you want:
Backbone.Marionette.Renderer.render = function(template, data){
return Mustache.to_html(template, data);
}
var rowTemplate = '{{ username }}{{ fullname }}';
// A Grid Row
var GridRow = Backbone.Marionette.ItemView.extend({
template: rowTemplate,
tagName: "tr"
});
Note that your JSFiddle still won't work even when you put this code in place, because the GridView is still using a jQuery selector/string as the template attribute. You'll need to replace this with the same type of template function to return mustache.
http://jsfiddle.net/derickbailey/d7qDz/

Resources