Files
LambdaMail/services/webmail/src/mail-state-integrity.test.ts
T
Lucas RayanandClaude Opus 5 64f1a2e145 fix(webmail): a barra lateral mostra so o que esta por ler
Mostrava tambem o tamanho de cada pasta. Sao numeros que nao mudam e
que ninguem esta a espera: enchiam a barra sem dizer nada de util.
Agora aparece um numero apenas onde ha correio por ler, e nada nas
restantes.

Consequencia a saber: Rascunhos nunca tem mensagens por ler - nada
marca um rascunho como lido - portanto passa a nao ter numero nenhum.
Quem quiser saber quantos rascunhos tem, ve-o no cabecalho da pasta.

O tamanho continua no tooltip do link, e o cabecalho da lista continua
a dize-lo por extenso, portanto a informacao nao se perdeu - deixou
apenas de estar sempre no campo de visao.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 15:30:17 -03:00

170 lines
7.3 KiB
TypeScript

import { describe, it, expect } from "vitest";
import { readFileSync } from "node:fs";
import { resolve } from "node:path";
/**
* The wiring behind the counters, the reader pane and the two surfaces.
*
* mailCounts.test.ts and emailBody.test.ts cover the rules themselves; these
* check that the screens actually go through them, because every defect in this
* file was a component computing the number or the markup inline where nothing
* could reach it.
*/
const read = (path: string) => readFileSync(resolve(process.cwd(), path), "utf8");
/** Strips comments, so a fix may name the code it removed without tripping. */
const code = (path: string) =>
read(path)
.replace(/\/\*[\s\S]*?\*\//g, "")
.replace(/\{\/\*[\s\S]*?\*\/\}/g, "")
.replace(/^\s*\/\/.*$/gm, "");
const USER_LAYOUT = "src/app/(user)/user/layout.tsx";
const MAIL_PAGE = "src/app/(user)/user/mail/[folder]/page.tsx";
const ADMIN_LOGIN = "src/app/(auth)/admin/login/page.tsx";
const USER_LOGIN = "src/app/(auth)/user/login/page.tsx";
describe("the counters the sidebar shows", () => {
// The badge was computed inline from folders.find(...).unread_count, which
// missed folders with no special-use role and printed nothing for Drafts.
it("goes through the tested counting rules", () => {
// folderBadge, not badgeCount: the sidebar needs to know which of the two
// numbers to emphasise, not just what the number is. Rendering was where
// the defect lived - a folder with nothing unread printed its total in the
// unread badge's place, so reading a message never moved it.
expect(code(USER_LAYOUT)).toContain("folderBadge");
expect(code(USER_LAYOUT)).not.toMatch(/unread_count\s*\?\?/);
});
// The sidebar shows what is waiting to be read and nothing else. It used to
// print the size of every folder beside its name - numbers that never change
// and that nobody is waiting on - and, worse, it printed the size in the
// unread badge's own place whenever nothing was unread, so an inbox of read
// mail showed "14" and reading another message never moved it.
it("renders only the unread count, never the folder size", () => {
const layout = code(USER_LAYOUT);
// The size survives in the tooltip, which is not the rail.
const rendered = layout.match(/\{badge\.total\}/g) ?? [];
expect(rendered).toHaveLength(0);
// Both folder templates - the standard ones and the user's own - guard
// their badge on there actually being unread mail.
expect(layout.match(/badge\.showsUnread/g) ?? []).toHaveLength(2);
});
// The folder list was fetched once on mount, so every badge froze at the
// value it had when the tab was opened: new mail did not raise it and reading
// a message did not lower it.
it("refreshes the folder list instead of fetching it once on mount", () => {
const layout = code(USER_LAYOUT);
expect(layout).toContain("useFolders");
expect(layout).not.toMatch(/useEffect\([^)]*\)\s*=>\s*\{\s*void fetch\("\/api\/v1\/mail\/folders"\)/);
});
it("re-reads the counters when the server pushes a mailbox event", () => {
expect(code("src/lib/useFolders.ts")).toContain("useMailEvents");
});
// Reading a message changes a counter that lives in another component, so the
// reader has to say so; nothing else can know.
it("gives the reader a way to announce that a counter moved", () => {
expect(code("src/lib/useFolders.ts")).toContain("MAIL_STATE_CHANGED");
expect(code(MAIL_PAGE)).toContain("notifyMailStateChanged");
});
});
describe("the counters the message list shows", () => {
// The header printed messages.length - the size of one page, capped at 50 -
// as though it were the size of the folder.
it("does not present the length of the loaded page as the folder total", () => {
const page = code(MAIL_PAGE);
expect(page).toContain("listHeaderCount");
expect(page).not.toContain("({messages.length})");
});
it("filters through the tested rule rather than a chain of inline predicates", () => {
expect(code(MAIL_PAGE)).toContain("filterMessages");
});
// A chip that filters by unread or by attachment is worth a number, and both
// were available on the page already.
it("shows how many messages each filter chip would keep", () => {
expect(code(MAIL_PAGE)).toContain("messageCounts");
});
});
describe("read and unread state", () => {
// Opening a message marked it read on the server, so the flag flipping back
// on the next reload was the list, the badge and the server disagreeing.
it("persists a read-state change through the API, not only in local state", () => {
expect(code(MAIL_PAGE)).toContain("/api/v1/mail/seen");
});
it("keeps the row in step through the tested helper", () => {
expect(code(MAIL_PAGE)).toContain("applySeen");
});
});
describe("the reader pane", () => {
it("resolves the message's own inline images", () => {
const page = code(MAIL_PAGE);
expect(page).toContain("resolveInlineImages");
expect(page).toContain("inline_images");
});
it("renders the body inside the tested document rather than a bare fragment", () => {
expect(code(MAIL_PAGE)).toContain("buildReaderDocument");
});
// A fixed 500px frame clipped every long message and left a slab of white
// under every short one.
it("sizes the frame from the message instead of pinning it to 500px", () => {
const page = code(MAIL_PAGE);
expect(page).not.toContain("h-[500px]");
expect(page).toContain("lm:reader-height");
// The height report needs script inside the frame, which sandbox="" forbids.
expect(page).toContain("allow-scripts");
});
// Every one of these was a Portuguese string typed into a component that had
// a translated bundle beside it, so the interface was half English for a
// Spanish reader and half Portuguese for an English one.
it("has no hardcoded strings left in the reader", () => {
const page = read(MAIL_PAGE);
for (const literal of ["Sem assunto", "Para:", "Imagens Habilitadas", "(sanitised)"]) {
expect(page).not.toContain(literal);
}
});
});
describe("moving between the two surfaces", () => {
// The mailbox is the account's own mail behind the account's own password.
it("does not ask the webmail sign-in for a second factor", () => {
const login = code(USER_LOGIN);
expect(login).not.toContain("mfa_required");
expect(login).not.toContain("challenge_token");
});
// Crossing into the console asked for the password again, though the session
// in hand had just proven it.
it("steps up into the console instead of sending the operator to a password form", () => {
expect(code(USER_LAYOUT)).toContain("/admin/step-up");
expect(code(USER_LAYOUT)).not.toContain('href="/admin/login"');
});
it("collects only the second factor on the step-up screen", () => {
const stepUp = code("src/app/(auth)/admin/step-up/page.tsx");
expect(stepUp).toContain("/api/v1/auth/admin/step-up");
expect(stepUp).not.toContain('type="password"');
});
// Leaving the console needs nothing: the admin sign-in now issues the webmail
// session too, so the link back cannot land on a login screen.
it("keeps a plain link out of the console", () => {
expect(code("src/app/(admin)/admin/layout.tsx")).toContain("/user/mail/inbox");
});
it("still requires the second factor at the admin sign-in itself", () => {
expect(code(ADMIN_LOGIN)).toContain("challenge_token");
});
});