Running Our Tests with GitHub Actions

Now that we have a few tests written for our Next.js store, let's setup GitHub Actions to run our Cypress tests against our app every time we make a pull request. This way we can be sure that our latest changes have not broken anything within the app.

Configuring GitHub Actions

Now that our repo is up on GitHub, we will need to create a GitHub actions config file.

Create a new file called .github/workflows/main.yml

Screen Shot 2022-05-09 at 1.05.14 PM.png

Next we will give our workflow a name:

name: E2E on Chrome

Then tell GitHub when to run this action:

name: E2E on Chrome

on: [push]

The on: directive tells GitHub to run this workflow each time a push is made to our repo. This way, each time we push up a new branch and create a new pull request, this GitHub Action Workflow will run.

Next, we will setup the job we want to run:

name: E2E on Chrome

on: [push]

jobs:
  install:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/[email protected]

We have a single install: job which will run on the latest version of Ubuntu. Then underneath the steps: directive we are using the actions/checkout GitHub Action. "This action checks-out your repository under $GITHUB_WORKSPACE, so your workflow can access it."

Next, we will have our GitHub Actions Workflow use the official cypress-io/github-action

name: E2E on Chrome

on: [push]

jobs:
  install:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/[email protected]

      - name: Cypress run
        uses: cypress-io/github-[email protected]
        with:
          project: ./site
          browser: chrome
          build: yarn build
          start: yarn start
          wait-on: "http://localhost:3000"

cypress-io/github-action is the official GitHub Action created by Cypress. Under the with: directive we are telling Cypress to:

  • project: Look for Cypress and our tests inside of the site/ directory.
  • browser: run our tests inside of the chrome browser.
  • build: run the build script in the package.json in the root of the repo which builds the production version of our Next.js application.
  • start: run the start script in the package.json in the root of the repo which serves the production build of our application with a local dev server.
  • wait-on: tells Cypress to make sure that [http://localhost:3000](http://localhost:3000) is up and running before it runs our tests.

You can find the documentation for this action here.

Finally, we need to pass in our env variables we currently live in the .env.local file.

The entire GitHub Actions Workflow config file should look like this:

name: E2E on Chrome

on: [push]

jobs:
  install:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/[email protected]

      - name: Cypress run
        uses: cypress-io/github-[email protected]
        with:
          project: ./site
          browser: chrome
          build: yarn build
          start: yarn start
          wait-on: "http://localhost:3000"
        env:
          COMMERCE_PROVIDER: ${{ secrets.COMMERCE_PROVIDER }}
          NEXT_PUBLIC_SHOPIFY_STOREFRONT_ACCESS_TOKEN: ${{ secrets.NEXT_PUBLIC_SHOPIFY_STOREFRONT_ACCESS_TOKEN }}
          NEXT_PUBLIC_SHOPIFY_STORE_DOMAIN: ${{ secrets.NEXT_PUBLIC_SHOPIFY_STORE_DOMAIN }}

Now we need to add the values for these environment variables in our GitHub repo so that our workflow has access to them.

Adding GitHub Secrets

At the top of your repo in GitHub, click on "Settings"

In the left sidebar, click on "Secrets" then “Actions”

screencapture-github-robertguss-vercel-commerce-settings-secrets-actions-2022-05-09-13_14_07.png

Then click on the "New repository secret" button in the top right.

Screen Shot 2021-12-16 at 1.35.35 PM.png

Now you can just add the key and value of the three env variables within the .env.local file.

screencapture-github-robertguss-vercel-commerce-settings-secrets-actions-2022-05-09-14_17_35.png

Running our Tests in GitHub Actions

Next, within your terminal, checkout a new branch in git.

git checkout -b github-actions-cypress-tests

Add your changes and push up your branch

git add .
git commit -m "added cypress tests and GitHub action config"
git push origin github-actions-cypress-tests

After your new branch has been pushed up, click on the "Code" link at the top of your repo.

Screen Shot 2021-12-16 at 1.41.22 PM.png

Then click on the "Compare & pull request" button to create a new PR.

Screen Shot 2021-12-16 at 1.41.28 PM.png

Then click "Create pull request"

Screen Shot 2021-12-16 at 1.42.47 PM.png

GitHub Actions in Action

You will now see the GitHub Actions workflow running. If you click on "Details" you can watch all of the steps inside of the workflow run.

Screen Shot 2021-12-16 at 1.43.33 PM.png

You will see that one of our tests has failed.

Screen Shot 2021-12-16 at 1.45.10 PM.png

It looks like Cypress could not find our product on the search results page. Let's run this test locally and make sure it is passing.

Screen Shot 2021-12-16 at 1.49.08 PM.png

We know that this test was passing locally, so why is it failing in CI?

Our error says the following:

warning

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

So it seems like the first part of our test is working, but once we get to the search results page it cannot find the product-tag element. We will add some data-test attributes to the components/product/ProductCard.tsx component. Around line 73

{
  !noNameTag && (
    <div className={s.header} data-test="product-card">
      <h3 className={s.name}>
        <span data-test="product-name">{product.name}</span>
      </h3>
      <div className={s.price} data-test="product-price">
        {`${price} ${product.price?.currencyCode}`}
      </div>
    </div>
  )
}

The entire component file should look like this:

import { FC } from 'react'
import cn from 'clsx'
import Link from 'next/link'
import type { Product } from '@commerce/types/product'
import s from './ProductCard.module.css'
import Image, { ImageProps } from 'next/image'
import WishlistButton from '@components/wishlist/WishlistButton'
import usePrice from '@framework/product/use-price'
import ProductTag from '../ProductTag'

interface Props {
  className?: string
  product: Product
  noNameTag?: boolean
  imgProps?: Omit<ImageProps, 'src' | 'layout' | 'placeholder' | 'blurDataURL'>
  variant?: 'default' | 'slim' | 'simple'
}

const placeholderImg = '/product-img-placeholder.svg'

const ProductCard: FC<Props> = ({
  product,
  imgProps,
  className,
  noNameTag = false,
  variant = 'default',
}) => {
  const { price } = usePrice({
    amount: product.price.value,
    baseAmount: product.price.retailPrice,
    currencyCode: product.price.currencyCode!,
  })

  const rootClassName = cn(
    s.root,
    { [s.slim]: variant === 'slim', [s.simple]: variant === 'simple' },
    className
  )

  return (
    <Link href={`/product/${product.slug}`}>
      <a className={rootClassName} aria-label={product.name}>
        {variant === 'slim' && (
          <>
            <div className={s.header}>
              <span>{product.name}</span>
            </div>
            {product?.images && (
              <div>
                <Image
                  quality="85"
                  src={product.images[0]?.url || placeholderImg}
                  alt={product.name || 'Product Image'}
                  height={320}
                  width={320}
                  layout="fixed"
                  {...imgProps}
                />
              </div>
            )}
          </>
        )}

        {variant === 'simple' && (
          <>
            {process.env.COMMERCE_WISHLIST_ENABLED && (
              <WishlistButton
                className={s.wishlistButton}
                productId={product.id}
                variant={product.variants[0]}
              />
            )}
            {!noNameTag && (
              <div className={s.header} data-test="product-card">
                <h3 className={s.name}>
                  <span data-test="product-name">{product.name}</span>
                </h3>
                <div className={s.price} data-test="product-price">
                  {`${price} ${product.price?.currencyCode}`}
                </div>
              </div>
            )}
            <div className={s.imageContainer}>
              {product?.images && (
                <div>
                  <Image
                    alt={product.name || 'Product Image'}
                    className={s.productImage}
                    src={product.images[0]?.url || placeholderImg}
                    height={540}
                    width={540}
                    quality="85"
                    layout="responsive"
                    {...imgProps}
                  />
                </div>
              )}
            </div>
          </>
        )}

        {variant === 'default' && (
          <>
            {process.env.COMMERCE_WISHLIST_ENABLED && (
              <WishlistButton
                className={s.wishlistButton}
                productId={product.id}
                variant={product.variants[0] as any}
              />
            )}
            <ProductTag
              name={product.name}
              price={`${price} ${product.price?.currencyCode}`}
            />
            <div className={s.imageContainer}>
              {product?.images && (
                <div>
                  <Image
                    alt={product.name || 'Product Image'}
                    className={s.productImage}
                    src={product.images[0]?.url || placeholderImg}
                    height={540}
                    width={540}
                    quality="85"
                    layout="responsive"
                    {...imgProps}
                  />
                </div>
              )}
            </div>
          </>
        )}
      </a>
    </Link>
  )
}

export default ProductCard

And update our test to be the following:

// header.spec.js

it("the search bar returns the correct search results", () => {
  cy.getBySel("search-input").eq(0).type("linux{enter}")

  cy.get('[data-test="product-card"]').within(() => {
    cy.get('[data-test="product-name"]').should("contain", "Linux Shirt")
    cy.get('[data-test="product-price"]').should("contain", "$25.00 USD")
  })
})

Screen Shot 2021-12-16 at 2.10.23 PM.png

Now let's push up these changes and see if our tests will pass now in CI.

git add .
git commit -m "updated search results test and added data-test attributes to ProductCard component"
git push origin github-actions-cypress-tests

Now all of our tests are passing.

Screen Shot 2021-12-16 at 2.14.31 PM.png

Summary

In this lesson we learned how to integrate Cypress with GitHub Actions so that our tests will run against every PR we make in our repo. We also learned how to fix a broken test by adding some additional data-test attributes to one of our app’s components.