Moving from page to page creates errors (duplicates?) - javascript

I'm trying to use the pushPage() function of the OnsenUI 2 (rc15 as of now). Alongside the OnsenUI, I'm using jQuery 3.
Here is my function that upon clicking certain elements should push a page:
$(".tile_handler").on("click", ".imalink", function () {
var link = $(this).data().href;
if(link != null){
document.querySelector("#myNavigator").pushPage(link, { animation: "slide-ios" });
}
})
When I push the page for the first time, it works fine. I use the iOS back button to return. Then I click it again and I get this error (and increasingly more as I repeat the process):
[index.js:450] Uncaught (in promise) pushPage is already running.
Here is another function that is supposed to load splitter pages:
$(".splitter_item").click(function () {
var address = $(this).data('address');
$('#content')[0].load(address).then(menu.close.bind($('#menu')[0]));
})
When I switch between two pages via Splitter it starts throwing this ( and more every time I switch between pages)
[undefined:1] Uncaught (in promise) Splitter side is locked.
What I assume is happening is that I load a page, leave it and when I access it again, it loads the page again. However that doesn't seem to be the behavior shown in OnsenUI examples such as this:
document.addEventListener('init', function(event) {
var page = event.target;
if (page.id === 'page1') {
page.querySelector('#push-button').onclick = function() {
document.querySelector('#myNavigator').pushPage('page2.html', {data: {title: 'Page 2'}});
};
} else if (page.id === 'page2') {
page.querySelector('ons-toolbar .center').innerHTML = page.data.title;
}
});
There is the popPage() function which should delete previously loaded page and would prevent this. But the references don't use it, so I assume I'm doing something wrong. But I don't know what.
UPDATE:
I managed to reproduce both problems in CodePen. Here is the Splitter error and here is the pushPage() one. It seems like the pushPage() one is a problem with my function as it adds a pushPage() request every time I click it, not sure why though.
Both errors only seem to happen in Ripple emulator (via VS2015). I don't seem to be able to reproduce them in Android Emulator (but the $(".tile_handler").on("click", ".imalink", function () { code fires incorreclty multiple times anyway). I'm testing this further.

Basically each time you push a page, that page fires an init event. However Onsen still keeps the initial page in the dom.
Navigator example (the same logic applies to the splitter one):
<ons-navigator>
<ons-page id="dashboard">
<div class="imalink" data-href="request_list.html"></div>
</ons-page>
</ons-navigator>
You have an init event for dashboard. Then you click the tile and go to another page.
Then request_list fires its own init event. However our initial page is still in the dom.
<ons-navigator>
<ons-page id="dashboard" style="display: none">
<div class="imalink" data-href="request_list.html"></div>
</ons-page>
<ons-page id="request_list">
...
</ons-page>
</ons-navigator>
You have something like this. However the following is called a second time:
$(".tile_handler").on("click", ".imalink", function () {
...
})
Which adds the listeners once more. $el.on("click") like an alias of addEventListener, meaning you are adding more and more listeners.
So whenever you navigate you keep adding them because the initial page was never removed from the dom.
Alternative solutions:
Use only the current page (e.target)
$('selector') // instead of this
$(e.target).find('selector') // use this
That way you limit to only finding elements within the page which you just created.
Enable the handlers from the start.
Since you are using jQuery there is actually a simpler way to do the things without relying on the init event.
Just do this outside of any init handler:
$(document).on("click", ".tile_handler .imalink", function () { ... })
This actually means that the handler is attached to the document itself and the handler will be called only when the target is .tile_handler .imalink - so it works with any future imalinks which you create.
This may not be the most efficient approach, but is definitely one of the simplest.

Both errors which you mention are actually a safe measure from you pushing or doing some action twice by mistake if you double click a button/link for example.
They appear when you try to make an action while an animation is running. As you can see in the examples generally there is no problem to push a page twice or more, as long as you start the second push after the first one is finished.
Here are demos with your exact code for both the splitter and navigator.
So the error does not come from the code which you provided, its someplace elsewhere.
The only thing I can come up with is that the behaviour which you mentioned would be seen if for some reason the popPage method failed to finish correctly. Maybe if you provide us with your own codepen where the issue can be reproduced we could debug it further.
An alternative, though highly NOT recommended would be to force the state before you do your action. This however would not be solving the problem, rather only masking it. And of course as with all hacks - it may break in future versions.
myNavigator._isRunning = false;
UPDATE:
Here are the two updated pens which you gave in the comments:
https://codepen.io/IliaSky/pen/YWOOkW?editors=1010
https://codepen.io/IliaSky/pen/QEVVGm?editors=1010
Basically you were adding the handlers on init event, which is fired whenever a page is added. so with your logic you are adding more and more handlers with each page. Just make sure you add them only once and you will be fine.
Add things like:
if (e.target.id == 'pagename') ...
or simply
$(document).on("init", '#dashboard_page', function(){ ... }

Take a look at .one() from jQuery - the event handler will only execute once per element in order to prevent the error: Uncaught (in promise) pushPage is already running
$(".tile_handler").one("click", ".imalink", function () {
var link = $(this).data().href;
if(link != null){
document.querySelector("#myNavigator").pushPage(link, { animation: "slide-ios" });
}})

Related

Cypress does not always executes click on element

I am automating Google Calculator.
And from time to time Cypress is not able to execute click on button.
The tests click on buttons (0 to 9 ) and do some simple math operations.
And in 30% chance it can not click on element and the test will fail.
I also recorded a video when issue appears.
Video here
My Project is located here:
https://github.com/afiliptsov/test-project
To run the test run : "npm run test:e2e:functional"
I tried to use different locator. Initially i was using just ID ex(#cwbt15 ) but after i made more specific locator ( #cwbt15 > .cwbtpl > .cwbts) and still having same issue.
Does anyone knows why it happens and how to avoid such behavior?
The project structure is :
cypress/PageObject.js - place where all elements declared.
cypress/support/commands.js - place where function click created and
verification of value getting updated.
cypress/integration/functional/delete.spec.js - test which was on the
video
2022 here and tested with cypress version: "6.x.x" until "10.x.x"
You could use { force: true } like:
cy.get("YOUR_SELECTOR").click({ force: true });
but this might not solve it ! The problem might be more complex, that's why check below
My solution:
cy.get("YOUR_SELECTOR").trigger("click");
Explanation:
In my case, I needed to watch a bit deeper what's going on. I started by pin the click action like this:
Then watch the console, and you should see something like:
Now click on line Mouse Events, it should display a table:
So basically, when Cypress executes the click function, it triggers all those events but somehow my component behave the way that it is detached the moment where click event is triggered.
So I just simplified the click by doing:
cy.get("YOUR_SELECTOR").trigger("click");
And it worked 🎉
Hope this will fix your issue or at least help you debug and understand what's wrong.
For me this code worked:
Inside your click methods add : { force: true } It will make force click.
Also add: cy.wait(150) to beforeEach or before click where your test fails.
It is just workaround not a solution.
Link to Cypress Issue
Also i saw this alternative:
cy.get('#query-btn').invoke('width').should('be.gt', 0)
cy.get('#query-btn').invoke('width').should('be. greaterThan', 0)
But it didnt work out for me. Maybe will be usefull for someone!
https://github.com/cypress-io/cypress/issues/2928 helped me.
cy.get('[data-qa="select_workers-list"]'.contains('+ New Worker').trigger('mouseover').click();
Whoever finds this problem, the official way of handling it is described here: https://www.cypress.io/blog/2019/01/22/when-can-the-test-click/
TLDR:
What #jpvantuyl said, cypress clicks the button before the onclick event is there.
The lib cypress-pipe will add a .pipe method that if followed by .should will retry a function until the condition is true or it times out.
Example:
cy
.get(numbers.result.idLocator)
.pipe($el => $el.click()) // try this
.pipe(
// next line will make assertions on the element returned by this pipe
() => cy.get(calculatorScreen.resultOutput.idLocator)
)
.should("contain", "0"); // until this happens or times out
This could be because the application is attaching behaviors to the button via JavaScript. When that script takes a long time to execute it allows Cypress to click the button before the OnClick event is there.
See: https://www.cypress.io/blog/2018/02/05/when-can-the-test-start/
Cypress recommends tracking when the underlying JS is in place with something like:
function waitForAppStart() {
// keeps rechecking "appHasStarted" variable
return new Cypress.Promise((resolve, reject) => {
const isReady = () => {
if (appHasStarted) {
return resolve()
}
setTimeout(isReady, 0)
}
isReady()
})
}
it('greets', () => {
cy.visit('app.html', {
onBeforeLoad: spyOnAddEventListener
}).then(waitForAppStart)
// all other assertion will run only when
// the application has started
cy.get('#name').type('Cypress{enter}')
cy.contains('#answer', 'Cypress')
})
Something I just learned from a colleague after none of the above worked for me and after hours of searching. Just blew my mind. Just add another .click()...
before:
cy.contains('some string').click();
In the left Cypress menu click on the action and you'll see the indicator that it clicks the correct part, but nothing happens. Do it manual in the browser and it works.
Fix:
cy.contains('some string').click().click();
and all of the sudden the string is clicked and test is moving on to the next page
In my case I had to make sure the button is visible(or not disabled), then mouseover, then click:
cy.contains(/activate/i)
.should('be.visible')
.trigger('mouseover')
.click({ force: true });
PS. /***/i means ignore letter case.
I don't know why, but for me this worked:
cy.get('[role="button"][aria-haspopup="listbox"]').trigger('mouseover').wait(1000).click().click({force:true});
I found that my button's event listeners were loaded but the button was not receiving the focus it needed. It was definitely clickable. I tried using the cypress-pipe package but to no avail. So, instead of invoking .click() on it 2 or more times, I ensured it was first set in focus prior to being clicked. This fixed it for me.
cy.get('button').focus().click()
FWIW: I was having problems submitting/navigating after a Google Places address selection. I believe my component wasn't re-rendering post-address selection.
To solve my issue, after address selection, I selected a random text element, clicked on it (no-op) and then clicked on my continue button and it worked fine.
I switched my app from being rendered as SPA to using SSR (basically removing ssr: false from my svelte.config) which started to fail my Cypress tests. Reverting fixed it for my test setup.

How to remove an event out of content script scope?

I'm trying to unbind an event from a specific element and upon research, I found this. Which is useful. In itself. I didn't know you could do that. But:
Is there a way to make it work in a browser/Chrome extension? I'm talking about content scripts.
The reason why this doesn't work the way it's described there is that the website which has attached the event in question with its own script is using a different jQuery object than the one in my extension's includes/ folder. And I can try to search the event via jQuery._data(el, 'click'); but that is my jQuery object, not the one of the website where the events are apparently stored. I'm glad I figured that out after hours of fiddling around.
Or maybe it is possible to access the website's jQuery object itself?
EDIT:
What I'm ultimately trying to achieve works in theory but … it's complicated. The original script uses a plugin event and keeps reinstalling it with .on('mouseleave',….
Anyway, this is what I got thanks to you, pdoherty926:
var $el = $('div.slideshow');
$('h2', $el).click(function(){ console.log('ouch!'); }); // test event
var $slides = $('.slides', $el).detach();
$copy = $el.clone(false);
$slides.prependTo($copy);
$el.replaceWith($copy);
The test event doesn't get triggered but the event I'm actually trying to remove still fires. I can imagine figuring it out, though, now that I got closer to my goal.
Okay, the aforementioned re-installation on mouseleave really messed up this otherwise satisfying suggestion. (The site is using the jQuery Timer plug-in by Cyntaxtech). So here's how I solved it instead: I simply changed the class name (-.-' )
Now the re-installation code cannot find the element anymore.
This is how my finished script looks like:
function stop_happening() {
var $el = $('div.fullwall div.slideshow');
$el
// first, stop the current automation.
.timer('stop') // Timer plug-in
// next, change class name in order to prevent the timer
// from being started again.
.removeClass('slideshow').addClass('slideshow-disabled-automation');
//--- copied some extra code from the website itself for the onclick
// events which are supposed to keep working. I wish I could do *that*
// programmatically but I'm glad I got as far as I got. ---//
// […]
}

Ensure that Kinetic code ALWAYS executes AFTER externally loading DIV

I'm using the Boxer library from www.formstone.it to display a modal popup window over my HTML page. On triggering the modal window, HTML content gets loaded into the modal DIV from a file on my server. The Boxer code:
$(".boxer.boxer_object").click(function(e) {
e.preventDefault();
e.stopPropagation();
$obj = $('<div class="outer_container" />').load("http://www.myserver.com/game_modal.html",function(){
setTimeout(function (){
... Kinetic code which loads several image and GUI elements for a simple game ...
}, 2000); // delay Kinetic code execution with 2 seconds
$.boxer($obj);
});
Even though it does seemingly only execute the KineticJS code AFTER the HTML code has loaded, I do still sporadically get the following error:
Uncaught TypeError: Cannot set property 'innerHTML' of null
As I understand it, this error occurs when the canvas is trying to target a DIV which does not yet exist. In other words, the KineticJS code executes AFTER the code has loaded but BEFORE the relevant container DIV has become part of the page structure.
As seen in my code above, I now use a setTimeout() function to delay the KineticJS code execution with 2 seconds. Even though less than ideal, I have not seen the error again, with the game graphics loading every time. However, this is a fix that may be working on my browser but which may fail for someone else in other conditions.
Is there a proper way in which to ensure that the KineticJS code will ALWAYS execute AFTER the externally loaded HTML code has become part of the page structure? i.e. after the container DIV which the KineticJS code targets for the HTML5 canvas actually exists?
You should be able to skip the ajax call and render the game's container and loading markup manually:
var $game = $('<div id="outside_container" style="text-align:center; width:900px; height=600px;"><span style="display:inline-block; line-height:600px; font-size: 4em;">LOADING...</span></div>');
Then use the 'callback' option to initialize the game:
$boxer($game, {
callback: initGame
});
function initGame() {
// kinetic js code
...
}
Disclaimer: I haven't used boxer.
I took a quick peek at your boxer link.
There is a callback which executes "after opening instance".
How about putting your Kinetic code in that callback function you can supply to boxer.
From what I can understand, you are trying to create a div with the class of 'outer_container' when the onclick event occurs. You then want to 'load' your game modal from your web service and then run the kinetic js and boxer code asynchronously (via the callback) when it has been returned.
Asynchronous functionality can always been a bit fiddly. In your asynchronous chain of events, I think creating the div at the same time as attaching a .load() function to it means that sometimes the web service may be ready before the div has been created.
Have you tried creating the divelement before calling a web service pointing at that element?
You could either create thediv when you first intialise the page or try this...
$(".boxer.boxer_object").click(function(e) {
e.preventDefault();
e.stopPropagation();
//create the div first before the web service call to ensure it exists...
var obj = document.createElement('div');
obj.setAttribute('class', 'outer_container');
$('.outer_container').load("http://www.myserver.com/game_modal.html",function(){
//your code here, called after the web service has returned data...
$.boxer(obj);
});
I would personally just declare the 'outer_container' div before the declaration of the onclick event.
I hope this helps :).

How do we set an onExpand event in a cfLayout accordion

We're using CFLayout to create a tab structure in our web application. After creation of that layout we call this function:
mytabs = ColdFusion.Layout.getTabLayout("#attributes.cflayoutName#");
mytabs.on('tabchange',
function(tablayout,tab) {
var tabtitle = tab.title;
alert(tabtitle); // Actual code does various useful 'stuff' here.
}
);
That piece of code works very well, and the alert will show each time the user clicks on a tab.
The problem is that we are now trying to do the same thing with a CFLayout type of "accordion", and I cannot get an event to fire when the user switches which accordion pane they are looking at. We've tried leaving the above as is, as well as changing the "tabchange" attribute to "expand", "beforeexpand", "activate", and "collapse".
For this testing I'm using the following simple JS function to avoid issues arising from the JS within the onchange event:
mytabs = ColdFusion.Layout.getAccordionLayout("#attributes.cflayoutName#");
mytabs.on('expand',
function(tablayout,tab) {
console.log('test');
}
);
We do not receive any errors. Nothing is logged to the console at all. I've tried replacing the console.log to an alert to rule out any problems with that line.
I found that the Ext library documentation to be very helpful with finding a solution to this problem: here.
The Ext library has a getComponent method that allows you to reference the accordion layout panel that you are trying to add the expand event to. Once you have this, you can use the "on" method you are using above to assign the expand event to each panel individually.
for (x=1; x<accordionLayoutArray.length; x++) {
mytabs.getComponent(accordionPanelName).on('expand',
function(tab) { ... });
}
This became too long for a comment so adding as an answer
After some Google searches I found what I think are some related posts. It appears as though the accordion in Ext JS does not have the same events as the tab. Instead you need to add a listener in order to catch the expanding.
See this post - in case something happens to that page here is the relevant piece:
You'd need to listen to the expand event of the child panels in the accordion, you could do something like:
Code:
myAccordion.add(myFunc('myTitle'));
function myFunc(title)
{
return new Ext.Panel(
{
title: title,
listeners: { 'expand': {fn: something, scope: foo}}
}
);
}
And I also found another similar post here on SO - see both answers
Once you know that the accordion needs a listener you can find a number of results on Google. Such as How do I attach an event handler to a panel in extJS?
This Google search will give you lots of examples.
Hope that helps.

Javascript callback not firing when AJAX operation complete

Given the following code on an ASP.NET MVC View:
<% using (Ajax.BeginForm("AddCommunity",
new AjaxOptions { UpdateTargetId = "community-list",
OnSuccess = "BindCommunityHover" }))
{ %>
Add Community: <input type="text" id="communityName" name="communityName" />
<input type="submit" value="Add" />
<% } %>
And the following JavaScript method in the file:
function BindCommunityHover() {
$(".community-item-li").hover(
function () { $(this).addClass("communityHover"); },
function () { $(this).removeClass("communityHover"); }
);
};
Is there any reason why BindCommunityHover is not being called when the AJAX result comes back?
The community-list div is properly updated (the action returns a partial view). I have also tried setting OnComplete (instead of OnSuccess) to no avail.
The BindCommunityHover method is called in a $(function(){...}); block when the page first loads, and for all existing .community-item-li elements, it works.
The partial result from my controller replaces all items in that div with more of the same class. The OnSuccess method is supposed to fire after the document is updated.
Update: k...this gets weird. I added the following to the BindCommunityHover method:
alert($(".community-item-li").size());
I'm getting 240 in the alert when the page loads and when the callback fires. So, the callback IS firing, jQuery is matching the elements but not applying the styles...
That's because your function is basically saying add a hover event for all of these items as they exist at the point in time when the function is called.
If you then add new elements they aren't automatically bound. There is a new feature in JQuery called Live Events. I've not dug into them but I think they might help here. Otherwise as you add new elements be sure to bind the hover functions.
Okay, there were two parts to this solution.
First culprit was some nasty caching thing that I can't figure out in Cassini/IE. I tried rebooting, stopping Cassini, restarting VS2010...nothing worked.
The code still won't work on IE on my account on this computer. The deployed-to-IIS version works on all browsers. It also works in IE if I change the filename. Something is borked there, though. If I run the project with Cassini/IE, then open FireFox and go to the site, it works.I tried to repro the error to file a bug, but I can't get it to go. I digress. To get around this I changed the name of the .js file and moved the reference to a different spot in the master page.
The other thing was that I did have to use OnSuccess. I switched to using OnComplete when I was trying to figure out what was wrong. After I figured out the file/browser/server problem, I realized that OnComplete (per the docs) fires before the document is updated; the elements were being updated but then thrown away.
The OnSuccess/OnComplete might help sort something out for someone, not sure about the file/browser/server issue...that might be environmental.

Categories