I'm executing a test that runs a functional use of a "click to add" feature.
The user is given a table of items that allow them to click an "Add" button to add the item to their cart.
The button executes an ajax call to append the item to the user's cart. When the item is added successfully, the item is then displayed in the Cart UI. The Cart UI is essentially another table.
//pseudo code
$('.addButton').on('click', function (event) {
$.ajax({
url:...,
success: updateCart
});
});
function updateCart (data) {
// use data to create tr_fragment
$("#cart-ui-target").append(tr_fragment); //new row
}
What I've tried is in the then statement is use waitFor until the #cart-ui-target has a <tr> size greater or equal to 1:
waitFor(5) {$('#cart-ui-target').find('tr').size() >= 1}
However, every once in a while the test fails with the following exception:
geb.waiting.WaitTimeoutException: condition did not pass in 5.0 seconds (failed with exception)
I've even tried to increase the waitFor time to 10 seconds with a 2 second interval, but it still doesn't work:
waitFor(10, 2) {$('#cart-ui-target').find('tr').size() >= 1}
What can I do to make this a better wait and prevent the sporadic failures?
UPDATE
This is what I'm seeing in the log info of phantomjs.
$('#cart-ui-target').find('tr').size()
| | |
| [] 0
[[[[[PhantomJSDriver: phantomjs on LINUX (113358b0-7bf1-11e4-a10e-9f2b2537fa31)] -> tag name: html]] -> css selector: #cart-ui-target]]
I think your trying to test the JavaScript behaviour, not the adding to the cart itself (probably tested in backend logic?).
I think the way to go is to mock $.ajax and call the callback from your mock using fixture data. If your using jasmine this can look something this:
it('should add something when something is succesfully addes', function() {
data = 'data_for_callback_here';
spyOn($, 'ajax').andCallFake(function(data) { data.success(data); } );
$('.addButton').click();
expect() // Expect something
});
Related
I'm using Inertia/Laravel/VueJs. I have a page with many posts and each post can be marked as completed. When a post is marked as completed on the front-end I have a v-bind which toggles a CSS class which should be applied to completed tasks.
The behaviour I would like is: a user scrolls down a page, clicks the completed button, and the back-end sends that newly updated data to the front-end, making the v-bind true, causing the CSS class to be applied without jumping to the top of the page.
With the code below I can click the completed button, and it is updated in the database, but that new data isn't sent to the front-end.
Controller:
public function markAsCompleted(Request $request)
{
$post = Post::find($request->id);
$post->completed = true;
$post->save();
return Redirect::route('posts');
}
Javascript function called at click of completed button:
completed(id) {
this.completedForm.put(`/endpoint/completed/${id}`, {
preserveScroll: true
});
},
If I change the Javascript function to:
completed(id) {
this.completedForm.put(`/endpoint/completed/${id}`, {
preserveScroll: true,
onSuccess: () => {
Inertia.get('posts');
},
});
},
In this case, the new data is returned to the front-end with the post being marked as completed, but the preserveScroll doesn't work, and jumps the user to the top of the page.
Any ideas on how to get my desired use case working? Users could have hundreds of posts so I can't have the page jump up to the top every time.
Thank you for any help!
To update your app while preserving the state:
Inertia.get('posts', params, {
preserveState: true,
})
One way, not the cleanest, but it is a solution. Just click "Completed Button" to send a request to the backend, and ONLY to check if the response is success, then update that task in frontend as complete. So you just update (add class) this element and you don't rerender whole DOM. Because, if you get success response, thats done in the backend for sure.
In a test spec, I need to click a button on a web page, and wait for the new page completely loaded.
emailEl.sendKeys('jack');
passwordEl.sendKeys('123pwd');
btnLoginEl.click();
// ...Here need to wait for page complete... How?
ptor.waitForAngular();
expect(ptor.getCurrentUrl()).toEqual(url + 'abc#/efg');
Depending on what you want to do, you can try:
browser.waitForAngular();
or
btnLoginEl.click().then(function() {
// do some stuff
});
to solve the promise. It would be better if you can do that in the beforeEach.
NB: I noticed that the expect() waits for the promise inside (i.e. getCurrentUrl) to be solved before comparing.
I just had a look at the source - Protractor is waiting for Angular only in a few cases (like when element.all is invoked, or setting / getting location).
So Protractor won't wait for Angular to stabilise after every command.
Also, it looks like sometimes in my tests I had a race between Angular digest cycle and click event, so sometimes I have to do:
elm.click();
browser.driver.sleep(1000);
browser.waitForAngular();
using sleep to wait for execution to enter AngularJS context (triggered by click event).
You don't need to wait. Protractor automatically waits for angular to be ready and then it executes the next step in the control flow.
With Protractor, you can use the following approach
var EC = protractor.ExpectedConditions;
// Wait for new page url to contain newPageName
browser.wait(EC.urlContains('newPageName'), 10000);
So your code will look something like,
emailEl.sendKeys('jack');
passwordEl.sendKeys('123pwd');
btnLoginEl.click();
var EC = protractor.ExpectedConditions;
// Wait for new page url to contain efg
ptor.wait(EC.urlContains('efg'), 10000);
expect(ptor.getCurrentUrl()).toEqual(url + 'abc#/efg');
Note: This may not mean that new page has finished loading and DOM is ready. The subsequent 'expect()' statement will ensure Protractor waits for DOM to be available for test.
Reference: Protractor ExpectedConditions
In this case, you can used:
Page Object:
waitForURLContain(urlExpected: string, timeout: number) {
try {
const condition = browser.ExpectedConditions;
browser.wait(condition.urlContains(urlExpected), timeout);
} catch (e) {
console.error('URL not contain text.', e);
};
}
Page Test:
page.waitForURLContain('abc#/efg', 30000);
I typically just add something to the control flow, i.e.:
it('should navigate to the logfile page when attempting ' +
'to access the user login page, after logging in', function() {
userLoginPage.login(true);
userLoginPage.get();
logfilePage.expectLogfilePage();
});
logfilePage:
function login() {
element(by.buttonText('Login')).click();
// Adding this to the control flow will ensure the resulting page is loaded before moving on
browser.getLocationAbsUrl();
}
Use this I think it's better
*isAngularSite(false);*
browser.get(crmUrl);
login.username.sendKeys(username);
login.password.sendKeys(password);
login.submit.click();
*isAngularSite(true);*
For you to use this setting of isAngularSite should put this in your protractor.conf.js here:
global.isAngularSite = function(flag) {
browser.ignoreSynchronization = !flag;
};
to wait until the click itself is complete (ie to resolve the Promise), use await keyword
it('test case 1', async () => {
await login.submit.click();
})
This will stop the command queue until the click (sendKeys, sleep or any other command) is finished
If you're lucky and you're on angular page that is built well and doesn't have micro and macro tasks pending then Protractor should wait by itself until the page is ready. But sometimes you need to handle waiting yourself, for example when logging in through a page that is not Angular (read how to find out if page has pending tasks and how to work with non angular pages)
In the case you're handling the waiting manually, browser.wait is the way to go. Just pass a function to it that would have a condition which to wait for. For example wait until there is no loading animation on the page
let $animation = $$('.loading');
await browser.wait(
async () => (await animation.count()) === 0, // function; if returns true it stops waiting; can wait for anything in the world if you get creative with it
5000, // timeout
`message on timeout`
);
Make sure to use await
you can do something like this
emailEl.sendKeys('jack');
passwordEl.sendKeys('123pwd');
btnLoginEl.click().then(function(){
browser.wait(5000);
});
browser.waitForAngular();
btnLoginEl.click().then(function() { Do Something });
to solve the promise.
I am trying to get jQuery to send a mousedown event to the Drupal 7 "add another item" button for a multi-value field, then wait until the ajax call has completed before filling in that new blank row with data from an element in a jQuery object (that has several elements). I need to use a loop to cycle through the elements (ingredients) in this jQuery object, but no matter what I try my page dies...
Currently, I have something like the following:
i = 0;
ingredients = newHtml.find('.recipe_ingredients > li');
ingredientsLength = ingredients.length;
$('#edit-field-ingredients-und-add-more').mousedown();
while(i < ingredientsLength) {
if ( document.readyState !== 'complete' ) {
// code to fill in the new blank row with data from 'ingredients'
$('#edit-field-ingredients-und-add-more').mousedown();
i++;
}
}
Because I don't yet know how to issue the ajax call myself using jQuery (or using Drupal) I've been trying to just check whether the call has completed by using .readyState and other hack-like methods. I'm just not sure what to try next!
Am I going about this the completely wrong way? Is there a straightforward way to make the "add another item" multi-value field ajax call using jQuery? Any help would be greatly appreciated...
I am not sure if there's a nicer way in Drupal 7, but in Drupal 6 you could use jQuery(document).ajaxComplete with the settings.url property to tell when a specific "Add another item" click had finished.
Start with:
(function($) {
$(document).ajaxComplete(function(e, xhr, settings)
{
alert(settings.url);
});
}(jQuery));
Once you've identified the right settings.url for your field, change that to:
(function($) {
$(document).ajaxComplete(function(e, xhr, settings)
{
if (settings.url == "the path from step 1") {
// Code to populate your fields here
}
});
}(jQuery));
And voila!
You might want to read the page by Jay Matwichuk where I originally learned this technique awhile back. All credit to him (and nclavaud for his comment there), really.
I have a delete button. which on click it should visit a url like /delete/{id} and once the response from that url is true. i want to delete the comment box like in facebook.
I wont add any extra than Leo's comment, but I will explain with some code. Presume that you are using jQuery...
$(document).ready(function(){
$('tr a.delete').live('click', function(e){
e.preventDefault();
var link = $(this);
$.get($(this).attr('href'), null, function(response){
if(response == 'ok'){ //you should invent how to get 'ok' or other string identifying that the deletion is successful.
link.parents('tr').remove();
} else {
alert('There is a problem while deleting this element');
}
});
})
});
if you put this code on your project it will handle all links which had .delete class and are in a table row.
There are two things which you should do:
You need to pass some string in
order to detect if the operation is
successful or not. In my example I
would print "ok" on success
deletion.
If your table has pagination, it wont
rebuild the table, while it just
will remove the row from the table.
and if you have let's say 5 rows per
page and you delete all of them the
page will remain empty while there
will be other records in the table.
That's why instead of removing the
tr I would reload the whole page.
In that case the code for successful deletion will look like this:
if(response == 'ok'){
$('#content').load(window.location);
}
The script is not optimised, but it will give you the ideas how to achieve your ideas.
HTH
So write an onClick event handler in your view, a php delete method on the appropriate controller called by the event handler and a javascript action to perform when the ajax call returns success.
I'm using the following jQuery plugin and it executes when there's a click action on the specified element :
http://www.andresvidal.com/labs/relcopy.html
I also created a click function for the same element, but I expected it to execute after the plugin. However that isn't the case, the click function always executes first, thus causing problems.
Please check my code below for example, I'm new to jQuery so any suggestions / help would really be appreciated!
$.getScript('../js/relCopy.js', function() {
$('#AddTable').relCopy({
limit: 10,
excludeSelector: '.newListSelected'
});
$('#AddTable').click(function() {
do something here after relcopy has finished
});
Event handlers are executed in the order they were bound, and since the callback from $.getScript() executes after that script is loaded, it's binding it's click handler after yours.
So get the order you want you need to bind in the order you want, that means binding your click handler in the callback as well, like this:
$.getScript('../js/relCopy.js', function() {
$('#AddTable').relCopy({
limit: 10,
excludeSelector: '.newListSelected'
}).click(function() {
//do something here after relcopy has finished
});
});