Skip to content
Merged
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
24 changes: 8 additions & 16 deletions frontend/src/hooks/stellar-wallets-kit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -49,19 +54,7 @@ vi.mock("@/lib/env", () => ({
// localStorage stub
// ---------------------------------------------------------------------------

const localStore: Record<string, string> = {};
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
Expand All @@ -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() {
Expand Down
49 changes: 19 additions & 30 deletions frontend/src/hooks/useNotificationPreferences.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,41 +17,32 @@
NOTIFICATION_PREFS_KEY,
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<string, string> = {};

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

// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -112,11 +103,7 @@
});

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));
Expand All @@ -142,7 +129,7 @@

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,
Expand All @@ -154,7 +141,7 @@
});

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) {
Expand Down Expand Up @@ -187,7 +174,7 @@
});

it("update merges partial preferences over existing ones", () => {
const prefs: NotificationPreferences = { ...DEFAULT_NOTIFICATION_PREFERENCES };
const prefs: NotificationPreferences = DEFAULT_NOTIFICATION_PREFS;
const partial: Partial<NotificationPreferences> = {
payments: false,
};
Expand All @@ -200,8 +187,10 @@
});

it("reset restores defaults", () => {
const modified: NotificationPreferences = MODIFIED_NOTIFICATION_PREFS;

Check warning on line 190 in frontend/src/hooks/useNotificationPreferences.test.ts

View workflow job for this annotation

GitHub Actions / Frontend Build and Format Checks

'modified' is assigned a value but never used

// 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]);
Expand Down
22 changes: 6 additions & 16 deletions frontend/src/lib/contributor-profile.test.ts
Original file line number Diff line number Diff line change
@@ -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);
Expand Down
83 changes: 41 additions & 42 deletions frontend/src/lib/form-validation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,27 @@
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,

Check warning on line 37 in frontend/src/lib/form-validation.test.ts

View workflow job for this annotation

GitHub Actions / Frontend Build and Format Checks

'INVALID_TASK_PAST_DEADLINE' is defined but never used
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,

Check warning on line 44 in frontend/src/lib/form-validation.test.ts

View workflow job for this annotation

GitHub Actions / Frontend Build and Format Checks

'STANDARD_STELLAR_ADDRESS' is defined but never used
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";

Check warning on line 51 in frontend/src/lib/form-validation.test.ts

View workflow job for this annotation

GitHub Actions / Frontend Build and Format Checks

'futureDeadline' is defined but never used

// ============================================================================
// validateRequired
Expand Down Expand Up @@ -688,14 +709,7 @@
// ============================================================================

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);
Expand All @@ -704,7 +718,7 @@
});

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();
});
Expand All @@ -716,40 +730,40 @@
});

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(VALID_TASK_DATA_WITH_OPTIONALS);
const result = validateCreateTaskForm({
...validForm,
token: "GABCDEFGHIJKLMNOPQRSTUVWXYZ234567ABCDEFGHIJKLMNOPQRSTUVW",
Expand All @@ -763,10 +777,7 @@
});

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();
});
Expand All @@ -793,46 +804,33 @@
// ============================================================================

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);
expect(result.ok).toBe(true);
});

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();
});
Expand All @@ -846,6 +844,7 @@
});

it("accepts valid contributor address", () => {
const result = validateWorkSubmissionForm(VALID_WORK_SUBMISSION_WITH_CONTRIBUTOR);
const result = validateWorkSubmissionForm({
...validForm,
contributorAddress: "GABCDEFGHIJKLMNOPQRSTUVWXYZ234567ABCDEFGHIJKLMNOPQRSTUVW",
Expand All @@ -866,13 +865,13 @@
});

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);
});
});
Expand Down
Loading
Loading