$state.go not working while testing resolve in ui-router - angular-ui-router

$state.go isn't transitioning to default.settings.customization but instead its going to our 'main frontpage' default.feed
I think it has to do with the resolve, since when I change var state to a different state, it works just fine.
app.config(['$stateProvider', function($stateProvider) {
$stateProvider
.state('default.settings.customization', {
url: '/settings/customization',
templateUrl: 'app/settings/customization/customization.html',
controller: 'CustomizationCtrl',
auth: {
authorizedRoles: ['admin']
},
resolve: {
customization: [
'CustomizationService',
function(CustomizationService) {
return CustomizationService.get().then(function(res) {
return res.data;
});
}
]
}
});
}]);
Test:
var state = 'default.settings.customization';
it('should resolve data', function() {
customizationServiceMock.get = jasmine.createSpy('customizationServiceMockGet')
.and.returnValue(apiResp.accountCustomization);
$state.go(state);
$rootScope.$apply();
expect($state.current.name).toBe(state);
});
LOG:
PhantomJS 1.9.8 (Linux) app/settings/customization router should resolve data FAILED
Expected 'default.feed' to be 'default.settings.customization'.
Error: Expected 'default.feed' to be 'default.settings.customization'.

I think it's related to your spy function, check that apiResp is available in your scope. I had a similary problem, I used $q to return a promise in my spy function but $q was not injected.

Related

How to stub a call to graphql using cypress?

I'm writing a Vue app that uses vue-apollo to interact with graphql. I'm wondering if it's possible to stub the graphql requests. I thought this should work:
it('should access a story', function() {
cy.server();
cy.route('http://localhost:3002/graphql', {
data: {
Story: { id: 2, title: 'story title', content: 'story content' }
}
});
cy.visit('/stories/2');
});
Unfortunately, I get an error from graphql complaining that id is an Int instead of an ObjectId. Am I missing something?
The problem was that stubbing fetch requests isn't yet implemented in Cypress (which is what Vue Apollo is using). I ended up following these instructions:
Install github/fetch
Add this to cypress/support/index.js:
.
Cypress.on('window:before:load', win => {
win.fetch = null;
win.Blob = null;
});
Now it works!
I got it working with this package here:
npm i #iam4x/cypress-graphql-mock
Add this line to 'support/commands.js'
import "#iam4x/cypress-graphql-mock";
go to your graphiql playground and download your schema
add task command to 'plugins/index.js' (REMEMBER TO CHANGE PATH TO SCHEMA FILE YOU DOWNLOADED EARLIER)
module.exports = (on, config) => {
on("task", {
getSchema() {
return fs.readFileSync(
path.resolve(__dirname, "../../../schema.graphql"),
"utf8"
);
}
});
};
write your tests with loaded schema
beforeEach(() => {
cy.server();
cy.task("getSchema").then(schema => {
cy.mockGraphql({
schema
});
});
});`
describe("Login Form", () => {
it("should redirect after login", () => {
cy.mockGraphqlOps({
operations: {
Login: {
login: {
jwt: "some-token",
user: {
id: "5d5a8e1e635a8b6694dd7cb0"
}
}
}
}
});
cy.visit("/login");
cy.getTestEl("email-input").type("Max Mustermann");
cy.getTestEl("password-input").type("passwort");
cy.getTestEl("submit").click();
cy.getTestEl("toolbar-title").should("exist");
});
})
Visit the original repo for further explanation as i find it less confusing. The package you have installed is just a working fork of this one:
https://github.com/tgriesser/cypress-graphql-mock

How to send the success of a test to testingBot from a Protractor project?

Following the testingBot example for protractor-based projects I got this code
var TestingBot = require('testingbot-api');
describe('Protractor Demo App', function () {
var tb;
beforeEach(function () {
tb = new TestingBot({
api_key: "master_key",
api_secret: "secret_007"
});
});
afterEach(function () {
browser.getSession().then(function (session) {
tb.updateTest({
'test[success]': true/*where do I get this 'test[success]' attribute? */
}, session.getId(), function () {
console.log("Hi! :D");
});
})
});
it('should have a title', function () {
browser.get('http://juliemr.github.io/protractor-demo/');
expect(browser.getTitle()).toEqual('Super Calculator');
});
});
I need to send the success of the test back through the tb.updateTest() but I don't know where I get the value of a passed or failed test. For now the value is a static true. I'd appreciate a jasmine approach too.
You can use a custom reporter with Jasmine.
There you can hook into specDone or suiteDone which has a result parameter, containing the test's success state.
You can then use this state to write a custom report or send it to somewhere else.

Async call in beforeAll

Here are 2 samples of the same test. The only difference is that first one uses a promise in beforeAll block to assign a value to the variable while the second one assigns the value directly.
I raised a similar question Running spec after promise has been resolved with one of the comments pointing to this issue https://github.com/jasmine/jasmine/issues/412 which says that this is not supported in Jasmine. Has somebody figured out any workaround?
This fails with TypeError: Cannot read property 'forEach' of undefined
describe('Async car test', function () {
var cars;
beforeAll(function (done) {
// getCars() is a promise which resolves to ['audi', 'bmw']
getCars().then(function (data) {
cars = data;
console.log(cars) // ['audi', 'bmw']
done();
});
});
cars.forEach(function (car) {
it('car ' + car, function () {
expect(car).toBe(car);
});
});
});
This works fine
describe('Car test', function () {
var cars = ['audi', 'bmw'];
cars.forEach(function (car) {
it('car ' + car, function () {
expect(car).toBe(car);
});
});
});
Posting it as an answer, because I can't see things properly in comments.
I'm actually generating tests in my spec as well, and I'm using https://www.npmjs.com/package/jasmine-data-provider , I think you probably cannot generate it directly from resolved promise. And wrapping in another it doesn't work for you. This should work:
var using = require('jasmine-data-provider');
using(cars.forEach, function (car) {
it(car + ' should be' + car, function () {
expect(car).toBe(car);
});
});
This is not an issue with jasmine, it is an issue with your code.
beforeAll does not block subsequent code below the statement. it blocks code that is defined in it('should ...', (done)=>{...});
it('should have cars', (done) => {
cars.forEach(function (car) {
expect(car).toBe(car);
});
});
Since Jasmine does not support adding tests at runtime, the trick is to request the asynchronous data before starting Jasmine, and then using the retrieved data during runtime instead. This can be achieved with a singleton and programmatically starting Jasmine.
See here for a working example.
// car-collection.js
class CarCollection {
static load() {
return this.request()
then((data) => this.cars = data);
}
static request() {
// in practice this function would do something cooler
return Promise.resolve(['audi', 'bmw']);
}
}
modules.export = CarCollection;
Since CarCollection has methods that are static they will be shared across imports and this.cars will persist.
// launcher.js
const Jasmine = require('jasmine');
const CarCollection = require('./car-collection');
CarCollection.load()
.then(() => {
console.log(`car count is ${CarCollection.cars.length}`); // prints: car count is 2
const jasmine = new Jasmine();
jasmine.loadConfigFile(...); // path to jasmine.json
jasmine.execute();
});
An important step here is configure jasmine to know where to look for the test files. Either by loading a config or passing specifics into the execute function.
// car.spec.js
const CarCollection = require('./car-collection');
describe('test', function () {
CarCollection.cars.forEach((car) => {
it('test' + car, () => {
expect(car).toBe(car);
});
});
});
Now run node ./launcher.js and the tests should run.

How to refresh content when using CrossroadJS and HasherJS with KnockoutJS

I was following Lazy Blogger for getting started with routing in knockoutJS using crossroads and hasher and it worked correctly.
Now I needed to refresh the content using ajax for Home and Settings page every time they are clicked. So I googled but could not find some useful resources. Only these two links
Stack Overflow Here I could not understand where to place the ignoreState property and tried these. But could not make it work.
define(["jquery", "knockout", "crossroads", "hasher"], function ($, ko, crossroads, hasher) {
return new Router({
routes:
[
{ url: '', params: { page: 'product' } },
{ url: 'log', params: { page: 'log' } }
]
});
function Router(config) {
var currentRoute = this.currentRoute = ko.observable({});
ko.utils.arrayForEach(config.routes, function (route) {
crossroads.addRoute(route.url, function (requestParams) {
currentRoute(ko.utils.extend(requestParams, route.params));
});
});
activateCrossroads();
}
function activateCrossroads() {
function parseHash(newHash, oldHash) {
//crossroads.ignoreState = true; First try
crossroads.parse(newHash);
}
crossroads.normalizeFn = crossroads.NORM_AS_OBJECT;
hasher.initialized.add(parseHash);
hasher.changed.add(parseHash);
hasher.init();
$('a').on('click', function (e) {
crossroads.ignoreState = true; //Second try
});
}
});
Crossroads Official Page Here too I could not find where this property need to be set.
If you know then please point me to some url where I can get more details about this.

How to set mandatory route parameters

I want to make a route with has a mandatory parameter. If not, it should fall into
$urlRouterProvider.otherwise("/home");
Current route:
function router($stateProvider) {
$stateProvider.state("settings", {
url: "^/settings/{id:int}",
views: {
main: {
controller: "SettingsController",
templateUrl: "settings.html"
}
}
});
}
Currently both the routes below are valid:
http://myapp/settings //Should be invalid route
http://myapp/settings/123
Any ideas?
Use a state change start listener to check if params were passed:
$rootScope.$on('$stateChangeStart',
function (event, toState, toParams, fromState, fromParams) {
if(toState.name==="settings")
{
event.preventDefault(); //stop state change
if (toParams.id===undefined)
$state.go("home");
else
$state.go(toState, toParams);
}
});
The following solution is valid for ui-router 1.0.0:
.config(($stateProvider, $transitionsProvider) => {
//Define state
$stateProvider.state('verifyEmail', {
parent: 'portal',
url: '/email/verify/:token/:optional',
component: 'verifyEmail',
params: {
token: {
type: 'string',
},
optional: {
value: null,
squash: true,
},
},
});
//Transition hooks
$transitionsProvider.onBefore({
to: 'verifyEmail',
}, transition => {
//Get params
const params = transition.params();
//Must have token param
if (!params.token) {
return transition.router.stateService.target('error', {
type: 'page-not-found',
});
}
});
})
The above will make the :token parameter mandatory and the :optional parameter optional. If you try to browse to the page without the token parameter it will fail the transition and redirect to your error page. If you omit the :optional parameter however, it will use the default value (null).
Remember to use squash: true on the trailing optional parameters, because otherwise you'll also get a 404 if you omit the trailing / in the URL.
Note: the hook is required, because if you browse to email/verify/ with a trailing slash, ui-router will think the token parameter is an empty string. So you need the additional handling in the transition hook to capture those cases.
In my app I had to make required parameters for a lot of routes. So I needed a reusable and DRY way to do it.
I define a constants area in my app to access global code. I use for other things as well.
I run this notFoundHandler at app config time. This is setting up a router state for handling errors. It is setting the otherwise route to this error route. You could define a different route for when a required parameter is missing, but for us this was defined as being the same as a 404 experience.
Now at app run time I also define a stateChangeErrorHandler which will look for a rejected route resolve with the 'required-param' string.
angular.module('app')
.constant('constants', constants)
.config(notFoundHandler)
.run(stateChangeErrorHandler);
// use for a route resolve when a param is required
function requiredParam(paramName) {
return ['$stateParams', '$q', function($stateParams, $q) {
// note this is just a truthy check. if you have a required param that could be 0 or false then additional logic would be necessary here
if (!$stateParams[paramName]) {
// $q.reject will trigger the $stateChangeError
return $q.reject('required-param');
}
}];
}
var constants = {
requiredParam: requiredParam,
// define other constants or globals here that are used by your app
};
// define an error state, and redirect to it if no other route matches
notFoundHandler.$inject = ['$stateProvider', '$urlRouterProvider'];
function notFoundHandler($stateProvider, $urlRouterProvider) {
$stateProvider
//abstract state so that we can hold all our ingredient stuff here
.state('404', {
url: '/page-not-found',
views: {
'': {
templateUrl: "/app/error/error.tpl.html",
}
},
resolve: {
$title: function () { return 'Page Not Found'; }
}
});
// redirect to 404 if no route found
$urlRouterProvider.otherwise('/page-not-found');
}
// if an error happens in changing state go to the 404 page
stateChangeErrorHandler.$inject = ['$rootScope', '$state'];
function stateChangeErrorHandler($rootScope, $state) {
$rootScope.$on('$stateChangeError', function(evt, toState, toParams, fromState, fromParams, error) {
if (error && error === 'required-param') {
// need location: 'replace' here or back button won't work on error page
$state.go('404', null, {
location: 'replace'
});
}
});
}
Now, elsewhere in the app, when I have a route defined, I can make it have a required parameter with this route resolve:
angular.module('app')
.config(routeConfig);
routeConfig.$inject = ['$stateProvider', 'constants'];
function routeConfig($stateProvider, constants) {
$stateProvider.state('app.myobject.edit', {
url: "/:id/edit",
views: {
'': {
template: 'sometemplate.html',
controller: 'SomeController',
controllerAs: 'vm',
}
},
resolve: {
$title: function() { return 'Edit MyObject'; },
// this makes the id param required
requiredParam: constants.requiredParam('id')
}
});
}
I'd like to point out that there shouldn't be any problem with accessing the /settings path, since it doesn't correspond to any state, unless you've used inherited states (see below).
The actual issue should happen when accessing the /settings/ path, because it will assign the empty string ("") to the id parameter.
If you didn't use inherited states
Here's a solution in plunker for the following problem:
accessing the /state_name/ path, when there's a state with url /state_name/:id
Solution explanation
It works through the onBefore hook (UI router 1.x or above) of the Transition service, which prevents transitioning to states with missing required parameters.
In order to declare which parameters are required for a state, I use the data hash like this:
.state('settings', {
url: '/settings/:id',
data: {
requiredParams: ['id']
}
});
Then in app.run I add the onBefore hook:
transitionService.onBefore({}, function(transition) {
var toState = transition.to();
var params = transition.params();
var requiredParams = (toState.data||{}).requiredParams || [];
var $state = transition.router.stateService;
var missingParams = requiredParams.filter(function(paramName) {
return !params[paramName];
});
if (missingParams.length) {
/* returning a target state from a hook
issues a transition redirect to that state */
return $state.target("home", {alert: "Missing params: " + missingParams});
}
});
If you used inherited states
You could implement the same logic via inherited states:
function router($stateProvider) {
$stateProvider
.state('settings', {
url: '/settings'
})
.state('settings.show", {
url: '/:id'
});
}
then you'd need to add the abstract property to the parent declaration, in order to make /settings path inaccessible.
Solution explanation
Here's what the documentation says about the abstract states:
An abstract state can never be directly activated. Use an abstract state to provide inherited properties (url, resolve, data, etc) to children states.
The solution:
function router($stateProvider) {
$stateProvider
.state('settings', {
url: '/settings',
abstract: true
})
.state('settings.show", {
url: '/:id'
});
}
Note: that this only solves the issue with /settings path and you still need to use the onBefore hook solution in order to also limit the access to /settings/.
it is not very well documented, but you can have required and optional parameters, and also parameters with default values.
Here is how you can set required params:
function router($stateProvider) {
$stateProvider.state("settings", {
url: "^/settings/{id:int}",
params: {
id: {}
},
views: {
main: {
controller: "SettingsController",
templateUrl: "settings.html"
}
}
});
}
I never used params with curly brackets, just with the semicolon, like this url: "^/settings/:id", but from what I read, those are equivalent.
For other types of parameters, please see the other half of my answer here: AngularJS UI Router - change url without reloading state
Please note that when I added that answer, I had to build ui-router from source, but I read that functionality has been added to the official release by now.

Resources