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.
118 lines
4.1 KiB
TypeScript
118 lines
4.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,
|
|
expectFolderUnread,
|
|
expectFolderTotal,
|
|
expectEmailVisible,
|
|
expectEmailUnread,
|
|
emailContextAction,
|
|
emailItem,
|
|
openFolder,
|
|
forceSync,
|
|
} from './helpers/app';
|
|
|
|
/**
|
|
* Message actions from the list context menu — mark read/unread, delete, spam —
|
|
* performed in the Inbox, with the outcome checked on both the UI (counters,
|
|
* row state) and the server (which mailbox the message ended up in).
|
|
*/
|
|
const alice = ACCOUNTS.alice;
|
|
let seq = 0;
|
|
const subj = (l: string) => `IT ${l} ${Date.now()}-${seq++}`;
|
|
const send = (subject: string) =>
|
|
sendMail({ from: alice.email, authPass: alice.password, to: alice.email, subject, body: 'x' });
|
|
|
|
test.describe('Inbox message actions', () => {
|
|
let jmap: JmapClient;
|
|
|
|
test.beforeEach(async () => {
|
|
jmap = await JmapClient.connect(alice.email, alice.password);
|
|
await jmap.reset();
|
|
});
|
|
|
|
test('mark read then unread toggles the row state and Inbox unread counter', async ({ page }) => {
|
|
const s = subj('act-read');
|
|
await send(s);
|
|
await jmap.waitForEmail(s);
|
|
|
|
await login(page, alice);
|
|
await expectFolderUnread(page, { role: 'inbox' }, 1);
|
|
await expectEmailUnread(page, s, true);
|
|
|
|
await emailContextAction(page, s, 'ctx-mark-read');
|
|
await expectEmailUnread(page, s, false);
|
|
await expectFolderUnread(page, { role: 'inbox' }, 0);
|
|
|
|
await emailContextAction(page, s, 'ctx-mark-unread');
|
|
await expectEmailUnread(page, s, true);
|
|
await expectFolderUnread(page, { role: 'inbox' }, 1);
|
|
});
|
|
|
|
test('delete moves the message to Trash and updates both counters', async ({ page }) => {
|
|
const s = subj('act-del');
|
|
await send(s);
|
|
await jmap.waitForEmail(s);
|
|
|
|
await login(page, alice);
|
|
await expectFolderTotal(page, { role: 'inbox' }, 1);
|
|
|
|
await emailContextAction(page, s, 'ctx-delete');
|
|
|
|
// Leaves the Inbox, lands in Trash — on the UI...
|
|
await expectFolderTotal(page, { role: 'inbox' }, 0);
|
|
await expectFolderTotal(page, { role: 'trash' }, 1);
|
|
await expect(emailItem(page, s)).toHaveCount(0);
|
|
|
|
// ...and on the server.
|
|
const trash = await jmap.mailboxByRole('trash');
|
|
const found = await jmap.findEmailBySubject(s, trash!.id);
|
|
expect(found, 'deleted message is in Trash on the server').toBeTruthy();
|
|
});
|
|
|
|
test('mark as spam moves the message to Junk', async ({ page }) => {
|
|
const s = subj('act-spam');
|
|
await send(s);
|
|
await jmap.waitForEmail(s);
|
|
|
|
await login(page, alice);
|
|
await expectFolderTotal(page, { role: 'inbox' }, 1);
|
|
|
|
await emailContextAction(page, s, 'ctx-spam');
|
|
|
|
// The destination (Junk) counter updates optimistically, but the source
|
|
// (Inbox) counter isn't always decremented until the next reconcile when
|
|
// the action fires moments after login — unlike delete, which decrements
|
|
// the source immediately. A visibility reconcile settles it deterministically.
|
|
await expectFolderTotal(page, { role: 'junk' }, 1);
|
|
await forceSync(page);
|
|
await expectFolderTotal(page, { role: 'inbox' }, 0);
|
|
|
|
const junk = await jmap.mailboxByRole('junk');
|
|
const found = await jmap.findEmailBySubject(s, junk!.id);
|
|
expect(found, 'spammed message is in Junk on the server').toBeTruthy();
|
|
});
|
|
|
|
test('spam then not-spam round-trips the message back out of Junk', async ({ page }) => {
|
|
const s = subj('act-notspam');
|
|
await send(s);
|
|
await jmap.waitForEmail(s);
|
|
|
|
await login(page, alice);
|
|
await emailContextAction(page, s, 'ctx-spam');
|
|
await expectFolderTotal(page, { role: 'junk' }, 1);
|
|
|
|
// Open Junk, then mark not-spam.
|
|
await openFolder(page, { role: 'junk' });
|
|
await expectEmailVisible(page, s);
|
|
await emailContextAction(page, s, 'ctx-not-spam');
|
|
|
|
await expectFolderTotal(page, { role: 'junk' }, 0);
|
|
const junk = await jmap.mailboxByRole('junk');
|
|
const stillInJunk = await jmap.findEmailBySubject(s, junk!.id);
|
|
expect(stillInJunk, 'message no longer in Junk').toBeFalsy();
|
|
});
|
|
});
|