Second test doesnt change URL - javascript

I have two test. The first test passes successfully. Then there is an url method call in the second test, but it doesn't change the url in the browser.
The baseUrl in wdio.conf.js is set to http://localhost/web/es/index.html#
Tests:
var assert = require('assert');
describe('user login ', function(){
it('user login', function(){
browser
.url('/system/login')
.setValue('[name="username"]','test')
.setValue('[name="password"]','test')
.click('=Potvrď');
assert(browser.waitUntil('=test test'));
});
it('user form', function(){
browser
.url('/user/form');
});
});
In the first test /system/login is opened correctly. But in the second test the url never changes to /user/form
I'm just starting with webdriverio so am i missing something ?

A lot of time passed since #user49126 asked the question, but since no one actually saw that this is in fact a routing issue, here is my answer:
It looks like you are using a bad baseUrl value. You have to go back and re-read it's description in the wdio.config.js file. Any value you pass to the browser.url() function will be postpended to your baseURL.
In your case, firstly you are going to http://localhost/web/es/index.html# + /system/login, which due to the # and probably your routing logic behind, evaluates correctly to a valid route. The second test most surely takes you to an invalid route.
In order to completely isolate your issue we will need the console output.
Here is the setup I used to debug this:
wdio.config.js:
// Set a base URL in order to shorten url command calls. If your url parameter starts
// with "/", then the base url gets PREPENDED.
baseUrl: ' http://127.0.0.1:8303',
Code:
describe("\nDebug routing issue", function() {
it("\nShould verify the URL, click something, move on\n", function() {
return browser
.url('/product-page.html')
.getUrl()
.then( function(retURL) {
assert.include(retURL, "product-page", "Yeap! You're good!");
})
.waitUntil(function() {
return browser.isVisible("div.top-bar-right a");
}, 3000, "\nLogin failed...")
.click('div.top-bar-right a')
// Casual 'pause' to witness the click
.pause('1000');
});
it("\nShould reload different URL and validate site routing\n", function() {
return browser
.url('/index.html')
.getUrl()
.then( function(retURL) {
assert.include(retURL, "index", "Yeap! You're good!");
})
// Casual 'pause' to witness the new route
.pause('1000')
});
TL;DR: Just make sure you are have the correct value in the baseUrl variable. In your case, change the baseUrl value to http://localhost/.
My suggestion is to also use the port, e.g. http://localhost:8303/, or the IP of your localhost server altogether, as I also experienced some issues using it as simply localhost.
PS: None of the other answers touch upon the real issue the user is experiencing. Unfortunately I have no way to comment/signal this. ('Not enough minera... errrg, rep points,' #feelsbadman)

Check the condition on browser.waitUntil(...). The first parameter is a function according to http://webdriver.io/api/utility/waitUntil.html.
Also, adding a timeout (3seconds) will help to see if the wait has been successful.
browser.waitUntil(function() {
return browser.getText('#some_id') === 'test test';
}, 3000, "Timeout!!");

That is because it's not really a test. What are you testing, where is your acceptance criteria? You told the browser to do something and didn't wait to check up on it. It's basically like you said: Selenium, do this and I don't care I am done, bye.
What you can use?
Try giving it a pause:
.pause(ms);
give it 10000 (10s) and visually see if it changes.
you can also add an additional:
.getUrl() - to get the URL after the time stated under ms.
Alternatively (instead of the things stated above):
On your new URL, is there an element in DOM that is not present in the DOM on old URL? If so, add to the end:
.waitForExist(selector, ms);
give it 1000 under ms, and make sure you got the right selector for that element
This way browser will wait for that element to appear.

Related

Cypress - wait for the API response and verify UI changes

I have a component that I want to cover with some e2e tests. This component takes the URL provided by the user in the input, calls the API after the button click and then returns the shortened version of that URL. After that, shortened url is added to the list below the input on the UI and makes some localStorage assertion. I want Cypress to wait for the API response and only then check the UI if the list item was added. I made this working but I hardcoded the wait time in the wait() method. How Can I achieve that programatically ?
describe("Shortener component", () => {
it("Should add the list item and data to localStorage", () => {
cy.visit("http://127.0.0.1:5500"); //Live server extension address
cy.get("#url-input").type("https://facebook.com");
cy.get("#form-submit-button").click();
// wait for the api response and make sure that the value has been added to the localStorage
cy.wait(40000); //todo - wait for the api response instead of hardcoding the wait time
const localStorageData = localStorage.getItem("linksData");
if (JSON.parse(localStorageData)) {
expect(JSON.parse(localStorageData)[0].inputValue).to.eq(
"https://facebook.com"
);
}
// check if the new list item with the corrct value has been addded
cy.get(".shortener-component__list-item")
.contains("https://facebook.com")
.should("be.visible");
//validation mesasge should not be visible
cy.get("#validationMesage")
.contains("Please add a valid link")
.should("not.be.visible");
});
});
I tried with intercept() however I failed. Not sure how to make it working. I also saw some similar SE topics on that but it did not help me.
Any ideas / examples apreciated :)
Thx !
From the order of events you've given
short URL returned
added to localStorage
added to list
just change the order of feature testing
test list - it is last event, but has retriable commands (you can increase the timeout)
now test localStorage, if UI has the short URL so will localStorage
cy.contains('.shortener-component__list-item', 'https://facebook.com', { timeout: 40000 })
.then(() => {
// nested inside .then() so that the above passes first
const localStorageData = localStorage.getItem("linksData");
const linksData = JSON.parse(localStorageData);
expect(linksData).not.to.eq(undefined);
expect(linksData[0].inputValue).to.eq("https://facebook.com");
})
Alternatively, to make use of retry and timeout on the localStorage check,
cy.wrap(localStorage.getItem("linksData"))
.should('not.eq', undefined, { timeout: 40000 }) // will retry the above until true
.then(data => JSON.parse(data))
.should(parsedData => {
expect(parsedData.inputValue).to.eq("https://facebook.com");
});
I guess you should also start the test with
cy.clearLocalStorage("linksData")
There're examples in the documentation, it only takes some reading and experimentation.
In general, you need three commands: cy.intercept(), .as(), and cy.wait():
cy.intercept(your_url).as('getShortenedUrl');
cy.wait('#getShortenedUrl');
you can also use .then() to access the interception object, e.g. a response:
cy.wait('#getShortenedUrl').then(interception => { });
or you can check something in the response using .its():
cy.wait('#getShortenedUrl').its('response.statusCode').should('eq', 200);
The point is that after cy.wait('#getShortenedUrl'), the response has been received.

How to check headers of most recent response in Cypress?

Our application has a feature where you can press a button, wait for a while, and then get some information after it's been delivered. It'll send about 10 responses with identical url's and only the last one should have the information we care about in it. My script right now is:
beforeEach(function() {
cy.server()
cy.route('get', /waiting_for_response/).as('response')
})
it('checks for good data', function() {
cy.get('.get-data').click()
cy.wait('#response', {timeout:10000}).then((xhr) => {
expect(xhr.responseHeaders.data).to.not.be.a('null')
})
})
This always fails since cy.wait is resolved on the first matching url and only the last response's headers have a data value of not null. Is there a way to wait for all of the responses to have come back before checking if any of them were not null? Or perhaps specify the route has to have a certain response header in order to match? Any help or ideas would be super appreciated, thanks in advance!
You can break the problem into smaller conditions
cy.wait(10000)
check length of cy.get('<>').should('have.length', 10)
now you can check condition expect(xhr.responseHeaders.data).to.not.be.a('null')
You can refer to documentation
https://docs.cypress.io/api/commands/route.html#With-Stubbing
Section: Making multiple requests to the same route

Cypress wait for API after button click

I've made a React app, which all works perfectly and I'm now writing some end to end tests using Cypress.
The React app all works on the same url, it's not got any routes, and api calls from inside the app are handled through button clicks.
The basis of the app is the end user selects some options, then presses filter to view some graphs that are dependant on the selected options.
cy.get('button').contains('Filter').click()
When the button is pressed in cypress, it runs the 3 api calls which return as expected, but looking over the cypress docs there is no easy way unless I use inline cy.wait(15000) which isn't ideal, as sometimes they return a lot faster, and sometimes they return slower, depending on the selected options.
Edit 1
I've tried using server and route:
cy.server({ method: 'GET' });
cy.route('/endpoint1*').as('one')
cy.route('/endpoint2*').as('two')
cy.route('/endpoint3*').as('three')
cy.get('button').contains('Filter').click()
cy.wait(['#one', '#two', '#three'], { responseTimeout: 15000 })
Which gives me the error:
CypressError: Timed out retrying: cy.wait() timed out waiting 5000ms for the 1st request to the route: 'one'. No request ever occurred.
After further investigation
Changing from responseTimeout to just timeout fixed the error.
cy.server({ method: 'GET' });
cy.route('/endpoint1*').as('one')
cy.route('/endpoint2*').as('two')
cy.route('/endpoint3*').as('three')
cy.get('button').contains('Filter').click()
cy.wait(['#one', '#two', '#three'], { timeout: 15000 }).then(xhr => {
// Do what you want with the xhr object
})
Sounds like you'll want to wait for the routes. Something like this:
cy.server();
cy.route('GET', '/api/route1').as('route1');
cy.route('GET', '/api/route2').as('route2');
cy.route('GET', '/api/route3').as('route3');
cy.get('button').contains('Filter').click();
// setting timeout because you mentioned it can take up to 15 seconds.
cy.wait(['#route1', '#route2', 'route3'], { responseTimeout: 15000 });
// This won't execute until all three API calls have returned
cy.get('#something').click();
Rather than using a .wait you can use a timeout parameter. That way if it finished faster, you don't have to wait.
cy.get('button').contains('Filter', {timeout: 15000}).click()
This is mentioned as one of the options parameters in the official docs here.
cy.server() and cy.route() are deprecated in Cypress 6.0.0 In a future release, support for cy.server() and cy.route() will be removed. Consider using cy.intercept() instead.
this worked for me...
ex:-
see the screen shot
cy.intercept('GET', 'http://localhost:4001/meta').as('route');
cy.get(':nth-child(2) > .nav-link').click();
cy.contains('Filter');
cy.wait(['#route'], { responseTimeout: 15000 });
Try this:
cy.contains('button', 'Save').click();
cy.get('[data-ng-show="user.manage"]', { timeout: 10000 }).should('be.visible').then(() => {
`cy.get('[data-ng-show="user.manage"]').click();
})

MEAN / AngularJS app check if object already posted

I have thig angularJS frontend and I use express, node and mongo on the backend.
My situation looks like:
//my data to push on server
$scope.things = [{title:"title", other proprieties}, {title:"title", other proprieties}, {title:"title", other proprieties}]
$scope.update = function() {
$scope.things.forEach(function(t) {
Thing.create({
title: t.title,
//other values here
}, function() {
console.log('Thing added');
})
})
};
//where Thing.create its just an $http.post factory
The HTML part looks like:
//html part
<button ng-click="update()">Update Thing</button>
Then on the same page the user has the ability to change the $scope.things and my problem is that when I call update() again all the things are posted twice because literally thats what I'm doing.
Can someone explain me how to check if the 'thing' its already posted to the server just to update the values ($http.put) and if its not posted on server to $http.post.
Or maybe its other way to do this?
I see a few decisions to be made:
1) Should you send the request after the user clicks the "Update" button (like you're currently doing)? Or should you send the request when the user changes the Thing (using ngChange)?
2) If going with the button approach for (1), should you send a request for each Thing (like you're currently doing), or should you first check to see if the Thing has been updated/newly created on the front end.
3) How can you deal with the fact that some Thing's are newly created and others are simply updated? Multiple routes? If so, then how do you know which route to send the request to? Same route? How?
1
To me, the upside of using the "Update" button seems to be that it's clear to the user how it works. By clicking "Update" (and maybe seeing a flash message afterwards), the user knows (and gets visual feedback) that the Thing's have been updated.
The cost to using the "Update" button is that there might be unnecessary requests being made. Network communication is slow, so if you have a lot of Thing's, having a request being made for each Thing could be notably slow.
Ultimately, this seems to be a UX vs. speed decision to me. It depends on the situation and goals, but personally I'd lean towards the "Update" button.
2
The trade-off here seems to be between code simplicity and performance. The simpler solution would just be to make a request for each Thing regardless of whether it has been updated/newly created (for the Thing's that previously existed and haven't changed, no harm will be done - they simply won't get changed).
The more complex but more performant approach would be to keep track of whether or not a Thing has been updated/newly created. You could add a flag called dirty to Thing's to keep track of this.
When a user clicks to create a new Thing, the new Thing would be given a flag of dirty: true.
When you query to get all things from the database, they all should have dirty: false (whether or not you want to store the dirty property on the database or simply append it on the server/front end is up to you).
When a user changes an existing Thing, the dirty property would be set to true.
Then, using the dirty property you could only make requests for the Thing's that are dirty:
$scope.things.forEach(function(thing) {
if (thing.dirty) {
// make request
}
});
The right solution depends on the specifics of your situation, but I tend to err on the side of code simplicity over performance.
3
If you're using Mongoose, the default behavior is to add an _id field to created documents (it's also the default behavior as MongoDB itself as well). So if you haven't overridden this default behavior, and if you aren't explicitly preventing this _id field from being sent back to the client, it should exist for Thing's that have been previously created, thus allow you to distinguish them from newly created Thing's (because newly created Thing's won't have the _id field).
With this, you can conditionally call create or update like so:
$scope.things.forEach(function(thing) {
if (thing._id) {
Thing.update(thing._id, thing);
}
else {
Thing.create(thing);
}
});
Alternatively, you could use a single route that performs "create or update" for you. You can do this by setting { upsert: true } in your update call.
In general, upsert will check to see if a document matches the query criteria... if there's a match, it updates it, if not, it creates it. In your situation, you could probably use upsert in the context of Mongoose's findByIdAndUpdate like so:
Thing.findByIdAndUpdate(id, newThing, { upsert: true }, function(err, doc) {
...
});
See this SO post.
#Adam Zemer neatly addressed concerns I raised in a comment, however I disagree on some points.
Firstly, to answer the question of having an update button or not, you have to ask yourself. Is there any reason why the user would like to discard his changes and not save the work he did. If the answer is no, then it is clear to me that the update should not be place and here is why.
To avoid your user from loosing his work you would need to add confirmations if he attempts to change the page, or close his browser, etc. On the other if everything is continuously saved he has the peace of mind that his work is always saved and you dont have to implement anything to prevent him from loosing his work.
You reduce his workload, one less click for a task may seem insignificant but he might click it many time be sure to have his work save. Also, if its a recurrent tasks it will definitely improve his experience.
Performance wise and code readability wise, you do small requests and do not have to implement any complicated logic to do so. Simple ng-change on inputs.
To make it clear to him that his work is continuously save you can simply say somewhere all your changes are saved and change this to saving changes... when you make a request. For exemple uses, look at office online or google docs.
Then all you would have to do is use the upsert parameter on your mongoDB query to be able to create and update your things with a single request. Here is how your controller would look.
$scope.update = function(changedThing) { // Using the ng-change you send the thing itself in parammeter
var $scope.saving = true; // To display the saving... message
Thing.update({ // This service call your method that update with upsert
title: changedThing.title,
//other values here
}).then( // If you made an http request, I suppose it returns a promise.
function success() {
$scope.saving = false;
console.log('Thing added');
},
function error() {
//handle errors
})
};

Backbone model.save() fails unless evaluated in DevTools console first

I have this function in a Backbone view:
updateToServer: function(e) {
e.preventDefault();
var id = e.target.getAttribute('data-id');
var file = this.collection.get(id);
var data = {};
$(e.target).serializeArray().map(function(x) {data[x.name] = x.value;});
file.save(data);
this.$el.modal('hide');
}
If I allow this to run naturally, I get undefined is not a function on file.save(data). However, if I set a breakpoint in Chrome DevTools at file.save(data) and evaluate that function manually in the console before resuming, both save functions work.
Why is this happening and how can I fix it?
Here's the entire view in case you need additional context: https://gist.github.com/raddevon/d3ddf1bba101b6b67c4b#file-supportfilesview-js-L155-L163
Update: New discovery: On the second run, this works. I have an even listener on form submit. When I click the submit button the first time, I get the error. If I click again, the model saves.
Can you try this
updateToServer: function(e) {
e.preventDefault();
var id = e.target.getAttribute('data-id');
var file = this.collection.get(id);
var data = {};
$(e.target).serializeArray().map(function(x) {data[x.name] = x.value;});
this.$el.modal('hide');
setTimeout(function(){
file.save(data);
}, 200); //try with different values for timer
}
I have added a 200 millisecond timer.
This might not be your actual solution but at least you will come to know if there is some asynchronous stuff going on before 'file' is actually formed.
Try different values for the timer. I mean keep increasing the timer and see if you are still not able to get rid of the error.
Once you are sure that 'file' is formed asynchronously then you can look into why that's happening.
And try console.logs instead of debuggers for debugging so that you can test without pausing the execution.
Hope that helps.
This was not at all what I suspected, and I hadn't given enough information in the question without realizing it. The line in my code that triggered the exception was file.save(), but the actual exception was happening inside Backgrid.
I provide a form to allow users to update models from the collection displayed in a grid. A particular column is defined as an integer column, but I hadn't converted the value coming from the form to an integer. As a result, Backgrid was trying to run toFixed on a string. I modified my form serialization code to convert strings containing only integers into integers. Now, everything works as expected.
Here's that serialization code:
$(e.target).serializeArray().map(function(x) {
data[x.name] = x.value === 'on' ? true : x.value;
if (!isNaN(parseInt(data[x.name])) && isFinite(data[x.name])) {
data[x.name] = parseInt(data[x.name]);
}
});
If I had to guess, I'd say that's probably a bit naive, but it seems to be working well in my application.
Thanks to everyone for the help!

Categories

Resources