I am currently writing tests for a Vue Component which implements a Vuetify Switch. As part of the testing I want to check the functionality of the vuetify switch. I am having troubling triggering a click on the switch to then verify that the switches value has changed (and once I have done that I will verify that the value bound to the switch has changed as well)
I have looked at the API docs for Vuetify and there are no methods to directly set the state of a Vuetify switch which is bewildering in my opinion. Because of this I am trying to perform a click on the VSwitch component using wrapper.find().trigger('click') but this isn't changing the switch value, leading me to believe the click isn't doing anything at all.
Below are two tests
the first checks that the switch has the correct state on creation, which is passing
The second tries to perform a click event and check that the state has changed, which is failing
Any help in resolving this problem would be greatly appreciated.
switch.vue
<template>
<v-row>
<v-col>
<label class="label-text" :for="`${fieldLabel}`">{{labelText}}</label>
<v-row>
<label class="left-label">{{toggleLeftText}}</label>
<v-switch
:id="`${fieldLabel}`"
v-model="toggleState"
class="ma-0 pa-0"
:data-qa="`${fieldLabel}Checkbox`"
>
</v-switch>
<label class="right-label">{{toggleRightText}}</label>
</v-row>
<!--Hidden input field includes switch value in form when submitted-->
<input type="hidden" :value="toggleState" :name="`${fieldLabel}`">
</v-col>
</v-row>
</template>
<script>
export default {
name: "Switch",
props: {
fieldLabel: {
type: String,
required: true
},
labelText: {
type: String,
required: true
},
toggleLeftText: {
type: String,
required: true
},
toggleRightText: {
type: String,
required: true
},
toggleValue: {
type: Boolean,
required: true
},
},
data: function () {
return {
toggleState: this.toggleValue
}
}
}
</script>
switch.spec.js
describe('Switch', () => {
const toggleState = true;
const localVue = createLocalVue();
localVue.use(Vuetify, {
components: {
VRow,
VCol,
VSwitch,
InputError
}
});
const wrapperFactory = () => {
return shallowMount(Switch, {
localVue,
vuetify: new Vuetify(),
propsData: testProps,
});
};
const testProps = {
labelText: "Test Label",
fieldLabel: "testLabel",
toggleLeftText: "No",
toggleRightText: "Yes",
toggleValue: toggleState
};
let wrapper;
beforeEach(() => {
wrapper = wrapperFactory(testProps);
});
afterEach(() => {
wrapper.destroy();
});
it("should have correct toggle value", () => {
const vSwitch = wrapper.find(VSwitch);
expect(vSwitch.vm.value).toBe(toggleState);
});
it("should have correct toggle value after click", async () => {
const vSwitch = wrapper.find(VSwitch);
await vSwitch.trigger('click');
expect(vSwitch.vm.value).toBe(!toggleState);
});
});
I might be a bit late for answering your question, but this way you should be able to get your v-switch.
const vSwitch = wrapper.find({ name: 'v-switch' });
and then trigger the event with
vSwitch.$emit('change', <true or false>);, depending on what you're testing.
The limit with this approach is that if you have multiple v-switches in your code, you would need to target them with a data-test-id, for example like this:
<v-switch data-test-id="my-switch-1"> ... </v-switch>;
<v-switch data-test-id="my-switch-2"> ... </v-switch>;
and then I defined a helper function on top of my test file, like so:
const getSwitchComponent = (wrapper: Wrapper<Vue>, testId: string): Wrapper<Vue> => {
const switches = wrapper.findAll({ name: 'v-switch' });
const component = switches.wrappers.find(wrapper =>
wrapper.contains(`[data-test-id="${testId}"]`),
);
if (!component) {
throw Error(`Element not found: ${testId}`);
}
return component;
};
which will let you do something like this:
const mySwitch1 = getSwitchComponent(wrapper, 'my-switch-1');
const mySwitch2 = getSwitchComponent(wrapper, 'my-switch-2');
and trigger the change event so:
mySwitch1.vm.$emit('change', false);
mySwitch2.vm.$emit('change', true);
Related
We have a requirement where I have to add a checkbox on the intro.js pop-up,
so I have used intro.js min and CSS files CDN links to use the Intro.js,
currently, I'm getting intro.js pop-up with the intro and checkbox,
Issues I am facing are:
If I am using checkboxes on multiple steps then I am not able to fetch the checkbox value, its works in the case of one step
Is there any other way to add a checkbox in intro.js
when I click the checkbox and click on the done button the value of the checkbox should be passed to the backend
I Have tried something like.
Reactjs code
const HomePage: React.FunctionComponent<Props> = ({ id }: Props) => {
const [checkTour, setCheckTour] = React.useState(false);
React.useEffect(() => {
if ((window as any).introJs) {
(window as any)
.introJs()
.setOptions({
disableInteraction: true,
exitOnEsc: false,
exitOnOverlyClicking: false,
overlayOpacity: 0.7,
showBullets: false,
showProgress: false,
showStepNumbers: false,
tooltipClass: "customTooltip",
steps: [
{
title: "Welcome",
intro: `Hello World! ๐ `,
element: document.querySelector("#logoHeighlight"),
tooltipClass: "welcomeTooltip",
},
{
intro: "<style>{color: 'red';};</style>step 1"! ๐ `,
element: document.querySelector("#cart"),
},
{
intro: `Go to plp page <label for='donotShowMeAgain'> Do Not Show Me Again </label><input type='checkbox' id='donotShowMeAgain'>`,
element: document.querySelector("#HeaderSignIn"),
},
],
})
.onbeforeexit(function () {
// eslint-disable-next-line no-console
console.log("before Done Calling");
})
.start();
const doneTour = document.getElementById("donotShowMeAgain");
doneTour?.addEventListener("click", donotShowMeAgainClicked);
return () => {
doneTour?.removeEventListener("click", donotShowMeAgainClicked);
};
}
}, []);
const donotShowMeAgainClicked = (e: any) => {
setCheckTour(e.target.checked);
};
// eslint-disable-next-line no-console
console.log("checkTour", checkTour);
return (
<Page data-test-selector="homePage" {...styles.homePageContainer} className="homePageContainer">
<Zone contentId={id} zoneName="Content" requireRows />
<AddToListModal />
</Page>
);
};
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.5.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.5.0/umd/react-dom.production.min.js"></script>
I have the following code to test the value of the Select element after changing:
it("changes value after selecting another field", () => {
doSetupWork();
let field = screen.getByLabelText("MySelectField");
expect(field).toHaveValue("");
fireEvent.change(field, { target: { value: "1" } });
// Insert one of two options from below
});
However, when I insert the following at the bottom, it does not work:
field = screen.getByLabelText("MySelectField");
expect(field).toHaveValue("1");
and gives the following error message:
Expected the element to have value: 1
Received:
But, when I wrap it in a setTimeout with just 1ms delay, it does work:
setTimeout(() => {
field = screen.getByLabelText("MySelectField");
expect(field).toHaveValue("1");
}, 1);
It feels like there should be a more elegant way of writing this without setTimeout. Any advice?
When I am using react-testing-library I tend to use render when I have events to interact with.
For instance:
In my App.js I have this code on the return method
const handleChoice = () => {};
const attributes = [
{ label: "One", value: "1" },
{ label: "Two", value: "2" },
{ label: "Three", value: "3" }
];
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<select onChange={handleChoice} data-testid="MySelectField">
<option value="0">Zero</option>
{attributes.map((item) => {
return (
<option key={item.value} value={item.value}>
{item.label}
</option>
);
})}
</select>
</div>
);
then my test would be something like this:
import { fireEvent, render } from "#testing-library/react";
import "#testing-library/jest-dom";
import App from "./App";
it("changes value after selecting another field", () => {
const { getByTestId } = render(<App />);
let field = getByTestId("MySelectField");
expect(field).toHaveValue("0");
fireEvent.change(field, { target: { value: "1" } });
expect(field.value).toBe("1");
fireEvent.change(field, { target: { value: "3" } });
expect(field.value).toBe("3");
// Insert one of two options from below
});
Take a look in this sandbox to see it working.
https://codesandbox.io/s/ecstatic-https-hzi5n?file=/src/App.spec.js
You could try using waitFor instead of setTimeout:
import {waitFor} from '#testing-library/react'
...
await waitFor(() => screen.getByLabelText("MySelectField").toHaveValue("1"))
What you're seeing is that it takes a finite amount of time for your app to react (excuse the pun) to the change that has occurred - specifically, it needs to re-render.
And yes, there is a nicer way - the waitFor function from testing-library/react:
import { screen, waitFor } from '#testing-library/react';
...
it("changes value after selecting another field", async () => {
...
fireEvent.change(field, { target: { value: "1" } });
await waitFor(async () => {
field = screen.getByLabelText("MySelectField");
expect(field).toHaveValue("1");
});
}
Note that the entire test body (i.e. after the test's name) has to be declared async in order to be able to await the new waitFor block.
I am new to Vuejs. I am using Primevue library to build the api using the composition vuejs 3.
my problem is that menu is not updating. I want to hide the show button when the element is shown and vice versa. I search all the internet and tried all the solutions I found but in vain.
Any help is appreciated, thank you
export default {
name: "Quote",
components: {
loader: Loader,
"p-breadcrumb": primevue.breadcrumb,
"p-menu": primevue.menu,
"p-button": primevue.button,
},
setup() {
const {
onMounted,
ref,
watch,
} = Vue;
const data = ref(frontEndData);
const quoteIsEdit = ref(false);
const toggle = (event) => {
menu.value.toggle(event);
};
const quote = ref({
display_item_date: true,
display_tax: true,
display_discount: false,
});
const menu = ref();
const items = ref([
{
label: data.value.common_lang.options,
items: [{
visible: quote.value.display_item_date,
label: data.value.common_lang.hide_item_date,
icon: 'pi pi-eye-slash',
command: (event) => {
quote.value.display_item_date = !quote.value.display_item_date;
}
},
{
visible: !quote.value.display_item_date,
label: data.value.common_lang.unhide_item_date,
icon: 'pi pi-eye',
command: () => {
quote.value.display_item_date = !quote.value.display_item_date;
}
}
]
]);
}
return {
data,
quoteIsEdit,
menu,
items,
toggle
};
},
template:
`
<div class="container-fluid" v-cloak>
<div class="text-right">
<p-menu id="overlay_menu" ref="menu" :model="items" :popup="true"></p-menu>
<p-button icon="pi pi-cog" class="p-button-rounded p-button-primary m-2" #click="toggle" aria-haspopup="true" aria-controls="overlay_menu"></p-button>
<p-button :label="data.common_lang.save + ' ' + data.common_lang.quote" class=" m-2" /></p-button>
</div>
</div>
`
};
The problem is the items subproperty change is not reactive, so the items.value.items[].visible props are not automatically updated when quote.value.display_item_date changes.
One solution is to make items a computed prop, so that it gets re-evaluated upon changes to the inner refs:
// const items = ref([...])
const items = computed(() => [...])
demo
I have a data structure with nested objects that I want to bind to sub-components, and I'd like these components to edit the data structure directly so that I can save it all from one place. The structure is something like
job = {
id: 1,
uuid: 'a-unique-value',
content_blocks: [
{
id: 5,
uuid: 'some-unique-value',
block_type: 'text',
body: { en: { content: 'Hello' }, fr: { content: 'Bonjour' } }
},
{
id: 9,
uuid: 'some-other-unique-value',
block_type: 'text',
body: { en: { content: 'How are you?' }, fr: { content: 'Comment รงa va?' } }
},
]
}
So, I instantiate my sub-components like this
<div v-for="block in job.content_blocks" :key="block.uuid">
<component :data="block" :is="contentTypeToComponentName(block.block_type)" />
</div>
(contentTypeToComponentName goes from text to TextContentBlock, which is the name of the component)
The TextContentBlock goes like this
export default {
props: {
data: {
type: Object,
required: true
}
},
created: function() {
if (!this.data.body) {
this.data.body = {
it: { content: "" },
en: { content: "" }
}
}
}
}
The created() function takes care of adding missing, block-specific data that are unknown to the component adding new content_blocks, for when I want to dynamically add blocks via a special button, which goes like this
addBlock: function(block_type) {
this.job.content_blocks = [...this.job.content_blocks, {
block_type: block_type,
uuid: magic_uuidv4_generator(),
order: this.job.content_blocks.length === 0 ? 1 : _.last(this.job.content_blocks).order + 1
}]
}
The template for TextContentBlock is
<b-tab v-for="l in ['fr', 'en']" :key="`${data.uuid}-${l}`">
<template slot="title">
{{ l.toUpperCase() }} <span class="missing" v-show="!data.body[l] || data.body[l] == ''">(missing)</span>
</template>
<b-form-textarea v-model="data.body[l].content" rows="6" />
<div class="small mt-3">
<code>{{ { block_type: data.block_type, uuid: data.uuid, order: data.order } }}</code>
</div>
</b-tab>
Now, when I load data from the API, I can correctly edit and save the content of these blocks -- which is weird considering that props are supposed to be immutable.
However, when I add new blocks, the textarea above wouldn't let me edit anything. I type stuff into it, and it just deletes it (or, I think, it replaces it with the "previous", or "initial" value). This does not happen when pulling content from the API (say, on page load).
Anyway, this led me to the discovery of immutability, I then created a local copy of the data prop like this
data: function() {
return {
block_data: this.data
}
}
and adjusted every data to be block_data but I get the same behaviour as before.
What exactly am I missing?
As the OP's comments, the root cause should be how to sync textarea value between child and parent component.
The issue the OP met should be caused by parent component always pass same value to the textarea inside the child component, that causes even type in something in the textarea, it still bind the same value which passed from parent component)
As Vue Guide said:
v-model is essentially syntax sugar for updating data on user input
events, plus special care for some edge cases.
The syntax sugar will be like:
the directive=v-model will bind value, then listen input event to make change like v-bind:value="val" v-on:input="val = $event.target.value"
So adjust your codes to like below demo:
for input, textarea HTMLElement, uses v-bind instead of v-model
then uses $emit to popup input event to parent component
In parent component, uses v-model to sync the latest value.
Vue.config.productionTip = false
Vue.component('child', {
template: `<div class="child">
<label>{{value.name}}</label><button #click="changeLabel()">Label +1</button>
<textarea :value="value.body" #input="changeInput($event)"></textarea>
</div>`,
props: ['value'],
methods: {
changeInput: function (ev) {
let newData = Object.assign({}, this.value)
newData.body = ev.target.value
this.$emit('input', newData) //emit whole prop=value object, you can only emit value.body or else based on your design.
// you can comment out `newData.body = ev.target.value`, then you will see the result will be same as the issue you met.
},
changeLabel: function () {
let newData = Object.assign({}, this.value)
newData.name += ' 1'
this.$emit('input', newData)
}
}
});
var vm = new Vue({
el: '#app',
data: () => ({
options: [
{id: 0, name: 'Apple', body: 'Puss in Boots'},
{id: 1, name: 'Banana', body: ''}
]
}),
})
.child {
border: 1px solid green;
}
<script src="https://unpkg.com/vue#2.5.16/dist/vue.js"></script>
<div id="app">
<span> Current: {{options}}</span>
<hr>
<div v-for="(item, index) in options" :key="index">
<child v-model="options[index]"></child>
</div>
</div>
I have a function named toggleFilter() in a react component which looks like this:
toggleFilter = (filterType, filterName) => {
const filterApplied = this.state.appliedFilterList[filterType].includes(filterName);
if (filterApplied) {
//Remove the applied filter
this.setState(prevState => ({
appliedFilterList: {
...prevState.appliedFilterList,
[filterType]: prevState.appliedFilterList[filterType].filter(filter => filter !== filterName)
}
}));
} else {
//Add the filter
this.setState(prevState => ({
appliedFilterList: {
...prevState.appliedFilterList,
[filterType]: [...prevState.appliedFilterList[filterType], filterName]
}
}));
}
};
This function is being passed to the child components as :
<ChildComponent toggleFilter={this.toggleFilter} />
So, i am trying to test this toggleFilter() function like this:
it("checks for the function calls", () => {
const toggleFilterMockFn = jest.fn();
const component = shallow(
<ProductList
headerText="Hello World"
productList={data}
paginationSize="10"
accessFilters={["a 1", "a 2"]}
bandwidthFilters={["b 1", "b 2"]}
termsFilters={["t 1", "t 2"]}
appliedFilterList={appliedFilter}
toggleFilter={toggleFilterMockFn}
/>
);
component.find(FilterDropdownContent).prop("toggleFilter")({ target: { value: "someValue" } });
});
But I get the error saying :
TypeError: Cannot read property 'includes' of undefined
What may be causing the issue? Can someone please help me with this.
EDIT 1: I tried the below test case:
expect(toggleFilterMockFn).toHaveBeenCalledWith(appliedFilter, "access");
But I get the below error :
expect(jest.fn()).toHaveBeenCalledWith(expected)
Expected mock function to have been called with:
[{"access": ["Access Type Of The Service"], "bandwidth": ["the allowed band width ", "the allowed band width"], "term": ["term associated with the service"]}, "access"]
But it was not called.
You can't render a parent and test a child function like that. Instead, you should render <FilterDropdownContent /> directly, and then write a test that simulates an event (like click) and checks to see if the function was called.
Something like this for example:
import React from 'react';
import { shallow } from 'enzyme';
describe('<FilterDropdownContent />', () => {
let wrapper, toggleFilter;
beforeEach(() => {
toggleFilter = jest.fn();
wrapper = shallow(
<FilterDropdownContent
toggleFilter={toggleFilter}
/>
);
});
describe('when clicking the .toggle-filter button', () => {
it('calls `props.toggleFilter()` with the correct data', () => {
wrapper.find('.toggle-filter').simulate('click');
expect(toggleFilter).toHaveBeenCalledWith({ target: { value: 'someValue' } });
});
}):
});
In this example, clicking a link with the .toggle-filter class calls the function, but you should be able to adapt this to your specific implementation.