diff --git a/app/package-lock.json b/app/package-lock.json index 083d8eb..2719567 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -59,7 +59,8 @@ "storybook": "^8.0.0", "tailwindcss": "^4", "typescript": "^5", - "vitest": "^3.0.0" + "vitest": "^3.0.0", + "vitest-axe": "^0.1.0" } }, "node_modules/@adobe/css-tools": { @@ -10493,6 +10494,13 @@ "dev": true, "license": "MIT" }, + "node_modules/lodash-es": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz", + "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==", + "dev": true, + "license": "MIT" + }, "node_modules/lodash.debounce": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", @@ -14854,6 +14862,37 @@ } } }, + "node_modules/vitest-axe": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/vitest-axe/-/vitest-axe-0.1.0.tgz", + "integrity": "sha512-jvtXxeQPg8R/2ANTY8QicA5pvvdRP4F0FsVUAHANJ46YCDASie/cuhlSzu0DGcLmZvGBSBNsNuK3HqfaeknyvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "aria-query": "^5.0.0", + "axe-core": "^4.4.2", + "chalk": "^5.0.1", + "dom-accessibility-api": "^0.5.14", + "lodash-es": "^4.17.21", + "redent": "^3.0.0" + }, + "peerDependencies": { + "vitest": ">=0.16.0" + } + }, + "node_modules/vitest-axe/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, "node_modules/vitest/node_modules/@vitest/expect": { "version": "3.2.7", "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.7.tgz", diff --git a/app/package.json b/app/package.json index e224f5a..d9a6719 100644 --- a/app/package.json +++ b/app/package.json @@ -48,6 +48,7 @@ "zod": "^3.25.0" }, "devDependencies": { + "@playwright/test": "^1.49.0", "@storybook/addon-essentials": "^8.0.0", "@storybook/addon-interactions": "^8.0.0", "@storybook/nextjs-vite": "^9.0.0", @@ -76,6 +77,6 @@ "tailwindcss": "^4", "typescript": "^5", "vitest": "^3.0.0", - "@playwright/test": "^1.49.0" + "vitest-axe": "^0.1.0" } } diff --git a/app/src/__tests__/signupValidation.test.tsx b/app/src/__tests__/signupValidation.test.tsx new file mode 100644 index 0000000..0817d9d --- /dev/null +++ b/app/src/__tests__/signupValidation.test.tsx @@ -0,0 +1,112 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { render, screen, fireEvent, waitFor } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import SignupPage from "@/app/signup/page"; + +// Mock next/navigation +vi.mock("next/navigation", () => ({ + useRouter: () => ({ + push: vi.fn(), + replace: vi.fn(), + }), +})); + +// Mock authService +const mockMutateAsync = vi.fn(); +vi.mock("@/services/authService", () => ({ + default: () => ({ + useRegisterEmail: () => ({ + mutateAsync: mockMutateAsync, + isPending: false, + }), + }), +})); + +// Mock sonner toast +vi.mock("sonner", () => ({ + toast: { + success: vi.fn(), + error: vi.fn(), + }, +})); + +describe("Signup Form Validation (Issue #98)", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("renders all form fields and disables submit button initially", () => { + render(); + + expect(screen.getByLabelText(/display name/i)).toBeInTheDocument(); + expect(screen.getByLabelText(/username/i)).toBeInTheDocument(); + expect(screen.getByLabelText(/email/i)).toBeInTheDocument(); + expect(screen.getByLabelText(/password/i)).toBeInTheDocument(); + + const submitBtn = screen.getByRole("button", { name: /sign up/i }); + expect(submitBtn).toBeDisabled(); + }); + + it("shows email format error for invalid email", async () => { + render(); + const user = userEvent.setup(); + + const emailInput = screen.getByLabelText(/email/i); + await user.type(emailInput, "not-an-email"); + + await waitFor(() => { + expect(screen.getByText(/please enter a valid email address/i)).toBeInTheDocument(); + }); + + // Clear and fix email + await user.clear(emailInput); + await user.type(emailInput, "valid.artist@example.com"); + + await waitFor(() => { + expect(screen.queryByText(/please enter a valid email address/i)).not.toBeInTheDocument(); + }); + }); + + it("shows password length and complexity errors", async () => { + render(); + const user = userEvent.setup(); + + const passwordInput = screen.getByLabelText(/password/i); + await user.type(passwordInput, "short"); + + await waitFor(() => { + expect(screen.getByText(/password must be at least 8 characters/i)).toBeInTheDocument(); + }); + + await user.clear(passwordInput); + await user.type(passwordInput, "12345678"); + + await waitFor(() => { + expect(screen.getByText(/password must contain at least one letter/i)).toBeInTheDocument(); + }); + + await user.clear(passwordInput); + await user.type(passwordInput, "Password123!"); + + await waitFor(() => { + expect(screen.queryByText(/password must/i)).not.toBeInTheDocument(); + }); + }); + + it("enables submit button only when all fields are valid", async () => { + render(); + const user = userEvent.setup(); + + const submitBtn = screen.getByRole("button", { name: /sign up/i }); + expect(submitBtn).toBeDisabled(); + + await user.type(screen.getByLabelText(/display name/i), "Artist Name"); + await user.type(screen.getByLabelText(/username/i), "artist_user"); + await user.type(screen.getByLabelText(/email/i), "artist@example.com"); + await user.type(screen.getByLabelText(/password/i), "ValidPass123!"); + + await waitFor(() => { + expect(submitBtn).not.toBeDisabled(); + }); + }); +});