wrapper.find({ ref:''}) not working in vue testing utils - vue.js

I have this component (using Pug framework):
v-btn.float-right(
ref='copyContentButton'
x-small
v-on='{...ontooltip}'
#click='copyContent'
)
v-icon(small color='grey darken-2') $vuetify.icons.faCopy
span Copy content
I am trying to test the Copy content button using the test below:
describe('Comment.vue', () => {
let options: ShallowMountOptions<Vue>;
let wrapper: Wrapper<Vue>;
beforeEach(() => {
options = {
localVue,
propsData: {
correspondence: TEST_CORRESPONDENCE,
},
vuetify: new Vuetify(),
};
wrapper = shallowMount(CorrespondenceComment, options);
});
describe('copy button', () => {
it('should include a copy content button', () => {
const copyButton = wrapper.find({ ref: 'copyContentButton' });
expect(copyButton.exists()).toBe(true);
});
it('should copy text and indicate success', async () => {
const copyButton = wrapper.find({ ref: 'copyContentButton' });
const copyText = jest.fn();
(wrapper.vm as any).$copyText = copyText;
const info = jest.fn();
(wrapper.vm as any).$toasted = { info };
wrapper.setProps({
correspondence: {
...TEST_CORRESPONDENCE,
content: 'my message content',
},
});
copyButton.trigger('click');
await wrapper.vm.$nextTick();
expect(copyText).toHaveBeenCalledWith('my message content');
expect(info).toHaveBeenCalledWith('Content copied to clipboard.');
});
});
The tests are failing because it is not able to find the component by ref, even though this is the right syntax.
Any idea what I may be missing?

Related

Is there a way to make my vue jest test with mock vuex instead of the app store

const localVue = createLocalVue();
localVue.use(Vuex);
describe('Dashboard component', () => {
let store;
let userDataStore;
beforeEach(() => {
userDataStore = {
namespaced: true,
state: {
sessionId: 'k5gv7lc3jvol82o91tddjtoi35kv16c3',
},
};
store = new Vuex.Store({
modules: {
userDataStore: userDataStore,
},
});
});
it('it renders the header component if there is a session id', () => {
const wrapper = shallowMount(DashboardPage, {
store,
localVue,
});
const headerComponent = wrapper.findComponent({ name: 'DashboardPage' });
expect(headerComponent.exists()).toBe(true);
});
});
but it keeps trying to access the app's main vuex and it should instead make use of the test mockup.

Vue unit test is holds old component code in wrapper

I am doing unit test of vue component methods through vue-test-utils and facing an weird issue. wrapper.vm.somemthod() is executing old code that was written earlier inside the method. It's printing old console.log statements. If I put new console.log statement, it's not printing that at all. Am I missing something?
import { mount, shallowMount } from '#vue/test-utils'
import TestComponent from '#/components/TestComponent.vue'
const mockMixin = {
methods: {
InnerMethod() {
return 2;
},
}
}
describe('Test Screen', () => {
let wrapper;
let mock;
beforeAll(() => {
mock = new MockAdapter(api);
})
beforeEach(() => {
jest.resetModules();
wrapper = mount(TestComponent, {
i18n,
vuetify,
mixins: [mockMixin],
data() {
},
mocks: {
$t: (msg) => msg,
$config: (value) => value,
$store: (value) => value,
$route: (value) => value,
}
});
})
afterEach(() => {
mock.reset();
wrapper.destroy();
wrapper = null;
});
describe('Component Test', () => {
it('getdata', async () => {
expect(wrapper.vm.somedata.length).toBe(0);
const spy = jest.spyOn(wrapper.vm, 'InnerMethod');
wrapper.vm.someMethod();
expect(spy).toBeCalledTimes(1);
expect(wrapper.vm.somedata.length).toBe(2);
});
});
});

Timeout simulation not working with testing-library and useFakeTimers

I'm working on a vueJS component that allows to display a modal after 5 seconds. the component works well as expected.
<template>
<vue-modal v-if="showModal" data-testid="modal-testid" />
</template>
<script>
export default {
name: "TimeoutExample",
data() {
return {
showModal: false,
}
},
mounted() {
setTimeout(() => this.displayModal(), 5000)
},
methods: {
displayModal: function() {
this.showModal = true;
}
}
};
</script>
I implemented the unit tests using jest, testing-library and I wanted to use jest.useFakeTimers to simulate the timeout, but the test is KO.
// testing file
describe.only('Vue Component (mobile) 2', () => {
beforeAll(() => {
isMobile.mockImplementation(() => true)
})
beforeEach(() => {
jest.useFakeTimers()
})
afterEach(() => {
jest.runOnlyPendingTimers()
jest.useRealTimers()
})
it('should render title after `props.delay` milliseconds', () => {
const { queryByTestId } = myRender({
localVue: myMakeLocalVue(),
})
jest.advanceTimersByTime(5001)
expect(queryByTestId('modal-testid')).toBeVisible()
})
})
do you have any idea how i can test this behavior?
remove this jest.spyOn(global, 'setTimeout'). jest will do it's own magic with for this with useFakeTimers
I suppose you can not use async and done callback in one test case. Which version of jest do you use?
Add await localVue.$nextTick() after advanceTimersByTime to wait until Vue apply all the changes
It works for me after calling advanceTimersByTime inside waitFor.
describe.only('Vue Component (mobile) 2', () => {
beforeAll(() => {
isMobile.mockImplementation(() => true)
})
beforeEach(() => {
jest.useFakeTimers()
})
afterEach(() => {
jest.runOnlyPendingTimers()
jest.useRealTimers()
})
it('should render title after `props.delay` milliseconds', async () => {
const { queryByTestId } = myRender({
localVue: myMakeLocalVue(),
})
await waitFor(() => {
jest.advanceTimersByTime(5001)
})
expect(queryByTestId('modal-testid')).toBeVisible()
})
})

How can I DRY up the code by mounting Vue component in the beforeEach hook using typescript?

Here is my code. I want to DRY up this case.
describe("Stored id", () => {
it("ID empty", () => {
// when
const wrapper = mount(SigninPage, options);
const vm = wrapper.vm;
});
it("ID exist", () => {
// when
localStorage.setItem(process.env.VUE_APP_SIGNIN_STORED_USER_ID, STORED_ID);
const wrapper = mount(SigninPage, options);
const vm = wrapper.vm;
});
});
How can I use the beforeEach hook like next using typescript?
I want to use the beforeEach hook. But I can not running test because of tsc. I think it will be possible when variable types is correct.
describe("Stored id", () => {
// problem
let wrapper: VueWrapper<??>;
let vm: ??;
beforeEach(() => {
wrapper = mount(SigninPage);
vm = wrapper.vm;
});
it("ID empty", () => {
// const wrapper = mount(SigninPage, options);
// const vm = wrapper.vm;
});
it("ID exist", () => {
// Should I save it before the wrapper is mounted?
localStorage.setItem(process.env.VUE_APP_SIGNIN_STORED_USER_ID, STORED_ID);
// const wrapper = mount(SigninPage, options);
// const vm = wrapper.vm;
});
});
Self answer. I handled like this.
describe("Header", () => {
// eslint-disable-next-line #typescript-eslint/no-explicit-any
let wrapper: VueWrapper<any>;
const code = "A005930";
const options = { propsData: { code } };
beforeEach(async () => {
wrapper = shallowMount(Header, options);
});
it("should have stored data set", async () => {
const mockQueryParams = { code: "A005930", page: 0, size: 100 };
await store.fetchPriceData(mockQueryParams);
// ...
});
// ...
}

Can't get props to work by passing data through vue-router

I found numorous of similar questions, tried almost all of them including defining params value as a string, and so on but still can't make it work.
I have defined the route to accept props as written on the docs:
const routes = [
...
...
{
// this doesn't work, but passed on the url
// path: '/profile/personal/update/:original',
path: '/profile/personal/update',
name: 'profile.personal.update',
component: () => import('#/views/Profile/Personal/PersonalUpdate.vue'),
props: true,
// props: route => ({ default: route.params.original }), // this doesn't work
},
]
const router = createRouter({
history: createWebHistory(process.env.BASE_URL),
routes,
})
export default router
When I try to push the route and pass the data, typescript complains that I can't pass object to the params property, so I tried to cast it as any and string by doing like this, it passed:
// Somewhere inside the usePersonalInfoComposition
const router = useRouter()
const personalInfo = reactive({
data: new PersonalInfo(),
isLoading: true,
})
const editPersonalInfo = () => {
logger.info('Preparing data to be edited and redirect')
router.push({
name: 'profile.personal.update',
params: {
original: JSON.stringify(personalInfo.data)
// original: personalInfo.data as any
}
})
}
However, when I try to grab the value of the prop which the value is given from route params, I got undefined.
export default defineComponent({
props: ['original'],
setup(props) {
const router = useRouter()
const { personalInfo, updatePersonalInfo } = usePersonalInfoComposition()
onBeforeMount(() => console.log(props.original)) // undefined
onMounted(() => console.log(props.original)) // undefined
// I would do this personally but it was kinda problematic
// and error-prone especially when the page is refreshed.
// So I decided to not doing it. Also, the `props` are now useless.
onBeforeMount(() => {
const original = router.currentRoute.value.params['original'] // "{ user: { id: 1, name: 'joe' } }"
let parsed;
// I need to do this since `params` returns either `string | string[]`
if (original instanceof Array) parsed = JSON.parse(original[0])
else parsed = JSON.parse(original)
console.log(parsed) // { user: { id: 1, name: 'joe' } }
})
}
})
Is there any way to pass the original data without only passing id and make the user fetch the whole time they refreshes the page? I see Vuex has some solution for this, but I don't want to rebuild the whole app from scratch only for this kind of problem.
I was wondering about using local storage like sqlite or the browser's built in local storage but I think it will add more unnecessary complexity. How do you think?
After a while, It comes to a solution but with different approach to fix this problem. I'm not a state management expert but it works.
Figuring out the problem
Let's say I have this on the personal info page:
PersonalInfo.vue
export default defineComponent({
name: 'PersonalInfoPage',
setup() {
const {
personalInfo,
fetchPersonalInfo,
editPersonalInfo,
} = usePersonalInfoComposition()
onBeforeMount(() => fetchPersonalInfo())
return {
personalInfo,
}
}
})
And an update page:
PersonalInfoUpdate.vue
export default defineComponent({
name: 'PersonalInfoUpdatePage',
setup() {
const { personalInfo } = usePersonalInfoComposition()
return {
personalInfo,
}
}
})
Both has the same state being used, but from what I see, the composition was creating a new instance every time I call the method so the previously fetched state is not there and I need to re-fetch the same stuff all over again.
Solution
Instead of doing it that way, I lifted the state up above the composition method to keep the fetched state and make a "getters" by using the computed property to be consumed inside my PersonalUpdate.vue
PersonalInfoComposition.ts -- before
export const usePersonalInfoComposition = () => {
const personalInfo = reactive({
data: new PersonalInfo(),
isLoading: true,
})
const editPersonalInfo = () => {
router.push({
name: 'profile.personal.update',
params: {
original: JSON.stringify(personalInfo.data),
}
})
}
const fetchPersonalInfo = () => {
state.isLoading = true
repository
.fetch()
.then(info => state.data = info)
.catch(error => logger.error(error))
.finally(() => state.isLoading = false)
}
return {
personalInfo,
fetchPersonalInfo,
editPersonalInf,
}
}
PersonalInfoComposition.ts -- after
const state = reactive({
data: new PersonalInfo(),
isLoading: true,
})
export const usePersonalInfoComposition = () => {
const personalInfo = computed(() => state.data)
const isLoading = computed(() => state.isLoading)
const editPersonalInfo = () => {
router.push({ name: 'profile.personal.update' })
}
const fetchPersonalInfo = () => {
state.isLoading = true
repository
.fetch()
.then(info => state.data = info)
.catch(error => logger.error(error))
.finally(() => state.isLoading = false)
}
return {
personalInfo,
isLoading,
fetchPersonalInfo,
editPersonalInfo,
}
}
Now it works, and no Vuex or local storage or even route.params needed.
The Downside
Since the state is shared, whenever I made any changes on the state attached to a v-model, the data inside the PersonalInfo.vue are also be changed.
This could be easily fixed by creating a clone of the state object to handle the updates and sync it whenever the new data persisted in the database.
PersonalInfoComposition.ts -- fixing the downside
const state = reactive({
data: new PersonalInfo(),
isLoading: true,
})
const editState = reactive({
data: new PersonalInfo(),
isLoading: true,
})
export const usePersonalInfoComposition = () => {
const personalInfo = computed(() => state.data)
const personalInfoClone = computed(() => editState.data)
const isLoading = computed(() => state.isLoading)
const editIsLoading = computed(() => editState.isLoading)
const editPersonalInfo = () => {
// This will clone the object as well as its reactivity, dont do this
// editState.data = state.data
// Clone the object and remove its reactivity
editState.data = JSON.parse(JSON.stringify(state.data))
router.push({ name: 'profile.personal.update' })
}
const fetchPersonalInfo = () => {
state.isLoading = true
repository
.fetch()
.then(info => state.data = info)
.catch(error => logger.error(error))
.finally(() => state.isLoading = false)
}
const updatePersonalInfo = () => {
editState.isLoading = true
repository
.update(editState.data.id)
.then(info => {
// This could be extracted to a new method, but keep it simple for now
state.data = info
if (state.data === info) router.back()
})
.catch(error => logger.error(error))
.finally(() => editState.isLoading = false)
}
return {
personalInfo,
personalInfoClone,
isLoading,
editIsLoading,
fetchPersonalInfo,
}
}