// @vitest-environment node import { describe, expect, it } from "vitest"; import EventUtils from "./EventUtils"; import type { TFunction } from "i18next"; function createTranslationSpy() { const calls: Array<{ key: string; values: Record }> = []; const t = ((key: string, values: Record) => { calls.push({ key, values }); return `translated:${key}`; }) as TFunction; return { calls, t }; } function stripDefaultValues(calls: Array<{ key: string; values: Record }>) { return calls.map(({ key, values }) => { const { defaultValue: _defaultValue, ...rest } = values; return { key, values: rest }; }); } describe("EventUtils user preference descriptions", () => { it("describes locale preference updates", () => { expect( EventUtils.getDescription("user.preferences.updated", { old: { locale: "en" }, new: { locale: "de" }, user: { id: "user-1", email: "user@example.com" }, }) ).toBe("user@example.com updated their language: en -> de"); }); it("translates locale preference updates when a translator is provided", () => { const { calls, t } = createTranslationSpy(); expect( EventUtils.getDescription( "user.preferences.updated", { old: { locale: "en" }, new: { locale: "de" }, user: { id: "user-1", email: "user@example.com" }, }, t ) ).toBe("translated:events.descriptions.userPreferences.localeUpdated"); expect(stripDefaultValues(calls)).toEqual([ { key: "shared.locales.en", values: {}, }, { key: "shared.locales.de", values: {}, }, { key: "events.descriptions.userPreferences.localeUpdated", values: { email: "user@example.com", newLocale: "translated:shared.locales.de", oldLocale: "translated:shared.locales.en", }, }, ]); }); it("describes light/dark mode preference updates", () => { expect( EventUtils.getDescription("user.preferences.updated", { old: { lightOrDarkMode: "light" }, new: { lightOrDarkMode: "dark" }, user: { id: "user-1", email: "user@example.com" }, }) ).toBe("user@example.com updated their theme mode: light -> dark"); }); it("translates theme preference updates with light as the default mode", () => { const { calls, t } = createTranslationSpy(); expect( EventUtils.getDescription( "user.preferences.updated", { old: {}, new: { lightOrDarkMode: "dark" }, user: { id: "user-1", email: "user@example.com" }, }, t ) ).toBe("translated:events.descriptions.userPreferences.themeUpdated"); expect(stripDefaultValues(calls)).toEqual([ { key: "shared.light", values: {}, }, { key: "shared.dark", values: {}, }, { key: "events.descriptions.userPreferences.themeUpdated", values: { email: "user@example.com", newTheme: "translated:shared.dark", oldTheme: "translated:shared.light", }, }, ]); }); it("translates generic preference updates", () => { const { calls, t } = createTranslationSpy(); expect( EventUtils.getDescription( "user.preferences.updated", { old: {}, new: {}, user: { id: "user-1", email: "user@example.com" }, }, t ) ).toBe("translated:events.descriptions.userPreferences.updated"); expect(stripDefaultValues(calls)).toEqual([ { key: "events.descriptions.userPreferences.updated", values: { email: "user@example.com", }, }, ]); }); }); describe("EventUtils translated event descriptions", () => { it("passes readable default values to translated event descriptions", () => { const { calls, t } = createTranslationSpy(); EventUtils.getDescription( "account.created", { tenant: { id: "tenant-1", name: "Acme" }, }, t ); expect(calls).toEqual([ { key: "events.descriptions.account.created", values: { defaultValue: "Account created: Acme", tenant: "Acme", }, }, ]); }); it("translates unmapped event names with the raw name as fallback", () => { const { calls, t } = createTranslationSpy(); expect(EventUtils.parseName("provider.webhook.delivered", t)).toBe("translated:events.names.unmapped"); expect(EventUtils.parseName("provider.webhook.delivered")).toBe("provider.webhook.delivered"); expect(calls).toEqual([ { key: "events.names.unmapped", values: { defaultValue: "provider.webhook.delivered", name: "provider.webhook.delivered", }, }, ]); }); it("recomputes persisted descriptions with the active translator", () => { const { calls, t } = createTranslationSpy(); expect( EventUtils.parseDescription( { id: "event-1", tenantId: "tenant-1", userId: "user-1", name: "account.created", data: JSON.stringify({ tenant: { id: "tenant-1", name: "Acme" } }), description: "Account created: Acme", createdAt: new Date("2026-05-08T10:00:00.000Z"), }, t ) ).toBe("translated:events.descriptions.account.created"); expect(stripDefaultValues(calls)).toEqual([ { key: "events.descriptions.account.created", values: { tenant: "Acme", }, }, ]); }); it("falls back to persisted descriptions when event payloads cannot be parsed", () => { const { t } = createTranslationSpy(); expect( EventUtils.parseDescription( { id: "event-1", tenantId: "tenant-1", userId: "user-1", name: "account.created", data: "{", description: "Persisted fallback", createdAt: new Date("2026-05-08T10:00:00.000Z"), }, t ) ).toBe("Persisted fallback"); }); it("uses the unknown event translation key when an unmapped payload has no persisted description", () => { const { calls, t } = createTranslationSpy(); expect( EventUtils.parseDescription( { id: "event-1", tenantId: "tenant-1", userId: "user-1", name: "provider.webhook.delivered", data: "{", description: null, createdAt: new Date("2026-05-08T10:00:00.000Z"), }, t ) ).toBe("translated:events.descriptions.unknownWithName"); expect(calls).toEqual([ { key: "events.descriptions.unknownWithName", values: { defaultValue: "Unknown event: provider.webhook.delivered", name: "provider.webhook.delivered", }, }, ]); expect( EventUtils.parseDescription({ id: "event-1", tenantId: "tenant-1", userId: "user-1", name: "provider.webhook.delivered", data: "{", description: null, createdAt: new Date("2026-05-08T10:00:00.000Z"), }) ).toBe("Unknown event: provider.webhook.delivered"); }); it("translates account descriptions", () => { const { calls, t } = createTranslationSpy(); expect( EventUtils.getDescription( "account.created", { tenant: { id: "tenant-1", name: "Acme" }, }, t ) ).toBe("translated:events.descriptions.account.created"); expect(stripDefaultValues(calls)).toEqual([ { key: "events.descriptions.account.created", values: { tenant: "Acme", }, }, ]); }); it("translates new-user invitation prefixes before composing accepted invitation descriptions", () => { const calls: Array<{ key: string; values: Record }> = []; const t = ((key: string, values: Record) => { calls.push({ key, values }); if (key === "events.descriptions.memberInvitation.newUserPrefix") return "translated-prefix "; return `translated:${key}`; }) as TFunction; expect( EventUtils.getDescription( "member.invitation.accepted", { newUser: true, user: { id: "user-1", email: "ada@example.com" }, tenant: { id: "tenant-1", name: "Acme" }, }, t ) ).toBe("translated:events.descriptions.memberInvitation.accepted"); expect(stripDefaultValues(calls)).toEqual([ { key: "events.descriptions.memberInvitation.newUserPrefix", values: {}, }, { key: "events.descriptions.memberInvitation.accepted", values: { email: "ada@example.com", prefix: "translated-prefix ", tenant: "Acme", }, }, ]); }); it("translates row descriptions with actor values", () => { const { calls, t } = createTranslationSpy(); expect( EventUtils.getDescription( "row.created", { entity: { id: "entity-1", name: "Contact" }, title: "Ada Lovelace", user: { id: "user-1", email: "ada@example.com" }, }, t ) ).toBe("translated:events.descriptions.row.created"); expect(stripDefaultValues(calls)).toEqual([ { key: "events.descriptions.row.created", values: { actor: "ada@example.com", entity: "Contact", title: "Ada Lovelace", }, }, ]); }); it("translates row task actors and status fragments", () => { const calls: Array<{ key: string; values: Record }> = []; const t = ((key: string, values: Record) => { calls.push({ key, values }); if (key === "events.actors.apiKey") return `api-key:${values.alias}`; if (key === "events.descriptions.rowTasks.changes.completedAt") return `completed:${values.date}`; return `translated:${key}`; }) as TFunction; expect( EventUtils.getDescription( "row.tasks.updated", { rowId: "row-1", new: { name: "Follow up", completedAt: "2026-05-27T01:00:00.000Z" }, apiKey: { id: "key-1", alias: "Zapier" }, }, t, { dateTime: () => "May 27, 2026, 3:00 AM" } ) ).toBe("translated:events.descriptions.rowTasks.updated"); expect(stripDefaultValues(calls)).toEqual([ { key: "events.descriptions.rowTasks.changes.completedAt", values: { date: "May 27, 2026, 3:00 AM", }, }, { key: "events.actors.apiKey", values: { alias: "Zapier", }, }, { key: "events.descriptions.rowTasks.updated", values: { actor: "api-key:Zapier", change: "completed:May 27, 2026, 3:00 AM", rowId: "row-1", task: "Follow up", }, }, ]); }); });