import 'package:flutter_test/flutter_test.dart'; import 'package:marianum_mobile/api/marianumcloud/search/search_chat_messages_response.dart'; import 'package:marianum_mobile/api/marianumcloud/talk/chat/get_chat_response.dart'; import 'package:marianum_mobile/api/marianumcloud/talk/room/get_room_response.dart'; import 'package:marianum_mobile/view/pages/talk/data/chat_message_matcher.dart'; GetChatResponseObject _msg({ required int id, required int timestamp, String actorDisplayName = 'Anyone', String message = '', String systemMessage = '', GetRoomResponseObjectMessageType type = GetRoomResponseObjectMessageType.comment, Map? params, }) => GetChatResponseObject( id, 'token', GetRoomResponseObjectMessageActorType.user, 'actor-id', actorDisplayName, timestamp, systemMessage, type, true, '', message, params, null, null, null, ); GetChatResponse _response(List messages) => GetChatResponse(messages.toSet()); void main() { group('ChatMessageMatcher.findMatches', () { test('empty query returns no matches', () { final response = _response([ _msg(id: 1, timestamp: 100, message: 'Hallo'), ]); expect(ChatMessageMatcher.findMatches(response, ''), isEmpty); expect(ChatMessageMatcher.findMatches(response, ' '), isEmpty); }); test('matches message text case-insensitively', () { final response = _response([ _msg(id: 1, timestamp: 100, message: 'Hallo Welt'), _msg(id: 2, timestamp: 200, message: 'nichts hier'), ]); final matches = ChatMessageMatcher.findMatches(response, 'WELT'); expect(matches.length, 1); expect(matches.first.messageId, 1); }); test('matches actor display name', () { final response = _response([ _msg( id: 1, timestamp: 100, actorDisplayName: 'Lisa Maier', message: 'irgendwas', ), _msg( id: 2, timestamp: 200, actorDisplayName: 'Tom Weber', message: 'auch was', ), ]); final matches = ChatMessageMatcher.findMatches(response, 'lisa'); expect(matches.length, 1); expect(matches.first.messageId, 1); }); test('system messages match on text but not on actor', () { final response = _response([ _msg( id: 1, timestamp: 100, actorDisplayName: 'Lisa', message: 'Lisa ist beigetreten', type: GetRoomResponseObjectMessageType.system, ), ]); // Match on text content expect(ChatMessageMatcher.findMatches(response, 'beigetreten').length, 1); // Actor name alone (not in text) should not match for system messages final actorOnlyResponse = _response([ _msg( id: 1, timestamp: 100, actorDisplayName: 'Lisa', message: 'jemand ist beigetreten', type: GetRoomResponseObjectMessageType.system, ), ]); expect( ChatMessageMatcher.findMatches(actorOnlyResponse, 'lisa'), isEmpty, ); }); test( 'reaction, poll_voted and message_deleted system messages are filtered out', () { final response = _response([ _msg( id: 1, timestamp: 100, message: 'Treffer', systemMessage: 'reaction', type: GetRoomResponseObjectMessageType.system, ), _msg( id: 2, timestamp: 200, message: 'Treffer', systemMessage: 'poll_voted', type: GetRoomResponseObjectMessageType.system, ), _msg( id: 4, timestamp: 250, message: 'Treffer', systemMessage: 'message_deleted', type: GetRoomResponseObjectMessageType.system, ), _msg(id: 3, timestamp: 300, message: 'Treffer'), ]); final matches = ChatMessageMatcher.findMatches(response, 'Treffer'); expect(matches.length, 1); expect(matches.first.messageId, 3); }, ); test('rich object parameters are searchable (e.g. file names)', () { final response = _response([ _msg( id: 1, timestamp: 100, message: '{file}', params: { 'file': RichObjectString( RichObjectStringObjectType.file, '42', 'hausaufgaben.pdf', null, null, ), }, ), ]); final matches = ChatMessageMatcher.findMatches(response, 'hausaufgab'); expect(matches.length, 1); expect(matches.first.messageId, 1); }); test('matches are sorted newest first', () { final response = _response([ _msg(id: 1, timestamp: 100, message: 'X'), _msg(id: 2, timestamp: 300, message: 'X'), _msg(id: 3, timestamp: 200, message: 'X'), ]); final matches = ChatMessageMatcher.findMatches(response, 'x'); expect(matches.map((m) => m.messageId).toList(), [2, 3, 1]); }); }); group('ChatMessageMatcher.merge', () { test('dedupes by id preferring local hits, newest id first', () { const local = [ChatSearchHit(messageId: 5, text: 'lokal')]; const server = [ ChatSearchHit(messageId: 9, text: 'neu'), ChatSearchHit(messageId: 5, text: 'server'), ChatSearchHit(messageId: 2, text: 'alt'), ]; final merged = ChatMessageMatcher.merge(local, server); expect(merged.map((h) => h.messageId).toList(), [9, 5, 2]); expect(merged[1].text, 'lokal'); }); }); group('SearchChatMessagesEntry.fromJson', () { test('reads id and timestamp from attributes', () { final entry = SearchChatMessagesEntry.fromJson({ 'title': 'Lisa Maier', 'subline': 'Die Hausaufgabe ist bis Freitag', 'attributes': { 'messageId': '123', 'actorId': 'lmaier', 'timestamp': '1700000000', }, }, term: 'hausaufgabe')!; expect(entry.messageId, 123); expect(entry.actorId, 'lmaier'); expect(entry.actorDisplayName, 'Lisa Maier'); expect(entry.excerpt, 'Die Hausaufgabe ist bis Freitag'); expect(entry.sentAt, DateTime.fromMillisecondsSinceEpoch(1700000000000)); }); test('detects excerpt in title and falls back to resourceUrl id', () { final entry = SearchChatMessagesEntry.fromJson({ 'title': '…die Hausaufgabe', 'subline': 'Lisa Maier', 'resourceUrl': 'https://cloud/index.php/call/abc#message_77', }, term: 'hausaufgabe')!; expect(entry.messageId, 77); expect(entry.excerpt, '…die Hausaufgabe'); expect(entry.actorDisplayName, 'Lisa Maier'); expect(entry.sentAt, isNull); }); test('entries without message id are dropped', () { final response = SearchChatMessagesResponse.fromJson({ 'isPaginated': true, 'cursor': 25, 'entries': [ {'title': 'x', 'subline': 'y'}, ], }, term: 'y'); expect(response.entries, isEmpty); expect(response.cursor, isNull); }); }); }