Waiting for the response of several promises in EmberJS - ember.js

I'm trying to make 3 API calls and then return an array of all the data. However, the console.log() (and therefore the function return) is empty as it's not waiting for the AJAX call to be resolved - but I can't do it in the loop as I need all the data.
let data = [];
parameters.forEach((parameter, index) => {
return Ember.$.ajax(url).then((response) => {
data.push({
name: parameter.get('displayName'),
color: parameter.get('color'),
type: chart.get('chartType'),
turboThreshold: 0,
data: response.data
});
});
});
console.log(data);
return data;
I think that I can use Ember.RSVP.hash() for this but I can't seem to get it to work... can anyone point me in the right direction?

Return a promise and resolve it when all of the inner promises are resolved. I didn't try out the code, But this should point you how to proceed.
return new Ember.RSVP.Promise(function (resolve) { //return a promise
let promises = [];
let data = [];
parameters.forEach((parameter, index) => {
promises[index] = Ember.$.ajax(url).then((response) => { //store all inner promises
data.push({
name: parameter.get('displayName'),
color: parameter.get('color'),
type: chart.get('chartType'),
turboThreshold: 0,
data: response.data
});
});
});
Ember.RSVP.all(promises).then(function(){
resolve(data); //resolve the promise when all inner promises are resolved
});
});

Actually you can't wait for promises unless you are in the routes' model hooks.
See model hooks on the guide
You have another option, try to set data directly to the item when data is resolved. (inside the then function)

You could try something like this :
return Promise.all(parameters.map(parameter => Ember.$.ajax(url).then(response => {
'name': parameter.get('displayName'),
'color': parameter.get('color'),
'type': chart.get('chartType'),
'turboThreshold': 0,
'data': response.data
}))).then(data => {
console.log(data);
return data;
});

Related

Cannot test AsyncTypeahead from react-bootstrap-typeahead with Enzyme

I am trying to test AsyncTypeahead from react-bootstrap-typeahead.
I have a very simple test component :
class AsyncTypeahead2 extends Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
isLoading: false,
};
}
render() {
return ( <AsyncTypeahead
isLoading={this.state.isLoading}
onSearch={query => {
this.setState({isLoading: true});
fetch("http://www.myHTTPenpoint.com")
.then(resp => resp.json())
.then(json => this.setState({
isLoading: false,
options: json.items,
}));
}}
options={this.state.options}
labelKey={option => `${option.stateName}`}
/> )
}
}
const url = "http://www.myHTTPenpoint.com"
fetchMock
.reset()
.get(
url,
{
items: [
{id:1, stateName:"Alaska"},
{id:2, stateName:"Alabama"}
]
},
);
(Note that the URL is mocked to return two elements)
When I run this in my storybook it looks fine :
But if I want to test it (with Enzyme) it does not recognise the < li > items that pop up.
let Compoment =
<div>Basic AsyncTypeahead Example
<AsyncTypeahead2/>
</div>
const wrapper = mount(Compoment);
let json = wrapper.html();
let sel = wrapper.find(".rbt-input-main").at(0)
sel.simulate('click');
sel.simulate('change', { target: { value: "al" } });
expect(wrapper.find(".rbt-input-main").at(0).getElement().props.value).toBe("al")
expect(wrapper.find(".dropdown-item").length).toBe(2) //but get just 1 element "Type to Search..."
Instead of finding two "dropdown-item" items there is just one item with the text "Type to Search...".
Is the AynchTypeahead not updating the DOM correctly with respect to Enzyme?
<AsyncTypeahead> is asynchronous. On the other hand simulate() is synchronous. So at the time you get to expect() AsyncTypeahead not even started to populate the dropdown with <li> elements. You need to wait for it.
It's not specified, but it looks like you are using fetch-mock package.
There is the flush function which
Returns a Promise that resolves once all fetches handled by fetch-mock have resolved
So this:
...
sel.simulate('click');
sel.simulate('change', { target: { value: "al" } });
await fetchMock.flush() // !!!
expect(wrapper.find(".rbt-input-main").at(0).getElement().props.value).toBe("al")
expect(wrapper.find(".dropdown-item").length).toBe(2)
should work.
...But probably it won't. Because
fetchMock.mock(...)
fetch(...)
await fetchMock.flush()
does work, but
fetchMock.mock(...)
setTimeout(() => fetch(...), 0)
await fetchMock.flush()
does not. await fetchMock.flush() returns right away if there was no call of fetch. And probably there won't be. Because <AsyncTypeahead> debounces.
(By the way, you can also try to mock fetch on a per-test basis. Just in case.)
So I see two options:
Use something else instead of fetch-mock package. Where you can resolve your own Promises on mocked requests completion.
https://tech.travelaudience.com/how-to-test-asynchronous-data-fetching-on-a-react-component-ff2ee7433d71
import waitUntil from 'async-wait-until';
...
test("test name", async () => {
let Compoment = <AsyncTypeahead2/>
...
await waitUntil(() => wrapper.state().isLoading === false);
// or even
// await waitUntil(() => wrapper.find(".dropdown-item").length === 2, timeout);
expect(...)
})
This options if not pretty. But maybe it's your only option - there is not only the fetch-mock you should worry about. setState also asynchronous... and it looks like there is no pretty way to check when it's done updating the state and the DOM without changing the real code (which is quite undesirable).
The exact solution to my problem is in the following code (copy and paste into a JS file to see it work).
Things to note :
I needed to use the waitUntil function from the async-wait-until library. fetch-mock on its own does not provide the functionality to test async code.
I needed to add an ugly hack at global.document.createRange because of some tooltip issue with react-bootstrap-typeahead and jest.
use waitUntil to wait on changes on the internal state of the component
It is very important to call wrapper.update() to update the DOM afterwards.
..
import React, {Component} from 'react';
import waitUntil from 'async-wait-until';
import {mount} from "enzyme";
import fetchMock from "fetch-mock";
import {AsyncTypeahead} from "react-bootstrap-typeahead";
describe('Autocomplete Tests ', () => {
test(' Asynch AutocompleteInput ', async () => {
class AsyncTypeaheadExample extends Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
isLoading: false,
finished: false
};
}
render() {
return (<AsyncTypeahead
isLoading={this.state.isLoading}
onSearch={query => {
this.setState({isLoading: true});
fetch("http://www.myHTTPenpoint.com")
.then(resp => resp.json())
.then(json => this.setState({
isLoading: false,
options: json.items,
finished: true
}));
}}
options={this.state.options}
labelKey={option => `${option.stateName}`}
/>)
}
}
const url = "http://www.myHTTPenpoint.com"
fetchMock
.reset()
.get(
url,
{
items: [
{id: 1, stateName: "Alaska"},
{id: 2, stateName: "Alabama"}
]
},
);
let Compoment =
<AsyncTypeaheadExample/>
// ugly hacky patch to fix some tooltip bug
// https://github.com/mui-org/material-ui/issues/15726
global.document.createRange = () => ({
setStart: () => {
},
setEnd: () => {
},
commonAncestorContainer: {
nodeName: 'BODY',
ownerDocument: document,
},
});
let wrapper = mount(Compoment);
let sel = wrapper.find(".rbt-input-main").at(0)
sel.simulate('click');
sel.simulate('change', {target: {value: "al"}});
expect(wrapper.find(".rbt-input-main").at(0).getElement().props.value).toBe("al")
//now the async stuff is happening ...
await waitUntil(() => {
return wrapper.state().finished === true;
}, 3000); //wait about 3 seconds
wrapper.update() //need to update the DOM!
expect(wrapper.find(".dropdown-item").length).toBe(2) //but get just 1 element "Type to Search..."
})
});
UPDATE
I can also compare on wrapper items rather than doing a direct comparison on the state :
//now the async stuff is happening ...
await waitUntil(() => {
wrapper.update() //need to update the DOM!
return wrapper.find(".dropdown-item").length > 1
}, 3000); //wait about 3 seconds
This is probably better because it means i dont need to know about the component internals.

How to test computed property that returns PromiseArray in Ember

I have a computed property that asks server for user data and then the other one that computes number of users. To propagate changes into the template, I'm using DS.PromiseArray wrapper. With this wrapper, I can't find an easy way to test this property.
// model
users: computed('name', function () {
let name = get(this, 'name');
return DS.PromiseArray.create({
promise: this.store.query('user', { name })
});
}),
numberOfUsers: computed('users', function () {
return get(this, 'users.length') || 0;
}),
// ....
test('numberOfUsers returns correct number', function (assert) {
let model = this.subject({
store: EmberObject.create({
query() {
return Promise.resolve([
{ name: 'Thomas' },
{ name: 'Thomas' },
{ name: 'Thomas' },
]);
}
}),
name: 'Thomas',
});
assert.equal(model.get('numberOfUsers'), 3);
});
This test fails with 0 !== 3. Any ideas?
Since model.get('users') is a promise, model.get('numberOfUsers') will not be 3 until the Promise resolves. In your test, you're immediately calling model.get('numberOfUsers'), and that is using the initial value of model.get('users'), which is an unresolved promise.
To get it to work, you could call users and put your assert inside the then of that returned promise:
model.get('users').then((user) => {
assert.equal(model.get('numberOfUsers'), 3);
})
A couple of side notes:
It is conventional in Ember to do your data fetching in the Route's model hook, not in a component like this.
Also, there's no need to manually create a PromiseArray in your application code, because an Ember Data query returns a type of Promise Array already. So you can just return this.store.query('user', { name }); (If you do this, you'll have to change your test query stub to generate a PromiseArray).

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')
});
});
});
}

Emberjs: UPDATED The response from a findQuery must be an Array, not undefined

I haved a google auto complete search box that keeps updating search.
the below code works for two searched and then I get The response from a findQuery must be an Array, not undefined.
unloading the store address and hotel data for every new search may not be a good thing. but i cannot think of any other solution for now.
Lost.HotelRoute = Ember.Route.extend({
queryParams: {
currentPlace: {
refreshModel: true
}
},
model: function (params) {
var self = this;
var hotelController = this.controllerFor('hotel');
var currentPlace = hotelController.get('currentPlace');
self.store.unloadAll('address');
self.store.unloadAll('hotel');
return this.store.find('address', {
locality: currentPlace
}).then(function (response) {
return self.store.all('hotel');
});
},
deactivate: function () {
this.controllerFor('city').set('routeNeedsAutoSearch', false);
}
});
I changed my approach.
I guess this is the a good way to do it unloading data for each search.
A better way is to user this.store.filter
so I am doing something like this now.
I may not be right I am learning so if some one suggest a better any I will select that as appropriate answer
return this.store.find('address', {
locality: currentPlace
}).then(function (response) {
response.forEach(function (item) {
addressId = item.get('id');
arr = self.store.filter('hotel', function (hotel) {
return hotel.get('address.id') == addressId;
});
});
});

Ajax without ember data - Uncaught TypeError: Object #<Object> has no method 'forEach'

I'm attempting to build a non blocking async call in an Ember.js app without using Ember Data.
I have the following Ember.js model:
App.Player = Ember.Object.extend({
id: '',
alias: '',
name: '',
twitterUserName: '',
isFeatured: ''
});
App.Player.reopenClass({
getPlayers: function () {
var players = Ember.ArrayProxy.create({ content: [] });
$.getJSON("/api/players").then(function (response) {
response.forEach(function (p) {
players.pushObject(App.Player.create(p));
});
});
return players;
}
});
And I am calling it as follows in my route:
App.IndexRoute = Ember.Route.extend({
model: function (params) {
return App.Player.getPlayers();
}
});
For some reason I am getting the following javascript error:
Uncaught TypeError: Object # has no method 'forEach'
I've tried a few variants I have seen around but nothing seems to work. Any help would be appreciated...
EDIT - Found the solution with some help from Darshan, here's the working code:
App.Player.reopenClass({
getPlayers: function () {
var players = [];
$.ajax({
url: "/api/players",
}).then(function (response) {
response.players.forEach(function (player) {
var model = App.Player.create(player);
players.addObject(model);
});
});
return players;
}
});
Your response.forEach suggests that you are expecting the json response body to be an array. It is probably wrapped in some root element like players or data like so.
{
"players": [...]
}
If that is the case you need to use forEach on that root element like response.players.forEach.
You also want to restructure that code to return a promise directly. The Ember router will then pause until your json is loaded and only proceed after it finishes. Something like this,
getPlayers: function () {
return $.getJSON("/api/players").then(function (response) {
var players = Ember.ArrayProxy.create({ content: [] });
response.players.forEach(function (p) {
players.pushObject(App.Player.create(p));
});
return players;
});
}
Returning players resolve the promise. And Ember understands that when a promise resolves that result is the model.