Back to Testing guides

E2E Testing Signup and Login Workflows with Playwright

Ayooluwa Isaiah
Updated on March 24, 2024

Synthetic monitoring is a powerful strategy for keeping tabs on mission-critical application workflows such as user registration and authentication, ordering and checkout, etc.

It involves employing browser automation to mimic the behavior of actual users, effectively reproducing steps a visitor might take, such as filling a registration form, clicking on an email verification link, and navigating to a dashboard.

By continuously testing and monitoring these key workflows, you'll be able to detect and resolve production issues on your website before real users notice them.

In the last few years, Playwright has emerged as one of the best tools for cross-platform, end-to-end web application testing and automation.

Designed for speed, reliability, and robustness, Playwright's API is compatible with major rendering engines like Chromium, Firefox, and WebKit, and it supports both headed and headless browser modes.

In this tutorial, you'll learn to write E2E tests for realistic user registration and authentication workflows and implement continuous monitoring so that production failures are detected, reported, and addressed immediately.

Let's get started!


To proceed with this article, you only need basic JavaScript knowledge and a working Node.js environment on your machine. If you're completely new to Playwright, you may find it helpful to review our Playwright testing essentials article before proceeding.

Step 1 — Setting up the demo project

To showcase the automation of end-to-end tests within registration and authentication workflows, I've prepared a sample application equipped with both registration and login functionalities, which we will be testing later on.

Begin by forking the project repo to your GitHub account, then clone it to your local environment using the command below:

git clone<your_username>/playwright-signup-login

After cloning, navigate to the project's directory and proceed to install its dependencies, including Playwright:

cd playwright-signup-login
npm install

With the dependencies installed, start the development server on port 3000 using:

npm start

You'll see output similar to the following, indicating that the server is up and running:

> playwright-signup-demo@1.0.0 start
> nodemon src/server.js

[nodemon] 3.1.0
[nodemon] to restart at any time, enter `rs`
[nodemon] watching path(s): *.*
[nodemon] watching extensions: js,mjs,cjs,json
[nodemon] starting `node src/server.js`
{"level":30,"time":1711128683520,"pid":412818,"hostname":"fedora","msg":"Server listening at http://[::1]:3000"}
{"level":30,"time":1711128683522,"pid":412818,"hostname":"fedora","msg":"Server listening at"}
{"level":30,"time":1711128683522,"pid":412818,"hostname":"fedora","msg":"Fastify is listening on port: http://[::1]:3000"}

When you visit http://localhost:3000 in your browser, you will be greeted with the application homepage:


You can also check out the login and sign up pages by visiting the /login and /signup routes respectively:



This demo project is designed with a simplistic user registration and authentication flow that closely mirrors what you might find in real-world applications. It uses an in-memory store for user management and also simulates sending an email for account verification.

Although the application lacks the extensive security features of a full-fledged application, it will serve the purpose of this demonstration very well.

Our focus is on testing from the user's perspective, ensuring the ability to register, verify email, sign in, and sign out smoothly, thus aiming for consistent user experience in all browsers.

In the next section, you'll create the first test that ensures that unauthorized users cannot access protected routes in your application.

Step 2 — Redirecting unauthenticated users

Let's start our testing journey with a simple test that verifies that all unauthenticated users are redirected to the login page.

The application dashboard is a protected route that has been configured to accessible only by users who have successfully logged in.

This security measure is enforced by verifying the presence and validity of a signedIn cookie. The absence or invalidity of this cookie triggers a redirection to the /login route, as illustrated in the server configuration below:

. . .
fastify.get('/dashboard', (req, reply) => {
  const signedIn = req.cookies.signedIn;
  if (!signedIn) {
    return reply.redirect(303, '/login');

  reply.view('./src/templates/dashboard.pug', {
    title: 'Dashboard',
. . .

To test this behavior, open the tests/auth.spec.js file and adjust its contents with the following code snippet:

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

test('ensure unauthenticated users are redirected to the login page', async ({
}) => {
  await page.goto('/dashboard');

  await expect(page).toHaveURL('/login');

This test attempts to navigate directly to the /dashboard route. It then checks if the application appropriately redirects to the /login page, confirming that the protective mechanism against unauthorized access is in effect.

You may execute the above test with the command below:

npx playwright test

It should pass on all browsers:

Running 3 tests using 3 workers
  3 passed (16.3s)

To open last HTML report run:

  npx playwright show-report

Though straightforward, this test crucially confirms that access to sensitive pages is securely restricted to logged-in users, providing some assurance against unauthorized entry.

Step 3 — Validating user registration errors

Ensuring that users receive appropriate error messages when filling out the sign up form incorrectly is an essential part of user interface testing. It's not just about validating the submitted data on the backend, but also about making sure users are informed of what they did wrong in a clear and helpful manner.

In this section, you'll add a new test that intentionally inputs invalid data on the signup form to trigger and verify error messages:

. . .

test('display sign up errors with invalid details', async ({ page }) => {
  await page.goto('/signup');

  await page.getByLabel('Username').fill('ay');

  await page.getByLabel('Email').fill('name@example');

  await page.getByLabel('Password', { exact: true }).fill('12345');

  await page.getByLabel('Confirm password').fill('1234');

  await page.getByRole('button', { name: 'Sign up!' }).click();

  await page.waitForLoadState();

  await expect(
    page.getByText('Username must be no less than 3 characters')

  await expect(
    page.getByText('Please provide a valid email address')

  await expect(
    page.getByText('Password must not be less than 6 characters')

  await expect(
    page.getByText('The provided passwords do not match')

This script simulates entering incorrect information in the signup form and checks if the application correctly displays the expected error messages, ensuring the validation system is effectively communicating with the user.

Upon running this test:

npx playwright test -g 'display'

You should see it pass, indicating the application correctly handles and displays validation errors:

Running 3 tests using 3 workers
  3 passed (15.7s)

To open last HTML report run:

  npx playwright show-report

To add an extra layer of certainty that errors are not only present but also displayed correctly, we'll incorporate a visual regression test:

test('display sign up errors with invalid details', async ({ page }) => {
 . . .

  await expect(
    page.getByText('The provided passwords do not match')

await expect(page).toHaveScreenshot('signup-errors.png');

Running the test now will fail due to the absence of a baseline screenshot for comparison:

npx playwright test -g 'display'
Running 3 test using 3 workers
  1) [chromium] › auth.spec.js:23:1 › display sign up errors with invalid details ──────────────────

    Error: A snapshot doesn't exist at /home/ayo/dev/betterstack/demo/playwright-signup/tests/auth.spec.js-snapshots/signup-errors-chromium-linux.png, writing actual.

      52 |   ).toBeVisible();
      53 |
    > 54 |   await expect(page).toHaveScreenshot('signup-errors.png');
         |   ^
      55 | });
      56 |

        at /home/ayo/dev/betterstack/demo/playwright-signup/tests/auth.spec.js:54:3

    attachment #1: signup-errors-actual.png (image/png) ────────────────────────────────────────────

  . . .

  3 failed
    [chromium] › auth.spec.js:23:1 › display sign up errors with invalid details ───────────────────
    [firefox] › auth.spec.js:23:1 › display sign up errors with invalid details ───────────────────
    [webkit] › auth.spec.js:23:1 › display sign up errors with invalid details ───────────────────

  Serving HTML report at http://localhost:9323. Press Ctrl+C to quit.

The HTML report should open automatically in your default browser, or you can go to http://localhost:9323 as mentioned in the error message:


When you click on any of the errors and scroll down the page, you will see a Screenshots section showing how the signup page looks at the point of capture:


This failure is expected and leads to the automatic creation of an initial screenshot, stored in tests/auth.spec.js-snapshots, which serves as the baseline for future test runs:

tree tests/auth.spec.js-snapshots
├── signup-errors-chromium-linux.png
├── signup-errors-firefox-linux.png
└── signup-errors-webkit-linux.png

After validating that the screenshots represent the correct error states, subsequent tests will compare against these baselines, ensuring that any deviation gets detected, whether due to a bug or a deliberate change.

You may re-run the test once again to confirm that it succeeds since there were no modifications to the page:

npx playwright test -g 'display'
Running 3 tests using 3 workers
  3 passed (15.7s)

To open last HTML report run:

  npx playwright show-report

Even the tiniest change to the page will cause the screenshots to differ, leading to an error. In such instances, you decide if you want to update the baseline screenshots (if the change is intentional) or fix the bug until the test passes again:

npx playwright test -g 'display' --update-snapshots # if the change is intentional

For more detailed information on working with screenshots and visual comparisons in Playwright, refer to their documentation on Screenshots and Visual comparisons.

Step 4 — Testing the user registration workflow

In this section, you will verify the account registration process, ensuring that a user can sign up successfully and receive a confirmation email. Automating email verification testing is a crucial skill for many projects, as it adds a layer of realism in your testing environment.

To facilitate sending verification emails, our demo application utilizes the Nodemailer package, configured to work with an SMTP server. I'll guide you through setting it up with a Gmail account first.

Start by activating 2-Step Verification for your Gmail at Google's security settings. Then, generate a unique App Password for the demo application, which will be used in your .env file:




To ensure the server process recognizes the updated values, restart it by using Ctrl-C and npm start respectively.

Before you can automate email testing, you must use an email testing service that allows you to create disposable email addresses and access emails sent to them through a web interface or API.

There are a few such services out there, but we'll be using Mail7 in this tutorial.

Let's test out the signup functionality by manually signing up for a Demo app account using a Mail7 email address.

Head over to http://localhost:3000/signup and fill in the form using a random email such as:

Mail7 allows you to instantly create email addresses by prepending a random string to the suffix. This provides you with a public inbox that is accessible through the URL below:


Once you submit the signup form, you will be presented with the following page:


Head over to your Mail7 email public inbox to see the confirmation email in your inbox:


Click it and find the confirmation link, then click on it:


You will subsequently be redirected to a page confirming that your email has been verified successfully:


Let's now translate the above steps into a Playwright test. Return to your auth.spec.js file and write the following test:

import { test, expect } from '@playwright/test';
import crypto from 'node:crypto';
. . . test('ensure successful user registration and email verification', async ({ page, browser, context }) => { const userEmail = `${crypto.randomBytes(20).toString('hex')}`; await page.goto('/signup'); await page.getByLabel('Username').fill('testuser'); await page.getByLabel('Email').fill(userEmail); await page.getByLabel('Password', { exact: true }).fill('123456'); await page.getByLabel('Confirm password').fill('123456'); await page.getByRole('button', { name: 'Sign up!' }).click(); await page.waitForLoadState(); await expect( page.getByRole('heading', { name: 'Verify your email address' }) ).toBeVisible(); await expect(page.getByText(userEmail)).toBeVisible(); const mail7Page = await browser.newPage(); await mail7Page.goto( `${userEmail}`, { waitUntil: 'domcontentloaded', } ); await mail7Page.waitForSelector( '.subject:has-text("Confirm your email address")' ); await'.subject:has-text("Confirm your email address")'); const iframe = await mail7Page.waitForSelector('.message iframe'); const iframeContent = await iframe.contentFrame(); if (!iframeContent) { throw new Error('message iframe is null!'); } const confirmLink = await iframeContent.waitForSelector( 'a:has-text("confirming your account")' ); const pagePromise = mail7Page.context().waitForEvent('page'); await; const newTab = await pagePromise; await newTab.waitForLoadState(); await expect( newTab.getByRole('heading', { name: `Your email, ${userEmail}, is verified successfully!`, }) ).toBeVisible(); });

This test script generates a unique email address using the crypto module and navigates to the application's registration page. It then fills in the required field before submitting the form by clicking the "Sign up!" button.

After submission, it checks for a page change that prompts the user to verify their email address, ensuring that the user's email is visible on this page. The script proceeds by opening a new browser page to access the Mail7 admin console, specifically targeting the inbox of the generated email address.

It waits for an email with the subject "Confirm your email address" to appear, clicks on it, and navigates to the content of the email loaded in an iframe. The script looks for a link within the iframe content including the text "confirming your account" and clicks it.

This action triggers a new page or tab that the script waits for, verifying that the email address has been successfully verified by checking for a heading indicating a successful verification. This comprehensive automation covers user registration, email verification, and the final assertion of a successful account setup.

When you execute the test, it should pass on all browsers, proving that your registration flow is working as intended:

npx playwright test -g 'ensure successful user registration'
Running 3 tests using 3 workers
  3 passed (16.3s)

To open last HTML report run:

  npx playwright show-report

With this successful result, you can be confident that your application's signup and email confirmation processes are working as intended.

In the next section, we'll focus on verifying login functionality to complete the user authentication journey.

Step 5 — Verifying login and logout functionality

Building on the previous test, this section focuses on incorporating login and logout operations into your testing suite. The aim to ensure that once a user has registered and confirmed their email, they can log in and log out of the application seamlessly.

Update the existing "user registration" test by including the highlighted sections and change its name as shown below:

test('ensure successful user registration and session management', async ({ page, browser }) => {
. . .
await expect(
newTab.getByRole('heading', {
name: `Your email, ${userEmail}, is verified successfully!`,
// Login
await newTab.getByRole('link', { name: 'You may now login' }).click();
await newTab.getByLabel('Email').fill(userEmail);
await newTab.getByLabel('Password', { exact: true }).fill(password);
await newTab.getByRole('button', { name: 'Log in' }).click();
await newTab.waitForLoadState();
await expect(
newTab.getByRole('heading', {
name: 'Welcome to your dashboard!',
// Logout
await newTab.getByRole('link', { name: 'Logout' }).click();
await newTab.waitForLoadState();
await expect(newTab.url()).toContain('/login');

The modifications above transition the test to the login phase, where login page is accessed via the provided link, and the user's credentials are inputted. Once the login button is clicked, it checks that the dashboard is successfully accessed post-login.

Finally, the "Logout" link is clicked, and it verifies that the browser is redirected to the login page, completing the end-to-end user registration journey test.

You can verify the test succeeds by executing the following command:

npx playwright test -g 'ensure successful'
Running 3 tests using 3 workers
  3 passed (16.3s)

To open last HTML report run:

  npx playwright show-report

Now that we have reasonable coverage of the application's registration and authentication flow, let's ensure that regressions are not introduced by future development efforts by integrating Playwright tests into a CI/CD pipeline.

Step 6 — Running Playwright tests with GitHub Actions

To prevent bugs and regressions from making it into your production environment, it's necessary to integrate E2E testing in your CI/CD pipeline so that issues are caught and fixed before the code is merged and deployed to production.

In this section, you will run your Playwright tests through GitHub actions when changes are made against the main branch. I've already created the GitHub workflow for you in the .github/workflows/playwright.yml file:

name: Run Playwright tests

      - main

    timeout-minutes: 60
    runs-on: ubuntu-latest

      - uses: actions/checkout@v4

      - name: Set up Node.js
        uses: actions/setup-node@v4
          node-version: 20

      - name: Install application dependencies
        run: npm install

      - name: Install Playwright Browsers
        run: npx playwright install --with-deps

      - name: Run end-to-end tests
        run: npx playwright test
          GMAIL_USER: ${{ secrets.GMAIL_USER }}
          GMAIL_PASSWORD: ${{ secrets.GMAIL_PASSWORD }}

      - name: Upload test report
        uses: actions/upload-artifact@v4
        if: always()
          name: playwright-report
          path: playwright-report/
          retention-days: 7

This action script defines a playwright job that is executed when a push or pull request is made to the main branch. The workflow sets up a Node.js environment, installs all the application dependencies, and runs the tests. It also creates an HTML report with a seven day retention.

There's no need to start the application server before running the tests because Playwright is already configured to do so in its configuration file:

// @ts-check
import { defineConfig, devices } from '@playwright/test';

. . .

export default defineConfig({
  . . .

  /* Run your local dev server before starting the tests */
  webServer: {
command: 'npm run playwright-server',
url: '', reuseExistingServer: !process.env.CI, }, });

Before you can send the confirmation emails in GitHub actions, you need to add the GMAIL_USER and GMAIL_PASSWORD environmental variables to your repository secrets.

Head over to your Settings tab, and find the Actions option under Secrets and variables. Create the GMAIL_USER and GMAIL_PASSWORD entries respectively with the appropriate values and save them.


Once done, return to your terminal and commit all the changes you've made to the project so far, including the Playwright test script and the generated reference screenshots.

git add -A
git commit -m 'Add Playwright tests'

Then push the changes to your repository:

git push origin main

Head back to your repo and check the Actions tab. You should observe the "Run Playwright test" workflow in progress. After a few moments, it should complete successfully:


You can click the entry to view the artifacts created during the test run:


A report of the test is always present and made downloadable as a zip file which is helpful for debugging test failures when they occur.

You can download this file by clicking on it, then extract it to a location that preferably already has Playwright installed:

unzip -d playwright-report

Once downloaded and extracted, run the command below to serve the report on a local server and open it in your default browser.

npx playwright show-report

Step 7 — Playwright test monitoring with Better Stack

End-to-end test automation continues beyond the CI/CD level if you're serious about detecting production issues for your critical application workflows. You also need to continuously test and monitor these workflows through a platform that can promptly alert you to failures.

With Better Stack, you can schedule your Playwright checks to run on a schedule against your production systems to keep tabs on your business-critical workflows.

To demonstrate this, let's continuously monitor the user registration, email verification, and session management workflows of our demo app in Better Stack.

Begin by making your local server accessible online using Ngrok, which creates a secure tunnel to your localhost. After installation, initiate Ngrok to expose port 3000:

ngrok http 3000
Session Status                online
Account                       Ayo (Plan: Free)
Version                       3.8.0
Region                        United States (California) (us-cal-1)
Latency                       242ms
Web Interface       
Forwarding -> http://localhost:3000
Connections ttl opn rt1 rt5 p50 p90 0 0 0.00 0.00 0.00 0.00

Take note of the generated Forwarding URL, as it will be your application's temporary public address.

Go ahead and create a free Better Stack account or log in to your existing account. Navigate to the Monitors section within your dashboard and opt to create a new monitor, selecting the Playwright scenario fails as your monitor type.

Name your scenario and paste the "successful user registration" test function into the provided field, ensuring to adjust the script's navigation URL to your public Ngrok address for external accessibility:

await page.goto('/signup'); // change this to:

await page.goto('https://<your_ngrok_url>/signup');


After configuring your monitor, use the Test Scenario feature to verify that it works. A successful execution indicates proper setup, and you can then finalize your monitor's settings, including the On-call escalation policies.


Once you've saved your changes, your monitor will be shown as "Up":


Better Stack defaults to running these checks every three minutes, though adjustments are available in the advanced settings section.

By default, Better Stack runs the test every three minutes, but you can configure this in the Advanced settings section depending on your tolerance for how long you are comfortable not knowing about a production issue.

To illustrate failure handling, intentionally introduce a breaking change in your application's code, such as altering the redirection behavior post-logout to navigate users to the home page instead of the login page.

. . .
fastify.get('/logout', (req, reply) => {
    .clearCookie('signedIn', {
      domain: appURL.hostname,
      path: '/',
.redirect(303, '/');

This modification should trigger a test failure, reflected in Better Stack's dashboard as a "Down" status and generating a notification based on your configured escalation policy.



You can click on the Ongoing incident button to view the details of the failure. In the Logs tab, you will see the entire output from Playwright test runner which can help you figure out why the test failed.


You can also switch to the Artifacts section, where you're able to download the Playwright report and other details to your computer.


These insights facilitate quick identification and correction of issues, with the monitor returning to an "Up" status following resolution (in this case by reverting your change), confirmed by a subsequent notification.


By leveraging Better Stack for real-time monitoring in this manner, you'll establish a vigilant watch over your application's critical workflows, ensuring quick issue discovery and resolution before your customers notice them.

Final thoughts

Throughout this article, I've guided you through the process of crafting tests for user registration and authentication workflows, integrating them into your CI/CD pipeline, and implementing continuous monitoring through Better Stack to detect production issues.

While we've explored a selection of test cases, there's ample scope to expand upon this foundation with additional scenarios tailored to your needs. It's also equally important to include tests for often-overlooked functionalities such as password recovery, password resetting, account deletion, and more.

Thanks for reading, and happy testing!

Author's avatar
Article by
Ayooluwa Isaiah
Ayo is the Head of Content at Better Stack. His passion is simplifying and communicating complex technical ideas effectively. His work was featured on several esteemed publications including, Digital Ocean, and CSS-Tricks. When he’s not writing or coding, he loves to travel, bike, and play tennis.
Got an article suggestion? Let us know
Next article
Automating Playwright Checks for User Registration and Login
This guide explore E2E automation testing's role in reliable user registration and login workflows, and setting it up with Playwright and Better Stack
Licensed under CC-BY-NC-SA

This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.

Make your mark

Join the writer's program

Are you a developer and love writing and sharing your knowledge with the world? Join our guest writing program and get paid for writing amazing technical guides. We'll get them to the right readers that will appreciate them.

Write for us
Writer of the month
Marin Bezhanov
Marin is a software engineer and architect with a broad range of experience working...
Build on top of Better Stack

Write a script, app or project on top of Better Stack and share it with the world. Make a public repository and share it with us at our email.

or submit a pull request and help us build better products for everyone.

See the full list of amazing projects on github