import 'package:flutter_test/flutter_test.dart'; import 'package:ikar/backend/models/chat_folder.dart'; import 'package:ikar/backend/modules/chats.dart'; import 'package:ikar/backend/modules/folders.dart'; const int _me = 4242; const int _contactId = 777; CachedChat _chat({ required int id, required String type, int unreadCount = 0, int dontDisturbUntil = 0, Set options = const {}, int? owner, }) => CachedChat( id: id, accountId: _me, type: type, title: 'chat $id', unreadCount: unreadCount, lastEventTime: 1700000000000, cachedAt: 0, dontDisturbUntil: dontDisturbUntil, isOnline: false, seenTime: 0, participants: {_me: 1700000000000}, options: options, owner: owner, ); bool _matches(CachedChat chat, ChatFolder folder) => FoldersModule.chatMatchesFolder( chat, folder, myId: _me, contactIds: const {_contactId}, ); void main() { test( 'archive stays out of automatic folders but explicit inclusion survives', () { final source = [ for (final id in [101, 102, 103]) _chat(id: id, type: 'CHANNEL'), ]; const folder = ChatFolder( id: 'test', title: 'Test', filters: [FolderFilter.channel], include: [102], ); final result = FoldersModule.chatsForFolder( source, folder, myId: _me, contactIds: {}, archivedIds: {102, 103}, ); expect(result.map((chat) => chat.id), [101, 102]); }, ); group('ChatFolder.fromJson', () { test('reads the server payload of FOLDERS_UPDATE', () { final folder = ChatFolder.fromJson(const { 'id': '6fd177c5-ba2e-4360-9593-3ae798326806', 'title': 'Каналы', 'include': [111, -222], 'filters': [0, 11, 2], 'favorites': [111], 'options': [1, 2], 'updateTime': 1700000000000, 'sourceId': 1, }); expect(folder.id, '6fd177c5-ba2e-4360-9593-3ae798326806'); expect(folder.include, [111, -222]); expect(folder.filters, [ FolderFilter.unread, FolderFilter.notMuted, FolderFilter.channel, ]); expect(folder.favorites, [111]); expect(folder.updateTime, 1700000000000); expect(folder.sourceId, 1); expect(folder.canDelete, isFalse); expect(folder.canEditTitle, isFalse); expect(folder.canEditFilters, isTrue); }); test('normalizes legacy string filters and the hideEmpty flag', () { final folder = ChatFolder.fromJson(const { 'id': 'legacy', 'title': 'legacy', 'filters': ['CHANNEL', 'GROUP'], 'hideEmpty': true, }); expect(folder.filters, [FolderFilter.channel, FolderFilter.chat]); expect(folder.options, [FolderOption.hideEmpty]); expect(folder.hideEmpty, isTrue); }); }); group('chatMatchesFolder', () { const channelsFolder = ChatFolder( id: 'f1', title: 'Каналы', filters: [FolderFilter.channel], ); test('type filters are combined with OR', () { const folder = ChatFolder( id: 'f2', title: 'Каналы и боты', filters: [FolderFilter.channel, FolderFilter.bot], ); expect(_matches(_chat(id: 1, type: 'CHANNEL'), folder), isTrue); expect( _matches(_chat(id: 2, type: 'DIALOG', options: {'BOT'}), folder), isTrue, ); expect(_matches(_chat(id: 3, type: 'CHAT'), folder), isFalse); }); test('a folder without filters only holds its included chats', () { const folder = ChatFolder(id: 'f3', title: 'Свои', include: [10]); expect(_matches(_chat(id: 10, type: 'CHAT'), folder), isTrue); expect(_matches(_chat(id: 11, type: 'CHAT'), folder), isFalse); }); test('show-only filters narrow both types and included chats', () { const folder = ChatFolder( id: 'f4', title: 'Непрочитанные каналы', include: [10], filters: [FolderFilter.channel, FolderFilter.unread], ); expect( _matches(_chat(id: 1, type: 'CHANNEL', unreadCount: 3), folder), isTrue, ); expect(_matches(_chat(id: 2, type: 'CHANNEL'), folder), isFalse); expect( _matches(_chat(id: 10, type: 'CHAT', unreadCount: 1), folder), isTrue, ); expect(_matches(_chat(id: 10, type: 'CHAT'), folder), isFalse); }); test('show-only filters are combined with AND', () { const folder = ChatFolder( id: 'f5', title: 'Непрочитанные с уведомлениями', filters: [ FolderFilter.channel, FolderFilter.unread, FolderFilter.notMuted, ], ); expect( _matches(_chat(id: 1, type: 'CHANNEL', unreadCount: 1), folder), isTrue, ); expect( _matches( _chat(id: 2, type: 'CHANNEL', unreadCount: 1, dontDisturbUntil: -1), folder, ), isFalse, ); }); test('an expired mute counts as not muted', () { const folder = ChatFolder( id: 'f6', title: 'С уведомлениями', filters: [FolderFilter.channel, FolderFilter.notMuted], ); expect( _matches( _chat(id: 1, type: 'CHANNEL', dontDisturbUntil: 1700000000000), folder, ), isTrue, ); }); test('unknown filters do not empty a folder', () { const folder = ChatFolder( id: 'f7', title: 'Каналы', filters: [FolderFilter.channel, FolderFilter.markedUnread], ); expect(_matches(_chat(id: 1, type: 'CHANNEL'), folder), isTrue); }); test('role filters keep only chats with that role', () { const folder = ChatFolder( id: 'f8', title: 'Мои группы', filters: [FolderFilter.chat, FolderFilter.owner], ); expect(_matches(_chat(id: 1, type: 'CHAT', owner: _me), folder), isTrue); expect(_matches(_chat(id: 2, type: 'CHAT', owner: 5), folder), isFalse); }); test('a folder of only show-only filters narrows all chats', () { const folder = ChatFolder( id: 'f9', title: 'Новые', filters: [FolderFilter.unread], ); expect( _matches(_chat(id: 1, type: 'DIALOG', unreadCount: 1), folder), isTrue, ); expect( _matches(_chat(id: 2, type: 'CHAT', unreadCount: 4), folder), isTrue, ); expect( _matches(_chat(id: 3, type: 'CHANNEL', unreadCount: 2), folder), isTrue, ); expect(_matches(_chat(id: 4, type: 'DIALOG'), folder), isFalse); }); test('a role-only folder narrows all chats', () { const folder = ChatFolder( id: 'f14', title: 'Мои', filters: [FolderFilter.owner], ); expect(_matches(_chat(id: 1, type: 'CHAT', owner: _me), folder), isTrue); expect( _matches(_chat(id: 2, type: 'CHANNEL', owner: _me), folder), isTrue, ); expect(_matches(_chat(id: 3, type: 'CHAT', owner: 5), folder), isFalse); }); test('hand-picked chats keep a show-only folder to themselves', () { const folder = ChatFolder( id: 'f15', title: 'Непрочитанные свои', include: [10], filters: [FolderFilter.unread], ); expect( _matches(_chat(id: 10, type: 'CHAT', unreadCount: 1), folder), isTrue, ); expect( _matches(_chat(id: 11, type: 'CHAT', unreadCount: 1), folder), isFalse, ); }); test('unknown filters alone do not pull in every chat', () { const folder = ChatFolder( id: 'f16', title: 'Помеченные', filters: [FolderFilter.markedUnread], ); expect( _matches(_chat(id: 1, type: 'CHAT', unreadCount: 1), folder), isFalse, ); }); test('channels folder ignores groups and dialogs', () { expect(_matches(_chat(id: 1, type: 'CHANNEL'), channelsFolder), isTrue); expect(_matches(_chat(id: 2, type: 'CHAT'), channelsFolder), isFalse); expect(_matches(_chat(id: 3, type: 'DIALOG'), channelsFolder), isFalse); }); }); group('chatsForFolder', () { List ids( List chats, ChatFolder folder, { Set archived = const {}, }) => FoldersModule.chatsForFolder( chats, folder, myId: _me, contactIds: const {_contactId}, archivedIds: archived, ).map((c) => c.id).toList(); test('an archived chat stays in the folder it was added to by hand', () { const folder = ChatFolder(id: 'f10', title: 'Свои', include: [10, 11]); final chats = [_chat(id: 10, type: 'CHAT'), _chat(id: 11, type: 'CHAT')]; expect(ids(chats, folder, archived: {10, 11}), [10, 11]); }); test('archiving hides a chat that only matched a type filter', () { const folder = ChatFolder( id: 'f11', title: 'Каналы', filters: [FolderFilter.channel], ); final chats = [ _chat(id: 20, type: 'CHANNEL'), _chat(id: 21, type: 'CHANNEL'), ]; expect(ids(chats, folder, archived: {21}), [20]); }); test('show-only filters still apply to an archived included chat', () { const folder = ChatFolder( id: 'f12', title: 'Непрочитанные', include: [30, 31], filters: [FolderFilter.unread], ); final chats = [ _chat(id: 30, type: 'CHAT', unreadCount: 2), _chat(id: 31, type: 'CHAT'), ]; expect(ids(chats, folder, archived: {30, 31}), [30]); }); test('without an archive the result matches chatMatchesFolder', () { const folder = ChatFolder( id: 'f13', title: 'Каналы и свои', include: [41], filters: [FolderFilter.channel], ); final chats = [ _chat(id: 40, type: 'CHANNEL'), _chat(id: 41, type: 'CHAT'), _chat(id: 42, type: 'DIALOG'), ]; expect(ids(chats, folder), [40, 41]); }); }); group('reshapesChatList', () { test('reacts to what moves a chat between folders', () { final read = _chat(id: 1, type: 'CHAT'); final unread = _chat(id: 1, type: 'CHAT', unreadCount: 1); final moreUnread = _chat(id: 1, type: 'CHAT', unreadCount: 5); final muted = _chat(id: 1, type: 'CHAT', dontDisturbUntil: -1); final owned = _chat(id: 1, type: 'CHAT', owner: _me); expect(ChatsModule.reshapesChatList(read, unread), isTrue); expect(ChatsModule.reshapesChatList(unread, read), isTrue); expect(ChatsModule.reshapesChatList(read, muted), isTrue); expect(ChatsModule.reshapesChatList(read, owned), isTrue); expect(ChatsModule.reshapesChatList(unread, moreUnread), isFalse); expect(ChatsModule.reshapesChatList(read, read), isFalse); }); }); group('newFolderId', () { test('generates distinct uuid v4 ids', () { final a = FoldersModule.newFolderId(); final b = FoldersModule.newFolderId(); expect(a, isNot(b)); expect( RegExp( r'^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$', ).hasMatch(a), isTrue, ); }); }); }