|
1 | | -import { describe, it, expect, beforeEach } from "vitest"; |
| 1 | +import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; |
2 | 2 | import { api, internal } from "@repo/backend"; |
3 | 3 | import type { Id } from "@repo/backend/_generated/dataModel"; |
4 | 4 | import { |
@@ -803,6 +803,136 @@ describe("Comments", () => { |
803 | 803 | }); |
804 | 804 | }); |
805 | 805 |
|
| 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 | + |
806 | 936 | // ── Feed Score Guard ───────────────────────────────────────── |
807 | 937 |
|
808 | 938 | describe("feed score guard", () => { |
|
0 commit comments