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.
This commit is contained in:
jcoffey-dev committed 2026-09-15 23:17:50 -07:00
1 parent 5cc31037c1
commit bd6a605d61
95 files changed
+104 -104

No files matched your search

@@ -0,0 +1,109 @@
import { describe, expect, it } from "vitest";
import { labelTree, visibleLabels, descendantKeywords } from "@/lib/mailbox/labelTree";
import type { Label } from "@/store/settings";
const L = (keyword: string, over: Partial<Label> = {}): Label => ({ keyword, name: keyword, color: "#000", ...over });
const flat = (labels: Label[], counts: Record<string, number> = {}) =>
visibleLabels(labelTree(labels, counts)).map((n) => `${" ".repeat(n.depth)}${n.label.keyword}`);
describe("labelTree", () => {
it("nests a label under its parent and indents it", () => {
const roots = labelTree([L("work"), L("work_urgent", { parent: "work" })]);
expect(roots).toHaveLength(1);
expect(roots[0]!.label.keyword).toBe("work");
expect(roots[0]!.children[0]!.label.keyword).toBe("work_urgent");
expect(roots[0]!.children[0]!.depth).toBe(1);
});
it("nests three deep", () => {
expect(flat([L("a"), L("b", { parent: "a" }), L("c", { parent: "b" })])).toEqual(["a", " b", " c"]);
});
it("puts a label back at the top when its parent no longer exists", () => {
// Settings sync between devices; a parent can be deleted on one while
// another still points at it. Dropping the child would lose it for good.
expect(flat([L("orphan", { parent: "gone" })])).toEqual(["orphan"]);
});
it("survives a cycle rather than hanging", () => {
const out = flat([L("a", { parent: "b" }), L("b", { parent: "a" })]);
expect(out).toHaveLength(2);
expect(out.map((s) => s.trim()).sort()).toEqual(["a", "b"]);
});
it("survives a label parented to itself", () => {
expect(flat([L("a", { parent: "a" })])).toEqual(["a"]);
});
it("carries each label's own unread count, not its children's", () => {
const roots = labelTree([L("a"), L("b", { parent: "a" })], { a: 2, b: 5 });
expect(roots[0]!.unread).toBe(2);
expect(roots[0]!.children[0]!.unread).toBe(5);
});
});
describe("visibleLabels", () => {
it("draws everything set to always", () => {
expect(flat([L("a"), L("b")])).toEqual(["a", "b"]);
});
it("never draws a hidden label", () => {
expect(flat([L("a"), L("b", { visibility: "hidden" })], { b: 9 })).toEqual(["a"]);
});
it("draws an unread-only label just while it has unread mail", () => {
const labels = [L("a", { visibility: "unread" })];
expect(flat(labels, { a: 0 })).toEqual([]);
expect(flat(labels, { a: 1 })).toEqual(["a"]);
});
it("keeps a parent that would otherwise be dropped, when a child survives", () => {
// A child cannot be drawn under a parent that is not there, and promoting
// it would silently rearrange the tree. The parent comes back as a
// container instead.
const labels = [L("work", { visibility: "unread" }), L("work_urgent", { parent: "work" })];
expect(flat(labels, { work: 0 })).toEqual(["work", " work_urgent"]);
});
it("keeps a hidden parent too, when a child survives", () => {
const labels = [L("work", { visibility: "hidden" }), L("work_urgent", { parent: "work" })];
expect(flat(labels, {})).toEqual(["work", " work_urgent"]);
});
it("drops a whole branch when nothing in it survives", () => {
const labels = [
L("work", { visibility: "unread" }),
L("work_urgent", { parent: "work", visibility: "unread" }),
L("other"),
];
expect(flat(labels, { work: 0, work_urgent: 0 })).toEqual(["other"]);
});
it("keeps a grandparent when only a grandchild survives", () => {
const labels = [
L("a", { visibility: "hidden" }),
L("b", { parent: "a", visibility: "hidden" }),
L("c", { parent: "b" }),
];
expect(flat(labels, {})).toEqual(["a", " b", " c"]);
});
it("treats a label with no visibility set as always, so old settings parse unchanged", () => {
const l = L("a");
expect(l.visibility).toBeUndefined();
expect(flat([l], {})).toEqual(["a"]);
});
});
describe("descendantKeywords", () => {
it("names everything below a label, so the parent picker cannot offer a cycle", () => {
const roots = labelTree([L("a"), L("b", { parent: "a" }), L("c", { parent: "b" }), L("d")]);
expect([...descendantKeywords(roots, "a")].sort()).toEqual(["b", "c"]);
expect([...descendantKeywords(roots, "d")]).toEqual([]);
});
it("says nothing about a label that is not there", () => {
expect([...descendantKeywords(labelTree([L("a")]), "missing")]).toEqual([]);
});
});