Files
ihasmail-inbuxa/web/src/lib/__tests__/describeRules.test.ts
T
jcoffey-dev bd6a605d61 Group six more clusters out of web/src/lib
Takes the flat module count from 66 to 42, continuing what admin/ and
calendar/ started.

  lib/mailbox/  archiveDate, emptyFolder, folderMove, labelTree,
                mailboxName, mailboxRoute
  lib/sieve/    sieve, sieveApply, sieveFolders
  lib/input/    keyboard, swipe, touch, listSelection, dropUpload
  lib/notify/   notify, webpush, webpushEnable
  lib/sw/       swCache, swFacts, staleBuild
  lib/text/     html, markdown, text, emlName

FOUR THINGS THE FILENAMES GET WRONG, each checked by reading the file
rather than trusting what it is called:

  - appFolder is not a mailbox. It is the `ihasmail` folder in JMAP
    *Files*, where the client keeps signature images and synced settings.
    It stays flat.
  - format holds no formatting of text. It re-exports the date and clock
    formatters, so it belongs with dates/datetime, not with text/.
  - preview is the file viewer deciding what it can show without
    downloading, and source is where to point someone asking for this
    instance's AGPL source. Neither is about text.
  - notify is not Web Push. It is the tab title, the favicon badge and
    the new-mail sound -- in-app notification, which is why it sits with
    webpush rather than under sw/ with the service worker's own concerns.

threadScroll stays flat too: it decides where a conversation opens, which
is view state rather than a gesture, and input/ is honest only if
everything in it interprets something the reader did.

No behavior change. Almost every reference was on the @/ alias; eight
relative imports in files that did not move, or that moved away from a
sibling, needed rewriting by hand.
2026-09-15 23:17:50 -07:00

94 lines
4.1 KiB
TypeScript

/**
* The two sentence builders, which had no tests while they were building
* English by concatenation -- and no test would have caught the thing wrong
* with them, since the English output was correct. These pin the two
* properties that matter now: every fragment goes through the catalog, and
* the joining is Intl's rather than a hardcoded " and ".
*/
import { describe, expect, it } from "vitest";
import { describeRule as describeSieve } from "../sieve/sieve";
import { describeRule as describeRecurrence, weekdayOptions } from "../calendar/recurrence";
import { setUiLanguageForFormatting } from "../datetime";
import { setCatalog } from "../i18n";
describe("sieve describeRule", () => {
it("names the header and operator through the catalog", () => {
const s = describeSieve({
id: "1", name: "r", join: "allof", enabled: true,
tests: [{ type: "header", header: "subject", op: "contains", value: "invoice" }],
actions: [{ type: "fileinto", mailbox: "Work" }],
} as never);
expect(s).toContain("Subject");
expect(s).toContain("contains");
expect(s).toContain("invoice");
expect(s).toContain("Work");
});
it("joins an allof rule as a conjunction and anyof as a disjunction", () => {
const base = {
id: "1", name: "r", enabled: true,
tests: [
{ type: "header", header: "from", op: "is", value: "a@b" },
{ type: "header", header: "to", op: "is", value: "c@d" },
],
actions: [{ type: "keep" }],
};
expect(describeSieve({ ...base, join: "allof" } as never)).toContain(" and ");
expect(describeSieve({ ...base, join: "anyof" } as never)).toContain(" or ");
});
it("says 'always' when a rule has no tests", () => {
const s = describeSieve({ id: "1", name: "r", join: "allof", enabled: true, tests: [], actions: [{ type: "stop" }] } as never);
expect(s).toContain("always");
});
});
describe("recurrence describeRule", () => {
it("describes the simple frequencies", () => {
expect(describeRecurrence(undefined)).toBe("Does not repeat");
expect(describeRecurrence({ "@type": "RecurrenceRule", frequency: "daily" } as never)).toBe("Daily");
expect(describeRecurrence({ "@type": "RecurrenceRule", frequency: "daily", interval: 3 } as never)).toBe("Every 3 days");
});
it("recognizes Monday to Friday as every weekday", () => {
const rule = {
"@type": "RecurrenceRule", frequency: "weekly",
byDay: ["mo", "tu", "we", "th", "fr"].map((day) => ({ "@type": "NDay", day })),
};
expect(describeRecurrence(rule as never)).toBe("Every weekday");
});
it("uses a word, not a suffix, for the nth weekday of a month", () => {
const s = describeRecurrence({
"@type": "RecurrenceRule", frequency: "monthly",
byDay: [{ "@type": "NDay", day: "tu", nthOfPeriod: 2 }],
} as never);
expect(s).toContain("second");
expect(s).not.toContain("2nd");
});
it("wraps the sentence for count and until rather than appending to it", () => {
const s = describeRecurrence({ "@type": "RecurrenceRule", frequency: "daily", count: 5 } as never);
expect(s).toBe("Daily, 5 times");
const u = describeRecurrence({ "@type": "RecurrenceRule", frequency: "daily", until: "2026-05-03T00:00:00" } as never);
expect(u).toBe("Daily, until 2026-05-03");
});
it("takes its weekday names from the locale, not a table of English", () => {
setUiLanguageForFormatting("de-DE");
const names = weekdayOptions().map((w) => w.label);
expect(names[0]).toBe("Montag");
expect(names).toHaveLength(7);
// The narrow forms collide in English ("T" for both Tuesday and Thursday),
// which is why they cannot be catalog keys and come from Intl instead.
expect(weekdayOptions().map((w) => w.short)).toHaveLength(7);
setUiLanguageForFormatting(null);
});
it("renders a translated rule through the catalog", () => {
setCatalog("de", { strings: { Daily: "Täglich" }, plurals: {} });
expect(describeRecurrence({ "@type": "RecurrenceRule", frequency: "daily" } as never)).toBe("Täglich");
setCatalog("en", { strings: {}, plurals: {} });
});
});