feat: implementing playwright tests

This commit is contained in:
2025-11-15 11:04:20 +00:00
parent d442bae300
commit f7015e68a7
8 changed files with 396 additions and 3 deletions

View File

@@ -0,0 +1,210 @@
import { test, expect, Page } from "@playwright/test";
const getFormLocators = (page: Page) => {
const form = page.getByTestId("petition-form").first();
return {
form,
anonymousCheckbox: form.locator("#anonymous"),
nameInput: form.locator('input[placeholder="Your Name"]'),
emailInput: form.locator('input[type="email"]'),
commentTextarea: form.locator("textarea"),
submitButton: form.locator('button[type="submit"]'),
};
};
test.beforeEach(async ({ page }) => {
const mockSignatures: any[] = [];
await page.route("**/sign", async (route) => {
if (route.request().method() === "POST") {
const body = JSON.parse(route.request().postData() || "{}");
const newSignature = {
id: "fceff5b5-a185-4d42-a539-3a9c7b5edacd",
createdAt: new Date().toISOString(),
...body,
};
mockSignatures.push(newSignature);
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify(newSignature),
});
} else if (route.request().method() === "GET") {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify(mockSignatures),
});
} else {
await route.continue();
}
});
await page.goto("/");
});
test("anonymous submission with valid email only", async ({ page }) => {
const { anonymousCheckbox, emailInput, submitButton } = getFormLocators(page);
await anonymousCheckbox.check();
await emailInput.fill("test@example.com");
await submitButton.click();
await expect(
page.getByText("Thank you for signing the petition.").first(),
).toBeVisible();
await expect(emailInput).toHaveValue("");
});
test("named submission with valid name and email", async ({ page }) => {
const { nameInput, emailInput, submitButton } = getFormLocators(page);
await nameInput.fill("John Doe");
await emailInput.fill("john@example.com");
await submitButton.click();
await expect(
page.getByText("Thank you for signing the petition.").first(),
).toBeVisible();
await expect(emailInput).toHaveValue("");
await expect(nameInput).toHaveValue("");
});
test("submission with valid comment", async ({ page }) => {
const { anonymousCheckbox, emailInput, commentTextarea, submitButton } =
getFormLocators(page);
await anonymousCheckbox.check();
await emailInput.fill("test@example.com");
await commentTextarea.fill("This carpark is essential for our community");
await submitButton.click();
await expect(
page.getByText("Thank you for signing the petition.").first(),
).toBeVisible();
await expect(commentTextarea).toHaveValue("");
});
test("comment with less than 5 characters shows validation error", async ({
page,
}) => {
const { anonymousCheckbox, emailInput, commentTextarea, submitButton } =
getFormLocators(page);
await anonymousCheckbox.check();
await emailInput.fill("test@example.com");
await commentTextarea.fill("Hi");
await submitButton.click();
const validationMessage = await commentTextarea.evaluate(
(el: HTMLTextAreaElement) => el.validationMessage,
);
expect(validationMessage).toBeTruthy();
});
test("invalid email shows validation error", async ({ page }) => {
const { anonymousCheckbox, emailInput, submitButton } = getFormLocators(page);
await anonymousCheckbox.check();
await emailInput.fill("notanemail");
await submitButton.click();
const validationMessage = await emailInput.evaluate(
(el: HTMLInputElement) => el.validationMessage,
);
expect(validationMessage).toBeTruthy();
});
test("empty required fields show validation error", async ({ page }) => {
const { nameInput, submitButton } = getFormLocators(page);
await submitButton.click();
const nameValidationMessage = await nameInput.evaluate(
(el: HTMLInputElement) => el.validationMessage,
);
expect(nameValidationMessage).toBeTruthy();
});
test("anonymous checkbox toggles name field visibility", async ({ page }) => {
const { anonymousCheckbox, nameInput } = getFormLocators(page);
await expect(nameInput).toBeVisible();
await anonymousCheckbox.check();
await expect(nameInput).toBeHidden();
await anonymousCheckbox.uncheck();
await expect(nameInput).toBeVisible();
});
test("submit button is disabled while submitting", async ({ page }) => {
const mockSignatures: any[] = [];
await page.route("**/sign", async (route) => {
if (route.request().method() === "POST") {
await new Promise((resolve) => setTimeout(resolve, 1000));
const body = JSON.parse(route.request().postData() || "{}");
const newSignature = {
id: `${Date.now()}-${Math.random()}`,
createdAt: new Date().toISOString(),
...body,
};
mockSignatures.push(newSignature);
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify(newSignature),
});
}
});
const { anonymousCheckbox, emailInput, submitButton } = getFormLocators(page);
await anonymousCheckbox.check();
await emailInput.fill("test@example.com");
await submitButton.click();
await expect(submitButton).toBeDisabled();
await expect(submitButton).toHaveText("Submitting...");
});
test("signature counter increases after submissions", async ({ page }) => {
const signatureCountLocator = page.locator("text=/\\d+ people/");
const { anonymousCheckbox, emailInput, submitButton } = getFormLocators(page);
await anonymousCheckbox.check();
await emailInput.fill("first@example.com");
await submitButton.click();
await expect(
page.getByText("Thank you for signing the petition.").first(),
).toBeVisible();
await expect(signatureCountLocator).toContainText("1 people");
await anonymousCheckbox.check();
await emailInput.fill("second@example.com");
await submitButton.click();
await expect(
page.getByText("Thank you for signing the petition.").first(),
).toBeVisible();
await expect(signatureCountLocator).toContainText("2 people");
});
test("submitted petition appears on testimonies page with correct information", async ({
page,
}) => {
const testName = "Jane Smith";
const testEmail = "jane@example.com";
const testComment =
"This carpark closure has significantly impacted my daily routine";
const { nameInput, emailInput, commentTextarea, submitButton } =
getFormLocators(page);
await nameInput.fill(testName);
await emailInput.fill(testEmail);
await commentTextarea.fill(testComment);
await submitButton.click();
await expect(
page.getByText("Thank you for signing the petition.").first(),
).toBeVisible();
await page.goto("testimonies");
await expect(page.getByText(testName)).toBeVisible();
await expect(page.getByText(testComment)).toBeVisible();
await expect(page.locator("text=/\\d+ Signatures/")).toContainText(
"1 Signatures",
);
});

View File

@@ -8,7 +8,10 @@
"build": "vite build",
"build:dev": "vite build --mode development",
"lint": "eslint .",
"preview": "vite preview"
"preview": "vite preview",
"test:e2e": "playwright test",
"test:e2e:ui": "playwright test --ui",
"test:e2e:debug": "playwright test --debug"
},
"dependencies": {
"@hookform/resolvers": "^3.10.0",
@@ -49,6 +52,7 @@
"input-otp": "^1.4.2",
"lucide-react": "^0.462.0",
"next-themes": "^0.3.0",
"playwright": "^1.56.1",
"react": "^18.3.1",
"react-day-picker": "^8.10.1",
"react-dom": "^18.3.1",
@@ -59,12 +63,13 @@
"sonner": "^1.7.4",
"tailwind-merge": "^2.6.0",
"tailwindcss-animate": "^1.0.7",
"types": "workspace:types",
"vaul": "^0.9.9",
"zod": "^4.1.12",
"types": "workspace:types"
"zod": "^4.1.12"
},
"devDependencies": {
"@eslint/js": "^9.32.0",
"@playwright/test": "^1.56.1",
"@tailwindcss/typography": "^0.5.16",
"@types/node": "^22.16.5",
"@types/react": "^18.3.23",

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,18 @@
import { defineConfig, devices } from "@playwright/test";
export default defineConfig({
testDir: "./e2e",
fullyParallel: true,
workers: "100%",
reporter: "html",
use: {
baseURL: "http://localhost:8080",
trace: "on-first-retry",
},
projects: [
{
name: "chromium",
use: { ...devices["Desktop Chrome"] },
},
],
});

View File

@@ -54,6 +54,7 @@ export const PetitionForm = ({ compact = false }: PetitionFormProps) => {
return (
<form
data-testid="petition-form"
onSubmit={handleSubmit}
className={`space-y-4 ${compact ? "max-w-md" : "max-w-xl"} mx-auto`}
>

View File

@@ -0,0 +1,56 @@
import { test } from '@playwright/test';
// Mock response for successful signature
const mockSignatureResponse = {
id: 'mock-signature-id',
name: 'Test User',
message: 'This is a test signature',
date: new Date().toISOString(),
};
// Global fetch mock for POST /sign
async function setupFetchMock(page) {
await page.route('**/sign', async (route, request) => {
if (request.method() === 'POST') {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(mockSignatureResponse),
});
} else {
await route.continue();
}
});
}
// Reset state helper
async function resetMocks(page) {
// Currently no stateful mocks, but placeholder for future
}
// Test suite for Petition Form page
test.describe('Petition Form Page', () => {
test.beforeEach(async ({ page }) => {
await setupFetchMock(page);
await resetMocks(page);
await page.goto('/');
// Scroll to petition form section
await page.locator('#petition').scrollIntoViewIfNeeded();
});
test('placeholder: anonymous submission', async ({ page }) => {
// TODO: Implement test for anonymous submission
});
test('placeholder: named submission', async ({ page }) => {
// TODO: Implement test for named submission
});
test('placeholder: validation errors', async ({ page }) => {
// TODO: Implement test for validation errors
});
test('placeholder: UI behavior', async ({ page }) => {
// TODO: Implement test for UI behavior
});
});

View File

@@ -0,0 +1,4 @@
{
"status": "passed",
"failedTests": []
}