The cypress docs(https://docs.cypress.io/guides/core-concepts/variables-and-aliases.html#Elements) are pretty unclear on how alias and variables can be used to store information during a test.
I'm trying to store the text of a div on one page to use later, for example:
// let vpcName;
it('Store current name to use later', () => {
// save name for later use - doesn't work
// cy.get('#value').then(elem => {
// vpcName = Cypress.$(elem).text;
// });
// using alias - also doesn't work
cy.get('#value')
.invoke('text')
.as('vpcName');
});
it('Use previous value to return to correct page', () => {
cy.contains(this.vpcName).click();
});
I just came across this article that explains how and why you store a variable and then use it later in the "Cypress way":
https://www.stevenhicks.me/blog/2020/02/working-with-variables-in-cypress-tests/
In respect to how it should work, here is my example which first, collects the message (the message is shown for only 3 secs then disappears). Secondly, it gets the value using the # sign. Lastly my code passes the stored message through to an empty function constructed to assert that the value Portsmouth is contained within the message.
it('Current Port change', () => {
cy.get('#viewport').find('div[id=message]').then(message => {
let wags = message;
cy.wrap(wags).as('wags')
});
cy.get('#wags').then(wags => {
expect(wags).to.contain("Portsmouth")
});
});
let me know if you need further clarification
Try this:
cy.get('button').then(($btn) => {
const txt = $btn.text()
// $btn is the object that the previous command yielded
})
Source: https://docs.cypress.io/guides/core-concepts/variables-and-aliases.html#Return-Values
I had to resort to
writeFile
readFile
When I was trying to save cookies from one test to another in Cypress
An alternative solution which doesn't actually store the element to a variable but serves some of the same purpose is to wrap the cy.get call in a function. Such as:
const btn = () => cy.get('[data-testid="reset-password-button"]')
btn().should('have.attr', 'disabled')
cy.get('[data-testid="new-password-input"]').type('someNewPW')
btn().should('not.have.attr', 'disabled')
This could be a good option if readability is your main goal.
I've been struggling with this for few days. Here is my approach if you want to use the saved text (for example) multiple times. However it seems too long to me and I think it could be optimized. Tested on Cypress v11.2.0
cy.xpath("//tbody/tr[2]/td[2]").then(($text) => {
let txt = $text.text()
//expect(txt).to.eq('SomeText')
cy.wrap(txt).as('txt')
})
cy.get('#txt').then(txt => {
//expect(txt).to.contain("SomeText")
cy.xpath("xpath of search field").type(txt)
})
Related
I'm trying to interact with some elements inside an iframe with cypress.
If I use the approach in https://bparkerproductions.com/how-to-interact-with-iframes-using-cypress-io/ for only one element per test, everything is fine.
# commands.js
Cypress.Commands.add(
'iframe',
{ prevSubject: 'element' },
($iframe) => {
return new Cypress.Promise(resolve => {
$iframe.on('load', () => {
resolve($iframe.contents().find('body'))
})
})
})
# landing_page.spec.js
cy.get('iframe').iframe().find('#happybutton').should('be.visible')
However, I want to look for multiple elements, click on them, and check if they are rendered correctly, but if I assign the iframe contents to a variable and reuse it to locate another element (for example, a button), cypress tries to locate the second element (for example, a menu) from the first element (the button, which is doomed to fail, because the button does not contain the menu).
# landing_page.spec.js
let iframeContent = cy.get('iframe').iframe()
iframeContent.find('#happybutton').should('be.visible')
iframeContent.find('#myMenu').should('be.visible')
I tried using different variables, or calling directly cy.get('iframe').iframe(), every time I wanted to interact with different elements, but cypress gets trapped in an infinite loop and the test never ends (but no errors or warnings are produced).
Does anybody knows a way to avoid this infinite loop? As I want to reproduce a sequence of steps to build a test case, it is not possible to isolate each interaction in a different test.
Or does anybody knows of a framework that is more suitable for working with iframes?
The problem is $iframe.on('load', only fires once, so you can't call cy.get('iframe').iframe() twice which is effectively what both .find() commands are doing.
let iframeContent = cy.get('iframe').iframe() doesn't store the iframe body, it stores a "chainer" which is treated like a function or getter.
The "infinite loop" is Cypress waiting for the promise resolve() call the second time, which never happens.
So you can nest the commands like this
cy.get('iframe').iframe().then(body => {
cy.wrap(body).find('#happybutton').should('be.visible')
cy.wrap(body).find('#myMenu').should('be.visible')
});
or you can enhance the command by adding a tag when the load event fires
Cypress.Commands.add('iframe', { prevSubject: 'element' }, ($iframe) => {
return $iframe._hasloaded
? $iframe.contents().find('body')
: new Cypress.Promise(resolve => {
$iframe.on('load', () => {
$iframe._hasloaded = true;
resolve($iframe.contents().find('body'))
})
})
})
Thanks to Marion's answer I found a way to refactor my code, so now it works!
Note: the iframe() function was left untouched
# commands.js
Cypress.Commands.add(
'iframe',
{ prevSubject: 'element' },
($iframe) => {
return new Cypress.Promise(resolve => {
$iframe.on('load', () => {
resolve($iframe.contents().find('body'))
})
})
})
# landing_page.spec.js
cy.get('iframe').iframe().as('iframeContent')
cy.get('#iframeContent').then((iframeContent) => {
cy.get(iframeContent).find('#happybutton').click()
cy.get(iframeContent).find('#myMenu')
cy.get(iframeContent).find('#anotherElement').should('be.visible')
})
The above answers pointed me to the right direction. By omittimg the 'then' phrase and the first cy.get('#iframeContent'), Semiramis' solution can be simplified a bit and made easier to understand like this:
cy.get('iframe').iframe().as('iframeContent')
cy.get('#iframeContent').find('#happybutton').click()
cy.get('#iframeContent').find('#myMenu')
cy.get('#iframeContent').find('#anotherElement').should('be.visible')
For Cypress newbees (like me): Cypress Variables and Aliases
I came across the following code:
let timeoutHandler;
clearTimeout(timeoutHandler);
timeoutHandler = setTimeout(() => {...});
This is an overly simplification since the original code is contained in a Vue application as follow:
public handleMultiSelectInput(value): void {
if (value === "") {
return;
}
clearTimeout(this.inputTimeoutHandler);
this.inputTimeoutHandler = setTimeout(() => {
axios.get(`${this.endpoint}?filter[${this.filterName}]=${value}`)
.then(response => {
console.log(response);
})
}, 400);
}
Does this mean this is some kind of cheap-ass debounce function?
Could someone explain what this exactly means.
Yes, it is a debounce function, which is when we wait for some amount of time to pass after the last event before we actually run some function.
There are actually many different scenarios where we might want to debounce some event inside of a web application.
The one you posted above seems to handle a text input. So it's debouncing the input, meaning that instead of fetching that endpoint as soon as the user starts to enter some character into the input, it's going to wait until the user stops entering anything in that input. It appears it's going to wait 400 milliseconds and then execute the network request.
The code you posted is kind of hard to read and understand, but yes, that is the idea of it.
I would have extracted out the network request like so:
const fetchData = async searchTerm => {
const response = await axios.get(`${this.endpoint}?filter[${this.filterName}]=${value}`);
console.log(response.data);
}
let timeoutHandler;
const onInput = event => {
if (timeoutHandler) {
clearTimeout(timeoutHandler);
}
timeoutHandler = setTimeout(() => {
fetchData(event.target.value);
}, 400);
}
Granted I am just using vanilla JS and the above is inside a Vuejs application and I am not familiar with the API the user is reaching out to. Also, even what I offer above could be made a lot clearer by hiding some of its logic.
Given a list of functions, I wish the user to be able to select any of the functions to run at startup. How can this be done so that the user can "save" their choice of function to run the next time the code is run ie what would function runSelectedFunction (below) look like since you can't "save" a javascript function to file? Also, assume the list of potential functions is extensible.
const first = ()=>{
console.log('first');
}
const second = ()=>{
console.log('second');
}
const third = ()=>{
console.log('third');
}
loadUserSelectedFunctionFromDB()
.then(runSelectedFunction)
To be clear, the goal is to persist the user choice even if the code stops executing and is restarted. Normally, this would be done by storing a value in a database but the question is how to store a reference to a function in a database given an extensible set of functions?
Use a map like this:
const m = {
first, second, third
};
let selectFuncName = "first"; // from user selection, maybe click a button
let selectFunc = m[selectFuncName];
loadUserSelectedFunctionFromDB()
.then(runSelectedFunction)
I'm trying to get .then() to work with .on() but it only works with .once().
I get playersRef.on(...).then is not a function, when trying it with on()
So what I have now is this:
let nodesRef = Firebase.database().ref('players')
let answers = {}
playersRef.on('value', (playersSnapshot) => {
playersRef.once('value').then((playersSnapshot) => {
playersSnapshot.forEach((playerSnapshot) => {
let playerKey = playerSnapshot.key
let answersRef = playerSnapshot.child('answers')
if (answersRef .exists()){
answers[playerKey ] = answersRef .val()
}
})
}).then(() => {
console.info(answers);
dispatch(setPlayerAnswers(answers))
})
})
But I don't like the fact that it is querying the ref twice.
How would I go about getting each player's answer in this example? What would be a more correct way? Because I don't think this is the way the Firebase devs intended this.
And what is the reasoning that it is not implemented for .on()? Because playersSnapshot.forEach(...).then is also not a function... How do I execute something only once whenever the .on('value') triggers?
firebaser here
I'm not entire certain of parts of your question, so will only answer the parts that I can.
what is the reasoning that [then()] is not implemented for .on()?
We implemented support for Promises in the Firebase JavaScript clients. The definition of a promise is that it resolves (or fails) only once. Since the Firebase on() methods can receive updated data multiple times, it does not fit in the definition of a promise.
How do I execute something only once whenever the .on('value') triggers?
When you want to respond only once to an event, you should use the once() method.
I'm not sure why you're having problems with forEach(). The following should work:
playersRef.on('value', (playersSnapshot) => {
playersSnapshot.forEach((playerSnapshot) => {
let playerKey = playerSnapshot.key
let answersRef = playerSnapshot.child('answers')
if (answersRef .exists()){
answers[playerKey ] = answersRef .val()
}
})
})
If you're having trouble making this work, can you reproduce that problem in a jsbin?
How do I focus an input with Cycle? Do I need to reach inside the DOM and call .focus() either with or without jQuery, or is there some other way with Cycle/RxJS?
Yes, you do need to reach inside the DOM and call .focus() either with or without jQuery. However this is a side-effect and it is Cycle.js convention to move these kinds of side effects to a so-called driver.
The two questions the driver needs to know are:
which element do you want to focus?
when do you want to focus the element?
The answer to both questions can be provided by a single stream of DOM elements.
Create the driver
First make your driver. Let's call it SetFocus. We'll make it a so-called read-only driver. It will read from the app's sinks but it will not provide a source to the app. Because it is reading, the driver's function will need to accept a formal parameter that will be a stream, call it elem$:
function makeSetFocusDriver() {
function SetFocusDriver(elem$) {
elem$.subscribe(elem => {
elem.focus();
});
}
return SetFocusDriver;
}
This driver takes whatever DOM element arrives in the stream and calls .focus() on it.
Use the Driver
Add it to the list of drivers provided to the Cycle.run function:
Cycle.run(main, {
DOM: makeDOMDriver('#app'),
SetFocus: makeSetFocusDriver() // add a driver
});
Then in your main function:
function main({DOM}) {
// setup some code to produce the elem$ stream
// that will be read by the driver ...
// [1]: say _when_ we want to focus, perhaps we need to focus when
// the user clicked somewhere, or maybe when some model value
// has changed
// [2]: say _what_ we want to focus
// provide the textbox dom element as actual value to the stream
// the result is:
// |----o-----o-----o--->
// where each o indicates we want to focus the textfield
// with the class 'field'
const textbox$ = DOM.select('.field').observable.flatMap(x => x); // [2]
const focusNeeded = [
clickingSomewhere$, // [1]
someKindofStateChange$ // [1]
];
const focus$ = Observable.merge(...focusNeeded)
.withLatestFrom(textbox$, (_, textbox) => textbox); // [2]
// ...
// [*]: Add driver to sinks, the driver reads from sinks.
// Cycle.js will call your driver function with the parameter
// `elem$` being supplied with the argument of `focus$`
return {
DOM: vtree$,
SetFocus: focus$, // [*]
};
}
You can then configure focusNeeded to say when you want .field to be focused.
You can tailor for your own situation, but this should illustrate how to solve your problem. Let's assume you have a text input and a button. When the button is clicked, you want the focus to remain on the text input.
First write the intent() function:
function intent(DOMSource) {
const textStream$ = DOMSource.select('#input-msg').events('keyup').map(e => e.target);
const buttonClick$ = DOMSource.select('#send-btn').events('click').map(e => e.target);
return buttonClick$.withLatestFrom(textStream$, (buttonClick, textStream) => {
return textStream;
});
}
Then the main which has a sink to handle the lost focus side effect
function main(sources) {
const textStream$ = intent(sources.DOM);
const sink = {
DOM: view(sources.DOM),
EffectLostFocus: textStream$,
}
return sink;
}
Then the driver to handle this side effect would look something like
Cycle.run(main, {
DOM: makeDOMDriver('#app'),
EffectLostFocus: function(textStream$) {
textStream$.subscribe((textStream) => {
console.log(textStream.value);
textStream.focus();
textStream.value = '';
})
}
});
The entire example is in this codepen.
Here's one example, written by Mr. Staltz himself: https://github.com/cyclejs/cycle-examples/blob/master/autocomplete-search/src/main.js#L298