cypress not executing "then" portion - testing

I am trying to write a custom cypress command but the code in my then portion is not executing. any help will be appreciated thanks
my command looks similar to this:
Cypress.Commands.add("ex", () => {
const links=[]
cy.get("*[class='link post']").each((link)=>{
links.push(link.href)
}).then(() => {
var i=0;
while (links[i]) {
cy.visit(link)
i++
}
})
})

There are a few things going on here we should step through.
In your each() block, link.href will return an undefined value, so when you get to your then method, you have no links in your array to visit. Instead of links.push(link.href), try links.push(links.attr('href') to grab the value of your href attribute.
In your then method, your while loop isn't the most efficient way of looping through your array (and it will most likely error out for you with an undefined value). You should instead use a .forEach(), like so:
links.forEach((link)=>{
cy.visit(link)
)
If you do not need to persist the links array, then your entire command can be majorly simplified:
Cypress.Commands.add("ex", () => {
cy.get("*[class='link post']")
.then((links) => {
links.each((link)=>{
cy.visit(link.attr('href'))
})
})
});

To add to Kerry's answer,
The parameter given to a .then() callback is a jQuery object, containing one or more elements found by cy.get(...)
To iterate over the elements, you need to de-structure the jQuery object with the spread operator,
Cypress.Commands.add("visitLinks", () => {
cy.get("*[class='link post']")
.then($links => { // $links is a jQuery wrapper
[...$links].forEach(link => { // link is a raw element
const url = link.getAttribute('href') // apply DOM method
cy.visit(url)
})
})
});
or if you want to use the Cypress iterator .each() instead of .then(),
Cypress.Commands.add("visitLinks", () => {
cy.get("*[class='link post']")
.each($link => { // jQuery wrapped element
const href = $link.attr('href') // apply jQuery method
cy.visit(href)
})
});
However
It's going to break.
cy.visit() navigates the page, which changes the DOM in the page, so on the 2nd iteration of .each(), Cypress sees things have changed and crashes (probably a "detached element" error).
You should separate the query (grabbing the links) from the action (visiting them).
Cypress.Commands.add("getLinks", () => {
const found = [];
cy.get("*[class='link post']")
.each($link => { // jQuery wrapped element
const href = $link.attr('href') // apply jQuery method
found.push(href)
})
.then(() => found) // wait until iteration finishes
// then return the array of links
});
Use it like this
cy.getLinks()
.then(links => {
links.forEach(link => cy.visit(link))
})

Related

Cypress spy on multiple calls of the same method

I'm trying to check that a method was not called again after a certain action.
My test:
it('if query is less than 3 symbols, api call is not made', () => {
cy.spy(foo, 'bar').as('bar');
cy.get('input').type('12').then(() => {
cy.get('#bar').its('callCount').then(res => {
expect(res).to.eq(1); // a basic check after mounted hook
});
});
});
My component:
async mounted(): Promise<void> {
await this.foo.bar();
}
async getSearchResults(): Promise<void> {
if (this.searchQuery.length < 3) {
return;
}
await this.foo.bar();
}
The problem is that bar was already called on mount, and it could have been called multiple times before, if query length was valid. I was thinking about saving bar's callCount to a variable and checking it after call, but that looks ugly. Kinda stuck here, any ideas are welcome.
It's not an issue. The call count is started at the point you set up the spy, not when the component is mounted.
Try this:
const foo = {
bar: () => console.log('bar called')
}
it('starts with a clean callcount', () => {
foo.bar() // make a call
cy.spy(foo, 'bar').as('bar'); // callCount === 0 on setup
cy.get('#bar')
.its('callCount')
.should('eq', 0) // passes
});
Even if you have some callcount from another test, you can always reset it before the current test:
it('allows reset of spy callCount', () => {
cy.spy(foo, 'bar').as('bar'); // callCount === 0 on setup
foo.bar() // make a call, count is now 1
cy.get('#bar').invoke('resetHistory') // remove prior calls
cy.get('#bar')
.its('callCount')
.should('eq', 0) // passes
});
I believe you can get the initial call count, and then wrap your test in that.
it('if query is less than 3 symbols, api call is not made', () => {
cy.spy(foo, 'bar').as('bar');
cy.get('#bar').its('callCount').then((initRes) => {
cy.get('input').type('12').then(() => {
cy.get('#bar').its('callCount').then(res => {
expect(res).to.eq(initRes); // a basic check after mounted hook
});
});
});
});
You would probably want to do a test that this would fail, to make sure that Cypress is getting '#bar' again.

nextTick() not triggering DOM update

I'm creating a messaging app and I'm having some trouble with scrolling to the bottom of an ion-content element when a new message is added to an array. I'm using the scrollToBottom() method that comes with ion-content, and I'm using the Composition API in Vue 3.
Consider this snippet:
setup(props) {
const replyContent = ref("")
const messages = ref([])
// References to ion-content in the template
const ionContent = ref(null)
const reply = async () => {
const message = await replyToThread(props.threadId, replyContent.value).then((message) => message)
messages.value.push(message)
nextTick(() => {
console.log("DOM updated!")
if (ionContent.value) {
ionContent.value.$el.scrollToBottom()
}
})
}
return { replyContent, messages, ionContent, reply }
}
replyToThread() performs an API call and returns the new message, and nextTick() should ensure me that the DOM has been updated so that I can have my way with it. The console does successfully log "DOM updated!", but no scrolling to the bottom happens.
But, and somehow this works every time nextTick() doesn't, when I replace the nextTick() code block with the following, it works flawlessly:
setTimeout(() => {
if (ionContent.value) {
ionContent.value.$el.scrollToBottom()
}
}, 200)
I have to set the timeout at around 200 ms, otherwise it doesn't work. But relying on this when something fancy like nextTick() should do the trick feels quite dirty. Does anyone know why this is happening?
That is because nextTick() only guarantees that the actual DOM has been updated: it doesn't mean that the browser has actually finished the layout of the page. That is the reason why you need an arbitrary timeout to ensure the scrolling works, because after 200ms the browser is likely to be done laying things out based on the updated DOM.
To fix this you will probably need to rely on window.requestAnimationFrame:
nextTick(() => {
window.requestAnimationFrame(() => {
if (ionContent.value) {
ionContent.value.$el.scrollToBottom()
}
});
});
If this feels like too much nesting for you, you can create a method that returns a promise based on rAF:
methods: {
rAF: function() {
return new Promise(r => window.requestAnimationFrame(r));
}
}
Then it's a matter of ensuring promises returned by both nextTick() and rAF() are resolved before scrolling:
await nextTick();
await this.rAF();
if (ionContent.value) {
ionContent.value.$el.scrollToBottom();
}

React Navigation array props

react-navigation navigate in array unexpected tokens ?
const { navigate } = this.props.navigation;
navigate('Properties', {
list.map((item) => {
["module"+item.id]:this.state["module"+item.id]
})
});
Returns error:
Unexpected token, expected ";"
First, when you provide a single expression arrow function that returns an object, you have to wrap that object with parentheses, otherwise the interpreter thinks the braces are a block, and not the object root.
list.map((item) => ({
["module"+item.id]: this.state["module"+item.id]
}))
Second, it looks like you're trying to create the params object from a list of values.
But the result of the map you wrote, is a list of objects, not an object.
One of the ways to create that object is by using the reduce function:
list.reduce((accumulator, current) => (Object.assign(accumulator, {["module"+current.id]: this.state["module"+current.id]})), {});
But perhaps a more performant and simpler way would be to just do it with a local side effect:
function prepareParams(list) {
let result = {};
list.forEach(item => result["module"+item.id] = this.state["module"+item.id]);
return result;
}
And then in your navigate:
navigate('Properties', prepareParams(list));
Navigation params have to be in object format so you have to add a key
navigate('Properties', { someKey: list.map((item) => { ... }) });

how to map nested array in componentWillMount using react native

I am creating react-native app. I have an array and want to use value outside the looping. I have declared value in this.state={current_cat_id:'',} I have tried it in componentWillMount like:
componentWillMount() {
var ids = [];
this.props.data.map((dataImage,Index) => {
dataImage['main-head'].map((subchild,Index2) => {
ids.push(subchild['path'])
})
})
this.setState({current_cat_id: ids})
}
its returning blank page. is this right approch
it should work for you. try this:-
componentWillMount() {
var ids = [];
this.props.data.map((dataImage) => {
dataImage['main-head'] != undefined && dataImage['main-head'].map((subchild) => {
ids.push(subchild['path'])
})
})
this.setState({current_cat_id: ids})
}
componentWillMount is called before the render method is executed. It is important to note that setting the state in this phase will not trigger a re-rendering. Avoid introducing any side-effects or subscriptions in this method. Use componentDidMount() instead.

Nightwatch JS page object returns undefined

I'm using Nightwatch with mocha.
I try to get an element's text from the page object. When trying to compare the received text to another text I receive an error "AssertionError: expected undefined to equal 'Text'".
This is the Page Object function:
const Commands = {
getInstanceLabel() {
this.getText('.DropdownSelect__label', (result) => {
return result.value;
});
}
}
And this is the Test code:
it('Should sort the collection in ascending order by default', (client) => {
const labelText = client.page.instanceCollectionPage().getInstanceLabel();
expect(labelText).to.equal('Text');
});
Why is this showing undefined?
The thing is that you are using arrow functions, and as mentioned in mdn:
An arrow function expression has a shorter syntax compared to function
expressions and does not bind its own this, arguments, super, or
new.target.
You can fix it in two different ways:
using function:
e.g. (you can use this)
it('Should launch', function (browser) {
const url = browser.launchUrl;
browser.url(url).waitForElementVisible('body', 1000);
browser.getText('#txtWelcome', function (result) {
this.verify.equal(result.value, 'Welcome');
});
});
using browser:
e.g. (you need to access the browser object direcly)
it('Should launch', (browser) => {
const url = browser.launchUrl;
browser.url(url).waitForElementVisible('body', 1000);
browser.getText('#txtWelcome', (result) => {
browser.verify.equal(result.value, 'Welcome');
});
});
Those are just examples on how to use this, I can not provide more details on your issue because you don't show what InstanceCollection does.