README
Extensions for dom-testing-library
API extensions to dom-testing-library for easier DOM testing.
Designed specifically to facilitae form and field testing, including changing and submitting field values.
Can be used with react-testing-library. See React with Jest example below.
Philosophy and design considerations
A core philosophy of this extension library is to better allow accessing elements by id
or name
as they are way less volatile reference markers.
Personally I like to use generators to generate most of my application artifacts from various schemas. Hence I don't know or care what labels will go in. This means that the core philosophy of "testing by end user usage" doesn't really suit my application development style, as I don't care about what the user sees at the end... until the end!
I personally tend to focus first on infrastructure and leave the UI/UX concerns including labels/text to the final stages of development (ie. "skinning").
Methods such as getByText
and getByLabel
are thus pretty useless to me, except perhaps for E2E testing at the end. Sure, you could somehow reference the same labels being injected, but why depend on that? And only works if the injection mechanism is "fixed" and not subject to change later, unless you then abstract away the mechanism with yet another wrapper :P
I much prefer to inject texts, labels, UI framework, theming etc. much later in the dev process. The key os to maintain flexibility with regards to i18n and other such cross cutting concerns.
Status
WIP: Under development. Has yet to be "battle tested" and is thus not published on npm just yet.
Currently using this library in react-app-builder
Quick start: react-testing-library
Dependencies
This library has no dependencies. It can be used directly with DOM elements, such as for js-dom.
PS: Might rename it later to not have the react
name as it is really not React specific.
Usage
import { apiFor, eventsApi } from "dom-testing-lib-extensions";
API
import { fireEvent } from "react-testing-library";
use eventsApi
to generate generic wrappers for fireEvent
and other library specific methods
const config = eventsApi({ fireEvent });
const api = apiFor(container, config);
The api
returned exposes the following methods:
elementBy
// return a DOM element by selector
elementBy({
parent,
tag,
id,
testId,
name,
type
});
elementsFor
// retrieve a map of elements. Optionally execute an effect on each element
elementsFor(obj, effect);
Example:
const elementsMap = {
name: {
// firstName field element selector
name: "firstName",
type: "text",
value: "no name"
},
age: {
// age field element selector
testId: "age",
value: 32
}
};
// set value of each
api.elementsFor(elementsMap, (api, opts) => api.setValue(opts));
forField
retrieve a dedicated form field value change API
forField(field);
// => { changeValue(value, opts), setValue(value) }
setValue
Set field value
attribute for element matching elementBy
selector.
Pass value
option
setValue({ name: "age", value: 32 });
Note: Can also handle checked
option, passing it to setChecked
setChecked
Set field checked
attribute for element matching elementBy
selector.
Pass checked
option (true
for checked, false
unchecked)
setChecked({ name: "married", checked: true });
changeValue
Notify DOM that value
attribute for element matching elementBy
selector has been changed to a given value. Pass value as value
option
changeValue({ name: "role", value: "admin" });
Find element matching elementBy
selector by default using: type: 'submit'
and element: 'button'
. If button found, clicks it, triggering form submit.
changeChecked
Notify DOM that checked
attribute for element matching elementBy
selector has been changed to a given status (checked or unchecked). Pass checked status as checked
option
changeValue({ name: "married", checked: "true" });
change
Generic change
method that can be used with either checked
or value
option
change({ name: "role", value: "admin" });
change({ name: "married", checked: "true" });
check
Convenience method for setChecked
with checked: true
check({ name: "married" });
uncheck
Convenience method for setChecked
with checked: false
uncheck({ name: "married" });
setValues
Convenience methods to set or change multiple field inputs by iterating a map of property set configurations.
setValues(obj);
Example:
const elementsMap = {
name: {
// firstName field element selector
name: "firstName",
type: "text",
value: "no name" // use setValue
},
age: {
// age field element selector
testId: "age",
value: 32 // use setValue
},
married: {
// age field element selector
testId: "age",
checked: true // use setChecked
}
};
// set value of each
api.setValues(elementsMap);
changeValues
changeValues(obj
const elementsMap = {
name: {
// firstName field element selector
name: "firstName",
type: "text",
value: "no name"
},
age: {
// age field element selector
testId: "age",
value: 32
},
married: {
name: "married",
checked: true // will fall back to use changeChecked instead
}
};
// set value of each
api.changeValues(elementsMap);
changeSelected
Call onChange
DOM event with the list of selectedOptions
as the target. An option
in selectedOptions
is set to state selected
if it matches any value in the list passed in the selected
option, such as java
or c#
in this example.
api.changeSelected({ name: "languages", selected: ["java", "c#"] });
setSelected
Similar to setSelected
but simply sets state of options. Doesn't specifically call onChange
DOM event. Set option
elements of a multi select
to be in state selected
for each matching values in the list passed via selected
option, such as java
or c#
in this example.
api.setSelected({ name: "languages", selected: ["java", "c#"] });
setUnselected
api.setUnselected({ name: "languages", unselected: ["java", "c#"] });
clearSelected
api.clearSelected({ name: "languages" });
clearValue
api.clearValue({ name: "firstName" });
withField
Execute function on matching field
api.withField({ name: "firstName" }, field => (field.style = "color: red"));
submit
Submit using first submit button
element (with type="submit"
)
api.submit();
Submit using first submit button
element (with type="submit"
) that has a parent element with id="payment-options"
api.submit({ parent: "#payment-options" });
reset
Reset the values of all elements in the form
api.reset({ name: "loginForm" });
Example Scenario
Imagine we have a person form with fields to update
name
age
The personForm
is created via a factory function that returns an object, consisting of:
- an internal
state
pointer - the form
Component
import { render } from "react-testing-library";
import { createPersonForm } from "./person/form/factory";
const personForm = createPersonForm({
// some configuration
// ...
state: {
// form state config here
}
});
For testing we use render
from react-testing-library
to give us the rendered (DOM) form element
const form = {
state: personForm.state,
rendered: render(<personForm.Component {...props} />)
};
We now want to simulate entering the following values into the form and perhaps test submit of the form as well
const values = {
name: "Kristian",
age: 32
};
We want to test:
change
event handler to if state is changed to reflect input value changesubmit
of the form (ie. clickingsubmit
button)
We start with a few helper functions and factories for added convenience and efficiency
const createTesterApi = (api, {name, value}) => {
return {
change() => api.changeValue(name, value),
set() => api.setValue(name, value)
}
}
const createInputTester = (field) => ({name, value, type = 'change'}) => {
const api = apiFor(field)
const testerApi = createTesterApi(api, {name, value})
// changes or sets value for the input
testerApi[type]()
}
const submitForm = (form, opts = {}) => {
const api = apiFor(form)
api.submit(opts)
}
const testInputs = ({inputs, form, type = 'change'}) => {
Object
.keys(inputs)
.map(key => {
const testInput = createInputTester(inputs[key])
testInput({name: key, value: values[key], type})
})
const api = apiFor(form.element)
// submit the form is we are using set
type === 'set' && submitForm(form.element)
}
Testing the form and fields, we use apiFor
to provide a convenient API for working efficiently with the DOM. We extract container from the form.rendered
object returned by the render
method of react-testing-library
const { container } = form.rendered;
const formApi = apiFor(container);
We have all the usual getByXYZ
methods available as usual if we need them.
const { container, getByTestId, getByLabelText, getByText } = form.rendered;
We use elementsBy
provided by the api returned to get a map of the fields (DOM elements) in the form
We could also extract the api methods we want to use directly as follows
const { elementsFor, setValues, changeValues } = apiFor(container);
We then use elementsFor
to retieve a map of DOM elements of the fields we want to put under test.
// retrieve the DOM elements for each field in the form, using name selector
const inputs = formApi.elementsFor({
name: { name: "name" },
age: { name: "age" }
});
We can then pass these field referenced to our helper testInputs
to simulate firing change
in each of the fields.
Note: For simplicity we here assume each field is a simple <input>
element (why we call the helper method testInputs
).
// use the helper to test each input (field) value change handler
testInputs({ inputs, form, type: "change" });
// TODO: iterate values
expect(form.state("name")).toEqual(values["name"]);
After having tested the effect of change
value events on the form state, we then test what happens when we set the input values and click submit
.
We use type: 'set'
to test to tell our test helper testInputs
to use setValue
instead of changeValue
.
testInputs({ inputs, form, type: "set" });
// TODO: test that state is submitted somehow...
Please note that we could optimize this testing further using the setValues
and changeValues
methods, but this scenario walk-through was meant to illustrate most of the core methods exposed. You can always add extra higher level methods as needed.
select element
See react-select examples and repo
Multi select
Could be done something like this:
handleChange({target}) {
this.setState({
selected: [...target.selectedOptions].map({value} => value)});
}
}
render() {
const { selected } = this.state
return (
<select multiple value={this.props.multiValue} onChange={this.handleChange}>
{options.map(option => {
<option value={option.value} selected={selected[option.value]}>{option.value}</option>
})}
</select>
);
}
Pre-fabricated select components
react-select-multiple to display as list of checkboxes that syncs with a multi select.
See react-select v2 demo with fixed options
export default class FixedOptions extends Component<*, State> {
onChange(value, { action, removedValue }) {
this.setState({ value: value });
}
render() {
return (
<Select
value={this.state.value}
isMulti
styles={styles}
isClearable={this.state.value.some(v => !v.isFixed)}
name="colors"
className="basic-multi-select"
classNamePrefix="select"
onChange={this.onChange}
options={colourOptions}
/>
);
}
}
Development
Create distribution
npx babel src --out-dir dist
License
MIT
2018 Kristian Mandrup