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.
Related
I'm building a text editor using React with Typescript.
The component hierarchy looks like this: TextEditor -> Blocks -> Block -> ContentEditable.
The ContentEditable is an npm package https://www.npmjs.com/package/react-contenteditable.
What i want it to do
The behavior I'm after is similar to Medium or Notions text editor. When a user writes in a block and hits enter on their keyboard, a new block should be created after the current block.
What it does
The behavior right now is strange to me. If I press enter and add one block, it works fine. But if I press enter again it overrides the previous block instead of creating a new one.
However, if I press enter and add a block, then puts the carrot (focusing) on the new block and press enter again, a new block is added after as expected.
Sandbox
Here is a sandbox with the complete code: https://codesandbox.io/s/texteditor-mxgbey?file=/src/components/Block.tsx:81-557
TextEditor
export default function TextEditor(props) {
const [blocks, setBlocks] = useState([
{ id: "1", tag: "h1", html: "Title1" },
{ id: "2", tag: "p", html: "Some text" }
]);
function handleAddBlock(id: string) {
const index = blocks.findIndex((b) => b.id === id);
let copiedBlocks = [...blocks];
let newBlock = { id: nanoid(), tag: "p", html: "New block..." };
copiedBlocks.splice(index + 1, 0, newBlock);
setBlocks(copiedBlocks);
}
return <Blocks injectedBlocks={blocks} handleAddBlock={handleAddBlock} />;
}
Blocks
export default function Blocks(props) {
const { injectedBlocks, handleAddBlock } = props;
return (
<>
{injectedBlocks.map((b) => {
return (
<Block
key={b.id}
id={b.id}
tag={b.tag}
html={b.html}
handleAddBlock={handleAddBlock}
/>
);
})}
</>
);
}
Block
export default function Block(props) {
const { id, tag, html, handleAddBlock } = props;
function handleChange(e: React.SyntheticEvent) {}
function handleKeyDown(e: React.KeyboardEvent) {
if (e.key === "Enter") {
console.log("Enter pressed on: ", id);
e.preventDefault();
handleAddBlock(id);
}
}
return (
<ContentEditable
tagName={tag}
html={html}
onChange={handleChange}
onKeyDown={handleKeyDown}
/>
);
}
State value not give the updated value while handleAddBlock function calls.
So use like this,
setBlocks((p) => {
let copiedBlocks = [...p];
let newBlock = { id: nanoid(), tag: "p", html: "New block..." };
copiedBlocks.splice(index + 1, 0, newBlock);
return copiedBlocks;
});
This will gives the updated state value immediately.
For testing an input it works like this:
it('test input', () => {
const { getByTestId, getByLabelText } = render(<MyComponent />);
const myButton = getByTestId('submit-button');
expect(myButton).toBeInTheDocument();
fireEvent.change(getByLabelText('first-name'), {
target: { value: 'ted' }
});
// do something
});
the above test is setting the value of first-name input to "ted" and moves forward.
I want to do something similar but for a drop-down selector.
This is the component:
<SelectInput
label='my-dropdown'
onChange={onChange}
options={myOptions}
/>;
myOptions is an array of this shape:
const myOptions = [
{ id: '0', name: 'zero' },
{ id: '1', name: 'one' },
{ id: '2', name: 'two' }
];
it works fine in the application, no errors from this part.
Here comes the testing of it, I did something but it doesn't work:
it('test dropdpwn', () => {
const { getByTestId, getByLabelText } = render(<MyComponent />);
const saveButton = getByTestId('submit-button');
expect(saveButton).toBeInTheDocument();
fireEvent.change(getByLabelText('my-dropdown'), {
target: { value: { id: '0', name: 'zero' } }
});
// do something
});
the above code doesn't work, it doesn't set the dropdown with that value.
Any ideas on how to solve this? Not sure if it's important, but all those inputs are inside a react-hook-form and at the end it should test that the onSubmit is working (it works only if all inputs are set).
So, since the select behavior is being achieved using a button and spans.
You need to first click the button this would bring all the options on the screen and then you need to click one of those options.
And then you can finally test that the selected option is now on the screen.
it("test dropdpwn", async () => {
const { getByTestId, getByLabelText } = renderWithClientInstance(
<MapSignalModal title={title} open={true} toggle={toggle} />
);
userEvent.click(screen.getAllByTestId("selectButton")[0]);
userEvent.click(screen.getByText("sensor pool 1"));
expect(
await screen.findByText(screen.getByText("sensor pool 1"))
).toBeInTheDocument();
});
Also, to be really sure you can try the following, this should fail because "sensor pool 1" option is not initially on the screen.
And it should pass when the text is changed to "sensor pool 0" because that's there on the screen initially.
it("test dropdpwn", async () => {
const { getByTestId, getByLabelText } = renderWithClientInstance(
<MapSignalModal title={title} open={true} toggle={toggle} />
);
expect(screen.getByText("sensor pool 1")).toBeInTheDocument();
// if you replace the above text to "sensor pool 0", it should work
});
I'm designing a form in React that has a main form builder (Create Job.js) and some form pages (AdditionalInfo.js) and (Confirmation.js). this form had a tag input that allows you to choose tags from a drop-down list provided by an API. the selected items need to be shown later in the confirmation page.
This is my main form builder that has props and functions:(CreateJob.js)
state = {
step:1,
Title:'',
requirements:'',
Location:'',
Benefits:'',
Company:'',
InternalCode:'',
Details:'',
Tags:[],
Address:'',
Department:'',
Salary:''
}
handleDropDown = input => value => {
this.setState({ [input]: value });
}
render () {
const { step } = this.state
const {Title,Benefits,Company,InternalCode,Detailss,Tags,Address,Department,Salary,requirements,Location } = this.state;
const values ={Title,Benefits,Company,InternalCode,Detailss,Tags,Address,Department,Salary,requirements,Location}
return (
<div>
....
<AdditionalInfo
nextStep={this.nextStep}
prevStep={this.prevStep}
handleChange={this.handleChange}
handleChangeRaw={this.handleChangeRaw}
handleDropDown={this.handleDropDown}
values={values}
/>
<Confirmation
nextStep={this.nextStep}
prevStep={this.prevStep}
values={values}
/>
....
and this is my form page which includes the list from API and the drop down using react-select(AdditionalInfo.js):
export class AdditionalInfo extends Component {
state = {
locations:[],
departments: [],
tagsList:[],
}
componentDidMount() {
axios.get('/api/jobs/list-tags',{headers:headers}).then(respo =>{
console.log(respo.data)
this.setState({
tagsList:respo.data.map(Tags=>({label: Tags.name, value: Tags.id}))
})
console.log(this.state.tagsList)
})
}
render() {
const {values, handleDropDown} = this.props
<Select placeholder='Select from pre-created Tags 'onChange={handleDropDown('Tags')} defaultValue={values.Tags} required isMulti options={this.state.tagsList}/>
...
this is the list of tags received from the API:
Object { label: "MongoDB", value: 1 }
Object { label: "JavaScript", value: 2 }
Object { label: "HTML", value: 3 }
Object { label: "CSS", value: 4 }
...
And this is my Confirmation page which needs to show the info received from previous pages (Confirmation.js)
.....
render () {
const {
values: {
Title, Benefits,
Company, InternalCode, Detailss, Department,Tags, Salary,requirements,Location
}} = this.props
<Row> Tags: {Tags.join(", ")}</Row>
....
the problem is that, instead of showing tags on the page like putting the labels next to each other
:JavaScript,
MongoDB,
... it shows this
: [object Object], [object Object], [object Object], [object Object]. sorry for the long code but Im a beginner in JavaScript and I dont know how to handle it so it shows the labels. How can I achieve this?
You are doing great, and you have done right, just simple tweak you need.
If React show anything like [Object Object] it means you are trying to render Javascript Object not a single value because you have got Tags from props which is an Array of objects.
Use it like this, it will work like butter -
import React from 'react';
const Confirmation = () => {
const tags = [ // which you got from props
{ label: "MongoDB", value: 1 },
{ label: "JavaScript", value: 2 },
{ label: "HTML", value: 3 },
{ label: "CSS", value: 4 }
];
return (
<div>
{tags.map(tag => tag.label).join(', ')} {/* map over tags to get the array of tag labels */}
</div>
);
}
export default Confirmation;
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);
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.