Skip to content

Commit 5f96129

Browse files
committed
Add regression tests for comment mention notifications
Backend tests (comments.test.ts): - Verifies comment_mention notification is sent to mentioned user via Tiptap JSON content with mention nodes - Verifies self-mentions do not trigger notifications - Verifies plain text comments do not trigger mention notifications Frontend tests (notification-utils.test.ts): - Verifies getNotificationMessage returns correct text for comment_mention - Verifies getNotificationLink routes comment_mention with commentId param - Verifies getNotificationLink handles missing commentId gracefully https://claude.ai/code/session_01V2EmfQSTiiAyke2NAwSjBn
1 parent 06e9ace commit 5f96129

2 files changed

Lines changed: 156 additions & 1 deletion

File tree

apps/web/tests/api/comments.test.ts

Lines changed: 131 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { describe, it, expect, beforeEach } from "vitest";
1+
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
22
import { api, internal } from "@repo/backend";
33
import type { Id } from "@repo/backend/_generated/dataModel";
44
import {
@@ -803,6 +803,136 @@ describe("Comments", () => {
803803
});
804804
});
805805

806+
// ── Comment Mention Notifications ───────────────────────────
807+
808+
describe("comment mention notifications", () => {
809+
beforeEach(() => {
810+
vi.useFakeTimers({ toFake: ["setTimeout", "clearTimeout"] });
811+
});
812+
813+
afterEach(async () => {
814+
await t.finishAllScheduledFunctions(vi.runAllTimers);
815+
vi.useRealTimers();
816+
});
817+
818+
function tiptapMention(userId: string): string {
819+
return JSON.stringify({
820+
type: "doc",
821+
content: [
822+
{
823+
type: "paragraph",
824+
content: [
825+
{ type: "text", text: "Hey " },
826+
{ type: "mention", attrs: { id: userId, label: "someone" } },
827+
{ type: "text", text: " nice work!" },
828+
],
829+
},
830+
],
831+
});
832+
}
833+
834+
it("sends comment_mention notification to mentioned user", async () => {
835+
const { ownerId, participantId, activityId } =
836+
await setupChallengeWithActivity();
837+
838+
// Create a third user to be mentioned
839+
const mentionedId = await createTestUser(t, {
840+
email: "mentioned@example.com",
841+
username: "mentioned",
842+
});
843+
const challengeId = (
844+
await t.run(async (ctx) => ctx.db.get(activityId))
845+
)!.challengeId;
846+
await createTestParticipation(t, mentionedId, challengeId);
847+
848+
const tOwnerAuth = t.withIdentity({
849+
subject: "owner-sub",
850+
email: "owner@example.com",
851+
});
852+
853+
await tOwnerAuth.mutation(api.mutations.comments.create, {
854+
activityId: activityId as Id<"activities">,
855+
content: tiptapMention(mentionedId as string),
856+
});
857+
858+
// Run the scheduled sendCommentMentionNotifications
859+
await t.finishAllScheduledFunctions(vi.runAllTimers);
860+
861+
const notifications = await t.run(async (ctx) => {
862+
return ctx.db
863+
.query("notifications")
864+
.withIndex("userId", (q) => q.eq("userId", mentionedId))
865+
.collect();
866+
});
867+
868+
expect(notifications.some((n) => n.type === "comment_mention")).toBe(
869+
true,
870+
);
871+
const mentionNotif = notifications.find(
872+
(n) => n.type === "comment_mention",
873+
)!;
874+
expect(mentionNotif.actorId).toBe(ownerId);
875+
expect(mentionNotif.data.activityId).toBe(activityId);
876+
});
877+
878+
it("does NOT send comment_mention for self-mentions", async () => {
879+
const { ownerId, activityId } = await setupChallengeWithActivity();
880+
881+
const tOwnerAuth = t.withIdentity({
882+
subject: "owner-sub",
883+
email: "owner@example.com",
884+
});
885+
886+
// Owner mentions themselves
887+
await tOwnerAuth.mutation(api.mutations.comments.create, {
888+
activityId: activityId as Id<"activities">,
889+
content: tiptapMention(ownerId as string),
890+
});
891+
892+
await t.finishAllScheduledFunctions(vi.runAllTimers);
893+
894+
const notifications = await t.run(async (ctx) => {
895+
return ctx.db
896+
.query("notifications")
897+
.withIndex("userId", (q) => q.eq("userId", ownerId))
898+
.collect();
899+
});
900+
901+
expect(notifications.some((n) => n.type === "comment_mention")).toBe(
902+
false,
903+
);
904+
});
905+
906+
it("does NOT send comment_mention for plain text comments", async () => {
907+
const { participantId, activityId } =
908+
await setupChallengeWithActivity();
909+
910+
const tOwnerAuth = t.withIdentity({
911+
subject: "owner-sub",
912+
email: "owner@example.com",
913+
});
914+
915+
await tOwnerAuth.mutation(api.mutations.comments.create, {
916+
activityId: activityId as Id<"activities">,
917+
content: "Just a plain text comment",
918+
});
919+
920+
await t.finishAllScheduledFunctions(vi.runAllTimers);
921+
922+
const notifications = await t.run(async (ctx) => {
923+
return ctx.db
924+
.query("notifications")
925+
.withIndex("userId", (q) => q.eq("userId", participantId))
926+
.collect();
927+
});
928+
929+
// Should only have the "comment" notification to the activity owner, no mention
930+
expect(notifications.some((n) => n.type === "comment_mention")).toBe(
931+
false,
932+
);
933+
});
934+
});
935+
806936
// ── Feed Score Guard ─────────────────────────────────────────
807937

808938
describe("feed score guard", () => {

apps/web/tests/lib/notification-utils.test.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,11 @@ describe("getNotificationMessage", () => {
4040
expect(getNotificationMessage(n)).toBe("Alice commented on your activity");
4141
});
4242

43+
it("returns correct message for comment_mention", () => {
44+
const n = makeNotification({ type: "comment_mention" });
45+
expect(getNotificationMessage(n)).toBe("Alice mentioned you in a comment");
46+
});
47+
4348
it("returns correct message for new_follower", () => {
4449
const n = makeNotification({ type: "new_follower" });
4550
expect(getNotificationMessage(n)).toBe("Alice started following you");
@@ -185,6 +190,26 @@ describe("getNotificationLink", () => {
185190
);
186191
});
187192

193+
it("routes comment_mention to activity page with commentId query param", () => {
194+
const n = makeNotification({
195+
type: "comment_mention",
196+
data: { activityId: "act-1", commentId: "comm-1" },
197+
});
198+
expect(getNotificationLink(n, challengeId)).toBe(
199+
"/challenges/challenge-1/activities/act-1?commentId=comm-1",
200+
);
201+
});
202+
203+
it("routes comment_mention to activity page without commentId when missing", () => {
204+
const n = makeNotification({
205+
type: "comment_mention",
206+
data: { activityId: "act-1" },
207+
});
208+
expect(getNotificationLink(n, challengeId)).toBe(
209+
"/challenges/challenge-1/activities/act-1",
210+
);
211+
});
212+
188213
it("routes like with activityId to activity page", () => {
189214
const n = makeNotification({
190215
type: "like",

0 commit comments

Comments
 (0)