Skip to content

Commit 7c73a1f

Browse files
authored
Merge pull request #137 from DavisVT/refactor/test-fixtures-mock-data
Refactor test suite with shared fixtures and mock data
2 parents 4ebaabe + e188ad7 commit 7c73a1f

10 files changed

Lines changed: 1089 additions & 187 deletions

frontend/src/hooks/stellar-wallets-kit.test.ts

Lines changed: 8 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@
55
* run in a pure Node environment without a real Stellar wallet or browser extension.
66
*/
77
import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
8+
import {
9+
cleanupLocalStorageMock,
10+
createLocalStorageMock,
11+
setupLocalStorageMock,
12+
} from "@/test/fixtures";
813

914
// ---------------------------------------------------------------------------
1015
// Mocks — declared before vi.mock() so they are available in the factory
@@ -49,19 +54,7 @@ vi.mock("@/lib/env", () => ({
4954
// localStorage stub
5055
// ---------------------------------------------------------------------------
5156

52-
const localStore: Record<string, string> = {};
53-
const localStorageMock = {
54-
getItem: vi.fn((key: string) => localStore[key] ?? null),
55-
setItem: vi.fn((key: string, value: string) => {
56-
localStore[key] = value;
57-
}),
58-
removeItem: vi.fn((key: string) => {
59-
delete localStore[key];
60-
}),
61-
clear: vi.fn(() => {
62-
Object.keys(localStore).forEach((k) => delete localStore[k]);
63-
}),
64-
};
57+
const localStorageMock = createLocalStorageMock();
6558

6659
// ---------------------------------------------------------------------------
6760
// Setup / teardown
@@ -73,12 +66,11 @@ beforeEach(() => {
7366
localStorageMock.clear();
7467

7568
// Stub window so `typeof window !== "undefined"` is true in the module
76-
vi.stubGlobal("window", { localStorage: localStorageMock });
77-
vi.stubGlobal("localStorage", localStorageMock);
69+
setupLocalStorageMock(localStorageMock);
7870
});
7971

8072
afterEach(() => {
81-
vi.unstubAllGlobals();
73+
cleanupLocalStorageMock();
8274
});
8375

8476
async function loadKit() {

frontend/src/hooks/useNotificationPreferences.test.ts

Lines changed: 19 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -17,41 +17,32 @@ import {
1717
NOTIFICATION_PREFS_KEY,
1818
type NotificationPreferences,
1919
} from "./useNotificationPreferences";
20+
import {
21+
cleanupLocalStorageMock,
22+
createLocalStorageMock,
23+
setupLocalStorageMock,
24+
} from "@/test/fixtures";
25+
import {
26+
DEFAULT_NOTIFICATION_PREFS,
27+
MODIFIED_NOTIFICATION_PREFS,
28+
} from "@/test/mock-data";
2029

2130
// ---------------------------------------------------------------------------
2231
// localStorage stub (node environment has no DOM)
2332
// ---------------------------------------------------------------------------
2433

25-
const store: Record<string, string> = {};
26-
27-
const localStorageMock = {
28-
getItem: vi.fn((key: string) => store[key] ?? null),
29-
setItem: vi.fn((key: string, value: string) => {
30-
store[key] = value;
31-
}),
32-
removeItem: vi.fn((key: string) => {
33-
delete store[key];
34-
}),
35-
clear: vi.fn(() => {
36-
Object.keys(store).forEach((k) => delete store[k]);
37-
}),
38-
get length() {
39-
return Object.keys(store).length;
40-
},
41-
key: vi.fn((index: number) => Object.keys(store)[index] ?? null),
42-
};
34+
const localStorageMock = createLocalStorageMock();
4335

4436
beforeEach(() => {
4537
// Expose the mock as globalThis.localStorage so the module's typeof window
4638
// check passes in the node test environment.
47-
vi.stubGlobal("window", { localStorage: localStorageMock });
48-
vi.stubGlobal("localStorage", localStorageMock);
39+
setupLocalStorageMock(localStorageMock);
4940
localStorageMock.clear();
5041
vi.clearAllMocks();
5142
});
5243

5344
afterEach(() => {
54-
vi.unstubAllGlobals();
45+
cleanupLocalStorageMock();
5546
});
5647

5748
// ---------------------------------------------------------------------------
@@ -112,11 +103,7 @@ describe("localStorage persistence helpers", () => {
112103
});
113104

114105
it("stores preferences as JSON and retrieves them correctly", async () => {
115-
const prefs: NotificationPreferences = {
116-
...DEFAULT_NOTIFICATION_PREFERENCES,
117-
payments: false,
118-
disputes: false,
119-
};
106+
const prefs: NotificationPreferences = MODIFIED_NOTIFICATION_PREFS;
120107

121108
// Manually simulate what saveToStorage does
122109
localStorageMock.setItem(NOTIFICATION_PREFS_KEY, JSON.stringify(prefs));
@@ -142,7 +129,7 @@ describe("localStorage persistence helpers", () => {
142129

143130
describe("preference mutation helpers", () => {
144131
it("toggle flips a single category", () => {
145-
const prefs: NotificationPreferences = { ...DEFAULT_NOTIFICATION_PREFERENCES };
132+
const prefs: NotificationPreferences = DEFAULT_NOTIFICATION_PREFS;
146133

147134
const toggled: NotificationPreferences = {
148135
...prefs,
@@ -154,7 +141,7 @@ describe("preference mutation helpers", () => {
154141
});
155142

156143
it("setAll(false) disables all categories", () => {
157-
const prefs: NotificationPreferences = { ...DEFAULT_NOTIFICATION_PREFERENCES };
144+
const prefs: NotificationPreferences = DEFAULT_NOTIFICATION_PREFS;
158145
const updated = { ...prefs } as NotificationPreferences;
159146

160147
for (const cat of NOTIFICATION_CATEGORIES) {
@@ -187,7 +174,7 @@ describe("preference mutation helpers", () => {
187174
});
188175

189176
it("update merges partial preferences over existing ones", () => {
190-
const prefs: NotificationPreferences = { ...DEFAULT_NOTIFICATION_PREFERENCES };
177+
const prefs: NotificationPreferences = DEFAULT_NOTIFICATION_PREFS;
191178
const partial: Partial<NotificationPreferences> = {
192179
payments: false,
193180
};
@@ -200,8 +187,10 @@ describe("preference mutation helpers", () => {
200187
});
201188

202189
it("reset restores defaults", () => {
190+
const modified: NotificationPreferences = MODIFIED_NOTIFICATION_PREFS;
191+
203192
// After reset we should get defaults back
204-
const afterReset: NotificationPreferences = { ...DEFAULT_NOTIFICATION_PREFERENCES };
193+
const afterReset: NotificationPreferences = DEFAULT_NOTIFICATION_PREFS;
205194

206195
for (const cat of NOTIFICATION_CATEGORIES) {
207196
expect(afterReset[cat]).toBe(DEFAULT_NOTIFICATION_PREFERENCES[cat]);

frontend/src/lib/contributor-profile.test.ts

Lines changed: 6 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,16 @@
11
import { describe, expect, it } from "vitest";
22

33
import { calculateContributorProfileCompletion } from "@/lib/contributor-profile";
4+
import {
5+
PARTIAL_CONTRIBUTOR_PROFILE,
6+
PROFILE_FIELD_DEFINITIONS,
7+
} from "@/test/mock-data";
48

59
describe("calculateContributorProfileCompletion", () => {
610
it("returns completion percentage and missing fields", () => {
711
const result = calculateContributorProfileCompletion(
8-
{
9-
name: "Ada Lovelace",
10-
headline: "Engineer",
11-
bio: "",
12-
location: "",
13-
skills: "",
14-
website: "",
15-
},
16-
[
17-
{ key: "name", label: "Full name" },
18-
{ key: "headline", label: "Headline" },
19-
{ key: "bio", label: "Bio" },
20-
{ key: "location", label: "Location" },
21-
{ key: "skills", label: "Skills" },
22-
{ key: "website", label: "Website" },
23-
],
12+
PARTIAL_CONTRIBUTOR_PROFILE,
13+
PROFILE_FIELD_DEFINITIONS,
2414
);
2515

2616
expect(result.percentage).toBe(33);

frontend/src/lib/form-validation.test.ts

Lines changed: 41 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,27 @@ import {
2828
MIN_MAX_SUBMISSIONS,
2929
MAX_MAX_SUBMISSIONS,
3030
} from "./form-validation";
31+
import {
32+
INVALID_EMAIL_NO_AT,
33+
INVALID_TASK_BAD_POSTER,
34+
INVALID_TASK_BAD_TOKEN,
35+
INVALID_TASK_EMPTY_DESCRIPTION,
36+
INVALID_TASK_EMPTY_TITLE,
37+
INVALID_TASK_PAST_DEADLINE,
38+
INVALID_TASK_ZERO_REWARD,
39+
INVALID_TASK_ZERO_SUBMISSIONS,
40+
INVALID_WORK_SUBMISSION_BAD_CONTRIBUTOR,
41+
INVALID_WORK_SUBMISSION_EMPTY_DESCRIPTION,
42+
INVALID_WORK_SUBMISSION_EMPTY_URL,
43+
INVALID_WORK_SUBMISSION_INVALID_URL,
44+
STANDARD_STELLAR_ADDRESS,
45+
VALID_EMAIL,
46+
VALID_TASK_DATA,
47+
VALID_TASK_DATA_WITH_OPTIONALS,
48+
VALID_WORK_SUBMISSION,
49+
VALID_WORK_SUBMISSION_WITH_CONTRIBUTOR,
50+
} from "@/test/mock-data";
51+
import { futureDeadline, pastDeadline } from "@/test/fixtures";
3152

3253
// ============================================================================
3354
// validateRequired
@@ -688,14 +709,7 @@ describe("validateSubmissionDescription", () => {
688709
// ============================================================================
689710

690711
describe("validateCreateTaskForm", () => {
691-
const validForm = {
692-
title: "Build a DEX Interface",
693-
description:
694-
"Create a React frontend for Stellar DEX with swap UI, wallet integration, and transaction history.",
695-
reward: "100",
696-
deadline: String(Math.floor(Date.now() / 1000) + 30 * 24 * 60 * 60), // 30 days
697-
maxSubmissions: "3",
698-
};
712+
const validForm = VALID_TASK_DATA;
699713

700714
it("accepts a completely valid form", () => {
701715
const result = validateCreateTaskForm(validForm);
@@ -704,7 +718,7 @@ describe("validateCreateTaskForm", () => {
704718
});
705719

706720
it("rejects empty title", () => {
707-
const result = validateCreateTaskForm({ ...validForm, title: "" });
721+
const result = validateCreateTaskForm(INVALID_TASK_EMPTY_TITLE);
708722
expect(result.ok).toBe(false);
709723
expect(result.errors.title).toBeDefined();
710724
});
@@ -716,40 +730,40 @@ describe("validateCreateTaskForm", () => {
716730
});
717731

718732
it("rejects empty description", () => {
719-
const result = validateCreateTaskForm({ ...validForm, description: "" });
733+
const result = validateCreateTaskForm(INVALID_TASK_EMPTY_DESCRIPTION);
720734
expect(result.ok).toBe(false);
721735
expect(result.errors.description).toBeDefined();
722736
});
723737

724738
it("rejects invalid reward", () => {
725-
const result = validateCreateTaskForm({ ...validForm, reward: "0" });
739+
const result = validateCreateTaskForm(INVALID_TASK_ZERO_REWARD);
726740
expect(result.ok).toBe(false);
727741
expect(result.errors.reward).toBeDefined();
728742
});
729743

730744
it("rejects past deadline", () => {
731-
const past = String(Math.floor(Date.now() / 1000) - 3600);
732-
const result = validateCreateTaskForm({ ...validForm, deadline: past });
745+
const result = validateCreateTaskForm({
746+
...validForm,
747+
deadline: String(pastDeadline()),
748+
});
733749
expect(result.ok).toBe(false);
734750
expect(result.errors.deadline).toBeDefined();
735751
});
736752

737753
it("rejects invalid max submissions", () => {
738-
const result = validateCreateTaskForm({ ...validForm, maxSubmissions: "0" });
754+
const result = validateCreateTaskForm(INVALID_TASK_ZERO_SUBMISSIONS);
739755
expect(result.ok).toBe(false);
740756
expect(result.errors.maxSubmissions).toBeDefined();
741757
});
742758

743759
it("validates token address if provided", () => {
744-
const result = validateCreateTaskForm({
745-
...validForm,
746-
token: "invalid-token",
747-
});
760+
const result = validateCreateTaskForm(INVALID_TASK_BAD_TOKEN);
748761
expect(result.ok).toBe(false);
749762
expect(result.errors.token).toBeDefined();
750763
});
751764

752765
it("accepts valid token address", () => {
766+
const result = validateCreateTaskForm(VALID_TASK_DATA_WITH_OPTIONALS);
753767
const result = validateCreateTaskForm({
754768
...validForm,
755769
token: "GABCDEFGHIJKLMNOPQRSTUVWXYZ234567ABCDEFGHIJKLMNOPQRSTUVW",
@@ -763,10 +777,7 @@ describe("validateCreateTaskForm", () => {
763777
});
764778

765779
it("validates poster address if provided", () => {
766-
const result = validateCreateTaskForm({
767-
...validForm,
768-
posterAddress: "bad-address",
769-
});
780+
const result = validateCreateTaskForm(INVALID_TASK_BAD_POSTER);
770781
expect(result.ok).toBe(false);
771782
expect(result.errors.posterAddress).toBeDefined();
772783
});
@@ -793,46 +804,33 @@ describe("validateCreateTaskForm", () => {
793804
// ============================================================================
794805

795806
describe("validateWorkSubmissionForm", () => {
796-
const validForm = {
797-
workUrl: "https://github.com/user/task-submission",
798-
description:
799-
"Implemented all required features for the DEX interface including swap, liquidity pools, and wallet integration.",
800-
};
807+
const validForm = VALID_WORK_SUBMISSION;
801808

802809
it("accepts a completely valid submission form", () => {
803810
const result = validateWorkSubmissionForm(validForm);
804811
expect(result.ok).toBe(true);
805812
});
806813

807814
it("rejects empty work URL", () => {
808-
const result = validateWorkSubmissionForm({ ...validForm, workUrl: "" });
815+
const result = validateWorkSubmissionForm(INVALID_WORK_SUBMISSION_EMPTY_URL);
809816
expect(result.ok).toBe(false);
810817
expect(result.errors.workUrl).toBeDefined();
811818
});
812819

813820
it("rejects invalid work URL", () => {
814-
const result = validateWorkSubmissionForm({
815-
...validForm,
816-
workUrl: "not-a-url",
817-
});
821+
const result = validateWorkSubmissionForm(INVALID_WORK_SUBMISSION_INVALID_URL);
818822
expect(result.ok).toBe(false);
819823
expect(result.errors.workUrl).toBeDefined();
820824
});
821825

822826
it("rejects empty description", () => {
823-
const result = validateWorkSubmissionForm({
824-
...validForm,
825-
description: "",
826-
});
827+
const result = validateWorkSubmissionForm(INVALID_WORK_SUBMISSION_EMPTY_DESCRIPTION);
827828
expect(result.ok).toBe(false);
828829
expect(result.errors.description).toBeDefined();
829830
});
830831

831832
it("validates contributor address if provided", () => {
832-
const result = validateWorkSubmissionForm({
833-
...validForm,
834-
contributorAddress: "bad",
835-
});
833+
const result = validateWorkSubmissionForm(INVALID_WORK_SUBMISSION_BAD_CONTRIBUTOR);
836834
expect(result.ok).toBe(false);
837835
expect(result.errors.contributorAddress).toBeDefined();
838836
});
@@ -846,6 +844,7 @@ describe("validateWorkSubmissionForm", () => {
846844
});
847845

848846
it("accepts valid contributor address", () => {
847+
const result = validateWorkSubmissionForm(VALID_WORK_SUBMISSION_WITH_CONTRIBUTOR);
849848
const result = validateWorkSubmissionForm({
850849
...validForm,
851850
contributorAddress: "GABCDEFGHIJKLMNOPQRSTUVWXYZ234567ABCDEFGHIJKLMNOPQRSTUVW",
@@ -866,13 +865,13 @@ describe("validateWaitlistForm", () => {
866865
});
867866

868867
it("rejects invalid email", () => {
869-
const result = validateWaitlistForm("not-email");
868+
const result = validateWaitlistForm(INVALID_EMAIL_NO_AT);
870869
expect(result.ok).toBe(false);
871870
expect(result.error).toBe("Please enter a valid email address.");
872871
});
873872

874873
it("accepts valid email", () => {
875-
const result = validateWaitlistForm("user@example.com");
874+
const result = validateWaitlistForm(VALID_EMAIL);
876875
expect(result.ok).toBe(true);
877876
});
878877
});

0 commit comments

Comments
 (0)