crm-store.test.ts6.7 KBView on GitHub
/**
 * CRM Store Tests
 *
 * Tests the Zustand store logic for CRM state management.
 * Location: tests/modules/crm/store/crm-store.test.ts
 * Source:   modules/crm/store/crm-store.ts
 */

import type { CRMRow, CRMRowDictionary } from '@/modules/crm/store/crmSlice';
import { act } from '@testing-library/react';
import { create } from 'zustand';

// Test store state type
// Self-contained fixture store. It deliberately does NOT extend the real `CRMState`: contacts
// moved out of the CRM slice into conversationsSlice, so these fields only exist here.
interface CRMStoreState {
  contacts: CRMRowDictionary;
  isInitialized: boolean;
  contactSelection: string[];
  initializeContacts: (rows: CRMRow[]) => void;
  setContacts: (rows: CRMRow[]) => void;
  mergeContacts: (rows: CRMRow[]) => void;
  updateContactField: (id: string, field: string, value: unknown) => void;
  setContactSelection: (ids: string[]) => void;
  toggleContactSelection: (id: string) => void;
  clearContactSelection: () => void;
  reset: () => void;
}

const row = (id: string, data: Record<string, unknown> = {}): CRMRow => ({
  id,
  ...data,
});

const toDictionary = (rows: CRMRow[]): CRMRowDictionary => {
  return rows.reduce<CRMRowDictionary>((acc, row) => {
    acc[row.id] = row;
    return acc;
  }, {});
};

// Create test store inline
const testStore = create<CRMStoreState>((set, get) => ({
  contacts: {},
  isInitialized: false,
  contactSelection: [],
  initializeContacts: (rows) => {
    if (get().isInitialized) return;
    set({ contacts: toDictionary(rows), isInitialized: true } as Partial<CRMStoreState>);
  },
  setContacts: (rows) => {
    const state = get();
    const newContacts = toDictionary(rows);
    const prunedSelection = state.contactSelection.filter((id) => !!newContacts[id]).sort();
    set({
      contacts: newContacts,
      isInitialized: true,
      contactSelection: prunedSelection,
    } as Partial<CRMStoreState>);
  },
  mergeContacts: (rows) => {
    const state = get();
    set({
      contacts: { ...state.contacts, ...toDictionary(rows) },
      isInitialized: true,
    } as Partial<CRMStoreState>);
  },
  updateContactField: (id, field, value) => {
    const state = get();
    const contact = state.contacts[id];
    if (!contact || contact[field] === value) return;
    set({
      contacts: { ...state.contacts, [id]: { ...contact, [field]: value } },
    } as Partial<CRMStoreState>);
  },
  setContactSelection: (ids) => {
    const state = get();
    const newSelection = Array.from(new Set(ids.filter((id) => !!state.contacts[id]))).sort();
    set({ contactSelection: newSelection } as Partial<CRMStoreState>);
  },
  toggleContactSelection: (id) => {
    const state = get();
    if (!state.contacts[id]) return;
    const exists = state.contactSelection.includes(id);
    const newSelection = exists
      ? state.contactSelection.filter((selectedId) => selectedId !== id).sort()
      : [...state.contactSelection, id].sort();
    set({ contactSelection: newSelection } as Partial<CRMStoreState>);
  },
  clearContactSelection: () => {
    set({ contactSelection: [] } as Partial<CRMStoreState>);
  },
  reset: () => {
    set({ contacts: {}, contactSelection: [], isInitialized: false } as Partial<CRMStoreState>);
  },
}));

const resetCRMStore = () => {
  testStore.setState({
    contacts: {},
    isInitialized: false,
    contactSelection: [],
  } as Partial<CRMStoreState>);
};

describe('CRM Store', () => {
  beforeEach(() => {
    if (typeof window !== 'undefined' && window.localStorage) {
      window.localStorage.clear();
    }
    resetCRMStore();
  });

  afterEach(() => {
    resetCRMStore();
  });

  it('initializes contacts only once', () => {
    act(() => {
      testStore.getState().initializeContacts([row('1'), row('2')]);
    });

    let state = testStore.getState();
    expect(state.isInitialized).toBe(true);
    expect(Object.keys(state.contacts)).toEqual(['1', '2']);

    act(() => {
      testStore.getState().initializeContacts([row('3')]);
    });

    state = testStore.getState();
    expect(Object.keys(state.contacts)).toEqual(['1', '2']);
  });

  it('overwrites contacts map via setContacts', () => {
    act(() => {
      testStore.getState().setContacts([row('a', { value: 1 })]);
    });

    expect(Object.keys(testStore.getState().contacts)).toEqual(['a']);

    act(() => {
      testStore.getState().setContacts([row('b'), row('c')]);
    });

    expect(Object.keys(testStore.getState().contacts)).toEqual(['b', 'c']);
  });

  it('merges incoming contacts without losing existing ones', () => {
    act(() => {
      testStore.getState().setContacts([row('x', { value: 'old' })]);
    });

    act(() => {
      testStore.getState().mergeContacts([row('x', { value: 'new' }), row('y')]);
    });

    const state = testStore.getState();
    expect(state.contacts['x']).toEqual({ id: 'x', value: 'new' });
    expect(state.contacts['y']).toEqual({ id: 'y' });
  });

  it('updates a contact field when the row exists', () => {
    act(() => {
      testStore.getState().setContacts([row('person', { name: 'Alice' })]);
    });

    act(() => {
      testStore.getState().updateContactField('person', 'name', 'Alicia');
    });

    expect(testStore.getState().contacts['person'].name).toBe('Alicia');

    act(() => {
      testStore.getState().updateContactField('missing', 'name', 'Nope');
    });

    expect(testStore.getState().contacts['missing']).toBeUndefined();
  });

  it('tracks contact selection with deduplicated sorted ids', () => {
    act(() => {
      testStore.getState().setContacts([row('2'), row('1')]);
    });

    act(() => {
      testStore.getState().setContactSelection(['1', '2', '2']);
    });

    expect(testStore.getState().contactSelection).toEqual(['1', '2']);

    act(() => {
      testStore.getState().toggleContactSelection('2');
    });

    expect(testStore.getState().contactSelection).toEqual(['1']);

    act(() => {
      testStore.getState().toggleContactSelection('3');
    });

    expect(testStore.getState().contactSelection).toEqual(['1']);

    act(() => {
      testStore.getState().clearContactSelection();
    });

    expect(testStore.getState().contactSelection).toEqual([]);
  });

  it('prunes selection when contacts change or reset', () => {
    act(() => {
      testStore.getState().setContacts([row('1'), row('2')]);
      testStore.getState().setContactSelection(['1', '2']);
    });

    act(() => {
      testStore.getState().setContacts([row('2')]);
    });

    expect(testStore.getState().contactSelection).toEqual(['2']);

    act(() => {
      testStore.getState().reset();
    });

    const state = testStore.getState();
    expect(state.contacts).toEqual({});
    expect(state.contactSelection).toEqual([]);
    expect(state.isInitialized).toBe(false);
  });
});