fix(webmail): corrige os contadores das pastas e as ações do leitor
Todos estes foram reproduzidos na interface real, com a conta em
mail.lucasrguerra.dev.br, e não em teste.
Contadores das pastas. O badge caía para o total sempre que não havia
nada por ler, no mesmo lugar e no mesmo estilo do contador de não
lidas: uma caixa de entrada com catorze mensagens lidas mostrava "14",
e ler mais uma nunca movia o número - ele nunca esteve contando o que
o leitor pensava. Os dois números agora são visualmente distintos e o
de não lidas só aparece quando existe mensagem por ler.
Em Rascunhos o badge e o total são o mesmo número por definição, então
o sidebar imprimia-o duas vezes: "Rascunhos 1 1". A regra passou para
folderBadge, onde é testada, e o tamanho da pasta é renderizado num
único sítio.
Ações do leitor. Todas as mensagens ofereciam os mesmos quatro botões,
onde a pasta devia decidir. Um e-mail que o próprio utilizador enviou
oferecia "marcar como não lida" - uma flag que nada lê numa pasta onde
nada é entregue - e um rascunho por acabar oferecia Responder e
Encaminhar, mas nenhuma forma de o terminar. Abrir um rascunho agora
abre o compositor com o que estava escrito e adota o seu UID, para que
continuar a escrever substitua esse rascunho em vez de deixar o antigo
para trás. Em Lixeira o botão de apagar diz que apaga em definitivo,
porque é o que faz.
Anexos. t("mail.attachments") era chamado sem o parâmetro que a string
declara, e a interface mostrava literalmente "Anexos ({count})" nos
três idiomas. Na lista, que sabe que há anexos mas não quantos, passa a
nomear o facto em vez de imprimir uma contagem que não tem.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
1dc87a06c8
commit
203316f2c3
9 files changed
+296
-22
No files matched your search
@@ -163,6 +163,8 @@
|
||||
"draftSaved": "Draft saved automatically",
|
||||
"draftSaving": "Saving draft...",
|
||||
"drafts": "Drafts",
|
||||
"deleteForever": "Delete permanently",
|
||||
"continueDraft": "Continue writing",
|
||||
"emptyBody": "This message has no text body.",
|
||||
"forward": "Forward",
|
||||
"imagesEnabled": "Images loaded",
|
||||
|
||||
@@ -163,6 +163,8 @@
|
||||
"draftSaved": "Borrador guardado automáticamente",
|
||||
"draftSaving": "Guardando borrador...",
|
||||
"drafts": "Borradores",
|
||||
"deleteForever": "Eliminar definitivamente",
|
||||
"continueDraft": "Seguir escribiendo",
|
||||
"emptyBody": "Este mensaje no tiene cuerpo de texto.",
|
||||
"forward": "Reenviar",
|
||||
"imagesEnabled": "Imágenes cargadas",
|
||||
|
||||
@@ -163,6 +163,8 @@
|
||||
"draftSaved": "Rascunho salvo automaticamente",
|
||||
"draftSaving": "Salvando rascunho...",
|
||||
"drafts": "Rascunhos",
|
||||
"deleteForever": "Excluir definitivamente",
|
||||
"continueDraft": "Continuar escrevendo",
|
||||
"emptyBody": "Esta mensagem não tem corpo de texto.",
|
||||
"forward": "Encaminhar",
|
||||
"imagesEnabled": "Imagens carregadas",
|
||||
|
||||
@@ -38,7 +38,7 @@ interface AttachedFile {
|
||||
export default function ComposePage({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams?: Promise<{ replyTo?: string; subject?: string; inReplyTo?: string }>;
|
||||
searchParams?: Promise<{ replyTo?: string; subject?: string; inReplyTo?: string; draft?: string }>;
|
||||
}) {
|
||||
const t = useTranslations();
|
||||
const params = searchParams ? use(searchParams) : {};
|
||||
@@ -76,6 +76,43 @@ export default function ComposePage({
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Reopening a draft loads what was written into the composer, and adopts its
|
||||
// UID so carrying on writing replaces that draft instead of leaving the old
|
||||
// copy behind and starting a second one.
|
||||
//
|
||||
// Opening a draft used to land in the reader, which offered to reply to the
|
||||
// half-written message and gave no way to finish it.
|
||||
const draftParam = params?.draft;
|
||||
useEffect(() => {
|
||||
const uid = Number(draftParam);
|
||||
if (!Number.isFinite(uid) || uid <= 0) return;
|
||||
|
||||
let cancelled = false;
|
||||
void (async () => {
|
||||
try {
|
||||
const res = await fetch(`/api/v1/mail/message/${uid}?folder=drafts`, { cache: "no-store" });
|
||||
if (!res.ok || cancelled) return;
|
||||
const message = await res.json();
|
||||
if (cancelled) return;
|
||||
|
||||
draftUidRef.current = uid;
|
||||
setTo((message.to ?? []).join(", "));
|
||||
setCc((message.cc ?? []).join(", "));
|
||||
if ((message.cc ?? []).length > 0) setShowCcBcc(true);
|
||||
setSubject(message.subject ?? "");
|
||||
const html = message.html || message.text || "";
|
||||
if (editorRef.current) editorRef.current.innerHTML = html;
|
||||
setBody(html);
|
||||
} catch {
|
||||
// A draft that cannot be loaded leaves an empty composer rather than
|
||||
// a broken screen; the draft itself is untouched on the server.
|
||||
}
|
||||
})();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [draftParam]);
|
||||
|
||||
// Saves the draft for real. This used to be a setTimeout that set the words
|
||||
// "draft saved automatically" and nothing else - no request, no storage - so
|
||||
// closing the tab lost the message while the screen had just said otherwise.
|
||||
|
||||
@@ -24,7 +24,7 @@ import { useTranslations } from "../../../i18n/provider";
|
||||
import { LanguageSwitcher } from "../../../i18n/LanguageSwitcher";
|
||||
import { useAccount, isAdminRole } from "../../../lib/useAccount";
|
||||
import { useFolders } from "../../../lib/useFolders";
|
||||
import { badgeCount, folderMetrics } from "../../../lib/mailCounts";
|
||||
import { folderBadge } from "../../../lib/mailCounts";
|
||||
import { initialsFor } from "../../../lib/initials";
|
||||
|
||||
export default function UserWebmailLayout({ children }: { children: React.ReactNode }) {
|
||||
@@ -88,10 +88,11 @@ export default function UserWebmailLayout({ children }: { children: React.ReactN
|
||||
{navItems.map((item) => {
|
||||
const Icon = item.icon;
|
||||
const isActive = pathname === item.href || (item.role === "inbox" && pathname === "/user/mail");
|
||||
// Drafts counts what is waiting rather than what is unread; see
|
||||
// badgeCount, which is where that rule is tested.
|
||||
const badge = badgeCount(folders, item.role);
|
||||
const { total } = folderMetrics(folders, item.role);
|
||||
// Both numbers plus which one to emphasise; see folderBadge, where
|
||||
// that rule is tested. It used to fall back to printing the total in
|
||||
// the unread badge's place, so an inbox of read mail showed "14" and
|
||||
// reading another message never moved it.
|
||||
const badge = folderBadge(folders, item.role);
|
||||
|
||||
return (
|
||||
<Link
|
||||
@@ -99,7 +100,7 @@ export default function UserWebmailLayout({ children }: { children: React.ReactN
|
||||
href={item.href}
|
||||
onClick={() => setMobileOpen(false)}
|
||||
data-active={isActive}
|
||||
title={total > 0 ? t("mail.messagesInFolder", { count: total }) : item.label}
|
||||
title={badge.total > 0 ? t("mail.messagesInFolder", { count: badge.total }) : item.label}
|
||||
className="lm-nav"
|
||||
>
|
||||
<span className="lm-nav-mark" />
|
||||
@@ -109,19 +110,27 @@ export default function UserWebmailLayout({ children }: { children: React.ReactN
|
||||
{/* The folder's size, shown quietly beside the badge: the
|
||||
sidebar reported unread only, so there was nowhere in the
|
||||
interface that said how much mail a folder holds. */}
|
||||
{total > 0 && badge > 0 && (
|
||||
<span className="text-[10.5px] tabular-nums text-slate-500">{total}</span>
|
||||
)}
|
||||
{badge > 0 ? (
|
||||
{/* The folder size, always in the same quiet style, so it
|
||||
can never be mistaken for an unread count. */}
|
||||
{badge.total > 0 && (
|
||||
<span
|
||||
className={`text-[11.5px] tabular-nums ${isActive ? "text-indigo-300" : "text-slate-400"}`}
|
||||
className="text-[11.5px] tabular-nums text-slate-500"
|
||||
title={t("mail.messagesInFolder", { count: badge.total })}
|
||||
>
|
||||
{badge}
|
||||
{badge.total}
|
||||
</span>
|
||||
)}
|
||||
{/* The unread count, only when there is unread mail, and
|
||||
visibly distinct from the size beside it. */}
|
||||
{badge.showsUnread && (
|
||||
<span
|
||||
className={`rounded-full px-1.5 py-px text-[11px] font-medium tabular-nums ${
|
||||
isActive ? "bg-indigo-500/25 text-indigo-200" : "bg-indigo-500/20 text-indigo-300"
|
||||
}`}
|
||||
title={t("mail.unreadCount", { count: badge.unread })}
|
||||
>
|
||||
{badge.unread}
|
||||
</span>
|
||||
) : (
|
||||
total > 0 && (
|
||||
<span className="text-[11.5px] tabular-nums text-slate-500">{total}</span>
|
||||
)
|
||||
)}
|
||||
</span>
|
||||
</Link>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import React, { use, useCallback, useEffect, useMemo, useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import {
|
||||
Search,
|
||||
@@ -14,6 +15,7 @@ import {
|
||||
Clock,
|
||||
MailOpen,
|
||||
Download,
|
||||
Pencil,
|
||||
RefreshCw,
|
||||
Trash2,
|
||||
X,
|
||||
@@ -27,6 +29,7 @@ import {
|
||||
messageCounts,
|
||||
listHeaderCount,
|
||||
applySeen,
|
||||
readerActions,
|
||||
type ListFilter,
|
||||
} from "../../../../../lib/mailCounts";
|
||||
import { sanitizeEmailHtml, blockRemoteImages, unblockRemoteImages } from "../../../../../lib/sanitizer";
|
||||
@@ -120,6 +123,12 @@ export default function MailFolderPage({ params }: { params: Promise<{ folder: s
|
||||
|
||||
const { folders } = useFolders();
|
||||
const metrics = folderMetrics(folders, folder);
|
||||
const router = useRouter();
|
||||
// Which buttons this folder's messages deserve. The reader offered the same
|
||||
// four everywhere, so Sent and Drafts got a mark-as-unread that means
|
||||
// nothing there, drafts got Reply instead of a way to finish writing, and
|
||||
// nothing anywhere got a delete.
|
||||
const actions = readerActions(folder);
|
||||
|
||||
const loadMessages = useCallback(
|
||||
async (search: string) => {
|
||||
@@ -237,6 +246,12 @@ export default function MailFolderPage({ params }: { params: Promise<{ folder: s
|
||||
);
|
||||
|
||||
const openMessage = async (uid: number) => {
|
||||
// A draft is an unfinished message: opening it means carrying on writing,
|
||||
// not reading it in a pane that offers to reply to yourself.
|
||||
if (actions.canEdit) {
|
||||
router.push(`/user/compose?draft=${uid}`);
|
||||
return;
|
||||
}
|
||||
setLoadRemoteImages(false);
|
||||
setReaderHeight(420);
|
||||
try {
|
||||
@@ -444,7 +459,10 @@ export default function MailFolderPage({ params }: { params: Promise<{ folder: s
|
||||
<span className="mt-px flex items-center gap-1.5">
|
||||
<span className="inline-flex items-center gap-1 rounded-md bg-slate-800 px-2 py-0.5 text-[10.5px] text-slate-300">
|
||||
<Paperclip className="h-2.5 w-2.5" />
|
||||
{t("mail.attachments")}
|
||||
{/* The list knows only that a row has attachments,
|
||||
not how many, so it names the fact rather than
|
||||
printing a count it does not have. */}
|
||||
{t("mail.attachmentsFilter")}
|
||||
</span>
|
||||
</span>
|
||||
)}
|
||||
@@ -490,6 +508,18 @@ export default function MailFolderPage({ params }: { params: Promise<{ folder: s
|
||||
{/* Toolbar: closing, replying and read state all in one row, so
|
||||
the reader is not the only pane with no way out of itself. */}
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{/* A draft is unfinished, so the thing to do with it is carry
|
||||
on writing. It used to offer Reply and Forward on the
|
||||
user's own half-written message and no way to edit it. */}
|
||||
{actions.canEdit && (
|
||||
<Link href={`/user/compose?draft=${selected.uid}`}>
|
||||
<Button variant="primary" size="sm">
|
||||
<Pencil className="h-3.5 w-3.5" />
|
||||
<span>{t("mail.continueDraft")}</span>
|
||||
</Button>
|
||||
</Link>
|
||||
)}
|
||||
{actions.canReply && (
|
||||
<Link
|
||||
href={`/user/compose?replyTo=${encodeURIComponent(selected.from)}&subject=${encodeURIComponent(selected.subject)}`}
|
||||
>
|
||||
@@ -498,15 +528,21 @@ export default function MailFolderPage({ params }: { params: Promise<{ folder: s
|
||||
<span>{t("mail.reply")}</span>
|
||||
</Button>
|
||||
</Link>
|
||||
)}
|
||||
{/* Forward carries the subject but no recipient: it used to
|
||||
prefill the original sender, which sends the message
|
||||
straight back to whoever wrote it. */}
|
||||
{actions.canForward && (
|
||||
<Link href={`/user/compose?subject=${encodeURIComponent(selected.subject)}`}>
|
||||
<Button variant="secondary" size="sm">
|
||||
<Forward className="h-3.5 w-3.5" />
|
||||
<span>{t("mail.forward")}</span>
|
||||
</Button>
|
||||
</Link>
|
||||
)}
|
||||
{/* Not offered in Sent or Drafts: nothing delivers to those
|
||||
folders, so an unread flag there says nothing. */}
|
||||
{actions.canMarkUnread && (
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
@@ -518,14 +554,15 @@ export default function MailFolderPage({ params }: { params: Promise<{ folder: s
|
||||
<MailOpen className="h-3.5 w-3.5" />
|
||||
<span>{selectedRow?.seen === false ? t("mail.markRead") : t("mail.markUnread")}</span>
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={() => void deleteMessage(selected.uid)}
|
||||
title={t("common.delete")}
|
||||
title={actions.deleteIsPermanent ? t("mail.deleteForever") : t("common.delete")}
|
||||
>
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
<span>{t("common.delete")}</span>
|
||||
<span>{actions.deleteIsPermanent ? t("mail.deleteForever") : t("common.delete")}</span>
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
@@ -606,7 +643,7 @@ export default function MailFolderPage({ params }: { params: Promise<{ folder: s
|
||||
{selected.attachments.length > 0 && (
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="text-[11px] uppercase tracking-[0.08em] text-slate-400">
|
||||
{t("mail.attachments")}
|
||||
{t("mail.attachments", { count: selected?.attachments.length ?? 1 })}
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{selected.attachments.map((filename, idx) => (
|
||||
|
||||
@@ -120,3 +120,77 @@ export function listHeaderCount(input: {
|
||||
export function applySeen<T extends MessageSummary>(messages: T[], uid: number, seen: boolean): T[] {
|
||||
return messages.map((m) => (m.uid === uid ? { ...m, seen } : m));
|
||||
}
|
||||
|
||||
/** What the sidebar shows beside one folder. */
|
||||
export interface FolderBadge {
|
||||
/** How many unread messages the folder holds. */
|
||||
unread: number;
|
||||
/** How many messages it holds in total. */
|
||||
total: number;
|
||||
/**
|
||||
* Whether the unread number is worth showing at all.
|
||||
*
|
||||
* Kept apart from the number itself because the sidebar used to fall back to
|
||||
* printing the total in the unread badge's place whenever nothing was
|
||||
* unread. An inbox holding fourteen read messages therefore showed "14", and
|
||||
* reading another message never moved it - the number was never counting
|
||||
* what the user thought it was.
|
||||
*/
|
||||
showsUnread: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* The two numbers a sidebar folder has, and which of them to emphasise.
|
||||
*
|
||||
* Drafts deliberately has no unread badge: nothing marks a draft read, so its
|
||||
* unread count is always zero and the number that matters there is how many
|
||||
* unfinished messages are waiting - which is the total. Returning showsUnread
|
||||
* false for it is what stops the sidebar rendering that same total twice, once
|
||||
* as the size and once as the badge, which is where "Rascunhos 1 1" came from.
|
||||
*/
|
||||
export function folderBadge(
|
||||
folders: FolderSummary[] | undefined | null,
|
||||
route: string,
|
||||
): FolderBadge {
|
||||
const { unread, total } = folderMetrics(folders, route);
|
||||
const isDrafts = route.trim().toLowerCase() === "drafts";
|
||||
return { unread, total, showsUnread: !isDrafts && unread > 0 };
|
||||
}
|
||||
|
||||
/** Which actions a message offers, decided by the folder it is in. */
|
||||
export interface ReaderActions {
|
||||
canReply: boolean;
|
||||
canForward: boolean;
|
||||
/** Reopen an unfinished message in the composer. */
|
||||
canEdit: boolean;
|
||||
canMarkUnread: boolean;
|
||||
canDelete: boolean;
|
||||
/** True in Trash, where delete destroys rather than moves. */
|
||||
deleteIsPermanent: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* The actions that make sense for a message, given where it lives.
|
||||
*
|
||||
* The reader offered the same four buttons everywhere, so a message the user
|
||||
* had sent themselves could be "marked unread" - a flag nothing reads on a
|
||||
* folder nothing delivers to - and a half-written draft offered Reply and
|
||||
* Forward but no way to carry on writing it. Meanwhile delete was offered
|
||||
* nowhere at all.
|
||||
*/
|
||||
export function readerActions(folder: string): ReaderActions {
|
||||
const role = folder.trim().toLowerCase();
|
||||
const isDrafts = role === "drafts";
|
||||
const isOwnCopy = isDrafts || role === "sent";
|
||||
|
||||
return {
|
||||
canReply: !isDrafts,
|
||||
canForward: !isDrafts,
|
||||
canEdit: isDrafts,
|
||||
// Sent mail and drafts are the user's own copies. Nothing ever delivers
|
||||
// to those folders, so an unread flag there conveys nothing.
|
||||
canMarkUnread: !isOwnCopy,
|
||||
canDelete: true,
|
||||
deleteIsPermanent: role === "trash",
|
||||
};
|
||||
}
|
||||
@@ -28,10 +28,21 @@ 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", () => {
|
||||
expect(code(USER_LAYOUT)).toContain("badgeCount");
|
||||
// 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 total was rendered twice for Drafts, where the badge and the size are
|
||||
// the same number: "Rascunhos 1 1".
|
||||
it("renders the folder size in exactly one place", () => {
|
||||
const layout = code(USER_LAYOUT);
|
||||
expect(layout.match(/\{badge\.total\}/g) ?? []).toHaveLength(1);
|
||||
});
|
||||
|
||||
// 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.
|
||||
|
||||
@@ -156,3 +156,103 @@ describe("marking a message read in the loaded list", () => {
|
||||
expect(messages.find((m) => m.uid === 1)?.seen).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
// --- what the sidebar actually renders -----------------------------------
|
||||
|
||||
import { folderBadge, readerActions } from "./lib/mailCounts";
|
||||
|
||||
/**
|
||||
* These reproduce what the running webmail shows, which the badgeCount tests
|
||||
* above could not: they check the number, and the defect was in how the number
|
||||
* is displayed.
|
||||
*/
|
||||
describe("the badge a sidebar folder renders", () => {
|
||||
const folders = [
|
||||
{ special_use: "inbox", name: "INBOX", unread_count: 0, total_count: 14 },
|
||||
{ special_use: "drafts", name: "Drafts", unread_count: 0, total_count: 1 },
|
||||
{ special_use: "sent", name: "Sent", unread_count: 0, total_count: 1 },
|
||||
{ special_use: "junk", name: "Junk", unread_count: 3, total_count: 9 },
|
||||
{ special_use: "archive", name: "Archive", unread_count: 0, total_count: 0 },
|
||||
];
|
||||
|
||||
// The inbox showed "14" with nothing unread. Reading every message left it
|
||||
// at 14, because 14 was the total wearing the unread badge's clothes.
|
||||
it("does not show a total where an unread count belongs", () => {
|
||||
const badge = folderBadge(folders, "inbox");
|
||||
expect(badge.unread).toBe(0);
|
||||
expect(badge.showsUnread).toBe(false);
|
||||
});
|
||||
|
||||
it("still reports the folder size, separately from the unread count", () => {
|
||||
expect(folderBadge(folders, "inbox").total).toBe(14);
|
||||
});
|
||||
|
||||
// Drafts counted total as its badge, so total and badge were the same
|
||||
// number and the sidebar printed it twice: "Rascunhos 1 1".
|
||||
it("never renders the same number twice for drafts", () => {
|
||||
const badge = folderBadge(folders, "drafts");
|
||||
const rendered = [badge.showsUnread ? badge.unread : null, badge.total].filter(
|
||||
(v) => v !== null,
|
||||
);
|
||||
expect(rendered).toEqual([1]);
|
||||
});
|
||||
|
||||
it("shows the unread count when there really is unread mail", () => {
|
||||
const badge = folderBadge(folders, "junk");
|
||||
expect(badge.showsUnread).toBe(true);
|
||||
expect(badge.unread).toBe(3);
|
||||
expect(badge.total).toBe(9);
|
||||
});
|
||||
|
||||
it("shows nothing at all for an empty folder", () => {
|
||||
const badge = folderBadge(folders, "archive");
|
||||
expect(badge.showsUnread).toBe(false);
|
||||
expect(badge.total).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("the actions a message offers", () => {
|
||||
// "Mark as unread" was offered on a message the user sent themselves and on
|
||||
// their own half-written draft. Nothing reads those, so the flag means
|
||||
// nothing there and the button only invites a pointless round trip.
|
||||
it("does not offer mark-as-unread in Sent or Drafts", () => {
|
||||
expect(readerActions("sent").canMarkUnread).toBe(false);
|
||||
expect(readerActions("drafts").canMarkUnread).toBe(false);
|
||||
});
|
||||
|
||||
it("offers mark-as-unread in the folders that receive mail", () => {
|
||||
expect(readerActions("inbox").canMarkUnread).toBe(true);
|
||||
expect(readerActions("archive").canMarkUnread).toBe(true);
|
||||
});
|
||||
|
||||
// Reply and forward on your own unfinished draft make no sense either: the
|
||||
// thing to do with a draft is finish it.
|
||||
it("offers editing rather than replying on a draft", () => {
|
||||
const draft = readerActions("drafts");
|
||||
expect(draft.canEdit).toBe(true);
|
||||
expect(draft.canReply).toBe(false);
|
||||
expect(draft.canForward).toBe(false);
|
||||
});
|
||||
|
||||
it("offers reply and forward on received mail", () => {
|
||||
const inbox = readerActions("inbox");
|
||||
expect(inbox.canReply).toBe(true);
|
||||
expect(inbox.canForward).toBe(true);
|
||||
expect(inbox.canEdit).toBe(false);
|
||||
});
|
||||
|
||||
// Delete has to be offered everywhere: it was offered nowhere, which is why
|
||||
// a draft left behind by a sent message could not be removed at all.
|
||||
it("offers delete in every folder", () => {
|
||||
for (const folder of ["inbox", "sent", "drafts", "archive", "junk", "trash"]) {
|
||||
expect(readerActions(folder).canDelete).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
// In Trash the same button has to say it destroys the message, because there
|
||||
// it does - there is nowhere further to move it.
|
||||
it("says delete is permanent once in Trash", () => {
|
||||
expect(readerActions("trash").deleteIsPermanent).toBe(true);
|
||||
expect(readerActions("inbox").deleteIsPermanent).toBe(false);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user