How to test Vue "Services" mounted to root, accessed via Vue.prototype - vue.js

First, I'd like to explain that I have a Vue component repository that is responsible for displaying data retrieved from an http service. Rather than the component itself managing the same data retrieval per instance and spamming the client with network requests, I've managed to find a solution which allows another component to be mounted to the root directly (which I've dubbed as a "Service" due to its similarity to Angular) to manage the data those components need instead. This works great and other components can access it via Vue.prototype (via this.$TestService.value). It has some caveats but for the most part it accomplishes exactly what I needed. This may be uncommon, but those that use Vuex are using a similar methodology and I don't want to use the store paradigm.
I've made a very simple Vue JsFiddle to show this in action...
https://jsfiddle.net/spronkets/8v31tcfd/18
Now, to the point... I'm using #testing-library/vue, #vue/test-utils, and Jest to test the components and get test coverage and now I get errors anytime I run the tests due to the service not existing on the Vue.prototype during the test execution. I don't want to mock out the functionality of the "Service" layer, so does anyone have a solution to test these root-mounted components? I've tried manually exporting the services (unmounted and mounted) and including them in the mock section as well as importing the files directly into the test files but the "Service" is always undefined when the component is trying to retrieve the value and ONLY during test execution...
I've also created a simple repository modelled after the Vue component repository I am working with below...
https://github.com/kcrossman/VueServiceExample
To get started, clone the repo and follow the README.md included in the repo. Thanks!

I would go against using the real service if it is asyncronous, but if you just want to register it to be available you can follow the mock instructions but instead of mocking with an object just import the real service. Although after seeing your TestService implementation you will need to separate the real service from the service registration and export it to be able to register it in local vue.

You need to create and prepare your custom Vue instance in your tests in order to use any custom functionalities in your unit tests (like stores, routers, and anything else). (You can use your real modules with the custom instance, don't have to mock anything.)
In your case you should create a new Vue instance with "createLocalVue" function from '#vue/test-utils' and apply your custom prototype functionalities on that. After that you can write proper test cases accessing that custom features as well.

Update:
For those that might be referring to this in the future, Vue Plugins might be a better solution for this kind of functionality.
I stumbled along this issue in GitHub and that led me to the fix I made below:
https://github.com/testing-library/vue-testing-library/issues/113
Specifically, this comment by user nikravi:
ok, I found the fix. The trick was to add
import Vue from "vue";
import Vuetify from "vuetify";
Vue.use(Vuetify);
and then the render() works without warnings.
After I manually imported Vue and set Vue.prototype.$TestService = TestService directly in the unit test, it got passed that error. Personally, I think this is pretty silly, but it worked.
After this worked, I also found that you can access the Vue instance directly within the render callback (from #testing-library/vue), so I finished on this code instead of importing Vue:
render(TestComponent, {}, vue => {
vue.prototype.$TestService = TestService;
});
I've included all the commits to solve my issue in the repo I posted previously:
https://github.com/kcrossman/VueServiceExample
Some of the tests were malformed but once I made those changes, the tests started to work and I updated some other files to be a bit nicer for people to refer to.

Related

Multiple Vue apps, multiple entry files, same Vuex/Vue3CompostitionApi store [lost reactivity]

I am trying to iteratively replace .cshtml razor views with what I wanted to call Vue "mini-apps". Which should be somewhere in between a micro-frontend and a classic SPA. The aim is to share some of the code base, mainly dependencies. Compile a common chunk-vendors.js and have the "mini-apps" as separate javascript entry files to place on appropriate views. As performance demand would grow, I would progress into splitting chunk-vendors.js and optimize via lazy-loading.
The problem I am hitting here is trying to make two root Vue instances talk to each other through a shared state. Right now only the app that is imported/mounted first stays reactive. I thought that my problem was in the Vue 2 reactivity system/how Vuex binds itself to a concrete Vue instance here. When I implemented a primitive store, the situation ended up being exactly the same.
What confuses me about this is that if I were to instantiate two applications in a single main.js entry file, the store sharing would just work. Which suggest that Vue is either creating some kind of hidden root instance or that my Vuex code analysis deduction of it binding to a concrete instance was incorrect.
I would highly appreciate it if someone could tell me why this can't work, optionally suggest a workaround?
I have created a reproduction both in Vue 2 with Vuex and in Vue 3 with composition API/primitiveStoreImplementation here.
Vue-cli is building the app in an MPA mode with pages specified in vue.config.json, then imported in the root index.html file. The store is initialised once and saved for later check/loading on the window object. In the context of asp/razor I would have webpack set up to remove the redundant files, only leaving javascript bundles. Also, the dev proxy would proxy everything except the path to the script bundles. All of this is removed for the sake of the demonstration.
(once I find a solution I hope to replace the source link with specific code snippets)
Options considered:
I am trying to avoid it, but I might have to always run a "coordinator" root instance that will check the presence of certain elements on a page and load/unload the "mini-apps" as components using something like portal-vue when needed. That coordinator would also contain a state with modules, some of which would be marked as "shared" thus operations from multiple "mini-apps" would be allowed (ownership flag check).
I have considered sessionStorage/localStorage, the problem is that the 'storage' events are only triggered across tabs and not within one document first |Note. I would have to trigger a custom event or play around with iframes. Feels too hacky, but that might be an axiom here. It would also duplicate the same state across many store instances.
These are some relevant articles I have found on this topic:
Probably closest to what I am trying to achieve:
Using Vuex with multiple Vue instances
Same but different:
Build Vue microfrontend app (with routing and vuex store)
The use case for multiple entries are sub-apps that don't coexist on the same page, although they can. They could be web components or regular app bundles. They can even interact with each other but they need something else for this - global event bus or a mediator app that passes data between them.
The problem is that there are more than one Vue library copies and/or more than one Vuex store instance. In order to avoid this, they would need to be precisely loaded only once on the page and reused between apps, i.e. vue and vuex are loaded as CDN libs, possibly used as Webpack externals to use window.Vue and window.Vuex transparently for respective import. Not only Vuex but store needs to be a singleton on the page (basically a said mediator). This is acceptable solution but primarily suitable for existing applications that have design restrictions and need a workaround.
I am trying to avoid it, but I might have to always run a "coordinator" root instance that will check the presence of certain elements on a page and load/unload the "mini-apps" as components using something like portal-vue when needed.
This is the common way to do this. Vue 3 has teleports that has give less control than portal-vue. It has no downsides for application design if done properly. The same thing is achieved similarly in other frameworks (Angular, React) as well, where portals appeared earlier.
I have considered sessionStorage/localStorage, the problem is that the 'storage' events are only triggered across tabs and not within one document
This is solved by using window postMessage and message event in the same tab. In case this shouldn't be limited to a single window, there are third party libs that use both for cross-tab synchronzation, a native alternative is BroadcastChannel that has less browser support than LS but doesn't have its limitations regarding tabs.

Workbox/Vue: Create a custom variation on an existing caching strategy handler

Background:
I'm building an SPA (Single Page Application) PWA (Progressive Web App) using Vue.js. I've a remote PostgreSQL database, serving the tables over HTTP with PostgREST. I've a working Workbox Service Worker and IndexedDB, which hold a local copy of the database tables. I've also registered some routes in my service-worker.js; everything is fine this far....
I'm letting Workbox cache GET calls that return tables from the REST service. For example:
https://www.example.com/api/customers will return a json object of the customers.
workbox.routing.registerRoute('https://www.example.com/api/customers', workbox.strategies.staleWhileRevalidate())
At this point, I need Workbox to do the stale-while-revalidate pattern, but to:
Not use a cache, but instead return the local version of this table, which I have stored in IndexedDB. (the cache part)
Make the REST call, and update the local version, if it has changed. (the network part)
I'm almost certain that there is no configurable option for this in this workbox strategy. So I would write the code for this, which should be fairly simple. The retrieval of the cache is simply to return the contents of the requested table from IndexedDB. For the update part, I'm thinking to add a data revision number to compare against. And thus decide if I need to update the local database.
Anyway, we're now zooming in on the actual question:
Question:
Is this actually a good way to use Workbox Routes/Caching, or am I now misusing the technology because I use IndexedDB as the cache?
and
How can I make my own version of the StaleWhileRevalidate strategy? I would be happy to understand how to simply make a copy of the existing Workbox version and be able to import it and use it in my Vue.js Service Worker. From there I can make my own necessary code changes.
To make this question a bit easier to answer, these are the underlying subquestions:
First of all, the StaleWhileRevalidate.ts (see link below) is a .ts (TypeScript?) file. Can (should) I simply import this as a module? I propably can. but then I get errors:
When I to import my custom CustomStaleWhileRevalidate.ts in my main.js, I get errors on all of the current import statements because (of course) the workbox-core/_private/ directory doesn't exist.
How to approach this?
This is the current implementation on Github:
https://github.com/GoogleChrome/workbox/blob/master/packages/workbox-strategies/src/StaleWhileRevalidate.ts
I don't think using the built-in StaleWhileRevalidate strategy is the right approach here. It might be possible to do what you're describing using StaleWhileRevalidate along with a number of custom plugin callbacks to override the default behavior... but honestly, you'd end up changing so much via plugins that starting from scratch would make more sense.
What I'd recommend that you do instead is to write a custom handlerCallback function that implements exactly the logic you want, and returns a Response.
// Your full logic goes here.
async function myCustomHandler({event, request}) {
event.waitUntil((() => {
const idbStuff = ...;
const networkResponse = await fetch(...);
// Some IDB operation go here.
return finalResponse;
})());
}
workbox.routing.registerRoute(
'https://www.example.com/api/customers',
myCustomHandler
);
You could do this without Workbox as well, but if you're using Workbox to handle some of your unrelated caching needs, it's probably easiest to also register this logic via a Workbox route.

Use Vue.js components inside Classic ASP application

I’m new to vue.js. I have a requirement to develop a calendar with a scheduling feature for a Classic ASP project. I did some research and found the following project on GitHub which is developed using vue.js.
https://github.com/ClickerMonkey/dayspan-vuetify
I can use this project to implement the feature as a separate application. But I need to plug this inside to classic ASP project since there is no API to develop separately.
I was spending a lot of time to find how to make that possible but couldn't find an easy guide. Any help would be greatly appreciated. Thanks.
Using Vue#Next(AKA v3) you can instantiate separate Vue apps on the client.
Simply include the vue3.js client file, extract the desired methods from Vue,
create the app with its configuration and mount it to the desired DOM element.
You'll need to support interfacing, likely when the component is "mounted", between the app and third party object like a calendar scheduler class, cookie class, localStorage whatever you want to support. You can even communicate between app if you set up handles between them. Vue 3 also has providers and injectors which let you affect state and methodology at a parent level and inject it at child levels without the need to pass through each child level.
It's great for learning, rapid development, prototyping, small test components, trial and error type work, but it's a step or few away from typical Vue development paradigm.
FYI, Vue3 also supports dynamic asynchronous components.
<script src="/dist/vue.global.js"></script>
const { createApp, defineAsyncComponent } = Vue;
const app1 = createApp({first app configuration object});
const app2 = createApp({second app configuration object});
const vmApp1 = app.mount( app1 selector );
const vmApp2 = app.mount( app2 selector );

Assign/add mixin or sub-component at runtime?

Can I dynamically add a mixin? I think I read that runtime insertion of mixins is not supported and will never be. Is the below runtime insertion possible?
My usecase is; all our pages are stored in a database, each page standard properties like; title, content and template. We have components for each template. Each template component displays the title and content differently. So I need to build the routes and say this page uses this (template) component. Maybe I can use sub-components to achieve this? Can I dynamically add sub-components at runtime?
The easiest solution is to do the following:
Router:
// myPages retrieved by REST call
const routes = _.map(myPages, page => {
return {
path: `/${page.url}`,
name: page.name,
component: DefaultPage // make all pages use DefaultPage component
}
});
DefaultPage.vue
<template>
</template>
<script>
import mixins from './mixins';
export default {
mixins: [mixins.Base]
beforeMount() {
// I dont think this is possible?
let templateMixin = mixins[ this.page.template ]
this.mixins.push( templateMixin );
}
}
</script>
Maybe its possible to assign a sub-component at runtime?
<template>
// Somehow call the sub-component (template)?
<template></template>
</template>
<script>
import templates from './templates';
export default {
components: {},
beforeMount() {
// Is this possible?
let templateCmp = templates[ this.page.template ]
this.components = {
templateCmp
}
}
}
</script>
Unfortunately there is a lot of misinformation on the web stating that "because of userland perils, it's not safe or secure to load dynamic or "runtime" components", and that doing so is "a security risk".
This stems from the reasoning that Vue uses the eval statement when compiling components. However, so does React and Angular. There is in fact no way to compile components without this. while eval is a sharp knife, so is any JavaScript. Saying that a sharp knife is insecure is false as long as you keep that sharp knife itself secure.
The notion that runtime components is insecure is utterly and completely false. There are tons of officially supported ways to do this if you control the source. In fact, there is even an officially Vue supported "userland" method to load dynamic/async components even if you don't control the source(!!)
Async components
Probably the easiest way to load a page from a database (which itself may have it's own sub-components, mixins and dependencies), is with async components.
This is an official part of Vue and I've used it in dozens of production apps. It works exactly like expected; nothing is invoked by your application until all the conditions are met, and when the load conditions are met, you are free to obtain the component however you see fit.
Note: You must control the source of these components, or XSS injection and other hacking is possible.
If you're using webpack, it's easy to get all the components dependencies into a single file. In the above URL are specific how-to articles including a video tutorial of how to produce a single js file for your pages dependencies that aren't part of your main application.
One caveat for loading multiple sub components this way is you may overlap/repeat loading (i.e. page A may load 5 sub components that no other static page loads, and page B may load 5 sub components, 3 of which are shared with page A, and 2 of which are specific to page B). Caveats like these can mess up your optimization techniques if you don't think them through. This can be unavoidable and fine, and still much faster than loading the entire app at once though.
Async components in userland
If you only want to load custom templates from a database (i.e. if you don't want to allow users to load their own custom components, mixins, filters, directives, etc), with each template, then you are in luck; Vue even has official support for async templates that are locked-down to only allowing template changes. This is enough to offer your component builders scaffolding for creating an app, but prevents them from executing arbitrary JavaScript code (this method won't let them setup a data section, or hook into the component lifecycle for example).
v-runtime-template is officially recognized by the Vue.js team as the official method for creating userland-safe Vue templates.
I've used v-runtime-template in platforms used by some of the biggest names in the industry across tens of millions of users without a single security breach, because this method only exposes the components you say are OK.
Further template lockdown using JSON schemas
If you need only form generation, or you can simplify your components further, and you only need to reason about data in simple ways like queries (i.e. if you're building a survey generator, or an analytics or other widget dashboard), you can build components out of JSON Schemas using vue-form-json-schema. This method takes 2 schemas: one for your data, and one for your form. The parent component you load specifies all the components that are accessible to the schema, so you must whitelist components that userland has access to, further, the forms cannot run arbitrary JavaScript, they can only call functions that you make available in the parent component, which are your JavaScript whitelist.
Userland-Safe Queries on arbitrary data
You can let users query specific JSON objects for use in completely-safe userland queries that can be provided by the public using JSONata. Developed by IBM, JSONata is a way to safely query and, with the exposure of a few functions, allows your users to even manipulate data in verified-safe ways.
Although JSONata is logically proven to be safe in unserland, it is also Turing-complete without functions, meaning your users can manipulate the Schema provided data by outputting new data that can technically do anything; i.e. your users can create Doom 3 using JSONata queries. People have made games like Pong and other interesting things out of JSONata.
JSONata's power cannot be overstated. It powers the magic behind nearly all no-code/low-code platforms like the open source Node-RED, and is behind many other proprietary no-code/low-code platforms that are closed source, including Google, Microsoft, Amazon and IBM platforms.
They use JSONata to translate data schemas between platforms, and whenever business logic is needed on data manipulation, and the manipulation must be left in user-land (i.e. an app that needs to manipulate data, but you want to run that app on your platform and not have to worry about QA or people being able to hack or write a nefarious app that breaks into your platform).

Application-wide globals in vue.js

Does Vue.js have an idiomatic way of declaring application-wide globals like baseUrl and then referencing them throughout the app (for API calls etc)?
If so, what is the best way to configure such globals at build-time to create development, test and prod instances? For example, I imagine doing export BASEURL='http://dev.myapp.com', running a build and getting an app with this baseUrl configured. This would in turn allow me to create automated builds for Continuous Delivery certain branches are updated (develop -> dev.myapp.com, master -> www.myapp.com etc).
Thanks!
What I do: use some state management tool, like Vuex.
If you don't want to add complexity to your app, I use a parent Vue instance, let say App.vue and define global data in that instance, then when I need that data from the parent I call this.$root.baseUrl in case you want the baseUrl. Notice that you can also call this.$parent.baseUrl but this will only work for direct childs, if you are inside a child of a child, you wont get the global data, that's why is better the $root object.