DEV Community

Cover image for How to use Playwright with GitHub Actions and GitHub Pages
Yusuf Aran
Yusuf Aran

Posted on • Originally published at ysfaran.github.io

How to use Playwright with GitHub Actions and GitHub Pages

Introduction

Playwright is a modern cross-browser testing framework developed by microsoft itself.
GitHub Actions is the out-of-the-box solution for anything related to CI/CD pipelines on GitHub.
Last but not least GitHub Pages is a GitHub feature which allows to deploy static websites.

And all of this comes completely free for public repositories!

In this blog post I will show you how to setup a basic a Playwright project, integrate it into GitHub Actions and finally deploy an HTML report of the test results to GitHub Pages.

ℹ️ INFO
The complete source code can be found in this repo: ysfaran/playwright-gh-actions-gh-pages

This post is not focusing on explaining all concepts of Playwright, but rather how to connect Playwright, GitHub Actions and GitHub Pages in a easy, non-cost way.

Setup Playwright

💡TIP
For an up-to-date installation guide please always refer to https://playwright.dev/docs/intro. At the point of writing this posts Playwright's latest version was 1.25.1.

Fortunately Playwright makes it really easy to setup a new project using the CLI:

yarn create playwright
Enter fullscreen mode Exit fullscreen mode

This will start an interactive session. Make sure to at least enable GitHub Action workflow generation. Following is the configuration I used:

✔ Do you want to use TypeScript or JavaScript? · TypeScript
✔ Where to put your end-to-end tests? · tests
✔ Add a GitHub Actions workflow? (y/N) · true
Enter fullscreen mode Exit fullscreen mode

Generate HTML Report

One of the most important generated files is playwright.config.ts, so let's have a look at it:

ℹ️ INFO
I simplified some of the generated files. For a full list of these changes see aab0b9c2.

const config: PlaywrightTestConfig = {
  testDir: "./tests",
  // Run all tests within a file in parallel to speed up test execution
  fullyParallel: true,
  // Helpful for uncontrollable flaky tests, which are tests, occasionally failing for various reasons
  retries: 3,
  // Generates a HTML report to ./playwright-report/
  reporter: "html",
  use: {
    // Tests will be run against this page
    baseURL: "https://playwright.dev/",
    // Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer
    trace: "on-first-retry",
  },
  // Cross-browser testing setup
  projects: [
    {
      name: "chromium",
      use: {
        ...devices["Desktop Chrome"],
      },
    },
    {
      name: "firefox",
      use: {
        ...devices["Desktop Firefox"],
      },
    },

    {
      name: "webkit",
      use: {
        ...devices["Desktop Safari"],
      },
    },
  ],
};
Enter fullscreen mode Exit fullscreen mode

Since the html reporter is already enabled by default, there are no other adaption necessary.
To get a nice HTML report covering all test result cases (sucess, fail and flaky) following tests have been implemented:

import { test, expect } from "@playwright/test";

test.beforeEach(({ page }) => page.goto("https://playwright.dev/"));

test("should succeed", async ({ page }) => {
  await expect(page).toHaveTitle(/Playwright/);
});

test("should fail", async ({ page }) => {
  await expect(page).not.toHaveTitle(/Playwright/);
});

test("should be flaky", async ({ page }) => {
  if (Math.random() > 0.5) {
    await expect(page).toHaveTitle(/Playwright/);
  } else {
    await expect(page).not.toHaveTitle(/Playwright/);
  }
});
Enter fullscreen mode Exit fullscreen mode

To run the tests execute:

npx playwright test
Enter fullscreen mode Exit fullscreen mode

After running all tests Playwright prints a summary of test restuls and publishes the HTML report to a server at a localhost address. Playwright also emits all sources for the HTML report to playwright-report/.

Integrate GitHub Actions

Luckily Playwright already generated a basic .github/workflows/playwright.yml during Setup Playwright to integrate Playwright tests into a GitHub Actions worklow:

name: Playwright Tests
on:
  push:
    branches: [main, master]
jobs:
  test:
    timeout-minutes: 60
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - uses: actions/setup-node@v2
        with:
          node-version: "14.x"
      - name: Install dependencies
        run: yarn
      - name: Install Playwright Browsers
        run: npx playwright install --with-deps
      - name: Run Playwright tests
        run: yarn playwright test
      - uses: actions/upload-artifact@v2
        if: always()
        with:
          name: playwright-report
          path: playwright-report/
          retention-days: 30
Enter fullscreen mode Exit fullscreen mode

Essentially the workflow is triggered on every push and executes the all tests, just like you would do it locally.
It's important to note that it also uploads the Playwright HTML report as workflow artifact.
This artifact will be available to download in the Actions tab of your GitHub repository in each workflow run.

Please refer to the official Playwright docs on how to download and view HTML report files of a specific workflow run.

Publish HTML Report to GitHub Pages

Being able to download, view and debug HTML reports generate by a CI/CD pipeline locally is already convenient, but there is an easier and faster way to check test results: automatically publish the HTML reports to GitHub Pages and view them directly in the browser.

Firstly a new orphan branch, which means the branch has no parent commit, for the GitHub Pages' static content needs to be created:

# Create a new branch without any commit on it
git checkout --orphan gh-pages

# Source files get autoamtically staged so remove them from git
git rm -rf .

# Optional: Check if there are really no files in the git staging area anymore
git status

# Create an initial, empty commit
git commit --allow-empty -m "setup empty branch for GitHub Pages"

# Push the branch to make it available online
git push --set-upstream origin gh-pages
Enter fullscreen mode Exit fullscreen mode

Secondly the GitHub Pages feature needs to be enabled for your repo. Usually GitHub is enabling GitHub Pages automatically when you name a branch gh-pages.
If it doesn't, make sure to enable GitHub Pages manually in your repository settings:

Enable GitHub Pages

Any change that is pushed to gh-pages will now automatically update the GitHub Pages website, which is publicly available at https://<user>.github.io/<repo>/.
Because there is no real commit on the gh-pages branch yet a 404 error page will be shown.

So the next logical step is to add a new job to the GitHub Actions workflow in order to push the HTML reports to gh-pages branch:

publish_report:
  name: Publish HTML Report
  # using always() is not ideal here, because it would also run if the workflow was cancelled
  if: "success() || needs.test.result == 'failure'"
  needs: [test]
  runs-on: ubuntu-latest
  continue-on-error: true
  env:
    # Unique URL path for each workflow run attempt
    HTML_REPORT_URL_PATH: reports/${{ github.ref_name }}/${{ github.run_id }}/${{ github.run_attempt }}
  steps:
    - name: Checkout GitHub Pages Branch
      uses: actions/checkout@v2
      with:
        ref: gh-pages
    - name: Set Git User
      # see: https://github.com/actions/checkout/issues/13#issuecomment-724415212
      run: |
        git config --global user.name "github-actions[bot]"
        git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
    - name: Download zipped HTML report
      uses: actions/download-artifact@v2
      with:
        name: playwright-report
        path: ${{ env.HTML_REPORT_URL_PATH }}
    - name: Push HTML Report
      timeout-minutes: 3
      # commit report, then try push-rebase-loop until it's able to merge the HTML report to the gh-pages branch
      # this is necessary when this job is running at least twice at the same time (e.g. through two pushes at the same time)
      run: |
        git add .
        git commit -m "workflow: add HTML report for run-id ${{ github.run_id }} (attempt:  ${{ github.run_attempt }})"

        while true; do
          git pull --rebase
          if [ $? -ne 0 ]; then
            echo "Failed to rebase. Please review manually."
            exit 1
          fi

          git push
          if [ $? -eq 0 ]; then
            echo "Successfully pushed HTML report to repo."
            exit 0
          fi
        done
    - name: Output Report URL as Worfklow Annotation
      run: |
        FULL_HTML_REPORT_URL=https://ysfaran.github.io/playwright-gh-actions-gh-pages/$HTML_REPORT_URL_PATH

        echo "::notice title=📋 Published Playwright Test Report::$FULL_HTML_REPORT_URL"
Enter fullscreen mode Exit fullscreen mode

Basically this job commits the HTML report files to the gh-pages branch, which will then automatically redploy the website.

It does so by specifying a new unique path for each test result:

HTML_REPORT_URL_PATH: reports/${{ github.ref_name }}/${{ github.run_id }}/${{ github.run_attempt }}
Enter fullscreen mode Exit fullscreen mode

The job then tries to push these changes and rebases if necessary until a push was succssful.
Rebasing is safe here, because - as already mentioned - each workflow run gets its own uniquie path, so there can't be any file conflicts ever.

Last but not least the publicly available URL is printed as GitHub Worfklow Annotation:

💡 TIP
It might take some while for GitHub Pages to update its content after a new push, so the printed URL will point to a 404 page initially.
In the Actions tab of your repo you can check pages-build-deployment workflow runs, which is automatically triggered for GitHub Pages, to see if the GitHub Pages deployment was successful.

URL as GitHub Worfklow Annotation

Add Multi-Branch Support

Only in rare cases you work on a single branch. Also the pipeline is currently not integrated in any pull request process, meaning that there are no restriction to push or even force push to your main/master.
Its time to change that!

First of all we need to enable branch protections rules for the default branch:

  1. Go to the settings of your repo
  2. Select Branches under Code and automation in the menu
  3. Hit the Add branch protection rule button
  4. Add your default branchs name for Branch name pattern
  5. Configure your branch protecting rules as desired, most importantly being:

Branch protections rules

Note that I only put Test under Status checks that are required. because you should still be allowed to merge to main branch if some HTML report couldn't be published.
A green Test job for a branch represents a valid state anyway in this case.

Secondly the workflow needs to be adapted:

on:
  push:
-    branches: [ main, master ]
-  pull_request:
-    branches: [ main, master ]
+    branches-ignore: [ main, gh-pages ]
Enter fullscreen mode Exit fullscreen mode

So the workflow is triggered after each push on every branch except main and gh-pages.
I purposely removed pull_request here, because it's really hard (at this point in time) to make push and pull_requests work together with GitHub Actions.
Feel free to check my answer on StackOverflow answer regarding this topic.

⚠️ CAUTION
If you want to work with pull_request every occurence of ${{ github.ref_name }} should be replaces with ${{ github.head_ref || github.ref_name }}.
Check the related StackOverflow answer for details: https://stackoverflow.com/a/71158878/6489012.

I also highly recommend to enable concurrency groups in your workflow file to make sure only one workflow is running at the same time for each branch:

concurrency:
  group: ${{ github.ref_name }}
  # optional
  cancel-in-progress: true
Enter fullscreen mode Exit fullscreen mode

Delete Reports from GitHub Pages

After a branch was successfully merged it makes sense to delete all corresponding HTML reports from GitHub Pages. This requires the implementation of a new workflow file:

name: Delete

on:
  delete:
    branches-ignore: [main, gh-pages]

# ensures that currently running Playwright workflow of deleted branch gets cancelled
concurrency:
  group: ${{ github.event.ref }}
  cancel-in-progress: true

jobs:
  delete_reports:
    name: Delete Reports
    runs-on: ubuntu-latest
    env:
      # Contains all reports for deleted branch
      BRANCH_REPORTS_DIR: reports/${{ github.event.ref }}
    steps:
      - name: Checkout GitHub Pages Branch
        uses: actions/checkout@v2
        with:
          ref: gh-pages
      - name: Set Git User
        # see: https://github.com/actions/checkout/issues/13#issuecomment-724415212
        run: |
          git config --global user.name "github-actions[bot]"
          git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
      - name: Check for workflow reports
        run: |
          if [ -z "$(ls -A $BRANCH_REPORTS_DIR)" ]; then
            echo "BRANCH_REPORTS_EXIST="false"" >> $GITHUB_ENV
          else
            echo "BRANCH_REPORTS_EXIST="true"" >> $GITHUB_ENV
          fi
      - name: Delete reports from repo for branch
        if: ${{ env.BRANCH_REPORTS_EXIST == 'true' }}
        timeout-minutes: 3
        run: |
          cd $BRANCH_REPORTS_DIR/..

          rm -rf ${{ github.event.ref }}
          git add .
          git commit -m "workflow: remove all reports for branch ${{ github.event.ref }}"

          while true; do
            git pull --rebase
            if [ $? -ne 0 ]; then
              echo "Failed to rebase. Please review manually."
              exit 1
            fi

            git push
            if [ $? -eq 0 ]; then
              echo "Successfully pushed HTML reports to repo."
              exit 0
            fi
          done
Enter fullscreen mode Exit fullscreen mode

The job of this workflow is pretty similiar to the one publishing the HTML report. This time it's just the other way around: gh-pages branch is checked out and the folder reports/<branch-name> is deleted.
Then the same push-rebase loop is initated to trigger a GitHub Pages deployment.

Important to note here is the following part:

ℹ️ INFO
This time the name of the (deleted) branch is neither retreived by github.head_ref and github.ref_name, but github.event.ref.
In case the workflow was triggered by a delete the value of github.ref_name will be the one of the default branch, here main.

concurrency:
  group: ${{ github.event.ref }}
  cancel-in-progress: true
Enter fullscreen mode Exit fullscreen mode

This statement makes sure that any running workflow within the same concurrency group will be cancelled.
In our case this means if tests for a deleted branch are still running, they will be cancelled.

Conclusion

We created a Playwright project from scratch, integrated tests in GitHub Actions, deployed test results to GitHub Pages and finally established branch protections rules with a simple but important pull request strategy.
All of this, without any costs.

All in all it's all about automation and process optimization. Keeping a team busy with daunting manual tasks can decrease effincy and motivation quite a bit.
This can go from manual deployment plans to smaller aspects like hard to debug test results. In the long run saving some minutes of daily, manual work can have a huge impact.

Questions and feedback are always welcome. 🙂

Top comments (2)

Collapse
 
adammescher profile image
Adam Mescher

This is exactly what I wanted and needed!

Thanks for sharing the specifics of exactly how to execute this idea well.

Collapse
 
ysfaran profile image
Yusuf Aran • Edited

You are welcome, happy to hear that it helped someone :)