Skip to content
Closed
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
88 changes: 88 additions & 0 deletions src/app/dashboard/referrals/page.invites.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
// @vitest-environment jsdom

import { act, fireEvent, render, screen, waitFor } from "@testing-library/react";
import { afterEach, describe, expect, it, vi } from "vitest";
import ReferralsPage from "./page";

function mockFetchForInviteFailure(failure: "network" | "invalid-json") {
vi.stubGlobal(
"fetch",
vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
const url = input.toString();
if (url === "/api/referrals/code") {
return {
ok: true,
json: async () => ({
code: "alice",
link: "https://ugig.net/?ref=alice",
}),
};
}

if (url === "/api/referrals" && init?.method === "POST") {
if (failure === "network") {
throw new Error("connection reset");
}

return {
ok: false,
json: async () => {
throw new Error("invalid json");
},
};
}

return {
ok: true,
json: async () => ({
data: [],
stats: {
total_invited: 0,
total_registered: 0,
conversion_rate: 0,
},
}),
};
})
);
}

async function submitInvite() {
render(<ReferralsPage />);

const textarea = await screen.findByPlaceholderText(
"Enter email addresses separated by commas or new lines"
);
await act(async () => {
fireEvent.change(textarea, { target: { value: "friend@example.com" } });
fireEvent.click(screen.getByRole("button", { name: /send invites/i }));
});
}

describe("ReferralsPage invite error recovery", () => {
afterEach(() => {
vi.unstubAllGlobals();
});

it("recovers from invite network errors", async () => {
Comment on lines +63 to +67

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Missing explicit cleanup between tests

afterEach only calls vi.unstubAllGlobals() — it never calls cleanup() from @testing-library/react. Auto-cleanup relies on @testing-library/react detecting a globally-available afterEach, which only happens when vitest's globals: true option is set. If the project runs vitest with globals disabled, the rendered <ReferralsPage /> from the first test remains mounted in the JSDOM document when the second test runs, and screen queries in the second test will see both instances, which could produce false positives. Adding import { cleanup } from "@testing-library/react" and cleanup() inside afterEach makes the teardown explicit and independent of the globals configuration.

mockFetchForInviteFailure("network");

await submitInvite();

await waitFor(() => {
expect(screen.getByText("Failed to send invites. Please try again.")).toBeTruthy();
});
expect(screen.getByRole("button", { name: /send invites/i })).not.toBeDisabled();
});

it("recovers from non-json invite error responses", async () => {
mockFetchForInviteFailure("invalid-json");

await submitInvite();

await waitFor(() => {
expect(screen.getByText("Failed to send invites")).toBeTruthy();
});
expect(screen.getByRole("button", { name: /send invites/i })).not.toBeDisabled();
});
});
43 changes: 24 additions & 19 deletions src/app/dashboard/referrals/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -84,27 +84,32 @@ export default function ReferralsPage() {
return;
}

const res = await fetch("/api/referrals", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ emails: emailList }),
});

const data = await res.json();

if (!res.ok) {
setError(data.error);
} else {
setSuccess(data.message);
setEmails("");
loadReferrals().then((d) => {
if (d) {
setReferrals(d.data || []);
setStats(d.stats);
}
try {
const res = await fetch("/api/referrals", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ emails: emailList }),
});

const data = await res.json().catch(() => null);

if (!res.ok) {
setError(data?.error || "Failed to send invites");
} else {
setSuccess(data?.message || "Invites sent");
setEmails("");
loadReferrals().then((d) => {
if (d) {
setReferrals(d.data || []);
setStats(d.stats);
}
});
}
} catch {
setError("Failed to send invites. Please try again.");
} finally {
setSending(false);
}
setSending(false);
};

const statusBadge = (status: string) => {
Expand Down
Loading