Lesson 10

[Sprint Two] - Editing Clients

Creating a multi-use component

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:

  1. View the list of clients
  2. Select a client
  3. Click an edit button
  4. Supply new/modified details
  5. 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:

  1. Click the first client in the list
  2. Click the edit button
  3. Clear the data in the first name field
  4. Type the test name in the first name field
  5. 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.

PRO

Thanks for checking out the preview of this lesson!

The full version of this lesson is only available to pro members. If you would like full access to this module and all of the other pro modules on Elite Ionic you can become a pro member (or log in if you are already a member).