draftSlice.test.ts9.0 KBView on GitHub /**
* Tests for DraftSlice — compose open/close, draft body/diff, injectDraftIntoThread
*/
import { act } from '@testing-library/react';
import { useCedarStore } from '@/modules/cedar-os/src/store/CedarStore';
const resetDraftState = () =>
useCedarStore.setState((s) => ({
...s,
newEmail: false,
isComposeOpen: false,
draftBody: null,
draftDiff: null,
}));
beforeEach(resetDraftState);
afterEach(resetDraftState);
// ---------------------------------------------------------------------------
// Basic setters
// ---------------------------------------------------------------------------
describe('setNewEmail', () => {
it('sets newEmail to true', () => {
act(() => useCedarStore.getState().setNewEmail(true));
expect(useCedarStore.getState().newEmail).toBe(true);
});
it('sets newEmail to false', () => {
act(() => useCedarStore.getState().setNewEmail(true));
act(() => useCedarStore.getState().setNewEmail(false));
expect(useCedarStore.getState().newEmail).toBe(false);
});
});
describe('setIsComposeOpen', () => {
it('opens the compose panel', () => {
act(() => useCedarStore.getState().setIsComposeOpen(true));
expect(useCedarStore.getState().isComposeOpen).toBe(true);
});
it('closes the compose panel', () => {
act(() => useCedarStore.getState().setIsComposeOpen(true));
act(() => useCedarStore.getState().setIsComposeOpen(false));
expect(useCedarStore.getState().isComposeOpen).toBe(false);
});
});
describe('setDraftBody', () => {
it('sets the draft body', () => {
act(() => useCedarStore.getState().setDraftBody('<p>Hello</p>'));
expect(useCedarStore.getState().draftBody).toBe('<p>Hello</p>');
});
it('clears the draft body when set to null', () => {
act(() => useCedarStore.getState().setDraftBody('<p>Hello</p>'));
act(() => useCedarStore.getState().setDraftBody(null));
expect(useCedarStore.getState().draftBody).toBeNull();
});
});
describe('setDraftDiff', () => {
it('sets the draft diff', () => {
act(() =>
useCedarStore.getState().setDraftDiff({ oldBody: 'old content', newBody: 'new content' }),
);
const diff = useCedarStore.getState().draftDiff;
expect(diff?.oldBody).toBe('old content');
expect(diff?.newBody).toBe('new content');
});
it('clears the draft diff when set to null', () => {
act(() => useCedarStore.getState().setDraftDiff({ oldBody: '', newBody: 'body' }));
act(() => useCedarStore.getState().setDraftDiff(null));
expect(useCedarStore.getState().draftDiff).toBeNull();
});
});
// ---------------------------------------------------------------------------
// openNewEmail
// ---------------------------------------------------------------------------
describe('openNewEmail', () => {
it('sets newEmail to true', () => {
act(() => useCedarStore.getState().openNewEmail());
expect(useCedarStore.getState().newEmail).toBe(true);
});
it('creates a draft session entry in threadData keyed by a draftSessionId', () => {
act(() => useCedarStore.getState().openNewEmail());
const { selectedThreadId, threadData } = useCedarStore.getState();
expect(selectedThreadId).toBeTruthy();
expect(selectedThreadId!.startsWith('draftSessionId-')).toBe(true);
expect(threadData[selectedThreadId!]).toBeDefined();
});
it('creates a draft entry with isDraft=true', () => {
act(() => useCedarStore.getState().openNewEmail());
const { selectedThreadId, threadData } = useCedarStore.getState();
const thread = threadData[selectedThreadId!];
expect(thread.messages[0].isDraft).toBe(true);
});
it('each call creates a unique draftSessionId', () => {
act(() => useCedarStore.getState().openNewEmail());
const id1 = useCedarStore.getState().selectedThreadId;
act(() => useCedarStore.getState().openNewEmail());
const id2 = useCedarStore.getState().selectedThreadId;
expect(id1).not.toBe(id2);
});
});
// ---------------------------------------------------------------------------
// openNewEmailTo
// ---------------------------------------------------------------------------
describe('openNewEmailTo', () => {
it('pre-fills the to field with the given recipients', () => {
const recipients = [{ email: '<email>', name: 'Alice' }];
act(() => useCedarStore.getState().openNewEmailTo(recipients));
const { selectedThreadId, threadData } = useCedarStore.getState();
const msg = threadData[selectedThreadId!]?.messages[0];
expect(msg?.to).toEqual(recipients);
});
it('sets newEmail to true', () => {
act(() => useCedarStore.getState().openNewEmailTo([{ email: '<email>' }]));
expect(useCedarStore.getState().newEmail).toBe(true);
});
});
// ---------------------------------------------------------------------------
// openNewEmailWithContent
// ---------------------------------------------------------------------------
describe('openNewEmailWithContent', () => {
it('pre-fills subject', () => {
act(() =>
useCedarStore.getState().openNewEmailWithContent({ subject: 'Q4 Review', body: '' }),
);
const { selectedThreadId, threadData } = useCedarStore.getState();
expect(threadData[selectedThreadId!]?.messages[0].subject).toBe('Q4 Review');
});
it('parses comma-separated to addresses', () => {
act(() =>
useCedarStore.getState().openNewEmailWithContent({
to: '<email>, <email>',
}),
);
const { selectedThreadId, threadData } = useCedarStore.getState();
const msg = threadData[selectedThreadId!]?.messages[0];
expect(msg?.to).toHaveLength(2);
expect(msg?.to[0].email).toBe('<email>');
expect(msg?.to[1].email).toBe('<email>');
});
it('sets body via processedHtml', () => {
act(() =>
useCedarStore.getState().openNewEmailWithContent({ body: '<p>Draft body</p>' }),
);
const { selectedThreadId, threadData } = useCedarStore.getState();
expect(threadData[selectedThreadId!]?.messages[0].processedHtml).toBe('<p>Draft body</p>');
});
it('handles empty params gracefully', () => {
expect(() => act(() => useCedarStore.getState().openNewEmailWithContent({}))).not.toThrow();
expect(useCedarStore.getState().newEmail).toBe(true);
});
});
// ---------------------------------------------------------------------------
// injectDraftIntoThread
// ---------------------------------------------------------------------------
describe('injectDraftIntoThread', () => {
it('creates a skeleton thread when threadData does not exist', () => {
act(() =>
useCedarStore.getState().injectDraftIntoThread('thread-new', { body: '<p>Body</p>' }),
);
const { threadData } = useCedarStore.getState();
expect(threadData['thread-new']).toBeDefined();
expect(threadData['thread-new'].messages[0].isDraft).toBe(true);
});
it('appends to an existing thread without disturbing existing messages', () => {
// Seed threadData with an existing message
useCedarStore.setState((s) => ({
...s,
threadData: {
'thread-existing': {
id: 'thread-existing',
messages: [
{
id: 'msg-1',
isDraft: false,
subject: 'Original',
tags: [],
sender: { email: '<email>' },
to: [],
cc: null,
bcc: null,
tls: true,
receivedOn: '2026-01-01T00:00:00Z',
unread: false,
processedHtml: '',
blobUrl: '',
},
],
latest: undefined as any,
hasUnread: false,
totalReplies: 1,
labels: [],
lastLoadedAt: Date.now(),
},
},
}));
act(() =>
useCedarStore
.getState()
.injectDraftIntoThread('thread-existing', { body: '<p>Draft reply</p>' }),
);
const { threadData } = useCedarStore.getState();
expect(threadData['thread-existing'].messages).toHaveLength(2);
expect(threadData['thread-existing'].messages[1].isDraft).toBe(true);
expect(threadData['thread-existing'].messages[0].isDraft).toBe(false);
});
it('stores the draft body in processedHtml', () => {
act(() =>
useCedarStore
.getState()
.injectDraftIntoThread('thread-abc', { body: '<p>Agent draft</p>' }),
);
const draft = useCedarStore.getState().threadData['thread-abc']?.messages[0];
expect(draft?.processedHtml).toBe('<p>Agent draft</p>');
});
it('sets to recipient when provided', () => {
act(() =>
useCedarStore
.getState()
.injectDraftIntoThread('thread-to', { body: '', to: '<email>' }),
);
const draft = useCedarStore.getState().threadData['thread-to']?.messages[0];
expect(draft?.to).toEqual([{ email: '<email>', name: '' }]);
});
it('does NOT set draftDiff (editor initialises from processedHtml directly)', () => {
act(() =>
useCedarStore.getState().injectDraftIntoThread('thread-nodiff', { body: '<p>No diff</p>' }),
);
expect(useCedarStore.getState().draftDiff).toBeNull();
});
});