group-cache.test.ts3.3 KBView on GitHub
import {
  patchCachedGroup,
  reorderCachedGroups,
} from '@/modules/userTasks/utils/group-cache';

/**
 * Every group surface renders from one cached `listGroups` payload, so these helpers are what make
 * a rename or a reorder land everywhere at once. They take `unknown` because the payload mixes real
 * group rows with the synthesized virtual-Misc row (`id: null`) — the cases below pin down that
 * Misc survives both operations untouched, which is the thing a careless `.map` would break.
 */

const payload = () => ({
  groups: [
    { id: 'a', name: 'Responses needed', color: '#3b82f6', icon: 'Reply', position: 0 },
    { id: 'b', name: 'Follow-ups', color: '#f97316', icon: 'Send', position: 1 },
    { id: 'c', name: 'CRM updates', color: '#14b8a6', icon: 'Database', position: 2 },
    { id: null, name: 'Misc', color: null, icon: null, position: Number.MAX_SAFE_INTEGER },
  ],
});

describe('reorderCachedGroups', () => {
  it('rewrites both array order and position, so List and Board agree', () => {
    const next = reorderCachedGroups(payload(), ['c', 'a', 'b']) as ReturnType<typeof payload>;
    expect(next.groups.map((g) => g.id)).toEqual(['c', 'a', 'b', null]);
    expect(next.groups.map((g) => g.position)).toEqual([0, 1, 2, Number.MAX_SAFE_INTEGER]);
  });

  it('leaves Misc last and untouched', () => {
    const next = reorderCachedGroups(payload(), ['c', 'b', 'a']) as ReturnType<typeof payload>;
    const misc = next.groups[next.groups.length - 1];
    expect(misc.id).toBeNull();
    expect(misc.position).toBe(Number.MAX_SAFE_INTEGER);
  });

  it('passes a payload it does not understand straight through', () => {
    expect(reorderCachedGroups(undefined, ['a'])).toBeUndefined();
    expect(reorderCachedGroups({ nope: 1 }, ['a'])).toEqual({ nope: 1 });
  });
});

describe('patchCachedGroup', () => {
  it('renames only the target group', () => {
    const next = patchCachedGroup(payload(), 'b', { name: 'Nudges' }) as ReturnType<typeof payload>;
    expect(next.groups.map((g) => g.name)).toEqual([
      'Responses needed',
      'Nudges',
      'CRM updates',
      'Misc',
    ]);
  });

  it('leaves unlisted fields alone', () => {
    const next = patchCachedGroup(payload(), 'b', { name: 'Nudges' }) as ReturnType<typeof payload>;
    expect(next.groups[1]).toMatchObject({ color: '#f97316', icon: 'Send', position: 1 });
  });

  it('clears a colour when explicitly passed null, but not when omitted', () => {
    const cleared = patchCachedGroup(payload(), 'a', { color: null }) as ReturnType<typeof payload>;
    expect(cleared.groups[0].color).toBeNull();

    const untouched = patchCachedGroup(payload(), 'a', { name: 'x' }) as ReturnType<typeof payload>;
    expect(untouched.groups[0].color).toBe('#3b82f6');
  });

  it('cannot match Misc, whose id is null', () => {
    // A caller can only ever pass a real group id, but the virtual row is the one thing in the
    // payload with no server row behind it — an edit landing on it would be unsaveable.
    const next = patchCachedGroup(payload(), 'a', { name: 'x' }) as ReturnType<typeof payload>;
    expect(next.groups[3]).toEqual(payload().groups[3]);
  });

  it('passes a payload it does not understand straight through', () => {
    expect(patchCachedGroup(undefined, 'a', { name: 'x' })).toBeUndefined();
  });
});