Unit Testing and Functional Testing: Frontend headless browser testing with Puppeteer

A big cost area in testing is manual user interface testing. Therefore, a wide range of tools has been developed to automate running tests at the HTTP level. Selenium is a popular tool implemented in Java, for example. In the Node.js world, we have a few interesting choices. The chai-http plugin to Chai would let us interact at the HTTP level with the Notes application while staying within the now-familiar Chai environment.

However, in this section, we’ll use Puppeteer (https://github.com/GoogleChrome/puppeteer). This tool is a high-level Node.js module used to control a headless Chrome or Chromium browser, using the DevTools protocol. This protocol allows tools to instrument, inspect, debug, and profile Chromium or Chrome browser instances. The key result is that we can test the Notes application in a real browser so that we have greater assurance it behaves correctly for users.

Puppeteer is meant to be a general-purpose test automation tool and has a strong feature set for that purpose. Because it’s easy to make web page screenshots with Puppeteer, it can also be used in a screenshot service.

Because Puppeteer is controlling a real web browser, your user interface tests will be very close to live browser testing, without having to hire a human to do the work.

Because it uses a headless version of Chrome, no visible browser window will show on your screen, and tests can be run in the background instead. It can also drive other browsers by using the DevTools protocol.

First, let’s set up a directory to work in.

1. Setting up a Puppeteer-based testing project directory

First, let’s set up the directory that we’ll install Puppeteer in, as well as the other packages that will be required for this project:

$ mkdir test-compose/notesui

$ cd test-compose/notesui

$ npm init

… answer the questions

$ npm install \

puppeteer@^4.x mocha@^7.x chai@^4.x supertest@^4.x bcrypt@^4.x

\ cross-env@7.x \


This installs not just Puppeteer, but Mocha, Chai, and Supertest. We’ll also be using the package.json file to record scripts.

During installation, you’ll see that Puppeteer causes Chromium to be downloaded, like so:

Downloading Chromium r756035 – 118.4 Mb [======= ] 35% 30.4s 

The Puppeteer package will launch that Chromium instance as needed, managing it as a background process and communicating with it using the DevTools protocol.

The approach we’ll follow is to test against the Notes stack we’ve deployed in the test Docker infrastructure. Therefore, we need to launch that infrastructure:

$ cd ..

$ docker stack deploy –compose-file docker-compose.yml notes

… as before

Depending on what you need to do, docker-compose build might also be required. In any case, this brings up the test infrastructure and lets you see the running system.

We can use a browser to visit http://localhost:3000 and so on. Because this system won’t contain any users, our test script will have to add a test user so that the test can log in and add notes.

Another item of significance is that tests will be running in an anonymous Chromium instance. Even if we use Chrome as our normal desktop browser, this Chromium instance will have no connection to our normal desktop setup. That’s a good thing from a testability standpoint since it means your test results will not be affected by your personal web browser configuration. On the other hand, it means Twitter login testing is not possible, because that Chromium instance does not have a Twitter login session.

With those things in mind, let’s write an initial test suite. We’ll start with a simple initial test case to prove we can run Puppeteer inside Mocha. Then, we’ll test the login and logout functionality, the ability to add notes, and a couple of negative test scenarios. We’ll close this section with a discussion on improving testability in HTML applications. Let’s get started.

2. Creating an initial Puppeteer test for the Notes application stack

Our first test goal is to set up the outline of a test suite. We will need to do the following, in order:

  1. Add a test user to the user authentication service.
  2. Launch the browser.
  3. Visit the home page.
  4. Verify the home page came up.
  5. Close the browser.
  6. Delete the test user.

This will establish that we have the ability to interact with the launched infrastructure, start the browser, and see the Notes application. We will continue with the policy and clean up after the test to ensure a clean environment for subsequent test runs and will add, then remove, a test user.

In the notesui directory, create a file named uitest.mjs containing the following code:

import Chai from ‘chai’;

const assert = Chai.assert;

import supertest from ‘supertest’;

const request = supertest(process.env.URL_USERS_TEST);

const authUser = ‘them’;

const authKey = ‘D4ED43C0-8BD6-4FE2-B358-7C0E230D11EF’;

import { default as bcrypt } from ‘bcrypt’;

const saltRounds = 10;

import puppeteer from ‘puppeteer’;

async function hashpass(password) {

let salt = await bcrypt.genSalt(saltRounds);

let hashed = await bcrypt.hash(password, salt);

return hashed;


This imports and configures the required modules. This includes setting up bcrypt support in the same way that is used in the authentication server. We’ve also copied in the authentication key for the user authentication backend service. As we did for the REST test suite, we will use the SuperTest library to add, verify, and remove the test user using the REST API snippets copied from the REST tests.

Add the following test block:

describe(‘Initialize test user’, function() {

it(‘should successfully add test user’, async function() {

await request.post(‘/create-user’).send({

username: “testme”, password: await hashpass(“w0rd”), provider: “local”,

familyName: “Einarrsdottir”, givenName: “Ashildr”,

middleName: “TEST”, emails: [ “md@stolen.test.tardis” ],

photos: []


.set(‘Content-Type’, ‘application/json’)

.set(‘Acccept’, ‘application/json’)

.auth(authUser, authKey);



This adds a user to the authentication service. Refer back and you’ll see this is similar to the test case in the REST test suite. If you want a verification phase, there is another test case that calls the /find/testme endpoint to verify the result. Since we’ve already verified the authentication system, we do not need to reverify it here. We just need to ensure we have a known test user we can use for scenarios where the browser must be logged in.

Keep this at the very end of uitest.mjs:

describe(‘Destroy test user’, function() {

it(‘should successfully destroy test user’, async function() {

await request.delete(‘/destroy/testme’)

.set(‘Content-Type’, ‘application/json’)

.set(‘Acccept’, ‘application/json’)

.auth(authUser, authKey);



At the end of the test execution, we should run this to delete the test user. The policy is to clean up after we execute the test. Again, this was copied from the user authentication service test suite. Between those two, add the following:

describe(‘Notes’, function() {


let browser; let page;

before(async function() {

browser = await puppeteer.launch({

sloMo: 500, headless: false


page = await browser.newPage();


it(‘should visit home page’, async function() {

await page.goto(process.env.NOTES_HOME_URL);

await page.waitForSelector(‘a.nav-item[href=”/users/login”]’);


// Other test scenarios go here. after(async function() {

await page.close();

await browser.close();



Remember that within describe, the tests are the it blocks. The before block is executed before all the it blocks, and the after block is executed afterward.

In the before function, we set up Puppeteer by launching a Puppeteer instance and starting a new Page object. Because puppeteer.launch has the headless option set to false, we’ll see a browser window on the screen. This will be useful so we can see what’s happening. The sloMo option also helps us see what’s happening by slowing down the browser interaction. In the after function, we call the close method on those objects in order to close out the browser. The puppeteer.launch method takes an options object, with a long list of attributes that are worth learning about.

The browser object represents the entire browser instance that the test is being run on. In contrast, the page object represents what is essentially the currently open tab in the browser. Most Puppeteer functions execute asynchronously. Therefore, we can use async functions and the await keywords.

The timeout setting is required because it sometimes takes a longish time for the browser instance to launch. We’re being generous with the timeout to minimize the risk of spurious test failures.

For the it clause, we do a tiny amount of browser interaction. Being a wrapper around a browser tab, the page object has methods related to managing an open tab. For example, the goto method tells the browser tab to navigate to the given URL. In this case, the URL is the Notes home page, which is passed in as an environment variable.

The waitForSelector method is part of a group of methods that wait for certain conditions. These include waitForFileChooser, waitForFunction, waitForNavigation, waitForRequest, waitForResponse, and waitForXPath. These, and the waitFor method, all cause Puppeteer to asynchronously wait for a condition to happen in the browser. The purpose of these methods is to give the browser time to respond to some input, such as clicking on a button. In this case, it waits until the web page loading process has an element visible at the given CSS selector. That selector refers to the Login button, which will be in the header.

In other words, this test visits the Notes home page and then waits until the Login button appears. We could call that a simple smoke test that’s quickly executed and determines that the basic functionality is there.

2.1. Executing the initial Puppeteer test

We have the beginning of a Puppeteer-driven test suite for the Notes application. We have already launched the test infrastructure using docker-compose. To run the test script, add the following to the scripts section of the package.json file:

“test”: “cross-env URL_USERS_TEST=http://localhost:5858

NOTES_HOME_URL=http://localhost:3000 mocha uitest.mjs”

The test infrastructure we deployed earlier exposes the user authentication service on port 5858 and the Notes application on port 3000. If you want to test against a different deployment, adjust these URLs appropriately. Before running this, the Docker test infrastructure must be launched, which should have already happened.

Let’s try running this initial test suite:

$ npm run test

> notesui@1.0.0 test /Users/David/Chapter13/compose-test/notesui

> URL_USERS_TEST=http://localhost:5858

NOTES_HOME_URL=http://localhost:3000 mocha uitest.mjs

Initialize test user

should successfully add test user (125ms)


should visit home page (1328ms)

Destroy test user

should successfully destroy test user (53ms)

 3 passing (5s) 

We have successfully created the structure that we can run these tests in. We have set up Puppeteer and the related packages and created one useful test. The primary win is to have a structure to build further tests on top of.

Our next step is to add more tests.

3. Testing login/logout functionality in Notes

In the previous section, we created the outline within which to test the Notes user interface. We didn’t do much testing regarding the application, but we proved that we can test Notes using Puppeteer.

In this section, we’ll add an actual test. Namely, we’ll test the login and logout functionality. The steps for this are as follows:

  1. Log in using the test user identity.
  2. Verify that the browser was logged in.
  3. Log out.
  4. Verify that the browser is logged out.

In uitest.js, insert the following test code:

describe(‘should log in and log out correctly’, function() {


it(‘should log in correctly’, async function() {

await page.click(‘a.nav-item[href=”/users/login”]’);

await page.waitForSelector(‘form[action=”/users/login”]’);

await page.type(‘[name=username]’, “testme”, {delay: 100});

await page.type(‘[name=password]’, “w0rd”, {delay: 100});

await page.keyboard.press(‘Enter’);

await page.waitForNavigation({

‘waitUntil’: ‘domcontentloaded’



it(‘should be logged in’, async function() {

assert.isNotNull(await page.$(‘a[href=”/users/logout”]’));


it(‘should log out correctly’, async function() {

await page.click(‘a[href=”/users/logout”]’);


it(‘should be logged out’, async function() {

await page.waitForSelector(‘a.nav-item[href=”/users/login”]’);



// Other test scenarios go here.

This is our test implementation for logging in and out. We have to specify the timeout value because it is a new describe block.

The click method takes a CSS selector, meaning this first click event is sent to the Login button. A CSS selector, as the name implies, is similar to or identical to the selectors we’d write in a CSS file. With a CSS selector, we can target specific elements on the page.

To determine the selector to use, look at the HTML for the templates and learn how to describe the element you wish to target. It may be necessary to add ID attributes into the HTML to improve testability.

Clicking on the Login button will, of course, cause the Login page to appear. To verify this, we wait until the page contains a form that posts to /users/login. That form is in login.hbs.

The type method acts as a user typing text. In this case, the selectors target the Username and Password fields of the login form. The delay option inserts a pause of 100 milliseconds after typing each character. It was noted in testing that sometimes, the text arrived with missing letters, indicating that Puppeteer can type faster than the browser can accept.

The page.keyboard object has various methods related to keyboard events. In this case, we’re asking to generate the equivalent to pressing Enter on the keyboard. Since, at that point, the focus is in the Login form, that will cause the form to be submitted to the Notes application. Alternatively, there is a button on that form, and the test could instead click on the button.

The waitForNavigation method has a number of options for waiting on page refreshes to finish. The selected option causes a wait until the DOM content of the new page is loaded.

The $ method searches the DOM for elements matching the selector, returning an array of matching elements. If no elements match, null is returned instead.

Therefore, this is a way to test whether the application got logged in, by looking to see if the page has a Logout button.

To log out, we click on the Logout button. Then, to verify the application logged out, we wait for the page to refresh and show a Login button:

$ npm run test 

> notesui@1.0.0 test /Users/David/Chapter13/compose-test/notesui

> URL_USERS_TEST=http://localhost:5858 NOTES_HOME_URL=http://localhost:3000 mocha mjs

Initialize test user

should successfully add test user (188ms)

should successfully verify test user exists 


should visit home page (1713ms)

log in and log out correctly

should log in correctly (2154ms)

should be logged in

should log out correctly (287ms)

should be logged out (55ms) 

Destroy test user

should successfully destroy test user (38ms)

should successfully verify test user gone (39ms) 

9 passing (7s) 

With that, our new tests are passing. Notice that the time required to execute some of the tests is rather long. Even longer times were observed while debugging the test, which is why we set long timeouts.

That’s good, but of course, there is more to test, such as the ability to add a Note.

4. Testing the ability to add Notes

We have a test case to verify login/logout functionality. The point of this application is adding notes, so we need to test this feature. As a side effect, we will learn how to verify page content with Puppeteer.

To test this feature, we will need to follow these steps:

  1. Log in and verify we are logged in.
  2. Click the Add Note button to get to the form.
  3. Enter the information for a Note.
  4. Verify that we are showing the Note and that the content is correct.
  5. Click on the Delete button and confirm deleting the Note.
  6. Verify that we end up on the home page.
  7. Log out.

You might be wondering “Isn’t it duplicative to log in again?” The previous tests focused on login/logout. Surely that could have ended with the browser in the logged-in state? With the browser still logged in, this test would not need to log in again. While that is true, it would leave the login/logout scenario incompletely tested. It would be cleaner for each scenario to be standalone in terms of whether or not the user is logged in. To avoid duplication, let’s refactor the test slightly.

In the outermost describe block, add the following two functions:

describe(‘Notes’, function() {


let browser; let page;

async function doLogin() {

await page.click(‘a.nav-item[href=”/users/login”]’);

await page.waitForSelector(‘form[action=”/users/login”]’);

await page.type(‘[name=username]’, “testme”, {delay: 150});

await page.type(‘[name=password]’, “w0rd”, {delay: 150});

await page.keyboard.press(‘Enter’);

await page.waitForNavigation({ ‘waitUntil’: ‘domcontentloaded’



async function checkLogin() {

const btnLogout = await page.$(‘a[href=”/users/logout”]’);




This is the same code as the code for the body of the test cases shown previously, but we’ve moved the code to their own functions. With this change, any test case that wishes to log into the test user can use these functions.

Then, we need to change the login/logout tests to this:

describe(‘log in and log out correctly’, function() {


it(‘should log in correctly’, doLogin);

it(‘should be logged in’, checkLogin);


All we’ve done is move the code that had been here into their own functions. This means we can reuse those functions in other tests, thus avoiding duplicative code.

Add the following code for the Note creation test suite to uitest.mjs:

describe(‘allow creating notes’, function() {


it(‘should log in correctly’, doLogin);

it(‘should be logged in’, checkLogin);

it(‘should go to Add Note form’, async function() {

await page.click(‘a[href=”/notes/add”]’);

await page.waitForSelector(‘form[action=”/notes/save”]’);

await page.type(‘[name=notekey]’, “testkey”, {delay: 200});

await page.type(‘[name=title]’, “Test Note Subject”, {delay:150});

await page.type(‘[name=body]’,

“Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.”,

{ delay: 100 });

await page.click(‘button[type=”submit”]’);


it(‘should view newly created Note’, async function() {

await page.waitForSelector(‘h3#notetitle’); assert.include(

await page.$eval(‘h3#notetitle’, el => el.textContent), “Test Note Subject”



await page.$eval(‘#notebody’, el => el.textContent), “Lorem ipsum dolor”


assert.include(page.url(), ‘/notes/view’);


it(‘should delete newly created Note’, async function() {

assert.isNotNull(await page.$(‘a#notedestroy’));

await page.click(‘a#notedestroy’);

await page.waitForSelector(‘form[action=”/notes/destroy/confirm”]’);

await page.click(‘button[type=”submit”]’);

await page.waitForSelector(‘#notetitles’);

assert.isNotNull(await page.$(‘a[href=”/users/logout”]’));

assert.isNotNull(await page.$(‘a[href=”/notes/add”]’));


it(‘should log out’, async function() {

await page.click(‘a[href=”/users/logout”]’);

await page.waitForSelector(‘a[href=”/users/login”]’);



These are our test cases for adding and deleting Notes. We start with the doLogin and checkLogin functions to ensure the browser is logged in.

After clicking on the Add Note button and waiting for the browser to show the form in which we enter the Note details, we need to enter text into the form fields. The page.type method acts as a user typing on a keyboard and types the given text into the field identified by the selector.

The interesting part comes when we verify the note being shown. After clicking the Submit button, the browser is, of course, taken to the page to view the newly created Note. To do this, we use page.$eval to retrieve text from certain elements on the screen.

The page.$eval method scans the page for matching elements, and for each, it calls the supplied callback function. The callback function is given the element, and in our case, we call the textContent method to retrieve the textual form of the element.

Then, we’re able to use the assert.include function to test that the element contains the required text.

The page.url() method, as its name suggests, returns the URL currently being viewed. We can test whether that URL contains /notes/view to be certain the browser is viewing a note.

To delete the note, we start by verifying that the Delete button is on the screen. Of course, this button is there if the user is logged in. Once the button is verified, we click on it and wait for the FORM that confirms that we want to delete the Note. Once it shows up, we can click on the button, after which we are supposed to land on the home page.

Notice that to find the Delete button, we need to refer to a#notedestroy. As it stands, the template in question does not have that ID anywhere. Because the HTML for the Delete button was not set up so that we could easily create a CSS selector, we must edit views/noteedit.hbs to change the Delete button to this:

<a class=”btn btn-outline-dark” id=”notedestroy”



All we did was add the ID attribute. This is an example of improving testability, which we’ll discuss later.

A technique we’re using is to call page.$ to query whether the given element is on the page. This method inspects the page, returning an array containing any matching elements. We are simply testing if the return value is non-null because page.$ returns null if there are no matching elements. This makes for an easy way to test if an element is present.

We end this by logging out by clicking on the Logout button. Having created these test cases, we can run the test suite again:

$ npm run test 

> notesui@1.0.0 test /Users/David/Chapter13/compose-test/notesui

> URL_USERS_TEST=http://localhost:5858

NOTES_HOME_URL=http://localhost:3000 mocha uitest.mjs 

Initialize test user

should successfully add test user (228ms)

should successfully verify test user exists (46ms) 


should visit home page (2214ms)

log in and log out correctly

should log in correctly (2567ms)

should be logged in

should log out correctly (298ms)    

should be logged out

allow creating notes

should log in correctly (2288ms)    

should be logged in

should go to Add Note form (18221ms)

should view newly created Note (39ms)

should delete newly created Note (1225ms) 

12 passing (1m) 

We have more passing tests and have made good progress. Notice how one of the test cases took 18 seconds to finish. That’s partly because we slowed text entry down to make sure it is correctly received in the browser, and there is a fair amount of text to enter. There was a reason we increased the timeout.

In earlier tests, we had success with negative tests, so let’s see if we can find any bugs that way.

5. Implementing negative tests with Puppeteer

Remember that a negative test is used to purposely invoke scenarios that will fail. The idea is to ensure the application fails correctly, in the expected manner.

We have two scenarios for an easy negative test:

  • Attempt to log in using a bad user ID and password
  • Access a bad URL

Both of these are easy to implement, so let’s see how it works.

5.1. Testing login with a bad user ID

A simple way to ensure we have a bad username and password is to generate random text strings for both. An easy way to do that is with the uuid package. This package is about generating Universal Unique IDs (that is, UUIDs), and one of the modes of using the package simply generates a unique random string. That’s all we need for this test; it is a guarantee that the string will be unique.

To make this crystal clear, by using a unique random string, we ensure that we don’t accidentally use a username that might be in the database. Therefore, we will be certain of supplying an unknown username when trying to log in.

In uitest.mjs, add the following to the imports:

import { v4 as uuidv4 } from ‘uuid’;

There are several methods supported by the uuid package, and the v4 method is what generates random strings.

Then, add the following scenario:

describe(‘reject unknown user’, function() {


it(‘should fail to log in unknown user correctly’, async function()


assert.isNotNull(await page.$(‘a[href=”/users/login”]’));

await page.click(‘a.nav-item[href=”/users/login”]’);

await page.waitForSelector(‘form[action=”/users/login”]’);

await page.type(‘[name=username]’, uuidv4(), {delay: 150});

await page.type(‘[name=password]’,

await hashpass(uuidv4()), {delay: 150});

await page.keyboard.press(‘Enter’);

await page.waitForSelector(‘form[action=”/users/login”]’);

assert.isNotNull(await page.$(‘a[href=”/users/login”]’));

assert.isNotNull(await page.$(‘form[action=”/users/login”]’));



This starts with the login scenario. Instead of a fixed username and password, we instead use the results of calling uuidv4(), or the random UUID string.

This does the login action, and then we wait for the resulting page. In trying this manually, we learn that it simply returns us to the login screen and that there is no additional message. Therefore, the test looks for the login form and ensures there is a Login button. Between the two, we are certain the user is not logged in.

We did not find a code error with this test, but there is a user experience error: namely, the fact that, for a failed login attempt, we simply show the login form and do not provide a message (that is, unknown username or password), which leads to a bad user experience. The user is left feeling confused over what just happened. So, let’s put that on our backlog to fix.

5.2. Testing a response to a bad URL

Our next negative test is to try a bad URL in Notes. We coded Notes to return a 404 status code, which means the page or resource was not found. The test is to ask the browser to visit the bad URL, then verify that the result uses the correct error message.

Add the following test case:

describe(‘reject unknown URL’, function() {


it(‘should fail on unknown URL correctly’, async function() {

let u = new URL(process.env.NOTES_HOME_URL);

u.pathname = ‘/bad-unknown-url’;

let response = await page.goto(u.toString());

await page.waitForSelector(‘header.page-header’);

assert.equal(response.status(), 404);


await page.$eval(‘h1’, el => el.textContent), “Not Found”



await page.$eval(‘h2’, el => el.textContent), “404”




This computes the bad URL by taking the URL for the home page (NOTES_HOME_URL) and setting the pathname portion of the URL to /bad-unknown-url. Since there is no route in Notes for this path, we’re certain to get an error. If we wanted more certainty, it seems we could use the uuidv4() function to make the URL random.

Calling page.goto() simply gets the browser to go to the requested URL. For the subsequent page, we wait until a page with a header element shows up. Because this page doesn’t have much on it, the header element is the best choice for determining when we have the subsequent page.

To check the 404 status code, we call response.status(), which is the status code that’s received in the HTTP response. Then, we call page.$eval to get a couple of items from the page and make sure they contain the text that’s expected.

In this case, we did not find any code problems, but we did find another user experience problem. The error page is downright ugly and user-unfriendly. We know the user experience team will scream about this, so add it to your backlog to do something to improve this page.

In this section, we wrapped up test development by creating a couple of negative tests. While this didn’t result in finding code bugs, we found a pair of user experience problems. We know this will result in an unpleasant discussion with the user experience team, so we’ve proactively added a task to the backlog to fix those pages. But we also learned about being on the lookout for any kind of problem that crops up along the way. It’s well-known that the lowest cost of fixing a problem is the issues found by the development or testing team. The cost of fixing problems goes up tremendously when it is the user community reporting the problems.

Before we wrap up this chapter, we need to talk a little more in-depth about testability.

6. Improving testability in the Notes UI

While the Notes application displays well in the browser, how do we write test software to distinguish one page from another? As we saw in this section, the UI test often performed an action that caused a page refresh and had to wait for the next page to appear. This means our test must be able to inspect the page and work out whether the browser is displaying the correct page. An incorrect page is itself a bug in the application. Once the test determines it is the correct page, it can then validate the data on the page.

The bottom line is a requirement stating that each HTML element must be easily addressable using a CSS selector.

While in most cases it is easy to code a CSS selector for every element, in a few cases, this is difficult. The Software Quality Engineering (SQE) manager has requested our assistance. At stake is the testing budget, which will be stretched further the more the SQE team can automate their tests.

All that’s necessary is to add a few id or class attributes to HTML elements to improve testability. With a few identifiers and a commitment to maintaining those identifiers, the SQE team can write repeatable test scripts to validate the application.

We have already seen one example of this: the Delete button in views/noteview.hbs. It proved impossible to write a CSS selector for that button, so we added an ID attribute that let us write the test.

In general, testability is about adding things to an API or user interface for the benefit of software quality testers. For an HTML user interface, that means making sure test scripts can locate any element in the HTML DOM. And as we’ve seen, the id and class attributes go a long way to satisfying that need.

In this section, we learned about user interface testing as a form of functional testing. We used Puppeteer, a framework for driving a headless Chromium browser instance, as the vehicle for testing the Notes user interface. We learned how to automate user interface actions and how to verify that the web pages that showed up matched with their correct behavior. That included test scenarios covering login, logout, adding notes, and logging in with a bad user ID. While this didn’t discover any outright failures, watching the user interaction told us of some usability problems with Notes.

With that, we are ready to close out this chapter.

Source: Herron David (2020), Node.js Web Development: Server-side web development made easy with Node 14 using practical examples, Packt Publishing.

Leave a Reply

Your email address will not be published. Required fields are marked *