Cypress: Lodash: Cypress._.sample flaky - javascript

I have written this method but it is very flaky.
Cypress.Commands.add("selectRandomDropdownOption", function () {
cy.get(".ng-option")
.as("dropdownOptions")
.should("be.visible")
.then((options) => {
Cypress._.sample(options).click({ force: true });
cy.get("#dropdownOptions").should("not.exist");
});
});
every second test fails because cypress says Cypress._.sample(...).click is not a function which makes no sense because sometimes the method works. Can you tell me why this is? Is it because of my code or lodash itself?

One explanation might be that options is sometimes empty when the .click() is attempted, due to loading latency.
Try adding a length check
cy.get(".ng-option")
.should("be.visible")
.should('have.length','gt', 0)
.then((options) => {
Cypress._.sample(options).click();
})
cy.get(".ng-option").should("not.exist")
Converting to an array
The problem is options is an object, not an array.
If you console.log(options) you'll see it has a couple of extra properties prevObject and selector which are sometimes selected by the randon function.
jQuery.fn.init(2) [option, option, prevObject: jQuery.fn.init(1), selector: '.ng-option']
But these properties do not support the .click() function, and are not what you want to select anyway.
To fix, first convert the options object to an array
cy.get(".ng-option")
.should("be.visible")
.should('have.length','gt', 0)
.then((options) => {
const optionsArray = [...options]
Cypress._.sample(optionsArray).click();
})
cy.get(".ng-option").should("not.exist")

Related

cy.wrap().its()... doesn't work when the value in .its() contains a period

I am looking to extract a URL parameter from the current URL I'm testing with Cypress. I was able to basically get the answer from this SO post, however, my extracted values are not available to me when I use Cypress's .its() command.
The parameters in the url all have periods in them, and I believe this is the cause for my error.
Here is my custom Cypress Command I'm building:
Cypress.Commands.add('getParmsCommand', function(value) {
cy.url().as('url')
cy.then( () => {
cy.log(this.url)
const kvPairArray = this.url.toString().split('?')[1].toString().split('&')
const paramObj = {}
kvPairArray.forEach(param => {
cy.log(param)
//default 'value' to 0 if it doesn't exist
const [ key, value="0" ] = param.split('=')
paramObj[key] = value
})
//forcefully adding a debug element to the key value store for testing
paramObj['beverage'] = 'soda'
cy.wrap(paramObj)
.its('timeline.ws') //doesn't work
// .its(`${Cypress.$.escapeSelector('timeline.ws')}`) doesn't work
// .its('timeline\.ws') doesn't work
// .its('"timeline.ws"') doesn't work
// .its('beverage') this DOES work!
.then(parmVal => {
cy.log(parmVal)
})
Here is the relevant part of the URL that I'm trying to extract from:
timeline.ws=3600000&timeline.to&timeline.fm&timeline.ar=false
You can see from the error that Cypress is only looking for the id timeline, NOT timeline.ws; it completely ignores everything after the period, and thus, never finds my parameter.
I saw there was a similar error with Cypress's .get() function back in 2018.
I am new to both javascript and Cypress, so I hope it's just a weird easy thing I'm overlooking. Any advice or educated guesses are greatly welcome at this point!
Thank you.
.its() is just a shorthand for property extraction. Since it fails with the period, you could instead use bracket notation in a .then().
cy.wrap(paramObj)
.then(paramObj => paramObj['timeline.ws'])
or just
cy.wrap(paramObj['timeline.ws'])
Playing around with the URL constructor
const urlString = 'http://example.com?timeline.ws=3600000&timeline.to&timeline.fm&timeline.ar=false'
const url = new URL(urlString)
cy.wrap(url.searchParams.get('timeline.ws'))
.should('eq', '3600000')
cy.wrap(url.searchParams.get('timeline.to'))
.should('be.empty')
cy.wrap(url.searchParams.get('timeline.ar'))
.should('eq', 'false')

Deep equal comparing not working on Cypress 9.7.0

I've updated my cypress to 9.7.0 version and right now I have a problem with deep equal. When I wrote test line of code:
expect([1,2,3]).to.deep.equal([1,2,3]);
Everything works correctly.
While I'm testing redux store I got an error which is looks
Timed out retrying after 4000ms: expected [ Array(2) ] to deeply equal [ Array(2) ]
Arrays in devtool console preview are the same... I've tried in two ways to write test. I also combined it with async and timeouts
First try:
it('Example redux check', () => {
cy.fixture('file.json').then((fixtures) => {
cy.window()
.its('store')
.invoke('getState')
.its('queue.queueItems').should('deep.equal', fixtures.store.queue.queueItems);
});
});
Second try
it('Example redux check', () => {
cy.fixture('file.json').then((fixtures) => {
const getQueueItems = (win) => {
return win.store.getState().queue.queueItems;
}
cy.window()
.pipe(getQueueItems)
.should('deep.equal', fixtures.store.queue.queueItems);
});
});
Had anyone similar issue or idea how to avoid that timeout? Exactly the same is happening while comparing async payloads...
I couldn't fault the deep.equal assertion in Cypress v9.7.0, even deeply nested arrays and objects - except when the order differed.
If your problem is difference in array order, try adding package deepEqualInAnyOrder
const deepEqualInAnyOrder = require('deep-equal-in-any-order');
chai.use(deepEqualInAnyOrder);
it('matches when ordering is different', () => {
const expected = [{a:{x:1}}, {b:2},{c:3}];
expect([{a:{x:1}}, {b:2}, {c:3}]).to.deep.equal(expected) // passes
expect([{b:2}, {a:{x:1}}, {c:3}]).to.deep.equal(expected) // fails
expect([{b:2}, {a:{x:1}}, {c:3}]).to.deep.equalInAnyOrder(expected) // passes
});
I also wanted to see if deep.equal on the <h1> element of http://example.com would succeed.
Here is my minimal, reproducible example.
// Cypress 9.7.0
it('passes deep-equal of two DOM objects', () => {
cy.visit('http://example.com')
cy.get('h1').then($el1 => { // get h1 1st time
cy.get('h1').then($el2 => { // get another copy
// Are they different objects?
expect($el1).to.not.equal($el2) // pass
expect($el1 === $el2).to.equal(false) // pass
// Do they deep-equal
expect($el1).to.deep.equal($el2) // pass
})
})
})
Nearly all frontends I use have Cypress including some open source ones.
It is related to a few issues opened on GitHub and affects latest versions of Cypress.
https://github.com/cypress-io/cypress/issues/21353
https://github.com/cypress-io/cypress/issues/21469
This is what I get on a WIP after upgrading some tests that use deep.equal:
Your first example looks more standard.
I recommend downgrading to a lower version. The following version worked for me before upgrade:
9.5.4

Reference changed before change of reference

What I am trying to do is to switch out an object's property (a string) with a matching (keyed) object from another object where the values are keyed.
So for example...
const dataRefs = {
'idkey1': { title: "Key1", /*...etc... */ },
'idkey2': { title: "Key2", /*...etc... */ },
'idkey3': { title: "Key3", /*...etc... */ },
// ...etc...
};
const pages = [
{ title: "A Page", data: 'idkey1' },
// ...etc...
];
Using the below code I want to switch out pages[n].data with the matching property in dataRefs. So using a forEach on the pages...
pages.forEach(page => page.data = dataRefs[page.data])
Doing this results in page.data property becoming undefined, even though it should match.
If I try to debug by outputting it to console, I get some unusual effect of seeing the undefined only when the code is added after the output....
// This works and does the match exactly as I want it.
pages.forEach(page => console.log("%s: ", page.data, dataRefs[page.data]));
// Output:
// idkey1: undefined
// This however results in bizzare behaviour and ends up with "undefined".
pages.forEach(page => {
// using console.log to see what's going on...
console.log("%s: ", page.data, dataRefs[page.data]);
page.data = dataRefs[page.data];
});
// Output:
// [Object object]: undefined
// Trying this alternative, just in case how the DOM inspector
// might be using references, but still the same issue...
pages.forEach(page => {
console.log(page.data + ": ", dataRefs[page.data]);
page.data = dataRefs[page.data];
});
// Output:
// [Object object]: undefined
Have checked spelling of variables and gone over and over the code trying so many variants but it seems that no matter what I do, calling page.data = dataRefs[page.data] does not work. Would this be some sort of complex race-condition or have I been watching too much Matrix of late?
This is being called in the Component's render() method.
Using Safari 14.1.2, if that helps.
The issue was related with Next.JS. Best guess is that Next.JS was pre-rendering the data, storing it in some JSON cache file and passing that to the component render function....or something like that.
Using the browser's inspector, a breakpoint at the problematic line page.data = dataRefs[page.data] was only ever triggered once, and showed the data had already been manipulated by the function, before it had been called. Which is simply odd. Removing the line, the breakpoint would trigger and the data not converted.
This leads me to believe it to be some sort of NextJS pre-lifecycle thing, possibly as part of the SSG process.
To resolve the issue and move on, I used a check if (page.data instanceof Object) return; to stop it from running twice, which seemed to do the trick. Not ideal, but without a better reason, this will have to suffice. So the code ultimately went like....
pages.forEach(page => {
// skip the loop if the data has already been converted
// could have also used a string check, but both should work for now.
if (page.data instanceof Object) return;
// now change the data if it's still a string referrence
page.data = dataRefs[page.data]));
});
Again, appologies that I don't have the best answer, but this was the only way to resolve it, and since Javascript does not do this normally (have done this sort of thing so many times without issue), it will have to be chalked up to a NextJS/SSG (or some other processor) issue.
Would love to get any NextJS expert's knowledge on how this could happen.

Unable to grab element by className even if I see it in the browser console

So I am having a very frustrating issue.
I am trying to grab a HTML element with React/JS which exists only in a library, not in my code so I can't use useRef or anything like that.
If I do this:
useEffect(() => {
const handleContainer = document.getElementsByClassName('handle-container');
console.log({ handleContainer: handleContainer[0] });
}, [document]);
It prints: { handleContainer: undefined }
But with this:
useEffect(() => {
const handleContainer = document.getElementsByClassName('handle-container');
console.log({ handleContainer });
}, [document]);
It prints this:
I am trying to disable the right click event on that element.
Like this:
document.getElementsByClassName('handle-container')?.[0]?.setAttribute('oncontextmenu', 'return false;');
But I am failing.
Any ideas?
Any ideas?
This most likely because the console is displaying the current state of the HTMLCollection being referenced using console.log({ handleContainer }); over the snapshot of the first element (which may not exist yet) which is happening when you perform console.log({ handleContainer: handleContainer[0] });...
Could you try adding a timeout like below and seeing what happens? This will hopefully give enough time for the library and DOM to load up and make it queryable at effect function run...
useEffect(() => {
setTimeout(() => {
const handleContainer = document.getElementsByClassName('handle-container');
console.log({ handleContainer: handleContainer[0] });
}, 1000);
}, [document]);
If this shows the expected element, then it would indicate that when the effect is called without a timer the class element that needs to be referenced using { handleContainer: handleContainer[0] } does not yet exist but because the console is displaying a reference of the HTMLCollection object using { handleContainer } it would would still display it in the console as it is not dereferencing it from the children array.
Refs: MDN - HTMLCollection

Cypress - how to find by text content?

In Cypress, I want to select a button from a group of buttons based on its text-content. How can I do it? Here is my approach:
export const getCustomerButton = () => getNavigationSidenav()
.find('mat-expansion-panel-header')
.each(($el, index, $list) => {
const text = $el.find('.mat-content > mat-panel-title').text();
if (text === 'Customer') {
return $el;
}
return null;
});
The problem I have now is that I have to filter out the nulls from the element array. Is there a less complicated way?
This code will yield the DOM element with YOUR_BUTTON_CLASS which contains text 'Customer'. Is that what you're looking for?
cy.get('.YOUR_BUTTON_CLASS').contains('Customer');
Here the documentation for .contains cypress command.
Or maybe an even slicker solution is to use this:
cy.contains('YOUR_BUTTON_CLASS', 'Customer');
This can be done since contains() can hold 2 arguments. And if it gets two arguments the first one is always the element and the second the text.
Another option that's not mentioned in the previous answers here.
Use testing-library/cypress-testing-library
After the installation, just import it in cypress' commands.js:
import '#testing-library/cypress/add-commands'
And in your tests
cy.findAllByText("Jackie Chan").click();
cy.findByText("Button Text").should("exist");
cy.findByText("Non-existing Button Text").should("not.exist");
cy.findByLabelText("Label text", { timeout: 7000 }).should("exist");
cy.get("form").within(() => {
cy.findByText("Button Text").should("exist");
});
cy.get("form").then((subject) => {
cy.findByText("Button Text", { container: subject }).should("exist");
});
This is pretty straightforward and easy to use. We use this in our production site along with react testing library. Highly recommend :)
The accepted answer "can" work. However: if the element is not visible on the first try, the element cannot be found in subsequent retries.
See: https://github.com/cypress-io/cypress/issues/3745
Cypress uses "Sizzle" as selector library - so this:
cy.get('button:contains("FooBar")')
would work in retries.
There are multiple ways to do that
Syntaxes:
cy.contains(content)
cy.contains(content, options)
cy.contains(selector, content)
cy.contains(selector, content, options)
Examples:
cy.contains('button', 'Customer')
cy.contains('.buttonClass', 'Customer')
cy.get('button:contains("Customer")')
cy.contains('Customer')
The simplest way to do it would be :
cy.get('Button_Class').contains('Button_Text')
In your case, the solution would be :
cy.get('.mat-content > mat-panel-title').contains('Customer')
There is a documentation for this here.

Categories