diff --git a/.gitignore b/.gitignore index a547bf3..0447179 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,9 @@ dist-ssr *.njsproj *.sln *.sw? + +# Playwright +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ diff --git a/bun.lock b/bun.lock index ff8906e..d411126 100644 --- a/bun.lock +++ b/bun.lock @@ -21,6 +21,7 @@ }, "devDependencies": { "@biomejs/biome": "2.3.2", + "@playwright/test": "^1.56.1", "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.0", "@testing-library/user-event": "^14.6.1", @@ -183,6 +184,8 @@ "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + "@playwright/test": ["@playwright/test@1.56.1", "", { "dependencies": { "playwright": "1.56.1" }, "bin": { "playwright": "cli.js" } }, "sha512-vSMYtL/zOcFpvJCW71Q/OEGQb7KYBPAdKh35WNSkaZA75JlAO8ED8UN6GUNTm3drWomcbcqRPFqQbLae8yBTdg=="], + "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.43", "", {}, "sha512-5Uxg7fQUCmfhax7FJke2+8B6cqgeUJUD9o2uXIKXhD+mG0mL6NObmVoi9wXEU1tY89mZKgAYA6fTbftx3q2ZPQ=="], "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.52.5", "", { "os": "android", "cpu": "arm" }, "sha512-8c1vW4ocv3UOMp9K+gToY5zL2XiiVw3k7f1ksf4yO1FlDFQ1C2u72iACFnSOceJFsWskc2WZNqeRhFRPzv+wtQ=="], @@ -495,6 +498,10 @@ "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], + "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=="], "pretty-format": ["pretty-format@27.5.1", "", { "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", "react-is": "^17.0.1" } }, "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ=="], @@ -649,6 +656,8 @@ "make-dir/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], + "playwright/fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="], + "pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], "@vitest/coverage-v8/@vitest/utils/@vitest/pretty-format": ["@vitest/pretty-format@4.0.7", "", { "dependencies": { "tinyrainbow": "^3.0.3" } }, "sha512-YY//yxqTmk29+/pK+Wi1UB4DUH3lSVgIm+M10rAJ74pOSMgT7rydMSc+vFuq9LjZLhFvVEXir8EcqMke3SVM6Q=="], diff --git a/e2e/AddTicket.spec.ts b/e2e/AddTicket.spec.ts new file mode 100644 index 0000000..f7da330 --- /dev/null +++ b/e2e/AddTicket.spec.ts @@ -0,0 +1,157 @@ +// Modules +import { expect, test } from "@playwright/test"; + +test.describe("Add Ticket Functionality", () => { + test.beforeEach(async ({ page }) => { + await page.goto("/"); + await page.waitForLoadState("networkidle"); + }); + + test("should open add ticket modal when clicking Add ticket button", async ({ + page, + }) => { + await page.getByRole("button", { name: /add ticket/i }).click(); + + await expect(page.getByText("CREATE NEW TICKET")).toBeVisible(); + + await expect(page.locator("#title")).toBeVisible(); + await expect(page.locator("#description")).toBeVisible(); + await expect(page.locator("#assignee")).toBeVisible(); + await expect(page.locator("#priority")).toBeVisible(); + }); + + test("should successfully add a new ticket with valid data", async ({ + page, + }) => { + await page.getByRole("button", { name: /add ticket/i }).click(); + + await page.locator("#title").fill("Bug Fix"); + await page + .locator("#description") + .fill("Fix the login authentication issue"); + await page.locator("#assignee").fill("developer@example.com"); + await page.locator("#priority").fill("HIGH"); + + await page.getByRole("button", { name: /submit/i }).click(); + + await expect(page.getByText("TICKET CREATED SUCCESSFULLY!")).toBeVisible(); + + await expect(page.getByText("Bug Fix")).toBeVisible(); + await expect(page.getByText("developer@example.com")).toBeVisible(); + }); + + test("should show validation error for title less than 3 characters", async ({ + page, + }) => { + await page.getByRole("button", { name: /add ticket/i }).click(); + + await page.locator("#title").fill("AB"); + await page + .locator("#description") + .fill("This is a valid description with more than ten characters"); + await page.locator("#assignee").fill("test@example.com"); + await page.locator("#priority").fill("MEDIUM"); + + await page.getByRole("button", { name: /submit/i }).click(); + + await expect( + page.getByText(/Title must be at least 3 characters long!/i), + ).toBeVisible(); + }); + + test("should show validation error for description less than 10 characters", async ({ + page, + }) => { + await page.getByRole("button", { name: /add ticket/i }).click(); + + await page.locator("#title").fill("Valid Title"); + await page.locator("#description").fill("Short"); + await page.locator("#assignee").fill("test@example.com"); + await page.locator("#priority").fill("LOW"); + + await page.getByRole("button", { name: /submit/i }).click(); + + await expect( + page.getByText(/Description must be at least 10 characters!/i), + ).toBeVisible(); + }); + + test("should show validation error for invalid email format", async ({ + page, + }) => { + await page.getByRole("button", { name: /add ticket/i }).click(); + + await page.locator("#title").fill("Valid Title"); + await page.locator("#description").fill("This is a valid description"); + await page.locator("#assignee").fill("invalid-email"); + await page.locator("#priority").fill("HIGH"); + + await page.getByRole("button", { name: /submit/i }).click(); + + await expect( + page.getByText(/Please provide a valid email address!/i), + ).toBeVisible(); + }); + + test("should show validation error for invalid priority", async ({ + page, + }) => { + await page.getByRole("button", { name: /add ticket/i }).click(); + + await page.locator("#title").fill("Valid Title"); + await page.locator("#description").fill("This is a valid description"); + await page.locator("#assignee").fill("test@example.com"); + await page.locator("#priority").fill("URGENT"); + + await page.getByRole("button", { name: /submit/i }).click(); + + await expect( + page.getByText(/Priority must be either HIGH, MEDIUM or LOW/i), + ).toBeVisible(); + }); + + test("should close modal when clicking close button", async ({ page }) => { + await page.getByRole("button", { name: /add ticket/i }).click(); + + await expect(page.getByText("CREATE NEW TICKET")).toBeVisible(); + await page.locator("#button-form-close").first().click(); + + await expect(page.getByText("CREATE NEW TICKET")).not.toBeVisible(); + }); + + test("should add multiple tickets successfully", async ({ page }) => { + await page.getByRole("button", { name: /add ticket/i }).click(); + await page.locator("#title").fill("First Ticket"); + await page.locator("#description").fill("Description for the first ticket"); + await page.locator("#assignee").fill("user1@example.com"); + await page.locator("#priority").fill("HIGH"); + await page.getByRole("button", { name: /submit/i }).click(); + + await expect(page.getByText("TICKET CREATED SUCCESSFULLY!")).toBeVisible(); + + await page.waitForTimeout(500); + + await page.getByRole("button", { name: /add ticket/i }).click(); + await page.locator("#title").fill("Second Task"); + await page + .locator("#description") + .fill("Description for the second ticket"); + await page.locator("#assignee").fill("user2@example.com"); + await page.locator("#priority").fill("MEDIUM"); + await page.getByRole("button", { name: /submit/i }).click(); + }); + + test("should accept different priority values (case-insensitive)", async ({ + page, + }) => { + await page.getByRole("button", { name: /add ticket/i }).click(); + await page.locator("#title").fill("Low Case"); + await page.locator("#description").fill("Testing lowercase priority"); + await page.locator("#assignee").fill("test@example.com"); + await page.locator("#priority").fill("low"); + await page.getByRole("button", { name: /submit/i }).click(); + + await expect(page.getByText("TICKET CREATED SUCCESSFULLY!")).toBeVisible(); + await expect(page.getByText("Low Case")).toBeVisible(); + }); +}); diff --git a/e2e/HomePage.spec.ts b/e2e/HomePage.spec.ts new file mode 100644 index 0000000..a6454a4 --- /dev/null +++ b/e2e/HomePage.spec.ts @@ -0,0 +1,12 @@ +// Modules +import { expect, test } from "@playwright/test"; + +test("Homepage", async ({ page }) => { + await page.goto("https://labquick-beta.vercel.app/"); + // Verifying the page url + await expect(page).toHaveURL("https://labquick-beta.vercel.app/"); + // Verifying the page title of the application + await expect(page).toHaveTitle("QUICKLAB"); + + await page.close(); +}); diff --git a/package.json b/package.json index 8903857..588576b 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,11 @@ "build": "tsc -b && vite build", "preview": "vite preview", "prepare": "husky", - "application:check": "concurrently \"bun run application:test\" \"bun run application:format\"" + "application:check": "concurrently \"bun run application:test\" \"bun run application:format\"", + "e2e": "playwright test", + "e2e:ui": "playwright test --ui", + "e2e:headed": "playwright test --headed", + "e2e:report": "playwright show-report" }, "dependencies": { "@hookform/resolvers": "^5.2.2", @@ -30,6 +34,7 @@ }, "devDependencies": { "@biomejs/biome": "2.3.2", + "@playwright/test": "^1.56.1", "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.0", "@testing-library/user-event": "^14.6.1", diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..e1f80fa --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,37 @@ +import { defineConfig, devices } from "@playwright/test"; + +export default defineConfig({ + testDir: "./e2e", + fullyParallel: true, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 0, + workers: process.env.CI ? 1 : undefined, + reporter: "html", + use: { + baseURL: "http://localhost:5173", + trace: "on-first-retry", + }, + + projects: [ + { + name: "chromium", + use: { ...devices["Desktop Chrome"] }, + }, + + { + name: "firefox", + use: { ...devices["Desktop Firefox"] }, + }, + + { + name: "webkit", + use: { ...devices["Desktop Safari"] }, + }, + ], + + webServer: { + command: "bun run application:dev", + url: "http://localhost:5173", + reuseExistingServer: !process.env.CI, + }, +}); diff --git a/src/components/Modal/Form/Form.tsx b/src/components/Modal/Form/Form.tsx index 9993235..fc8f9ad 100644 --- a/src/components/Modal/Form/Form.tsx +++ b/src/components/Modal/Form/Form.tsx @@ -51,6 +51,7 @@ const Form = ({ onFormModalClose }: FormPropTypes): JSX.Element => {