Creating a Modern Firebase Powered Application with TDD
Use reactive programming and tests to build a professional app
Preparing for Delivery
Tidying up loose ends from the sprint
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
Now E2E tests are great, but it's still worthwhile to go through the application ourselves as well just to get a sense of the overall UX and to spot any glaring issues. When thinking about how things should work it's easy to forget some obvious things.
NOTE: Remember to serve with npm start
not ionic serve
as we need to make sure our emulators are started.
After serving the application and playing around with the existing functionality you can see there is obviously a lot missing and a long way to go in terms of styling. However, everything that we have set out to do is working. There is just one main thing I came across that strikes me as something that has been blatantly missed and that is that there is no back button present on the ClientDetail
page or the ClientAdd
page. This is certainly something we want before sending it to the client.
Another issue is that although we have added the ability to edit a client, we haven't actually provided a way to delete a client. There is also currently no "loading" UI state for the clients page, and there is not an option to log out.
We are not going to address all of these now, but we will add new issues to be addressed later (possibly in the next sprint). We generally want to keep work confined to our planned sprints, but a little polish can go a long way when delivering an iteration to a client - so, we are going to address some things now.
Project management
We will create issues for everything first, because we will need them even for the stuff that we are working on now. Make sure to create all of the following issues - we are also going to follow the user story process for each new feature:
As a massage therapist, I want the ability to log out, so that I can protect sensitive client information and interact with the application as a non-admin user
As a massage therapist, I want the ability to delete a client, so that I can remove clients entries mistakingly added or remove clients that I no longer provide services for
As a massage therapist, I want an indication that data is loading, to provide a more pleasant experience when using the app
We will not follow the user story process for the back button issue - this is more of an oversight/bug fix than a "feature". We will simply create an issue with no body for these ones:
bug: can not navigate back from client detail page
bug: can not navigate back from the client add page
Once you have created all these issues, make sure to create a new task brach for the issue we will be working on first: the back button.
The issues we are going to address right now are:
- Adding the back button
- Adding loading state to the template
The back button is critical to add as it breaks the usability of the application otherwise. The loading state isn't so necessary - the data loads so fast that it is barely noticeable - but it's easy to add and these tiny details really can really make the application feel a lot better.
Adding the back button
Let's add the back button functionality. This will be fairly simple to add, but remember that we aren't just going to add a back button. We are using TDD, so of course we are going to add a test for it first.
Project management
We created two separate issues for the back buttons, but let's just deal with them both at the same time. Move both cards in the Kanban, and just pick one issue number to use for your task branch. When we create a pull request to merge the branch, we can just add an extra closes #x
message to the body of the pull request to close the other associated issue automatically (or you can just close the issue manually).
Let's start with an E2E test to get back from the ClientDetail
page:
Add the following test to
clients.cy.ts
:
it('can navigate back to the clients list after viewing a specific client', () => {
const testClient = {
name: {
first: 'Josh',
last: 'Morony',
},
phone: '333',
email: '[email protected]',
notes: '',
};
cy.callFirestore('set', 'clients/abc123', testClient);
getItemsInList().first().click();
getClientDetailBackButton().click();
getItemsInList().should('contain.text', 'Josh Morony');
});
and we will need to add a new utility method:
export const getClientDetailBackButton = () =>
cy.get('[data-test="client-detail-back-button"]');
NOTE: We are adding specific test attributes to each button rather than just grabbing the back buttons by the ion-back-button
selector. This is because in some cases Ionic will actually keep views cached in the DOM, and we don't want to accidentally grab the back button of a page that is not active.
Now let's test that it fails:
Timed out retrying after 4000ms: Expected to find element: [data-test="client-detail-back-button"], but never found it.
Add the back button to our template:
<ion-header>
<ion-toolbar>
<ion-title>ClientDetail</ion-title>
<ion-buttons slot="start">
<ion-back-button data-test="client-detail-back-button"></ion-back-button>
</ion-buttons>
<ion-buttons slot="end">
<ion-button
*ngIf="client$ | async as client"
routerLink="/clients/{{client.id}}/edit"
routerDirection="forward"
data-test="edit-button"
>
<ion-icon slot="icon-only" name="create-outline"></ion-icon>
</ion-button>
</ion-buttons>
</ion-toolbar>
</ion-header>
and test that it passes...
and it does!
Now for our ClientAdd
page we are actually going to add two tests, because there are two different scenarios in which it is used. In one case, we want to go from the client-dashboard
to the client-add
page to add a new client and then go back to the dashboard. In the other case, we want to go from the client-detail
page to the client-add
page and then go back to the client-detail
page.
We will add the first scenario to the same clients.cy.ts
file, but since our second scenario starts from a different page we are going to create a separate client-detail.cy.ts
file.
Let's start with the first scenario:
it('can navigate back to the clients list after going to the client-add page', () => {
getAddClientButton().first().click();
getAddClientBackButton().click();
getTitle().should('contain.text', 'Clients');
});
and add our new utility method:
export const getAddClientBackButton = () =>
cy.get('[data-test="add-client-back-button"]');
check that it fails:
Timed out retrying after 4000ms: Expected to find element: [data-test="add-client-back-button"], but never found it.
and then add the implementation:
<ion-header>
<ion-toolbar>
<ion-title>
<ng-container *ngIf="{ client: client$ | async } as vm">
{{ vm.client ? 'Edit Client' : 'Add Client' }}
</ng-container>
</ion-title>
<ion-buttons slot="start">
<ion-back-button data-test="add-client-back-button"></ion-back-button>
</ion-buttons>
</ion-toolbar>
</ion-header>
and check that it passes...
and it does!
Now let's add the other test for this. We will create a new file at cypress/e2e/client-detail.cy.ts
:
import {
getAddClientBackButton,
getEditButton,
getNameDisplay,
} from '../support/utils';
describe('Client Detail', () => {
const testId = 'abc123';
const testClient = {
name: {
first: 'Josh',
last: 'Morony',
},
phone: '333',
email: '[email protected]',
notes: '',
};
beforeEach(() => {
cy.login();
cy.callFirestore('delete', 'clients');
cy.callFirestore('set', `clients/${testId}`, testClient);
cy.visit(`clients/${testId}`);
});
it('can navigate back to the clients detail page after visiting the client add page', () => {
getEditButton().click();
getAddClientBackButton().click();
getNameDisplay().should('contain.text', testClient.name.first);
});
});
We're taking a slightly different approach here. We want to start each test on the ClientDetail
page but we need some data in order to do that. In the beforeEach
we add a test client, and then navigate directly to the detail page for that client.
Now if we check to see if this test fails...
it will actually pass. This is because we have already added the back button to the ClientAdd
page. Since we're responsible TDD testers, let's purposely break the back button to check that it does indeed fail:
<ion-header>
<ion-toolbar>
<ion-title>
<ng-container *ngIf="{ client: client$ | async } as vm">
{{ vm.client ? 'Edit Client' : 'Add Client' }}
</ng-container>
</ion-title>
<ion-buttons slot="start"> </ion-buttons>
</ion-toolbar>
</ion-header>
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).