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();
  });
});