EmberJS - Call super action after promise resolves - javascript

I am using Ember 2.6.0
I am extending a third party component that has some functionality defined in a action and I want to catch that action in my subclass, call a function that returns a promise and fire the supers action when the promise resolves.
So the third party component does this:
import Ember from 'ember';
export default Ember.Component.extend({
actions: {
theAction() {
this._somePrivateFunction();
//do other stuff
}
}
});
And in my subclass I am doing:
import Ember from 'ember';
import ThirdPartyComponent from 'path/to/component'
export default ThirdPartyComponent.extend({
_funcThatReturnsPromise() {
return new Ember.RSVP.Promise();
}
actions: {
theAction() {
const thePromise = this._funcThatReturnsPromise();
thePromise.then(() => {
// undefined!
this._super(...arguments);
})
}
}
});
this._super() does not resolve to the parent component action when called in the promises callback. I've tried storing the supers function as a property and calling it:
theAction() {
const thePromise = this._funcThatReturnsPromise();
this.set('superFunc', this._super);
thePromise.then(() => {
// calls the super but "this" inside the supers action is undefined
this._super(...arguments);
})
}
This, in addition to being ugly, results in this inside the supers action to be undefined. I'm not sure why that is.. looking through some docs.
There is also the option of calling send() in my subclasses action:
theAction() {
const thePromise = this._funcThatReturnsPromise();
this.set('superFunc', this._super);
thePromise.then(() => {
//infinite loop!
this.send('theAction');
});
}
But this of course results in an infinite loop since the function ends up calling itself.
I'm not sure how to proceed here. Could anyone tell me if there is a clean way to do what I am trying to do here? Any advice would be appreciated. Thanks much!

In child component do like :
theAction() {
const thePromise = this._funcThatReturnsPromise();
let parentAction = this._super;
let self = this;
thePromise.then(() => {
//parent usage
parentAction();
// this usage
self.doSome();
});
}

Related

Vue/Vuex - Module two depends on module one, and module one gets data from server

Check this out:
import accountModule from '#/store/modules/account/account';
import otherModule from '#/store/modules/other/other';
export default new Vuex.Store({
modules: {
account: accountModule,
other: otherModule,
}
});
The data initialization in other depends on the account module because the account module has user specific settings. Suppose other.state.list depends on account.state.settings.listOrder. However, I want the data for the account module to come from the server. Which is async. So when other is trying to get set up, it can't just try to reference account.state.settings.listOrder because the response from the server may not have come back yet.
I tried exporting a promise in accountModule that resolves with the module itself. But that approach doesn't seem to work.
import accountModulePromise from '#/store/modules/account/account';
accountModulePromise.then(function (accountMoudle) {
import otherModule from '#/store/modules/other/other';
...
});
This gives me an error saying that import statements need to be top level.
The following doesn't work either:
let accountModule = await import '#/store/modules/account/account';
import otherModule from '#/store/modules/other/other';
...
It gives me an error saying that await is a reserved word. I'm confused though, because https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import says that I should be able to do it.
Your last code block didn't work because of await have to be inside async function.
Remember, the await keyword is only valid inside async functions. If
you use it outside of an async function's body, you will get a
SyntaxError.
From MDN.
You can use Dynamic Module Registration:
accountModulePromise.then(async () => {
let otherModule = await import('#/store/modules/other/other');
store.registerModule('other', otherModule.default);
});
But when you want to get state or dispatch actions you have to check whether module is registered which is pretty bad.
In my opinion it would be better if you redesign your module structure to decoupling each other. Try to move your initialize code to main.js or App.vue then dispatch actions to update module states from that.
Updates
From your last update, Another idea to decoupling your store, I think you should store your list without order and sort it only when you use. You can do this with:
Computed property:
...
computed: {
list () {
let list = this.$store.state.other.list
let order = this.$store.state.account.settings.listOrder
if (!list || !order) return []
return someSort(list, order)
}
},
beforeCreate () {
this.$store.dispatch('other/fetchList')
this.$store.dispatch('account/fetchListOrder')
}
...
Or Vuex getters:
...
getters: {
list: (state) => (order) => {
return someSort(state.list, order)
}
}
...
...
computed: {
list () {
let order = this.$store.state.account.settings.listOrder
return this.$store.getters['others/list'](order)
}
}
...
Okay, so you have two modules. One with state that is fetched from the server, the other with state that is dependent on the first, correct?
I would suggest the following approach:
Set up your modules with empty 'state' to begin with. Then create an action within accountModule to set up the state from the server. Use a getter on other to order the list. Finally, dispatch your action upon app creation.
const account = {
namespaced: true,
state: {
listOrder: ''
},
mutations: {
setListOrder (state, newListOrder) {
state.listOrder = newListOrder
}
},
actions: {
async fetchServerState (ctx) {
let result = await fetch("/path/to/server")
ctx.commit('setListOrder', result.listOrder)
// or whatever your response is, this is an example
}
}
}
const other = {
namespaced: true,
state: {
unorderedList: []
},
getters: {
list (state, getters, rootState) {
return someSort(state.unorderedList, rootState.account.listOrder);
}
}
}
within App.vue (or wherever)
created () {
this.$store.dispatch('account/fetchServerState')
}

How to use Vue plugin in Store?

Is there a proper / documented way of using a plugin inside vuex module or plain js module?
I am using event bus to acheive it, not sure if it is the correct / best way. Please help.
Plugin1.plugin.js:
const Plugin1 = {
install(Vue, options) {
Vue.mixin({
methods: {
plugin1method(key, placeholderValues = []) {
return key;
},
},
});
},
};
export default Plugin1;
In App.vue:
Vue.use(Plugin1, { messages: this.plugin1data });
In store / plain-js module:
const vue = new Vue();
const plugin1method = vue.plugin1method;
you can access your Vue instance using this._vm;
and the Vue global using import Vue from 'vue'; and then Vue;
I'm guessing you defined an instance method, so it would be the former (this._vm.plugin1method())
update
I can't tell you which way you should use it because it I can't see how your function is defined in your plugin.
However, here is an example that should illustrate the difference between instance and global
const myPlugin = {
install: function(Vue, options) {
// 1. add global method or property
Vue.myGlobalMethod = function() {
// something logic ...
console.log("run myGlobalMethod");
};
Vue.mixin({
methods: {
plugin1method(key, placeholderValues = []) {
console.log("run mixin method");
return key;
}
}
});
// 4. add an instance method
Vue.prototype.$myMethod = function(methodOptions) {
console.log("run MyMethod");
// something logic ...
};
}
};
Vue.use(Vuex);
Vue.use(myPlugin);
const store = new Vuex.Store({
state: {
count: 0
},
mutations: {
increment(state) {
this._vm.$myMethod();
Vue.myGlobalMethod();
this._vm.$options.methods.plugin1method(); // <-- plugin mixin custom method
state.count++;
}
}
});
when you commit the increment ie: this.$store.commit('increment') both methods will execute

Vue creating a plugin

I feel a bit like I'm missing something very simple, but I've been trying different stuff out and searching all over the place and can't figure out how to use a method from a custom plugin in my Vue application.
In 'vuePlugin.js' I have something like:
const myPlugin = {};
myPlugin.install = function(Vue, options){
Vue.myMethod = function(){
console.log("It worked!");
}
}
In my main.js I have:
import myPlugin from './js/vuePlugin.js'
Vue.use(myPlugin);
Then in my App.vue I have:
export default {
name: 'app',
props: {},
data () {
return{ someData: 'data' }
},
beforeCreate: function(){
myMethod();
}
}
With this I get an error that "myMethod is not defined".
I've tried saying:
var foo = myPlugin();
console.log(foo);
In my console I get an object called "install" with arguments:
"Exception: TypeError: 'caller' and 'arguments' are restricted function properties and cannot be accessed in this context. at Function.remoteFunction"
All of the documentation seems to just show how to create the plugin and "use" it, but not actually how to access anything in it. What am I missing here?
You have to export your object to be used in vuejs as follows
file vuePlugin.js
const myPlugin = {}
myPlugin.install = function (Vue, options) {
Vue.myMethod = function () {
console.log('It worked!')
}
Vue.prototype.mySecondMethod = function () {
console.log('My second Method ')
}
}
export default myPlugin
while calling the method you cannot call the method directly, you have to use as following code shown
file App.vue
export default {
name: 'app',
props: {},
data () {
return{ someData: 'data' }
},
beforeCreate: function(){
Vue.myMethod(); // call from Vue object , do not directly call myMethod()
this.mySecondMethod() // if you used prototype based method creation in your plugin
}
}
hopes it will help you

How to mock a method thats imported inside of another import

I'm testing in a ES6 babel-node environment. I want to mock a method thats used inside of the method I'm importing. The challenging part seems to be that the method I want to mock is imported into the file where the method I want to test resides. I've explored proxyquire, babel-plugin-rewire but I can't seem to get them to work on methods imported inside of other imports. From reading through various github issues I get the feeling that this might be a known limitation/frustration. Is this not possible or am I missing something?
No errors are produced when using proxyquire or babel-plugin-rewire. The method just doesn't get mocked out and it returns the methods normal value.
Here's a generic example of the import situation.
// serviceLayer.js
getSomething(){
return 'something';
}
// actionCreator.js
import { getSomething } from './serviceLayer.js';
requestSomething(){
return getSomething(); <------- This is what I want to mock
}
// actionCreator.test.js
import test from 'tape';
import {requestSomething} from 'actionCreator.js'
test('should return the mock' , (t) => {
t.equal(requestSomething(), 'something else');
});
I'm answering my own question here... Turns out I was just using babel-plugin-rewire incorrectly. Here's an example of how I'm using it now with successful results.
// serviceLayer.js
export const getSomething = () => {
return 'something';
}
// actionCreator.js
import { getSomething } from './serviceLayer.js';
export const requestSomething = () => {
return getSomething(); <------- This is what I want to mock
}
// actionCreator.test.js
import test from 'tape';
import { requestSomething, __RewireApi__ } from 'actionCreator.js'
__RewireApi__.Rewire('getSomething' , () => {
return 'something else''
});
test('should return the mock' , (t) => {
t.equal(requestSomething(), 'something else');
});

Cannot mock a module with jest, and test function calls

I create a project using create-app-component, which configures a new app with build scripts (babel, webpack, jest).
I wrote a React component that I'm trying to test. The component is requiring another javascript file, exposing a function.
My search.js file
export {
search,
}
function search(){
// does things
return Promise.resolve('foo')
}
My react component:
import React from 'react'
import { search } from './search.js'
import SearchResults from './SearchResults'
export default SearchContainer {
constructor(){
this.state = {
query: "hello world"
}
}
componentDidMount(){
search(this.state.query)
.then(result => { this.setState({ result, })})
}
render() {
return <SearchResults
result={this.state.result}
/>
}
}
In my unit tests, I want to check that the method search was called with the correct arguments.
My tests look something like that:
import React from 'react';
import { shallow } from 'enzyme';
import should from 'should/as-function';
import SearchResults from './SearchResults';
let mockPromise;
jest.mock('./search.js', () => {
return { search: jest.fn(() => mockPromise)};
});
import SearchContainer from './SearchContainer';
describe('<SearchContainer />', () => {
it('should call the search module', () => {
const result = { foo: 'bar' }
mockPromise = Promise.resolve(result);
const wrapper = shallow(<SearchContainer />);
wrapper.instance().componentDidMount();
mockPromise.then(() => {
const searchResults = wrapper.find(SearchResults).first();
should(searchResults.prop('result')).equal(result);
})
})
});
I already had a hard time to figure out how to make jest.mock work, because it requires variables to be prefixed by mock.
But if I want to test arguments to the method search, I need to make the mocked function available in my tests.
If I transform the mocking part, to use a variable:
const mockSearch = jest.fn(() => mockPromise)
jest.mock('./search.js', () => {
return { search: mockSearch};
});
I get this error:
TypeError: (0 , _search.search) is not a function
Whatever I try to have access to the jest.fn and test the arguments, I cannot make it work.
What am I doing wrong?
The problem
The reason you're getting that error has to do with how various operations are hoisted.
Even though in your original code you only import SearchContainer after assigning a value to mockSearch and calling jest's mock, the specs point out that: Before instantiating a module, all of the modules it requested must be available.
Therefore, at the time SearchContainer is imported, and in turn imports search , your mockSearch variable is still undefined.
One might find this strange, as it would also seem to imply search.js isn't mocked yet, and so mocking wouldn't work at all. Fortunately, (babel-)jest makes sure to hoist calls to mock and similar functions even higher than the imports, so that mocking will work.
Nevertheless, the assignment of mockSearch, which is referenced by the mock's function, will not be hoisted with the mock call. So, the order of relevant operations will be something like:
Set a mock factory for ./search.js
Import all dependencies, which will call the mock factory for a function to give the component
Assign a value to mockSearch
When step 2 happens, the search function passed to the component will be undefined, and the assignment at step 3 is too late to change that.
Solution
If you create the mock function as part of the mock call (such that it'll be hoisted too), it'll have a valid value when it's imported by the component module, as your early example shows.
As you pointed out, the problem begins when you want to make the mocked function available in your tests. There is one obvious solution to this: separately import the module you've already mocked.
Since you now know jest mocking actually happens before imports, a trivial approach would be:
import { search } from './search.js'; // This will actually be the mock
jest.mock('./search.js', () => {
return { search: jest.fn(() => mockPromise) };
});
[...]
beforeEach(() => {
search.mockClear();
});
it('should call the search module', () => {
[...]
expect(search.mock.calls.length).toBe(1);
expect(search.mock.calls[0]).toEqual(expectedArgs);
});
In fact, you might want to replace:
import { search } from './search.js';
With:
const { search } = require.requireMock('./search.js');
This shouldn't make any functional difference, but might make what you're doing a bit more explicit (and should help anyone using a type-checking system such as Flow, so it doesn't think you're trying to call mock functions on the original search).
Additional note
All of this is only strictly necessary if what you need to mock is the default export of a module itself. Otherwise (as #publicJorn points out), you can simply re-assign the specific relevant member in the tests, like so:
import * as search from './search.js';
beforeEach(() => {
search.search = jest.fn(() => mockPromise);
});
In my case, I got this error because I failed to implement the mock correctly.
My failing code:
jest.mock('react-native-some-module', mockedModule);
When it should have been an arrow function...
jest.mock('react-native-some-module', () => mockedModule);
When mocking an api call with a response remember to async() => your test and await the wrapper update. My page did the typical componentDidMount => make API call => positive response set some state...however the state in the wrapper did not get updated...async and await fix that...this example is for brevity...
...otherImports etc...
const SomeApi = require.requireMock('../../api/someApi.js');
jest.mock('../../api/someApi.js', () => {
return {
GetSomeData: jest.fn()
};
});
beforeEach(() => {
// Clear any calls to the mocks.
SomeApi.GetSomeData.mockClear();
});
it("renders valid token", async () => {
const responseValue = {data:{
tokenIsValid:true}};
SomeApi.GetSomeData.mockResolvedValue(responseValue);
let wrapper = shallow(<MyPage {...props} />);
expect(wrapper).not.toBeNull();
await wrapper.update();
const state = wrapper.instance().state;
expect(state.tokenIsValid).toBe(true);
});

Categories