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:
Stefan Hildebrandt
2026-07-11 21:15:20 +02:00
parent b8809c2e69
commit e05fbb2fe9
8 changed files with 529 additions and 15 deletions
+11
View File
@@ -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