This tutorial covers:

  1. Setting up a sample progressive web app
  2. Creating tests for the sample app
  3. Building the continuous integration pipeline

Web and browser technology continues to advance, narrowing the gap between the performance of web and native applications. Features that were once exclusive to native applications can be implemented in web applications. This is due in part to the emergence of progressive web applications (PWAs). Web applications can now be installed, receive push notifications, and even work offline. In this post, we will build a simple PWA, write tests for it, and automate the testing process by building a continuous integration (CI) pipeline.

Prerequisites

To follow this post, a few things are required:

  1. Basic knowledge of JavaScript
  2. Node.js installed on your system
  3. HTTP Server Module installed globally on your system (npm install -g http-server)
  4. A CircleCI account

Our tutorials are platform-agnostic, but use CircleCI as an example. If you don’t have a CircleCI account, sign up for a free one here.

Setting up the demo application

To begin, create an application folder by running this command:

mkdir my-pwa

Next, inside the root of the application, create a file named index.html. This file will be the home page for the application. Paste this code into the file:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <link rel="manifest" href="manifest.json" />
    <link rel="stylesheet" type="text/css" href="styles.css" media="all" />


    <title>My PWA Application</title>
  </head>
  <body>

    <h2>
      Welcome to my Progressive Web Application.
    </h2>


    <script src="app.js"></script>
  </body>
</html>

This code block shows a typical HTML page with a title that reads “My PWA Application” and a welcome message. This file also references a manifest.json file (to configure our PWA for installation), a styles.css file for some basic styling, and an app.js file which will load in our service worker. All of these files will be created in the course of this tutorial.

To get a preview, run this command at the root of the application:

http-server

This invokes the http-server module to spin up an ad hoc server to serve the application. The address will be displayed on the console after you run the command. Then you can go to the URL for the application.

Application Home Page

Note: I am running in Incognito mode on the Google Chrome browser with the developer tools opened and mobile view activated. I prefer running PWAs in Incognito mode during development because it ensures that I get updates to my service worker.

Next, add styles by creating a styles.css file in the root folder of the application. Paste this into the file:

/* ./styles.css */

h2 {
  color: blue;
  padding: 5px;
}

This file simply gives the h2 header some padding and colors it blue.

Adding a service worker

Service workers create an engine room that powers PWA capabilities. We will be adding a service worker to this project by creating a new file named serviceworker.js in the root folder of the application. Paste this code into it:

// ./serviceworker.js

var cacheName = "sw-v1";
var filesToCache = ["./", "./index.html", "./styles.css"];

self.addEventListener("install", function (e) {
  console.log("[ServiceWorker] Install v1");
  e.waitUntil(
    caches.open(cacheName).then(function (cache) {
      console.log("[ServiceWorker] Caching app shell");
      return cache.addAll(filesToCache);
    })
  );
});

self.addEventListener("activate", (event) => {
  event.waitUntil(self.clients.claim());
});

self.addEventListener("fetch", function (event) {
  event.respondWith(
    caches.match(event.request).then(function (response) {
      if (response) {
        return response;
      }
      return fetch(event.request);
    })
  );
});

If you have worked on a PWA project before, this code will seem familiar. It first sets a name for the cache, which sets the version of your service worker when the file is updated. When the browser installs the updated service worker file, you can easily identify the version that is currently running. I have used the name sw-v1 to identify it as my first version. This is where you cache your application root files (index.html and styles.css).

Next, an array of the files to be cached is created. These are the files that will be cached in the browser’s memory for the user to access while offline.

Next,, an install event uses the cache name to create a cache where your files are stored.

The next event,activate, is fired when a new version of a service worker is detected after it has been installed. This event specifies that the old service worker stops serving the cached files.

The last event, fetch, intercepts requests by the application and checks for a new cached version of the requested resource. If a new one is available, the cached resource is served. If a new version is not found, a fresh request is made for the resource.

You then need to load the service worker you just created into your application. Create an app.js file in the root folder of the application and paste in this code:

// ./app.js

if ("serviceWorker" in navigator) {
  window.addEventListener("load", function () {
    navigator.serviceWorker.register("./serviceworker.js").then(
      function (registration) {
        console.log("Hurray! Service workers with scope: ", registration.scope);
      },
      function (err) {
        console.log("Oops! ServiceWorker registration failed: ", err);
      }
    );
  });
}

It is time to take your service worker for a test run. Make sure that your app is still running, then do a hard reload on the browser tab it is on: Ctrl + Shift + R. Check the browser console for the log messages you wrote to confirm the installation of the service worker.

Service worker installation

Great!

Your service worker is installed and files are cached for offline access. To confirm the cache, go to the Application tab in Chrome developer tools and expand the Cache Storage section. You will see the named cache that our service worker just created.

Service worker cache

To confirm that your service worker has provided offline capabilities, shut down the http-server service using Ctrl + C. Then refresh the application in your browser. Previously, the offline page would be displayed at this point because the application is no longer running. With the magic that is a service worker, though, your application home page is still available with no interruption.

Adding a manifest file

To finish creating your PWA, you need to create a manifest file. This file activates the Add To Home Screen feature of your application. This defines how your application is installed on the device it is being used on.

Create a manifest.json file in the root of the project. Paste this into it:

{
  "name": "My PWA",
  "short_name": "My PWA",
  "background_color": "#ffffff",
  "display": "standalone",
  "orientation": "portrait",
  "scope": "/index.html",
  "start_url": "/index.html",
  "icons": [
    {
      "src": "icons/icon-128x128.png",
      "sizes": "128x128",
      "type": "image/png"
    },
    {
      "src": "icons/icon-144x144.png",
      "sizes": "144x144",
      "type": "image/png"
    },
    {
      "src": "icons/icon-152x152.png",
      "sizes": "152x152",
      "type": "image/png"
    },
    {
      "src": "icons/icon-192x192.png",
      "sizes": "192x192",
      "type": "image/png"
    },
    {
      "src": "icons/icon-512x512.png",
      "sizes": "512x512",
      "type": "image/png"
    }
  ]
}

This code block defines the name of the application, the preferred orientation, and the background_color for the splash screen. It is a best practice to add the short_name. This optional field specifies the name that will be displayed in the app launcher or a new tab page. Otherwise, the name will be used, and it will be truncated if it is more than 12 characters long.

Note: You can generate a manifest file here. You can also use that site to generate icons for your application.

This is a very simple, lean app, so I have ignored features like theme_color, splash_pages, some standard icon sizes, and iOS icon support.

The application home page is set at /index.html, and the installation icon sizes for different devices are defined in the icons property. I have also moved the icons directory containing all my icons to the root of the project. That is everything you need for your application to function as a PWA.

To confirm this, run a Lighthouse test. Go to the Lighthouse tab in Chrome developer tools to run the test.

Lighthouse

Click Generate Report then Analyze page load to get an audit report for your PWA.

Lighthouse Report

Click the PWA icon on the far right to go to the results of the PWA compatibility test.

Note: The failing check, Does not redirect HTTP traffic to HTTPS, will pass when you deploy your site and enable HTTPS.

If you want to learn more about getting a clean build, check out these links:

Adding tests

To begin adding tests to your application, create a package.json file for the npm packages you will be installing. To create a basic file at the project root and skip the QA process, run this command:

npm init -y

To set up tests in the application, install these packages:

Install these packages all at once using this command:

npm install --save-dev @testing-library/dom @testing-library/jest-dom jsdom jest

When these are installed, create a test file named index.test.js in the root folder. Paste this code into it:

// ./index.test.js

const { getByText } = require("@testing-library/dom");
require("@testing-library/jest-dom/extend-expect");
const { JSDOM } = require("jsdom");
const fs = require("fs");
const path = require("path");

const html = fs.readFileSync(path.resolve(__dirname, "./index.html"), "utf8");

let dom;
let container;

describe("Test for elements on Home page", () => {
  beforeEach(() => {
    dom = new JSDOM(html, { runScripts: "dangerously" });
    container = dom.window.document.body;
  });

  it("Page renders a heading element", () => {
    expect(container.querySelector("h2")).not.toBeNull();
    expect(getByText(container, "Welcome to my Progressive Web Application.")
    ).toBeInTheDocument();
  });
});

This code block starts by fetching all the necessary dependencies and loading the HTML file. The beforeEach method in the describe block creates the DOM with JSDOM.

A test is run in the it block to check for the occurrence of the header element text: Welcome to my progressive web application.

Now, set up your test script in the package.json file. Edit the existing test key-value pair with this:

...
"scripts": {
    "test": "jest"
},
...

Now run the test file using this command:

npm run test

Your tests will all pass. Now you are ready to start building your CI pipeline.

Local Tests Run

Building a pipeline for PWA continuous integration

Steps for creating a CI pipeline for your application:

  • Add a pipeline configuration script to the application.
  • Push the project to a remote repository: you can use GitHub.
  • Add the repository as a project on CircleCI.
  • Run the pipeline using the configuration file in your project.

At the root of your project, create a folder named .circleci. Add a file named config.yml. Inside the config.yml file, enter this code:

version: 2.1
jobs:
  build:
    working_directory: ~/repo
    docker:
      - image: cimg/node:17.4.0
    steps:
      - checkout
      - run:
          name: Update NPM
          command: "sudo npm install -g npm"
      - restore_cache:
          key: dependency-cache-{{ checksum "package-lock.json" }}
      - run:
          name: Install Dependencies
          command: npm install
      - save_cache:
          key: dependency-cache-{{ checksum "package-lock.json" }}
          paths:
            - ./node_modules
      - run:
          name: Run tests
          command: npm run test

This code block checks out the application from the repository and updates the npm version for your Node.js environment. It then installs dependencies and caches the node_modules folder. The last step is running the tests.

Push your project changes to the GitHub repository. Make sure to add a .gitignore file to the node_modules folder.

Now, set up the repository for your application as a CircleCI project. On the CircleCI dashboard, locate your project and click Set Up Project.

Select Project

You will be prompted to either write a new configuration file or use the existing one. Select the existing one and enter the name of the branch where your code is stored on GitHub. Click Set Up Project.

Select Configuration file

This triggers the CI pipeline to run the build process. This process can be reviewed on the Pipelines page of your CircleCI account. You should now have a successful build.

Add Project

Congratulations! All steps were run perfectly and your tests passed successfully. Now you have everything set up for your continuous integration practice. All you need to do is push your code changes to your repository and the CI pipeline will automatically build and test your app.

Conclusion

In this article, you successfully built a PWA and set up a CI pipeline for test automation.

Happy coding!


Fikayo Adepoju is a LinkedIn Learning (Lynda.com) Author, Full-stack developer, technical writer, and tech content creator proficient in Web and Mobile technologies and DevOps with over 10 years experience developing scalable distributed applications. With over 40 articles written for CircleCI, Twilio, Auth0, and The New Stack blogs, and also on his personal Medium page, he loves to share his knowledge to as many developers as would benefit from it. You can also check out his video courses on Udemy.

Read more posts by Fikayo Adepoju