399 lines
11 KiB
TypeScript
399 lines
11 KiB
TypeScript
// @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<string, unknown> }> = [];
|
|
const t = ((key: string, values: Record<string, unknown>) => {
|
|
calls.push({ key, values });
|
|
return `translated:${key}`;
|
|
}) as TFunction;
|
|
|
|
return { calls, t };
|
|
}
|
|
|
|
function stripDefaultValues(calls: Array<{ key: string; values: Record<string, unknown> }>) {
|
|
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<string, unknown> }> = [];
|
|
const t = ((key: string, values: Record<string, unknown>) => {
|
|
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<string, unknown> }> = [];
|
|
const t = ((key: string, values: Record<string, unknown>) => {
|
|
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",
|
|
},
|
|
},
|
|
]);
|
|
});
|
|
});
|