Use American English spelling throughout
This commit is contained in:
@@ -94,7 +94,7 @@ describe("generated passwords", () => {
|
||||
expect(p).not.toMatch(/[01lIO]/);
|
||||
});
|
||||
|
||||
it("skip bytes that would favour the start of the alphabet", () => {
|
||||
it("skip bytes that would favor the start of the alphabet", () => {
|
||||
// 256 % 55 leaves 36 byte values over; a plain modulo would hand those to
|
||||
// the first 36 characters twice as often. Bytes of 220 and up are dropped
|
||||
// and more are drawn, so a batch of nothing but those costs a draw.
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { client, JmapMethodError } from "@/jmap/client";
|
||||
import { balancedColumns, countObjects, isRefused, loadMetrics, summariseMetrics, type MetricRecord } from "@/lib/adminDashboard";
|
||||
import { balancedColumns, countObjects, isRefused, loadMetrics, summarizeMetrics, type MetricRecord } from "@/lib/adminDashboard";
|
||||
|
||||
const counter = (metric: string, count: number, timestamp = "2026-09-15T14:00:00Z"): MetricRecord => ({ "@type": "Counter", metric, count, timestamp });
|
||||
|
||||
describe("the dashboard's message numbers", () => {
|
||||
it("adds received and sent up over the metric names Stalwart's own dashboard uses", () => {
|
||||
const stats = summariseMetrics([
|
||||
const stats = summarizeMetrics([
|
||||
counter("queue.message-queued", 6),
|
||||
counter("queue.message-queued", 4, "2026-09-15T13:00:00Z"),
|
||||
counter("queue.authenticated-message-queued", 2),
|
||||
@@ -20,7 +20,7 @@ describe("the dashboard's message numbers", () => {
|
||||
});
|
||||
|
||||
it("reads memory from the newest gauge, not the first one listed", () => {
|
||||
const stats = summariseMetrics([
|
||||
const stats = summarizeMetrics([
|
||||
{ "@type": "Gauge", metric: "server.memory", count: 100, timestamp: "2026-09-15T12:00:00Z" },
|
||||
{ "@type": "Gauge", metric: "server.memory", count: 300, timestamp: "2026-09-15T14:00:00Z" },
|
||||
{ "@type": "Gauge", metric: "queue.count", count: 7, timestamp: "2026-09-15T15:00:00Z" },
|
||||
@@ -29,8 +29,8 @@ describe("the dashboard's message numbers", () => {
|
||||
});
|
||||
|
||||
it("tells a history that records nothing from a quiet day", () => {
|
||||
expect(summariseMetrics([]).recorded).toBe(false);
|
||||
const quiet = summariseMetrics([{ "@type": "Gauge", metric: "server.memory", count: 1, timestamp: "2026-09-15T14:00:00Z" }]);
|
||||
expect(summarizeMetrics([]).recorded).toBe(false);
|
||||
const quiet = summarizeMetrics([{ "@type": "Gauge", metric: "server.memory", count: 1, timestamp: "2026-09-15T14:00:00Z" }]);
|
||||
expect(quiet).toMatchObject({ recorded: true, received: 0, sent: 0 });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -62,7 +62,7 @@ describe("the account query", () => {
|
||||
* live server gave, or one its source says it gives.
|
||||
*/
|
||||
describe("refusals in the reader's language", () => {
|
||||
it("recognises the registry's validators and says it again, without the server's words", () => {
|
||||
it("recognizes the registry's validators and says it again, without the server's words", () => {
|
||||
// Live, 2026-09-13: a reserved TLD, and a catch-all without a domain.
|
||||
const domain = describeDirectoryError(new DirectoryError("invalidPatch", "Invalid domain name", ["name"]), "domain");
|
||||
expect(domain).toMatch(/isn't a valid domain name/);
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { describeLinked, dkimAlgorithm, looksLikeDomain, normaliseDomain, parseZoneFile } from "@/lib/adminDomains";
|
||||
import { describeLinked, dkimAlgorithm, looksLikeDomain, normalizeDomain, parseZoneFile } from "@/lib/adminDomains";
|
||||
|
||||
/**
|
||||
* Written the way Stalwart's BIND serialiser writes it (dns-update's
|
||||
* Written the way Stalwart's BIND serializer writes it (dns-update's
|
||||
* `BindSerializer`): `name IN TYPE value`, and a TXT over 255 bytes as a
|
||||
* parenthesised run of quoted chunks.
|
||||
* parenthesized run of quoted chunks.
|
||||
*/
|
||||
const long = "v=DKIM1; k=rsa; h=sha256; p=" + "A".repeat(400);
|
||||
const zone = [
|
||||
@@ -46,7 +46,7 @@ describe("reading the zone file", () => {
|
||||
|
||||
describe("domain names", () => {
|
||||
it("are written back lower-case without the root dot", () => {
|
||||
expect(normaliseDomain(" Example.COM. ")).toBe("example.com");
|
||||
expect(normalizeDomain(" Example.COM. ")).toBe("example.com");
|
||||
});
|
||||
|
||||
it("are checked loosely before the server decides", () => {
|
||||
|
||||
@@ -34,7 +34,7 @@ describe("the span an availability bar covers", () => {
|
||||
expect(w.span).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("marks a single day every three hours, labelling every six", () => {
|
||||
it("marks a single day every three hours, labeling every six", () => {
|
||||
const w = availabilityWindow(at("2026-09-02T09:00:00"), at("2026-09-02T10:00:00"));
|
||||
expect(w.scale).toBe("hours");
|
||||
expect(hours(w)).toEqual(["2@0", "2@3", "2@6", "2@9", "2@12", "2@15", "2@18", "2@21"]);
|
||||
|
||||
@@ -73,7 +73,7 @@ describe("birthdaysInRange", () => {
|
||||
expect(birthdaysInRange([card("c2", "", { month: 6, day: 15 })], s, e)).toEqual([]);
|
||||
});
|
||||
|
||||
it("falls back to a name built from components, then to the organisation", () => {
|
||||
it("falls back to a name built from components, then to the organization", () => {
|
||||
const [s, e] = range("2026-01-01", "2027-01-01");
|
||||
const parts = {
|
||||
id: "c1",
|
||||
@@ -115,7 +115,7 @@ describe("birthdaysInRange", () => {
|
||||
expect(out.map((b) => b.name)).toEqual(["Amy", "Zoe"]);
|
||||
});
|
||||
|
||||
it("gives each occurrence a stable, unique id that marks it as synthesised", () => {
|
||||
it("gives each occurrence a stable, unique id that marks it as synthesized", () => {
|
||||
const [s, e] = range("2025-01-01", "2027-01-01");
|
||||
const out = birthdaysInRange([card("c1", "Ada", { month: 6, day: 15 })], s, e);
|
||||
expect(new Set(out.map((b) => b.id)).size).toBe(out.length);
|
||||
|
||||
@@ -6,7 +6,7 @@ import { DEFAULT_APP_NAME } from "@/lib/brand";
|
||||
*
|
||||
* `APP_NAME` is a runtime variable, so every place showing the name has to ask
|
||||
* the server rather than have it written in. The sign-in page did not (#236's
|
||||
* neighbour): it fetched `/api/config`, received the name and used only
|
||||
* neighbor): it fetched `/api/config`, received the name and used only
|
||||
* `sourceUrl`, so a rebranded instance still said "ihasmail" on the page a new
|
||||
* user meets first. These pin the shape of the answer rather than the name.
|
||||
*/
|
||||
|
||||
@@ -97,14 +97,14 @@ describe("explicit date formats", () => {
|
||||
});
|
||||
|
||||
describe("clock preference", () => {
|
||||
it("honours 24-hour regardless of locale", () => {
|
||||
it("honors 24-hour regardless of locale", () => {
|
||||
setDateTimePrefs({ locale: "en-US", timeFormat: "24" });
|
||||
expect(formatClock(SAMPLE)).toBe("18:23");
|
||||
expect(uses24Hour()).toBe(true);
|
||||
expect(formatHourLabel(13)).toBe("13");
|
||||
expect(formatHourLabel(9)).toBe("09");
|
||||
});
|
||||
it("honours 12-hour regardless of locale", () => {
|
||||
it("honors 12-hour regardless of locale", () => {
|
||||
setDateTimePrefs({ locale: "de-DE", timeFormat: "12" });
|
||||
expect(formatClock(SAMPLE)).toBe("6:23 PM");
|
||||
expect(uses24Hour()).toBe(false);
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* 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 catalogue, and
|
||||
* 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";
|
||||
@@ -12,7 +12,7 @@ import { setUiLanguageForFormatting } from "../datetime";
|
||||
import { setCatalog } from "../i18n";
|
||||
|
||||
describe("sieve describeRule", () => {
|
||||
it("names the header and operator through the catalogue", () => {
|
||||
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" }],
|
||||
@@ -50,7 +50,7 @@ describe("recurrence describeRule", () => {
|
||||
expect(describeRecurrence({ "@type": "RecurrenceRule", frequency: "daily", interval: 3 } as never)).toBe("Every 3 days");
|
||||
});
|
||||
|
||||
it("recognises Monday to Friday as every weekday", () => {
|
||||
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 })),
|
||||
@@ -80,12 +80,12 @@ describe("recurrence describeRule", () => {
|
||||
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 catalogue keys and come from Intl instead.
|
||||
// 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 catalogue", () => {
|
||||
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: {} });
|
||||
|
||||
@@ -93,14 +93,14 @@ describe("canMoveFolderTo", () => {
|
||||
});
|
||||
|
||||
describe("folderColor", () => {
|
||||
it("returns the colour chosen for that folder, and null for the rest", () => {
|
||||
it("returns the color chosen for that folder, and null for the rest", () => {
|
||||
const colors = { work: "#7c3aed" };
|
||||
expect(folderColor(colors, "work")).toBe("#7c3aed");
|
||||
expect(folderColor(colors, "news")).toBeNull();
|
||||
expect(folderColor({}, "work")).toBeNull();
|
||||
});
|
||||
|
||||
it("is keyed by id, so a renamed folder keeps its colour", () => {
|
||||
it("is keyed by id, so a renamed folder keeps its color", () => {
|
||||
// The id is stable across a rename; the name and path are not.
|
||||
expect(folderColor({ mb1: "#0f766e" }, "mb1")).toBe("#0f766e");
|
||||
});
|
||||
|
||||
@@ -34,7 +34,7 @@ describe("sanitizeEmailHtml", () => {
|
||||
});
|
||||
|
||||
describe("htmlDeclaresColors", () => {
|
||||
it("is false for mail that brings no colours", () => {
|
||||
it("is false for mail that brings no colors", () => {
|
||||
expect(htmlDeclaresColors("<p>Hi there</p>")).toBe(false);
|
||||
expect(htmlDeclaresColors("<div><b>bold</b> and <i>italic</i></div>", "font-family:Arial")).toBe(false);
|
||||
expect(htmlDeclaresColors('<a href="https://x.io/?color=red">link</a>')).toBe(false);
|
||||
@@ -54,9 +54,9 @@ describe("htmlDeclaresColors", () => {
|
||||
/**
|
||||
* Forcing the theme onto mail that styles itself — issue #290.
|
||||
*
|
||||
* The switch above it leaves nearly all HTML mail alone, because one colour
|
||||
* The switch above it leaves nearly all HTML mail alone, because one color
|
||||
* anywhere opts a message out. What this half has to get right is telling a
|
||||
* sheet the design sits on from a surface painted on top of it: neutralise the
|
||||
* sheet the design sits on from a surface painted on top of it: neutralize the
|
||||
* first and the white card goes away, keep the second and a button keeps a
|
||||
* label you can still read.
|
||||
*/
|
||||
@@ -70,15 +70,15 @@ describe("relativeLuminance", () => {
|
||||
expect(relativeLuminance("rgba(255,255,255,0.5)")).toBeCloseTo(1, 5);
|
||||
});
|
||||
|
||||
it("has nothing to say about a colour it cannot read", () => {
|
||||
it("has nothing to say about a color it cannot read", () => {
|
||||
// Not a failure: the caller treats null as "no deliberate surface", which
|
||||
// is the safe way round — an unreadable colour must not keep a white sheet.
|
||||
// is the safe way round — an unreadable color must not keep a white sheet.
|
||||
expect(relativeLuminance("color-mix(in srgb, red, blue)")).toBeNull();
|
||||
expect(relativeLuminance("var(--brand)")).toBeNull();
|
||||
expect(relativeLuminance("")).toBeNull();
|
||||
});
|
||||
|
||||
it("treats a fully transparent colour as painting nothing", () => {
|
||||
it("treats a fully transparent color as painting nothing", () => {
|
||||
expect(relativeLuminance("rgba(0,0,0,0)")).toBeNull();
|
||||
expect(relativeLuminance("transparent")).toBeNull();
|
||||
});
|
||||
@@ -100,21 +100,21 @@ describe("markKeptSurfaces", () => {
|
||||
* Marking is only half of it — the other half is the rule in EMAIL_BASE_CSS
|
||||
* that reads the marks, and #310 was a bug in that half rather than in the
|
||||
* marking. So these assert what the reader actually sees: does the
|
||||
* neutraliser hit this element? The selector is lifted out of the stylesheet
|
||||
* neutralizer hit this element? The selector is lifted out of the stylesheet
|
||||
* rather than copied, so a test cannot quietly drift from the rule it checks.
|
||||
*/
|
||||
const NEUTRALISER = (() => {
|
||||
const NEUTRALIZER = (() => {
|
||||
const m = EMAIL_BASE_CSS.match(
|
||||
/\.ihm-email-root\.forced\s+(\*:not\([^{]*?)\s*\{\s*color: inherit/,
|
||||
);
|
||||
if (!m) throw new Error("could not find the neutraliser rule in EMAIL_BASE_CSS");
|
||||
if (!m) throw new Error("could not find the neutralizer rule in EMAIL_BASE_CSS");
|
||||
return m[1]!.trim();
|
||||
})();
|
||||
|
||||
/** True when the theme is forced onto this element rather than leaving it alone. */
|
||||
const neutralised = (el: Element) => el.matches(NEUTRALISER);
|
||||
const neutralized = (el: Element) => el.matches(NEUTRALIZER);
|
||||
|
||||
it("keeps a coloured button and drops the white sheet around it", () => {
|
||||
it("keeps a colored button and drops the white sheet around it", () => {
|
||||
// The shape reported in #290: a Shopify/Klaviyo template whose outer 600px
|
||||
// wrapper carries bgcolor="#ffffff" and whose CTA carries bgcolor="#1155CC".
|
||||
const d = frag('<table bgcolor="#ffffff"><tr><td bgcolor="#1155CC"><a style="color:#FFFFFF">Buy</a></td></tr></table>');
|
||||
@@ -127,7 +127,7 @@ describe("markKeptSurfaces", () => {
|
||||
expect(d.querySelector("a")!.hasAttribute("data-ihm-in-keep")).toBe(true);
|
||||
});
|
||||
|
||||
it("neutralises a light panel nested inside a dark painted card", () => {
|
||||
it("neutralizes a light panel nested inside a dark painted card", () => {
|
||||
// The shape reported in #310: a dark Klaviyo campaign whose 600px cards
|
||||
// are dark enough to be marked, with light content tables inside them.
|
||||
// Those tables used to inherit the card's exemption and render as beige
|
||||
@@ -153,11 +153,11 @@ describe("markKeptSurfaces", () => {
|
||||
// The fix, stated the way the reader experiences it: the nested sheet is
|
||||
// themed, and so is the copy inside it. Before #310 both were exempt for
|
||||
// being descendants of the card.
|
||||
expect(neutralised(nested)).toBe(true);
|
||||
expect(neutralised(d.querySelector("td")!)).toBe(true);
|
||||
expect(neutralized(nested)).toBe(true);
|
||||
expect(neutralized(d.querySelector("td")!)).toBe(true);
|
||||
// The card itself is still left alone, and the page surround still goes.
|
||||
expect(neutralised(card)).toBe(false);
|
||||
expect(neutralised(surround)).toBe(true);
|
||||
expect(neutralized(card)).toBe(false);
|
||||
expect(neutralized(surround)).toBe(true);
|
||||
});
|
||||
|
||||
it("still keeps a button that sits inside a nested light panel", () => {
|
||||
@@ -172,11 +172,11 @@ describe("markKeptSurfaces", () => {
|
||||
'</div>',
|
||||
);
|
||||
expect(markKeptSurfaces(d)).toBe(2);
|
||||
expect(neutralised(d.querySelector("table")!)).toBe(true);
|
||||
expect(neutralised(d.querySelector("td")!)).toBe(false);
|
||||
expect(neutralized(d.querySelector("table")!)).toBe(true);
|
||||
expect(neutralized(d.querySelector("td")!)).toBe(false);
|
||||
// The label keeps its white, which is the thing #294 bought and this must
|
||||
// not spend.
|
||||
expect(neutralised(d.querySelector("a")!)).toBe(false);
|
||||
expect(neutralized(d.querySelector("a")!)).toBe(false);
|
||||
});
|
||||
|
||||
it("leaves no light panel exempt across the whole reported specimen", () => {
|
||||
@@ -201,7 +201,7 @@ describe("markKeptSurfaces", () => {
|
||||
expect(panels.length).toBe(21);
|
||||
|
||||
expect(markKeptSurfaces(d)).toBe(7);
|
||||
expect(panels.filter((p) => !neutralised(p))).toHaveLength(0);
|
||||
expect(panels.filter((p) => !neutralized(p))).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("reads an inline background as well as the attribute", () => {
|
||||
@@ -233,7 +233,7 @@ describe("markKeptSurfaces", () => {
|
||||
* The control that actually stops it is layout containment on an ancestor of
|
||||
* the shadow host, which mail CSS has no selector for; that lives in app.css
|
||||
* and is asserted at the bottom of this file, because jsdom does no layout and
|
||||
* cannot prove it here. These cover the second line of defence.
|
||||
* cannot prove it here. These cover the second line of defense.
|
||||
*/
|
||||
describe("mail CSS cannot climb out of its card", () => {
|
||||
const render = (html: string) => sanitizeEmailHtml(html).html;
|
||||
@@ -271,7 +271,7 @@ describe("mail CSS cannot climb out of its card", () => {
|
||||
describe("the containment that mail CSS cannot override", () => {
|
||||
it("is still applied to the message body container", async () => {
|
||||
// jsdom does no layout, so this asserts the control is present rather than
|
||||
// that it works; the behaviour was verified in a real browser. Without it,
|
||||
// that it works; the behavior was verified in a real browser. Without it,
|
||||
// a message can cover the viewport regardless of what the sanitizer does.
|
||||
const { readFile } = await import("node:fs/promises");
|
||||
const { join } = await import("node:path");
|
||||
|
||||
@@ -36,7 +36,7 @@ describe("deciding whether a message has an HTML alternative", () => {
|
||||
expect(hasHtmlAlternative({ type: "text/htmlish" }, "<p>Hi</p>")).toBe(false);
|
||||
});
|
||||
|
||||
it("matches the type case-insensitively, since a header may be capitalised", () => {
|
||||
it("matches the type case-insensitively, since a header may be capitalized", () => {
|
||||
expect(hasHtmlAlternative({ type: "TEXT/HTML" }, "<p>Hi</p>")).toBe(true);
|
||||
});
|
||||
|
||||
|
||||
@@ -29,12 +29,12 @@ describe("t", () => {
|
||||
expect(currentLanguage()).toBe("en");
|
||||
});
|
||||
|
||||
it("translates once a catalogue is in force", () => {
|
||||
it("translates once a catalog is in force", () => {
|
||||
setCatalog("de", de);
|
||||
expect(t("Archive")).toBe("Archivieren");
|
||||
});
|
||||
|
||||
it("falls back per string, not per catalogue", () => {
|
||||
it("falls back per string, not per catalog", () => {
|
||||
setCatalog("de", de);
|
||||
expect(t("Report spam")).toBe("Report spam");
|
||||
});
|
||||
@@ -60,7 +60,7 @@ describe("interpolation", () => {
|
||||
describe("plural", () => {
|
||||
const FORMS = { one: "{n} message", other: "{n} messages" };
|
||||
|
||||
it("picks the English form without a catalogue", () => {
|
||||
it("picks the English form without a catalog", () => {
|
||||
expect(plural(1, FORMS)).toBe("1 message");
|
||||
expect(plural(0, FORMS)).toBe("0 messages");
|
||||
expect(plural(5, FORMS)).toBe("5 messages");
|
||||
@@ -73,7 +73,7 @@ describe("plural", () => {
|
||||
expect(plural(7, FORMS)).toBe("7 сообщений"); // many
|
||||
});
|
||||
|
||||
it("falls back to `other` when the catalogue lacks the category", () => {
|
||||
it("falls back to `other` when the catalog lacks the category", () => {
|
||||
setCatalog("de", de);
|
||||
// German has no "few"; asking for 3 must not render undefined.
|
||||
expect(plural(3, FORMS)).toBe("3 Nachrichten");
|
||||
@@ -95,7 +95,7 @@ describe("tNode", () => {
|
||||
|
||||
it("lets a translator move the element", () => {
|
||||
// Splitting the sentence into two t() calls could not do this: the
|
||||
// fragments would render in the English order whatever the catalogue said.
|
||||
// fragments would render in the English order whatever the catalog said.
|
||||
setCatalog("de", de);
|
||||
expect(render(tNode("Open {scheme} links here", { scheme: <code>mailto:</code> })))
|
||||
.toBe("<code>mailto:</code>-Links hier öffnen");
|
||||
|
||||
@@ -111,7 +111,7 @@ describe("parseIcsDuration", () => {
|
||||
});
|
||||
|
||||
describe("looksLikeCalendar", () => {
|
||||
it("recognises a calendar and rejects an error page", () => {
|
||||
it("recognizes a calendar and rejects an error page", () => {
|
||||
expect(looksLikeCalendar("BEGIN:VCALENDAR\r\nEND:VCALENDAR")).toBe(true);
|
||||
expect(looksLikeCalendar("<!doctype html><title>404</title>")).toBe(false);
|
||||
});
|
||||
|
||||
@@ -30,7 +30,7 @@ const find = (e: JSCalendarEvent[], prefix: string) => eventLines(e).filter((l)
|
||||
const one = (e: JSCalendarEvent, prefix: string) => find([e], prefix)[0];
|
||||
|
||||
describe("the document around the events", () => {
|
||||
it("is a calendar a reader will recognise", () => {
|
||||
it("is a calendar a reader will recognize", () => {
|
||||
const l = lines([base]);
|
||||
expect(l[0]).toBe("BEGIN:VCALENDAR");
|
||||
expect(l).toContain("VERSION:2.0");
|
||||
@@ -105,7 +105,7 @@ describe("recurrence", () => {
|
||||
expect(one(e, "RRULE")).toBe("RRULE:FREQ=MONTHLY;BYDAY=-1TH");
|
||||
});
|
||||
|
||||
it("turns a cancelled occurrence into an EXDATE", () => {
|
||||
it("turns a canceled occurrence into an EXDATE", () => {
|
||||
const e = { ...weekly, recurrenceOverrides: { "2026-09-09T09:00:00": null } };
|
||||
expect(one(e, "EXDATE")).toBe("EXDATE;TZID=Europe/Berlin:20260909T090000");
|
||||
expect(find([e], "BEGIN:VEVENT")).toHaveLength(1);
|
||||
@@ -166,7 +166,7 @@ describe("the rest of an event", () => {
|
||||
expect(one(e, "TRANSP")).toBe("TRANSP:TRANSPARENT");
|
||||
});
|
||||
|
||||
it("writes the organiser and the guests, with what each answered", () => {
|
||||
it("writes the organizer and the guests, with what each answered", () => {
|
||||
const e = {
|
||||
...base,
|
||||
organizerCalendarAddress: "mailto:[email protected]",
|
||||
|
||||
@@ -19,7 +19,7 @@ describe("resolveUiLanguage", () => {
|
||||
});
|
||||
|
||||
it("refuses a language whose strings are not shipped", () => {
|
||||
// The account travels between machines and can outlive a catalogue. A
|
||||
// The account travels between machines and can outlive a catalog. A
|
||||
// page that says lang="fr" while rendering English is worse than one that
|
||||
// admits to English: it stops the reader translating it themselves.
|
||||
// Derived rather than named, so shipping another language does not turn
|
||||
@@ -30,7 +30,7 @@ describe("resolveUiLanguage", () => {
|
||||
});
|
||||
|
||||
it("carries the Beta flag until a person has signed the language off", () => {
|
||||
// Not a completeness measure. A catalogue can be word-for-word finished
|
||||
// Not a completeness measure. A catalog can be word-for-word finished
|
||||
// and still read like a machine wrote it, which is what this marks.
|
||||
// Every shipped language except English is unreviewed, and stays marked
|
||||
// until a person says otherwise.
|
||||
@@ -40,12 +40,12 @@ describe("resolveUiLanguage", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("honours one that is", () => {
|
||||
it("honors one that is", () => {
|
||||
for (const l of UI_LANGUAGES) expect(resolveUiLanguage(l.tag)).toBe(l.tag);
|
||||
});
|
||||
|
||||
it("only offers languages that resolve to themselves", () => {
|
||||
// Guards the ordering mistake: adding a picker entry before its catalogue.
|
||||
// Guards the ordering mistake: adding a picker entry before its catalog.
|
||||
for (const l of UI_LANGUAGES) {
|
||||
expect(resolveUiLanguage(l.tag)).toBe(l.tag);
|
||||
expect(l.name.trim()).not.toBe("");
|
||||
|
||||
@@ -94,7 +94,7 @@ describe("comparatorsFor, custom levels", () => {
|
||||
});
|
||||
|
||||
describe("optional sorts, which a server is allowed to refuse", () => {
|
||||
it("recognises the keyword properties", () => {
|
||||
it("recognizes the keyword properties", () => {
|
||||
expect(isOptionalSort({ property: "hasKeyword", keyword: "$seen" })).toBe(true);
|
||||
expect(isOptionalSort({ property: "someInThreadHaveKeyword", keyword: "$flagged" })).toBe(true);
|
||||
expect(isOptionalSort({ property: "receivedAt" })).toBe(false);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { isLocalisedName, mailboxDisplayName, mailboxDisplayPath } from "@/lib/mailboxName";
|
||||
import { isLocalizedName, mailboxDisplayName, mailboxDisplayPath } from "@/lib/mailboxName";
|
||||
import { setCatalog, type Catalog } from "@/lib/i18n";
|
||||
import type { Mailbox } from "@/jmap/types";
|
||||
|
||||
@@ -19,7 +19,7 @@ const mb = (id: string, name: string, role: string | null = null, parentId: stri
|
||||
afterEach(() => setCatalog("en", { strings: {}, plurals: {} }));
|
||||
|
||||
describe("mailboxDisplayName", () => {
|
||||
it("is the server's name until a catalogue says otherwise", () => {
|
||||
it("is the server's name until a catalog says otherwise", () => {
|
||||
expect(mailboxDisplayName(mb("1", "Deleted Items", "trash"))).toBe("Deleted Items");
|
||||
});
|
||||
|
||||
@@ -43,18 +43,18 @@ describe("mailboxDisplayName", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("isLocalisedName", () => {
|
||||
describe("isLocalizedName", () => {
|
||||
it("tells an editor when the name on screen is not the server's", () => {
|
||||
// A rename box prefilled with "Papierkorb" would rename the folder to that
|
||||
// the moment somebody pressed Save — a real change made by accident.
|
||||
expect(isLocalisedName(mb("1", "Deleted Items", "trash"))).toBe(true);
|
||||
expect(isLocalisedName(mb("2", "Newsletters"))).toBe(false);
|
||||
expect(isLocalisedName(mb("3", "Work", "subscribed"))).toBe(false);
|
||||
expect(isLocalizedName(mb("1", "Deleted Items", "trash"))).toBe(true);
|
||||
expect(isLocalizedName(mb("2", "Newsletters"))).toBe(false);
|
||||
expect(isLocalizedName(mb("3", "Work", "subscribed"))).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("mailboxDisplayPath", () => {
|
||||
it("localises each part that has a role and leaves the rest", () => {
|
||||
it("localizes each part that has a role and leaves the rest", () => {
|
||||
setCatalog("de", de);
|
||||
const all = { a: mb("a", "Inbox", "inbox"), b: mb("b", "Projects", null, "a") };
|
||||
expect(mailboxDisplayPath(all.b!, all)).toBe("Posteingang / Projects");
|
||||
|
||||
@@ -35,7 +35,7 @@ describe("renderMarkdown", () => {
|
||||
/*
|
||||
* Markdown passes raw HTML through by design, and the file came from
|
||||
* somewhere else -- an upload, or a share from another account. Every one of
|
||||
* these renders as a script tag without a sanitiser.
|
||||
* these renders as a script tag without a sanitizer.
|
||||
*/
|
||||
it("takes out anything that would execute", () => {
|
||||
const html = renderMarkdown("<script>alert(1)</script>\n\n<img src=x onerror=alert(1)>\n\n<iframe src='https://evil.example'></iframe>\n");
|
||||
|
||||
@@ -85,7 +85,7 @@ describe("the rest of the schema", () => {
|
||||
expect(phones).toContainEqual(expect.objectContaining({ number: "3", features: { pager: true } }));
|
||||
});
|
||||
|
||||
it("reads the organisation, its units and the job title", () => {
|
||||
it("reads the organization, its units and the job title", () => {
|
||||
const c = card("dn: cn=X\ncn: X\no: Example Corp\nou: Research\nou: Optics\ntitle: Lens Grinder\n")!;
|
||||
expect(values(c.organizations)[0]).toMatchObject({
|
||||
name: "Example Corp",
|
||||
|
||||
@@ -5,7 +5,7 @@ describe("the palettes themselves", () => {
|
||||
it("has a light and a dark half for every one of them", () => {
|
||||
// The reason there is no "this palette is dark only" machinery: there is
|
||||
// no such palette. ihasmail's own gained a light half, and the override,
|
||||
// the toggle's memory and a greyed-out control all went with it.
|
||||
// the toggle's memory and a grayed-out control all went with it.
|
||||
expect(PALETTES.map((p) => p.id)).toEqual([
|
||||
"default", "ihasmail", "dracula", "gruvbox", "rose-pine", "tokyo-night",
|
||||
"catppuccin", "solarized", "ayu", "kanagawa", "everforest", "primer",
|
||||
@@ -75,7 +75,7 @@ describe("legacyTheme, read by a device still on an older build", () => {
|
||||
});
|
||||
|
||||
describe("toggleTarget", () => {
|
||||
it("flips the mode and keeps the colours, whatever the palette", () => {
|
||||
it("flips the mode and keeps the colors, whatever the palette", () => {
|
||||
for (const palette of ["default", "ihasmail", "gruvbox", "dracula", "rose-pine", "tokyo-night"] as const) {
|
||||
expect(toggleTarget({ palette, mode: "dark" }, false)).toEqual({ palette, mode: "light" });
|
||||
expect(toggleTarget({ palette, mode: "light" }, false)).toEqual({ palette, mode: "dark" });
|
||||
|
||||
@@ -30,7 +30,7 @@ describe("permission labels", () => {
|
||||
* does not: a missing one would show English in the middle of a translated
|
||||
* picker, and a stale one would never be looked up.
|
||||
*/
|
||||
describe("the permission catalogues", () => {
|
||||
describe("the permission catalogs", () => {
|
||||
const modules = import.meta.glob<{ permissionCatalog: PermissionCatalog }>("../../locales/permissions/*.ts");
|
||||
const tagOf = (path: string) => path.split("/").pop()!.replace(/\.ts$/, "");
|
||||
const languages = UI_LANGUAGES.map((l) => l.tag).filter((tag) => tag !== "en");
|
||||
|
||||
@@ -7,7 +7,7 @@ import { settingsAlreadyLoadedFor, stopSettingsSync } from "../settingsSync";
|
||||
* Settings used to live only in localStorage, so nothing followed the user
|
||||
* between devices — issue #54, whose sharpest case is the default identity:
|
||||
* with none set, the address that sorts first wins, so mail goes out from an
|
||||
* address the recipient may not recognise.
|
||||
* address the recipient may not recognize.
|
||||
*
|
||||
* The split is written as a list of exceptions, which means the interesting
|
||||
* test is not "does this key sync" but "does a key added later sync without
|
||||
|
||||
@@ -109,7 +109,7 @@ describe("sharing a file", () => {
|
||||
await expect(shareFile(aFile())).resolves.toBe("unsupported");
|
||||
});
|
||||
|
||||
it("raises anything it does not recognise, so a real fault is still reported", async () => {
|
||||
it("raises anything it does not recognize, so a real fault is still reported", async () => {
|
||||
stubNavigator({
|
||||
share: vi.fn(async () => { throw new DOMException("boom", "DataError"); }),
|
||||
canShare: (() => true) as unknown as Navigator["canShare"],
|
||||
|
||||
@@ -2,7 +2,7 @@ import { describe, expect, it } from "vitest";
|
||||
import { buildMarkerSignature, byteLength, compactHtml, markerOf, signatureTooLong, SIGNATURE_LIMIT } from "../signatureHtml";
|
||||
|
||||
describe("signature compaction", () => {
|
||||
it("strips office cruft and non-essential styles but keeps colours and links", () => {
|
||||
it("strips office cruft and non-essential styles but keeps colors and links", () => {
|
||||
const src = `<!--[if gte mso 9]><xml>x</xml><![endif]--><div class="WordSection1" style="mso-margin-top-alt:auto;line-height:115%;font-family:'Calibri',sans-serif;color:windowtext"><p class="MsoNormal" style="margin:0cm;font-size:11pt"><span lang="EN-US" style="font-size:12pt;color:#1F4E79;mso-fareast-language:EN-US"><b>John Coffey</b></span><o:p></o:p></p><p><span></span></p><a href="https://linuxexpert.org" target="_blank" data-x="1">linuxexpert.org</a><img src="https://x/y.png" width="100" style="mso-foo:bar"></div>`;
|
||||
const out = compactHtml(src);
|
||||
expect(out).not.toContain("mso-");
|
||||
|
||||
@@ -6,8 +6,8 @@ import { catalog as de } from "@/locales/de";
|
||||
|
||||
/**
|
||||
* The briefing is the only thing standing between a notification action and a
|
||||
* button labelled in a language the reader does not use — the worker is plain
|
||||
* JavaScript outside the bundle and cannot reach a catalogue.
|
||||
* button labeled in a language the reader does not use — the worker is plain
|
||||
* JavaScript outside the bundle and cannot reach a catalog.
|
||||
*
|
||||
* It is also the only place the archive mailbox is named, and getting that
|
||||
* wrong does not fail visibly: a message would be filed somewhere, just not
|
||||
@@ -45,7 +45,7 @@ describe("the worker's briefing", () => {
|
||||
});
|
||||
|
||||
it("carries the worker's text in the language the tab is in", async () => {
|
||||
// The worker has no catalogue. Everything it will say has to be said here
|
||||
// The worker has no catalog. Everything it will say has to be said here
|
||||
// first, or a German reader gets English buttons on their lock screen.
|
||||
setCatalog("de", de);
|
||||
const { store } = fakeCaches();
|
||||
|
||||
@@ -2,7 +2,7 @@ import { describe, expect, it } from "vitest";
|
||||
import { SWIPE_CHOICES, describeSwipe, type SwipeAction } from "../swipe";
|
||||
|
||||
/**
|
||||
* A swipe names what it is about to do on a coloured strip the reader sees for
|
||||
* A swipe names what it is about to do on a colored strip the reader sees for
|
||||
* about a third of a second before letting go. These check that the name is
|
||||
* true in the folder it is being read in — which is the whole reason the
|
||||
* descriptor exists rather than a fixed label per setting.
|
||||
|
||||
@@ -138,7 +138,7 @@ describe("remembering the palette you were on", () => {
|
||||
expect(toggleTarget(away, false)).toEqual({ palette: "ihasmail", mode: "dark" });
|
||||
});
|
||||
|
||||
it("keeps the colours when the palette has both sides", () => {
|
||||
it("keeps the colors when the palette has both sides", () => {
|
||||
const away = toggleTarget({ palette: "gruvbox", mode: "dark" }, false);
|
||||
expect(away.palette).toBe("gruvbox");
|
||||
expect(away.mode).toBe("light");
|
||||
|
||||
@@ -65,7 +65,7 @@ const file = (name: string, body: string, extra: Attr[] = []): Attr[] => [
|
||||
const text = (a: Uint8Array) => new TextDecoder().decode(a);
|
||||
|
||||
describe("isTnef", () => {
|
||||
it("recognises the types and the filename", () => {
|
||||
it("recognizes the types and the filename", () => {
|
||||
expect(isTnef("application/ms-tnef", null)).toBe(true);
|
||||
expect(isTnef("application/vnd.ms-tnef; name=winmail.dat", null)).toBe(true);
|
||||
expect(isTnef("application/octet-stream", "winmail.dat")).toBe(true);
|
||||
|
||||
@@ -27,7 +27,7 @@ describe("lockAxis", () => {
|
||||
expect(lockAxis(30, 25)).toBe("y");
|
||||
});
|
||||
|
||||
it("counts distance on either axis towards committing", () => {
|
||||
it("counts distance on either axis toward committing", () => {
|
||||
expect(lockAxis(0, AXIS_SLOP)).toBe("y");
|
||||
expect(lockAxis(AXIS_SLOP, 0)).toBe("x");
|
||||
});
|
||||
|
||||
@@ -5,7 +5,7 @@ import { createRoot, type Root } from "react-dom/client";
|
||||
/*
|
||||
* The guard's answers, and which of them the dialog leans on.
|
||||
*
|
||||
* It shipped with "Discard changes" as the only choice carrying a colour, which
|
||||
* It shipped with "Discard changes" as the only choice carrying a color, which
|
||||
* made losing the work the easy thing to click on a dialog whose entire purpose
|
||||
* is to stop that (#175). The emphasis belongs on the safe answer; the
|
||||
* destructive one stays legible as destructive without being the loudest thing
|
||||
|
||||
Reference in New Issue
Block a user