Learn how to run, write, and debug end-to-end tests in Sabo using Playwright for comprehensive application testing.
Sabo includes a complete Playwright E2E testing suite covering authentication, dashboard, marketing pages, blog, changelog, and legal pages. Tests run across multiple browsers and viewports to ensure cross-platform compatibility.
When to use: CI/CD pipelines, pre-commit checks, final verification before deployment.
Copy
pnpm test:e2e
Behavior:
Runs all tests in headless mode (no browser UI)
Tests run in parallel for speed
Automatically builds and starts your app
Generates HTML report at the end
Exit code 0 (success) or 1 (failure) for CI integration
Output:
Copy
Running 85 tests using 5 workers ✓ homepage.spec.ts:8:5 › Homepage › should load homepage successfully (1.2s) ✓ sign-in.spec.ts:8:5 › Sign In Page › should load sign in page successfully (890ms) ... 85 passed (1.5m)
pnpm test:e2e:ui - Interactive UI mode
When to use: Developing new tests, debugging flaky tests, exploring test coverage.
Copy
pnpm test:e2e:ui
Behavior:
Opens Playwright’s interactive test runner UI
Watch mode: tests re-run when files change
Time-travel debugging with DOM snapshots
Click through test steps one at a time
Filter tests by name, file, or status
Features:
See test execution in real-time
Inspect DOM at each step
View network requests and console logs
Record new tests by clicking in your app
Compare screenshots side-by-side
UI mode is the fastest way to write new tests. Use the “Record” feature to generate test code automatically.
pnpm test:e2e:headed - Visual browser mode
When to use: Debugging visual issues, understanding test flow, demonstrating tests to team.
Copy
pnpm test:e2e:headed
Behavior:
Tests run with visible browser windows
See exactly what Playwright sees
Slower than headless mode
Useful for debugging timing issues
Example workflow:
Run pnpm test:e2e:headed
Watch browser open and navigate your app
Identify visual bugs or timing issues
Fix and re-run specific tests
pnpm test:e2e:debug - Step-by-step debugging
When to use: Investigating test failures, understanding complex interactions, learning Playwright.
Copy
pnpm test:e2e:debug
Behavior:
Opens Playwright Inspector
Pauses at each test step
Step forward/backward through test
Explore locators and selectors
Modify test code on the fly
Debugging features:
Pick locator: Click any element to generate a selector
Step over/into: Control test execution
Console: Run Playwright commands interactively
Source: View test code with current line highlighted
Copy
# Debug a specific test filepnpm exec playwright test tests/e2e/auth/sign-in.spec.ts --debug# Debug a specific test by namepnpm exec playwright test --grep "should load homepage" --debug
pnpm test:e2e:report - View test results
When to use: After test run completes, reviewing failures, sharing results with team.
Enables parallel test execution across multiple worker processes. Dramatically speeds up test runs.Impact:
Without parallel: 85 tests in ~8 minutes
With parallel (5 workers): 85 tests in ~1.5 minutes
Tests must be independent (no shared state) for parallel execution to work correctly.
retries - Handle flaky tests
Copy
retries: process.env.CI ? 2 : 0
Automatically retry failed tests on CI environments. Local failures don’t retry (faster feedback).Why retry on CI:
Network latency
Resource constraints
Timing issues in CI environments
Best practice: Fix flaky tests instead of relying on retries. Use retries as a temporary safety net.
workers - Control parallelism
Copy
workers: process.env.CI ? 1 : undefined
Local: Uses all CPU cores (undefined = auto)
CI: Sequential execution (1 worker) for stability
Override workers:
Copy
# Run with specific number of workerspnpm exec playwright test --workers=3# Run tests sequentially (helpful for debugging)pnpm exec playwright test --workers=1
baseURL - Simplified navigation
Copy
use: { baseURL: "http://localhost:3000"}
Allows relative URLs in tests:
Copy
// With baseURLawait page.goto("/"); // Goes to http://localhost:3000/await page.goto("/dashboard"); // Goes to http://localhost:3000/dashboard// Without baseURL (not recommended)await page.goto("http://localhost:3000/dashboard");
test.describe("Feature Name", () => { // All tests for this feature});
Benefits:
Logical organization
Shared setup/teardown with beforeEach/afterEach
Better test reports (grouped by feature)
Can be nested for sub-features
test.beforeEach() - Setup before each test
Copy
test.beforeEach(async ({ page }) => { // Navigate to page await page.goto("/dashboard"); // Set up authentication (if needed) await setupAuthenticatedUser(page); // Reset state await page.evaluate(() => localStorage.clear());});
Use cases:
Navigate to a common page
Set up authentication
Clear browser state
Inject test data
Locators - Find elements on the page
Playwright provides multiple ways to find elements:
Copy
// By role (most recommended - accessible)page.getByRole("button", { name: "Submit" })page.getByRole("link", { name: "Sign In" })// By text contentpage.locator("text=Hello World")page.locator("text=/success|thank you/i") // Regex, case-insensitive// By CSS selectorpage.locator("form")page.locator('input[type="email"]')page.locator(".btn-primary")// By test ID (best for dynamic content)page.locator('[data-testid="user-menu"]')// By placeholderpage.getByPlaceholder("Enter your email")// By label text (for form inputs)page.getByLabel("Email address")
Best practices:
Prefer getByRole for accessibility
Use data-testid for dynamic content
Avoid brittle selectors (CSS classes that may change)
Use regex for flexible text matching
Assertions - Verify expected behavior
Copy
// Page assertionsawait expect(page).toHaveURL("/dashboard");await expect(page).toHaveTitle(/Dashboard/);// Element visibilityawait expect(page.locator("form")).toBeVisible();await expect(page.locator(".loading")).toBeHidden();// Element stateawait expect(page.locator("button")).toBeEnabled();await expect(page.locator("input")).toBeDisabled();await expect(page.locator("checkbox")).toBeChecked();// Text contentawait expect(page.locator("h1")).toHaveText("Welcome");await expect(page.locator("p")).toContainText("Hello");// Countawait expect(page.locator("li")).toHaveCount(5);// Attributeawait expect(page.locator("a")).toHaveAttribute("href", "/about");
Playwright’s assertions are auto-waiting: they retry until the condition is met or timeout occurs (default 30s).
# Run only your new test filepnpm exec playwright test new-feature.spec.ts# Run with UI for easier debuggingpnpm exec playwright test new-feature.spec.ts --ui
If your test passes, you’ll see a green checkmark. If it fails, Playwright will show exactly which assertion failed and why.
The auth helper is located at tests/e2e/helpers/auth.ts:
tests/e2e/helpers/auth.ts
Copy
import { Page } from "@playwright/test";export async function setupAuthenticatedUser(page: Page): Promise<void> { const { createClient } = await import("@supabase/supabase-js"); const supabase = createClient( process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY! ); const { data, error } = await supabase.auth.signInWithPassword({ email: process.env.TEST_USER_EMAIL!, password: process.env.TEST_USER_PASSWORD!, }); if (error) { throw new Error(`Failed to authenticate test user: ${error.message}`); } await page.context().addCookies([ { name: "sb-access-token", value: data.session.access_token, domain: "localhost", path: "/", httpOnly: true, secure: false, sameSite: "Lax", }, { name: "sb-refresh-token", value: data.session.refresh_token, domain: "localhost", path: "/", httpOnly: true, secure: false, sameSite: "Lax", }, ]); console.warn( "setupAuthenticatedUser is not implemented yet. Add test credentials to .env.test and uncomment the implementation." );}
The helper logs a warning by default as a reminder to configure test credentials. Once .env.test is set up and you have confirmed the helper works, delete or comment out the console.warn line to keep test output clean.
In the repository, tests that require authentication are marked with test.skip until setupAuthenticatedUser() is configured. Once your test credentials are in place, remove the skips to exercise the protected flows.
Want to reuse Playwright’s built-in authentication storage? See the official Playwright authentication guide. If you need to understand how Supabase sessions are issued in Sabo before writing tests, review Auth with Supabase.
Create a test user in your Supabase dashboard, then add credentials to .env.test:
.env.test
Copy
# Test user credentialsTEST_USER_EMAIL=test@example.comTEST_USER_PASSWORD=your_secure_test_password# Supabase credentials (same as .env.local)NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.coNEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key
Never commit.env.test to version control. Add it to .gitignore. Use a dedicated test user, not a real user account.
Playwright doesn’t automatically load .env.test. Either export these variables in your shell before running tests or run commands through the dotenv CLI: pnpm exec dotenv -e .env.test -- pnpm test:e2e. This ensures helpers like setupAuthenticatedUser() can read the credentials.
test("should submit contact form successfully", async ({ page }) => { // Fill form fields await page.locator('input[name="firstName"]').fill("John"); await page.locator('input[name="lastName"]').fill("Doe"); await page.locator('input[type="email"]').fill("john@example.com"); await page.locator('textarea[name="message"]').fill("Hello, this is a test message."); // Submit form await page.locator('button[type="submit"]').click(); // Verify success message await expect(page.locator("text=/success|thank you/i")).toBeVisible(); // Verify form is cleared await expect(page.locator('input[name="firstName"]')).toHaveValue("");});
test("should be responsive on mobile", async ({ page }) => { // Set mobile viewport await page.setViewportSize({ width: 375, height: 667 }); await page.goto("/"); // Mobile menu should be visible const mobileMenuButton = page.locator('button[aria-label="Menu"]'); await expect(mobileMenuButton).toBeVisible(); // Open mobile menu await mobileMenuButton.click(); // Mobile nav should appear const mobileNav = page.locator('nav[data-mobile]'); await expect(mobileNav).toBeVisible();});
The Playwright Inspector provides step-by-step debugging:
Copy
# Debug specific testpnpm exec playwright test auth/sign-in.spec.ts --debug
Features:
Step through: Execute one action at a time
Pick locator: Click elements to generate selectors
Console: Run Playwright commands interactively
Screenshots: Capture state at each step
Common debugging commands:
Copy
// In Playwright Inspector consolepage.locator('button') // Test selectorpage.screenshot() // Capture current statepage.pause() // Add breakpoint in test code
Screenshots are automatically captured on test failures. Find them in test-results/ directory.
Using test.skip() and test.only()
Control which tests run during debugging:
Copy
// Run only this test (ignore all others)test.only("focused test", async ({ page }) => { // ...});// Skip this test temporarilytest.skip("broken test to fix later", async ({ page }) => { // ...});// Conditional skiptest("auth test", async ({ page }) => { test.skip(!process.env.TEST_USER_EMAIL, "Test credentials not configured"); // ...});
Remove test.only() before committing! CI will fail if test.only() is detected (forbidOnly: true in config).
Analyzing trace files
Trace files provide complete test execution history:
Copy
# Run test with trace (automatically enabled on failures)pnpm test:e2e# View trace from failed testpnpm exec playwright show-trace test-results/path/to/trace.zip
Playwright tests can run automatically in your CI/CD pipeline.
For provider-specific examples (GitHub Actions, GitLab, Jenkins, Azure), refer to Playwright’s CI guide; the workflow below shows how we configure GitHub Actions for Sabo.