list-tracking-metadata.test.ts3.0 KBView on GitHub
/**
 * listThreads-borne open/click tracking reaching the thread store.
 *
 * The "opened" badge reads `threadData[id].trackingData`, which used to be populated
 * only by `mail.get` — so it rendered late, after a thread was clicked or idle-
 * prefetched, instead of with the list row. `listThreads` now carries the same records
 * in its preview payload; these lock in that they survive the metadata-population path,
 * including the change-detection guard that short-circuits an otherwise-identical page.
 */
import { useCedarStore } from '@/modules/store';
import type { ThreadPreviewMetadata } from '@/modules/threads/threadList/store/threadSlice';

const trackingRecord = (trackingId: string, openCount: number) => ({
  trackingId,
  gmailMessageId: `msg-${trackingId}`,
  openCount,
  clickCount: 0,
  firstOpenedAt: null,
  lastOpenedAt: null,
  firstClickedAt: null,
  openTrackingEnabled: true,
  linkTrackingEnabled: true,
});

const metadata = (over: Partial<ThreadPreviewMetadata> = {}): ThreadPreviewMetadata => ({
  hasUnread: false,
  totalReplies: 1,
  labels: [{ id: 'SENT', name: 'SENT' }],
  preview: { sender: { email: '<email>' }, subject: 'hi', snippet: '' },
  ...over,
});

const trackingFor = (threadId: string) =>
  useCedarStore.getState().threadData[threadId]?.trackingData;

describe('listThreads tracking -> thread store', () => {
  it('populates tracking for a thread the store has never seen', () => {
    useCedarStore.getState().batchPopulateThreadMetadata({
      't-new': metadata({ trackingData: [trackingRecord('trk-a', 2)] }),
    });

    expect(trackingFor('t-new')?.map((t) => t.openCount)).toEqual([2]);
  });

  it('refreshes tracking on a page whose other metadata is unchanged', () => {
    // Opens move independently of labels and reply count, so the batch path's
    // "nothing changed, skip this thread" guard must not swallow the new counts.
    const base = metadata({ trackingData: [trackingRecord('trk-a', 1)] });
    useCedarStore.getState().batchPopulateThreadMetadata({ 't-1': base });
    expect(trackingFor('t-1')?.[0]?.openCount).toBe(1);

    useCedarStore.getState().batchPopulateThreadMetadata({
      't-1': metadata({ trackingData: [trackingRecord('trk-a', 4)] }),
    });

    expect(trackingFor('t-1')?.[0]?.openCount).toBe(4);
  });

  it('leaves existing tracking alone when a page carries none', () => {
    // A page with no tracking records for the thread (e.g. the lookup degraded) must
    // not wipe what mail.get already loaded.
    useCedarStore.getState().batchPopulateThreadMetadata({
      't-2': metadata({ trackingData: [trackingRecord('trk-a', 3)] }),
    });

    useCedarStore.getState().batchPopulateThreadMetadata({ 't-2': metadata() });

    expect(trackingFor('t-2')?.[0]?.openCount).toBe(3);
  });

  it('populates tracking through the single-thread path too', () => {
    useCedarStore
      .getState()
      .populateThreadMetadata('t-3', metadata({ trackingData: [trackingRecord('trk-b', 7)] }));

    expect(trackingFor('t-3')?.map((t) => t.trackingId)).toEqual(['trk-b']);
  });
});