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();
+ });
+ });
+});