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