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.
135 lines
5.1 KiB
TypeScript
135 lines
5.1 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,
|
|
expectFolderUnread,
|
|
expectFolderTotal,
|
|
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 expectFolderUnread(page, { name: SHARED, shared: true }, 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 forceSync(page);
|
|
await expectFolderUnread(page, { name: SHARED, shared: true }, 1);
|
|
|
|
await emailContextAction(page, s, 'ctx-mark-read');
|
|
await expectEmailUnread(page, s, false);
|
|
await expectFolderUnread(page, { name: SHARED, shared: true }, 0);
|
|
|
|
await emailContextAction(page, s, 'ctx-mark-unread');
|
|
await expectFolderUnread(page, { name: SHARED, shared: true }, 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 forceSync(page);
|
|
await expectFolderTotal(page, { name: SHARED, shared: true }, 1);
|
|
|
|
await emailContextAction(page, s, 'ctx-delete');
|
|
await forceSync(page);
|
|
|
|
// Source shared folder drains, and the message really is in the owner's
|
|
// Trash on the server. (Note: the shared *destination* counter — the shared
|
|
// Trash — does NOT refresh live in the sidebar; forceSync reconciles the
|
|
// active account only, not shared-account counters. Asserted server-side.)
|
|
await expectFolderTotal(page, { name: SHARED, shared: true }, 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 forceSync(page);
|
|
await expectFolderTotal(page, { name: SHARED, shared: true }, 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();
|
|
});
|
|
});
|