I am trying to simulate choosing an option in a <select>
element in my Vitest test. The simulated selection should set the value of the ref that is bound to the <select>
's v-model
. As you can see below, I have tried many different ways, based on other questions here and elsewhere on the internet, but I just cannot make it work in my case.
I am using the following:
I have created a very basic Vue component that includes a heading and a <select>
element:
<script setup lang="ts">
import { ref } from "vue";
const items = ["item 1", "item 2", "item 3", "item 4", "item 5"];
const selectedItem = ref<string>("");
// this is a placeholder for a more complex function
async function handleSubmit(): Promise<void> {
console.log("in handleSubmit()");
console.log(quotedString(selectedItem.value));
}
// this is just here for a prettier output
function quotedString(s: string): string {
return s === undefined ? s : '"' + s + '"';
}
</script>
<template>
<h1>Foo</h1>
<p>
<select v-model="selectedItem">
<option disabled value="">Please Select</option>
<option v-for="item in items" :value="item" :key="item">
{{ item }}
</option>
</select>
</p>
<p>Selected {{ selectedItem }}</p>
<p><button @click="handleSubmit">Submit</button></p>
</template>
The <select>
is bound to selectedItem
and when I open that code in the browser everything works fine, meaning that after selecting an element the name of the element is shown on the page and after clicking the submit button, the name of the element is written to the console.
But when I try the following test code, the statement console.log(quotedString(selectedItem.value));
either produces undefined or an empty string, meaning that the simulated selection did not work. Here is the test code including several different attempts.
import { describe, test } from "vitest";
import { shallowMount, VueWrapper } from "@vue/test-utils";
import FooView from "./FooView.vue";
describe("FooView", (): void => {
test("can simulate select selection", async (): Promise<void> => {
const fooViewWrapper = shallowMount(FooView);
const selectFieldWrapper = fooViewWrapper.find("select");
const options = selectFieldWrapper.findAll("option");
// ATTEMP 0
// This is the preferred way according to https://v1.test-utils.vuejs.org/api/wrapper/setvalue.html but still does not work
// await selectFieldWrapper.setValue("item 2");
// expect(selectFieldWrapper.element.value).toBe("item 2");
// alternatively
// selectFieldWrapper.element.value = "item 2";
// selectFieldWrapper.trigger('change')
// expect(selectFieldWrapper.element.value).toBe("item 2");
// ATTEMPT 1
// // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
// await options.at(2)!.trigger("click");
// --> output: empty string
// ATTEMPT 2
// await options.at(2)?.setSelected(); // setSelected() is private :(
// --> output: empty string
// ATTEMPT 3
// // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
// options.at(2)!.element.selected = true;
// // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
// await options.at(2)!.trigger("input");
// --> output: empty string
// ATTEMPT 4
// await selectFieldWrapper.setValue("item 2");
// await selectFieldWrapper.trigger("change");
// --> output: undefined
// ATTEMPT 5
// await selectFieldWrapper.setValue("item 2");
// await selectFieldWrapper.trigger("input");
// --> output: undefined
// ATTEMPT 6
// await selectFieldWrapper.setValue("item 2");
// await selectFieldWrapper.trigger("click");
// --> output: undefined
// ATTEMPT 7
// selectFieldWrapper.element.selectedIndex = 2;
// await selectFieldWrapper.trigger("change");
// --> output: undefined
// ATTEMPT 8
// selectFieldWrapper.element.selectedIndex = 2;
// await selectFieldWrapper.trigger("input");
// --> output: empty string
// ATTEMPT 9
// selectFieldWrapper.element.selectedIndex = 2;
// await selectFieldWrapper.trigger("click");
// --> output: empty string
// ATTEMPT 10
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
// await options.at(2)!.trigger("click");
// --> output: empty string
// ATTEMPT 10
// await selectFieldWrapper.trigger("click");
// // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
// await options.at(2)!.trigger("click");
// await selectFieldWrapper.trigger("input");
// --> output: empty string
// ATTEMPT 11
// selectFieldWrapper.element.click();
// // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
// options.at(2)!.element.click();
// await nextTick();
// --> output: empty string
// ATTEMPT 12
// // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
// options.at(1)!.trigger('click');
// await nextTick();
// --> output: empty string
// ATTEMPT 13
// await selectFieldWrapper.setValue(2);
// await selectFieldWrapper.trigger("click");
// --> output: undefined
// ATTEMPT 14
// await selectFieldWrapper.setValue(2);
// await selectFieldWrapper.trigger("change");
// --> output: undefined
// selectFieldWrapper.element.selectedIndex = 2;
// await selectFieldWrapper.trigger("input");
// console.log(options.at(2)?.element.selected);
// console.log(selectFieldWrapper.element.selectedIndex);
await clickSubmitButton(fooViewWrapper);
});
});
async function clickSubmitButton(fooViewWrapper: VueWrapper): Promise<void> {
const submitButtonWrapper = fooViewWrapper.find("button");
submitButtonWrapper.element.click();
}
What I find interesting is that I can set the selected option like so:
options.at(2)!.element.selected = true;
console.log(options.at(2)?.element.selected); // prints *true*
But even then I do not get the desired output.
To answer my own question: The issue was not with the test itself but with the vitest configuration. I was using happy-dom as its environment, which apparently does not work in this particular case. Switching the environment to jsdom was the solution. With this my Attempt 0 was successful, so this works:
await selectFieldWrapper.setValue("item 2");
expect(selectFieldWrapper.element.value).toBe("item 2");