test(integration): All Mail, message actions, and shared-folder sync
Extend the integration suite (now 22 tests) to cover: - All Mail view (04): single-account merge of Inbox + custom folders with Junk excluded, and cross-account aggregation across every logged-in account. - Message actions from the list context menu (05): mark read/unread, delete (→ Trash), mark-as-spam (→ Junk) and not-spam round-trip, verified on both the UI counters/row state and the server mailbox the message ends up in. - Shared/delegated folders (06): a delegated folder (+ Trash/Junk) shared alice→carol; the shared folder renders with its counter, and read/unread/ delete/spam performed there land correctly (server-verified). Hooks added: data-testid on context-menu delete/spam/read-unread items (via a testId prop on ContextMenuItem), data-shared on folder rows, and testId/data-expanded on sidebar section headers to drive the Shared section. Observations surfaced by the suite (asserted server-side / with a reconcile): - mark-as-spam doesn't optimistically decrement the *source* counter the way delete does; a visibility reconcile settles it. - shared *destination* counters (shared Trash/Junk) don't refresh live — forceSync reconciles the active account only, not shared accounts.
This commit is contained in:
@@ -295,6 +295,7 @@ export function EmailContextMenu({
|
||||
<ContextMenuItem
|
||||
icon={Trash2}
|
||||
label={t("delete")}
|
||||
testId="ctx-delete"
|
||||
onClick={() =>
|
||||
handleAction(showBatchActions ? onBatchDelete! : onDelete!)
|
||||
}
|
||||
@@ -410,6 +411,7 @@ export function EmailContextMenu({
|
||||
<ContextMenuItem
|
||||
icon={isInJunkFolder ? ShieldCheck : ShieldAlert}
|
||||
label={isInJunkFolder ? t("not_spam") : t("mark_as_spam")}
|
||||
testId={isInJunkFolder ? "ctx-not-spam" : "ctx-spam"}
|
||||
onClick={() =>
|
||||
handleAction(
|
||||
showBatchActions
|
||||
@@ -429,6 +431,7 @@ export function EmailContextMenu({
|
||||
<ContextMenuItem
|
||||
icon={isUnread ? MailOpen : Mail}
|
||||
label={isUnread ? t("mark_read") : t("mark_unread")}
|
||||
testId={isUnread ? "ctx-mark-read" : "ctx-mark-unread"}
|
||||
onClick={() =>
|
||||
handleAction(() =>
|
||||
showBatchActions
|
||||
|
||||
@@ -259,6 +259,7 @@ interface SidebarRowProps {
|
||||
testRole?: string | null;
|
||||
testName?: string;
|
||||
testMailboxId?: string;
|
||||
testShared?: boolean;
|
||||
}
|
||||
|
||||
function SidebarRow({
|
||||
@@ -282,6 +283,7 @@ function SidebarRow({
|
||||
testRole,
|
||||
testName,
|
||||
testMailboxId,
|
||||
testShared,
|
||||
}: SidebarRowProps) {
|
||||
const t = useTranslations('sidebar');
|
||||
const leftPad = isCollapsed ? 0 : ROW_PX_BASE + depth * INDENT_STEP;
|
||||
@@ -294,6 +296,7 @@ function SidebarRow({
|
||||
data-folder-role={testRole ?? undefined}
|
||||
data-folder-name={testName ?? undefined}
|
||||
data-mailbox-id={testMailboxId ?? undefined}
|
||||
data-shared={testShared ? 'true' : undefined}
|
||||
style={{ paddingBlock: 'var(--density-sidebar-py)' }}
|
||||
className={cn(
|
||||
"group w-full flex items-center max-lg:min-h-[44px] text-sm transition-colors duration-150",
|
||||
@@ -373,6 +376,7 @@ function SidebarSectionHeader({
|
||||
first,
|
||||
icon,
|
||||
sub,
|
||||
testId,
|
||||
}: {
|
||||
label: string;
|
||||
expanded: boolean;
|
||||
@@ -383,6 +387,7 @@ function SidebarSectionHeader({
|
||||
first?: boolean;
|
||||
icon?: ReactNode;
|
||||
sub?: boolean;
|
||||
testId?: string;
|
||||
}) {
|
||||
if (isCollapsed) {
|
||||
return first ? null : <div className="h-px bg-border/50 mx-2 my-2" aria-hidden />;
|
||||
@@ -397,6 +402,9 @@ function SidebarSectionHeader({
|
||||
return (
|
||||
<button
|
||||
onClick={onToggle}
|
||||
data-testid={testId}
|
||||
data-section-name={label}
|
||||
data-expanded={expanded ? 'true' : 'false'}
|
||||
className={cn(
|
||||
"group w-full flex items-center pb-1 select-none rounded-sm hover:bg-muted/40 transition-colors",
|
||||
paddingX,
|
||||
@@ -497,6 +505,7 @@ function MailboxTreeItem({
|
||||
testRole={node.role}
|
||||
testName={node.name}
|
||||
testMailboxId={node.id}
|
||||
testShared={node.isShared}
|
||||
depth={node.depth}
|
||||
isSelected={isSelected}
|
||||
isVirtual={isVirtualNode}
|
||||
@@ -1212,6 +1221,7 @@ export function Sidebar({
|
||||
expanded={sharedExpanded}
|
||||
onToggle={toggleShared}
|
||||
isCollapsed={isCollapsed}
|
||||
testId="section-shared"
|
||||
/>
|
||||
{((sharedExpanded && !isCollapsed) || isCollapsed) && (
|
||||
<>
|
||||
@@ -1226,6 +1236,7 @@ export function Sidebar({
|
||||
isCollapsed={isCollapsed}
|
||||
sub
|
||||
icon={<User className="w-3.5 h-3.5 text-muted-foreground" />}
|
||||
testId="section-shared-account"
|
||||
/>
|
||||
{accountExpanded && !isCollapsed && account.children.map((child) => (
|
||||
<MailboxTreeItem
|
||||
|
||||
@@ -108,6 +108,8 @@ interface ContextMenuItemProps {
|
||||
disabled?: boolean;
|
||||
destructive?: boolean;
|
||||
shortcut?: string;
|
||||
/** Stable hook for integration tests (not user-visible). */
|
||||
testId?: string;
|
||||
}
|
||||
|
||||
export function ContextMenuItem({
|
||||
@@ -117,10 +119,12 @@ export function ContextMenuItem({
|
||||
disabled = false,
|
||||
destructive = false,
|
||||
shortcut,
|
||||
testId,
|
||||
}: ContextMenuItemProps) {
|
||||
return (
|
||||
<button
|
||||
role="menuitem"
|
||||
data-testid={testId}
|
||||
disabled={disabled}
|
||||
className={cn(
|
||||
"w-full px-3 py-1.5 text-sm text-start flex items-center gap-2",
|
||||
|
||||
Reference in New Issue
Block a user