mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-10-31 08:13:50 +00:00
test(integration): verify unread/starred cross views keep the acted-on row
Adds 11-cross-view-retain.spec.ts: with the Unread cross view open, reading a mail keeps it in the list (status + unread counter update only, no removal); with the Starred cross view open, un-starring keeps it. Both force the focus/push refresh that previously dropped the row and assert it survives. Supporting test hooks: - thread-list-item: expose data-starred on the row. - email-context-menu: testId ctx-star / ctx-unstar on the star item. - integration helpers: expectEmailStarred (app.ts), setFlagged (jmap.ts). - integration policy.json: enable crossStarredViewEnabled. Verified: 08-shared-moves-style run of 11-cross-view-retain → 2/2 passed against the dockerized stack.
This commit is contained in:
@@ -372,6 +372,7 @@ export function EmailContextMenu({
|
||||
label={isStarred ? t("unstar") : t("star")}
|
||||
onClick={() => handleAction(onToggleStar!)}
|
||||
disabled={!onToggleStar}
|
||||
testId={isStarred ? "ctx-unstar" : "ctx-star"}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -190,6 +190,7 @@ const SingleEmailItem = React.forwardRef<HTMLDivElement, SingleEmailItemProps>(
|
||||
data-email-id={email.id}
|
||||
data-subject={email.subject || ''}
|
||||
data-unread={isUnread ? 'true' : 'false'}
|
||||
data-starred={email.keywords?.$flagged ? 'true' : 'false'}
|
||||
className={cn(
|
||||
"relative group cursor-pointer select-none transition-shadow duration-200 border-b border-border overflow-hidden",
|
||||
resolvedRowTint ? resolvedRowTint : (
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { ACCOUNTS } from './helpers/config';
|
||||
import { sendMail } from './helpers/smtp';
|
||||
import { JmapClient } from './helpers/jmap';
|
||||
import {
|
||||
login,
|
||||
seedSettings,
|
||||
openFolder,
|
||||
forceSync,
|
||||
expectEmailVisible,
|
||||
expectEmailUnread,
|
||||
expectEmailStarred,
|
||||
emailItem,
|
||||
emailContextAction,
|
||||
expectFolderCountsSynced,
|
||||
} from './helpers/app';
|
||||
|
||||
/**
|
||||
* The Unread and Starred cross views filter on the very keyword the read/star
|
||||
* actions flip. Acting on a mail from within the view (reading it in Unread,
|
||||
* un-starring it in Starred) must NOT make the row vanish on the next push /
|
||||
* focus refresh - only its status + the counters update; the row stays until
|
||||
* the view is re-opened. See the unit coverage in
|
||||
* stores/__tests__/email-store-retained-in-view.test.ts.
|
||||
*/
|
||||
const alice = ACCOUNTS.alice;
|
||||
const UNREAD = '__cross_unread__';
|
||||
const STARRED = '__cross_starred__';
|
||||
|
||||
let seq = 0;
|
||||
const subj = (l: string) => `IT ${l} ${Date.now()}-${seq++}`;
|
||||
const send = (s: string) => sendMail({ from: alice.email, authPass: alice.password, to: alice.email, subject: s, body: 'x' });
|
||||
|
||||
test.describe('Cross unread/starred views keep the acted-on row', () => {
|
||||
let jmap: JmapClient;
|
||||
|
||||
test.beforeEach(async () => {
|
||||
jmap = await JmapClient.connect(alice.email, alice.password);
|
||||
await jmap.reset();
|
||||
});
|
||||
|
||||
test('reading a mail in the Unread view keeps it visible (status + counter only)', async ({ page }) => {
|
||||
const a = subj('cr-unread-a');
|
||||
const b = subj('cr-unread-b');
|
||||
await send(a);
|
||||
await send(b);
|
||||
await jmap.waitForEmail(a);
|
||||
await jmap.waitForEmail(b);
|
||||
|
||||
await seedSettings(page, { enableUnifiedMailbox: true, enableCrossUnreadView: true });
|
||||
await login(page, alice);
|
||||
|
||||
await openFolder(page, { name: UNREAD });
|
||||
await forceSync(page);
|
||||
await expectEmailVisible(page, a);
|
||||
await expectEmailVisible(page, b);
|
||||
await expectFolderCountsSynced(page, { name: UNREAD }, { unread: 2 });
|
||||
|
||||
// Read one from the list's context menu.
|
||||
await emailContextAction(page, a, 'ctx-mark-read');
|
||||
await expectEmailUnread(page, a, false); // status updated in place
|
||||
await expectEmailVisible(page, a); // and still shown
|
||||
|
||||
// Force the focus/push refresh that previously dropped the read row.
|
||||
await forceSync(page);
|
||||
await page.waitForTimeout(1500);
|
||||
|
||||
await expect(emailItem(page, a)).toHaveCount(1); // retained, not removed
|
||||
await expectEmailUnread(page, a, false);
|
||||
await expectEmailVisible(page, b);
|
||||
// The unread counter still dropped to 1 (status + counter update happened).
|
||||
await expectFolderCountsSynced(page, { name: UNREAD }, { unread: 1 });
|
||||
});
|
||||
|
||||
test('un-starring a mail in the Starred view keeps it visible', async ({ page }) => {
|
||||
const a = subj('cr-star-a');
|
||||
const b = subj('cr-star-b');
|
||||
await send(a);
|
||||
await send(b);
|
||||
const ea = await jmap.waitForEmail(a);
|
||||
const eb = await jmap.waitForEmail(b);
|
||||
await jmap.setFlagged(ea.id, true);
|
||||
await jmap.setFlagged(eb.id, true);
|
||||
|
||||
await seedSettings(page, { enableUnifiedMailbox: true, enableCrossStarredView: true });
|
||||
await login(page, alice);
|
||||
|
||||
await openFolder(page, { name: STARRED });
|
||||
await forceSync(page);
|
||||
await expectEmailVisible(page, a);
|
||||
await expectEmailVisible(page, b);
|
||||
|
||||
// Remove the star from one via the context menu.
|
||||
await emailContextAction(page, a, 'ctx-unstar');
|
||||
await expectEmailStarred(page, a, false); // status updated in place
|
||||
await expectEmailVisible(page, a); // and still shown
|
||||
|
||||
await forceSync(page);
|
||||
await page.waitForTimeout(1500);
|
||||
|
||||
await expect(emailItem(page, a)).toHaveCount(1); // retained, not removed
|
||||
await expectEmailStarred(page, a, false);
|
||||
await expectEmailVisible(page, b);
|
||||
});
|
||||
});
|
||||
@@ -355,6 +355,11 @@ export async function expectEmailUnread(page: Page, subject: string, unread: boo
|
||||
await expect(emailItem(page, subject).first()).toHaveAttribute('data-unread', String(unread), { timeout });
|
||||
}
|
||||
|
||||
/** Assert an email row's starred state (from its `data-starred` attribute). */
|
||||
export async function expectEmailStarred(page: Page, subject: string, starred: boolean, timeout = 20000): Promise<void> {
|
||||
await expect(emailItem(page, subject).first()).toHaveAttribute('data-starred', String(starred), { timeout });
|
||||
}
|
||||
|
||||
/**
|
||||
* Open an email's right-click context menu and click one of its actions.
|
||||
* `testId` is one of: `ctx-delete`, `ctx-spam`, `ctx-not-spam`,
|
||||
|
||||
@@ -264,6 +264,12 @@ export class JmapClient {
|
||||
]);
|
||||
}
|
||||
|
||||
async setFlagged(emailId: string, flagged: boolean): Promise<void> {
|
||||
await this.request([
|
||||
['Email/set', { accountId: this.accountId, update: { [emailId]: { [`keywords/$flagged`]: flagged ? true : null } } }, '0'],
|
||||
]);
|
||||
}
|
||||
|
||||
/** Look up an email id by subject within an optional mailbox. */
|
||||
async findEmailBySubject(subject: string, mailboxId?: string): Promise<any | undefined> {
|
||||
const filter: Record<string, unknown> = { subject };
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"features": {
|
||||
"unifiedCrossAccountEnabled": true,
|
||||
"crossUnreadViewEnabled": true,
|
||||
"crossStarredViewEnabled": true,
|
||||
"crossAllViewEnabled": true
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user