test(integration): draft handling and shared-folder moves

Add draft and shared-folder-move coverage (suite now 31 tests). Findings are
asserted server-side or pinned with test.fail where the UI is incomplete.

Drafts (07):
- multiple recipients (committed and typed-but-uncommitted) persist, and the
  draft reopens via the continue-draft button;
- a server-created draft (with $draft) shows the continue-draft button;
- a changed sender identity is saved to the draft on the server;
- KNOWN BUG (test.fail): reopening a draft resets the From selector to the
  default identity instead of the one the draft was saved with.

Shared-folder moves (08):
- shared -> shared (same owner) moves work in both directions (server-verified);
- KNOWN LIMITATION (test.fail): cross-account moves (own account <-> shared
  folder) don't relocate the message — the Move-to submenu offers the target
  but clicking it is a no-op.

Hooks added: composer From select + save-status, viewer edit-draft button,
context-menu "Move to" submenu + per-target testids (testId on
ContextMenuSubMenu). Helpers: JMAP identities/createDraft/sharing, composer
drive + move-via-submenu. README documents the findings.
This commit is contained in:
Stefan Hildebrandt
2026-07-11 21:15:27 +02:00
parent e05fbb2fe9
commit 060c5d00d1
9 changed files with 420 additions and 4 deletions
+146
View File
@@ -0,0 +1,146 @@
import { test, expect } from '@playwright/test';
import { ACCOUNTS } from './helpers/config';
import { JmapClient } from './helpers/jmap';
import {
login,
openComposer,
addRecipient,
setFrom,
setSubject,
waitDraftSaved,
closeComposer,
composerRecipients,
openFolder,
emailItem,
} from './helpers/app';
/**
* Draft handling. Focus areas reported as flaky by the user:
* - the "continue draft" (edit-draft) button in the message view,
* - multiple recipients being persisted to the draft,
* - a changed sender identity being persisted to the draft.
*
* Each test drives the composer, lets it auto-save, then verifies the draft on
* the server (JMAP) and by reopening it in the UI.
*/
const { alice, bob, carol } = ACCOUNTS;
const subj = (l: string) => `IT ${l} ${Date.now()}`;
async function draftBody(page: import('@playwright/test').Page, text: string) {
await page.locator('.ProseMirror').first().fill(text);
}
test.describe('Drafts', () => {
let jmap: JmapClient;
test.beforeEach(async () => {
jmap = await JmapClient.connect(alice.email, alice.password);
await jmap.reset();
});
test('multiple recipients save and reopen via the continue-draft button', async ({ page }) => {
const subject = subj('draft-multi');
await login(page, alice);
await openComposer(page);
await addRecipient(page, bob.email);
await addRecipient(page, carol.email);
await setSubject(page, subject);
await draftBody(page, 'draft body');
await waitDraftSaved(page);
await closeComposer(page);
// Server: the draft carries BOTH recipients.
const drafts = await jmap.mailboxByRole('drafts');
const draft = await jmap.waitForEmail(subject, { mailboxId: drafts!.id });
const to = (draft.to ?? []).map((r: { email: string }) => r.email).sort();
expect(to).toEqual([bob.email, carol.email].sort());
// UI: opening the draft shows the continue-draft button, which reopens the
// composer with both recipients intact.
await openFolder(page, { role: 'drafts' });
await emailItem(page, subject).first().click();
await page.locator('[data-testid="edit-draft"]').click();
await page.locator('[data-testid="email-composer"]').waitFor({ state: 'visible' });
const recips = await composerRecipients(page);
expect(recips).toContain(bob.email);
expect(recips).toContain(carol.email);
});
test('a recipient typed but not committed to a chip is still saved', async ({ page }) => {
const subject = subj('draft-uncommitted');
await login(page, alice);
await openComposer(page);
await addRecipient(page, bob.email); // committed chip
// Type a second address but do NOT press Enter — leave it as raw input.
const input = page.locator('[data-testid="composer-to"] input').first();
await input.click();
await input.fill(carol.email);
await setSubject(page, subject); // blur the To field
await draftBody(page, 'uncommitted body');
await waitDraftSaved(page);
await closeComposer(page);
const drafts = await jmap.mailboxByRole('drafts');
const draft = await jmap.waitForEmail(subject, { mailboxId: drafts!.id });
const to = (draft.to ?? []).map((r: { email: string }) => r.email).sort();
// Both the committed and the still-in-the-input recipient must survive.
expect(to).toEqual([bob.email, carol.email].sort());
});
test('a server-created draft shows the continue-draft button when viewed', async ({ page }) => {
const subject = subj('draft-server');
await jmap.createDraft(subject, bob.email);
await login(page, alice);
await openFolder(page, { role: 'drafts' });
await emailItem(page, subject).first().click();
// The edit-draft ("continue draft") button must be present for any message
// carrying the $draft keyword, regardless of how the draft was created.
await expect(page.locator('[data-testid="edit-draft"]')).toBeVisible();
});
test('a changed sender identity is saved to the draft (server)', async ({ page }) => {
const altId = await jmap.ensureIdentity('Alice Team', alice.email);
const subject = subj('draft-from');
await login(page, alice);
await openComposer(page);
await setFrom(page, altId);
await addRecipient(page, bob.email);
await setSubject(page, subject);
await draftBody(page, 'from-change body');
await waitDraftSaved(page);
await closeComposer(page);
const drafts = await jmap.mailboxByRole('drafts');
const draft = await jmap.waitForEmail(subject, { mailboxId: drafts!.id });
expect((draft.from ?? [])[0]?.name, 'draft From carries the selected identity').toBe('Alice Team');
});
// KNOWN BUG (documented via test.fail): a draft composed with a non-default
// identity is saved with the right From on the server (see the test above),
// but reopening the draft resets the composer's From selector to the default
// identity instead of restoring the one the draft was written with. If this
// starts passing, the reopen path was fixed — flip this back to a plain test.
test.fail('reopening a draft restores the changed sender in the From selector', async ({ page }) => {
const altId = await jmap.ensureIdentity('Alice Team', alice.email);
const subject = subj('draft-from-reopen');
await login(page, alice);
await openComposer(page);
await setFrom(page, altId);
await addRecipient(page, bob.email);
await setSubject(page, subject);
await draftBody(page, 'reopen body');
await waitDraftSaved(page);
await closeComposer(page);
await openFolder(page, { role: 'drafts' });
await emailItem(page, subject).first().click();
await page.locator('[data-testid="edit-draft"]').click();
await expect(page.locator('[data-testid="composer-from"]')).toHaveValue(altId);
});
});