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
116 changes: 101 additions & 15 deletions __tests__/admin/PrintRecordViews.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,27 +22,68 @@ vi.mock("@/lib/config/env", () => ({
},
}));

vi.mock("@/lib/actions/users/getUserById", () => ({
getUserById: vi.fn().mockResolvedValue({
user: {
_id: "usr_999",
name: "Zaynab Idris",
email: "zaynab@deenbridge.org",
role: "educator",
status: "active",
createdAt: "2025-03-01T00:00:00Z",
walletAddress: "GC3BDSVU7WAKCCGLTDTBQLP3Y4S7G45P6W6Y5Z2XJ3K4L5M6N7P8Q111",
bio: "Quran & Tajweed Educator",
purchases: [{ id: "p1", title: "Fiqh of Worship", amount: "42.00", date: "2026-01-01" }],
},
}),
const getUserByIdMock = vi.hoisted(() => vi.fn());
const fetchVerificationHistoryMock = vi.hoisted(() => vi.fn());

vi.mock("@/lib/actions/users/getUserById", () => ({ getUserById: getUserByIdMock }));
vi.mock("@/lib/actions/admin-verification-history", () => ({
fetchEducatorVerificationHistory: fetchVerificationHistoryMock,
}));

const educatorUser = {
_id: "usr_999",
name: "Zaynab Idris",
email: "zaynab@deenbridge.org",
role: "educator",
status: "active",
createdAt: "2025-03-01T00:00:00Z",
walletAddress: "GC3BDSVU7WAKCCGLTDTBQLP3Y4S7G45P6W6Y5Z2XJ3K4L5M6N7P8Q111",
bio: "Quran & Tajweed Educator",
verification: {
submittedAt: "2026-01-01T09:00:00.000Z",
approvedAt: "2026-01-02T10:00:00.000Z",
reviewedBy: "Amina Admin",
},
purchases: [{ id: "p1", title: "Fiqh of Worship", amount: "42.00", date: "2026-01-01" }],
};

const defaultHistory = {
source: "composed",
events: [
{
id: "approved-1",
type: "approved",
label: "Approved",
actor: "Amina Admin",
timestamp: "2026-01-02T10:00:00.000Z",
note: null,
},
{
id: "submitted-1",
type: "submitted",
label: "Submitted",
actor: "Zaynab Idris",
timestamp: "2026-01-01T09:00:00.000Z",
note: null,
},
],
};

function deferred() {
let resolve;
const promise = new Promise((resolvePromise) => {
resolve = resolvePromise;
});
return { promise, resolve };
}

describe("Print-Friendly Views for Records (#338)", () => {
let originalPrint;

beforeEach(() => {
vi.clearAllMocks();
getUserByIdMock.mockResolvedValue({ user: educatorUser });
fetchVerificationHistoryMock.mockResolvedValue(defaultHistory);
originalPrint = window.print;
window.print = vi.fn();
});
Expand Down Expand Up @@ -91,7 +132,11 @@ describe("Print-Friendly Views for Records (#338)", () => {
render(<AdminUserDetailPage params={params} />);
});

expect(await screen.findByText("Zaynab Idris")).toBeInTheDocument();
expect(await screen.findByRole("heading", { name: "Zaynab Idris" })).toBeInTheDocument();
expect(await screen.findByText("Verification History")).toBeInTheDocument();
expect(screen.getByText("Approved")).toBeInTheDocument();
expect(screen.getAllByText(/Actor:/)).toHaveLength(2);
expect(screen.getByText(/Backend history endpoint is not available yet/i)).toBeInTheDocument();

const printRoot = document.body.querySelector(".print-root");
expect(printRoot).toBeInTheDocument();
Expand All @@ -104,6 +149,47 @@ describe("Print-Friendly Views for Records (#338)", () => {
expect(window.print).toHaveBeenCalledTimes(1);
});

it("keeps the latest educator history when an earlier request resolves last", async () => {
const userAHistory = deferred();
const userBHistory = deferred();
getUserByIdMock.mockImplementation(async (userId) => ({
user: { ...educatorUser, _id: userId, name: userId === "usr_a" ? "Educator A" : "Educator B" },
}));
fetchVerificationHistoryMock.mockImplementation((userId) =>
userId === "usr_a" ? userAHistory.promise : userBHistory.promise
);

let rerender;
await act(async () => {
({ rerender } = render(
<AdminUserDetailPage params={Promise.resolve({ userId: "usr_a" })} />
));
});
await waitFor(() => expect(fetchVerificationHistoryMock).toHaveBeenCalledWith("usr_a", expect.any(Object)));

await act(async () => {
rerender(<AdminUserDetailPage params={Promise.resolve({ userId: "usr_b" })} />);
});
await waitFor(() => expect(fetchVerificationHistoryMock).toHaveBeenCalledWith("usr_b", expect.any(Object)));

await act(async () => {
userBHistory.resolve({
source: "backend",
events: [{ ...defaultHistory.events[0], id: "user-b", actor: "Reviewer B" }],
});
});
expect(await screen.findByText("Reviewer B")).toBeInTheDocument();

await act(async () => {
userAHistory.resolve({
source: "backend",
events: [{ ...defaultHistory.events[0], id: "user-a", actor: "Reviewer A" }],
});
});
await waitFor(() => expect(screen.queryByText("Reviewer A")).not.toBeInTheDocument());
expect(screen.getByText("Reviewer B")).toBeInTheDocument();
});

it("DisputesPage detail modal renders print-root container and print record button", async () => {
const { container } = render(<DisputesPage />);

Expand Down
96 changes: 96 additions & 0 deletions __tests__/admin/admin-verification-history.service.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { describe, it, expect, vi, beforeEach } from "vitest";

const mockAxios = vi.hoisted(() => ({
get: vi.fn(),
}));

vi.mock("@/lib/config/axios.config", () => ({
default: mockAxios,
}));

import {
composeVerificationHistoryFromUser,
fetchEducatorVerificationHistory,
} from "@/lib/actions/admin-verification-history";

describe("admin verification history service", () => {
beforeEach(() => {
mockAxios.get.mockReset();
});

it("uses backend verification events when the endpoint is available", async () => {
mockAxios.get.mockResolvedValue({
data: {
events: [
{
id: "evt_1",
type: "approved",
actor: { name: "Admin reviewer" },
timestamp: "2026-01-03T12:00:00.000Z",
},
],
},
});

const result = await fetchEducatorVerificationHistory("usr_1", {});

expect(mockAxios.get).toHaveBeenCalledWith(
"/api/admin/educators/usr_1/verification-history"
);
expect(result.source).toBe("backend");
expect(result.events[0]).toMatchObject({
type: "approved",
actor: "Admin reviewer",
label: "Approved",
});
});

it("composes newest-first fallback events from educator record fields", async () => {
mockAxios.get.mockRejectedValue(new Error("Not found"));

const result = await fetchEducatorVerificationHistory("usr_2", {
email: "educator@example.com",
verification: {
submittedAt: "2026-01-01T09:00:00.000Z",
infoRequestedAt: "2026-01-02T09:00:00.000Z",
approvedAt: "2026-01-03T09:00:00.000Z",
reviewedBy: "Amina Admin",
infoRequestReason: "Certificate scan was unclear.",
},
});

expect(result.source).toBe("composed");
expect(result.events.map((event) => event.type)).toEqual([
"approved",
"info_requested",
"submitted",
]);
expect(result.events[1].note).toBe("Certificate scan was unclear.");
});

it("normalizes explicit history arrays from the user record", () => {
const events = composeVerificationHistoryFromUser({
verificationHistory: [
{ type: "submitted", timestamp: "2026-01-01T00:00:00Z", actor: "Educator" },
{ type: "re_verified", timestamp: "2026-01-04T00:00:00Z", actor: "Admin" },
],
});

expect(events).toHaveLength(2);
expect(events[0].type).toBe("re_verified");
expect(events[0].label).toBe("Re-verified");
});

it("does not treat a generic review timestamp as rejection for an approved record", () => {
const events = composeVerificationHistoryFromUser({
verification: {
status: "approved",
submittedAt: "2026-01-01T00:00:00Z",
reviewedAt: "2026-01-02T00:00:00Z",
approvedAt: "2026-01-02T00:00:00Z",
},
});

expect(events.map((event) => event.type)).toEqual(["approved", "submitted"]);
});
});
9 changes: 7 additions & 2 deletions __tests__/admin/useMediaBlur.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,15 @@ afterEach(() => {
// ── Tests ──────────────────────────────────────────────────────────────

describe("useMediaBlur", () => {
it("starts with blur disabled by default", () => {
it("starts with blur disabled by default", async () => {
const { result } = renderHook(() => useMediaBlur());
expect(result.current.loaded).toBe(true);

await act(async () => {
await new Promise((r) => setTimeout(r, 0));
});

expect(result.current.blurEnabled).toBe(false);
expect(result.current.loaded).toBe(true);
});

it("uses overrideDefault when no stored value exists", async () => {
Expand Down
13 changes: 7 additions & 6 deletions __tests__/verification/VerificationPage.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* VerificationPage - status center component tests
*/

import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { describe, it, expect, vi, beforeAll, beforeEach, afterEach } from "vitest";
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
import { VERIFICATION_STATUS } from "@/lib/actions/educators/fetchVerificationStatus";

Expand Down Expand Up @@ -66,14 +66,15 @@ function clickBtn(testId) {
if (inner) fireEvent.click(inner);
}
let _VerificationPage;
beforeEach(async () => {
beforeAll(async () => {
const mod = await import("@/app/[locale]/account/verification/page");
_VerificationPage = mod.default;
}, 60000);

beforeEach(() => {
_hookState.current = makeHook();
mockPush.mockClear();
mockFetchSignedUrl.mockClear();
if (!_VerificationPage) {
const mod = await import("@/app/[locale]/account/verification/page");
_VerificationPage = mod.default;
}
});
afterEach(() => vi.clearAllMocks());

Expand Down
10 changes: 5 additions & 5 deletions app/[locale]/admin/courses/[courseId]/page.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@

import { useState, useEffect } from "react";
import { useParams } from "next/navigation";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger
} from "@/components/ui/accordion";
import { Play, BookOpen, FileText, Settings, ShieldAlert, Calendar, BarChart3, Tag } from "lucide-react";
import { PageShell } from "@/components/ui/page-shell";
Expand Down
Loading
Loading