I have a file that imports another. I want to mock the other import differently in each test yet have it show through the file that imports it.
I have tried various mocking and importing approaches through googling but none have worked.
Consider the files:
settings.js
export default { mySetting: null };
store.js
import settings from "./settings";
export default {
settings,
};
settingsDemo.js
import store from "./store";
it("default settings", () => {
expect(store.settings.mySetting).toBe(null);
});
it("mocked to true", () => {
expect(store.settings.mySetting).toBe(true);
});
it("mocked to false", () => {
expect(store.settings.mySetting).toBe(false);
});
how do I mock settings.js within settingsDemo.js to have all 3 tests pass?
From "Exploring ES6"
:
"In ES6, imports are live read-only views on exported values."
"Note that while you can’t change the values of imports, you can change the objects that they are referring to."
In other words, it is not possible to assign settings to a different object, but it is possible to change properties on settings and those changes will automatically be seen wherever it is imported.
With that in mind, here is a working test:
import store from "./store";
import settings from './settings'; // import settings
it("default settings", () => {
expect(store.settings.mySetting).toBe(null); // SUCCESS
});
it("mocked to true", () => {
settings.mySetting = true; // change the mySetting property
expect(store.settings.mySetting).toBe(true); // SUCCESS
});
it("mocked to false", () => {
settings.mySetting = false; // change the mySetting property
expect(store.settings.mySetting).toBe(false); // SUCCESS
});
Related
we're currently trying to unit / integration test our react application, which uses the forge viewer cdn script.
the to be tested code assumes that Autodesk is available on the window object, which is the case in the application (via script tags), but not in the context of testing. this leads to errors like these:
Test suite failed to run
ReferenceError: Autodesk is not defined
> 1 | export class ExtendedGuiViewer3D extends Autodesk.Viewing.GuiViewer3D {
according to the license comments, the forge viewer script only allows using it through the Autodesk servers, so I cant just download it, and require the file locally.
has anyone successfully tested components that use the forge viewer scripts?
Intro
Disclaimer, I've only recently been experimenting with "Reactifying" the Autodesk Forge Viewer!
I currently believe the 'correct' way to consume the forge viewer css / js is to pull code from the Autodesk hosted cdn. The types are still available on npm though.
For example, the endpoints for v7.52.0:
js https://developer.api.autodesk.com/modelderivative/v2/viewers/7.52.0/viewer3D.min.js
css https://developer.api.autodesk.com/modelderivative/v2/viewers/7.52.0/style.min.css
Steps
1. Add type information from npm
Firstly, if you are using typescript, you can still install the viewer types from npm with:
yarn add -D #types/forge-viewer (check/add specific version to match the version of the script from the cdn - you can verify in your package.json)
2. Sample ViewingContext.tsx
In your React code you may wish to create a React Context to manage the the script downloading for you. This example is based on next.js:
import React, { PropsWithChildren, useEffect, useState } from "react";
import Script from "next/script";
export const ViewingContext = React.createContext({
initialized: false,
});
export interface ViewingContextProps {
options: Autodesk.Viewing.InitializerOptions;
}
// Place a single global ViewingContextProvider component around the common root of all your Autodesk.Viewing (LMV) components.
// https://forge.autodesk.com/en/docs/viewer/v7/developers_guide/overview/
export const ViewingContextProvider = ({
options,
children,
}: PropsWithChildren<ViewingContextProps>): JSX.Element => {
const [scriptLoaded, setScriptLoaded] = useState(
typeof window !== "undefined" &&
window.Autodesk?.Viewing?.Initializer !== undefined
);
const [initialized, setInitialized] = useState(false);
useEffect(() => {
if (scriptLoaded && !initialized) {
Autodesk.Viewing.Initializer(options, () => setInitialized(true));
}
}, [options, initialized, scriptLoaded]);
return (
<ViewingContext.Provider value={{ initialized }}>
<link
rel="stylesheet"
href="https://developer.api.autodesk.com/modelderivative/v2/viewers/7.52.0/style.min.css"
type="text/css"
/>
<Script
async
src="https://developer.api.autodesk.com/modelderivative/v2/viewers/7.52.0/viewer3D.min.js"
onLoad={(): void => setScriptLoaded(true)}
/>
{children}
</ViewingContext.Provider>
);
};
3. Sample Viewer.tsx Component
Only mount this component as a child of the ViewingContext. You can also modify/replace this component with the ExtendedGuiViewer3D you mentioned.
import React, { useContext, useEffect, useRef } from "react";
import { ViewingContext } from "./ViewingContext";
export interface ViewerProps {
config?: Autodesk.Viewing.Viewer3DConfig;
onLoaded?: (viewer: Autodesk.Viewing.GuiViewer3D) => void;
onError?: (code: number) => void;
}
// Thin wrapper around https://forge.autodesk.com/en/docs/viewer/v7/developers_guide/overview/
// Add your own imperative hook code after GuiViewer object is loaded with the onLoaded callback.
// Place inside a relative layout div.
export const Viewer = ({
config,
onLoaded,
onError,
}: ViewerProps): JSX.Element => {
const { initialized: viewingContextInitialized } = useContext(ViewingContext);
const viewerDivRef = useRef<HTMLDivElement>(null);
const viewer = useRef<Autodesk.Viewing.GuiViewer3D>();
// Viewer imperative loading code
useEffect(() => {
if (viewingContextInitialized && !viewer.current && viewerDivRef.current) {
viewer.current = new Autodesk.Viewing.GuiViewer3D(
viewerDivRef.current,
config
);
const startedCode = viewer.current.start();
if (startedCode > 0) {
onError && onError(startedCode);
return;
}
if (onLoaded) onLoaded(viewer.current);
}
}, [config, onLoaded, onError, viewingContextInitialized]);
// Viewer destructor
useEffect(() => {
return (): void => {
if (viewer.current) {
viewer.current.finish();
}
};
}, []);
return (
<div
style={{
position: "absolute",
width: "100%",
height: "100%",
overflow: "hidden",
}}
>
<div
style={{
margin: 0,
width: "100%",
height: "100%",
}}
ref={viewerDivRef}
/>
</div>
);
};
Hope this answers your question!
so after months of fighting, these are the two options I've come up with so far.
option 1: dirty mock everything
there's a few #ts-ignore, because I dont want to mock out the whole package. i'll only mock the parts my application uses.
you could to type assertion like global.THREE = {...} as unknown as typeof THREE too. whatever floats your boat.
// setupTests.ts
// NOP_VIEWER global is not part of the #types declaration, so we need to tell typescript that there will be a global
declare global {
namespace NodeJS {
interface Global {
NOP_VIEWER: ExtendedGuiViewer3DTypes;
}
}
}
global.Autodesk = {
// #ts-ignore
Viewing: {
GuiViewer3D: jest.fn(),
Extension: jest.fn(),
ToolInterface: jest.fn(),
},
};
// #ts-ignore
global.THREE = {
Color: jest.fn(),
Vector4: jest.fn(),
};
global.NOP_VIEWER = {
disableSelection: jest.fn(),
resize: jest.fn(),
// #ts-ignore
model: {
getExternalIdMapping: (successCallback: any, _: any) => {
successCallback({ 'test-guid': 1 });
},
},
clearThemingColors: jest.fn(),
setThemingColor: jest.fn(),
isLoadDone: () => true,
isolate: jest.fn(),
};
option 2: download and require
As Autodesk Developer Adam Nagy pointed out, you probably wont get sent to jail, if you download the script file and require it locally for your tests only. (note that this is just a "probably")
keep in mind that even if you require the file, you still have to mock NOP_VIEWER as this global is only available after initializing the viewer (which you dont want to do in your tests)
// setupTests.ts
// replace the mocks of `Autodesk` and `THREE` with this require.
require('./vendors/Autodesk/viewer3D.min');
in my tests i can then use the jest spies on NOP_VIEWER
expect(NOP_VIEWER.clearThemingColors).toHaveBeenCalled();
Context: I am trying to get Google Maps place data via the place_id on the beforeEnter() route guard. Essentially, I want the data to load when someone enters the url exactly www.example.com/place/{place_id}. Currently, everything works directly when I use my autocomplete input and then enter the route but it does not work when I directly access the url from a fresh tab. I've been able to solve this using the beforeEnter() route guard in traditional Vue, but cannot solve for this using Nuxt. Please help!
Question: How can I access the Vuex Store before a page loads in Nuxt?
Error: Any solution I try (see below) I either end up with a blank page or the page will not load (I think it is stuck in a loop and cannot resolve the Promise).
Attempted Solutions:
Using Middleware like below:
middleware({ store, params }) {
return store.dispatch('myModule/fetchLocation', params.id)
}
Using asyncData like below:
data(){
return{
filteredLocation: {}
}
}
// snip
async asyncData({ store, params }) {
const { data } = await store.dispatch('myModule/fetchLocation', params.id)
return filteredLocation = data
}
I tried looking into fetch, but apparently you no longer have access to context
Example Code:
In one of my store modules:
/* global google */
import Vue from 'vue'
import * as VueGoogleMaps from '~/node_modules/vue2-google-maps/src/main'
Vue.use(VueGoogleMaps, {
load: {
key: process.env.VUE_APP_GMAP_KEY,
libraries: 'geometry,drawing,places'
}
})
export const state = () => ({
selectedLocation: {}
})
export const actions = {
fetchLocation({ commit }, params) {
return new Promise((resolve) => {
Vue.$gmapApiPromiseLazy().then(() => {
const request = {
placeId: params,
fields: [
'name',
'rating',
'formatted_phone_number',
'geometry',
'place_id',
'website',
'review',
'user_ratings_total',
'photo',
'vicinity',
'price_level'
]
}
const service = new google.maps.places.PlacesService(
document.createElement('div')
)
service.getDetails(request, function(place, status) {
if (status === 'OK') {
commit('SET_PLACE', place)
resolve()
}
})
})
})
}
}
export const mutations = {
SET_PLACE: (state, selection) => {
state.selectedInstructor = selection
}
}
EDIT: I already have it in a plugin named google-maps.js and in my nuxt.config.js file I have:
plugins: [
{ src: '~/plugins/google-maps.js' }
]
//
//
build: {
transpile: [/^vue2-google-maps.js($|\/)/],
extend(config, ctx) {}
}
Using Middleware is how we can access Vuex before page loads. try putting the configuration part in a custom Nuxt plugin.
Create a file in Plugins folder (you can name it global.js).
Put this
import Vue from 'vue'
import * as VueGoogleMaps from '~/node_modules/vue2-google-maps/src/main'
Vue.use(VueGoogleMaps, {
load: {
key: process.env.VUE_APP_GMAP_KEY,
libraries: 'geometry,drawing,places'
}
})
in global.js.
Then add the plugin in nuxt.config.js like this.
plugins: [
'~/plugins/global.js'
]
Also, make sure you're using underscore before 'page_id' name in your folder structure.
I can't seem to figure out how to load a local .json file and read the content - so I can dump it into some ´state´.
The code looks like this so far:
import React, { Component } from 'react'
import Files from 'react-files'
class LoadFile extends Component {
render() {
return (
<div className="files">
<Files
className="files-dropzone"
onChange={file => {
console.log(file)
}}
onError={err => console.log(err)}
accepts={['.json']}
multiple
maxFiles={3}
maxFileSize={10000000}
minFileSize={0}
clickable
>
Drop files here or click to upload
</Files>
</div>
)
}
}
export default LoadFile
The logged object does not have any of the data buried inside of it..
[Object]
0: Object
id: "files-1"
extension: "json"
sizeReadable: "288B"
preview: Object
type: "file"
Like #dkniffin said, what behind react-files is DataTransfer.
You could utilize the FileReader API to get the file content and parse it in JSON format, you could see the result in the console section of CodeSandbox below:
constructor() {
super();
this.state = {
jsonFile: {}
};
this.fileReader = new FileReader();
this.fileReader.onload = (event) => {
// or do whatever manipulation you want on JSON.parse(event.target.result) here.
this.setState({ jsonFile: JSON.parse(event.target.result) }, () => {
console.log(this.state.jsonFile);
});
};
}
...
render() {
return (
<div className="files">
<Files
...
onChange={file => {
// we choose readAsText() to load our file, and onload
// event we rigister in this.fileReader would be triggered.
this.fileReader.readAsText(file[0]);
}}
>
Drop files here or click to upload
</Files>
</div>
);
}
Unfortunately, I don't have a complete answer for you. However, after digging into the source code of react-files for a bit, I found this line:
let filesAdded = event.dataTransfer ? event.dataTransfer.files : event.target.files
It appears that filesAdded will be a FileList, which is basically just an array of File objects. Unfortunately, I don't see a way to get the content from a File object. Maybe someone else can help out with that.
I have created a Rust library of type cdylib using
cargo web build --target=wasm32-unknown-unknown
I use a modified version of the "rust-wasm-loader" NPM package to build and load the WASM file. rust-wasm-loader uses this as a way to use the Rust code:
const wasm = require('./main.rs')
wasm.initialize().then(module => {
// Use your module here
const doub = module.cwrap('doub', 'number', ['number'])
console.log(doub(21))
})
I do not want to initialize the module every time I want to use the code. How do I load the module and use it like a library?
Since the loading of WebAssembly is asynchronous and may actually take some time for large modules, you need to handle the state when the module is not loaded, and then let the rest of the application know when the WebAssembly module is loaded.
You do not say how you are handling state in your Vue application, but if you are e.g. using Vuex you can do something like this:
const doubPlugin = store => {
wasm.initialize().then(module => {
const doub = module.cwrap('doub', 'number', ['number'])
store.subscribe((mutation, state) => {
if (mutation.type === 'DOUB_REQUEST') {
store.commit('DOUB_RESULT', doub(mutation.payload))
}
})
store.commit('DOUB_READY')
})
}
const store = new Vuex.Store({
state,
mutations,
plugins: [doubPlugin]
})
I've done a similar thing in an Elm/WebAssembly application (relevant JavaScript), so if you want to see how this can be applied in practice you can check that out.
Making a wrapper JS module that performs initialization and re-exports the promise seems like the most straightforward approach.
// main.js
module.exports = require("./main.rs").initialize().then(module => {
return {
doub: module.cwrap('doub', 'number', ['number'])
};
});
Then anything can do
require("./main.js").then(api => {
console.log(api.doub(21));
});
and will always get the same module. Or alternatively you could invert the async part and do
// main.js
const api = require("./main.rs").initialize().then(module => {
return {
doub: module.cwrap('doub', 'number', ['number'])
};
});
exports.doub = async function (val) {
return (await api).doub(val);
};
Then users of your module could do
const api = require("./main.js");
api.doub(21).then(result => {
console.log(result);
});
I created a class to wrap the WebAssembly loading and created a cwrap for every function:
class mkLib {
ready = false
_mod = require("./main.rs").initialize().then(module => {
this._mod = module
this.doub = module.cwrap('doub', 'number', ['number'])
this.ready = true
})
}
export default mkLib
In the Vue component's data there is a variable for the new class and in watch I wait for a change in the ready property:
data () {
return {
mod: new mkLib,
ready: false
}
},
watch: {
'mod.ready': function () {
this.ready = true
// now this.mod.FUNC(PARAMS) can be used
console.log(this.mod.doub(20))
}
}
This question is specific to vuejs router, however may simply be a misunderstanding of importing js objects and assigning to the window object.
I am watching for url changes on a page which works fine with the watcher code in the component file. I need to use the same watcher code for multiple components so I extracted it to its own file, assigned it to the global scope, and cannot get it to work. Here are the details:
Working code in with the watcher in the component:
watch:{
$route () {
console.log('route changed')
//was it a reset?
console.log( this.$route.query.sort)
if(this.$route.query.sort === undefined){
if(this.$route.meta.reset){
//reset was pressed... actually do nothing here
this.$route.meta['reset'] = false;
}
else{
this.loading = true;
this.searchableTable.removeResultsTable();
this.searchableTable.options.search_query = this.$route.fullPath;
this.searchableTable.updateSearchPage();
}
}
else
{
//sort change just update the table view
}
}
}
So then I extracted the watch to a file routeWatcher.js:
export default {
$route () {
console.log('route changed')
//was it a reset?
console.log(this.$route.query.sort)
if (this.$route.query.sort === undefined) {
if (this.$route.meta.reset) {
//reset was pressed... actually do nothing here
this.$route.meta['reset'] = false;
}
else {
this.loading = true;
this.searchableTable.removeResultsTable();
this.searchableTable.options.search_query = this.$route.fullPath;
this.searchableTable.updateSearchPage();
}
}
else {
//sort change just update the table view
}
}
}
then I import and use, which works fine....
import searchableTableRouteWatcher from '../../controllers/routeWatcher'
...
watch:searchableTableRouteWatcher
again works fine.
Now the problem - I want to avoid the import in multiple files, so I thought I could put it on the window as a global
in my main.js file:
import searchableTableRouteWatcher from './controllers/routeWatcher'
window.searchableTableRouteWatcher = searchableTableRouteWatcher;
Then in my component:
watch:searchableTableRouteWatcher
results in searchableTableRouteWatcher is not defined
watch:window.searchableTableRouteWatcher
results in no errors, but the code is not being called
I have a feeling it has to do with this and there is confusion on $route()
For your purpose there are 'Mixins' in Vue.js: documentation
What you can do:
create a file, say mixins/index.js:
export const routeWatcher = {
watch: {... your watcher code pasted here ... }
};
import into your component:
import { routeWatcher } from 'path/to/mixins/index';
add mixin to your component properties and methods:
<script>
export default {
mixins: [routeWatcher];
data () ...... all your original component's script content
}
Mixin's content will be merged with component's original properties and act if it was hardcoded there.
Addition after your comment:
You can also declare Mixin globally, like this:
above 'new Vue' declaration put this code:
Vue.mixin({
watch: {....}
});
This mixin will appear in every component.