E2E Testing Signup and Login Workflows with Playwright
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!
Prerequisites
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 https://github.com/<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 http://127.0.0.1:3000"}
{"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 ({
page,
}) => {
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')
).toBeVisible();
await expect(
page.getByText('Please provide a valid email address')
).toBeVisible();
await expect(
page.getByText('Password must not be less than 6 characters')
).toBeVisible();
await expect(
page.getByText('The provided passwords do not match')
).toBeVisible();
});
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')
).toBeVisible();
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) ────────────────────────────────────────────
test-results/auth-display-sign-up-errors-with-invalid-details-chromium/signup-errors-actual.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
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:
GMAIL_USER=<your_gmail_address>
GMAIL_PASSWORD=<your_app_password>
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
mail7.io
email such as:
demoapptestuser@mail7.io
Mail7 allows you to instantly create email addresses by prepending a random
string to the @mail7.io
suffix. This provides you with a public inbox that is
accessible through the URL below:
https://console.mail7.io/admin/inbox/inbox?username=demoapptestuser@mail7.io
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')}@mail7.io`;
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(
`https://console.mail7.io/admin/inbox/inbox?username=${userEmail}`,
{
waitUntil: 'domcontentloaded',
}
);
await mail7Page.waitForSelector(
'.subject:has-text("Confirm your email address")'
);
await mail7Page.click('.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 confirmLink.click();
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!`,
})
).toBeVisible();
// 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!',
})
).toBeVisible();
// 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
on:
push:
pull_request:
branches:
- main
jobs:
playwright:
timeout-minutes: 60
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
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
env:
GMAIL_USER: ${{ secrets.GMAIL_USER }}
GMAIL_PASSWORD: ${{ secrets.GMAIL_PASSWORD }}
- name: Upload test report
uses: actions/upload-artifact@v4
if: always()
with:
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:
[playwright.config.js]
// @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: 'http://127.0.0.1:3001',
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 playwright-report.zip -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 http://127.0.0.1:4040
Forwarding https://9d5a-146-70-173-76.ngrok-free.app -> 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) => {
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!
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 usBuild 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.
community@betterstack.comor submit a pull request and help us build better products for everyone.
See the full list of amazing projects on github