Cypress command vs JS function - cypress

The Cypress documentation suggests that commands are the right way to reuse fragments of code, e.g.
Cypress.Commands.add("logout", () => {
cy.get("[data-cy=profile-picture]").click();
cy.contains("Logout").click();
});
cy.logout();
For simple cases like this, why would I use a command over a plain JS function (and all the nice IDE assistance that comes with it). What are the drawbacks of rewriting the above snippet as
export function logout(){
cy.get("[data-cy=profile-picture]").click();
cy.contains("Logout").click();
}
// and now somewhere in a test
logout();

Based on my experience with Cypress (one year project and several hundred test cases), I can say that a plan JS function is great for grouping cy commands.
From my point of view, a custom cy command may be really useful only if it is incorporated into the chain processing (utilizes the subject parameter or returns a Chainable to be used further in the chain). Otherwise, a plain JS function is preferable due to it simplicity and full IDE support (unless you're using an additional plugin).
If you for any reason need to do something inside the cypress loop, you can always wrap you code by cy.then() in a plain JS function:
function myFunction() {
cy.then(() => {
console.log(("I'm inside the Cypress event loop"))
})
}

Commands are for behavior that is needed across all tests. For example, cy.setup or cy.login. Otherwise, use functions.
See official docs: https://docs.cypress.io/api/cypress-api/custom-commands#1-Don-t-make-everything-a-custom-command

Related

What is the advantage of Cypress commands over vanilla functions?

It is known that in Cypress we can define custom commands in the commands.js file, with that syntax:
Cypress.Commands.add('login', (email, pw) => {})
Cypress.Commands.overwrite('visit', (orig, url, options) => {})
These commands will then become available across all our tests and can be used from the cy object.
cy.login('my#email.com', '123456')
cy.visit('www.stackoverflow.com', 'www.google.com', { redirect: true })
But I don't get the gist of it. What is the point of doing that when you could just write a regular function?
function login(email, pw) { /* ... */ }
login('my#email.com', '123456')
The only advantage I see is making the function available everywhere without having to export/import it, but you can do that with globals as well. Is that it, or am I missing something?
1. Don't make everything a custom command.
Don't be forget cypress provide API for overwriting existing commands not for only creating new commands.
JS function is great for grouping cy commands and preferable due to it simplicity and readability.
There’s no reason to add this level of complexity when you’re only wrapping a couple of commands only for avoiding Export/Import.
2. Don't overcomplicate things.
A good test automation framework will be is easily understandable, readable and maintainable.Try not to overcomplicate things and create too many abstractions. When in doubt, Always use a JS function.
check out here: https://docs.cypress.io/api/cypress-api/custom-commands#Best-Practices
Yes you are right! The goal is to write cleaner code and availability. With custom commands you can basically write the function in the form of a cypress command and also you don't have to write an import statement in the tests suites. Hence your test suite files remain uniform and cleaner.

Cypress: How to capture text from a selector on one page to use as text on another page

New cypress user here, I am aware that cypress does not handle variables like how testcafe and others do due to the asyn nature of it. Using the example given and what I could find I have this as an example:
cy.get('selector').invoke('text').as('text_needed')
cy.get('#text_needed')
const txtneeded = this.text_needed
cy.log(txtneeded)
This looks at a given selector, takes what it finds and uses it as text and set it as a variable usable at any time in the test and outputs it to the log. The plan is to use that text in a search filter in another page to find the item it references.
The problem is that it fails with Cannot read properties of undefined (reading 'text_needed')
Is this because the content of the selector is not assigned to text properly, the outer html is <a data-v-78d50a00="" data-v-3d3629a7="" href="#">PO90944</a> The PO90944 is what I want to capture.
Your help would be appreciated!
You cannot save an alias and access it via this.* in the same execution context (callback) because it's a synchronous operation and your alias is not yet resolved at this time.
This is a correct way to go:
cy.get('selector').invoke('text').as('text_needed')
cy.get('#text_needed').then(txtneeded => {
cy.log(txtneeded)
})
First, make sure to define it as traditional function, not as an arrow function as this context doesn't work as you'd expect there, more info here.
Next, typically in a single test you should use .then() callback to perform additional actions on elements, and use aliases when you need to share context between hooks or different tests, so please consider the following:
// using aliases together with this within the single test won't work
cy.get(<selector>).invoke('text').as('text_needed')
cy.get('#text_needed').should('contain', 'PO90944') // works fine
cy.log(this.text_needed) // undefined
// this will work as expected
cy.get(<selector>).invoke('text').then(($el) => {
cy.wrap($el).should('contain', 'PO90944'); // works fine
cy.log($el) // works fine
});
Setting alias in beforeEach hook for example, would let you access this.text_needed in your tests without problems.
Everything nicely explained here.
Edit based on comments:
it('Some test', function() {
cy.visit('www.example.com');
cy.get('h1').invoke('text').as('someVar');
});
it('Some other test', function() {
cy.visit('www.example.com');
cy.log('I expect "Example Domain" here: ' + this.someVar);
});
And here's the output from cypress runner:

Accessing aliases in Cypress with "this"

I'm trying to share values between my before and beforeEach hooks using aliases. It currently works if my value is a string but when the value is an object, the alias is only defined in the first test, every test after that this.user is undefined in my beforeEach hook. How can I share a value which is an object between tests?
This is my code:
before(function() {
const email = `test+${uuidv4()}#example.com`;
cy
.register(email)
.its("body.data.user")
.as("user");
});
beforeEach(function() {
console.log("this.user", this.user); // This is undefined in every test except the first
});
The alias is undefined in every test except the first because aliases are cleared down after each test.
Aliased variables are accessed via cy.get('#user') syntax. Some commands are inherently asynchronous, so using a wrapper to access the variable ensures it is resolved before being used.
See documentation Variables and Aliases and get.
There does not seem to be a way to explicitly preserve an alias, as the is with cookies
Cypress.Cookies.preserveOnce(names...)
but this recipe for preserving fixtures shows a way to preserve global variables by reinstating them in a beforeEach()
let city
let country
before(() => {
// load fixtures just once, need to store in
// closure variables because Mocha context is cleared
// before each test
cy.fixture('city').then((c) => {
city = c
})
cy.fixture('country').then((c) => {
country = c
})
})
beforeEach(() => {
// we can put data back into the empty Mocha context before each test
// by the time this callback executes, "before" hook has finished
cy.wrap(city).as('city')
cy.wrap(country).as('country')
})
If you want to access a global user value, you might try something like
let user;
before(function() {
const email = `test+${uuidv4()}#example.com`;
cy
.register(email)
.its("body.data.user")
.then(result => user = result);
});
beforeEach(function() {
console.log("global user", user);
cy.wrap(user).as('user'); // set as alias
});
it('first', () => {
cy.get('#user').then(val => {
console.log('first', val) // user alias is valid
})
})
it('second', () => {
cy.get('#user').then(val => {
console.log('second', val) // user alias is valid
})
})
Replace
console.log("global user", this.user);
with
cy.log(this.user);
and it should work as expected.
The reason for this is the asynchronous nature of cypress commands. Think of it as a two-step process: All the cypress commands are not doing what you think, when they run. They just build up a chain of commands. This chain is executed as the test later on.
This is obviously not the case for other commands like console.log(). This command is executed when preparing the test.
This is explained in great detail in the cypress documentation:
But I felt it very hard to get my head around this. You have to get used to it.
One rule of thumb: Almost every command in your test should be a cypress command.
So just use cy.log instead of console.log
If you must use console.log you can do it like this:
cy.visit("/).then(() => console.log(this.user))
this way the console.log is chained. Or if you do not have a subject to chain off, build your own custom command like this:
Cypress.Commands.add("console", (message) => console.log(message))
cy.console(this.user)
Another mistake with using this in cypress is using arrow functions. If you do, you don't have access to the this you are expecting. See Avoiding the use of this in the cypress docs.
TL;DR: If you want an aliased user object available in each of your tests, you must define it in a beforeEach hook not a before hook.
Cypress performs a lot of cleanup between tests and this includes clearing all aliases. According to the Sharing Contexts section of Variables and Aliases: "Aliases and properties are automatically cleaned up after each test." The result you are seeing (your alias is cleaned after the first test and subsequently undefined) is thus expected behavior.
I cannot determine what register does in the original post, but it seems your intention is to save the overhead of performing API calls repeatedly in a beforeEach hook. It is definitely easiest to put everything you want in the beforeEach hook and ignore the overhead (also, pure API calls with no UI interaction will not incur much penalty).
If you really need to avoid repetition, this should not be accomplished through regular variables due to potential timing problems with Cypress' custom chainables. This is an anti-pattern they publish. The best way to do this would be:
Create a fixture file with static user data that you will use to conduct the test. (Remove the uuidv4.)
For the set of tests that need your user data, call register in a before hook using the fixture data. This will create the data in the system under test.
Use a beforeEach hook to load the fixture data and alias it for each of your tests. Now, the static data you need is accessible with no API calls and it is guaranteed to be in the system properly thanks to the before hook.
Run your tests using the alias.
Clean up the data in an after hook (since your user no longer has a random email, you need to add this step).
If you need to do the above for the whole test suite, put your before and after hooks in the support file to make them global.

QUnit exports global functions

I'm trying to get QUnit to work with my production environment, and noticed that QUnit exports a log function with this definition:
function ( callback ) {
config[key].push( callback );
}
Why does it do this? There seem to be a hole bunch of functions that are exported globally, like begin, deepEqual, done, etc etc. Isn't it better if all these functions are within a namespace instead?
Shouldn't this behaviour be noted in the documentation? I had another log function defined in my codebase (shorthand for console.log), and this rendered strange bugs from QUnits side because log("a log message") is not correct usage of QUnit's log function.
Is there a way to contain QUnit within its namespace? Giving the code an object instead of window does not work.

Prevent re-loading of javascript if functions already exist. Otherwise ensure synchronous loading

Using JQuery.load(), I change the content of my website's mainWindow to allow the user to switch between tabs. For each tab, there is one or multiple scipts that contain functions that are executed once the tab content is loaded.
Obviously when switching to the tab for the first time, the script has to be fetched from the server and interpreted, but this shouldn't happen if the user switches back to the tab later on. So, to put it short:
Load() html
make sure javascript functions exist, otherwise load script and interpret it.
call a a function on the javascript after the DOM is rebuilt.
Step one and two have to be complete before step 3 is performed.
At the moment, I am using nested callbacks to realize this:
function openFirstTab(){
$("#mainWindow").load("firstTab.php", function(){
if(typeof(onloadfFirstTab) != "function"){
jQuery.getScript("assets/js/FirstTab.js", function(){
onloadFirstTab();
});
}
else{
onloadFirstTab();
}
} );
}
but I feel that there should be a better way.
You can't write the code entirely synchronously since you can't load script synchronously after page load ( unless you do a synchronous XHR request and eval the results - not recommended ).
You've got a couple of choices. There are pre-existing dependency management libs like RequireJS which may fit the bill here, or if you just need to load a single file you can do something like this to clean up your code a bit rather than using if/else:
function loadDependencies() {
// For the sake of example, the script adds "superplugin" to the jQuery prototype
return $.getScript( "http://mysite.com/jquery.superplugin.js" );
}
function action() {
// If superplugin hasn't been loaded yet, then load it
$.when( jQuery.fn.superplugin || loadDependencies() ).done(function() {
// Your dependencies are loaded now
});
}
This makes use of jQuery Deferreds/Promises to make the code much nicer.
If you don't want to load the JS more than once and you are going to dynamically load it, then the only way to know whether it's already loaded is to test for some condition that indicates it has already been loaded. The choices I'm aware of are:
The simplest I know of is what you are already doing (check for the existence of a function that is defined in the javascript).
You could also use a property on each tab (using jQuery's .data() that you set to true after you load the script.
You could write the dynamically loaded code so that it knows how to avoid re-initializing itself if it has already been loaded. In that case, you just load it each time, but the successive times don't do anything. Hint, you can't have any statically defined globals and you have to test if it's already been loaded before it runs its own initialization code.
(Haven't tested it yet, so I am not sure if it works, especially since I didn't yet really understand scope in javascript:)
function require(scripts, callback){
var loadCount = 0;
function done(){
loadCount -=1;
if (loadCount==0){
callback();
}
}
for ( var script in scripts){
if (!script.exitsts()){
loadCount +=1;
jQuery.getScript(script.url, done);
}
}
}
This function takes an array of scripts that are required and makes sure all of them are interpreted before it calls the callback().
The "script" class:
function script(url, testFunc){
this.url =url;
this.testFunction = testFunc;
this.exists = function(){
if(typeof(testFunction)=="function"){
return true;
}
else{
return false;
}
}
}
Where the test-function is a function that is defined (only) in the concerned script.
PS:
To enable caching in JQuery and thus prevent the browser from doing a GET request every time getScript() is called, you can use one of the methods that are presented here.
Even though unnecessary GET - requests are avoided, the script is still getting interpreted every time getScript() is called. This might sometimes be the desired behavior. But in many cases, there is no need to re-interpret library functions. In these cases it makes sense to avoid calling getScript() if the required library functions are already available. (As it is done in this example with script.exists().

Resources