Deep equal comparing not working on Cypress 9.7.0 - javascript

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

Related

Cypress: Lodash: Cypress._.sample flaky

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")

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.

Why does my nodelist contain 0 items when console logged from my JS file?

The JS code that I'm concerned with is this:
const modals=document.querySelectorAll('.modal');
const modalsArray=[...modals];
console.log(modalsArray);
document.addEventListener('click', function (e) {
e.stopPropagation();
e.preventDefault();
if (e.target.classList.contains("item")) {
itemIndex=e.target.classList.item(1);
const modals=document.querySelectorAll('.modal');
const modalsArray=[...modals];
console.log(itemIndex);
console.log(modalsArray);
modalsArray.filter(function (modal) {
if (modal.classList.item(1)===itemIndex) {
modal.classList.add('on');
modalContainer.classList.remove('off');
modalContainer.classList.add('on');
}
})
}
});
const arrow=document.querySelectorAll('.arrow');
const arrowArray=[...arrow];
console.log(arrowArray);
Whenever I console log the modalsArray or the arrowArray from my JS file, the array shows up empty. But I found that if I write out the code from scratch in the console to make the arrays, then all the items in the array show up properly.
So why is this happening? Why is an empty array currently showing up on my console? Does it have to do with scope or something?
When I put the modalsArray inside the eventlistener, and I click the respective target, an array will show properly and the rest of the code in that eventlistener works properly.
I'm noticing the same thing with the arrowArray and realized these two elements are having the same problem. I figured I should ask then because something is definitely up.
So why is an empty array showing up for both of these elements as seen in the image?
Check out my fiddle for more information:
https://jsfiddle.net/apasric4/kvs25wbL/3/
Your console statements are executing even before the data is fetched. I chained them in another "then" function (promise chaining) as follows. This way your console statements run after your results are parsed to hmtl.
https://jsfiddle.net/v83zqofp/1/
function generateHTML (data) {
fetchData(data)
.then(function(data){
let results=data.results;
return results.map(function(result,index){
-------------
main.lastElementChild.insertAdjacentHTML('afterend', html);
modalContainer.insertAdjacentHTML('beforeend', overlayHtml);
})
}).then (function (data) {
const modals=document.querySelectorAll('.modal');
const modalsArray=[...modals];
console.log(modalsArray);
const arrow=document.querySelectorAll('.arrow');
const arrowArray=[...arrow];
console.log(arrowArray);
})
}

Is there a way to assert that a route has not been called in Cypress?

I am trying to assert that a route has not been called in Cypress. I thoroughly looked through the documentation and have found nothing.
I am trying to do something like this:
cy.get('#myRouteAlias').should('have.not.been.called');
I am currently working around this by asserting that the successful request toast message is not being displayed but it is a flimsy solution.
Any ideas?
It is very difficult to test a situation where an action has not occured. With this type of assertion, you can really only say:
"The XHR request was not made within the 400ms that Cypress looked for this XHR request to have been made (or whatever you set your timeout to be)"
This doesn't really confirm that the XHR request was never called.
That being said, Cypress offers a way to retrieve all XHR requests made using the undocumented cy.state('requests'). You could check the length of that, filter them by alias, etc to probably determine what you want.
Unfortunately none of the above really worked for me, I got it working with this command :
Cypress.Commands.add('shouldBeCalled', (alias, timesCalled) => {
expect(
cy.state('requests').filter(call => call.alias === alias),
`${alias} should have been called ${timesCalled} times`
).to.have.length(timesCalled);
});
Which I then use like this :
// Checks that FetchChatList has not been called
cy.shouldBeCalled('FetchChatList', 0);
As a variant set in routes options onResponse function which drops test
e.g. expect(true).to.be.false;
it will fire error if call happened for current route
cy.route({
url: <url>,
onResponse: function () {
expect("Unexpected Https call").to.be.false;
}
})
Here is the correct way to assert requests count using cypress's commands.
Put this in your commands.js file:
Cypress.Commands.add('requestsCount', (alias) =>
cy
.wrap()
.then(() => cy.state('requests').filter(req => req.alias === alias).length),
);
Than in your tests use a new command as follows:
it('should count requests', () => {
cy.server();
cy.route('**').alias('theRequest');
cy.wait('#theRequest');
cy.requestsCount('theRequest').should('eq', 1);
});
None of this worked for me in version 7.6, but I have found a very simple solution.
Given you have an interception like this:
cy.intercept('GET', '**/foo/bar**').as('myRequest');
Now you can just do this:
cy.wait(2000);
cy.get('#myRequest.all').then((interceptions) => {
expect(interceptions).to.have.length(0);
});
So you wait a certain time, when the request COULD have happened, and make sure after the wait that it didn't. Works perfectly fine for me, and no additional commands are needed.
I found that solution here: https://www.gitmemory.com/issue/cypress-io/cypress/15036/780706160
It is worth considering the asynchronous nature of this test, something the previous examples have not taken into account. Here is a working example:
cy.route('/my-route').as('myRoute')
const noExpectedCalls = 1
cy.get('#myRoute').then(() => {
expect(cy.state('requests').filter(r => r.alias === 'myRoute')).to.have.length(noExpectedCalls)
})
To simplify #Jennifer Shehane's great answer:
let requestsCount = (alias) => cy.state('requests').filter(a => a.alias === alias).length;
expect(requestsCount('putRequest')).to.eq(0);
And you could put it in your Cypress commands file, too!
This is how the cypress team does it (source):
it("throws when alias is never requested", (done) => {
Cypress.config("requestTimeout", 100);
cy.on("fail", (err) => {
expect(err.message).to.include(
"`cy.wait()` timed out waiting `100ms` for the 1st request to the route: `foo`. No request ever occurred."
);
done();
});
cy.server().route(/foo/, {}).as("foo").wait("#foo.request");
});
And from the related docs:
Fires when the test has failed. It is technically possible to prevent the test from actually failing by binding to this event and invoking an async done callback. However this is strongly discouraged. Tests should never legitimately fail. This event exists because it’s extremely useful for debugging purposes
cy.state seems to be undefined when 0.
Also, if you want to call the command with the #, then this will work.
Cypress.Commands.add('shouldBeCalled', (alias, timesCalled) => {
const aliasname = alias.substring(1);
const requests = cy.state('requests') || [];
expect(
requests.filter((call) => call.alias === aliasname),
`${aliasname} should have been called ${timesCalled} times`
).to.have.length(timesCalled);
});
cy.shouldBeCalled('#updateCalc', 1);
Update for cy.intercept() after cy.route() deprecation.
If you are using cy.intercept(), cy.state('requests') will return objects with undefined alias, so I used xhr.url instead.
I adapted the solution of #SleepWalker like this:
Command in commands.js file:
Cypress.Commands.add('requestsCountByUrl', url =>
cy.wrap().then(() => {
const requests = cy.state('requests') || [];
return requests.filter(req => req.xhr.url === url).length;
})
);
Usage in test:
cy.requestsCountByUrl('http://theUrl.com').should('eq', 1);
I tried the simplified version that Jonathan posted, but am seeing TypeError: Cannot read property 'filter' of undefined and cy.state('requests') is always undefined.
When we have the route:
cy.intercept('PUT', '**/shoes/*', body).as('updateShoes');
The following solution worked for me:
cy.get('#updateShoes').then((interception) => {
assert.isNull(interception)
});
Cypress says:
expected null to equal null
When the '#updateShoes' route was called than (interception) is a Object:
{id: "interceptedRequest551", routeId: "1623772693273-2831", request: {…}, state: "Complete", requestWaited: false, …}
id: "interceptedRequest551"
log: {get: ƒ, unset: ƒ, invoke: ƒ, toJSON: ƒ, set: ƒ, …}
request: {headers: {…}, url: "http://localhost:8080/api/shoes/38de4e08", method: "PUT", httpVersion: "1.1", body: {…}}
requestWaited: false
response: {headers: {…}, body: {…}, url: "http://localhost:8080/api/shoes/38de4e08", method: null, httpVersion: null, …}
responseWaited: false
routeId: "1623772693273-2831"
state: "Complete"
subscriptions: []
...}
And Cypress throws an error:
AssertionError
expected { Object (id, routeId, ...) } to equal null
As of Cypress 6.0.0, cy.route is replaced by cy.intercept and cy.state is not documented properly.
Thereby, building on the Feodor's answer and the new format,
cy.intercept(<url>, (_) => {
expect("Unexpected Https call").to.be.false;
})
I think I found a way that works for me the way I expected, using cy.intercept and cy.state.
Add your route to be sniffed via cy.intercept
Wait an amount of time, your choice for what you trust
Then see if your URL is in cy.state('routes').
it(`should NOT make foo request`, () => {
// listen for any request with "foo" using cy.intercept
// I like to return success just to not see warnings in the console...
cy.intercept(/.foo./, { success: true }).as("fooRequest");
cy.window().then(win => {
// do what ever logic could make the request
makeFooRequestOrSomething();
});
// use cy.wait to wiat whatever amount of time you trust that your logoc should have run
cy.wait(1000);
/*
* cy.intercept does not provide any information unless a request is made, so instead
* we can use the state and make sure our route is not in the list
*/
let routes = cy.state('routes'); // An object representing all the routes setup via cy.intercept
let fooRoutes = [];
for (let route in routes) {
// routes[route].requests is an object representing each request
for (let req in routes[route].requests) {
let reqUrl = routes[route].requests[req].request.url;
// test each URL for "foo" and if it has it, add the URL to the array
if((/foo/).test(reqUrl)) {
fooRoutes.push(reqUrl);
}
}
};
// if no request was made to our URL, our array should be empty
expect(fooRoutes).to.have.property("length", 0);
});
routes[route] probably has the alias somewhere you could use to if you want to filter the data a different way and then see if routes[route].requests is empty.
I did not find this documented anywhere, so please let me know if there are better definitions to link to, especially for the cy.state method.
Assertion 'have.not.been.called' is working with .spy() command. It is simple, readable and can be combined with intercept() and wait()
cy.intercept('/my-route', cy.spy().as('myRequest'));
// later in the test
cy.get('#myRequest').should('not.have.been.called'); // not yet intercepted
// something triggers the API call
cy.get('#myRequest').should('have.been.calledOnce'); // now is intercepted
See: https://docs.cypress.io/api/commands/spy
Credits to: https://glebbahmutov.com/blog/cypress-tips-and-tricks/#check-if-the-network-call-has-not-been-made
This answer is taken from here

Podio JS: Cannot successfully subscribe to push service

What I want to do
I want to subscribe to push notifications notifying me when I make changes to items in Podio.
What I have done to achieve this
I am using the Podio JS library and have read the documentation and gone through the detailed example. I have in accordance with the instructions:
successfully authenticated with the PODIO api; and
successfully made a podio.request call (receiving responseBody with push property)
The next step in my code is as follows:
var subscribe = getItem.then (function (responseBody){
return podio.push(responseBody.push).subscribe(callback);
});
var notification = subscribe.then (function () { // never gets here!
console.log ('subscribed');
});
What is not working
The code never gets to the notification part and consequently does not execute console.log ('subscribed'). The callback I pass to podio.push(responseBody.push).subscribe(callback) never gets invoked even though I make changes to the relevant item in my Podio account.
When I run console.log(subscribe) at an interval of 1000 ms the output is and stays:
lib$es6$promise$promise$$Promise {
_id: 2,
_state: undefined,
_result: undefined,
_subscribers:
[ lib$es6$promise$promise$$Promise {
_id: 3,
_state: undefined,
_result: undefined,
_subscribers: [] },
[Function],
undefined ],
_onerror: null }
What my question is
I would very much appreciate help identifying why the push service is not working even though I - as I see it - as done exactly as instructed by the documentation.
Thank you in advance!
The subscription to push services was actually broken in podio-js and it has been fixed in version 1.6.0. (See this PR if interested in the code changes)
Please upgrade to the latest version of podio-js and you should be good to go.
A small example (using ES6 notation)
// assuming you already have an authenticated SDK in the variable podio
const onNotificationReceived = (message) => {
console.log(message);
};
const subscribeToPushNotifications = (itemId) => {
podio.request('GET', `/item/${itemId}`).then((data) => {
podio.push(data.push).subscribe(this.onNotificationReceived)
.then(() => console.log(`All is well, we've been subscribed!`));
}).catch(err => {
throw new Error(err)
});
};
// Whatever item you want to receive notifications about
const itemId = 99999999;
subscribeToPushNotifications(itemId);

Categories