test(integration): dockerized webmail⇆Stalwart Playwright sync suite
Add an end-to-end integration harness that runs the webmail against a real Stalwart mail server in Docker and drives it with Playwright, focused on the mail/folder synchronisation behaviour (unread/total counters, folder-list sync, account-scoped Unified Mailbox) that the unified-mailbox work touches. - integration/ stack: Stalwart (declarative bootstrap: alice/bob/carol, submission + IMAP listeners, permissive CORS) + webmail (dev mode, so the browser's plaintext cross-origin JMAP calls aren't blocked by the prod CSP). - Helpers: dependency-free SMTP submit client, JMAP client for seeding / inspecting server state, and page helpers (login, add/switch account, locale-independent folder-counter reads). - Specs: login, single-account sync (receive/read/move/delete/folder-create/ burst) and multi-account (per-account isolation + cross-account Unified Inbox aggregation + background-account delivery). 12 tests, all green. - Add focused data-testid hooks to the mail UI (folder rows + counters, email list items, account switcher, composer) for stable selectors. - Exclude examples/ and integration/ from tsconfig/eslint/.dockerignore.
This commit is contained in:
committed by
Linus Rath
parent
4dc76bbb47
commit
8d8bc7cb13
@@ -151,6 +151,8 @@ export function AccountSwitcher({ variant = "rail", className }: AccountSwitcher
|
||||
<button
|
||||
ref={buttonRef}
|
||||
onClick={() => setOpen(!open)}
|
||||
data-testid="account-switcher"
|
||||
data-active-account-id={activeAccountId ?? undefined}
|
||||
className={cn(
|
||||
"flex items-center gap-2 rounded-md transition-colors",
|
||||
variant === "rail"
|
||||
@@ -213,6 +215,9 @@ export function AccountSwitcher({ variant = "rail", className }: AccountSwitcher
|
||||
>
|
||||
<button
|
||||
onClick={() => handleSwitch(account.id)}
|
||||
data-testid="account-option"
|
||||
data-account-id={account.id}
|
||||
data-account-email={account.email || account.username}
|
||||
className={cn(
|
||||
"w-full flex items-start gap-3 px-3 py-2.5 text-start transition-colors",
|
||||
isActive ? "bg-accent/50" : "hover:bg-muted",
|
||||
@@ -274,6 +279,7 @@ export function AccountSwitcher({ variant = "rail", className }: AccountSwitcher
|
||||
<div className="border-t border-border">
|
||||
<button
|
||||
onClick={handleAddAccount}
|
||||
data-testid="add-account"
|
||||
className="w-full flex items-center gap-2 px-3 py-2 text-sm text-foreground hover:bg-muted transition-colors"
|
||||
role="menuitem"
|
||||
>
|
||||
|
||||
@@ -220,8 +220,13 @@ function SidebarRowCounts({
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<span className="ms-2 flex-shrink-0 flex items-baseline gap-1" title={totalCount > 0 ? `${unreadCount} unread / ${totalCount} total` : `${unreadCount} unread`}>
|
||||
|
||||
<span
|
||||
className="ms-2 flex-shrink-0 flex items-baseline gap-1"
|
||||
title={totalCount > 0 ? `${unreadCount} unread / ${totalCount} total` : `${unreadCount} unread`}
|
||||
data-testid="folder-counts"
|
||||
data-unread={unreadCount}
|
||||
data-total={totalCount}
|
||||
>
|
||||
{unreadNode}
|
||||
{unreadCount > 0 && totalCount > 0 && (
|
||||
<span className="text-xs text-muted-foreground/60">/</span>
|
||||
@@ -249,6 +254,10 @@ interface SidebarRowProps {
|
||||
isValidDropTarget?: boolean;
|
||||
isInvalidDropTarget?: boolean;
|
||||
onContextMenu?: (e: React.MouseEvent) => void;
|
||||
/** Stable identifiers for integration tests (not user-visible). */
|
||||
testRole?: string | null;
|
||||
testName?: string;
|
||||
testMailboxId?: string;
|
||||
}
|
||||
|
||||
function SidebarRow({
|
||||
@@ -269,6 +278,9 @@ function SidebarRow({
|
||||
isValidDropTarget,
|
||||
isInvalidDropTarget,
|
||||
onContextMenu,
|
||||
testRole,
|
||||
testName,
|
||||
testMailboxId,
|
||||
}: SidebarRowProps) {
|
||||
const t = useTranslations('sidebar');
|
||||
const leftPad = isCollapsed ? 0 : ROW_PX_BASE + depth * INDENT_STEP;
|
||||
@@ -277,6 +289,10 @@ function SidebarRow({
|
||||
<div
|
||||
{...(dropHandlers || {})}
|
||||
onContextMenu={onContextMenu}
|
||||
data-testid="folder-row"
|
||||
data-folder-role={testRole ?? undefined}
|
||||
data-folder-name={testName ?? undefined}
|
||||
data-mailbox-id={testMailboxId ?? 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",
|
||||
@@ -477,6 +493,9 @@ function MailboxTreeItem({
|
||||
<SidebarRow
|
||||
icon={<Icon className={getIconClass(isSelected, isVirtualNode, colorful, roleKey)} />}
|
||||
label={label}
|
||||
testRole={node.role}
|
||||
testName={node.name}
|
||||
testMailboxId={node.id}
|
||||
depth={node.depth}
|
||||
isSelected={isSelected}
|
||||
isVirtual={isVirtualNode}
|
||||
@@ -1047,6 +1066,9 @@ export function Sidebar({
|
||||
key={unifiedId}
|
||||
icon={<Icon className={getIconClass(isSelected, false, colorfulSidebarIcons, count.role)} />}
|
||||
label={t(`unified_${count.role}`)}
|
||||
testRole={count.role}
|
||||
testName={`unified-${count.role}`}
|
||||
testMailboxId={unifiedId}
|
||||
depth={0}
|
||||
isSelected={isSelected}
|
||||
unread={count.unreadEmails}
|
||||
@@ -1068,6 +1090,8 @@ export function Sidebar({
|
||||
key={id}
|
||||
icon={<Icon className={getIconClass(isSelected, false, colorfulSidebarIcons)} />}
|
||||
label={label}
|
||||
testName={id}
|
||||
testMailboxId={id}
|
||||
depth={0}
|
||||
isSelected={isSelected}
|
||||
unread={unread}
|
||||
|
||||
Reference in New Issue
Block a user