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");Navigation
// 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:
| Priority | Selector Type | Example | Stability |
|---|---|---|---|
| 1 | Role-based | getByRole("button", { name: "Submit" }) | ⭐⭐⭐ |
| 2 | Test IDs | getByTestId("submit-btn") | ⭐⭐⭐ |
| 3 | Label/placeholder | getByLabel("Email") | ⭐⭐ |
| 4 | Text content | getByText("Get Started") | ⭐⭐ |
| 5 | CSS selectors | page.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
- Playwright Locators — Deep dive into resilient selectors
- Tips & Best Practices — Advanced patterns for reliable tests
- Environment Variables — Managing secrets securely
- API Monitoring — Combine browser and API testing
Troubleshooting Synthetic Monitoring Checks
Fix common synthetic monitoring issues. Solutions for timeout errors, authentication failures, flaky tests, selector problems, and network errors in supaguard.
PagerDuty Integration: On-Call Alerts for Critical Failures
Connect supaguard to PagerDuty for on-call incident alerting. Set up escalation policies to page engineers only for critical synthetic monitoring failures.