Creating a Modern Firebase Powered Application with TDD
Use reactive programming and tests to build a professional app
[Sprint Three] View Feedback
Another custom component and modifying state
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
Feat: View feedback that has been submitted anonymously
Now we can move on to our final user story for this sprint - we need a way for the admin to actually view the feedback that has been submitted.
User story: As a massage therapist, I want the ability to view feedback that has been submitted anonymously, so that I can use that information to improve my services
Project management
Remember to move the card for this task to the Test
column, and create a new task branch to work on.
Again, this isn't a feature that we have already accounted for in our initial wireframe so we will need to decide where in the admin section the massage therapist will be able to retrieve this feedback.
At the moment in the admin interface we have a list of clients and then the ability to click a client to view them. We also have two buttons at the top, one for triggering the logout and one for triggering the add new client page:
We don't want to store feedback inside of a particular client because it is anonymous, so we are going to need a way to navigate to the view feedback page from the main dashboard that we are looking at above.
Some approaches we could consider are:
- Adding a side menu
- Adding tabs
- Adding an additional button
I won't delve too much into how to decide what approach might be best for navigation, as we cover that quite extensively in Navigation Concepts in the UI/UX module.
Viewing the feedback is a relatively minor feature, and I don't really want to go to the extreme of drastically changing the navigation architecture to facilitate it. What I am going to do is add a FAB button in the bottom-right corner of the main admin dashboard that will launch the view feedback page.
This is somewhat limiting. If we were to add even more pages in the future it might become necessary to re-architect into a tab-based approach or something like that, but for now this will work fine and at least right now I'm not anticipating needing to add more sections of the application.
Let's start by adding an E2E test for this user story. Although we already have a feedback.cy.ts
file, the test we are creating now starts from the /clients
page so I am going to utilise that test file instead:
Add the following tests to
clients.cy.ts
:
it('can view feedback', () => {
const feedback = {
response: '{"someProperty": "someValue"}',
};
cy.callFirestore('set', 'feedback/abc123', feedback);
getViewFeedbackButton().click();
getItemsInFeedbackList().first().click();
cy.contains('someValue');
});
it('can navigate back to the clients page from the feedback page', () => {
const feedback = {
response: '{}',
};
cy.callFirestore('set', 'feedback/abc123', feedback);
getViewFeedbackButton().click();
getItemsInFeedbackList().first().click();
getViewFeedbackDetailBackButton().click();
getViewFeedbackBackButton().click();
getTitle().should('contain.text', 'Clients');
});
Since we are dealing with a new collection now, we are going to make sure to clean up after ourselves in the beforeEach
block:
beforeEach(() => {
cy.login();
cy.visit('/clients');
cy.callFirestore('delete', 'clients');
cy.callFirestore('delete', 'feedback');
});
We will need to add some new utility methods as well:
export const getViewFeedbackButton = () =>
cy.get('[data-test="view-feedback-button"]');
export const getViewFeedbackBackButton = () =>
cy.get('[data-test="view-feedback-back-button"]');
export const getViewFeedbackDetailBackButton = () =>
cy.get('[data-test="view-feedback-detail-back-button"]');
export const getItemsInFeedbackList = () =>
cy.get('[data-test="feedback-list"] ion-item');
and now let's check that it fails:
Timed out retrying after 4000ms: Expected to find element: [data-test="view-feedback-button"], but never found it.
Great! Now I'm going to jump ahead and implement a few things at once here predicting the future test failures, I am going to:
- Create the
ViewFeedback
page that will display a list of all of the available feedback - Create the
ViewFeedbackDetail
page for viewing specific feedback - Add the FAB button for launching the
ViewFeedback
page
ionic g page clients/feature/ClientFeedback --module clients/feature/client-shell/client-shell-routing.module --route-path="feedback"
ionic g page clients/feature/ClientFeedbackDetail --module clients/feature/client-feedback/client-feedback-routing.module --route-path=":id"
The commands above will auto-generate out routes for us, but it is important to keep in mind the order of the routes. This is what our client-shell-routing.module.ts
will look like after running the command above:
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
const routes: Routes = [
{
path: '',
loadChildren: () =>
import('../client-dashboard/client-dashboard.module').then(
(m) => m.ClientDashboardPageModule
),
},
{
path: 'add',
loadChildren: () =>
import('../client-add/client-add.module').then(
(m) => m.ClientAddPageModule
),
},
{
path: ':id',
loadChildren: () =>
import('../client-detail/client-detail.module').then(
(m) => m.ClientDetailPageModule
),
},
{
path: 'feedback',
loadChildren: () => import('../client-feedback/client-feedback.module').then( m => m.ClientFeedbackPageModule)
},
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
})
export class ClientShellRoutingModule {}
This is a problem because feedback
comes after our :id
route. This means that when we go to a URL with feedback
it is just going to treat that like an id
and activate the detail page. We need to make sure to move our feedback
route above the :id
route:
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
const routes: Routes = [
{
path: '',
loadChildren: () =>
import('../client-dashboard/client-dashboard.module').then(
(m) => m.ClientDashboardPageModule
),
},
{
path: 'add',
loadChildren: () =>
import('../client-add/client-add.module').then(
(m) => m.ClientAddPageModule
),
},
{
path: 'feedback',
loadChildren: () =>
import('../client-feedback/client-feedback.module').then(
(m) => m.ClientFeedbackPageModule
),
},
{
path: ':id',
loadChildren: () =>
import('../client-detail/client-detail.module').then(
(m) => m.ClientDetailPageModule
),
},
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
})
export class ClientShellRoutingModule {}
Now let's add the FAB button to the client dashboard page:
<ion-content class="ion-padding">
<ng-container *ngIf="{ clients: clientsStore.clients$ | async } as vm">
<app-client-list [clients]="vm.clients"></app-client-list>
</ng-container>
<ion-fab vertical="bottom" horizontal="end" slot="fixed">
<ion-fab-button
data-test="view-feedback-button"
class="feedback-button"
routerLink="feedback"
routerDirection="forward"
>
<ion-icon name="chatbubbles-outline"></ion-icon>
</ion-fab-button>
</ion-fab>
</ion-content>
Now let's see where we are with the E2E test:
Timed out retrying after 4000ms: Expected to find element: [data-test=list] ion-item, but never found it.
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).