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