In the Angular version of the app, we can see that this component will be a div wrapping a label and an input. This is a classic form field.
Create a branch feat/inputDetail. Create 2 files under src/components/ folder; InputDetail.cy.tsx, InputDetail.tsx. As usual, start minimal with a component rendering; copy the below to the files and execute the test after opening the runner with yarn cy:open-ct.
Let's add our first failing test. There needs to be an input with a placeholder attribute. We can use Testing Library's findByPlaceholderText to check for both (Red 1).
We can immediately tell that this needs to be a property, because of the hard-coded value. We enhance the test and the component to accept a prop (Refactor 1).
The next tag we need is label. This is a usual pattern in forms, a div wrapping a label and an input with css & attributes. Let's write a failing test (Red 2).
Form fields also have a value attribute, which can be used display a readonly value from the network, or writable form fields. For now, let's write a failing test checking for the defaultValue of a form, while also adding the styles (Red 3).
We suggested 2 variants of the form field; a readonly vs a writable field. Let's create two cases. The first test which checks that a field can be modified should pass. The second test checking that the input is readonly should fail for now. We also get a compiler indicator that we are trying to pass a non-existing readOnly prop (Red 4).
// src/components/InputDetail.cy.tsximport InputDetail from"./InputDetail";import"../styles.scss";describe("InputDetail", () => {it("should allow the input field to be modified", () => {constplaceholder="Aslaug";constname="name";constvalue="some value";constnewValue="42";cy.mount( <InputDetailname={name} value={value} placeholder={placeholder} /> );cy.contains(name);cy.findByPlaceholderText(placeholder).clear().type(newValue);cy.get("input").should("have.value", newValue); });it("should not allow the input field to be modified when readonly", () => {constplaceholder="Aslaug";constname="name";constvalue="some value";cy.mount( <InputDetailname={name}value={value}placeholder={placeholder}readOnly={true} /> );cy.contains(name);cy.findByPlaceholderText(placeholder).should("have.attr","readOnly"); });});
We can add the readOnly prop to the types, to the arguments and the readonly attribute of the component to get a passing test (Green 4). At this time, it is worth noting that name and value are the mandatory fields, and the rest are optional.
The final attribute we need for a form field is onChange, in case it is a writable field. Let's enhance the test to check that the onChange event is called when the form field changes (Red 5). The value of onChange is simply cy.stub().as('onChange')
// src/components/InputDetail.cy.tsximport InputDetail from"./InputDetail";import"../styles.scss";describe("InputDetail", () => {it("should allow the input field to be modified", () => {constplaceholder="Aslaug";constname="name";constvalue="some value";constnewValue="42";cy.mount( <InputDetailname={name}value={value}placeholder={placeholder}onChange={cy.stub().as("onChange")} /> );cy.contains(name);cy.findByPlaceholderText(placeholder).clear().type(newValue);cy.get("input").should("have.value", newValue);cy.get("@onChange").should("have.been.called"); });it("should not allow the input field to be modified when readonly", () => {constplaceholder="Aslaug";constname="name";constvalue="some value";cy.mount( <InputDetailname={name}value={value}placeholder={placeholder}readOnly={true} /> );cy.contains(name);cy.findByPlaceholderText(placeholder).should("have.attr","readOnly");cy.get("input").should("have.value", value); });});
To make the test pass, once again we have to add the type for the new prop onChange, the prop as the argument to the component, and we need an attribute for the input tag (Green 5).
We can enhance the test to be more specific with the onChange check. It should be called 2 times when typing 42 (Refactor 5).
// src/components/InputDetail.cy.tsximport InputDetail from"./InputDetail";import"../styles.scss";describe("InputDetail", () => {constplaceholder="Aslaug";constname="name";constvalue="some value";constnewValue="42";it("should allow the input field to be modified", () => {cy.mount( <InputDetailname={name}value={value}placeholder={placeholder}onChange={cy.stub().as("onChange")} /> );cy.contains("label", name);cy.findByPlaceholderText(placeholder).clear().type(newValue);cy.findByDisplayValue(newValue);cy.get("@onChange").its("callCount").should("eq",newValue.length+1); });it("should not allow the input field to be modified", () => {cy.mount( <InputDetailname={name}value={value}placeholder={placeholder}readOnly={true} /> );cy.contains("label", name);cy.findByPlaceholderText(placeholder).should("have.value", value).and("have.attr","readOnly"); });});
As a final touch up, we add a data-cy selector to make the component easier to reference when it is used as a child.
// src/components/InputDetail.test.tsximport InputDetail from"./InputDetail";import { render, screen } from"@testing-library/react";import userEvent from"@testing-library/user-event";describe("InputDetail", () => {constplaceholder="Aslaug";constname="name";constvalue="some value";constnewValue="42";it("should allow the input field to be modified",async () => {constonChange=jest.fn();render( <InputDetailname={name}value={value}placeholder={placeholder}onChange={onChange} /> );awaitscreen.findByText(name);constinputField=awaitscreen.findByPlaceholderText(placeholder);awaituserEvent.clear(inputField);awaituserEvent.type(inputField, newValue);expect(inputField).toHaveDisplayValue(newValue);expect(onChange).toHaveBeenCalledTimes(newValue.length); });it("should not allow the input field to be modified",async () => {render( <InputDetailname={name}value={value}placeholder={placeholder}readOnly={true} /> );awaitscreen.findByText(name);constinputField=awaitscreen.findByPlaceholderText(placeholder);expect(inputField).toHaveDisplayValue(value);expect(inputField).toHaveAttribute("readOnly"); });});
Summary
We started with an input placeholder text check using Testing Library's findByPlaceholderText command (Red 1).
We hard-coded a value for the placeholder attribute to make the test pass (Green 1).
We refactored the hard-coded value to be instead a prop. We added the prop to the types, to the arguments of the component, and we used that argument for the value of the placeholder attribute (Refactor 1).
We identified a usual pattern in forms; a div wrapping a label and an input with css and attributes. We wrote a failing test that checks for the field name under a label tag (Red 2).
We added the new prop to the types, to the arguments of the component, and to the label tag. (Green 2).
We identified a key knowledge on forms that input type="text" makes an input a text input, label htmlFor={someValue} links the label and input tags. We enhanced the component with this knowledge (Refactor 2).
We added a test for a new prop value / defaultValue (Red 3).
As in the previous cycles, we made the test green by adding defaultValue to the type, to the arguments of the component, and to the input attribute (Green 3)
We decided to add support for two variants of the component; one for writable fields, and the other for readonly fields. We added a new test verifying that a readonly field should not be modified (Red 4).
As in the previous cycles, we added the readOnly type, the argument to the component, and the attribute with a matching prop (Green 4).
Finally, we wanted an onChange prop for the field. We added a test checking that the onChange event is called while modifying the field (Red 5).
We added the type for the new prop, the argument to the component, and the attribute with a matching prop (Green 5).
We enhanced the test to check for a specific number of onChange calls (Refactor 5).
Takeaways
When adding a prop to the component test:
Add the prop to the component types.
Add it to the arguments or the component.
Use the prop in the component.
A div wrapping a label and an input is a usual pattern to create form fields. input type="text" makes an input a text input, label htmlFor={someValue} links the label and input tags.