We're about to add state to our component. The component will show a button for each appointment. When the button is clicked, the component stores the array index of the appointment that it refers to. To do that, we'll use the useState hook.
Hooks are a feature of React that manage various non-rendering related operations. The useState hook stores data across multiple renders of your function. The call to useState returns you both the current value in storage and a setter function that allows it to be set.
If you're new to hooks, check out the Further learning section at the end of this chapter. Alternatively, you could just follow along and see how much you can pick up just by reading the tests!
Let's start by asserting that eachlielement has abuttonelement:
Add the following test, just below the last one you added. The second expectation is a little peculiar in that it is checking the type of the button to bebutton. If you haven't seen this before, it's idiomatic when using button elements to define its role by setting thetypeattribute, as I'm doing here:
it('has a button element in each li', () => { render(<AppointmentsDayView appointments={appointments} />); expect( container.querySelectorAll('li > button') ).toHaveLength(2); expect( container.querySelectorAll('li > button')[0].type ).toEqual('button'); });
We don't need to be pedantic about checking the content or placement of the button element within its parent. For example, this test would pass if we put an empty button child at the end of li. But, thankfully, doing the right thing is just as simple as doing the wrong thing, so we can opt to do the right thing instead. All we need to do to make this pass is wrap the existing content in the new tag.
Make this test pass by modifying the AppointmentsDayView return value, as shown:
We can now test what happens when the button is clicked. Back in test/Appointment.test.js, add the following as the next test. This uses theReactTestUtils.Simulate.click function to perform the click action:
it('renders another appointment when selected', () => { render(<AppointmentsDayView appointments={appointments} />); const button = container.querySelectorAll('button')[1]; ReactTestUtils.Simulate.click(button); expect(container.textContent).toMatch('Jordan'); });
React components respond to what it calls synthetic events. React uses these to mask browser discrepancies in the DOM event model. That means we can't raise standard events that we'd fire through JSDOM. Instead, we use the ReactTestUtils.Simulate object to raise events.
Include the following import at the top of test/Appointment.test.js:
import ReactTestUtils from 'react-dom/test-utils';
Go ahead and run the test:
● AppointmentsDayView ? renders appointment when selected
expect(received).toMatch(expected)
Expected value to match: "Jordan" Received: "12:0013:00Ashley"
We're getting all of the list content dumped out too, because we've used container.textContent in our expectation rather than something more specific.
At this stage, I'm not too bothered about where the customer name appears on screen. Testing container.textContentis like saying I want this text to appear somewhere, but I don't care where. Later on, we'll see techniques for expecting text in specific places.
There's a lot we now need to get in place in order to make the test pass: we need to introduce state and we need to add the handler. But, first, we'll need to modify our definition to use a block with a return statement:
Set the last test to skip, usingit.skip.
We never refactor on red. It's against the rules! But if you're on red, you can cheat a little by rewinding to green by skipping the test that you've just written.
It may seem a little pedantic to do that for the very tiny change we're about to make, but it's good practice.
Wrap the constant definition in curly braces, and then return the existing value. Once you've made this change, run your tests and check you're all green:
We can now use thisselectedAppointmentrather than hard-coding an index selecting the right appointment. Change the return value to use this new state value when selecting an appointment:
Run your tests, and you should find they're all green:
PASS test/Appointment.test.js Appointment ? renders the customer first name (18ms) ? renders another customer first name (2ms) AppointmentsDayView ? renders a div with the right id (7ms) ? renders multiple appointments in an ol element (16ms) ? renders each appointment in an li (4ms) ? initially shows a message saying there are no appointments today (6ms) ? selects the first element by default (2ms) ? has a button element in each li (2ms) ? renders another appointment when selected (3ms)
Our component is now complete and ready to be used in the rest of our application. That is, once we've built the rest of the application!