supaguardsupaguardDocs
Guides

Writing Playwright Tests for supaguard

Learn to write manual Playwright scripts for supaguard. Covers clicking, typing, waiting, and best practices for resilient selectors and focused test design.

While supaguard's AI can generate tests automatically, sometimes you need the precision of a hand-crafted script. supaguard uses standard Playwright syntax—so everything you know about Playwright works here.

The Basics

Every check runs in an isolated browser environment. You have access to the standard Playwright page and request objects.

A Simple Example

Here's a script that verifies your pricing page loads and displays the correct price:

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

test("check pricing page", async ({ page }) => {
  // 1. Visit the page
  await page.goto("https://your-saas.com/pricing");

  // 2. Click the monthly toggle
  await page.getByRole("button", { name: "Monthly" }).click();

  // 3. Verify the price is correct
  await expect(page.getByTestId("starter-price")).toContainText("$49");
});

Common Actions

Clicking

// Preferred: semantic locators
await page.getByRole("button", { name: "Submit" }).click();
await page.getByRole("link", { name: "Dashboard" }).click();

// Alternative: test IDs (stable across UI changes)
await page.getByTestId("submit-btn").click();

// Text-based (for unique visible text)
await page.getByText("Get Started").click();

Typing and Form Filling

// Fill replaces existing content
await page.getByLabel("Email").fill("user@example.com");
await page.getByLabel("Password").fill("secure-password");

// Type simulates key-by-key input (for autocomplete/search)
await page.getByPlaceholder("Search...").type("synthetic monitoring");

Selecting from Dropdowns

// Native <select> element
await page.getByLabel("Country").selectOption("US");

// Custom dropdown (click to open, then select option)
await page.getByRole("combobox", { name: "Region" }).click();
await page.getByRole("option", { name: "Europe" }).click();

File Uploads

// Upload a file
await page.getByLabel("Upload").setInputFiles("/path/to/file.pdf");

Keyboard Shortcuts

// Press Enter to submit
await page.keyboard.press("Enter");

// Use keyboard shortcuts
await page.keyboard.press("Control+A");
// Navigate and wait for load
await page.goto("https://your-saas.com/dashboard");

// Wait for a specific URL after a redirect
await page.waitForURL("**/dashboard/**");

// Go back/forward
await page.goBack();

Waiting

Playwright auto-waits for elements to be actionable before interacting with them. You rarely need manual waits, but for special cases:

// Wait for a specific API response
await page.waitForResponse(
  (resp) => resp.url().includes("/api/auth") && resp.status() === 200
);

// Wait for an element to appear
await page.getByText("Dashboard loaded").waitFor({ state: "visible" });

// Wait for network to settle (useful for SPAs)
await page.waitForLoadState("networkidle");

[!CAUTION] Avoid page.waitForTimeout(ms). Hard-coded delays make tests slow and fragile. Use Playwright's built-in auto-waiting or explicit event waits instead.

Assertions

Visibility and Content

// Element is visible
await expect(page.getByRole("heading", { name: "Welcome" })).toBeVisible();

// Text content matches
await expect(page.getByTestId("user-name")).toHaveText("John Doe");
await expect(page.getByTestId("balance")).toContainText("$");

// Element count
await expect(page.getByRole("listitem")).toHaveCount(5);

Page-Level Assertions

// URL matches
await expect(page).toHaveURL(/.*dashboard/);

// Title matches
await expect(page).toHaveTitle(/Dashboard — Your App/);

Negative Assertions

// Element should NOT be visible
await expect(page.getByText("Error")).not.toBeVisible();

// Element should be disabled
await expect(page.getByRole("button", { name: "Submit" })).toBeDisabled();

Using Environment Variables

Never hardcode credentials in your scripts. Use environment variables set in your Organization Settings:

test("authenticated flow", async ({ page }) => {
  await page.goto("https://app.example.com/login");
  await page.getByLabel("Email").fill(process.env.TEST_USER_EMAIL!);
  await page.getByLabel("Password").fill(process.env.TEST_USER_PASSWORD!);
  await page.getByRole("button", { name: "Sign In" }).click();

  await expect(page.getByText("Welcome")).toBeVisible();
});

[!TIP] Define variables in Settings → Organization → Variables. They are injected securely at runtime and never stored in plain text.

Best Practices

1. Use Resilient Selectors

Selectors break when UI changes. Use this priority order:

PrioritySelector TypeExampleStability
1Role-basedgetByRole("button", { name: "Submit" })⭐⭐⭐
2Test IDsgetByTestId("submit-btn")⭐⭐⭐
3Label/placeholdergetByLabel("Email")⭐⭐
4Text contentgetByText("Get Started")⭐⭐
5CSS selectorspage.click(".btn-primary")

2. Keep Tests Focused

One check should test one user flow. Don't try to test your entire app in a single script.

// ✅ Good — focused on one flow
test("user can log in", async ({ page }) => { /* ... */ });

// ❌ Bad — testing everything in one script
test("full app test", async ({ page }) => {
  // login, then dashboard, then settings, then checkout...
});

3. Clean Up After Tests

If your test creates data, make sure it doesn't pollute your production environment:

// Clean up: delete the test resource after verification
await page.getByRole("button", { name: "Delete" }).click();
await page.getByRole("button", { name: "Confirm" }).click();

4. Handle Dynamic Content

SPAs and async content need careful handling:

// Wait for loading state to finish
await expect(page.getByTestId("loading-spinner")).not.toBeVisible();

// Then assert on the loaded content
await expect(page.getByTestId("data-table")).toBeVisible();

Next Steps

On this page