Lesson 13

Building out Core Functionality

Adding more tests to cover our core requirements

PRO

Lesson Outline

Building out Core Functionality

So far, we've successfully implemented two of our original six requirements, and we are now up to the last requirement that we considered to be part of the "core functionality":

  • After selecting a specific lesson, the user should be able to see content for that lesson

In order to implement the E2E test for this, we will be creating a new E2E spec file. It doesn't particularly matter where you put your E2E tests, technically speaking they could all be in one giant spec file. However, if a test starts on a particular page then we should create a new spec file for that page. There are a couple of benefits to doing this:

  • It keeps your tests more organised
  • It allows you to more easily arrange your tests with the beforeEach function if all of the tests start from the same page

We will also be creating some additional helper functions and a custom command because there will be a third page (the Lesson page) thrown into the mix.

Create a New E2E Spec File

Let's start by creating a new E2E spec file for our LessonSelect page. Before we start working on the requirement that we will be focusing on this lesson, we are going to add in another E2E test related to the work we did in the last lesson. You may remember that for our Home page we added an additional E2E test to make sure that there was content in the list of modules (i.e. that it wasn't just a list of blank items, which would technically pass the original E2E test). We are going to do the same now for the LessonSelect page, and check that the list of lessons also contains some kind of text content.

Create a file at cypress/e2e/lesson-select.cy.ts and add the following:

import { getLessonListItems } from '../support/utils';

describe('Lesson Select', () => {
  beforeEach(() => {
    cy.navigateToLessonSelectPage();
  });

  it('the list of lessons should contain the titles of the lessons', () => {
    getLessonListItems().first().should('contain.text', 'lesson1');
  });
});

We will run the E2E tests again, but you might notice now that since we have two different spec files we can choose to run all of them or just the one spec file we are interested in now (the lesson-select.cy.ts file). Our tests run pretty quickly at the moment so it won't really matter too much either way, but you might prefer at some point to just run the tests you are currently working on, and then every now and then run all of the tests to make sure nothing unexpected has broken elsewhere.

After running the E2E tests, we will get the following error:

Timed out retrying after 4000ms: Expected to find element: [data-test="lesson-list-item"], but never found it.

The test failed, as we might have expected, but probably not for the reason you expected. We knew the list contained blank items, but this error is telling us that it couldn't find the list at all.

This is because our navigateToLessonSelectPage() command is not set up correctly. Currently, it is just pointing to the root URL, but that means the test would be starting on the Home page.

There are two ways we could approach implementing the navigateToLessonSelectPage() method. Since we are using Angular routing and we know that our routes expect an id, we could just navigate directly to the /module/1 URL or the /module/2 URL. Alternatively, you could take a more behaviour driven approach and instead simulate the steps required to actually get the application to the lesson select page (e.g. load the home page and trigger a click on a lesson). I don't think there is necessarily a "right" way here, just go with whatever makes more sense for your tests. Personally, I find it gives me greater confidence if my E2E behave as close to the way a real user would behave as possible.

We're going to modify our navigateToLessonSelectPage command to navigate to the page just like a user would through the application.

Modify the cypress/support/commands.ts file to reflect the following:

import { getModuleListItems } from '../support/utils';

declare global {
  // eslint-disable-next-line @typescript-eslint/no-namespace
  namespace Cypress {
    interface Chainable<Subject> {
      navigateToHomePage(): typeof navigateToHomePage;
      navigateToLessonSelectPage(): typeof navigateToLessonSelectPage;
    }
  }
}

const navigateToHomePage = () => {
  cy.visit('/');
};

const navigateToLessonSelectPage = () => {
  navigateToHomePage();
  getModuleListItems().first().click();
};

Cypress.Commands.add('navigateToHomePage', navigateToHomePage);
Cypress.Commands.add('navigateToLessonSelectPage', navigateToLessonSelectPage);

export {};

We have modified the navigateToLessonSelectPage() function to first call the navigateToHomePage() function, and once we are on the home page we grab the list of module items (we have imported the getModuleListItems function to help with this) and click the first one. This will get us to the lesson select page in a way that a real user would get there.

If we run the E2E tests again now, it still fails, but we get a different error:

expected <ion-item.item.md.item-fill-none.in-list.ion-activatable.ion-focusable.hydrated> to contain text lesson1, but the text was ''

Let's fix this test by adding in the title to the template now.

Modify the list in src/app/lesson-select/lesson-select.page.html to reflect the following:

<ion-header>
  <ion-toolbar>
    <ion-title>LessonSelect</ion-title>
  </ion-toolbar>
</ion-header>

<ion-content>
  <ion-list *ngIf="module$ | async as module">
    <ion-item
      data-test="lesson-list-item"
      button
      *ngFor="let lesson of module.lessons"
    >
      {{ lesson.title }}
    </ion-item>
  </ion-list>
</ion-content>

Now if we run the E2E tests:

expected <ion-item.item.md.in-list.ion-activatable.ion-focusable.hydrated> to contain text lesson1, but the text was test

We still get an error because, unlike with our unit tests, the data is being supplied by the real module service which looks like this:

  public getModuleById(id: number) {
    return of({
      id: 1,
      title: 'Module One',
      description: 'Test',
      lessons: [{ title: '1' }],
    });
  }

Let's modify that to include the type of lesson data we would expect.

Modify the getModuleById method in src/app/services/modules.service.ts to reflect the following:

  public getModuleById(id: number) {
    return of({
      id: 0,
      title: '',
      description: '',
      lessons: [
        { title: 'lesson1' },
        { title: 'lesson2' },
        { title: 'lesson3' },
        { title: 'lesson4' },
      ],
    });
  }

If we run the E2E tests once more:

Home
  ✓ should be able to view a list of modules
  ✓ the list of modules should contain the titles of the modules
  ✓ after selecting a specific module, the user should be able to see a list of available lessons

Lesson Select
  ✓ the list of lessons should contain the titles of the lessons

All of the tests will pass. One thing I'd like to point out at this stage is how little you need to rely on running ionic serve to check your application when using a TDD approach. All of the little "Oops! I forgot to add this" moments are covered by the tests, and this becomes your method for iterating on the application rather than constantly manually inspecting your application in the browser.

Now let's get to work on implementing our main E2E test for this lesson.

PRO

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).