AngularJS UI router child states not waiting for parent resolves - angularjs

Seems like child states are not waiting till parent state resolved. I am using resolve config option to load common data soon after user login to the system in a common parent state. Then all children can use it. Also I have a auth interceptor to check data has been loaded. I have created a simple example to demonstrate my problem. You don't need a username or a password to login and just see the logs. Application not navigate you to the home page because user data hasn't been loaded. I used one of a examples found in http://scotch.io to create this simple app. I wonder how to configure ui router, to child states wait till parents promises are resolved.
When authorize fires for app.home state on $stateChangeStart even data has not been loaded even I am loading data in app state which is the parent for app.home
function authorize(event, toState, toParams){
if(/^app/.test(toState.name)) {
console.log('toState', toState, 'cache', cache);
if(!cache.hasOwnProperty('user')) {
event.preventDefault();
$state.transitionTo('login');
console.log('data not loaded', event);
}
}
}
Example
http://plnkr.co/edit/hS8jKONk0ZfyJfE4r7Ty?p=preview

Related

Test fails because AngularJS has not initialized in time

I'm trying out TestCafe for an AngularJS (v1.6) application.
I have a button then when clicked, opens a modal (from UI bootstrap). This works fine when I try myself in Chrome.
<button class="btn" ng-click="open()">Open</button>
Our application requires user authentication, and the login page is not Angular-based. That phase of my test works fine.
However, when the actual test runs, it "clicks" the button but nothing happens.
I suspect, but can't prove, that it's clicked before AngularJS has properly initialized on the page.
With some research, I found the testcafe-angular-selectors project and a waitForAngular method but that appears to apply only to Angular2+.
import { Role, Selector } from 'testcafe';
const regularAccUser = Role('http://127.0.0.1:8080', async t => {
await t
.typeText('[name=username]', 'abc')
.typeText('[name=password]', '123')
.click('.btn-primary');
});
fixture`Characters Modal`;
test('modal title', async t => {
await t
.useRole(regularAccUser)
.navigateTo('http://127.0.0.1:8080/fake/page')
.click('.btn')
.expect(Selector('.modal-title').innerText).eql('Insert Symbol');
});
Adding .wait(1000) before the click solves the issue. It's not waiting for Angular to load. I'd rather not have waits in every test - is there some other technique I can use?
You can use TestCafe assertions as a mechanism to wait until an element is ready before acting on it.
A typical waiting mechanism would be:
const button = Selector('button.btn')
.with({visibilityCheck: true});
await t
.expect(button.exists) // wait until component is mounted in DOM
.ok({timeout: 10000}) // wait enough time
.hover(button) // move TestCafe cursor over the component
.expect(button.hasAttribute('disabled'))
.notOk({timeout: 10000}) // wait until the button is enabled
.click(button); // now we are sure the button is there and is clickable
This article may also help you in managing all those waiting mechanisms.
As you correctly mentioned, the waitForAngular method is intended for Angular only, not for AngularJS.
I recommend you create your own waitForAngularJS function and call it on the beforeEach hook and after the role was initialized.
In the simplest case, it can be implemented as follows:
function waitForAngularJS (t) {
await t.wait(1000);
}
fixture `App tests`
.page('page with angularjs')
.beforeEach(async t => {
await waitForAngularJS(t);
});
However, the use of the wait method is not a solid solution. I recommend you find a way to detect if AngularJS is loaded on a page on the client side. If it is possible, you can implement the waitForAngularJS method using the TestCafe ClientFunctions mechanism.
This post can be usefulĀ as well: How to check if angular is loaded correctly

Preventing page navigation inside a Backbone-driven SPA

The justification
In my BB app, I allow rapid input from users which gets queued & sent off periodically in the background to the server. The problem I currently have is if a user leaves the page they effectively discard any pending changes sitting in the queue.
So basically what I want to do is inform the user before they leave to give them the opportunity to wait for the changes to be saved rather than just exiting & discarding.
The nitty gritty
So for the general cases where the user refreshes or attempts to navigate to an external URL we can handle the onbeforeunload event. Where it becomes slightly tricky is when we are in the context of an SPA whereby switching between pages does not cause a page refresh.
My immediate thought was to use a global click event handler for all anchors and validate whether or not I want to allow the click, which would work for in-site link navigation. However, where this falls over is navigating via the browsers Back/Forward buttons.
I also had a look at Backbone.routefilter, which at first glance appeared to do exactly what I needed. However, using the simple case as described in the docs, the route was still being executed.
The question
How do we intercept navigation for all scenarios within a Backbone SPA?
Direct link navigation
Use a global event handler to capture all click events
$(document).on('click', 'a[href^="/"]', function (e) {
var href = $(e.currentTarget).attr('href');
e.preventDefault();
if (doSomeValidation()) {
router.navigate(href, { trigger: true });
}
});
Page refreshing / external URL navigation
Handle the onbeforeunload event on the window
$(window).on('beforeunload', function (e) {
if (!doSomeValidation()) {
return 'Leaving now will may result in data loss';
}
});
Browser back/forward button navigation
Behind the scenes Backbone.Router uses the Backbone.history which ultimately leverages the HTML5 pushstate API. Depending on what options you pass to Backbone.history.start, and what your browser is capable of, the API will hook into either the onhashchange event or the onpopstate event.
Delving into the source for Backbone.history.start it becomes apparent that regardless of whether you are using push state or not, the same event handler is used i.e. checkUrl.
if (this._hasPushState) {
addEventListener('popstate', this.checkUrl, false);
} else if (this._wantsHashChange && this._hasHashChange && !this.iframe) {
addEventListener('hashchange', this.checkUrl, false);
} else if (this._wantsHashChange) {
this._checkUrlInterval = setInterval(this.checkUrl, this.interval);
}
Therefore, we can override this method & perform our validation in there
var originalCheckUrl = Backbone.history.checkUrl;
Backbone.history.checkUrl = function (e) {
if (doSomeValidation()) {
return originalCheckUrl.call(this, e);
} else {
// re-push the current page into the history (at this stage it's been popped)
window.history.pushState({}, document.title, Backbone.history.fragment);
// cancel the original event
return false;
}
};

AngularJS: $previousState unaware of prevented state transition during $stateChangeStart

I have an AngularJS application, that uses ui-router and ui-router-extras, where some transitions between states are intercepted and redirected to another ones.
So basically, when a $stateChangeStart event is triggered, the app checks if that transition is allowed or needs to be blocked and then redirect the user somewhere else (eg: a login dialog, a missing step in the process, etc...). Transitions can be also cancelled.
The interception works this way:
$rootScope.$on("$stateChangeStart", function(e,to,toParams,from,fromParams){
if( <mustBeIntercepted> || <mustBeCancelled> ){
// this prevents the actual state change from happening
e.preventDefault();
if( <redirectionRequired> ){
$state.go( <stateName>, <stateParams>);
}
}
});
And this works nicely so far. The problem is that when cancelling a state change, $previousState is not aware of the cancellation, and registers the current one as previous, in the same way that if you do not cancel the transition. So basically $previousState is unaware of the transition cancellation. This causes that when trying to return to the previous state programmatically, it does not move because $state == $previousState.
I created a live demo here: http://plnkr.co/edit/dXjj2iSwDU1DtPMuqW1W?p=preview
Basically, you click on Step1, and then on Confirm, the app will cancel the transition and prompt you if you want to confirm. If you confirm, then it will transit to the originally desired state. Note that in the real app this is not done with confirm but with a custom overlay that cannot block for an answer in the same way that confirm does.
As you will notice, in the top bar there are two labels that show the content of $state and $previousState. If you cancel the confirm prompt, you will see that $previousState changes to "Step1" despite of that the transition didn't happen.
How can I avoid this?
The bug was reported and it has been resolved: https://github.com/christopherthielen/ui-router-extras/issues/120

Wait until logout is done

Controller in my default route gets executed after I logout. For loginG out I am just calling a server route using $http.post. As the thing is async, execution continues to go to my default route '\' but I don't want controller of my default route to execute until logout is complete. How can I create a link between logout and my controller? I know I can use promise to wait for Logout but this would be only in the function where I am calling logout. How can I wait in my controller for logout to finish?
Will appreciate any help.
Thanks.
Promises let you add a handler to their completion that's chainable:
// inside your controller action causing the log out:
$http.post("/logout").done(function(result){
// here you're logged out, the promise has its value
$location.href = "/"; // change route
});
Note: you can also wait in your router, but I assume from your question you'd like to avoid that.
Note2: I'd extract membership into a service.

Handing cold navigation to internal URL in Backbone?

I'm wondering how people handle the following case in Backbone: usually when a user navigates to the root of your app, a certain set of data is loaded from the backend, processed right away and then displayed in the DOM.
There are links in the app that will navigate you to different sub-sections of it. The router catches the navigation and replaces the current page with whatever page you navigated to, all based on the same data that's already been fetched.
The problem is that the user could bookmark that internal/secondary URL and navigate to it "cold", as in before the data has had a chance to be fetched, without going through the root URL. Is there an idiomatic/conventional way of handling that situation (in the router, I'm assuming)?
One way is, in the various router path-handling functions, to always call a method that will check if there's sufficient data to complete the operation, and if not, fetch it and then proceed?
Backbone won't hit the initial route in your router before you call Backbone.history.start, so you can delay it until you've done the necessary setup. I typically define a start method on my application's main router. Looks something like:
var AppRouter = Backbone.Router.extend({
start: function() {
//init code here
something.fetch({success: function() {
//only call history start after the necessary initial setup is done
Backbone.history.start();
}});
}
});
And then start the application using that method:
window.app = new AppRouter();
window.app.start();
It's good to remember that there is nothing confining you to build your application using only the predefined pieces provided by Backbone. If your startup code is heavy, it may not belong to the router. In such case you should define a helper function to encapsulate the startup logic, and leave the router out of it altogether:
//startup.js
function startup(onComplete) {
//do initialization stuff...
onComplete();
});
//main.js
startup(function() {
Backbone.history.start();
});

Resources