Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,9 @@ dist-ssr
*.njsproj
*.sln
*.sw?

# Playwright
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/
9 changes: 9 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

157 changes: 157 additions & 0 deletions e2e/AddTicket.spec.ts
Original file line number Diff line number Diff line change
@@ -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();
});
});
12 changes: 12 additions & 0 deletions e2e/HomePage.spec.ts
Original file line number Diff line number Diff line change
@@ -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();
});
7 changes: 6 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down
37 changes: 37 additions & 0 deletions playwright.config.ts
Original file line number Diff line number Diff line change
@@ -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,
},
});
3 changes: 2 additions & 1 deletion src/components/Modal/Form/Form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ const Form = ({ onFormModalClose }: FormPropTypes): JSX.Element => {
</h2>
<button
type="button"
id="button-form-close"
onClick={onFormModalClose}
className="text-gray-400 hover:text-red-500 hover:cursor-pointer transition"
aria-label="Close"
Expand Down Expand Up @@ -117,7 +118,7 @@ const Form = ({ onFormModalClose }: FormPropTypes): JSX.Element => {
</label>
<input
{...register("assignedTo")}
type="email"
type="text"
id="assignee"
placeholder="Enter email..."
className="w-full border border-neutral-700 bg-[#0b1220] text-white rounded-lg px-3 py-2 outline-none focus:ring-2 focus:ring-blue-600 placeholder:text-xs"
Expand Down
1 change: 1 addition & 0 deletions vitest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@ export default defineConfig({
globals: true,
environment: "jsdom",
setupFiles: "./tests/setup.ts",
exclude: ["**/node_modules/**", "**/dist/**", "**/e2e/**"],
},
});