ES

Navigate back to the homepage

Keep your Documentation updated with Cypress and Github Actions

Ema Suriano
October 26th, 2020 · 5 min read

One of my favorite ways to document projects is by adding screenshots of how it looks, to provide a quick overview of what it does and looks like. Sadly these images are quite easy to get out-dated, and I was being forced to manually update them … In this post, how I automatize this task by using Cypress and GitHub Actions.

My Case in Particular

I am the owner and maintainer of the Gatsby Starter Mate, which allows developers or tech writers to bootstrap their portfolio and manage its content with a CMS.

Although one can always open the Demo website and navigate through it to see how the project is, I decided to create a simple Table where I show how each of the sections looks like. Here is the table extracted from the README.md.

SectionScreenshot
Home
Home
About me
About me
Projects
Projects
Writing
Writing
404
404

As I highlighted at the beginning of this post, in case I made a change in the UI: CSS change, content change, add/remove new components, etc. I had to regenerate these screenshots … The process was as follows:

  1. Start the development server.
  2. Open the website in the browser (trying to always use the same window size).
  3. Take a screenshot and store it in the repository.
  4. Remove the old image and rename the new with the original name.

Besides the process was not hard to follow, I had to repeat it for each of the sections and in many times I forgot to do it after I made any change … It was time to bring the machines to the play! 🦾

Time to automate
Time to automate

Setting up Cypress 🤖

Cypress is a JavaScript End-to-End Testing that provides a nice framework to work with and it’s capable of emulating a browser that can interact with any website by using the browser APIs.

Even though this tool was built in mind of Testing you can use it as a Photographer, to make the Browser to take screenshots of your website and store them on your repository!

As this tool is not needed when the application is running in production, it’s always recommended installing it as a development dependency:

1# npm
2> npm install cypress --save-dev
3
4# yarn
5> yarn add cypress --dev

Then you have to create a file called cypress.json located at the root of your directory, providing the URL of your project:

1{
2 "baseUrl": "http://localhost:3000/",
3 "screenshotsFolder": "screenshots"
4}

DISCLAIMER: In case you provided a localhost route, you have to ensure that your development server is running in the background when Cypress is running.

Next, let’s add a simple health check that will navigate to the Home Page of the project and check if the local server is answering correctly. For that create a file called health.spec.js, located inside a folder called cypress/integration at the root folder of the repository:

1// cypress/integration/health.spec.js
2
3it('health test', () => {
4 cy.visit('/');
5});

After this you can finally execute Cypress by running:

1> yarn cypress run
2
3====================================================================
4
5 (Run Starting)
6
7 ┌────────────────────────────────────────────────────────────────────────────────────────────────┐
8 │ Cypress: 5.4.0 │
9 │ Browser: Electron 85 (headless)
10 │ Specs: 1 found (health.spec.js)
11 └────────────────────────────────────────────────────────────────────────────────────────────────┘
12
13
14────────────────────────────────────────────────────────────────────────────────────────────────────
15
16 Running: health.spec.js (1 of 1)
17
18
19 ✓ health test (293ms)
20
21 1 passing (304ms)
22
23
24 (Results)
25
26 ┌────────────────────────────────────────────────────────────────────────────────────────────────┐
27 │ Tests: 1 │
28 │ Passing: 1 │
29 │ Failing: 0 │
30 │ Pending: 0 │
31 │ Skipped: 0 │
32 │ Screenshots: 0 │
33 │ Video: false
34 │ Duration: 0 seconds │
35 │ Spec Ran: health.spec.js │
36 └────────────────────────────────────────────────────────────────────────────────────────────────┘
37
38
39====================================================================
40
41 (Run Finished)
42
43
44 Spec Tests Passing Failing Pending Skipped
45 ┌────────────────────────────────────────────────────────────────────────────────────────────────┐
46 │ ✔ health.spec.js 301ms 1 1 - - - │
47 └────────────────────────────────────────────────────────────────────────────────────────────────┘
48 ✔ All specs passed! 301ms 1 1 - - -
49
50✨ Done in 9.67s.

This command will create the folders of plugins and support inside the Cypress folder with some boilerplate inside, which will come in handy for the future steps.

Taking screenshots with Cypress 📸

Time to make Cypress take those screenshots! Let’s start by adding a new file inside integration called photographer.test.js, with some basic tests:

1describe('Photographer', () => {
2 beforeEach(() => {
3 cy.viewport('macbook-15'); // Desktop viewport
4 });
5
6 // Taking screenshots by Sections
7 it('Landing Section', () => {
8 cy.visit('/');
9 cy.get('#home').screenshot('Landing');
10 });
11
12 it('About Section', () => {
13 cy.visit('/');
14 cy.get('#about')
15 .scrollIntoView()
16 .screenshot('About');
17 });
18
19 // Taking screenshots of the whole page
20 it('404 Page', () => {
21 cy.visit('/');
22 cy.get('#404').screenshot('404');
23 });
24});

The URL of the pages to visit and CSS selectors may change depending on your project, but the idea will remain the same:

  • Visit a particular route of your application.
  • Use a CSS selector to pick a section or grab all the content.
  • Take a screenshot and save it using a descriptive name.

Once you are done with setting the routes and name of your section, run Cypress by executing yarn cypress run, and you will see a new folder called screenshots located at the root of your project with the screenshots taken by Cypress.

1📦screenshots
2 ┗ 📂photographer.test.js
3 ┣ 🖼404.png
4 ┣ 🖼About.png
5 ┣ 🖼Landing.png
6 ┣ 🖼Projects.png
7 ┗ 🖼Writing.png

Be aware that Cypress will remove and recreate this folder on every run, so don’t worry if your images disappear between runs. Also, at this point you can remove the test health.spec.js, because it’s not needed anymore 👍

The last piece will be changing your documentation to point to the auto generated pictures instead of the manual. I recommend using a table because it provides a very clear interface for the readers to visualize the pictures. This is the one I used:

1## Screenshot and Design 🖼
2
3| Section | Screenshot |
4| -------- | :--------------------------------------------------------: |
5| Home | ![Home](screenshots/photographer.test.js/Landing.png) |
6| About me | ![About me](screenshots/photographer.test.js/About.png) |
7| Projects | ![Projects](screenshots/photographer.test.js/Projects.png) |
8| Writing | ![Writing](screenshots/photographer.test.js/Writing.png) |
9| 404 | ![404](screenshots/photographer.test.js/404.png) |

Automatizing the screenshot (now for real)

Up to this point, Cypress is taking the screenshots for me, but I still have the task to call it by running yarn cypress run, and also I have to make sure my development server is running at that time to generate valid screenshots.

To address the latter, I recommend using an npm package called start-server-and-test which does exactly what is says: start any server, wait for a specific port to be open, and finally run the test. It’s very useful in these situations 🙌

To install it, you can use either yarn or npm.

1# npm
2> npm install start-server-and-test --save-dev
3
4# yarn
5> yarn add start-server-and-test --dev

Then inside your package.json, you can add the following commands inside the scripts property:

1{
2 "scripts": {
3 "test": "cypress run",
4 "start": "react-scripts start",
5 "take-screenshots": "start-server-and-test 3000"
6 }
7}

To test it, you can simply execute the take-screenshot command and you should be able to see something like:

1➜ my-project git:(master)yarn take-screenshots
2yarn run v1.22.5
3$ start-server-and-test 3000
41: starting server using command "npm run start"
5and when url "[ 'http://localhost:3000' ]" is responding with HTTP status code 200
6running tests using command "npm run test"
7
8...
Problem Solved
Problem Solved

Now I only need something that will call this command automatically for me and push it into my Repository. This is when GitHub Actions comes into the Game!

DISCLAIMER: you can use any Continuous Integration technology (Travis CI, Circle CI, etc.), but I find the implementation with Actions quite simple and straight forward.

To enable GitHub Actions, you have to create a new Workflow file located inside .github/workflows folder. You can name this file as you want, the only rule to follow is that it has to be a YAML file.

1# .github/workflows/update-docs.yml
2
3# Workflow name
4name: Update Docs
5
6# Run on every push to master
7on:
8 push:
9 branches: master
10
11jobs:
12 # Job name
13 update-readme:
14 # Environment setup
15 runs-on: ubuntu-latest
16
17 # job
18 steps:
19 - name: Checkout repository
20 uses: actions/checkout@v2
21 - name: Setup Node
22 uses: actions/setup-node@v1
23 - name: Install dependencies
24 run: yarn install --frozen-lockfile
25 - name: Take screenshots
26 run: yarn take-screenshots
27 - name: Commit screenshots
28 uses: stefanzweifel/git-auto-commit-action@v4
29 with:
30 commit_message: Update Screenshots

Something I like about the Workflows from GitHub Actions is how descriptive they are. It consists of:

  • Name of the workflow, which serves as an identifier (name).
  • When the Workflow will be triggered (on).
  • What it will do once triggered (jobs). Each Job has:
    • Unique identifier.
    • On which environment it will run (runs-on).
    • Set of steps to run scripts or call other Actions (steps).

One of the best features of GitHub Actions is the ability to compose Actions. In the example from above, to commit the changes inside the screenshots I am using the git-auto-commit-action which will look at my changes and then perform a git commit -am "commit_message" into my repository.

Finally, to test if your Workflow is working you have to push your changes into your master branch. Here you can see a normal execution I run inside gatsby-starter-mate.

Taking it to the next level 🚀

In case you have noticed inside the photographer.test.js file, I had to type the name of my routes and the selectors that I wanted to use. But what if, I can also automate this too? 🤔

The first step will be to collect all the routes inside the application. This will change depending on the technology and routing you have picked. But from here you should produce a file called routes.json with the list of all the routes.

Next, you have to store this file inside the folder cypress/fixtures, so then Cypress can have access to it inside the photographer.test.js file:

1describe('Dynamic Photographer', () => {
2 const routes = require('../fixtures/routes.json');
3
4 beforeEach(() => {
5 cy.viewport('macbook-15'); // Desktop viewport
6 });
7
8 routes.forEach(route => {
9 it(`${route}`, () => {
10 cy.visit('/');
11 cy.get('#shared-section-id').screenshot(route);
12 });
13 });
14});

Finally, you have to generate the table based on the routes.json file or from the generated images. In this case, I will do it from the file, but in both cases, the output should be the same.

1// generate-docs-readme.js
2const fs = require('fs');
3const routes = require('./cypress/fixtures/routes.json');
4
5const routeToTable = route =>
6 `| ${route} | ![${route}](screenshots/photographer.test.js/${route}.png) |`;
7
8const content = [
9 '## Pages Screenshots',
10 'Dynamic screenshots based on the last version deployed.',
11 '| Page | Screenshot |',
12 '| --- | :---: |',
13 ...routes.map(routeToTable),
14];
15
16fs.writeFileSync('photographer.md', content.join('\n'));

This idea can be improved in several ways:

  • Expose specific CSS selectors depending on the screen, to have more accurate screenshots.
  • Expose names of screens instead of treating them as routes.
  • With the combination of the idea from above, your file routes.json can tell Cypress to take more than one picture from a Page.

Last Words

I had a ton of fun integrating these two tools and, I think the result it’s quite useful. I hope more people also found it appealing and decided to try it on their own projects to battle against out-dated documentation 😅

In this post, I nearly used all the potential from these two tools. I highly recommend passing by though their official documentation: Cypress Docs and GitHub Actions Docs. They provide much more functionality inside.

Thanks for reading and let’s keep building stuff together! 👷‍♂️

References

Newsletter Subscription 🚨

Gets notified in case I published a new article. I tend to write about my challenges inside the weird and fast Frontend world, from learning a specific tool or framework to building a project from scratch ✨

I try to publish one article per month, if life does not get in the middle ... No SPAM, no hiring, no application marketing, just tech posts.

More articles from Ema Suriano

Dynamic Types Validation in Typescript

There is no doubt that Typescript has gained a huge adoption on the JavaScript ecosystem, and one of the great benefits it provides is the type checking of all the variables inside our code. It will check if performing any operation on a variable is possible given its type.

June 1st, 2020 · 8 min read

End to end testing in React Native with Detox

End-to-end testing is a technique that is widely performed in the web ecosystem with frameworks like Cypress, Puppeteer, or maybe with your own custom implementation.

October 9th, 2019 · 5 min read
© 2018–2020 Ema Suriano
Link to $https://github.com/EmaSurianoLink to $https://twitter.com/EmaSurianoLink to $https://www.linkedin.com/in/emanuel-suriano/Link to $https://dev.to/emasurianoLink to $https://medium.com/@emasurianoLink to $https://www.youtube.com/c/EmaSuriano