ember: promise for a controller action that calls a service method - ember.js

I have an action on my controller that calls a service method. The service method is an ember-data query. I need to return a message contained in that ember-data payload back to the controller (print it to the screen).
I am having a hard time figuring out how to get the controller action (function) to "wait" for the service method to finish.
The controller action:
// controller action
processCoupon() {
// the service method I want to wait for the response for
let messageObject = DS.PromiseObject.create({
promise: this.get('cart').processCoupon()
});
// the message
messageObject.then(response => {
let promo_message = messageObject.get('promo_message');
if (promo_message.message && promo_message.alert) {
if (!promo_message.success) {
// show error message in alert with ember-cli-notifcations
} else {
// show success message in alert with ember-cli-notifcations
}
}
});
},
Method in the service I want to wait for the response for:
// service method syncs cart info (containing promo) with the backend
// promo_message is in the response payload
processCoupon() {
return this.get('store').findRecord('cart', get(this, 'cartObj.id')).then(cart => {
cart.save().then(newCart => {
set(this, 'cartObj', newCart); // sets response to property on service
return newCart.get('promo_message');
});
});
},
the 'response' in the promise is empty, and the MessageObject itself has no content. So I'm doing something wrong here (and it's likely misunderstanding promises).
I messed around with RSVP promises and didn't do well there either. What am I missing, OR is there a better way to do this?

Your service method should return a promise. Then you can use it like this: this.get('cart').process().then((response) => {/*your code working with service's response*/});
You also should be aware that if you use ember data, it will return a model instance, not an original response from your back-end.
And, in order to return promise, you need to wrap your service's method in new Promise((resolve, reject) => {/*asynchronous code here*/});

I've done something similar with Ember.RSVP.Promise() is that what you want?
// controller
myMessage = null,
actions:
cartCoupon() {
let msg = this.get('cart').processCoupon();
msg.then(myDataMessage => this.set('myMessage', myDataMessage);
}
//service
processCoupon() {
return new Ember.RSVP.Promise( resolve => {
let data = this.get('store').findRecord('cart', get(this, 'cartObj.id')).then(cart => {
cart.save().then(newCart => {
set(this, 'cartObj', newCart); // sets response to property on service
return newCart.get('promo_message');
});
});
resolve(data);
});
}

Related

Angular 6 - Jasmine - mock httpClient get map and error flows

I'm new to Angular testing and am trying to figure out how to write a test that mocks an error response of HttpClient.get() function. Basically my service has both map() and catchError() inside of its pipe() and I would like to excercise both flows. Here's what I have so far:
my.service.ts:
getItems(): Observable<ItemViewModels[]> {
return
this.httpClient.get<any>(this.getItemsUrl)
.pipe(
map(json => {
return json.map(itemJson => this.getVmFromItemJson(itemJson));
}),
catchError(() => {
// Log stuff here...
return of(null);
})
);
}
my.service.spec.ts:
it('should catch error and return null if API returns error', () => {
spyOn(service.httpClient, 'get').and.returnValue(new Observable()); // Mock error here
service.getItems().subscribe($items => expect($items).toBe(null));
});
it('should return valid view model array if API returns a valid json', () => {
const mockResponse = [
new SideNavItemViewModel(1),
new SideNavItemViewModel(2),
new SideNavItemViewModel(3)
];
spyOn(service.httpClient, 'get').and.returnValue(of(JSON.stringify(mockResponse)));
service.getSidenavViewModel().subscribe(x => expect(x).toBe(mockResponse));
});
So the actual issue is that the observables that I mock for the httpClient to return on get in the unit tests don't seem to get into the .pipe() function, which means that my tests aren't working :(
Any ideas?
Thanks!
Have you tried injecting your service into the test? I also try test the function that subscribes to the api call instead of creating another subscribe:
for errors:
it('should display error when server error occurs',
inject([HttpTestingController, AService],
(httpMock: HttpTestingController, svc: MyService) => {
svc.getItems(); // has the subscribe in it
const callingURL = svc['API']; // your api call eg. data/items
httpMock.expectOne((req: HttpRequest < any > ) => req.url.indexOf(callingURL) !== -1)
.error(new ErrorEvent('Customer Error', {
error: 500
}), {
status: 500,
statusText: 'Internal Server Error'
});
httpMock.verify();
expect(component.svc.Jobs).toBeUndefined();
fixture.detectChanges();
// UI check here
}));
data test
it('should display the correct amount of data elements',
inject([HttpTestingController, AService],
(httpMock: HttpTestingController, svc: MyService) => {
svc.getItems(); // has the subscribe in it
const callingURL = svc['API']; // your api call eg. data/items
const mockReq = httpMock.expectOne((req: HttpRequest < any > ) => req.url.indexOf(callingURL) !== -1);
mockReq.flush(mockData);
httpMock.verify();
expect(component.svc.data.length).toBe(mockData.length);
fixture.detectChanges();
// UI check here
}));
So basically these functions:
call your get and subscribe
checks that your api url is contained in the http call
mocks the response - you pass in the data - 'mockData'
mockReq.flush(mockData); will trigger the call
httpMock.verify(); will check the url and other things
now the service data can be tested - if you subscribe sets anything in there
fixture.detectChanges(); - then this will allow to test ui components
I prefer this way because you can keep your logic and tests separate.

Using fetch inside an action within my component

I'm curious about how I could implement this, I'd like to not hit this API every time the page loads on the route, but would rather start the call on an action (I suppose this action could go anywhere, but it's currently in a component). I'm getting a server response, but having trouble getting this data inside my component/template. Any ideas? Ignore my self.set property if I'm on the wrong track there....Code below..Thanks!
import Component from '#ember/component';
export default Component.extend({
res: null,
actions: {
searchFlight(term) {
let self = this;
let url = `https://test.api.amadeus.com/v1/shopping/flight-offers?origin=PAR&destination=LON&departureDate=2018-09-25&returnDate=2018-09-28&adults=1&travelClass=BUSINESS&nonStop=true&max=2`;
return fetch(url, {
headers: {
'Content-Type': 'application/vnd.amadeus+json',
'Authorization':'Bearer JO5Wxxxxxxxxx'
}
}).then(function(response) {
self.set('res', response.json());
return response.json();
});
}
}
});
Solved below...
import Component from '#ember/component';
export default Component.extend({
flightResults: null,
actions: {
searchFlight(term) {
let self = this;
let url = `https://test.api.amadeus.com/v1/shopping/flight-offers?origin=PAR&destination=LON&departureDate=2018-09-25&returnDate=2018-09-28&adults=1&travelClass=BUSINESS&nonStop=true&max=2`;
return fetch(url, {
headers: {
'Content-Type': 'application/vnd.amadeus+json',
'Authorization':'Bearer xxxxxxxxxxxxxx'
}
}).then(function(response) {
return response.json();
}).then(flightResults => {
this.set('flightResults', flightResults);
});
}
}
});
You might find ember-concurrency to be useful in this situation. See the example of "Type-ahead search", modified for your example:
const DEBOUNCE_MS = 250;
export default Controller.extend({
flightResults: null;
actions: {
searchFlight(term) {
this.set('flightResults', this.searchRepo(term));
}
},
searchRepo: task(function * (term) {
if (isBlank(term)) { return []; }
// Pause here for DEBOUNCE_MS milliseconds. Because this
// task is `restartable`, if the user starts typing again,
// the current search will be canceled at this point and
// start over from the beginning. This is the
// ember-concurrency way of debouncing a task.
yield timeout(DEBOUNCE_MS);
let url = `https://test.api.amadeus.com/v1/shopping/flight-offers?origin=PAR&destination=LON&departureDate=2018-09-25&returnDate=2018-09-28&adults=1&travelClass=BUSINESS&nonStop=true&max=2`;
// We yield an AJAX request and wait for it to complete. If the task
// is restarted before this request completes, the XHR request
// is aborted (open the inspector and see for yourself :)
let json = yield this.get('getJSON').perform(url);
return json;
}).restartable(),
getJSON: task(function * (url) {
let xhr;
try {
xhr = $.getJSON(url);
let result = yield xhr.promise();
return result;
// NOTE: could also write this as
// return yield xhr;
//
// either way, the important thing is to yield before returning
// so that the `finally` block doesn't run until after the
// promise resolves (or the task is canceled).
} finally {
xhr.abort();
}
}),
});

Test async middleware in redux with thunk

I have a middleware that waits for a ARTICLE_REQUEST action, performs a fetch and dispatches either an ARTICLE_SUCCESS or an ARTICLE_FAILURE action when fetch is done. Like so
import { articleApiUrl, articleApiKey } from '../../environment.json';
import { ARTICLE_REQUEST, ARTICLE_SUCCESS, ARTICLE_FAILURE } from '../actions/article';
export default store => next => action => {
// Prepare variables for fetch()
const articleApiListUrl = `${articleApiUrl}list`;
const headers = new Headers({ 'Content-Type': 'application/json', 'x-api-key': articleApiKey });
const body = JSON.stringify({ ids: [action.articleId] });
const method = 'POST';
// Quit when action is not to be handled by this middleware
if (action.type !== ARTICLE_REQUEST) {
return next(action)
}
// Pass on current action
next(action);
// Call fetch, dispatch followup actions and return Promise
return fetch(articleApiListUrl, { headers, method, body })
.then(response => response.json());
.then(response => {
if (response.error) {
next({ type: ARTICLE_FAILURE, error: response.error });
} else {
next({ type: ARTICLE_SUCCESS, article: response.articles[0] });
}
});
}
I really wonder how to test this async code. I want to see if the follow-up actions will be dispatched properly and maybe if the fetch call gets invoked with the proper URL and params. Can anyone help me out?
PS: I am using thunk although I am not absolutely sure of its function as I just followed another code example
You can mock the fetch() function like so:
window.fetch = function () {
return Promise.resolve({
json: function () {
return Prommise.resolve({ … your mock data object here … })
}
})
}
Or you wrap the entire middleware in a Function like so:
function middlewareCreator (fetch) {
return store => next => action => { … }
}
and then create the middleware with the actual fetch method as parameter, so you can exchange it for tests or production.

Ember promise isPending not firing if thenable

I want to show a loading spinner in an ember component when I load some data from the store. If there is an error from the store, I want to handle it appropriately.
Here is a snippet of my current code:
cachedCampaignDataIsPending: computed.readOnly('fetchCachedCampaignData.isPending'),
fetchCachedCampaignData: computed('campaign', function() {
return this.get('store').findRecord('cachedCampaignMetric').then( (cachedMetrics) => {
this.set('cachedCampaignData', cachedMetrics);
}, () => {
this.set('errors', true);
});
}),
This will properly set the errors to true if the server responds with an error, however, the cachedCampaignDataIsPending is not acting properly. It does not get set to true.
However, if I rewrite my code to make the computed property not be thenable, then it does fire propertly.
fetchCachedCampaignData: computed('campaign', function() {
return this.get('store').findRecord('cachedCampaignMetric');
}),
Anyone know the reason why, or how to make it fire properly, using the then/catch logic?
Thanks.
Update
I found that this works for me and gives me what I need.
cachedCampaignDataIsPending: computed.readOnly('fetchCachedCampaignData.isPending'),
fetchCachedCampaignData: computed('campaign', function() {
let promise = this.get('store').findRecord('cachedCampaignMetric');
promise.then( (cachedMetrics) => {
this.set('cachedCampaignData', cachedMetrics);
}, () => {
this.set('errors', true);
});
return promise;
}),

How to continue even if Ember.js model hook doesn't load all promises?

I'm loading a route. Its model hook loads some models. Some are fetch from ember store and some are promises requested through AJAX:
model: function () {
return Em.RSVP.hash({
//the server data might not be loaded if user is offline (application runs using appcache, but it's nice to have)
someServerData: App.DataService.get(),
users: this.store.find('user')
});
}
The App.DataService.get() is defined as:
get: function () {
return new Ember.RSVP.Promise(function(resolve, reject) {
//ajax request here
});
}
Obviously if the request is rejected, the flow is interrupted and I cannot display the page at all.
Is there a way to overcome this?
Ember.RSVP.hashSettled is exactly meant for this purpose.
From tildeio/rsvp.js Github repository:
hashSettled() work exactly like hash(), except that it fulfill with a hash of the constituent promises' result states. Each state object will either indicate fulfillment or rejection, and provide the corresponding value or reason. The states will take one of the following formats:
{ state: 'fulfilled', value: value }
or
{ state: 'rejected', reason: reason }
Here is an example for using it (working JS Bin example):
App.IndexRoute = Ember.Route.extend({
fallbackValues: {
firstProperty: null,
secondProperty: null
},
model: function() {
var fallbackValues = this.get('fallbackValues');
return new Ember.RSVP.Promise(function(resolve, reject) {
Ember.RSVP.hashSettled({
firstProperty: Ember.RSVP.Promise.resolve('Resolved data despite error'),
secondProperty: (function() {
var doomedToBeRejected = $.Deferred();
doomedToBeRejected.reject({
error: 'some error message'
});
return doomedToBeRejected.promise();
})()
}).then(function(result) {
var objectToResolve = {};
Ember.keys(result).forEach(function(key) {
objectToResolve[key] = result[key].state === 'fulfilled' ? result[key].value : fallbackValues[key];
});
resolve(objectToResolve);
}).catch(function(error) {
reject(error);
});
});
}
});
fallbackValues can be useful for managing resolved hash's properties' fallback values without using conditions inside the promise function.
Taking into account that Ember.RSVP.hashSettled is not available in my Ember version. I come up with the following solution:
model: function(params) {
var self = this;
return new Em.RSVP.Promise(function(resolve, reject){
// get data from server
App.DataService.get().then(function(serverData) { //if server responds set it to the promise
resolve({
serverData: serverData,
users: self.store.find('user')
});
}, function(reason){ //if not ignore it, and send the rest of the data
resolve({
users: self.store.find('user')
});
});
});
}