Files
cinny/src/app/features/message-search/useMessageSearch.test.ts
T
Lotus CIandClaude Opus 5.5 0d08ba0a55
CI / Build & Quality Checks (pull_request) Successful in 2m57s
CI / Trigger Desktop Build (pull_request) Skipped
CI / Docker image build & smoke test (pull_request) Skipped
CI / Secret scan (gitleaks) (pull_request) Successful in 14s
CI / Playwright smoke (e2e) (pull_request) Successful in 14m21s
fix(search): an edited message shows once, with its latest text (#288)
Server search also returns the m.replace events, so one message edited
twice came back as three rows. Fold edits into their message: one row
at the best-ranked position, showing the latest m.new_content and
pointing at the original event so Open jumps to the real message.

Fixes #288

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017PCb59uY3UKqDqt7B6hzd5
2026-10-17 20:14:09 -04:00

157 lines
5.2 KiB
TypeScript

import { test } from 'node:test';
import assert from 'node:assert/strict';
import {
foldEdits,
filterGroupsByDateRange,
filterGroupsByMsgType,
filterGroupsByPinned,
ResultGroup,
} from './useMessageSearch';
// Minimal ResultGroup/ResultItem fixtures — only the fields the filters read
// (event.content.msgtype, event.event_id, group.roomId).
const item = (msgtype: string | undefined, eventId: string) => ({
rank: 1,
event: { event_id: eventId, content: msgtype === undefined ? {} : { msgtype } },
context: {},
});
const tsItem = (eventId: string, ts: number) => ({
rank: 1,
event: { event_id: eventId, origin_server_ts: ts, content: {} },
context: {},
});
const mkGroups = (
...groups: { roomId: string; items: ReturnType<typeof item>[] }[]
): ResultGroup[] => groups as unknown as ResultGroup[];
test('filterGroupsByMsgType: empty filter returns groups unchanged', () => {
const groups = mkGroups({ roomId: '!r1', items: [item('m.text', '$1'), item('m.image', '$2')] });
assert.equal(filterGroupsByMsgType(groups, []), groups);
});
test('filterGroupsByMsgType: keeps only matching msgtypes (union)', () => {
const groups = mkGroups({
roomId: '!r1',
items: [item('m.text', '$1'), item('m.image', '$2'), item('m.file', '$3')],
});
const out = filterGroupsByMsgType(groups, ['m.image', 'm.file']);
assert.equal(out.length, 1);
assert.deepEqual(
out[0].items.map((i) => i.event.event_id),
['$2', '$3'],
);
});
test('filterGroupsByMsgType: drops groups left empty', () => {
const groups = mkGroups(
{ roomId: '!r1', items: [item('m.text', '$1')] },
{ roomId: '!r2', items: [item('m.image', '$2')] },
);
const out = filterGroupsByMsgType(groups, ['m.image']);
assert.equal(out.length, 1);
assert.equal(out[0].roomId, '!r2');
});
test('filterGroupsByMsgType: ignores items with a non-string msgtype', () => {
const groups = mkGroups({ roomId: '!r1', items: [item(undefined, '$1'), item('m.video', '$2')] });
const out = filterGroupsByMsgType(groups, ['m.video']);
assert.equal(out[0].items.length, 1);
assert.equal(out[0].items[0].event.event_id, '$2');
});
test('filterGroupsByDateRange: no bounds returns groups unchanged', () => {
const groups = mkGroups({ roomId: '!r1', items: [tsItem('$1', 100)] });
assert.equal(filterGroupsByDateRange(groups, undefined, undefined), groups);
});
test('filterGroupsByDateRange: keeps only items within an inclusive range', () => {
const groups = mkGroups({
roomId: '!r1',
items: [tsItem('$1', 50), tsItem('$2', 100), tsItem('$3', 150), tsItem('$4', 200)],
});
const out = filterGroupsByDateRange(groups, 100, 150);
assert.deepEqual(
out[0].items.map((i) => i.event.event_id),
['$2', '$3'],
);
});
test('filterGroupsByDateRange: drops groups left empty and supports one-sided bounds', () => {
const groups = mkGroups(
{ roomId: '!r1', items: [tsItem('$1', 50)] },
{ roomId: '!r2', items: [tsItem('$2', 500)] },
);
const out = filterGroupsByDateRange(groups, 100, undefined);
assert.equal(out.length, 1);
assert.equal(out[0].roomId, '!r2');
});
test('filterGroupsByPinned: disabled returns groups unchanged', () => {
const groups = mkGroups({ roomId: '!r1', items: [item('m.text', '$1')] });
assert.equal(
filterGroupsByPinned(groups, false, () => false),
groups,
);
});
test('filterGroupsByPinned: keeps pinned items and drops empty groups', () => {
const groups = mkGroups(
{ roomId: '!r1', items: [item('m.text', '$1'), item('m.text', '$2')] },
{ roomId: '!r2', items: [item('m.text', '$3')] },
);
const pinned = new Set(['!r1/$2']);
const out = filterGroupsByPinned(groups, true, (roomId, eventId) =>
pinned.has(`${roomId}/${eventId}`),
);
assert.equal(out.length, 1);
assert.equal(out[0].roomId, '!r1');
assert.deepEqual(
out[0].items.map((i) => i.event.event_id),
['$2'],
);
});
const sr = (eventId: string, ts: number, content: Record<string, unknown>) =>
({
rank: 1,
result: {
event_id: eventId,
room_id: '!r:x',
origin_server_ts: ts,
type: 'm.room.message',
content,
},
}) as unknown as Parameters<typeof foldEdits>[0][number];
test('foldEdits: one row per message, showing the latest edit (#288)', () => {
const edit = (id: string, ts: number, body: string) =>
sr(id, ts, {
body: `* ${body}`,
'm.new_content': { body },
'm.relates_to': { rel_type: 'm.replace', event_id: '$orig' },
});
const out = foldEdits([
edit('$e2', 30, 'Tuesday 11am room B'),
sr('$orig', 10, { body: 'Monday 10am room A' }),
edit('$e1', 20, 'Tuesday 10am room A'),
sr('$other', 5, { body: 'meeting notes' }),
]);
assert.deepEqual(
out.map((r) => r.result.event_id),
['$orig', '$other'],
);
assert.deepEqual(out[0].result.content['m.new_content'], { body: 'Tuesday 11am room B' });
assert.equal(out[0].result.content.body, 'Monday 10am room A');
});
test('foldEdits: an edit whose original did not match stands in for it', () => {
const out = foldEdits([
sr('$e1', 20, {
'm.new_content': { body: 'x' },
'm.relates_to': { rel_type: 'm.replace', event_id: '$orig' },
}),
]);
assert.equal(out.length, 1);
assert.equal(out[0].result.event_id, '$orig');
});