Trigger Quasar QBtn click using vue-test-utils / jest - vue.js

I am trying to simulate a button click of a quasar QBtn component in Jest(using vue-test-utils).
I need to test if the #click method gets called when the button is clicked so I did the following
it("Expects createAccount to be called", async () => {
const button = wrapper.findComponent(QBtn);
await button.trigger('click');
expect(methods.createAccount).toBeCalled();
})
And I also mocked createAccount function using jest.fn()
But I always get 0 calls of the function, although it works if I directly use
wrapper.vm.createAccount()
And just check if the function got called...
Any ideas how I can trigger the click event on the QBtn? I also tried using find('button') and triggering click, did not work either

I would do it this way 🤘 Hope it helps.
it("expects createAccount to be called when button is clicked", async () => {
// Arrange
const button = wrapper.find('BUTTON CLASS NAME');
const createAccount = jest.spyOn(wrapper.vm, 'createAccount');
// Action
await button.trigger('click');
await wrapper.vm.$nextTick();
// Assert
expect(createAccount).toHaveBeenCalled();
})

For those types of tests, I use cypress; personnaly I prefer to use jest to test methods, computed, etc...

Related

How to stop Vue.js 3 watch() API triggering on exit

I have implemented a watch within a Vue component that displays product information. The watch watches the route object of vue-router for a ProductID param to change. When it changes, I want to go get the product details from the back-end API.
To watch the route, I do this in Product.vue:
import { useRoute } from 'vue-router'
export default {
setup() {
const route = useRoute();
async function getProduct(ProductID) {
await axios.get(`/api/product/${ProductID}`).then(..do something here)
}
// fetch the product information when params change
watch(() => route.params.ProductID, async (newID, oldID) => {
await getProduct(newId)
},
//watch options
{
deep: true,
immediate: true
}
)
},
}
The above code works, except that if a user navigates away from Product.vue, for example using the back button to go back to the homepage, the watch is triggered again and tries to make a call to the API using undefined as the ProductID (becaues ProductID param does not exist on the homepage route) e.g. http://localhost:8080/api/product/undefined. This causes an error to be thrown in the app.
Why does the watch trigger when a user has navigated away from Product.vue?
How can this be prevented properly? I can do it using if(newID) { await getProduct(newId) } but it seems counterintuitive to what the watch should be doing anyway.
UPDATE & SOLUTION
Place the following at the top replacing the name for whatever your route is called:
if (route.name !== "YourRouteName") {
return;
}
That will ensure nothing happens if you are not on the route you want to watch.
I ran into the same problem. Instead of watching the current route, use vue-router onBeforeRouteUpdate, which only gets called if the route changed and the same component is reused.
From https://next.router.vuejs.org/guide/advanced/composition-api.html#navigation-guards:
import { onBeforeRouteLeave, onBeforeRouteUpdate } from 'vue-router'
import { ref } from 'vue'
export default {
setup() {
// same as beforeRouteLeave option with no access to `this`
onBeforeRouteLeave((to, from) => {
const answer = window.confirm(
'Do you really want to leave? you have unsaved changes!'
)
// cancel the navigation and stay on the same page
if (!answer) return false
})
const userData = ref()
// same as beforeRouteUpdate option with no access to `this`
onBeforeRouteUpdate(async (to, from) => {
// only fetch the user if the id changed as maybe only the query or the hash changed
if (to.params.id !== from.params.id) {
userData.value = await fetchUser(to.params.id)
}
})
},
}
watch registers the watcher inside an vue-internal, but component-independent object. I think it's a Map. So destroying the component has no effect on the reactivity system.
Just ignore the case where newID is undefined, like you already did. But to prevent wrapping your code in a big if block just use if(newID === undefined)return; at the beginning of your callback. If your ids are always truthy (0 and "" are invalid ids) you can even use if(!newID)return;.
well, in your use case the best approach would be to have a method or function which makes the api call to the server, having watch is not a really good use of it, because it will trigger whenever route changes and you do not want that to happen, what you want is simply get the productID from route and make the api call,
so it can be done with getting the productID in the created or mounted and make the api call!

Expo React Native execute function when entering view

I'm trying to make a function execute when a view is in foreground, but just once not on each update of the component. If the user navigates to another view and goes back to the first view it should execute that function again, but just once. Is there a solution to this?
if using useEffect without second parameter it executes on each update, if I add [] as second parameter it only executes the first time the view is rendered but not when navigating back to it.
Any help appreciated!
if you are using react-navigation you can do this by listen on screen focus see here
React.useEffect(() => {
const unsubscribe = navigation.addListener('focus', () => {
// The screen is focused
// Call any action
});
// Return the function to unsubscribe from the event so it gets removed on unmount
return unsubscribe;
}, [navigation]);

get relevant value from useSelector

I have two hooks:
const dispatch = useDispatch();
const response = useSelector(state => state.responseData);
And submit function:
const submit = () => {
dispatch(connectToServer(`${BASE_URL}user/signIn`, {
email: state.email,
password: state.password
}, 'post'))
if (response.data.token) <--- this data is 1 step late
//do smth
I see relevant data only in JSX elements when they are rendered, but there is no way to make a function based on this data, this data is 1 step late.
There's three issues here:
The connectToServer action is presumably doing some async work, so there's no way a response can have been updated by the time the next line runs
Even if you do await dispatch(connectToServer()), React may not have re-rendered yet
The callback can only reference the variable values that existed in scope at the time the callback was defined, ie, the render before the user clicked the "Submit" button.
You'll need to either:
Move the response handling into the thunk itself
Have the thunk retrieve the updated data from the store and return it / use it somehow
Move the response token handling into a useEffect and wait for the next re-render that has the right data

Trigger Topbar buttons in test

I'm trying to test navigation events on a screen using react-native-testing-library.
I'm listening to the events globally using Navigation.events().registerNavigationButtonPressedListener with the following hook inside my Functional component:
const useTopBarBtnPress = function (
componentId: string,
onTopBtnPressed: OnTopBtnPressed) {
useEffect(() => {
const topBtnListener = Navigation.events().registerNavigationButtonPressedListener((event) => {
if (event.componentId === componentId)
onTopBtnPressed(event, BtnIds)
})
return () => topBtnListener.remove()
}, [onTopBtnPressed])
}
Is it possible to simulate a topBar button for the test ? I guess using the testID but I can't find it in the doc.
Or do I need to mock registerNavigationButtonPressedListener ? Or use Detox ?
Also, is there a way to test the layout ? (eg. Icon color)
There's no need to actually mock TopBar buttons to test navigationButtonPressed. Simply invoke navigationButtonPressed yourself with the correct NavigationButtonPressedEvent parameter simulating a button press.

Access variables in a `useEffect` without triggering the effect when they get updated

I need to access variables in a useEffect but only trigger the method when SOME of them get updated.
For example:
I want to call useEffect when data changes, but NOT when saveTimeout or saveMethod change.
In the same fashion, I want to call saveMethod when my component dismounts, but I can't because it needs to be in the dependency array, therefore calling it at every change of saveMethod.
function SavingComponent({data, apiInfo}){
[saveTimeout, setSaveTimeout] = useState(null);
const saveMethod = useCallBack(()=>{
clearTimeout(saveTimeout);
// api call to save the data using apiInfo
}, [data, saveTimeout, apiInfo])
// Start a timer to save the data 2 seconds after it is changed (not working)
useEffect(()=>{
clearTimeout(saveTimeout);
setSaveTimeout(setTimeout(saveMethod, 2000));
}, [data, saveTimeout, saveMethod]);
// Save immediately on dismount only (not working)
useEffect(()=>{
return ()=> { saveMethod(); }
},[saveMethod])
return (// some rendering)
}
This is an issue I am constantly running into with other cases and have to hack around. Usually using a custom usePrevious method. I would compare the previous value of the prop to the current and return immediately if the prop I am interested in didn't change.
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref.current;
}
What is the proper way to only call useEffect when SOME dependencies get updated?
First thing that having variables and functions which are used in useEffect as dependencies is recommended and best practice. This is a very good article written by Dan Abramov which is deep dive in how useEffect works and why you need to follow that best practice.
Now back to your case.
I don't think you want the component re-render when you set saveTimeout, so instead use useState you can useRef for that saveTimeout = useRef(null) so you can remove saveTimeout from dependencies.
If you don't want saveMethod to be in dependencies you can move it inside useEffect so that you can remove it from dependencies too.
function SavingComponent({data, apiInfo}){
const saveTimeout = useRef(null);
// move saveMethod inside
useEffect(()=>{
const saveMethod = ()=>{
clearTimeout(saveTimeout.current);
// api call to save the data using apiInfo
}
// saveMethod will be called after 2 seconds
saveTimeout.current = setTimeout(saveMethod, 2000);
}, [data]);
// move saveMethod inside
useEffect(()=>{
const saveMethod = ()=>{
// api call to save the data using apiInfo
}
// saveMethod will be call when the component unmounted
// But make sure you are not update any state inside saveMethod
return saveMethod
},[])
return (// some rendering)
}