Skip to Content
Quality AssuranceTestsPlaywright Testing

Playwright Testing

For basic information about Playwright, installation, configuration, and general usage, see the Playwright documentation in Frontend Tech.

Examples from phpreaction-frontend-integration-tests

Simple Login Test

import { test, expect } from "@playwright/test"; import dotenv from "dotenv"; import path from "path"; dotenv.config({ path: path.resolve(__dirname, "../..", ".env.local") }); test("Successful login", async ({ page }) => { await page.goto( `https://${process.env.NEXT_PUBLIC_TENANT}${process.env.NEXT_PUBLIC_ENV}dashboard.phpr.link/fr` ); await expect( page.locator("#pfqcn_phprconfig_login_login_form_username-input") ).toBeVisible(); await page .locator("#pfqcn_phprconfig_login_login_form_username-input") .fill(process.env.NEXT_PUBLIC_USERNAME || ""); await page .locator("#pfqcn_phprconfig_login_login_form_password-input") .fill(process.env.NEXT_PUBLIC_PASSWORD || ""); await Promise.all([ page.waitForURL( `https://${process.env.NEXT_PUBLIC_TENANT}${process.env.NEXT_PUBLIC_ENV}dashboard.phpr.link/fr`, { waitUntil: "domcontentloaded", } ), page.locator("#pfqcn_phprconfig_login_login_form_login-button").click(), ]); // Optionally verify dashboard elements are visible await expect(page.locator("text=Dashboard")).toBeVisible(); });

Security Testing - Rate Limiting

test.describe("Rate Limiting", () => { test("should block login attempts after multiple failed attempts", async ({ page, }) => { await page.goto(loginUrl); const usernameInput = page.locator( "#pfqcn_phprconfig_login_login_form_username-input" ); const passwordInput = page.locator( "#pfqcn_phprconfig_login_login_form_password-input" ); const loginButton = page.locator( "#pfqcn_phprconfig_login_login_form_login-button" ); await expect(usernameInput).toBeVisible(); const maxAttempts = 6; const errorMessageLocator = page.locator( "#pfqcn_phprconfig_login_login_form_error-message" ); let rateLimited = false; for (let i = 0; i < maxAttempts; i++) { await usernameInput.fill("invalid_username"); await passwordInput.fill("invalid_password"); await loginButton.click(); await page.waitForTimeout(1000); const errorMessageVisible = await errorMessageLocator.isVisible().catch(() => false); if (errorMessageVisible) { const errorText = await errorMessageLocator.textContent(); if (errorText === "Trop de demandes, veuillez réessayer plus tard.") { rateLimited = true; await expect(errorMessageLocator).toBeVisible(); await expect(errorMessageLocator).toHaveText( "Trop de demandes, veuillez réessayer plus tard." ); break; } } } expect(rateLimited).toBe(true); }); });

Security Testing - SQL Injection Prevention

test.describe("SQL Injection Prevention", () => { const sqlInjectionPayloads = [ "' OR '1'='1", "' OR '1'='1' --", "' OR '1'='1' /*", "admin'--", "admin'/*", "' UNION SELECT * FROM users--", "1' OR '1'='1", "admin' OR '1'='1'--", "' OR 1=1--", "admin'; DROP TABLE users--", ]; for (const payload of sqlInjectionPayloads) { test(`should prevent SQL injection with payload: ${payload}`, async ({ page, }) => { await page.goto(loginUrl); const usernameInput = page.locator( "#pfqcn_phprconfig_login_login_form_username-input" ); const passwordInput = page.locator( "#pfqcn_phprconfig_login_login_form_password-input" ); const loginButton = page.locator( "#pfqcn_phprconfig_login_login_form_login-button" ); await expect(usernameInput).toBeVisible(); await usernameInput.fill(payload); await passwordInput.fill(payload); await loginButton.click(); await page.waitForTimeout(2000); // Should NOT successfully login const currentUrl = page.url(); expect(currentUrl).toMatch(/login\.phpr\.link/); // Should not have access to dashboard await page.goto(dashboardUrl); const finalUrl = page.url(); expect(finalUrl).toMatch(/login\.phpr\.link/); }); } });

Security Testing - XSS Prevention

test.describe("XSS Prevention", () => { const xssPayloads = [ '<script>alert("xss")</script>', '<img src=x onerror=alert("xss")>', 'javascript:alert("xss")', '<svg onload=alert("xss")>', '"><script>alert("xss")</script>', "<iframe src='javascript:alert(\"xss\")'>", "<body onload=alert('xss')>", ]; for (const payload of xssPayloads) { test(`should prevent XSS with payload: ${payload.substring(0, 30)}...`, async ({ page, }) => { let xssExecuted = false; page.on("dialog", (dialog) => { xssExecuted = true; dialog.dismiss(); }); await page.goto(loginUrl); const usernameInput = page.locator( "#pfqcn_phprconfig_login_login_form_username-input" ); const passwordInput = page.locator( "#pfqcn_phprconfig_login_login_form_password-input" ); const loginButton = page.locator( "#pfqcn_phprconfig_login_login_form_login-button" ); await expect(usernameInput).toBeVisible(); // Attempt to inject XSS payload await usernameInput.fill(payload); await passwordInput.fill(payload); await loginButton.click(); await page.waitForTimeout(2000); // Verify XSS was NOT executed expect(xssExecuted).toBe(false); const pageContent = await page.content(); expect(pageContent).not.toContain(payload); }); } });

Reusable Login Helper Function

import { Page, expect } from "@playwright/test"; interface LoginOptions { username?: string; password?: string; waitForDashboard?: boolean; locale?: string; } /** * Performs login on the demo dashboard * @param page - Playwright page object * @param options - Login configuration options */ export async function loginToApp( page: Page, appUrl: string, options: LoginOptions = {} ) { const { username = process.env.NEXT_PUBLIC_USERNAME || "", password = process.env.NEXT_PUBLIC_PASSWORD || "", } = options; const baseUrl = `${appUrl}`; // Navigate to login page await page.goto(baseUrl); // Wait for and fill username const usernameInput = page.locator( "#pfqcn_phprconfig_login_login_form_username-input" ); await expect(usernameInput).toBeVisible(); await usernameInput.fill(username); // Fill password await page .locator("#pfqcn_phprconfig_login_login_form_password-input") .fill(password); // Click login and wait for navigation await Promise.all([ page.waitForURL(baseUrl, { waitUntil: "domcontentloaded" }), page.locator("#pfqcn_phprconfig_login_login_form_login-button").click(), ]); }
Last updated on