Files
SRCmail/integration/tests/06-shared-folders.spec.ts
T
Stefan Hildebrandt e1e83c4a83 test(integration): make reconcile-dependent counter assertions robust
The single `forceSync` + assert pattern flaked under full-suite load: one
reconcile can miss (or a shared/cross-account counter refresh lands late) with
no retry, so the assertion polls stale DOM until timeout. The flake moved
between reconcile-dependent tests (server-side move, spam source drain, unified
/ shared counters) run to run.

- Add expectFolderCountsSynced(): nudges a reconcile (visibilitychange ->
  checkForStateChanges) before *every* poll, so a missed reconcile is retried
  for the full window. Compares only the provided unread/total fields.
- Use it for the reconcile-dependent counter checks in 02 (move/delete),
  03 (multi-account isolation + unified aggregation), 04 (All Mail), 05 (spam
  source), 06 (shared folders); drop the now-redundant standalone forceSync.
  Pure live-push assertions (incoming/burst, background-login-live) keep the
  plain helpers so they still prove push works.
- The spam->not-spam round-trip could stall the Junk badge reconcile even with
  retries under load; assert the optimistic list removal + authoritative server
  round-trip (out of Junk, back in Inbox) instead of the badge.

Validated with two back-to-back full-suite runs: 35 passed each.
2026-07-11 21:15:57 +02:00

129 lines
5.0 KiB
TypeScript

import { test, expect } from '@playwright/test';
import { ACCOUNTS } from './helpers/config';
import { sendMail } from './helpers/smtp';
import { JmapClient } from './helpers/jmap';
import {
login,
expandSharedFolders,
folderRow,
openFolder,
expectFolderCountsSynced,
expectEmailVisible,
expectEmailUnread,
emailContextAction,
emailItem,
forceSync,
} from './helpers/app';
/**
* Shared (delegated) folders. Alice shares a custom folder — plus her Trash and
* Junk so delete/spam can route to the owner's system folders — with carol, who
* then acts on the mail from her own session and checks the shared counters.
*
* carol is the grantee (not asserted on by other specs), so the shared-account
* visibility this leaves in Stalwart's session cache doesn't leak elsewhere.
*/
const { alice, carol } = ACCOUNTS;
const SHARED = 'TeamShared';
let seq = 0;
const subj = (l: string) => `IT ${l} ${Date.now()}-${seq++}`;
test.describe('Shared folder actions', () => {
let ja: JmapClient; // owner (alice)
let sharedId: string;
test.beforeEach(async () => {
ja = await JmapClient.connect(alice.email, alice.password);
const jc = await JmapClient.connect(carol.email, carol.password);
await ja.reset();
await jc.reset();
// Delegate a custom folder + Trash + Junk to carol.
sharedId = await ja.createSharedFolder(SHARED, carol.email);
await ja.shareMailboxByRole('trash', carol.email);
await ja.shareMailboxByRole('junk', carol.email);
});
async function seedIntoShared(subject: string): Promise<void> {
await sendMail({ from: alice.email, authPass: alice.password, to: alice.email, subject, body: 'x' });
const m = await ja.waitForEmail(subject);
await ja.moveEmail(m.id, sharedId);
}
test('shared folder appears with its counter and message', async ({ page }) => {
const s = subj('sh-show');
await seedIntoShared(s);
await login(page, carol);
await expandSharedFolders(page, alice.email);
await expect(folderRow(page, { name: SHARED, shared: true }).first()).toBeVisible();
await expectFolderCountsSynced(page, { name: SHARED, shared: true }, { unread: 1 });
await openFolder(page, { name: SHARED, shared: true });
await forceSync(page);
await expectEmailVisible(page, s);
});
test('mark read/unread in a shared folder updates its counter', async ({ page }) => {
const s = subj('sh-read');
await seedIntoShared(s);
await login(page, carol);
await expandSharedFolders(page, alice.email);
await openFolder(page, { name: SHARED, shared: true });
await expectFolderCountsSynced(page, { name: SHARED, shared: true }, { unread: 1 });
await emailContextAction(page, s, 'ctx-mark-read');
await expectEmailUnread(page, s, false);
await expectFolderCountsSynced(page, { name: SHARED, shared: true }, { unread: 0 });
await emailContextAction(page, s, 'ctx-mark-unread');
await expectFolderCountsSynced(page, { name: SHARED, shared: true }, { unread: 1 });
// Owner sees the same state on the server.
const found = await ja.findEmailBySubject(s, sharedId);
expect(found.keywords?.$seen).toBeFalsy();
});
test('delete in a shared folder moves the message to the shared Trash', async ({ page }) => {
const s = subj('sh-del');
await seedIntoShared(s);
await login(page, carol);
await expandSharedFolders(page, alice.email);
await openFolder(page, { name: SHARED, shared: true });
await expectFolderCountsSynced(page, { name: SHARED, shared: true }, { total: 1 });
await emailContextAction(page, s, 'ctx-delete');
// Source shared folder drains, and the message really is in the owner's
// Trash on the server. (We assert the destination server-side rather than
// the shared Trash badge to keep the check independent of sidebar layout.)
await expectFolderCountsSynced(page, { name: SHARED, shared: true }, { total: 0 });
const trash = await ja.mailboxByRole('trash');
expect(await ja.findEmailBySubject(s, trash!.id), 'message in owner Trash').toBeTruthy();
});
test('mark as spam in a shared folder moves the message to the shared Junk', async ({ page }) => {
const s = subj('sh-spam');
await seedIntoShared(s);
await login(page, carol);
await expandSharedFolders(page, alice.email);
await openFolder(page, { name: SHARED, shared: true });
await expectFolderCountsSynced(page, { name: SHARED, shared: true }, { total: 1 });
await emailContextAction(page, s, 'ctx-spam');
// The message leaves the shared folder's list, and on the server it has
// moved to the owner's Junk and out of the shared folder. (Unlike delete,
// spam doesn't optimistically drain the source *counter*, and forceSync
// can't reconcile a shared account — so we assert list + server state.)
await expect(emailItem(page, s)).toHaveCount(0);
const junk = await ja.mailboxByRole('junk');
expect(await ja.findEmailBySubject(s, junk!.id), 'message in owner Junk').toBeTruthy();
expect(await ja.findEmailBySubject(s, sharedId), 'message no longer in shared folder').toBeFalsy();
});
});