Skip to content

Commit f0a230a

Browse files
committed
test: add test coverage
1 parent c218bbb commit f0a230a

13 files changed

Lines changed: 791 additions & 36 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@
152152
},
153153
"lint-staged": {
154154
"*.{js,jsx,ts,tsx}": [
155-
"eslint --fix --max-warnings 0",
155+
"eslint --fix --max-warnings 0 --no-warn-ignored",
156156
"prettier --write"
157157
],
158158
"*.{json,css}": "prettier --write"

src/lib/__tests__/edge-client.test.ts

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,14 @@ import {
1818
fetchAdminTeams,
1919
fetchAdminUsers,
2020
fetchNotificationEmailConfig,
21+
fetchNotificationEmailPreview,
2122
fetchNotificationMessages,
2223
fetchNotificationPreferences,
2324
fetchNotificationRules,
2425
fetchPublicOverview,
2526
fetchPublicPages,
2627
fetchPublicReferrers,
28+
fetchPublicSite,
2729
fetchPublicTrend,
2830
loginAdminAccount,
2931
markAllNotificationMessagesRead,
@@ -46,6 +48,7 @@ import {
4648
upsertAdminSiteConfig,
4749
} from "@/lib/edge-client";
4850
import { handleDemoRequest } from "@/lib/realtime/mock";
51+
import { handleDemoNotificationEmailPreview } from "@/lib/realtime/mock/notification-email-preview";
4952

5053
vi.mock("@/lib/auth", () => ({
5154
getSessionToken: vi.fn(),
@@ -55,7 +58,14 @@ vi.mock("@/lib/realtime/mock", () => ({
5558
handleDemoRequest: vi.fn(),
5659
}));
5760

61+
vi.mock("@/lib/realtime/mock/notification-email-preview", () => ({
62+
handleDemoNotificationEmailPreview: vi.fn(),
63+
}));
64+
5865
const getSessionTokenMock = vi.mocked(getSessionToken);
66+
const handleDemoNotificationEmailPreviewMock = vi.mocked(
67+
handleDemoNotificationEmailPreview,
68+
);
5969
const handleDemoRequestMock = vi.mocked(handleDemoRequest);
6070

6171
function fetchMock() {
@@ -121,6 +131,35 @@ describe("edge client request wrappers", () => {
121131
expect(getSessionTokenMock).not.toHaveBeenCalled();
122132
});
123133

134+
it("unwraps public site metadata and rejects missing public sites", async () => {
135+
fetchMock().mockResolvedValueOnce(
136+
jsonResponse({
137+
ok: true,
138+
data: {
139+
id: "site-1",
140+
slug: "public-site",
141+
name: "Public Site",
142+
domain: "example.test",
143+
},
144+
}),
145+
);
146+
147+
await expect(fetchPublicSite("public site")).resolves.toEqual({
148+
id: "site-1",
149+
slug: "public-site",
150+
name: "Public Site",
151+
domain: "example.test",
152+
});
153+
expect(lastFetchCall()[0]).toBe(
154+
"http://127.0.0.1:8787/api/public/share/public%20site/site",
155+
);
156+
157+
fetchMock().mockResolvedValueOnce(jsonResponse({ ok: false, data: null }));
158+
await expect(fetchPublicSite("missing")).rejects.toThrow(
159+
"Public site not found",
160+
);
161+
});
162+
124163
it("encodes public slugs and query params", async () => {
125164
await fetchPublicOverview("public/site?draft=true", { from: 0, to: 1 });
126165

@@ -558,6 +597,64 @@ describe("edge client request wrappers", () => {
558597
);
559598
});
560599

600+
it("fetches notification email previews in every supported format", async () => {
601+
fetchMock().mockResolvedValueOnce(
602+
jsonResponse({
603+
ok: true,
604+
data: {
605+
subject: "Demo subject",
606+
html: "<p>Demo</p>",
607+
text: "Demo",
608+
},
609+
}),
610+
);
611+
612+
await expect(
613+
fetchNotificationEmailPreview({
614+
type: "report",
615+
locale: "en",
616+
format: "json",
617+
}),
618+
).resolves.toEqual({
619+
subject: "Demo subject",
620+
html: "<p>Demo</p>",
621+
text: "Demo",
622+
});
623+
expect(new URL(lastFetchCall()[0]).searchParams.get("format")).toBe("json");
624+
625+
fetchMock().mockResolvedValueOnce(new Response("<p>HTML</p>"));
626+
await expect(
627+
fetchNotificationEmailPreview({
628+
type: "health",
629+
locale: "zh",
630+
format: "html",
631+
}),
632+
).resolves.toBe("<p>HTML</p>");
633+
634+
fetchMock().mockResolvedValueOnce(new Response("plain text"));
635+
await expect(
636+
fetchNotificationEmailPreview({
637+
type: "threshold",
638+
locale: "en",
639+
format: "text",
640+
}),
641+
).resolves.toBe("plain text");
642+
});
643+
644+
it("throws descriptive errors for failed notification email previews", async () => {
645+
fetchMock().mockResolvedValueOnce(
646+
new Response("preview denied", { status: 403 }),
647+
);
648+
649+
await expect(
650+
fetchNotificationEmailPreview({
651+
type: "test",
652+
locale: "en",
653+
format: "html",
654+
}),
655+
).rejects.toThrow("Email preview failed (403): preview denied");
656+
});
657+
561658
it("throws descriptive errors for non-OK edge responses", async () => {
562659
fetchMock().mockResolvedValueOnce(new Response("denied", { status: 403 }));
563660

@@ -712,6 +809,34 @@ describe("edge client request wrappers", () => {
712809
expect(getSessionTokenMock).not.toHaveBeenCalled();
713810
});
714811

812+
it("delegates notification email previews to the demo handler in demo mode", async () => {
813+
vi.stubEnv("NEXT_PUBLIC_DEMO_MODE", "1");
814+
const preview = {
815+
subject: "Demo",
816+
html: "<p>Demo</p>",
817+
text: "Demo",
818+
};
819+
const mockModule =
820+
await import("@/lib/realtime/mock/notification-email-preview");
821+
vi.mocked(mockModule.handleDemoNotificationEmailPreview).mockResolvedValue(
822+
preview,
823+
);
824+
825+
await expect(
826+
fetchNotificationEmailPreview({
827+
type: "test",
828+
locale: "en",
829+
format: "json",
830+
}),
831+
).resolves.toBe(preview);
832+
expect(mockModule.handleDemoNotificationEmailPreview).toHaveBeenCalledWith({
833+
type: "test",
834+
locale: "en",
835+
format: "json",
836+
});
837+
expect(fetchMock()).not.toHaveBeenCalled();
838+
});
839+
715840
it("wires the edge-client filter helper into at least one exported request wrapper", () => {
716841
const source = readFileSync("src/lib/edge-client.ts", "utf8");
717842
const callSites = source.match(/\bwithFilters\s*\(/g) ?? [];

src/lib/dashboard/__tests__/client-core-data.test.ts

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

33
import {
4+
createFunnel,
5+
deleteFunnel,
46
fetchEventRecordDetail,
57
fetchEventsRecords,
68
fetchEventsSummary,
79
fetchEventsTrend,
810
fetchEventTypeDetail,
911
fetchEventTypeFieldValues,
1012
fetchFunnelDetail,
13+
fetchFunnels,
1114
fetchPerformance,
1215
fetchSessionDetail,
1316
fetchSessions,
@@ -223,12 +226,49 @@ describe("fetchSessionDetail", () => {
223226
});
224227

225228
describe("fetchFunnelDetail", () => {
229+
it("fetches funnel lists by site", async () => {
230+
fetchPrivateJsonMock.mockResolvedValueOnce({ funnels: [] } as any);
231+
232+
await fetchFunnels("site-1");
233+
234+
expect(fetchPrivateJsonMock).toHaveBeenCalledWith("/api/private/funnels", {
235+
siteId: "site-1",
236+
});
237+
});
238+
226239
it("throws for empty funnelId", async () => {
227240
await expect(fetchFunnelDetail("site-1", " ", window)).rejects.toThrow(
228241
"Funnel id is required",
229242
);
230243
expect(fetchPrivateJsonMock).not.toHaveBeenCalled();
231244
});
245+
246+
it("creates and deletes funnels through mutation requests", async () => {
247+
fetchPrivateJsonMutateMock.mockResolvedValueOnce({ ok: true } as any);
248+
fetchPrivateJsonMutateMock.mockResolvedValueOnce({ ok: true } as any);
249+
250+
await createFunnel("site-1", "Signup", [
251+
{ id: "step-1", type: "page", value: "/signup" },
252+
] as any);
253+
await deleteFunnel("site-1", "funnel-1");
254+
255+
expect(fetchPrivateJsonMutateMock).toHaveBeenNthCalledWith(
256+
1,
257+
"/api/private/funnels",
258+
"POST",
259+
{ siteId: "site-1" },
260+
{
261+
name: "Signup",
262+
steps: [{ id: "step-1", type: "page", value: "/signup" }],
263+
},
264+
);
265+
expect(fetchPrivateJsonMutateMock).toHaveBeenNthCalledWith(
266+
2,
267+
"/api/private/funnels",
268+
"DELETE",
269+
{ siteId: "site-1", id: "funnel-1" },
270+
);
271+
});
232272
});
233273

234274
describe("fetchEventTypeDetail", () => {

src/lib/dashboard/__tests__/format.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
percentFormat,
88
shortDate,
99
shortDateTime,
10+
shortDateTimeWithSeconds,
1011
} from "@/lib/dashboard/format";
1112

1213
describe("Dashboard Format Utilities", () => {
@@ -90,6 +91,7 @@ describe("Dashboard Format Utilities", () => {
9091
const isoStr = "2026-05-25T03:43:43Z";
9192
expect(shortDate("en", isoStr, "UTC")).toContain("May");
9293
expect(shortDateTime("en", isoStr, "UTC")).toContain("43");
94+
expect(shortDateTimeWithSeconds("en", isoStr, "UTC")).toContain("43");
9395
});
9496

9597
it("should format without a timezone override when one is not provided", () => {

src/lib/edge-client.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -734,7 +734,7 @@ export async function fetchNotificationEmailPreview(input: {
734734
> {
735735
if (process.env.NEXT_PUBLIC_DEMO_MODE === "1") {
736736
const { handleDemoNotificationEmailPreview } =
737-
await import("@/lib/realtime/mock");
737+
await import("@/lib/realtime/mock/notification-email-preview");
738738
return handleDemoNotificationEmailPreview(input);
739739
}
740740
const baseUrl = await edgeBaseUrl();

src/lib/edge/__tests__/admin-notifications.test.ts

Lines changed: 77 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { beforeEach, describe, expect, it, vi } from "vitest";
22

33
import {
4+
handleNotificationEmailPreviewAdmin,
45
handleNotificationPreferences,
56
handleNotificationRead,
67
handleNotificationRulePreviewAdmin,
@@ -31,6 +32,7 @@ const createManualTestNotification = vi.hoisted(() => vi.fn());
3132
const createNotificationRulePreview = vi.hoisted(() => vi.fn());
3233
const runNotificationRuleManually = vi.hoisted(() => vi.fn());
3334
const runScheduledTask = vi.hoisted(() => vi.fn());
35+
const renderNotificationEmail = vi.hoisted(() => vi.fn());
3436

3537
vi.mock("@/lib/edge/admin-auth", () => ({
3638
requireActor,
@@ -75,6 +77,10 @@ vi.mock("@/lib/edge/scheduled-task-runner", () => ({
7577
runScheduledTask,
7678
}));
7779

80+
vi.mock("@/lib/notifications/email-renderer", () => ({
81+
renderNotificationEmail,
82+
}));
83+
7884
function request(path: string, body: Record<string, unknown>): Request {
7985
return new Request(`https://edge.test${path}`, {
8086
method: "POST",
@@ -141,6 +147,11 @@ describe("admin notification handlers", () => {
141147
summary: { emailFailed: 0 },
142148
messageCount: 0,
143149
});
150+
renderNotificationEmail.mockResolvedValue({
151+
subject: "Preview",
152+
html: "<p>Preview</p>",
153+
text: "Preview text",
154+
});
144155
createManualTestNotification.mockResolvedValue({
145156
message: { id: "msg-test", deliveryStatus: "sent" },
146157
summary: { messagesCreated: 1 },
@@ -723,13 +734,78 @@ describe("admin notification handlers", () => {
723734
request("/api/private/admin/notifications/test", { teamId: "team-1" }),
724735
{} as never,
725736
),
737+
handleNotificationEmailPreviewAdmin(
738+
getRequest("/api/private/admin/notification-email-preview"),
739+
{} as never,
740+
new URL(
741+
"https://edge.test/api/private/admin/notification-email-preview",
742+
),
743+
),
726744
]);
727745

728746
expect(responses.map((response) => response.status)).toEqual([
729-
401, 401, 401, 401, 401, 401, 401,
747+
401, 401, 401, 401, 401, 401, 401, 401,
730748
]);
731749
});
732750

751+
it("renders notification email previews for admins", async () => {
752+
requireActor.mockResolvedValue({
753+
user: { id: "admin-1" },
754+
isAdmin: true,
755+
});
756+
757+
const html = await handleNotificationEmailPreviewAdmin(
758+
getRequest("/api/private/admin/notification-email-preview?type=bad"),
759+
{} as never,
760+
new URL(
761+
"https://edge.test/api/private/admin/notification-email-preview?type=bad&locale=bad",
762+
),
763+
);
764+
const text = await handleNotificationEmailPreviewAdmin(
765+
getRequest("/api/private/admin/notification-email-preview?format=text"),
766+
{} as never,
767+
new URL(
768+
"https://edge.test/api/private/admin/notification-email-preview?type=health&format=text",
769+
),
770+
);
771+
const json = await handleNotificationEmailPreviewAdmin(
772+
getRequest("/api/private/admin/notification-email-preview?format=json"),
773+
{} as never,
774+
new URL(
775+
"https://edge.test/api/private/admin/notification-email-preview?type=test&format=json",
776+
),
777+
);
778+
779+
expect(html.headers.get("content-type")).toContain("text/html");
780+
expect(await html.text()).toBe("<p>Preview</p>");
781+
expect(await text.text()).toBe("Preview text");
782+
expect(await json.json()).toMatchObject({
783+
ok: true,
784+
data: { subject: "Preview" },
785+
});
786+
expect(renderNotificationEmail).toHaveBeenCalledTimes(3);
787+
});
788+
789+
it("rejects non-admin and non-GET email preview requests", async () => {
790+
const forbidden = await handleNotificationEmailPreviewAdmin(
791+
getRequest("/api/private/admin/notification-email-preview"),
792+
{} as never,
793+
new URL("https://edge.test/api/private/admin/notification-email-preview"),
794+
);
795+
requireActor.mockResolvedValueOnce({
796+
user: { id: "admin-1" },
797+
isAdmin: true,
798+
});
799+
const method = await handleNotificationEmailPreviewAdmin(
800+
methodRequest("POST", "/api/private/admin/notification-email-preview"),
801+
{} as never,
802+
new URL("https://edge.test/api/private/admin/notification-email-preview"),
803+
);
804+
805+
expect(forbidden.status).toBe(403);
806+
expect(method.status).toBe(405);
807+
});
808+
733809
it("returns method and validation errors for preferences and read handlers", async () => {
734810
const unsupportedPreferences = await handleNotificationPreferences(
735811
methodRequest("POST", "/api/private/notification-preferences", {}),

0 commit comments

Comments
 (0)