How do I get a text from a div class Cypress - javascript

Alright, I want to get a text from a div and a class. The html looks like this:
<div class="inventory_item_name">Backpack</div>
And my Code is this:
const article = cy.get('.inventory_item_price').then((theElement) => {
theElement.text();
});
The problem: When I do cy.log(article) I get Object{5}

Just like Alex mentioned in the comment, you cannot return a value from cy commands. But you can do so in the .then block just like:
it("should have text of 'Backpack'", () => {
// I have changed the selector since the class name in your HTML is ".inventory_item_name" not ".inventory_item_price"
cy.get(".inventory_item_name").then(($el) => {
const text = $el.text(); // Now you have the text "Backpack"
// Do the assertion here
expect(text).to.eq("Backpack");
});
});
You can learn more about why Cypress doesn't return value in this documentation

As cypress documentation says
You cannot assign or work with the return values of any
Cypress command. Commands are enqueued and run asynchronously.
// this won't work the way you think it does
const button = cy.get('button')
const form = cy.get('form')
button.click()
For more information refer : variables-and-aliases
You have a few options. But you need to keep in mind the asynchronous nature of cypress
Use alias (if you want to share the article variable in other tests ) so the code will look like this,
Example:
it("Test 1", () => {
cy.get('.inventory_item_price').invoke('text').as('article')
})
//Now you can use this.article in all the other test cases
//If you are using aliases remember Not to use arrow functions on hooks
it("Test 2", function(){
//this will log Backpack
cy.log(this.article)
})
Use .then, As mentioned by #konekoya in the answer, Since cypress is asynchronous you need to use .then to execute a task after a previous task is completed.
Example:
cy.get('.inventory_item_price').invoke('text').then(article=> {
//you can use articale variable as much as you want inside .then
//you can't use it outside
const value = articale ;
cy.log(article)
cy.log(value )
})
Note: to get inner Text you there are several methods. You can do the same thing using,
cy.get('.inventory_item_price').then(article => {
const value = articale.text() ;
cy.log(article.text())
cy.log(value)
})

You can't assign text values like this.
const article = cy.get('.inventory_item_price').then((theElement) => {
theElement.text();
});
The reason is the bellow code does not return a text element.
cy.get('.inventory_item_price').then((theElement) => {
theElement.text();
});
So you need to change it like this.
cy.get('.inventory_item_price').then((theElement) => {
const article = theElement.text();
//not you can use article
});

You can return the article text with below code. Please also look at the link where I have shared the answer for more details.
Storing an element's text in Cypress outside of a chainer method
return cy.get('.inventory_item_price').then((theElement) => {
return theElement.text();
}).then(article => {
cy.log(`article is ${article}`);
})

Related

Cypress: End chain conditionally

I'm quite new to cypress, but I am wondering if there is some way to end a command chain conditionally? I know that conditional testing should be avoided in cypress, but I want try this out anyway.
What I've tried
I tried to solve it by passing in the chain to a custom command, but it doesn't seem to work.
Cypress.Commands.add('ifExists', { prevSubject: true }, (subject: object, element: string) => {
cy.get('body').then(($body) => {
if ($body.find(element).length) {
cy.wrap(object).end();
} else {
// something else
}
})
})
and also
Cypress.Commands.add('ifExists', { prevSubject: true }, (subject: JQuery<HTMLBodyElement>, element: string) => {
cy.wrap(subject).then(($body) => {
if ($body.find(element).length) {
return cy.wrap(subject);
} else {
return cy.wrap(subject).end();
}
})
})
And just a clarification on what I want from this command. I want to be able to add it to a chain like this:
cy.get('body > #specialThing').ifExists().then((thing) => ...)
or this:
cy.ifExists('body > #specialThing').then((thing) => ...)
All help and advice is appreciated!
I'd check out the Cypress documentation on conditional testing, in particular the part about element existence. The tl;dr is you just need to search for a more general element that yields something that you can then assert on.
y.get('body')
.then(($body) => {
// synchronously query from body
// to find which element was created
if ($body.find('#specialThing').length) {
// input was found, do something else here
return 'input'
}
// else assume it was textarea
return 'textarea'
})
.then((selector) => {
// selector is a string that represents
// the selector we could use to find it
cy.get(selector).type(`found the element by selector ${selector}`)
})
You can adapt the jQuery OR operator shown here Function to check if element exist in cypress.
You cannot it call using the cy.get('body > #specialThing').ifExists()... because the cy.get() will fail if the element isn't there, so this implies your custom command must be a parent command (no prevSubject).
Cypress.Commands.add('ifExists', { prevSubject: false }, (selector: string) => {
const orSelector = `${selector}, body`; // note comma, if "selector" fails return body
return Cypress.$(orSelector);
})
cy.ifExists('#specialThing').then((thing) => ...)
You may find it more useful to return null rather than body so that you can test the result more easily,
Cypress.Commands.add('ifExists', { prevSubject: false }, (selector: string) => {
const result = Cypress.$(selector);
return result.length ? cy.wrap(result) : null;
})
cy.ifExists('#specialThing').then((thing) => if (thing) ... else ... )
Warning this will inevitably lead to flaky tests. There is no retry in this command.
See Element existence
You cannot do conditional testing on the DOM unless you are either:
Server side rendering with no asynchronous JavaScript.
Using client side JavaScript that only ever does synchronous rendering.
It is crucial that you understand how your application works else you will write flaky tests.
Using cy.get('body').then(...) does not give you retry on the element you want to test conditionally.

Call function from another function with parameters passed from both functions

I have this load-more listener on a button that calls the functions and it works fine.
let moviesPage = 1;
let seriesPage = 1;
document.getElementById('load-more').addEventListener('click', () => {
if (document.querySelector('#movies.active-link')) {
moviesPage++;
getMovies(moviesPage);
//getMovies(genreId, moviesPage);
} else if (document.querySelector('#series.active-link')) {
seriesPage++;
getSeries(seriesPage);
}
});
Now I have another listener on a list of links that calls the following code. It takes the genreId from the event parameter to sent as an argument to the api call. Also works fine so far.
document.querySelector('.dropdown-menu').addEventListener('click',
getByGenre);
function getByGenre (e) {
const genreId = e.target.dataset.genre;
movie.movieGenre(genreId)
.then(movieGenreRes => {
ui.printMovieByGenre(movieGenreRes);
})
.catch(err => console.log(err));
};
What I want to do is to call getByGenre from the load-more listener while passing also the moviesPage argument as you can see on the commented code so it can also be passed to the api call.
What would be the best way to do that? I've looked into .call() and .bind() but I'm not sure if it's the right direction to look at or even how to implement it in this situation.
Short Answer
Kludge: Global State
The simplest, though not the most elegant, way for you to solve this problem right now is by using some global state.
Take a global selection object that holds the selected genreId. Make sure you declare the object literal before using it anywhere.
So, your code might look something like so:
var selection = { };
document.querySelector('.dropdown-menu').addEventListener('click',
getByGenre);
function getByGenre (e) {
const genreId = e.target.dataset.genre;
selection.genreId = genreId;
movie.movieGenre(...);
};
...
let moviesPage = 1;
let seriesPage = 1;
document.getElementById('load-more').addEventListener('click', () => {
if (document.querySelector('#movies.active-link')) {
...
if (selection.genreId !== undefined) {
getMovies(selection.genreId, moviesPage);
}
} else if (...)) {
...
}
});
Closure
A more elegant way for you to accomplish this is by using a closure, but for that I have to know your code structure a bit more. For now, global state like the above will work for you.
Longer Answer
Your concerns have not been separated. You are mixing up more than one concern in your objects.
For e.g. to load more movies, in your load-more listener, you call a function named getMovies. However, from within the .dropdown-menu listener, you call into a movie object's method via the getByGenre method.
Ideally, you want to keep your UI concerns (such as selecting elements by using a query selector or reading data from elements) separate from your actual business objects. So, a more extensible model would have been like below:
var movies = {
get: function(howMany) {
if (howMany === undefined) {
howMany = defaultNumberOfMoviesToGetPerCall;
}
if (movies.genreId !== undefined) {
// get only those movies of the selected genre
} else {
// get all kinds of movies
}
},
genreId : undefined,
defaultNumberOfMoviesToGetPerCall: 25
};
document.get...('.load-more').addEventListener('whatever', (e) => {
var moviesArray = movies.get();
// do UI things with the moviesArray
});
document.get...('.dropdown-menu').addEventListener('whatever', (e) => {
movies.genreId = e.target.dataset.genreId;
var moviesArray = movies.get();
// do UI things with the moviesArray
});

Jasmine Unit Test: should I test the logic or the actual mocked data?

when performing unit testing is it better to test the literal result that I expect hard-coding it into the test (expect(x).toBe(17)), or is it better to test the logic and not the specific mock data that I am using (expect(x).toBe(mockedData.value))
The first approach seems safer because I am sure that the test is actually testing the literal result that I expect, however the second approach is more flexible since it allows me to test the logic rather than worry about the mock data (which I can also change later without having to rewrite the test itself)
What are the advantages/disadvantages of either approach? What is the best practice in these cases?
Following is a quick example:
// MockedData is a very long array of complex objects
// each of them has a property 'value' of type number
import mockedData from 'data.mock';
class ClassToTest {
private data;
constructor(data) {
this.data = data;
}
plusOne(): number {
return this.data.value + 1;
}
}
describe('test', () => {
let instance: ClassToTest;
beforeEach(() => {
instance = new ClassToTest(mockedData[0]);
})
it('plusOne() should return the property "value" plus one', () => {
// Should I write this...
expect(instance.plusOne()).toBe(mockedData[0] + 1);
// ...or this?
expect(instance.plusOne()).toBe(17); // Because I know that mockedData[0].value is 16
})
});
Thank you very much!! :)
In your test you want to test your unit, which in your case is the logic inside of your plusOne() function. So you want to only know if something changes inside the function.
The most dangerous path is to use expect(instance.plusOne()).toBe(17);, because if someone changes your logic to return this.data.value + 2;, you will never spot from test only if the problem is in the function logic or in the mockedData.
The less dangerous approach is to use expect(instance.plusOne()).toBe(mockedData[0] + 1);, because this will tell you if the logic in your function change. Still not optimal, since you depend on an external mock to run your test that you don't need. Why would you want to depend on an external mocked data to test your unit?
The best way to test your unit logic here is to do something like this:
describe('test', () => {
let instance: ClassToTest;
const mockedValue = 1;
beforeEach(() => {
instance = new ClassToTest(mockedValue);
})
it('plusOne() should return the property "value" plus one', () => {
expect(instance.plusOne()).toBe(mockedValue + 1);
})
});
Then, you can implement separate tests for your service, here you only test the logic inside plusOne().

Logical OR for expected results in Jest

It will be the best explain in on example
expected(someNumber).toBe(1).or.toBe(-2).or.toBe(22) // expect result is 1 or -2 or 22
This is bad syntax, but can do sth like that in jest?
A simple way around this is to use the standard .toContain() matcher (https://jestjs.io/docs/en/expect#tocontainitem) and reverse the expect statement:
expect([1, -2, 22]).toContain(someNumber);
If you really needed to do exactly that, I suppose you could put the logical comparisons inside the expect call, e.g.
expect(someNumber === 1 || someNumber === -2 || someNumber === 22).toBeTruthy();
If this is just for a "quick and dirty" check, this might suffice.
However, as suggested by several comments under your question, there seem to be several "code smells" that make both your initial problem as well as the above solution seem like an inappropriate way of conducting a test.
First, in terms of my proposed solution, that use of toBeTruthy is a corruption of the way Jasmine/Jest matchers are meant to be used. It's a bit like using expect(someNumber === 42).toBeTruthy(); instead of expect(someNumber).toBe(42). The structure of Jest/Jasmine tests is to provide the actual value in the expect call (i.e. expect(actualValue)) and the expected value in the matcher (e.g. toBe(expectedValue) or toBeTruthy() where expectedValue and true are the expected values respectively). In the case above, the actual value is (inappropriately) provided in the expect call, with the toBeTruthy matcher simply verifying this fact.
It might be that you need to separate your tests. For example, perhaps you have a function (e.g. called yourFunction) that you are testing that provides (at least) 3 different possible discrete outputs. I would presume that the value of the output depends on the value of the input. If that is the case, you should probably test all input/output combinations separately, e.g.
it('should return 1 for "input A" ', () => {
const someNumber = yourFunction("input A");
expect(someNumber).toBe(1);
});
it('should return -2 for "input B" ', () => {
const someNumber = yourFunction("input B");
expect(someNumber).toBe(-2);
});
it('should return 22 for "input C" ', () => {
const someNumber = yourFunction("input C");
expect(someNumber).toBe(22);
});
..or at least...
it('should return the appropriate values for the appropriate input ', () => {
let someNumber;
someNumber = yourFunction("input A");
expect(someNumber).toBe(1);
someNumber = yourFunction("input B");
expect(someNumber).toBe(-2);
someNumber = yourFunction("input C");
expect(someNumber).toBe(22);
});
One of the positive consequences of doing this is that, if your code changes in the future such that, e.g. one (but only one) of the conditions changes (in terms of either input or output), you only need to update one of three simpler tests instead of the single more complicated aggregate test. Additionally, with the tests separated this way, a failing test will more quickly tell you exactly where the problem is, e.g. with "input A", "input B", or "input C".
Alternatively, you may need to actually refactor yourFunction, i.e. the code-under-test itself. Do you really want to have a particular function in your code returning three separate discrete values depending on different input? Perhaps so, but I would examine the code separately to see if it needs to be re-written. It's hard to comment on this further without knowing more details about yourFunction.
To avoid putting all the logical comparisons in one statement and using toBeTruthy(), you can use nested try/catch statements:
try {
expect(someNumber).toBe(1)
}
catch{
try {
expect(someNumber).toBe(-2)
}
catch{
expect(someNumber).toBe(22)
}
}
To make it more convenient and more readable, you can put this into a helper function:
function expect_or(...tests) {
if (!tests || !Array.isArray(tests)) return;
try {
tests.shift()?.();
} catch (e) {
if (tests.length) expect_or(...tests);
else throw e;
}
}
NB: With Typescript replace line 1 with function expect_or(...tests: (() => void)[]) { to add types to the function parameter.
and use it like this:
expect_or(
() => expect(someNumber).toBe(1),
() => expect(someNumber).toBe(-2),
() => expect(someNumber).toBe(22)
);
As #JrGiant suggested, there could be a toBeOneOf, however, it is easy top implement your own matcher:
Example in TypeScript:
expect.extend({
toBeOneOf(received: any, items: Array<any>) {
const pass = items.includes(received);
const message = () =>
`expected ${received} to be contained in array [${items}]`;
if (pass) {
return {
message,
pass: true
};
}
return {
message,
pass: false
};
}
});
// Declare that jest contains toBeOneOf
// If you are not using TypeScript, remove this "declare global" altogether
declare global {
namespace jest {
interface Matchers<R> {
toBeOneOf(items: Array<any>): CustomMatcherResult;
}
}
}
describe("arrays", () => {
describe("getRandomItemFromArray", () => {
it("should return one of the expected - 1", () => {
expect(getRandomItemFromArray([1, 2])).toBeOneOf([1, 2])
});
});
});
I was also looking for a solution for the expect.oneOf issue. You may want to checkout d4nyll's solution.
Here is an example of how it could work.
expect(myfunction()).toBeOneOf([1, -2, 22]);
I recommend using the .toContain(item) matcher. The documentation can be found here.
The below code should work well:
expect([1, -2, 22]).toContain(someNumber);

Loop in js for specific value without if

I use the following code which is working great but I wonder if in JS there is a way to avoid the if and to do it inside the loop, I want to use also lodash if it helps
for (provider in config.providers[0]) {
if (provider === "save") {
....
You can chain calls together using _.chain, filter by a value, and then use each to call a function for each filtered result. However, you have to add a final .value() call at the end for it to evaluate the expression you just built.
I'd argue that for short, simple conditional blocks, an if statement is easier and more readable. I'd use lodash- and more specifically chaining- if you are combining multiple operations or performing sophisticated filtering, sorting, etc. over an object or collection.
var providers = ['hello', 'world', 'save'];
_.chain(providers)
.filter(function(provider) {
return provider === 'save';
}).each(function(p) {
document.write(p); // your code here
}).value();
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.8.0/lodash.js"></script>
Edit: My mistake; filter does not have an overload where you can just supply a literal value. If you want to do literal value checking you have to supply a function as in my amended answer above.
I'd argue that what you have there is pretty good, clean and readable, but since you mentioned lodash, I will give it a try.
_.each(_.filter(config.providers[0], p => p === 'save'), p => {
// Do something with p
...
});
Note that the arrow function/lambda of ECMAScript 6 doesn't come to Chrome until version 45.
Basically, you are testing to see if config.providers[0], which is an object, contains a property called save (or some other dynamic value, I'm using a variable called provider to store that value in my example code below).
You can use this instead of using a for .. in .. loop:
var provider = 'save';
if (config.providers[0][provider] !== undefined) {
...
}
Or using #initialxy's (better!) suggestion:
if (provider in config.providers[0]) {
...
}
How about:
for (provider in config.providers[0].filter(function(a) {return a === "save"}) {
...
}
Strategy, you are looking for some kind of strategy pattern as,
Currenlty the save is hardcoded but what will you do if its coming from other varible – Al Bundy
var actions = {
save: function() {
alert('saved with args: ' + JSON.stringify(arguments))
},
delete: function() {
alert('deleted')
},
default: function() {
alert('action not supported')
}
}
var config = {
providers: [{
'save': function() {
return {
action: 'save',
args: 'some arguments'
}
},
notSupported: function() {}
}]
}
for (provider in config.providers[0]) {
(actions[provider] || actions['default'])(config.providers[0][provider]())
}
Push „Run code snippet” button will shows two pop-ups - be carefull
It is not clearly stated by the original poster whether the desired output
should be a single save - or an array containing all occurrences of
save.
This answer shows a solution to the latter case.
const providers = ['save', 'hello', 'world', 'save'];
const saves = [];
_.forEach(_.filter(providers, elem => { return elem==='save' }),
provider => { saves.push(provider); });
console.log(saves);
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.19/lodash.js"></script>

Categories