diff --git a/integration/README.md b/integration/README.md index c322ee75..c1fe6111 100644 --- a/integration/README.md +++ b/integration/README.md @@ -93,10 +93,14 @@ integration/ Some tests assert server-side truth (or use `test.fail` to pin a known gap) because the UI behaviour is currently incomplete. Worth a look: -- **Shared destination counters don't refresh live.** `forceSync` - (visibilitychange) reconciles only the *active* account, so moving a message - into a shared Trash/Junk/folder doesn't update that shared folder's sidebar - badge until a full reload. Source counters and server state are correct. +- **Shared-account counters now reconcile on focus/interval** (`09-live-counters`). + Stalwart's SSE only pushes StateChange for the *primary* account, so a + background change in a shared/delegated account is never pushed. The client + now also polls the session's secondary accounts, so their folder badges and + the unified/All-Mail counter refresh on the visibility reconcile and on a slow + background poll. (A *login* account already updates live via its own SSE.) + Note: these shared counters still don't update the instant a local action + runs — they follow the reconcile, not the optimistic path. - **`mark-as-spam` doesn't optimistically decrement the source counter** the way `delete` does; it settles after a reconcile. - **Reopening a draft resets the From selector** to the default identity even diff --git a/integration/tests/09-live-counters.spec.ts b/integration/tests/09-live-counters.spec.ts new file mode 100644 index 00000000..04bb9101 --- /dev/null +++ b/integration/tests/09-live-counters.spec.ts @@ -0,0 +1,74 @@ +import { test, expect } from '@playwright/test'; +import { ACCOUNTS } from './helpers/config'; +import { sendMail } from './helpers/smtp'; +import { JmapClient } from './helpers/jmap'; +import { + login, + addAccount, + seedUnifiedSettings, + seedAllMailSettings, + expandSharedFolders, + folderCounts, + expectFolderUnread, + forceSync, +} from './helpers/app'; + +/** + * Live currency of the unified / All-Mail counters across every source folder. + * + * Stalwart's SSE only pushes StateChange for the *primary* account, so: + * - a background *login* account updates the badge live (each login has its + * own SSE) — asserted with no reconcile; + * - a *shared/delegated* account gets no push at all, so the client polls the + * session's secondary accounts too; the badge reconciles on focus/interval. + * (Regression test for the shared-account state-poll.) + */ +const { alice, bob, carol } = ACCOUNTS; +const subj = (l: string) => `IT ${l} ${Date.now()}`; + +async function deliverIntoSharedFolder(owner: JmapClient, folderId: string, subject: string) { + const acct = ACCOUNTS.alice; + await sendMail({ from: acct.email, authPass: acct.password, to: acct.email, subject, body: 'x' }); + const m = await owner.waitForEmail(subject); + await owner.moveEmail(m.id, folderId); +} + +test.describe('Live unified/All-Mail counters', () => { + test('a background login account updates the unified counter live (no reconcile)', async ({ page }) => { + for (const a of [alice, bob]) { + const j = await JmapClient.connect(a.email, a.password); + await j.reset(); + } + await seedUnifiedSettings(page); + await login(page, alice); + await addAccount(page, bob); // bob active, alice in the background + await expectFolderUnread(page, { name: 'unified-inbox' }, 0); + + // Mail lands in alice's inbox while bob is active — no focus/forceSync here. + await sendMail({ from: alice.email, authPass: alice.password, to: alice.email, subject: subj('bg-live'), body: 'x' }); + await expectFolderUnread(page, { name: 'unified-inbox' }, 1); + }); + + test('a shared-folder change reconciles the All-Mail counter on focus', async ({ page }) => { + const ja = await JmapClient.connect(alice.email, alice.password); + const jc = await JmapClient.connect(carol.email, carol.password); + await ja.reset(); + await jc.reset(); + const shared = await ja.createSharedFolder('TeamShared', carol.email); + + await seedAllMailSettings(page, { crossAccount: false }); + await login(page, carol); + await expandSharedFolders(page, alice.email); + expect((await folderCounts(page, { name: '__cross_all__' })).unread).toBe(0); + + // A background change in the shared (delegated) account gets no SSE push. + await deliverIntoSharedFolder(ja, shared, subj('sh-live')); + + // Focus reconcile now polls the shared account too, so the All-Mail badge + // picks up the shared folder's new unread. + await forceSync(page); + await expect + .poll(async () => (await folderCounts(page, { name: '__cross_all__' })).unread, { timeout: 15000 }) + .toBe(1); + }); +});