How to dynamically mock ES6 modules with SystemJS? - testing

I have a single-page application written in ES6. The code in transpiled server-side into classic javascript by babelJs, then loaded by SystemJs.
Javascript present in my html file:
System.config({
baseURL: '/js',
meta: {
'/js/*': { format: 'cjs' }
}});
System.defaultJSExtensions = true;
System.import("index.js")
.catch(function (error) {
console.error(error)
});
index.js:
import f1 from 'file1';
import f2 from 'file2';
// code here ...
Everything works fine. index.js is loaded, and all import statements are correctly executed.
Now, I want to create some pages with mocked ES6 modules, for testing purpose. My goal is to display pages by replacing model classes (contained in ES6 modules) with other static test classes.
Let's say I have 3 files: real_model.js, fake_model.js and component.js. component.js import the real model (import Model from 'real_model';).
How can I replace the real model by the fake one (in the component) dynamically ?

It's been a while since this question was posted, but maybe this solution might still be of help to anyone else.
With SystemJS it is possible to create a module on-the-fly using System.newModule. Then you can use System.set to overwrite existing modules with the new one. In our tests we use the following helper function to mock existing modules:
function mockModule(name, value) {
const normalizedName = System.normalizeSync(name);
System.delete(normalizedName);
System.set(normalizedName, System.newModule(Object.assign({ default: value }, value)));
}
Then, e.g. inside the beforeEach callback, we assign the mock and then import the module to be tested using System.import:
let [component, fake_model] = [];
beforeEach(() => {
// define mock
fake_model = { foo: 'bar' };
// overwrite module with mock
mockModule('real_model', fake_model);
// delete and reimport module
System.delete(System.normalizeSync('component'));
return System.import('src/testing').then((m) => {
component = m.default;
}).catch(err => console.error(err));
});
// test your component here ...
A big advantage of this approach is that you don't need an additional mocking library and it works solely with SystemJS.

Related

Nuxt Generate dynamic routes from the store

I'm using nuxt.js and now need to generate my dynamic pages by running npm run generate. However my list items needed to make dynamic items are stored in the store, so I need to map over them somehow so the generate can make the routes for them
How can I access the store in my nuxt.config.js?
generate: {
dir: 'wwwroot', //override the default generation dir to create everything straight in wwwroot
routes() {
let vdc = this.$store.vdcServers.map(server => `/virtual-data-centres/${server.slug}`);
return Promise.all([vdc]).then(values => {
return values.join().split(',');
})
}
}
Output
ERROR Could not resolve routes
FATAL Cannot read properties of undefined (reading '$store')
To my knowledge, you cannot access the store from that place. Maybe with some hooks? I doubt.
Meanwhile, if you have your elements available in your store you should be able to find them back by making a quick axios call or like I think.
This kind of approach is totally fine
import axios from 'axios'
export default {
generate: {
routes(callback) {
axios
.get('https://my-api/users')
.then(res => {
const routes = res.data.map(user => {
return '/users/' + user.id
})
callback(null, routes)
})
.catch(callback)
}
}
}
It may be a bit of code duplication, meanwhile, it's still the simplest way to go.
Otherwise, you could try to persist it to some localStorage or any similar solution, to have it both in Vuex and during your generation.

An updated approach to testing vuex actions

In the "Testing Actions" documentation, there is a recommendation to use inject-loader in order to mock entire module imports, and their respective dependencies, when unit testing.
For example, you can mock the ../api/shop import found inside the ./actions file with the following:
const actionsInjector = require('inject-loader!./actions')
// create the module with our mocks
const actions = actionsInjector({
'../api/shop': {
getProducts (cb) {
setTimeout(() => {
cb([ /* mocked response */ ])
}, 100)
}
}
})
Unfortunately, that package does not support Webpack 5.
Is there an updated approach, or alternative package, that is recommended?
Thanks #Estus Flask.
The clarification/mixup was that, whilst I was using vi.mock, and it's auto-mocking algorithm :
"...will mock the module itself by invoking it and mocking every
export."
i.e. it still runs the import statements. To prevent this, a factory must be provided (see documentation), e.g.:
vi.mock('module', <factory-here>)
Which then replaces the entire file import.

Vue/Nuxt: How to define a global method accessible to all components?

I just want to be able to call
{{ globalThing(0) }}
in templates, without needing to define globalThing in each .vue file.
I've tried all manner of plugin configurations (or mixins? not sure if Nuxt uses that terminology.), all to no avail. It seems no matter what I do, globalThing and this.globalThing remain undefined.
In some cases, I can even debug in Chrome and see this this.globalThing is indeed defined... but the code crashes anyway, which I find very hard to explain.
Here is one of my many attempts, this time using a plugin:
nuxt.config.js:
plugins: [
{
src: '~/plugins/global.js',
mode: 'client'
},
],
global.js:
import Vue from 'vue';
Vue.prototype.globalFunction = arg => {
console.log('arg', arg);
return arg;
};
and in the template in the .vue file:
<div>gloabal test {{globalFunction('toto')}}</div>
and... the result:
TypeError
_vm.globalFunction is not a function
Here's a different idea, using Vuex store.
store/index.js:
export const actions = {
globalThing(p) {
return p + ' test';
}
};
.vue file template:
test result: {{test('fafa')}}
.vue file script:
import { mapActions } from 'vuex';
export default {
methods: {
...mapActions({
test: 'globalThing'
}),
}
};
aaaaaaaaand the result is.........
test result: [object Promise]
OK, so at least the method exists this time. I would much prefer not to be forced to do this "import mapActions" dance etc. in each component... but if that's really the only way, whatever.
However, all I get is a Promise, since this call is async. When it completes, the promise does indeed contain the returned value, but that is of no use here, since I need it to be returned from the method.
EDIT
On the client, "this" is undefined, except that..... it isn't! That is to say,
console.log('this', this);
says "undefined", but Chrome's debugger claims that, right after this console log, "this" is exactly what it is supposed to be (the component instance), and so is this.$store!
I'm adding a screenshot here as proof, since I don't even believe my own eyes.
https://nuxtjs.org/guide/plugins/
Nuxt explain this in Inject in $root & context section.
you must inject your global methods to Vue instance and context.
for example we have a hello.js file.
in plugins/hello.js:
export default (context, inject) => {
const hello = (msg) => console.log(`Hello ${msg}!`)
// Inject $hello(msg) in Vue, context and store.
inject('hello', hello)
// For Nuxt <= 2.12, also add 👇
context.$hello = hello
}
and then add this file in nuxt.config.js:
export default {
plugins: ['~/plugins/hello.js']
}
Use Nuxt's inject to get the method available everywhere
export default ({ app }, inject) => {
inject('myInjectedFunction', (string) => console.log('That was easy!', string))
}
Make sure you access that function as $myInjectedFunction (note $)
Make sure you added it in nuxt.config.js plugins section
If all else fails, wrap the function in an object and inject object so you'd have something like $myWrapper.myFunction() in your templates - we use objects injected from plugins all over the place and it works (e.g. in v-if in template, so pretty sure it would work from {{ }} too).
for example, our analytics.js plugin looks more less:
import Vue from 'vue';
const analytics = {
setAnalyticsUsersData(store) {...}
...
}
//this is to help Webstorm with autocomplete
Vue.prototype.$analytics = analytics;
export default ({app}, inject) => {
inject('analytics', analytics);
}
Which is then called as $analytics.setAnalyticsUsersData(...)
P.S. Just noticed something. You have your plugin in client mode. If you're running in universal, you have to make sure that this plugin (and the function) is not used anywhere during SSR. If it's in template, it's likely it actually is used during SSR and thus is undefined. Change your plugin to run in both modes as well.
This would be the approach with Vuex and Nuxt:
// store/index.js
export const state = () => ({
globalThing: ''
})
export const mutations = {
setGlobalThing (state, value) {
state.globalThing = value
}
}
// .vue file script
export default {
created() {
this.$store.commit('setGlobalThing', 'hello')
},
};
// .vue file template
{{ this.$store.state.globalThing }}

How to create unit test cases with Vue, Karma, browserify

I am trying to build some unit test cases to my existing Vue project.
I found some documents there but not useful especially for testing on functions such as Watch, Promise and Then.
Is there any specific and detailed guide line on unit testing with Vue and these plugins?
The target vue has defined a function named test.
const vm = new Vue(target).$mount();
vm.test("message");
But the error message is vm.test is not a function
I do not know why I could not use the function defined in the target.vue.
Meanwhile once I use the test function to change some data, the target vue will update the data automatically.
But it seems that Vue.nextTick does not work on this situation.
Could someone help me on this point?
Thank you very much for your help.
Hellocomponent
export default {
name: 'hello',
data () {
return {
msg: 'Welcome to Your Vue.js App',
test: 'Testing'
}
}
}
Hello.spec.js //for testing Hello.vue
describe('Hello', () => {
it('set correct default data', () => {
expect(typeof Hello.data).to.equal('function')
assert.typeOf(Hello.data, 'function')
const defaultdata = Hello.data()
expect(defaultdata.test).to.be.a('string')
expect(defaultdata.test).to.equal('Testing')
})
})
This is test case of Hello component of vue.js which is created automatically when new template is created. This is using Karma+Mocha+Chai.

How can I load a module with RequireJS for testing in a testing framework like Jasmine?

I am new to JavaScript and try to test functions defined in a RequireJS Module.
That means i have some code like this:
define([...], function(...){
var ModuleName = Base.extend({
init: function(){
//some code
};
});
}
Now I want to test the function init().
I load the object from my spec.js, this works:
describe("ModuleName", function(){
var mod = require(['../js/app/ModuleName.js'], function(ModuleName) {});
it("exists", function(){
expect(mod).toBeDefined();
});
});
This passes well.
But when I add this code, it fails:
it("contains init", function(){
expect(mod.init).toBeDefined();
});
I don't understand why.
You're not using RequireJS properly.
The following solution needs the use of beforeAll, which can be added to Jasmine with this package. Your code could be something like this:
describe("ModuleName", function() {
var mod;
beforeAll(function (done) {
// This loads your module and saves it in `mod`.
require(['../js/app/ModuleName'], function(mod_) {
mod = _mod;
done();
});
});
it("exists", function(){
expect(mod).toBeDefined();
expect(mod.init).toBeDefined();
});
});
As I recall, the return value of require called with an array of dependencies is a reference to require itself. So yes, it is defined but, no, it is not the value of the module you were trying to load. To get a module value, you have to do something like I did in the code above.
If your tests happen to be in a RequireJS module, you could also just add the module to be tested to the list of dependencies:
define([..., '../js/app/ModuleName'], function (..., mod) {
describe("ModuleName", function() {
it("exists", function(){
expect(mod).toBeDefined();
expect(mod.init).toBeDefined();
});
});
});
I've used both methods above in different circumstances.
Side note: I've removed the .js from the module name in the code above. You generally do not want to put the .js extension to module names you give to RequireJS.