feat: implementing playwright tests
This commit is contained in:
14
bun.lock
14
bun.lock
@ -65,6 +65,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",
|
||||
@ -81,6 +82,7 @@
|
||||
},
|
||||
"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",
|
||||
@ -237,6 +239,8 @@
|
||||
|
||||
"@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="],
|
||||
|
||||
"@playwright/test": ["@playwright/test@1.56.1", "", { "dependencies": { "playwright": "1.56.1" }, "bin": { "playwright": "cli.js" } }, "sha512-vSMYtL/zOcFpvJCW71Q/OEGQb7KYBPAdKh35WNSkaZA75JlAO8ED8UN6GUNTm3drWomcbcqRPFqQbLae8yBTdg=="],
|
||||
|
||||
"@radix-ui/number": ["@radix-ui/number@1.1.1", "", {}, "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g=="],
|
||||
|
||||
"@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="],
|
||||
@ -835,6 +839,10 @@
|
||||
|
||||
"pirates": ["pirates@4.0.7", "", {}, "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA=="],
|
||||
|
||||
"playwright": ["playwright@1.56.1", "", { "dependencies": { "playwright-core": "1.56.1" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-aFi5B0WovBHTEvpM3DzXTUaeN6eN0qWnTkKx4NQaH4Wvcmc153PdaY2UBdSYKaGYw+UyWXSVyxDUg5DoPEttjw=="],
|
||||
|
||||
"playwright-core": ["playwright-core@1.56.1", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-hutraynyn31F+Bifme+Ps9Vq59hKuUCz7H1kDOcBs+2oGguKkWTU50bBWrtz34OUWmIwpBTWDxaRPXrIXkgvmQ=="],
|
||||
|
||||
"postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="],
|
||||
|
||||
"postcss-import": ["postcss-import@15.1.0", "", { "dependencies": { "postcss-value-parser": "^4.0.0", "read-cache": "^1.0.0", "resolve": "^1.1.7" }, "peerDependencies": { "postcss": "^8.0.0" } }, "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew=="],
|
||||
@ -1063,6 +1071,8 @@
|
||||
|
||||
"glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
|
||||
|
||||
"playwright/fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="],
|
||||
|
||||
"postcss-nested/postcss-selector-parser": ["postcss-selector-parser@6.1.2", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg=="],
|
||||
|
||||
"prop-types/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
|
||||
@ -1075,6 +1085,8 @@
|
||||
|
||||
"tailwindcss/postcss-selector-parser": ["postcss-selector-parser@6.1.2", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg=="],
|
||||
|
||||
"types/@types/bun": ["@types/bun@1.3.2", "", { "dependencies": { "bun-types": "1.3.2" } }, "sha512-t15P7k5UIgHKkxwnMNkJbWlh/617rkDGEdSsDbu+qNHTaz9SKf7aC8fiIlUdD5RPpH6GEkP0cK7WlvmrEBRtWg=="],
|
||||
|
||||
"vite/esbuild": ["esbuild@0.21.5", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.21.5", "@esbuild/android-arm": "0.21.5", "@esbuild/android-arm64": "0.21.5", "@esbuild/android-x64": "0.21.5", "@esbuild/darwin-arm64": "0.21.5", "@esbuild/darwin-x64": "0.21.5", "@esbuild/freebsd-arm64": "0.21.5", "@esbuild/freebsd-x64": "0.21.5", "@esbuild/linux-arm": "0.21.5", "@esbuild/linux-arm64": "0.21.5", "@esbuild/linux-ia32": "0.21.5", "@esbuild/linux-loong64": "0.21.5", "@esbuild/linux-mips64el": "0.21.5", "@esbuild/linux-ppc64": "0.21.5", "@esbuild/linux-riscv64": "0.21.5", "@esbuild/linux-s390x": "0.21.5", "@esbuild/linux-x64": "0.21.5", "@esbuild/netbsd-x64": "0.21.5", "@esbuild/openbsd-x64": "0.21.5", "@esbuild/sunos-x64": "0.21.5", "@esbuild/win32-arm64": "0.21.5", "@esbuild/win32-ia32": "0.21.5", "@esbuild/win32-x64": "0.21.5" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw=="],
|
||||
|
||||
"wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
|
||||
@ -1133,6 +1145,8 @@
|
||||
|
||||
"string-width-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||
|
||||
"types/@types/bun/bun-types": ["bun-types@1.3.2", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-i/Gln4tbzKNuxP70OWhJRZz1MRfvqExowP7U6JKoI8cntFrtxg7RJK3jvz7wQW54UuvNC8tbKHHri5fy74FVqg=="],
|
||||
|
||||
"vite/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.21.5", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ=="],
|
||||
|
||||
"vite/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.21.5", "", { "os": "android", "cpu": "arm" }, "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg=="],
|
||||
|
||||
210
packages/frontend/e2e/petition-form.spec.ts
Normal file
210
packages/frontend/e2e/petition-form.spec.ts
Normal 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",
|
||||
);
|
||||
});
|
||||
@ -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",
|
||||
|
||||
85
packages/frontend/playwright-report/index.html
Normal file
85
packages/frontend/playwright-report/index.html
Normal file
File diff suppressed because one or more lines are too long
18
packages/frontend/playwright.config.ts
Normal file
18
packages/frontend/playwright.config.ts
Normal 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"] },
|
||||
},
|
||||
],
|
||||
});
|
||||
@ -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`}
|
||||
>
|
||||
|
||||
56
packages/frontend/src/tests/petition-form.spec.ts
Normal file
56
packages/frontend/src/tests/petition-form.spec.ts
Normal 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
|
||||
});
|
||||
});
|
||||
4
packages/frontend/test-results/.last-run.json
Normal file
4
packages/frontend/test-results/.last-run.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"status": "passed",
|
||||
"failedTests": []
|
||||
}
|
||||
Reference in New Issue
Block a user