Creating a Modern Firebase Powered Application with TDD
Use reactive programming and tests to build a professional app
[Sprint Two] - Adding Clients
Feature to add clients
PROModule Outline
- Source Code & Resources PRO
- Lesson 1: Introduction PUBLIC
- Lesson 2: The Structure of this Module PUBLIC
- Lesson 3: [Sprint One] Setting up Firebase PUBLIC
- Lesson 4: [Sprint One] Creating Security Rules with TDD PRO
- Lesson 5: [Sprint One] Testing Authentication PRO
- Lesson 6: [Sprint One] Component Store PRO
- Lesson 7: [Sprint One] Circumventing Firebase Authentication for E2E Tests PRO
- Lesson 8: [Sprint Two] Displaying Client List from Firestore PRO
- Lesson 9: [Sprint Two] - Adding Clients PRO
- Lesson 10: [Sprint Two] - Editing Clients PRO
- Lesson 11: [Sprint Two] - Client Details PRO
- Lesson 12: Preparing for Delivery PRO
- Lesson 13: Configuring for Production PRO
- Lesson 14: [Sprint Three] - Refactoring PRO
- Lesson 15: [Sprint Three] Setting up PWA PRO
- Lesson 16: [Sprint Three] Logout PRO
- Lesson 17: [Sprint Three] Delete a Client PRO
- Lesson 18: [Sprint Three] - Feedback Mechanism PRO
- Lesson 19: [Sprint Three] View Feedback PRO
- Lesson 20: More Styling PRO
- Lesson 21: [Sprint Four] - Refactoring Feedback PRO
- Lesson 22: [Sprint Four] - Feedback Dates PRO
- Lesson 23: [Sprint Four] - Client Survey PRO
- Lesson 24: [Sprint Four] - View Survey PRO
- Lesson 25: Final Touches PRO
- Lesson 26: Conclusion PRO
Lesson Outline
Feature: See a list of all clients
Let's move on to our next user story:
User story: As a massage therapist, I want to be able to record a client's details so that I don't need to ask them for the same information on their next visit
Project management
Remember to move the card for this user story to the Test
column, and create a new task branch to work on.
As always, our first step is going to be to create an E2E test for this user story. We already have an interface for a clients details, so we know what details we need to create a new client:
interface ClientName {
first: string;
last: string;
}
interface SurveyResponse {
values: string;
}
export interface Client {
name: ClientName;
email: string;
phone: string;
appointments: string[];
notes: string;
survey: SurveyResponse[];
}
Let's encapsulate that in a new E2E test.
Add the following test to
clients.cy.ts
:
it('can add a new client', () => {
// Add a new client
getAddClientButton().click();
getFirstNameField().type('Josh');
getLastNameField().type('Morony');
getEmailField().type('[email protected]');
getPhoneField().type('555555555');
getNotesField().type('this is a note');
getSaveButton().click();
// Expect that the client is now in the clients list
const listOfClients = getItemsInList();
listOfClients.should('contain.text', 'Josh Morony');
});
We will also need to make sure to add those utility methods to support/utils.ts
and import them into our test (if you are using VS Code you can just put your cursor over the missing imports, bring up the Quick Fix
options, and choose the Add all missing imports
option):
// Client form
export const getAddClientButton = () => cy.get('[data-test="add-client-button"]');
export const getFirstNameField = () => cy.get('[data-test="first-name-input"] input');
export const getLastNameField = () => cy.get('[data-test="last-name-input"] input');
export const getEmailField = () => cy.get('[data-test="email-input"] input');
export const getPhoneField = () => cy.get('[data-test="phone-input"] input');
export const getNotesField = () => cy.get('[data-test="notes-input"] textarea');
export const getSaveButton = () => cy.get('[data-test="save-client-button"]');
NOTE: We need to make sure we access the underlying input
and textarea
elements, not the <ion-input>
and <ion-textarea>
wrapper components otherwise not all of the Cypress methods will work.
For now we will just keep all of the utility methods in one file, but as this grows you might decide to split this out into multiple files.
Let's see what happens when we run this test:
Timed out retrying after 4000ms: Expected to find element: [data-test="add-client-button"], but never found it.
We get an error because it can't find the button for adding a new client. Now we could address each error we run into one at a time, i.e. we could just add an add button and nothing else first, but this is going to slow us down. We know that after our test clicks the add button it's going to try to get the firstName
field and it's going to fail to find that. Then it's going to fail to find the lastName
field and so on. We're going to plan ahead a bit, and think about how we are going to approach this.
Deciding on the Architecture
Our client-add
page does not exist yet so we will need to add that. This will be a smart/routed component. We also have a form we want to display within this page. Last time we just put everything into our routed component and later we refactored it. This time, we are going to create smart/dumb components from the beginning. Our strategy here is going to be:
- Add the button to trigger the
client-add
page - Create the
client-add
page - Create a
client-editor
component for the client details form - Use the component in the
client-add
page - Redirect back to the
client-dashboard
page when the form is submitted
We aren't going to implement all of the functionality for the form right away. At the moment, we are just interested in progressing our E2E test, so we mostly just want to be able to visit the page, enter in the details, and then get back to the original page - it doesn't actually have to do anything with the data (yet). We are also planning ahead here a bit by making the dumb component for the form a generic "editor" as opposed to being specifically for adding clients - we could easily just pass in an existing client to this form to have it function as a way to edit a client.
Add the add button to
client-dashboard.page.html
:
<ion-header>
<ion-toolbar>
<ion-title data-test="page-title">Clients</ion-title>
<ion-buttons slot="end">
<ion-button
data-test="add-client-button"
routerLink="add"
routerDirection="forward"
>
<ion-icon slot="icon-only" name="add"></ion-icon>
</ion-button>
</ion-buttons>
</ion-toolbar>
</ion-header>
Create the
client-add
page:
ionic g page clients/feature/ClientAdd --module clients/feature/client-shell/client-shell-routing.module --route-path add
NOTE: We are making full use of the generator here so that it automatically adds this to the client-shell-routing
instead of the default app-routing
, and we also want to use a path of add
instead of the default client-add
Create the
client-editor
component:
ionic g component clients/ui/ClientEditor --create-module --export
Again, we are supplying some extra options here to do some config work for us. The --create-module
flag will automatically create a module for this component, and the --export
flag will export the component from that module (effectively creating a SCAM component for us).
Implementing the Client Editor
Now let's do some work - we are going to implement the basics of our client-editor
before moving on. All we want for is for the form fields to be available in the component, we aren't going to have them do anything just yet.
This brings us to a similar spot as before when we created out client-list
component - do we write a test for this? Again, the fields we will be adding are currently covered by our E2E test, but what if we want to use this component somewhere else? We might eventually write unit tests for this component when we are implementing important behaviour, but we aren't going to add unit tests just for the presence of particular form elements. In my opinion, this is better suited to the E2E test (even if we do reuse this somewhere else).
Modify
client-editor.component.html
to reflect the following:
<form>
<ion-item>
<ion-label position="floating">First Name</ion-label>
<ion-input
data-test="first-name-input"
type="text"
></ion-input>
</ion-item>
<ion-item>
<ion-label position="floating">Last Name</ion-label>
<ion-input
data-test="last-name-input"
type="text"
></ion-input>
</ion-item>
<ion-item>
<ion-label position="floating">Email</ion-label>
<ion-input data-test="email-input" type="email"></ion-input>
</ion-item>
<ion-item>
<ion-label position="floating">Phone</ion-label>
<ion-input data-test="phone-input" type="tel"></ion-input>
</ion-item>
<ion-item>
<ion-label position="floating">Notes</ion-label>
<ion-textarea data-test="notes-input"></ion-textarea>
</ion-item>
<ion-button data-test="save-client-button" type="submit">
Save
</ion-button>
</form>
Use the component in the
client-add
page:
Thanks for checking out the preview of this lesson!
You do not have the appropriate membership to view the full lesson. If you would like full access to this module you can view membership options (or log in if you are already have an appropriate membership).