unifiedInboxOrder.test.ts3.9 KBView on GitHub import { act } from '@testing-library/react';
import { useCedarStore } from '@/modules/store';
import { selectMergedFeed } from '@/modules/inbox/store/inboxSlice';
import type { InboxItem } from '@/modules/inbox/types';
/**
* `getUnifiedInboxList()` is the list every selection surface indexes into — keyboard nav,
* shift-range select, the bulk actions and `getActiveListItems`. It must therefore return the
* RENDERED order, which since Phase 8 is the merged order cut at the watermark: a row the
* store has loaded but is holding back is not on screen, so selecting or stepping onto it
* would move the cursor somewhere the rep cannot see.
*/
const item = (channel: 'email' | 'slack', id: string, sortedAt: string): InboxItem =>
({
id: channel === 'email' ? `email:${id}` : id,
channel,
sortedAt,
counterpart: { name: id },
snippet: '',
unread: false,
starred: false,
hasDraft: false,
done: false,
labelIds: [],
participantCount: 1,
ref:
channel === 'email'
? { kind: 'email', threadId: id, connectionId: 'conn-1' }
: { kind: 'slack', conversationId: '', slackChannelId: id, workspaceId: 'W1' },
}) as unknown as InboxItem;
beforeEach(() => {
act(() => useCedarStore.getState().setChannelFeeds({}));
});
describe('getUnifiedInboxList', () => {
it('is the merged order, interleaved across channels by time', () => {
act(() =>
useCedarStore.getState().setChannelFeeds({
email: {
items: [
item('email', 'a', '2026-08-30T10:00:00.000Z'),
item('email', 'b', '2026-08-30T08:00:00.000Z'),
],
settled: true,
exhausted: true,
},
slack: {
items: [item('slack', 'slack:1', '2026-08-30T09:00:00.000Z')],
settled: true,
exhausted: true,
},
}),
);
// Email rows are keyed by their BARE thread id — the selection id, which is what
// `selectedThreadId`, `bulkSelected` and `data-thread-id` all speak.
expect(useCedarStore.getState().getUnifiedInboxList().map((row) => row.id)).toEqual([
'a',
'slack:1',
'b',
]);
});
it('stops at the watermark, so nothing selectable is off screen', () => {
act(() =>
useCedarStore.getState().setChannelFeeds({
email: {
items: [item('email', 'a', '2026-08-30T10:00:00.000Z')],
settled: true,
exhausted: false,
},
slack: {
items: [item('slack', 'slack:held', '2026-08-30T06:00:00.000Z')],
settled: true,
exhausted: false,
},
}),
);
const state = useCedarStore.getState();
expect(state.getUnifiedInboxList().map((row) => row.id)).toEqual(['a']);
expect(selectMergedFeed(state).held).toBe(1);
// Held, not dropped: the row is still loaded, so a deep link to it still resolves.
expect(state.getInboxItem('slack:held')).toBeDefined();
});
it('is empty while any participating channel is still resolving', () => {
act(() =>
useCedarStore.getState().setChannelFeeds({
email: {
items: [item('email', 'a', '2026-08-30T10:00:00.000Z')],
settled: true,
exhausted: true,
},
slack: { items: [], settled: false, exhausted: false },
}),
);
expect(useCedarStore.getState().getUnifiedInboxList()).toEqual([]);
});
it('returns the same array identity until the feed changes', () => {
const feeds = {
email: {
items: [item('email', 'a', '2026-08-30T10:00:00.000Z')],
settled: true,
exhausted: true,
},
};
act(() => useCedarStore.getState().setChannelFeeds(feeds));
// The merge sorts, and `useMailNavigation` takes this list as a dependency — recomputing
// it per render would re-sort the feed on every keystroke and churn the effect.
expect(useCedarStore.getState().getUnifiedInboxList()).toBe(
useCedarStore.getState().getUnifiedInboxList(),
);
});
});