mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-10-31 08:13:50 +00:00
229 lines
8.8 KiB
TypeScript
229 lines
8.8 KiB
TypeScript
import { describe, it, expect, beforeEach } from 'vitest';
|
|
import { useEmailStore } from '../email-store';
|
|
|
|
function makeEmail(id: string, threadId = `thread-${id}`) {
|
|
return {
|
|
id,
|
|
threadId,
|
|
mailboxIds: { inbox: true },
|
|
keywords: {},
|
|
size: 100,
|
|
receivedAt: new Date().toISOString(),
|
|
from: [{ name: 'Test', email: 'test@example.com' }],
|
|
to: [{ name: 'User', email: 'user@example.com' }],
|
|
subject: `Email ${id}`,
|
|
preview: 'preview',
|
|
hasAttachment: false,
|
|
textBody: [],
|
|
htmlBody: [],
|
|
bodyValues: {},
|
|
};
|
|
}
|
|
|
|
describe('email-store selection', () => {
|
|
beforeEach(() => {
|
|
useEmailStore.setState({
|
|
emails: [makeEmail('a'), makeEmail('b'), makeEmail('c'), makeEmail('d'), makeEmail('e')],
|
|
selectedEmailKeys: new Set(),
|
|
lastSelectedEmailKey: null,
|
|
selectedEmail: null,
|
|
listRows: null,
|
|
});
|
|
});
|
|
|
|
describe('toggleEmailSelection', () => {
|
|
it('should add email to selection', () => {
|
|
useEmailStore.getState().toggleEmailSelection({ id: 'b' });
|
|
expect(useEmailStore.getState().selectedEmailKeys.has('b')).toBe(true);
|
|
expect(useEmailStore.getState().lastSelectedEmailKey).toBe('b');
|
|
});
|
|
|
|
it('should remove email from selection when toggled again', () => {
|
|
useEmailStore.getState().toggleEmailSelection({ id: 'b' });
|
|
useEmailStore.getState().toggleEmailSelection({ id: 'b' });
|
|
expect(useEmailStore.getState().selectedEmailKeys.has('b')).toBe(false);
|
|
});
|
|
|
|
it('should support selecting multiple emails', () => {
|
|
useEmailStore.getState().toggleEmailSelection({ id: 'a' });
|
|
useEmailStore.getState().toggleEmailSelection({ id: 'c' });
|
|
const ids = useEmailStore.getState().selectedEmailKeys;
|
|
expect(ids.has('a')).toBe(true);
|
|
expect(ids.has('c')).toBe(true);
|
|
expect(ids.size).toBe(2);
|
|
});
|
|
});
|
|
|
|
describe('selectRangeEmails', () => {
|
|
it('should select range from last selected to target (forward)', () => {
|
|
useEmailStore.getState().toggleEmailSelection({ id: 'b' }); // anchor at index 1
|
|
useEmailStore.getState().selectRangeEmails({ id: 'd' }); // target at index 3
|
|
const ids = useEmailStore.getState().selectedEmailKeys;
|
|
expect(ids.has('b')).toBe(true);
|
|
expect(ids.has('c')).toBe(true);
|
|
expect(ids.has('d')).toBe(true);
|
|
expect(ids.size).toBe(3);
|
|
});
|
|
|
|
it('should select range backward', () => {
|
|
useEmailStore.getState().toggleEmailSelection({ id: 'd' }); // anchor at index 3
|
|
useEmailStore.getState().selectRangeEmails({ id: 'b' }); // target at index 1
|
|
const ids = useEmailStore.getState().selectedEmailKeys;
|
|
expect(ids.has('b')).toBe(true);
|
|
expect(ids.has('c')).toBe(true);
|
|
expect(ids.has('d')).toBe(true);
|
|
expect(ids.size).toBe(3);
|
|
});
|
|
|
|
it('should use first email as anchor when no previous selection', () => {
|
|
useEmailStore.getState().selectRangeEmails({ id: 'c' }); // no anchor → uses first email 'a'
|
|
const ids = useEmailStore.getState().selectedEmailKeys;
|
|
expect(ids.has('a')).toBe(true);
|
|
expect(ids.has('b')).toBe(true);
|
|
expect(ids.has('c')).toBe(true);
|
|
expect(ids.size).toBe(3);
|
|
});
|
|
|
|
it('should add to existing selection', () => {
|
|
useEmailStore.getState().toggleEmailSelection({ id: 'a' });
|
|
useEmailStore.getState().toggleEmailSelection({ id: 'b' }); // anchor now at 'b'
|
|
useEmailStore.getState().selectRangeEmails({ id: 'd' });
|
|
const ids = useEmailStore.getState().selectedEmailKeys;
|
|
// 'a' still selected, plus b-d range
|
|
expect(ids.has('a')).toBe(true);
|
|
expect(ids.has('b')).toBe(true);
|
|
expect(ids.has('c')).toBe(true);
|
|
expect(ids.has('d')).toBe(true);
|
|
expect(ids.size).toBe(4);
|
|
});
|
|
|
|
it('should handle single-item range', () => {
|
|
useEmailStore.getState().toggleEmailSelection({ id: 'c' });
|
|
useEmailStore.getState().selectRangeEmails({ id: 'c' });
|
|
const ids = useEmailStore.getState().selectedEmailKeys;
|
|
expect(ids.has('c')).toBe(true);
|
|
expect(ids.size).toBe(1);
|
|
});
|
|
});
|
|
|
|
// #1230: the list shows a thread once, at its newest message. A range used
|
|
// to walk the flat date order, so a thread whose older message fell between
|
|
// the two ends got pulled in although its row was outside the range.
|
|
describe('selectRangeEmails over list rows', () => {
|
|
// Flat date order: t3 17:00, A 16:45, B 16:00, C 15:15, t2 15:00, D 14:30, t1 13:00.
|
|
const flat = ['t3', 'A', 'B', 'C', 't2', 'D', 't1'];
|
|
const rows = [
|
|
{ key: 't3', members: ['t3', 't2', 't1'] },
|
|
{ key: 'A', members: ['A'] },
|
|
{ key: 'B', members: ['B'] },
|
|
{ key: 'C', members: ['C'] },
|
|
{ key: 'D', members: ['D'] },
|
|
];
|
|
|
|
beforeEach(() => {
|
|
useEmailStore.setState({
|
|
emails: flat.map((id) => makeEmail(id, id.startsWith('t') ? 'thread-t' : `thread-${id}`)),
|
|
listRows: rows,
|
|
});
|
|
});
|
|
|
|
it('selects the rows between the two ends, not a thread dated between them', () => {
|
|
useEmailStore.getState().toggleEmailSelection({ id: 'D' });
|
|
useEmailStore.getState().selectRangeEmails({ id: 'B' });
|
|
expect([...useEmailStore.getState().selectedEmailKeys].sort()).toEqual(['B', 'C', 'D']);
|
|
});
|
|
|
|
it('selects every message of a thread whose row is in the range', () => {
|
|
useEmailStore.getState().toggleEmailSelection({ id: 'B' });
|
|
useEmailStore.getState().selectRangeEmails({ id: 't3' });
|
|
expect([...useEmailStore.getState().selectedEmailKeys].sort()).toEqual(['A', 'B', 't1', 't2', 't3']);
|
|
});
|
|
|
|
it('finds a thread row from an older message of that thread', () => {
|
|
useEmailStore.setState({ lastSelectedEmailKey: 't1' });
|
|
useEmailStore.getState().selectRangeEmails({ id: 'A' });
|
|
expect([...useEmailStore.getState().selectedEmailKeys].sort()).toEqual(['A', 't1', 't2', 't3']);
|
|
});
|
|
});
|
|
|
|
// #992: Shift+Arrow grows the selection from the open message one row at a
|
|
// time and shrinks it again when the direction reverses.
|
|
describe('extendSelection', () => {
|
|
const keys = () => [...useEmailStore.getState().selectedEmailKeys].sort();
|
|
|
|
beforeEach(() => {
|
|
// Opening a message makes it the anchor, as selectEmail does.
|
|
useEmailStore.setState({ lastSelectedEmailKey: 'b', selectionFocus: null });
|
|
});
|
|
|
|
it('selects the open message and the next one on the first press', () => {
|
|
useEmailStore.getState().extendSelection(1);
|
|
expect(keys()).toEqual(['b', 'c']);
|
|
});
|
|
|
|
it('keeps growing in the same direction', () => {
|
|
useEmailStore.getState().extendSelection(1);
|
|
useEmailStore.getState().extendSelection(1);
|
|
expect(keys()).toEqual(['b', 'c', 'd']);
|
|
});
|
|
|
|
it('shrinks back when the direction reverses', () => {
|
|
useEmailStore.getState().extendSelection(1);
|
|
useEmailStore.getState().extendSelection(1);
|
|
useEmailStore.getState().extendSelection(-1);
|
|
expect(keys()).toEqual(['b', 'c']);
|
|
});
|
|
|
|
it('crosses the anchor and grows the other way', () => {
|
|
useEmailStore.getState().extendSelection(1);
|
|
useEmailStore.getState().extendSelection(-1);
|
|
useEmailStore.getState().extendSelection(-1);
|
|
expect(keys()).toEqual(['a', 'b']);
|
|
});
|
|
|
|
it('stops at the end of the list', () => {
|
|
for (let i = 0; i < 10; i++) useEmailStore.getState().extendSelection(1);
|
|
expect(keys()).toEqual(['b', 'c', 'd', 'e']);
|
|
});
|
|
|
|
it('continues a shift-click range', () => {
|
|
useEmailStore.getState().selectRangeEmails({ id: 'd' });
|
|
useEmailStore.getState().extendSelection(-1);
|
|
expect(keys()).toEqual(['b', 'c']);
|
|
});
|
|
|
|
it('walks list rows, taking a whole thread with its row', () => {
|
|
useEmailStore.setState({
|
|
emails: ['t2', 'x', 't1', 'y'].map((id) => makeEmail(id, id.startsWith('t') ? 'thread-t' : `thread-${id}`)),
|
|
listRows: [
|
|
{ key: 'x', members: ['x'] },
|
|
{ key: 't2', members: ['t2', 't1'] },
|
|
{ key: 'y', members: ['y'] },
|
|
],
|
|
lastSelectedEmailKey: 'x',
|
|
});
|
|
useEmailStore.getState().extendSelection(1);
|
|
expect(keys()).toEqual(['t1', 't2', 'x']);
|
|
useEmailStore.getState().extendSelection(-1);
|
|
expect(keys()).toEqual(['x']);
|
|
});
|
|
});
|
|
|
|
describe('selectAllEmails', () => {
|
|
it('should select all emails', () => {
|
|
useEmailStore.getState().selectAllEmails();
|
|
expect(useEmailStore.getState().selectedEmailKeys.size).toBe(5);
|
|
});
|
|
});
|
|
|
|
describe('clearSelection', () => {
|
|
it('should clear all selections and reset anchor', () => {
|
|
useEmailStore.getState().toggleEmailSelection({ id: 'a' });
|
|
useEmailStore.getState().toggleEmailSelection({ id: 'b' });
|
|
useEmailStore.getState().clearSelection();
|
|
expect(useEmailStore.getState().selectedEmailKeys.size).toBe(0);
|
|
expect(useEmailStore.getState().lastSelectedEmailKey).toBeNull();
|
|
});
|
|
});
|
|
});
|