From 983a0e296d32be53e3910be98ad18d46570f5ed0 Mon Sep 17 00:00:00 2001 From: DavisVT Date: Thu, 30 Jul 2026 02:50:00 +0100 Subject: [PATCH] Refactor test suite with shared fixtures and mock data - Create reusable fixtures in frontend/src/test/fixtures.ts - Create mock data objects in frontend/src/test/mock-data.ts - Add comprehensive documentation in frontend/src/test/README.md - Update 7 test files to use shared fixtures and mock data - Remove duplicate setup code across test suite - Improve test readability and maintainability --- .../src/hooks/stellar-wallets-kit.test.ts | 24 +- .../hooks/useNotificationPreferences.test.ts | 53 +- frontend/src/lib/contributor-profile.test.ts | 22 +- frontend/src/lib/form-validation.test.ts | 92 ++-- frontend/src/lib/task-submission-e2e.test.ts | 54 +- .../src/lib/task-submission-files.test.ts | 46 +- frontend/src/lib/taskValidation.test.ts | 26 +- frontend/src/test/README.md | 185 +++++++ frontend/src/test/fixtures.ts | 293 +++++++++++ frontend/src/test/mock-data.ts | 494 ++++++++++++++++++ 10 files changed, 1088 insertions(+), 201 deletions(-) create mode 100644 frontend/src/test/README.md create mode 100644 frontend/src/test/fixtures.ts create mode 100644 frontend/src/test/mock-data.ts diff --git a/frontend/src/hooks/stellar-wallets-kit.test.ts b/frontend/src/hooks/stellar-wallets-kit.test.ts index a537938..709d2fd 100644 --- a/frontend/src/hooks/stellar-wallets-kit.test.ts +++ b/frontend/src/hooks/stellar-wallets-kit.test.ts @@ -5,6 +5,11 @@ * run in a pure Node environment without a real Stellar wallet or browser extension. */ import { describe, expect, it, vi, beforeEach, afterEach } from "vitest"; +import { + cleanupLocalStorageMock, + createLocalStorageMock, + setupLocalStorageMock, +} from "@/test/fixtures"; // --------------------------------------------------------------------------- // Mocks — declared before vi.mock() so they are available in the factory @@ -49,19 +54,7 @@ vi.mock("@/lib/env", () => ({ // localStorage stub // --------------------------------------------------------------------------- -const localStore: Record = {}; -const localStorageMock = { - getItem: vi.fn((key: string) => localStore[key] ?? null), - setItem: vi.fn((key: string, value: string) => { - localStore[key] = value; - }), - removeItem: vi.fn((key: string) => { - delete localStore[key]; - }), - clear: vi.fn(() => { - Object.keys(localStore).forEach((k) => delete localStore[k]); - }), -}; +const localStorageMock = createLocalStorageMock(); // --------------------------------------------------------------------------- // Setup / teardown @@ -73,12 +66,11 @@ beforeEach(() => { localStorageMock.clear(); // Stub window so `typeof window !== "undefined"` is true in the module - vi.stubGlobal("window", { localStorage: localStorageMock }); - vi.stubGlobal("localStorage", localStorageMock); + setupLocalStorageMock(localStorageMock); }); afterEach(() => { - vi.unstubAllGlobals(); + cleanupLocalStorageMock(); }); async function loadKit() { diff --git a/frontend/src/hooks/useNotificationPreferences.test.ts b/frontend/src/hooks/useNotificationPreferences.test.ts index e1006c1..0f2ce5c 100644 --- a/frontend/src/hooks/useNotificationPreferences.test.ts +++ b/frontend/src/hooks/useNotificationPreferences.test.ts @@ -18,41 +18,32 @@ import { type NotificationCategory, type NotificationPreferences, } from "./useNotificationPreferences"; +import { + cleanupLocalStorageMock, + createLocalStorageMock, + setupLocalStorageMock, +} from "@/test/fixtures"; +import { + DEFAULT_NOTIFICATION_PREFS, + MODIFIED_NOTIFICATION_PREFS, +} from "@/test/mock-data"; // --------------------------------------------------------------------------- // localStorage stub (node environment has no DOM) // --------------------------------------------------------------------------- -const store: Record = {}; - -const localStorageMock = { - getItem: vi.fn((key: string) => store[key] ?? null), - setItem: vi.fn((key: string, value: string) => { - store[key] = value; - }), - removeItem: vi.fn((key: string) => { - delete store[key]; - }), - clear: vi.fn(() => { - Object.keys(store).forEach((k) => delete store[k]); - }), - get length() { - return Object.keys(store).length; - }, - key: vi.fn((index: number) => Object.keys(store)[index] ?? null), -}; +const localStorageMock = createLocalStorageMock(); beforeEach(() => { // Expose the mock as globalThis.localStorage so the module's typeof window // check passes in the node test environment. - vi.stubGlobal("window", { localStorage: localStorageMock }); - vi.stubGlobal("localStorage", localStorageMock); + setupLocalStorageMock(localStorageMock); localStorageMock.clear(); vi.clearAllMocks(); }); afterEach(() => { - vi.unstubAllGlobals(); + cleanupLocalStorageMock(); }); // --------------------------------------------------------------------------- @@ -120,11 +111,7 @@ describe("localStorage persistence helpers", () => { }); it("stores preferences as JSON and retrieves them correctly", async () => { - const prefs: NotificationPreferences = { - ...DEFAULT_NOTIFICATION_PREFERENCES, - payments: false, - disputes: false, - }; + const prefs: NotificationPreferences = MODIFIED_NOTIFICATION_PREFS; // Manually simulate what saveToStorage does localStorageMock.setItem(NOTIFICATION_PREFS_KEY, JSON.stringify(prefs)); @@ -150,7 +137,7 @@ describe("localStorage persistence helpers", () => { describe("preference mutation helpers", () => { it("toggle flips a single category", () => { - const prefs: NotificationPreferences = { ...DEFAULT_NOTIFICATION_PREFERENCES }; + const prefs: NotificationPreferences = DEFAULT_NOTIFICATION_PREFS; const toggled: NotificationPreferences = { ...prefs, @@ -162,7 +149,7 @@ describe("preference mutation helpers", () => { }); it("setAll(false) disables all categories", () => { - const prefs: NotificationPreferences = { ...DEFAULT_NOTIFICATION_PREFERENCES }; + const prefs: NotificationPreferences = DEFAULT_NOTIFICATION_PREFS; const updated = { ...prefs } as NotificationPreferences; for (const cat of NOTIFICATION_CATEGORIES) { @@ -195,7 +182,7 @@ describe("preference mutation helpers", () => { }); it("update merges partial preferences over existing ones", () => { - const prefs: NotificationPreferences = { ...DEFAULT_NOTIFICATION_PREFERENCES }; + const prefs: NotificationPreferences = DEFAULT_NOTIFICATION_PREFS; const partial: Partial = { payments: false, }; @@ -208,14 +195,10 @@ describe("preference mutation helpers", () => { }); it("reset restores defaults", () => { - const modified: NotificationPreferences = { - ...DEFAULT_NOTIFICATION_PREFERENCES, - payments: false, - disputes: false, - }; + const modified: NotificationPreferences = MODIFIED_NOTIFICATION_PREFS; // After reset we should get defaults back - const afterReset: NotificationPreferences = { ...DEFAULT_NOTIFICATION_PREFERENCES }; + const afterReset: NotificationPreferences = DEFAULT_NOTIFICATION_PREFS; for (const cat of NOTIFICATION_CATEGORIES) { expect(afterReset[cat]).toBe(DEFAULT_NOTIFICATION_PREFERENCES[cat]); diff --git a/frontend/src/lib/contributor-profile.test.ts b/frontend/src/lib/contributor-profile.test.ts index e2b8c28..76467cc 100644 --- a/frontend/src/lib/contributor-profile.test.ts +++ b/frontend/src/lib/contributor-profile.test.ts @@ -1,26 +1,16 @@ import { describe, expect, it } from "vitest"; import { calculateContributorProfileCompletion } from "@/lib/contributor-profile"; +import { + PARTIAL_CONTRIBUTOR_PROFILE, + PROFILE_FIELD_DEFINITIONS, +} from "@/test/mock-data"; describe("calculateContributorProfileCompletion", () => { it("returns completion percentage and missing fields", () => { const result = calculateContributorProfileCompletion( - { - name: "Ada Lovelace", - headline: "Engineer", - bio: "", - location: "", - skills: "", - website: "", - }, - [ - { key: "name", label: "Full name" }, - { key: "headline", label: "Headline" }, - { key: "bio", label: "Bio" }, - { key: "location", label: "Location" }, - { key: "skills", label: "Skills" }, - { key: "website", label: "Website" }, - ], + PARTIAL_CONTRIBUTOR_PROFILE, + PROFILE_FIELD_DEFINITIONS, ); expect(result.percentage).toBe(33); diff --git a/frontend/src/lib/form-validation.test.ts b/frontend/src/lib/form-validation.test.ts index 1829c78..f8d426e 100644 --- a/frontend/src/lib/form-validation.test.ts +++ b/frontend/src/lib/form-validation.test.ts @@ -28,6 +28,27 @@ import { MIN_MAX_SUBMISSIONS, MAX_MAX_SUBMISSIONS, } from "./form-validation"; +import { + INVALID_EMAIL_NO_AT, + INVALID_TASK_BAD_POSTER, + INVALID_TASK_BAD_TOKEN, + INVALID_TASK_EMPTY_DESCRIPTION, + INVALID_TASK_EMPTY_TITLE, + INVALID_TASK_PAST_DEADLINE, + INVALID_TASK_ZERO_REWARD, + INVALID_TASK_ZERO_SUBMISSIONS, + INVALID_WORK_SUBMISSION_BAD_CONTRIBUTOR, + INVALID_WORK_SUBMISSION_EMPTY_DESCRIPTION, + INVALID_WORK_SUBMISSION_EMPTY_URL, + INVALID_WORK_SUBMISSION_INVALID_URL, + STANDARD_STELLAR_ADDRESS, + VALID_EMAIL, + VALID_TASK_DATA, + VALID_TASK_DATA_WITH_OPTIONALS, + VALID_WORK_SUBMISSION, + VALID_WORK_SUBMISSION_WITH_CONTRIBUTOR, +} from "@/test/mock-data"; +import { futureDeadline, pastDeadline } from "@/test/fixtures"; // ============================================================================ // validateRequired @@ -688,14 +709,7 @@ describe("validateSubmissionDescription", () => { // ============================================================================ describe("validateCreateTaskForm", () => { - const validForm = { - title: "Build a DEX Interface", - description: - "Create a React frontend for Stellar DEX with swap UI, wallet integration, and transaction history.", - reward: "100", - deadline: String(Math.floor(Date.now() / 1000) + 30 * 24 * 60 * 60), // 30 days - maxSubmissions: "3", - }; + const validForm = VALID_TASK_DATA; it("accepts a completely valid form", () => { const result = validateCreateTaskForm(validForm); @@ -704,7 +718,7 @@ describe("validateCreateTaskForm", () => { }); it("rejects empty title", () => { - const result = validateCreateTaskForm({ ...validForm, title: "" }); + const result = validateCreateTaskForm(INVALID_TASK_EMPTY_TITLE); expect(result.ok).toBe(false); expect(result.errors.title).toBeDefined(); }); @@ -716,44 +730,40 @@ describe("validateCreateTaskForm", () => { }); it("rejects empty description", () => { - const result = validateCreateTaskForm({ ...validForm, description: "" }); + const result = validateCreateTaskForm(INVALID_TASK_EMPTY_DESCRIPTION); expect(result.ok).toBe(false); expect(result.errors.description).toBeDefined(); }); it("rejects invalid reward", () => { - const result = validateCreateTaskForm({ ...validForm, reward: "0" }); + const result = validateCreateTaskForm(INVALID_TASK_ZERO_REWARD); expect(result.ok).toBe(false); expect(result.errors.reward).toBeDefined(); }); it("rejects past deadline", () => { - const past = String(Math.floor(Date.now() / 1000) - 3600); - const result = validateCreateTaskForm({ ...validForm, deadline: past }); + const result = validateCreateTaskForm({ + ...validForm, + deadline: String(pastDeadline()), + }); expect(result.ok).toBe(false); expect(result.errors.deadline).toBeDefined(); }); it("rejects invalid max submissions", () => { - const result = validateCreateTaskForm({ ...validForm, maxSubmissions: "0" }); + const result = validateCreateTaskForm(INVALID_TASK_ZERO_SUBMISSIONS); expect(result.ok).toBe(false); expect(result.errors.maxSubmissions).toBeDefined(); }); it("validates token address if provided", () => { - const result = validateCreateTaskForm({ - ...validForm, - token: "invalid-token", - }); + const result = validateCreateTaskForm(INVALID_TASK_BAD_TOKEN); expect(result.ok).toBe(false); expect(result.errors.token).toBeDefined(); }); it("accepts valid token address", () => { - const result = validateCreateTaskForm({ - ...validForm, - token: "GABCDEFGHIJKLMNOPQRSTUVWXYZ234567ABCDEFGHIJKLMNOPQRSTUVWXYZ234567", - }); + const result = validateCreateTaskForm(VALID_TASK_DATA_WITH_OPTIONALS); expect(result.ok).toBe(true); }); @@ -763,10 +773,7 @@ describe("validateCreateTaskForm", () => { }); it("validates poster address if provided", () => { - const result = validateCreateTaskForm({ - ...validForm, - posterAddress: "bad-address", - }); + const result = validateCreateTaskForm(INVALID_TASK_BAD_POSTER); expect(result.ok).toBe(false); expect(result.errors.posterAddress).toBeDefined(); }); @@ -793,11 +800,7 @@ describe("validateCreateTaskForm", () => { // ============================================================================ describe("validateWorkSubmissionForm", () => { - const validForm = { - workUrl: "https://github.com/user/task-submission", - description: - "Implemented all required features for the DEX interface including swap, liquidity pools, and wallet integration.", - }; + const validForm = VALID_WORK_SUBMISSION; it("accepts a completely valid submission form", () => { const result = validateWorkSubmissionForm(validForm); @@ -805,34 +808,25 @@ describe("validateWorkSubmissionForm", () => { }); it("rejects empty work URL", () => { - const result = validateWorkSubmissionForm({ ...validForm, workUrl: "" }); + const result = validateWorkSubmissionForm(INVALID_WORK_SUBMISSION_EMPTY_URL); expect(result.ok).toBe(false); expect(result.errors.workUrl).toBeDefined(); }); it("rejects invalid work URL", () => { - const result = validateWorkSubmissionForm({ - ...validForm, - workUrl: "not-a-url", - }); + const result = validateWorkSubmissionForm(INVALID_WORK_SUBMISSION_INVALID_URL); expect(result.ok).toBe(false); expect(result.errors.workUrl).toBeDefined(); }); it("rejects empty description", () => { - const result = validateWorkSubmissionForm({ - ...validForm, - description: "", - }); + const result = validateWorkSubmissionForm(INVALID_WORK_SUBMISSION_EMPTY_DESCRIPTION); expect(result.ok).toBe(false); expect(result.errors.description).toBeDefined(); }); it("validates contributor address if provided", () => { - const result = validateWorkSubmissionForm({ - ...validForm, - contributorAddress: "bad", - }); + const result = validateWorkSubmissionForm(INVALID_WORK_SUBMISSION_BAD_CONTRIBUTOR); expect(result.ok).toBe(false); expect(result.errors.contributorAddress).toBeDefined(); }); @@ -846,11 +840,7 @@ describe("validateWorkSubmissionForm", () => { }); it("accepts valid contributor address", () => { - const result = validateWorkSubmissionForm({ - ...validForm, - contributorAddress: - "GABCDEFGHIJKLMNOPQRSTUVWXYZ234567ABCDEFGHIJKLMNOPQRSTUVWXYZ234567", - }); + const result = validateWorkSubmissionForm(VALID_WORK_SUBMISSION_WITH_CONTRIBUTOR); expect(result.ok).toBe(true); }); }); @@ -867,13 +857,13 @@ describe("validateWaitlistForm", () => { }); it("rejects invalid email", () => { - const result = validateWaitlistForm("not-email"); + const result = validateWaitlistForm(INVALID_EMAIL_NO_AT); expect(result.ok).toBe(false); expect(result.error).toBe("Please enter a valid email address."); }); it("accepts valid email", () => { - const result = validateWaitlistForm("user@example.com"); + const result = validateWaitlistForm(VALID_EMAIL); expect(result.ok).toBe(true); }); }); diff --git a/frontend/src/lib/task-submission-e2e.test.ts b/frontend/src/lib/task-submission-e2e.test.ts index 783e108..28a517a 100644 --- a/frontend/src/lib/task-submission-e2e.test.ts +++ b/frontend/src/lib/task-submission-e2e.test.ts @@ -4,30 +4,16 @@ import { GET as getTaskById } from "@/app/api/tasks/[taskId]/route"; import { POST as submitTaskWork } from "@/app/api/tasks/[taskId]/submissions/route"; import { POST as createTask } from "@/app/api/tasks/route"; import { resetTaskWorkflowStore } from "@/lib/task-workflow"; - -const POSTER = "GPOSTER1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ"; -const CONTRIBUTOR = "GCONTRIB1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ"; - -function createPdfFile(name = "submission.pdf") { - return new File( - [ - new Uint8Array([0x25, 0x50, 0x44, 0x46, 0x2d, 0x31, 0x2e, 0x37]), - "\nMock pdf payload", - ], - name, - { - type: "application/pdf", - }, - ); -} - -function futureDeadline(offsetSeconds = 86_400) { - return Math.floor(Date.now() / 1000) + offsetSeconds; -} - -function taskRouteContext(taskId: string) { - return { params: Promise.resolve({ taskId }) }; -} +import { + createPdfFile, + futureDeadline, + taskRouteContext, +} from "@/test/fixtures"; +import { + CONTRIBUTOR_ADDRESS, + POSTER_ADDRESS, + VALID_TASK_DATA, +} from "@/test/mock-data"; async function createTaskRequest(overrides: Record = {}) { return createTask( @@ -35,12 +21,8 @@ async function createTaskRequest(overrides: Record = {}) { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ - poster: POSTER, - title: "Build task submission E2E coverage", - description: "Verify create, submit, and status transitions end to end.", - reward: 10_000_000, - deadline: futureDeadline(), - maxSubmissions: 3, + poster: POSTER_ADDRESS, + ...VALID_TASK_DATA, ...overrides, }), }), @@ -65,7 +47,7 @@ describe("task submission workflow (e2e)", () => { ok: true, task: { id: "1", - poster: POSTER, + poster: POSTER_ADDRESS, status: "open", submissionCount: 0, }, @@ -83,7 +65,7 @@ describe("task submission workflow (e2e)", () => { expect(initialTaskBody.task.status).toBe("open"); const formData = new FormData(); - formData.append("contributor", CONTRIBUTOR); + formData.append("contributor", CONTRIBUTOR_ADDRESS); formData.append("description", "Delivered the requested implementation with proof attached."); formData.append("workUrl", "https://example.com/proof"); formData.append("files", createPdfFile()); @@ -110,7 +92,7 @@ describe("task submission workflow (e2e)", () => { submission: { id: "1", taskId, - contributor: CONTRIBUTOR, + contributor: CONTRIBUTOR_ADDRESS, status: "pending", workUrl: "https://example.com/proof", }, @@ -140,7 +122,7 @@ describe("task submission workflow (e2e)", () => { const taskId = created.task.id as string; const formData = new FormData(); - formData.append("contributor", CONTRIBUTOR); + formData.append("contributor", CONTRIBUTOR_ADDRESS); formData.append("description", "First submission."); formData.append("files", createPdfFile("first.pdf")); @@ -154,7 +136,7 @@ describe("task submission workflow (e2e)", () => { expect(firstSubmit.status).toBe(201); const duplicateFormData = new FormData(); - duplicateFormData.append("contributor", CONTRIBUTOR); + duplicateFormData.append("contributor", CONTRIBUTOR_ADDRESS); duplicateFormData.append("description", "Duplicate submission."); duplicateFormData.append("files", createPdfFile("second.pdf")); @@ -207,7 +189,7 @@ describe("task submission workflow (e2e)", () => { const taskId = created.task.id as string; const formData = new FormData(); - formData.append("contributor", CONTRIBUTOR); + formData.append("contributor", CONTRIBUTOR_ADDRESS); formData.append("description", "Missing proof files."); const submitResponse = await submitTaskWork( diff --git a/frontend/src/lib/task-submission-files.test.ts b/frontend/src/lib/task-submission-files.test.ts index 3fb8981..f28af15 100644 --- a/frontend/src/lib/task-submission-files.test.ts +++ b/frontend/src/lib/task-submission-files.test.ts @@ -7,21 +7,14 @@ import { MAX_TASK_SUBMISSION_FILE_SIZE_BYTES, validateTaskSubmissionFiles, } from "@/lib/task-submission-files"; - -function createPdfFile(name = "submission.pdf") { - return new File([ - new Uint8Array([0x25, 0x50, 0x44, 0x46, 0x2d, 0x31, 0x2e, 0x37]), - "\nMock pdf payload", - ], name, { - type: "application/pdf", - }); -} - -function createZipFile(name = "submission.zip") { - return new File([new Uint8Array([0x50, 0x4b, 0x03, 0x04, 0x14, 0x00])], name, { - type: "application/zip", - }); -} +import { + createFileValidationFormData, + createFormDataPostRequest, + createMarkdownFile, + createOversizedFile, + createPdfFile, + createZipFile, +} from "@/test/fixtures"; describe("validateTaskSubmissionFiles", () => { it("accepts a supported file when extension, MIME type, and content match", async () => { @@ -42,9 +35,7 @@ describe("validateTaskSubmissionFiles", () => { it("accepts UTF-8 markdown files even when the browser sends a generic MIME type", async () => { const result = await validateTaskSubmissionFiles([ - new File(["# Task proof\nCompleted all requested changes."], "proof.md", { - type: "application/octet-stream", - }), + createMarkdownFile(), ]); expect(result.ok).toBe(true); @@ -100,11 +91,7 @@ describe("validateTaskSubmissionFiles", () => { }); it("rejects oversized files", async () => { - const oversized = new File([ - new Uint8Array(MAX_TASK_SUBMISSION_FILE_SIZE_BYTES + 1), - ], "oversized.pdf", { - type: "application/pdf", - }); + const oversized = createOversizedFile(MAX_TASK_SUBMISSION_FILE_SIZE_BYTES + 1); const result = await validateTaskSubmissionFiles([oversized]); @@ -132,14 +119,10 @@ describe("extractTaskSubmissionFiles", () => { describe("POST /api/task-submissions/validate", () => { it("returns validation metadata for valid uploads", async () => { - const formData = new FormData(); - formData.append("files", createPdfFile()); + const formData = createFileValidationFormData(createPdfFile()); const response = await POST( - new Request("http://localhost/api/task-submissions/validate", { - method: "POST", - body: formData, - }), + createFormDataPostRequest("http://localhost/api/task-submissions/validate", formData), ); expect(response.status).toBe(200); @@ -162,10 +145,7 @@ describe("POST /api/task-submissions/validate", () => { formData.append("taskId", "123"); const response = await POST( - new Request("http://localhost/api/task-submissions/validate", { - method: "POST", - body: formData, - }), + createFormDataPostRequest("http://localhost/api/task-submissions/validate", formData), ); expect(response.status).toBe(400); diff --git a/frontend/src/lib/taskValidation.test.ts b/frontend/src/lib/taskValidation.test.ts index 1d11542..bf37913 100644 --- a/frontend/src/lib/taskValidation.test.ts +++ b/frontend/src/lib/taskValidation.test.ts @@ -1,15 +1,16 @@ import { describe, it, expect } from 'vitest'; import { taskSchema } from './taskValidation'; +import { + INVALID_TASK_SCHEMA_NEGATIVE_REWARD, + INVALID_TASK_SCHEMA_PAST_DEADLINE, + INVALID_TASK_SCHEMA_PRESENT_DEADLINE, + INVALID_TASK_SCHEMA_ZERO_REWARD, + VALID_TASK_SCHEMA_DATA, +} from '@/test/mock-data'; +import { futureDateIso, pastDateIso } from '@/test/fixtures'; describe('taskValidation schema', () => { - const validData = { - title: 'Valid Task Title', - description: 'This is a valid task description.', - tokenAddress: 'GBVVRXLMNCJTIGXBP2K3C6KHK6BOMW2G5B2ZNYAQUV4K3QY4K6SDBPQD', - reward: 100, - deadline: new Date(Date.now() + 86400000).toISOString(), // Tomorrow - maxSubmissions: 5, - }; + const validData = VALID_TASK_SCHEMA_DATA; it('should pass with valid data (happy path)', () => { const result = taskSchema.safeParse(validData); @@ -17,11 +18,8 @@ describe('taskValidation schema', () => { }); it('should reject when reward is zero or negative', () => { - const invalidZero = { ...validData, reward: 0 }; - const invalidNegative = { ...validData, reward: -50 }; - - const resultZero = taskSchema.safeParse(invalidZero); - const resultNegative = taskSchema.safeParse(invalidNegative); + const resultZero = taskSchema.safeParse(INVALID_TASK_SCHEMA_ZERO_REWARD); + const resultNegative = taskSchema.safeParse(INVALID_TASK_SCHEMA_NEGATIVE_REWARD); expect(resultZero.success).toBe(false); expect(resultNegative.success).toBe(false); @@ -30,7 +28,7 @@ describe('taskValidation schema', () => { it('should reject when deadline is in the past or present', () => { const invalidPast = { ...validData, - deadline: new Date(Date.now() - 86400000).toISOString(), // Yesterday + deadline: pastDateIso(), // Yesterday }; const invalidPresent = { diff --git a/frontend/src/test/README.md b/frontend/src/test/README.md new file mode 100644 index 0000000..7b26f59 --- /dev/null +++ b/frontend/src/test/README.md @@ -0,0 +1,185 @@ +# Test Fixtures and Mock Data + +This directory contains reusable test utilities to reduce duplicated setup code across the test suite. + +## Files + +- **fixtures.ts** - Helper functions for common test scenarios +- **mock-data.ts** - Pre-configured mock data objects for testing + +## Fixtures (`fixtures.ts`) + +### File Creation Helpers + +- `createPdfFile(name?)` - Creates a mock PDF file with valid magic bytes +- `createZipFile(name?)` - Creates a mock ZIP file with valid magic bytes +- `createMarkdownFile(name?, content?)` - Creates a mock markdown file +- `createOversizedFile(size, name?)` - Creates an oversized file for testing size limits + +### Date/Time Helpers + +- `futureDeadline(offsetSeconds?)` - Returns a Unix timestamp for a future deadline (default: 1 day) +- `pastDeadline(offsetSeconds?)` - Returns a Unix timestamp for a past deadline (default: 1 hour) +- `futureDateIso(offsetMs?)` - Returns an ISO date string for a future date +- `pastDateIso(offsetMs?)` - Returns an ISO date string for a past date + +### API Route Context Helpers + +- `createRouteContext(params)` - Creates a mock route context object for Next.js API routes +- `taskRouteContext(taskId)` - Creates a task route context with a taskId + +### localStorage Mock Setup + +- `createLocalStorageMock()` - Creates a localStorage mock for Node.js test environments +- `setupLocalStorageMock(localStorageMock?)` - Sets up localStorage as a global mock (call in beforeEach) +- `cleanupLocalStorageMock()` - Cleans up localStorage global mocks (call in afterEach) + +### FormData Helpers + +- `createSubmissionFormData(contributor, description, workUrl, files?)` - Creates FormData with task submission data +- `createFileValidationFormData(file)` - Creates FormData for file validation + +### Request Helpers + +- `createRequest(url, options?)` - Creates a mock Request object for API route testing +- `createJsonPostRequest(url, body)` - Creates a POST request with JSON body +- `createFormDataPostRequest(url, formData)` - Creates a POST request with FormData body + +## Mock Data (`mock-data.ts`) + +### Stellar Addresses + +- `VALID_STELLAR_ADDRESS` - A valid Stellar public key (56 chars, starts with G) +- `STANDARD_STELLAR_ADDRESS` - Standard-format G... address with valid base32 characters +- `INVALID_STELLAR_ADDRESS_*` - Various invalid Stellar addresses for testing validation +- `POSTER_ADDRESS` - Mock poster address for task creation tests +- `CONTRIBUTOR_ADDRESS` - Mock contributor address for submission tests + +### Email Addresses + +- `VALID_EMAIL` - A valid email address +- `VALID_EMAIL_SUBDOMAIN` - Valid email with subdomain +- `VALID_EMAIL_PLUS` - Valid email with plus sign +- `INVALID_EMAIL_*` - Various invalid email addresses for testing validation + +### Task Data + +- `VALID_TASK_DATA` - A valid task объект for task creation tests +- `VALID_TASK_DATA_WITH_OPTIONALS` - Valid task with all optional fields +- `INVALID_TASK_*` - Various invalid task data objects for testing validation +- `VALID_TASK_SCHEMA_DATA` - Task data for Zod schema validation tests +- `INVALID_TASK_SCHEMA_*` - Invalid task schema data for testing + +### Work Submissions + +- `VALID_WORK_SUBMISSION` - A valid work submission form object +- `VALID_WORK_SUBMISSION_WITH_CONTRIBUTOR` - Valid submission with contributor address +- `INVALID_WORK_SUBMISSION_*` - Various invalid work submission objects + +### URLs + +- `VALID_HTTPS_URL` - A valid HTTPS URL +- `VALID_IPFS_URL` - A valid IPFS URL +- `VALID_ARWEAVE_URL` - A valid Arweave URL +- `INVALID_URL_*` - Various invalid URLs for testing validation + +### Contributor Profiles + +- `PARTIAL_CONTRIBUTOR_PROFILE` - A partially complete contributor profile +- `COMPLETE_CONTRIBUTOR_PROFILE` - A fully complete contributor profile +- `PROFILE_FIELD_DEFINITIONS` - Profile field definitions for completion calculation + +### Notification Preferences + +- `DEFAULT_NOTIFICATION_PREFS` - Default notification preferences (all enabled) +- `MODIFIED_NOTIFICATION_PREFS` - Modified preferences (some disabled) +- `ALL_DISABLED_NOTIFICATION_PREFS` - All notification preferences disabled + +### Auth Credentials + +- `VALID_USER_CREDENTIALS` - Valid user credentials for auth tests +- `VALID_USER_CREDENTIALS_2` - Another valid user for testing multiple users +- `VALID_USER_CREDENTIALS_3` - Another valid user for testing session expiration +- `INVALID_PASSWORD` - Invalid password for authentication failure tests + +### Dashboard Stats + +- `EXPECTED_DASHBOARD_STATS` - Expected dashboard statistics values +- `EXPECTED_DASHBOARD_GROUP` - Expected dashboard group data + +## Usage Examples + +### Using File Helpers + +```typescript +import { createPdfFile, createZipFile } from "@/test/fixtures"; + +const pdf = createPdfFile("submission.pdf"); +const zip = createZipFile("archive.zip"); +``` + +### Using Date Helpers + +```typescript +import { futureDeadline, pastDeadline } from "@/test/fixtures"; + +const future = futureDeadline(86400); // 1 day from now +const past = pastDeadline(3600); // 1 hour ago +``` + +### Using Mock Data + +```typescript +import { VALID_TASK_DATA, INVALID_TASK_EMPTY_TITLE } from "@/test/mock-data"; + +// Use valid data +const result = validateCreateTaskForm(VALID_TASK_DATA); + +// Use invalid data for testing error cases +const errorResult = validateCreateTaskForm(INVALID_TASK_EMPTY_TITLE); +``` + +### Using localStorage Mock + +```typescript +import { createLocalStorageMock, setupLocalStorageMock, cleanupLocalStorageMock } from "@/test/fixtures"; + +describe("my test suite", () => { + const localStorageMock = createLocalStorageMock(); + + beforeEach(() => { + setupLocalStorageMock(localStorageMock); + }); + + afterEach(() => { + cleanupLocalStorageMock(); + }); + + it("tests localStorage behavior", () => { + localStorageMock.setItem("key", "value"); + expect(localStorageMock.getItem("key")).toBe("value"); + }); +}); +``` + +### Using FormData Helpers + +```typescript +import { createSubmissionFormData, createPdfFile } from "@/test/fixtures"; +import { CONTRIBUTOR_ADDRESS } from "@/test/mock-data"; + +const formData = createSubmissionFormData( + CONTRIBUTOR_ADDRESS, + "My submission description", + "https://github.com/user/repo", + createPdfFile() +); +``` + +## Benefits + +- **Reduced duplication**: Common test setup code is centralized +- **Improved readability**: Tests are more concise and focused on behavior +- **Easier maintenance**: Changes to test data only need to be made in one place +- **Consistency**: All tests use the same mock data format +- **Type safety**: TypeScript provides autocomplete and type checking diff --git a/frontend/src/test/fixtures.ts b/frontend/src/test/fixtures.ts new file mode 100644 index 0000000..e84f8a6 --- /dev/null +++ b/frontend/src/test/fixtures.ts @@ -0,0 +1,293 @@ +/** + * Reusable test fixtures for common testing scenarios. + * + * This file provides helper functions and setup utilities to reduce + * duplicated test code across the test suite. + */ + +import { vi } from "vitest"; + +// ============================================================================ +// File Creation Helpers +// ============================================================================ + +/** + * Creates a mock PDF file with valid PDF magic bytes. + * + * @param name - The filename for the PDF file (default: "submission.pdf") + * @returns A File object representing a PDF + */ +export function createPdfFile(name = "submission.pdf"): File { + return new File( + [ + new Uint8Array([0x25, 0x50, 0x44, 0x46, 0x2d, 0x31, 0x2e, 0x37]), + "\nMock pdf payload", + ], + name, + { + type: "application/pdf", + }, + ); +} + +/** + * Creates a mock ZIP file with valid ZIP magic bytes. + * + * @param name - The filename for the ZIP file (default: "submission.zip") + * @returns A File object representing a ZIP + */ +export function createZipFile(name = "submission.zip"): File { + return new File( + [new Uint8Array([0x50, 0x4b, 0x03, 0x04, 0x14, 0x00])], + name, + { + type: "application/zip", + }, + ); +} + +/** + * Creates a mock markdown file. + * + * @param name - The filename for the markdown file (default: "proof.md") + * @param content - The content of the markdown file + * @returns A File object representing a markdown file + */ +export function createMarkdownFile( + name = "proof.md", + content = "# Task proof\nCompleted all requested changes.", +): File { + return new File([content], name, { + type: "application/octet-stream", + }); +} + +/** + * Creates an oversized file for testing size limits. + * + * @param size - The size of the file in bytes + * @param name - The filename for the oversized file (default: "oversized.pdf") + * @returns A File object of the specified size + */ +export function createOversizedFile( + size: number, + name = "oversized.pdf", +): File { + return new File([new Uint8Array(size)], name, { + type: "application/pdf", + }); +} + +// ============================================================================ +// Date/Time Helpers +// ============================================================================ + +/** + * Returns a Unix timestamp for a future deadline. + * + * @param offsetSeconds - Seconds from now for the deadline (default: 86400 = 1 day) + * @returns Unix timestamp as a number + */ +export function futureDeadline(offsetSeconds = 86_400): number { + return Math.floor(Date.now() / 1000) + offsetSeconds; +} + +/** + * Returns a Unix timestamp for a past deadline. + * + * @param offsetSeconds - Seconds in the past (default: 3600 = 1 hour ago) + * @returns Unix timestamp as a number + */ +export function pastDeadline(offsetSeconds = 3600): number { + return Math.floor(Date.now() / 1000) - offsetSeconds; +} + +/** + * Returns an ISO date string for a future date. + * + * @param offsetMs - Milliseconds from now (default: 86400000 = 1 day) + * @returns ISO date string + */ +export function futureDateIso(offsetMs = 86_400_000): string { + return new Date(Date.now() + offsetMs).toISOString(); +} + +/** + * Returns an ISO date string for a past date. + * + * @param offsetMs - Milliseconds in the past (default: 86400000 = 1 day ago) + * @returns ISO date string + */ +export function pastDateIso(offsetMs = 86_400_000): string { + return new Date(Date.now() - offsetMs).toISOString(); +} + +// ============================================================================ +// API Route Context Helpers +// ============================================================================ + +/** + * Creates a mock route context object for Next.js API routes. + * + * @param params - The route parameters object + * @returns A mock route context with params as a Promise + */ +export function createRouteContext(params: Record) { + return { params: Promise.resolve(params) }; +} + +/** + * Creates a task route context with a taskId. + * + * @param taskId - The task ID for the route + * @returns A mock route context for task routes + */ +export function taskRouteContext(taskId: string) { + return { params: Promise.resolve({ taskId }) }; +} + +// ============================================================================ +// localStorage Mock Setup +// ============================================================================ + +/** + * Creates a localStorage mock for Node.js test environments. + * + * @returns A localStorage mock object with getItem, setItem, removeItem, clear, length, and key + */ +export function createLocalStorageMock() { + const store: Record = {}; + + return { + getItem: vi.fn((key: string) => store[key] ?? null), + setItem: vi.fn((key: string, value: string) => { + store[key] = value; + }), + removeItem: vi.fn((key: string) => { + delete store[key]; + }), + clear: vi.fn(() => { + Object.keys(store).forEach((k) => delete store[k]); + }), + get length() { + return Object.keys(store).length; + }, + key: vi.fn((index: number) => Object.keys(store)[index] ?? null), + }; +} + +/** + * Sets up localStorage as a global mock in the test environment. + * This should be called in beforeEach hooks. + * + * @param localStorageMock - The localStorage mock to use (creates one if not provided) + */ +export function setupLocalStorageMock(localStorageMock = createLocalStorageMock()) { + vi.stubGlobal("window", { localStorage: localStorageMock }); + vi.stubGlobal("localStorage", localStorageMock); + return localStorageMock; +} + +/** + * Cleans up localStorage global mocks. + * This should be called in afterEach hooks. + */ +export function cleanupLocalStorageMock() { + vi.unstubAllGlobals(); +} + +// ============================================================================ +// FormData Helpers +// ============================================================================ + +/** + * Creates a FormData object with task submission data. + * + * @param contributor - The contributor's Stellar address + * @param description - The submission description + * @param workUrl - The URL to the work + * @param files - Optional files to attach + * @returns A FormData object populated with submission data + */ +export function createSubmissionFormData( + contributor: string, + description: string, + workUrl: string, + files?: File | File[], +): FormData { + const formData = new FormData(); + formData.append("contributor", contributor); + formData.append("description", description); + formData.append("workUrl", workUrl); + + if (files) { + if (Array.isArray(files)) { + files.forEach((file) => formData.append("files", file)); + } else { + formData.append("files", files); + } + } + + return formData; +} + +/** + * Creates a minimal FormData object for task submission validation. + * + * @param file - The file to validate + * @returns A FormData object with a file + */ +export function createFileValidationFormData(file: File): FormData { + const formData = new FormData(); + formData.append("files", file); + return formData; +} + +// ============================================================================ +// Request Helpers +// ============================================================================ + +/** + * Creates a mock Request object for API route testing. + * + * @param url - The request URL + * @param options - RequestInit options (method, headers, body, etc.) + * @returns A Request object + */ +export function createRequest( + url: string, + options: RequestInit = {}, +): Request { + return new Request(url, { + headers: { "Content-Type": "application/json" }, + ...options, + }); +} + +/** + * Creates a POST request with JSON body. + * + * @param url - The request URL + * @param body - The JSON body object + * @returns A Request object with POST method and JSON body + */ +export function createJsonPostRequest(url: string, body: Record): Request { + return createRequest(url, { + method: "POST", + body: JSON.stringify(body), + }); +} + +/** + * Creates a POST request with FormData body. + * + * @param url - The request URL + * @param formData - The FormData body + * @returns A Request object with POST method and FormData body + */ +export function createFormDataPostRequest(url: string, formData: FormData): Request { + return new Request(url, { + method: "POST", + body: formData, + }); +} diff --git a/frontend/src/test/mock-data.ts b/frontend/src/test/mock-data.ts new file mode 100644 index 0000000..d78f11f --- /dev/null +++ b/frontend/src/test/mock-data.ts @@ -0,0 +1,494 @@ +/** + * Reusable mock data objects for common testing scenarios. + * + * This file provides pre-configured mock data objects to reduce + * duplicated test data across the test suite. + */ + +// ============================================================================ +// Stellar Address Mocks +// ============================================================================ + +/** + * A valid Stellar public key (56 characters, starts with G). + */ +export const VALID_STELLAR_ADDRESS = "GBDIT6QJ3HYH6C7OAVJ4XKZXONJ6PUVL2PVIOQHHGLK6M2S6TQXAAAAAAAA"; + +/** + * A standard-format G... address of 56 chars with valid base32 characters. + */ +export const STANDARD_STELLAR_ADDRESS = "GABCDEFGHIJKLMNOPQRSTUVWXYZ234567ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; + +/** + * An invalid Stellar address (doesn't start with G). + */ +export const INVALID_STELLAR_ADDRESS_NO_G = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567ABCDEFGHIJKLMNOPQRSTUVWXYZ234567ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; + +/** + * An invalid Stellar address (too short). + */ +export const INVALID_STELLAR_ADDRESS_TOO_SHORT = "GABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; + +/** + * An invalid Stellar address (too long). + */ +export const INVALID_STELLAR_ADDRESS_TOO_LONG = "GABCDEFGHIJKLMNOPQRSTUVWXYZ234567ABCDEFGHIJKLMNOPQRSTUVWXYZ234567EXTRA"; + +/** + * An invalid Stellar address with lowercase characters. + */ +export const INVALID_STELLAR_ADDRESS_LOWERCASE = "gABCDEFGHIJKLMNOPQRSTUVWXYZ234567ABCDEFGHIJKLMNOPQRSTUVWXYZ23456"; + +/** + * An invalid Stellar address with ambiguous characters (contains '0'). + */ +export const INVALID_STELLAR_ADDRESS_AMBIGUOUS = "GABCDEFGHIJKLMNOPQRSTUVWXYZ234567ABCDEFGHIJKLMNOPQRSTUVWXYZ234560"; + +/** + * Mock poster address for task creation tests. + */ +export const POSTER_ADDRESS = "GPOSTER1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + +/** + * Mock contributor address for submission tests. + */ +export const CONTRIBUTOR_ADDRESS = "GCONTRIB1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + +// ============================================================================ +// Email Mocks +// ============================================================================ + +/** + * A valid email address. + */ +export const VALID_EMAIL = "user@example.com"; + +/** + * A valid email with subdomain. + */ +export const VALID_EMAIL_SUBDOMAIN = "user@sub.example.com"; + +/** + * A valid email with plus sign. + */ +export const VALID_EMAIL_PLUS = "user+tag@example.com"; + +/** + * An invalid email (no @ symbol). + */ +export const INVALID_EMAIL_NO_AT = "notanemail"; + +/** + * An invalid email (no domain). + */ +export const INVALID_EMAIL_NO_DOMAIN = "user@"; + +/** + * An invalid email (no name). + */ +export const INVALID_EMAIL_NO_NAME = "@domain.com"; + +/** + * An extremely long email that should be rejected. + */ +export const INVALID_EMAIL_TOO_LONG = "a".repeat(250) + "@b.com"; + +// ============================================================================ +// Task Data Mocks +// ============================================================================ + +/** + * A valid task object for task creation tests. + */ +export const VALID_TASK_DATA = { + title: "Build a DEX Interface", + description: "Create a React frontend for Stellar DEX with swap UI, wallet integration, and transaction history.", + reward: "100", + deadline: String(Math.floor(Date.now() / 1000) + 30 * 24 * 60 * 60), // 30 days + maxSubmissions: "3", +}; + +/** + * A valid task object with all optional fields. + */ +export const VALID_TASK_DATA_WITH_OPTIONALS = { + ...VALID_TASK_DATA, + token: STANDARD_STELLAR_ADDRESS, + posterAddress: POSTER_ADDRESS, +}; + +/** + * Invalid task data with empty title. + */ +export const INVALID_TASK_EMPTY_TITLE = { + ...VALID_TASK_DATA, + title: "", +}; + +/** + * Invalid task data with short title. + */ +export const INVALID_TASK_SHORT_TITLE = { + ...VALID_TASK_DATA, + title: "ab", +}; + +/** + * Invalid task data with empty description. + */ +export const INVALID_TASK_EMPTY_DESCRIPTION = { + ...VALID_TASK_DATA, + description: "", +}; + +/** + * Invalid task data with short description. + */ +export const INVALID_TASK_SHORT_DESCRIPTION = { + ...VALID_TASK_DATA, + description: "short", +}; + +/** + * Invalid task data with zero reward. + */ +export const INVALID_TASK_ZERO_REWARD = { + ...VALID_TASK_DATA, + reward: "0", +}; + +/** + * Invalid task data with negative reward. + */ +export const INVALID_TASK_NEGATIVE_REWARD = { + ...VALID_TASK_DATA, + reward: "-50", +}; + +/** + * Invalid task data with reward below minimum (0.09 XLM). + */ +export const INVALID_TASK_LOW_REWARD = { + ...VALID_TASK_DATA, + reward: "0.09", +}; + +/** + * Invalid task data with past deadline. + */ +export const INVALID_TASK_PAST_DEADLINE = { + ...VALID_TASK_DATA, + deadline: String(Math.floor(Date.now() / 1000) - 3600), +}; + +/** + * Invalid task data with zero max submissions. + */ +export const INVALID_TASK_ZERO_SUBMISSIONS = { + ...VALID_TASK_DATA, + maxSubmissions: "0", +}; + +/** + * Invalid task data with decimal max submissions. + */ +export const INVALID_TASK_DECIMAL_SUBMISSIONS = { + ...VALID_TASK_DATA, + maxSubmissions: "3.5", +}; + +/** + * Invalid task data with invalid token address. + */ +export const INVALID_TASK_BAD_TOKEN = { + ...VALID_TASK_DATA, + token: "invalid-token", +}; + +/** + * Invalid task data with invalid poster address. + */ +export const INVALID_TASK_BAD_POSTER = { + ...VALID_TASK_DATA, + posterAddress: "bad-address", +}; + +/** + * Task data for Zod schema validation tests. + */ +export const VALID_TASK_SCHEMA_DATA = { + title: "Valid Task Title", + description: "This is a valid task description.", + tokenAddress: "GBVVRXLMNCJTIGXBP2K3C6KHK6BOMW2G5B2ZNYAQUV4K3QY4K6SDBPQD", + reward: 100, + deadline: new Date(Date.now() + 86400000).toISOString(), // Tomorrow + maxSubmissions: 5, +}; + +/** + * Invalid task schema data with zero reward. + */ +export const INVALID_TASK_SCHEMA_ZERO_REWARD = { + ...VALID_TASK_SCHEMA_DATA, + reward: 0, +}; + +/** + * Invalid task schema data with negative reward. + */ +export const INVALID_TASK_SCHEMA_NEGATIVE_REWARD = { + ...VALID_TASK_SCHEMA_DATA, + reward: -50, +}; + +/** + * Invalid task schema data with past deadline. + */ +export const INVALID_TASK_SCHEMA_PAST_DEADLINE = { + ...VALID_TASK_SCHEMA_DATA, + deadline: new Date(Date.now() - 86400000).toISOString(), // Yesterday +}; + +/** + * Invalid task schema data with present deadline. + */ +export const INVALID_TASK_SCHEMA_PRESENT_DEADLINE = { + ...VALID_TASK_SCHEMA_DATA, + deadline: new Date(Date.now() - 1000).toISOString(), // Just a second ago +}; + +/** + * Invalid task schema data with missing required fields. + */ +export const INVALID_TASK_SCHEMA_MISSING_FIELDS = { + title: "Valid Task Title", + // missing description and others +}; + +// ============================================================================ +// Work Submission Mocks +// ============================================================================ + +/** + * A valid work submission form object. + */ +export const VALID_WORK_SUBMISSION = { + workUrl: "https://github.com/user/task-submission", + description: "Implemented all required features for the DEX interface including swap, liquidity pools, and wallet integration.", +}; + +/** + * Valid work submission with contributor address. + */ +export const VALID_WORK_SUBMISSION_WITH_CONTRIBUTOR = { + ...VALID_WORK_SUBMISSION, + contributorAddress: CONTRIBUTOR_ADDRESS, +}; + +/** + * Invalid work submission with empty work URL. + */ +export const INVALID_WORK_SUBMISSION_EMPTY_URL = { + ...VALID_WORK_SUBMISSION, + workUrl: "", +}; + +/** + * Invalid work submission with invalid URL. + */ +export const INVALID_WORK_SUBMISSION_INVALID_URL = { + ...VALID_WORK_SUBMISSION, + workUrl: "not-a-url", +}; + +/** + * Invalid work submission with empty description. + */ +export const INVALID_WORK_SUBMISSION_EMPTY_DESCRIPTION = { + ...VALID_WORK_SUBMISSION, + description: "", +}; + +/** + * Invalid work submission with short description. + */ +export const INVALID_WORK_SUBMISSION_SHORT_DESCRIPTION = { + ...VALID_WORK_SUBMISSION, + description: "short", +}; + +/** + * Invalid work submission with bad contributor address. + */ +export const INVALID_WORK_SUBMISSION_BAD_CONTRIBUTOR = { + ...VALID_WORK_SUBMISSION, + contributorAddress: "bad", +}; + +// ============================================================================ +// URL Mocks +// ============================================================================ + +/** + * A valid HTTPS URL. + */ +export const VALID_HTTPS_URL = "https://github.com/user/repo"; + +/** + * A valid IPFS URL. + */ +export const VALID_IPFS_URL = "ipfs://QmXxxx1234ABCDEF"; + +/** + * A valid Arweave URL. + */ +export const VALID_ARWEAVE_URL = "ar://txid12345abcdef"; + +/** + * An invalid URL (not a URL). + */ +export const INVALID_URL = "not-a-url"; + +/** + * An invalid URL with unsupported protocol (FTP). + */ +export const INVALID_URL_FTP = "ftp://example.com/file"; + +/** + * A URL that's too short. + */ +export const INVALID_URL_TOO_SHORT = "https://a.b"; + +// ============================================================================ +// Contributor Profile Mocks +// ============================================================================ + +/** + * A partially complete contributor profile. + */ +export const PARTIAL_CONTRIBUTOR_PROFILE = { + name: "Ada Lovelace", + headline: "Engineer", + bio: "", + location: "", + skills: "", + website: "", +}; + +/** + * A fully complete contributor profile. + */ +export const COMPLETE_CONTRIBUTOR_PROFILE = { + name: "Ada Lovelace", + headline: "Engineer", + bio: "First computer programmer", + location: "London", + skills: "Mathematics, Programming", + website: "https://adalovelace.example.com", +}; + +/** + * Profile field definitions for completion calculation. + */ +export const PROFILE_FIELD_DEFINITIONS = [ + { key: "name", label: "Full name" }, + { key: "headline", label: "Headline" }, + { key: "bio", label: "Bio" }, + { key: "location", label: "Location" }, + { key: "skills", label: "Skills" }, + { key: "website", label: "Website" }, +]; + +// ============================================================================ +// Notification Preferences Mocks +// ============================================================================ + +/** + * Default notification preferences (all enabled). + */ +export const DEFAULT_NOTIFICATION_PREFS = { + task_updates: true, + submission_activity: true, + payments: true, + disputes: true, + platform_announcements: true, +}; + +/** + * Modified notification preferences (some disabled). + */ +export const MODIFIED_NOTIFICATION_PREFS = { + ...DEFAULT_NOTIFICATION_PREFS, + payments: false, + disputes: false, +}; + +/** + * All notification preferences disabled. + */ +export const ALL_DISABLED_NOTIFICATION_PREFS = { + task_updates: false, + submission_activity: false, + payments: false, + disputes: false, + platform_announcements: false, +}; + +// ============================================================================ +// Auth Mocks +// ============================================================================ + +/** + * Valid user credentials for auth tests. + */ +export const VALID_USER_CREDENTIALS = { + email: "alice@example.com", + password: "super-secret", +}; + +/** + * Another valid user for testing multiple users. + */ +export const VALID_USER_CREDENTIALS_2 = { + email: "bob@example.com", + password: "correct-horse-battery-staple", +}; + +/** + * Another valid user for testing session expiration. + */ +export const VALID_USER_CREDENTIALS_3 = { + email: "carol@example.com", + password: "very-secret", +}; + +/** + * Invalid password for authentication failure tests. + */ +export const INVALID_PASSWORD = "wrong-password"; + +// ============================================================================ +// Dashboard Stats Mocks +// ============================================================================ + +/** + * Expected dashboard statistics values. + */ +export const EXPECTED_DASHBOARD_STATS = { + activeGroupCount: 5, + totalFunds: 70050, + totalMembers: 35, + maxFunds: 24500, + totalTransactions: 118, +}; + +/** + * Expected dashboard group data. + */ +export const EXPECTED_DASHBOARD_GROUP = { + id: "1", + name: "Paymesh Core", + totalFunds: 24500, + members: 8, + activity: "high", +};