Issues

Not Your Usual Compliance Monitoring Tool

Monitoring websites to meet security policies such as PCI compliance or GDPR compliance can feel like a daunting task. These standards aren’t just checkboxes, they’re critical safeguards for protecting sensitive customer data and ensuring trust in online transactions.

Often, tools like Site 24/7 or similar monitoring platforms are used to verify payment endpoints and ensure URLs remain accessible and consistent. But what happens when the URL you need to monitor isn’t straightforward? Imagine a payment API hidden behind multiple layers: a login wall, a validated form submission, and several redirects before you even reach the endpoint.

This was the exact challenge I faced recently. I needed to monitor a payment API URL that lived behind a sequence of steps. On top of that, the monitoring had to run on a schedule or be automated in some format, reliably simulating the same user journey every time. It forced me to rethink traditional monitoring approaches and explore potential alternatives that could handle authentication, form validation, and scheduled execution - all while staying compliant.

What did I rule out?

A tool we have used for monitoring uptime in the past is Site 24x7. Site 24x7 is a monitoring solution that provides the ability to track performance, uptime and availability across many locations worldwide. We have used Site 24x7 in the past for similar payment-URL monitoring, but this has been on sites where there is no login required or a validation form. From the initial investigation into this, it felt like Site 24x7 didn’t really fit the brief.

The next thought was API testing. The idea was to hit a series of endpoints - such as logging in, then completing the form… This worked until reaching the payment gateway. Validating that endpoint would have required building a new endpoint just to return the gateway URL, which felt unnecessary and impractical.

The decision.

Finally, I landed on Playwright. Playwright is an open-source framework for cross-browser end-to-end automated testing. I enjoy working with Playwright; it's quick to set up, flexible across multiple languages, and I’ve had great success with it in the past. I’ve used it to automate content setup in Umbraco CMS, test filtering functionality, and run smoke tests on websites. In all these cases, it’s proven reliable and effective. A few of these tasks had been done within the build pipeline, which could meet the automation/scheduling requirement of this task. 

With Playwright, we can simulate the user’s journey, like so;

  1. Navigate the website to find the login page
  2. Complete the login form
  3. Navigate to the product page and add an item to the basket
  4. Begin the checkout process

This also adds an extra level of testing as if any of those steps fail too, the test would fail, raising other possible alarms that something may have gone wrong.

Another great advantage of Playwright is that all of the testing can live in a separate repository, completely independent of the site’s codebase, as the tests run directly in the browser, there’s no need to touch the existing pipeline. This means the code can be executed, rebuilt, and iterated on as often as needed - without adding load or impacting the performance of the full site build. Playwright it is!

How did I do it?

Create the test.

The test itself was fairly straightforward - only one was needed. The first step was accepting cookies and logging in. To handle this, I used test.beforeEach, which ensures these steps run before every test. This is a good way to avoid duplicating setup code and it makes it easier to add more tests later without breaking existing ones.

test.describe('navigation', () => {
  test.beforeEach(async ({ page }) => {
    const loginHelper = new Login(page);
    const acceptCookies = new AcceptCookies(page);


    await loginHelper.goto();
    await acceptCookies.AcceptCookies(true);
  });

Even though this repo only contains a single test, I like to set things up this way from the start. It future‑proofs the project and keeps the structure consistent as it grows.

I also made use of test helpers. In a similar way to beforeEach, test helpers are useful for writing reusable, easy-to-maintain code. Again, this may seem OTT for this project - but I was thinking about my future self, or anyone else who may pick this up in the future.

import { type Locator, type Page } from '@playwright/test';


export class Login {
  readonly page: Page;
  readonly loginButton: Locator;
  readonly email: Locator;
  readonly password: Locator;


  constructor(page: Page) {
    this.page = page;
    this.loginButton = page.locator('#loginBtn');
    this.email = page.getByLabel('Email');
    this.password = page.getByLabel('Password');
  }


  async goto() {
    await this.page.goto(`${process.env.BASE_URL}/login`);
  }


  async AddLoginDetails() {
    await this.email.click();
    await this.email.fill(`${process.env.USER_NAME}`);
    await this.password.click();
    await this.password.fill(`${process.env.PASSWORD}`);
  }


  async Login() {
    await this.AddLoginDetails();
    await this.loginButton.click();
  }
}

And then this test helper is used like so.

  test('payment monitoring: Check the form exists', async ({ page }) => {
    const loginHelper = new Login(page);
    const selectProduct = new SelectProduct(page);
    const addressCheck = new EnterAddress(page);


    await loginHelper.AddLoginDetails();
    await loginHelper.Login();

Notice in the first example of the beforeEach hook, it calls the goto() function from the login test helper. Test helpers are valuable because they let you break your tests into smaller, focused functions. This makes it easy to reuse pieces of logic in different ways across your test suite.

Now we are logged in, it's a case of stepping through the website, going to the product page, adding a product to the basket, and then locating the payment URL.

test('payment monitoring: Check the form exists', async ({ page }) => {
    const loginHelper = new Login(page);
    const selectProduct = new SelectProduct(page);
    const addressCheck = new EnterAddress(page);


    await loginHelper.AddLoginDetails();
    await loginHelper.Login();
    await selectProduct.SelectMenuItem();
    await addressCheck.AddToCartWithAddress();


    const basketButton = page.getByText('1 Item');
    await expect(basketButton).toBeVisible();


    const checkForm = new CheckFormURL(page);
   
    await checkForm.CheckFormMatchesURL();
  });


  test.afterEach(async ({ context }) => {
    await context.clearCookies();
  });

Let’s take a look at the code for the form check:

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


export class CheckFormURL {
  readonly page: Page;


  constructor(page: Page) {
    this.page = page;
  }


  async CheckFormMatchesURL(timeout = 15000) {
    const expected = process.env.FORM_URL;
    if (!expected) {
      throw new Error('Environment variable is not set');
    }


    await this.page.waitForURL((url) => 	url.toString().startsWith(expected), { timeout });


    const current = this.page.url();
    console.log(`Navigated to payment page: ${current}`);


    expect(current.startsWith(expected)).toBeTruthy();
  }
}

A third‑party provider powers this form, which we simply had to plug into our site. Not ideal, but sometimes you have to work with what you have. As you can see from this code, I am using a timeout. Playwright Test enforces a timeout for each test, 30 seconds by default.

Normally, that default is more than enough, and in fact, increasing timeouts isn’t something you want to lean on too often - it can hide performance problems and slow down your test suite. But in this case, the third‑party was rather slow, and since I had no control over its response times, I had to extend the timeout. It’s a trade‑off: better to have a slightly slower but reliable test than one that fails intermittently because of external delays.

Note the use of environment variables. This was a conscious decision to keep the codebase secure while giving us the flexibility to update values in the .env file without exposing them directly in the code. It also means we can swap these variables out easily when testing locally or against different environments, which keeps our setup both safe and adaptable. And then finally, a console.log is used to output the payment url found. This adds a further layer of checking, as this will be output at the end of the run, which can be viewed and checked.

The build pipeline

To configure the automated aspect of the requirement, I ensured that the test could be executed on a schedule, as often as needed. To achieve this, I set up the codebase to run through an Azure build pipeline, which provides both flexibility and confidence that the tests will be executed consistently. Additionally, I configured the pipeline to send email notifications to the necessary people whenever a build completes.

Here’s some of the YAML:

schedules: - cron: 0 10 * * *
branches: 
    include: 
        - refs/heads/main always: true

This cron trigger runs the pipeline every day at 10:00 UTC on the main branch. This makes it easy to increase or decrease the test frequency if required.

Next, the required dependencies, such as Node.js and Playwright are installed.

- task: qetza.replacetokens.replacetokens-task.replacetokens@6
  displayName: Replace tokens
  inputs:
    targetFiles: ".env.production"
    tokenPattern: "custom"
    tokenPrefix: "#{"
    tokenSuffix: "}#"
    actionOnNoFiles: "warn"

This YAML here injects secrets into .env.production by replacing placeholders like #{PASSWORD}# with pipeline variables.

- task: Npm@1 
displayName: Test Execution 
inputs: 
    command: custom verbose: false 
    customCommand: 
    run-script env:production

Executes the npm script env:production, which runs the Playwright test suite against production configuration.

To summarise, this pipeline automates the Playwright test execution daily, keeps secrets secure via environment variables, and ensures dependencies are installed consistently. And after all of that, our team receive automated emails everytime it runs - letting us know if the build was successful or not

Conclusion

Playwright is often thought of as a solution just for cross-browser testing, ideal for validating functionality and ensuring user journeys work as expected. And whilst this is true, there are many other great possibilities. Playwright can solve all kinds of edge cases and automate tasks that don’t fit neatly into traditional testing workflows. 

With the introduction of the Playwright MCP Server, its capabilities are expanding even further - enabling it to write, build, and support development tasks in many new ways. In this project, Playwright became a reliable and flexible tool for automated payment URL monitoring, offering more confidence and the ability to make updates whenever necessary. It’s a reminder that Playwright doesn’t have to be limited to end‑to‑end testing; it can be a powerful automation tool wherever you need one.

comments powered by Disqus