Reputation: 1440
I'm attempting to write a data-fetching "hook" (not entirely clear what the word for this is in Vue, but a state function that does not render a template). The Hook takes an asynchronous data resolver as a parameter. The hook itself is very simple, it simply adds loading state to a function that returns a promise.
import { ref, watch } from "vue";
export function useDataFetcher<T>(
resolver: () => Promise<T>,
) {
const isLoading = ref(false);
const data = ref<T>();
watch(resolver, () => {
isLoading.value = true;
resolver(...parameters)
.then((fetchedData) => {
data.value = fetchedData;
})
.finally(() => {
isLoading.value = false;
});
});
return {
isLoading: isLoading.value,
data,
parameters,
};
}
I am attempting to write a test against this function to ensure that the isLoading method is updating correctly:
import { useDataFetcher } from "./useDataFetcher";
test("While the fetcher is loading data, isLoading should be true", async () => {
const promise = new Promise<void>((resolver) =>
setTimeout(() => resolver(), 2000)
);
const { isLoading } = useDataFetcher(() => promise);
expect(isLoading).toBeTruthy();
await promise;
expect(isLoading).toBeFalsy();
});
As written, this test is not working. I have not seen a lot of information in the interwebs about testing these state functions in Vue. There are two stack overflow questions that seem related: Is it possible to test a Vue 3 Composition library which uses inject without a wrapping component?
and How to unit test standalone Vue composition
But neither of these seem to quite scratch the itch I'm having here.
In React, you have the @testing-library/react-hooks library to manage these tests, and it makes it very simple. It seems to me that I'm missing something to the effect of await Vue.nextTick()
.
So, finally, the question: What exactly is the best way to test these Vue hooks that don't render templates?
Upvotes: 2
Views: 2305
Reputation: 1440
So, I ended up putting together a solution for my problem and publishing an npm module for it: https://www.npmjs.com/package/vue-composable-function-tester. I would love feedback on the solution.
Here's an example of what it looks like:
it("Reacts to a resolving promise", async () => {
const resolvedData = {
hello: "world",
};
const promise = Promise.resolve(resolvedData);
const composable = mountComposableFunction(() =>
useAsynchronousLoader(() => promise)
);
await composable.nextTick();
expect(composable.data.isLoading.value).toBe(true);
await promise;
await composable.nextTick();
expect(composable.data.data.value).toStrictEqual(resolvedData);
await composable.nextTick();
expect(composable.data.isLoading.value).toBe(false);
});
export function useAsynchronousLoader<T>(promiseCreator: () => Promise<T>) {
const isLoading = ref(false);
const data = ref<T>();
const error = ref<object>();
isLoading.value = true;
promiseCreator()
.then((newData) => {
data.value = newData;
})
.catch((e) => {
error.value = e;
})
.finally(() => {
isLoading.value = false;
});
return {
isLoading,
data,
error,
};
}
EDIT: Improved code samples.
Upvotes: 1
Reputation: 3840
You need to return the loading ref to keep the reactivity.
return {
isLoading,
data,
parameters,
};
By passing isLoading.value
you only pass the value at that time and loose the reactivity
Upvotes: 0