Creating a Modern Firebase Powered Application with TDD
Use reactive programming and tests to build a professional app
[Sprint Two] - Editing Clients
Creating a multi-use component
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: Edit a clients details
We're officially halfway through our sprint! We've completed 2 out of 4 user stories, and I think we've got a bit of a flow going on for this one. Let's move on to our next user story:
User story: As a massage therapist, I want to be able to edit a clients details, so that I can manually make updates if necessary
Project management
Remember to move the card for this user story to the Test
column, and create a new task branch to work on.
Let's first focus on our E2E test. We already have a way to view a list of clients, so presumably if we wanted to edit a client we would do something like this:
- View the list of clients
- Select a client
- Click an edit button
- Supply new/modified details
- Click a save button
Now we don't currently have the ability to view a clients details, that is something we would be covering in the next user story:
#5 As a massage therapist, I want to be able to access the full details of a specific client, so that I can access all of the information that is required to complete my services
Does that mean that we are blocked on #3
(our current user story) until we finish #5
first? No. This might make it slightly more awkward, and it probably would be more optimal to do #5
first, but we are going to stick with #3
to demonstrate that it doesn't really matter in the end. We will have to implement parts of the "view a specific client" functionality on our way to implementing our edit functionality the way we want, but we will leave everything out except what is specifically required by our E2E test to make it pass. What that will likely mean is that we will need to set up a client-detail
page, but it won't need to have anything on it except the specific field we want to edit/verify (e.g. the clients name) and a button that can take us to the edit page.
Let's have a go at creating that E2E test now.
Add the following test to
clients.cy.ts
it('can edit a clients details', () => {
cy.callFirestore('set', 'clients/abc123', {
name: {
first: 'Josh',
last: 'Morony',
},
});
const testEditedName = 'Amir';
getItemsInList().first().click();
getEditButton().click();
getFirstNameField().clear();
getFirstNameField().type(testEditedName);
getSaveButton().click();
getNameDisplay().should('contain.text', testEditedName);
});
We've also added a couple more utility methods in the test above:
export const getEditButton = () => cy.get('[data-test="edit-button"]');
export const getNameDisplay = () => cy.get('[data-test="client-name-display"]');
The basic idea with this test is that we set some initial data in our Firestore database to test, and then perform the following actions:
- Click the first client in the list
- Click the edit button
- Clear the data in the first name field
- Type the test name in the first name field
- Click the save button
Once this happens, we would expect that we were taken back to the client-detail
page. We grab the getNameDisplay()
which will be the container we are using to display the clients name, and we check that it contains the new edited name we used.
An important thing to note here is that this test just tests updating one field. Updating the clients name might work fine, but what if updating their phone number is broken? This E2E test does not ensure that all of the fields work. This is a big part of why we have the unit tests as well, it will be in our unit tests that we create more granular tests that ensure all of the appropriate updated fields are sent to the right place.
One of the key practical downsides of having your E2E tests do everything (e.g. trying to cover all conceivable cases) are that they are much slower than unit tests. If we had 100s of E2E tests testing every possible combination of interactions a user might perform, then our E2E test suite is going to execute very slowly. This reduces its value as well because developers are going to execute these tests much less frequently. The faster our tests are the faster and more frequently we can execute them and get feedback. Unit tests are much faster to execute, and we should generally aim to have a much larger number of unit tests than E2E tests.
Let's verify that this new E2E test fails:
Timed out retrying after 4000ms: Expected to find element: [data-test="edit-button"], but never found it.
Perfect. The client is being clicked on but nothing happens and there is no edit button anywhere on the screen so it fails. Let's move on. The first thing we are going to need to do to progress is to actually have our client detail page launch when we click on a client in the list.
Again, we could set up a unit test to test that when the client is clicked that the appropriate navigation is triggered, but that hasn't been our testing philosophy so far. This behaviour is already verified by our E2E test, so we don't really gain anything by creating a unit test as well. It actually might end up making our lives more complicated because in a unit test we would be more explicitly defining how that navigation should happen. With just our E2E test, we don't care how the navigation happens, we just verify that we get where we need to go.
Creating the Client Detail Page
Let's go ahead and create our client detail page:
ionic g page clients/feature/ClientDetail --module clients/feature/client-shell/client-shell-routing.module --route-path=":id"
And make sure that we trigger navigation when clicking items in our client-list
:
<ion-list data-test="list">
<ion-item
*ngFor="let client of clients"
button
routerLink="/clients/{{ client.id }}"
routerDirection="forward"
>
<ion-label>{{ client.name.first }} {{ client.name.last }}</ion-label>
</ion-item>
</ion-list>
We run into a bit of a problem here. Our Client
interface is defined like this:
export interface Client {
name: ClientName;
email: string;
phone: string;
appointments: string[];
notes: string;
survey: SurveyResponse[];
}
Notably, it does not have an id
field. We don't create an id
field in our application, but an id
is created for the client document by Firestore and we want to use this id
in the application. Let's add an id
field to the Client
in client.store.ts
:
export interface Client {
id?: string;
name: ClientName;
email: string;
phone: string;
appointments: string[];
notes: string;
survey: SurveyResponse[];
}
But notice that we make it optional. This will allow us to create a new client without having to specify an id
, but it will also allow us to use the id
returned from Firestore.
We will also need to make sure to include the RouterModule
in the module for our client-list
:
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { IonicModule } from '@ionic/angular';
import { ClientListComponent } from './client-list.component';
@NgModule({
imports: [IonicModule, CommonModule, RouterModule],
declarations: [ClientListComponent],
exports: [ClientListComponent],
})
export class ClientListComponentModule {}
and to keep our tests from breaking, we will again need the RouterTestingModule
in the spec file for client-list
:
TestBed.configureTestingModule({
declarations: [ClientListComponent],
imports: [IonicModule.forRoot(), RouterTestingModule],
})
and for our new client-detail
spec as well:
TestBed.configureTestingModule({
declarations: [ClientDetailPage],
imports: [IonicModule.forRoot(), RouterTestingModule],
}).compileComponents();
If we were to run our E2E test again now we still get the same error:
Timed out retrying after 4000ms: Expected to find element: [data-test="edit-button"], but never found it.
Debugging and Improving a Test
We just finished updating our Client
interface to include an id
so that we can send it to the route for our ClientDetail
page, but we haven't created an edit button or anything like that yet. However, if you inspect the test closely you will notice that it never actually gets to the ClientDetail
page - the client in the list is clicked but nothing happens.
I wasn't expecting this and after a bit of debugging I realised that the id
field is never being returned from Firestore. If you remember from before, our ClientsService
retrieves the clients from Firestore like this:
public getClients() {
const clientsCollection = collection(this.firestore, 'clients');
return collectionData(clientsCollection) as Observable<Client[]>;
}
However, by default Firestore won't give us the id
field. If you are using an editor like Visual Studio Code you can just hover over the collectionData
method and in the pop up you will be able to see that the method also accepts a second parameter: an options object with an idField
string. We can use that to specify the field we want the id
value stored on.
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).