OSDN Git Service

TabInformationsクラスのテストケースを追加
[opentween/open-tween.git] / OpenTween.Tests / Models / TabInformationTest.cs
1 // OpenTween - Client of Twitter
2 // Copyright (c) 2014 kim_upsilon (@kim_upsilon) <https://upsilo.net/~upsilon/>
3 // All rights reserved.
4 //
5 // This file is part of OpenTween.
6 //
7 // This program is free software; you can redistribute it and/or modify it
8 // under the terms of the GNU General Public License as published by the Free
9 // Software Foundation; either version 3 of the License, or (at your option)
10 // any later version.
11 //
12 // This program is distributed in the hope that it will be useful, but
13 // WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
14 // or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
15 // for more details.
16 //
17 // You should have received a copy of the GNU General Public License along
18 // with this program. If not, see <http://www.gnu.org/licenses/>, or write to
19 // the Free Software Foundation, Inc., 51 Franklin Street - Fifth Floor,
20 // Boston, MA 02110-1301, USA.
21
22 using System;
23 using System.Collections.Generic;
24 using System.Linq;
25 using System.Reflection;
26 using System.Text;
27 using System.Windows.Forms;
28 using Xunit;
29 using Xunit.Extensions;
30
31 namespace OpenTween.Models
32 {
33     public class TabInformationTest
34     {
35         private readonly TabInformations tabinfo;
36
37         public TabInformationTest()
38         {
39             this.tabinfo = this.CreateInstance();
40
41             // TabInformation.GetInstance() で取得できるようにする
42             var field = typeof(TabInformations).GetField("Instance",
43                 BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.SetField);
44             field.SetValue(null, this.tabinfo);
45
46             // 標準のタブを追加
47             this.tabinfo.AddTab(new HomeTabModel("Recent"));
48             this.tabinfo.AddTab(new MentionsTabModel("Reply"));
49             this.tabinfo.AddTab(new DirectMessagesTabModel("DM"));
50             this.tabinfo.AddTab(new FavoritesTabModel("Favorites"));
51         }
52
53         private TabInformations CreateInstance()
54             => (TabInformations)Activator.CreateInstance(typeof(TabInformations), true);
55
56         [Fact]
57         public void AddTab_Test()
58         {
59             var tab = new FilterTabModel("MyTab");
60             var ret = this.tabinfo.AddTab(tab);
61
62             Assert.True(ret);
63             Assert.Same(tab, this.tabinfo.Tabs.Last());
64         }
65
66         [Fact]
67         public void AddTab_DuplicateTest()
68         {
69             var ret = this.tabinfo.AddTab(new FilterTabModel("Recent"));
70
71             Assert.False(ret);
72         }
73
74         [Fact]
75         public void RemoveTab_InnerStorageTabTest()
76         {
77             var tab = new PublicSearchTabModel("search");
78             tab.AddPostQueue(new PostClass { StatusId = 100L });
79             this.tabinfo.AddTab(tab);
80             this.tabinfo.SubmitUpdate();
81
82             Assert.True(this.tabinfo.ContainsTab("search"));
83             Assert.Empty(this.tabinfo.RemovedTab);
84
85             this.tabinfo.RemoveTab("search");
86
87             Assert.False(this.tabinfo.ContainsTab("search"));
88             Assert.Single(this.tabinfo.RemovedTab);
89             Assert.Contains(tab, this.tabinfo.RemovedTab);
90         }
91
92         [Fact]
93         public void RemoveTab_FilterTab_MovedPost_OrphanedTest()
94         {
95             var filterTab = new FilterTabModel("filter");
96             filterTab.AddFilter(new() { FilterName = "opentween", MoveMatches = true });
97             this.tabinfo.AddTab(filterTab);
98
99             this.tabinfo.AddPost(new PostClass { StatusId = 100L, ScreenName = "opentween" });
100             this.tabinfo.DistributePosts();
101             this.tabinfo.SubmitUpdate();
102
103             Assert.False(this.tabinfo.HomeTab.Contains(100L));
104             Assert.True(filterTab.Contains(100L));
105
106             this.tabinfo.RemoveTab("filter");
107
108             Assert.False(this.tabinfo.ContainsTab("filter"));
109             Assert.Single(this.tabinfo.RemovedTab);
110             Assert.Contains(filterTab, this.tabinfo.RemovedTab);
111
112             // 他に MoveMatches で移動している振り分けタブが存在しなければ Home タブに戻す
113             Assert.True(this.tabinfo.HomeTab.Contains(100L));
114         }
115
116         [Fact]
117         public void RemoveTab_FilterTab_MovedPost_NotOrphanedTest()
118         {
119             var filterTab1 = new FilterTabModel("filter1");
120             filterTab1.AddFilter(new() { FilterName = "opentween", MoveMatches = true });
121             this.tabinfo.AddTab(filterTab1);
122
123             var filterTab2 = new FilterTabModel("filter2");
124             filterTab2.AddFilter(new() { FilterName = "opentween", MoveMatches = true });
125             this.tabinfo.AddTab(filterTab2);
126
127             this.tabinfo.AddPost(new PostClass { StatusId = 100L, ScreenName = "opentween" });
128             this.tabinfo.DistributePosts();
129             this.tabinfo.SubmitUpdate();
130
131             Assert.False(this.tabinfo.HomeTab.Contains(100L));
132             Assert.True(filterTab1.Contains(100L));
133             Assert.True(filterTab2.Contains(100L));
134
135             this.tabinfo.RemoveTab("filter1");
136
137             Assert.False(this.tabinfo.ContainsTab("filter1"));
138             Assert.Single(this.tabinfo.RemovedTab);
139             Assert.Contains(filterTab1, this.tabinfo.RemovedTab);
140
141             // 他に MoveMatches で移動している振り分けタブが存在する場合は Home タブに戻さない
142             Assert.False(this.tabinfo.HomeTab.Contains(100L));
143             Assert.True(filterTab2.Contains(100L));
144         }
145
146         [Fact]
147         public void RemoveTab_FilterTab_CopiedPost_Test()
148         {
149             var filterTab = new FilterTabModel("filter");
150             filterTab.AddFilter(new() { FilterName = "opentween", MoveMatches = false });
151             this.tabinfo.AddTab(filterTab);
152
153             this.tabinfo.AddPost(new PostClass { StatusId = 100L, ScreenName = "opentween" });
154             this.tabinfo.DistributePosts();
155             this.tabinfo.SubmitUpdate();
156
157             Assert.True(this.tabinfo.HomeTab.Contains(100L));
158             Assert.True(filterTab.Contains(100L));
159
160             this.tabinfo.RemoveTab("filter");
161
162             Assert.False(this.tabinfo.ContainsTab("filter"));
163             Assert.Single(this.tabinfo.RemovedTab);
164             Assert.Contains(filterTab, this.tabinfo.RemovedTab);
165
166             // 振り分けタブにコピーされた発言は Home タブにも存在しているため何もしない
167             Assert.True(this.tabinfo.HomeTab.Contains(100L));
168         }
169
170         [Fact]
171         public void RenameTab_PositionTest()
172         {
173             var replyTab = this.tabinfo.Tabs["Reply"];
174             Assert.Equal(1, this.tabinfo.Tabs.IndexOf(replyTab));
175
176             this.tabinfo.RenameTab("Reply", "Reply12345");
177
178             Assert.Equal("Reply12345", replyTab.TabName);
179             Assert.Equal(1, this.tabinfo.Tabs.IndexOf(replyTab));
180         }
181
182         [Fact]
183         public void RenameTab_SelectedTabTest()
184         {
185             var replyTab = this.tabinfo.Tabs["Reply"];
186
187             this.tabinfo.SelectTab("Reply");
188             this.tabinfo.RenameTab("Reply", "Reply12345");
189
190             Assert.Equal("Reply12345", replyTab.TabName);
191             Assert.Equal("Reply12345", this.tabinfo.SelectedTabName);
192             Assert.Equal(replyTab, this.tabinfo.SelectedTab);
193         }
194
195         [Fact]
196         public void MoveTab_MoveToStart_Test()
197         {
198             Assert.Equal(0, this.tabinfo.Tabs.IndexOf("Recent"));
199             Assert.Equal(1, this.tabinfo.Tabs.IndexOf("Reply"));
200
201             this.tabinfo.MoveTab(0, this.tabinfo.MentionTab);
202
203             Assert.Equal(0, this.tabinfo.Tabs.IndexOf("Reply"));
204             Assert.Equal(1, this.tabinfo.Tabs.IndexOf("Recent"));
205         }
206
207         [Fact]
208         public void MoveTab_MoveToEnd_Test()
209         {
210             Assert.Equal(4, this.tabinfo.Tabs.Count);
211             Assert.Equal(2, this.tabinfo.Tabs.IndexOf("DM"));
212             Assert.Equal(3, this.tabinfo.Tabs.IndexOf("Favorites"));
213
214             this.tabinfo.MoveTab(3, this.tabinfo.DirectMessageTab);
215
216             Assert.Equal(2, this.tabinfo.Tabs.IndexOf("Favorites"));
217             Assert.Equal(3, this.tabinfo.Tabs.IndexOf("DM"));
218         }
219
220         [Fact]
221         public void MoveTab_OutOfRangeError_Test()
222         {
223             Assert.Equal(4, this.tabinfo.Tabs.Count);
224             Assert.Throws<ArgumentOutOfRangeException>(
225                 () => this.tabinfo.MoveTab(-1, this.tabinfo.HomeTab)
226             );
227             Assert.Throws<ArgumentOutOfRangeException>(
228                 () => this.tabinfo.MoveTab(4, this.tabinfo.HomeTab)
229             );
230         }
231
232         [Theory]
233         [InlineData("Reply", true)]
234         [InlineData("UNKNOWN NAME", false)]
235         public void ContainsTab_TabName_Test(string tabName, bool expected)
236             => Assert.Equal(expected, this.tabinfo.ContainsTab(tabName));
237
238         [Fact]
239         public void ContainsTab_TabInstance_Test()
240         {
241             Assert.True(this.tabinfo.ContainsTab(this.tabinfo.HomeTab));
242             Assert.False(this.tabinfo.ContainsTab(new PublicSearchTabModel("tab")));
243         }
244
245         [Fact]
246         public void SelectTab_Test()
247         {
248             this.tabinfo.SelectTab("Reply");
249
250             Assert.Equal("Reply", this.tabinfo.SelectedTabName);
251             Assert.IsType<MentionsTabModel>(this.tabinfo.SelectedTab);
252         }
253
254         [Fact]
255         public void SelectTab_NotExistTest()
256             => Assert.Throws<ArgumentException>(() => this.tabinfo.SelectTab("INVALID"));
257
258         [Theory]
259         [InlineData(MyCommon.TabUsageType.Home, typeof(HomeTabModel))]
260         [InlineData(MyCommon.TabUsageType.Mentions, typeof(MentionsTabModel))]
261         [InlineData(MyCommon.TabUsageType.DirectMessage, typeof(DirectMessagesTabModel))]
262         [InlineData(MyCommon.TabUsageType.Favorites, typeof(FavoritesTabModel))]
263         [InlineData(MyCommon.TabUsageType.UserDefined, typeof(FilterTabModel))]
264         [InlineData(MyCommon.TabUsageType.UserTimeline, typeof(UserTimelineTabModel))]
265         [InlineData(MyCommon.TabUsageType.PublicSearch, typeof(PublicSearchTabModel))]
266         [InlineData(MyCommon.TabUsageType.Lists, typeof(ListTimelineTabModel))]
267         [InlineData(MyCommon.TabUsageType.Mute, typeof(MuteTabModel))]
268         public void CreateTabFromSettings_TabTypeTest(MyCommon.TabUsageType tabType, Type expected)
269         {
270             var tabSetting = new SettingTabs.SettingTabItem
271             {
272                 TabName = "tab",
273                 TabType = tabType,
274             };
275             var tabinfo = this.CreateInstance();
276             var tab = tabinfo.CreateTabFromSettings(tabSetting);
277             Assert.IsType(expected, tab);
278         }
279
280         [Fact]
281         public void CreateTabFromSettings_FilterTabTest()
282         {
283             var tabSetting = new SettingTabs.SettingTabItem
284             {
285                 TabName = "tab",
286                 TabType = MyCommon.TabUsageType.UserDefined,
287                 FilterArray = new PostFilterRule[]
288                 {
289                     new() { FilterName = "foo" },
290                 },
291             };
292             var tabinfo = this.CreateInstance();
293             var tab = tabinfo.CreateTabFromSettings(tabSetting);
294             Assert.IsType<FilterTabModel>(tab);
295
296             var filterTab = (FilterTabModel)tab!;
297             Assert.Equal("foo", filterTab.FilterArray.First().FilterName);
298         }
299
300         [Fact]
301         public void CreateTabFromSettings_PublicSearchTabTest()
302         {
303             var tabSetting = new SettingTabs.SettingTabItem
304             {
305                 TabName = "tab",
306                 TabType = MyCommon.TabUsageType.PublicSearch,
307                 SearchWords = "foo",
308                 SearchLang = "ja",
309             };
310             var tabinfo = this.CreateInstance();
311             var tab = tabinfo.CreateTabFromSettings(tabSetting);
312             Assert.IsType<PublicSearchTabModel>(tab);
313
314             var searchTab = (PublicSearchTabModel)tab!;
315             Assert.Equal("foo", searchTab.SearchWords);
316             Assert.Equal("ja", searchTab.SearchLang);
317         }
318
319         [Fact]
320         public void AddDefaultTabs_Test()
321         {
322             var tabinfo = this.CreateInstance();
323             Assert.Equal(0, tabinfo.Tabs.Count);
324
325             tabinfo.AddDefaultTabs();
326
327             Assert.Equal(4, tabinfo.Tabs.Count);
328             Assert.IsType<HomeTabModel>(tabinfo.Tabs[0]);
329             Assert.IsType<MentionsTabModel>(tabinfo.Tabs[1]);
330             Assert.IsType<DirectMessagesTabModel>(tabinfo.Tabs[2]);
331             Assert.IsType<FavoritesTabModel>(tabinfo.Tabs[3]);
332
333             var homeTab = tabinfo.HomeTab;
334             Assert.False(homeTab.Notify);
335
336             var mentionsTab = tabinfo.MentionTab;
337             Assert.True(mentionsTab.Notify);
338
339             var dmTab = tabinfo.DirectMessageTab;
340             Assert.True(dmTab.Notify);
341
342             var favsTab = tabinfo.FavoriteTab;
343             Assert.False(favsTab.Notify);
344         }
345
346         [Fact]
347         public void MakeTabName_Test()
348         {
349             var baseTabName = "NewTab";
350             Assert.Equal("NewTab", this.tabinfo.MakeTabName(baseTabName, 5));
351         }
352
353         [Fact]
354         public void MakeTabName_RetryTest()
355         {
356             this.tabinfo.AddTab(new FilterTabModel("NewTab"));
357             this.tabinfo.AddTab(new FilterTabModel("NewTab2"));
358
359             var baseTabName = "NewTab";
360             Assert.Equal("NewTab3", this.tabinfo.MakeTabName(baseTabName, 5));
361         }
362
363         [Fact]
364         public void MakeTabName_RetryErrorTest()
365         {
366             this.tabinfo.AddTab(new FilterTabModel("NewTab"));
367             this.tabinfo.AddTab(new FilterTabModel("NewTab2"));
368             this.tabinfo.AddTab(new FilterTabModel("NewTab3"));
369             this.tabinfo.AddTab(new FilterTabModel("NewTab4"));
370             this.tabinfo.AddTab(new FilterTabModel("NewTab5"));
371
372             var baseTabName = "NewTab";
373             Assert.Throws<TabException>(() => this.tabinfo.MakeTabName(baseTabName, 5));
374         }
375
376         [Fact]
377         public void SetSortMode_Test()
378         {
379             this.tabinfo.SetSortMode(ComparerMode.Id, SortOrder.Descending);
380             Assert.Equal(ComparerMode.Id, this.tabinfo.SortMode);
381             Assert.Equal(SortOrder.Descending, this.tabinfo.SortOrder);
382             Assert.Equal(ComparerMode.Id, this.tabinfo.HomeTab.SortMode);
383             Assert.Equal(SortOrder.Descending, this.tabinfo.HomeTab.SortOrder);
384
385             this.tabinfo.SetSortMode(ComparerMode.Source, SortOrder.Ascending);
386             Assert.Equal(ComparerMode.Source, this.tabinfo.SortMode);
387             Assert.Equal(SortOrder.Ascending, this.tabinfo.SortOrder);
388             Assert.Equal(ComparerMode.Source, this.tabinfo.HomeTab.SortMode);
389             Assert.Equal(SortOrder.Ascending, this.tabinfo.HomeTab.SortOrder);
390         }
391
392         [Fact]
393         public void ToggleSortOrder_SameMode_Test()
394         {
395             this.tabinfo.SetSortMode(ComparerMode.Id, SortOrder.Descending);
396             Assert.Equal(ComparerMode.Id, this.tabinfo.SortMode);
397             Assert.Equal(SortOrder.Descending, this.tabinfo.SortOrder);
398
399             this.tabinfo.ToggleSortOrder(ComparerMode.Id);
400             Assert.Equal(ComparerMode.Id, this.tabinfo.SortMode);
401             Assert.Equal(SortOrder.Ascending, this.tabinfo.SortOrder);
402
403             this.tabinfo.ToggleSortOrder(ComparerMode.Id);
404             Assert.Equal(ComparerMode.Id, this.tabinfo.SortMode);
405             Assert.Equal(SortOrder.Descending, this.tabinfo.SortOrder);
406         }
407
408         [Fact]
409         public void ToggleSortOrder_OtherMode_Test()
410         {
411             this.tabinfo.SetSortMode(ComparerMode.Id, SortOrder.Descending);
412             Assert.Equal(ComparerMode.Id, this.tabinfo.SortMode);
413             Assert.Equal(SortOrder.Descending, this.tabinfo.SortOrder);
414
415             this.tabinfo.ToggleSortOrder(ComparerMode.Source);
416             Assert.Equal(ComparerMode.Source, this.tabinfo.SortMode);
417             Assert.Equal(SortOrder.Ascending, this.tabinfo.SortOrder);
418         }
419
420         [Fact]
421         public void IsMuted_Test()
422         {
423             this.tabinfo.MuteUserIds = new HashSet<long> { 12345L };
424
425             var post = new PostClass
426             {
427                 UserId = 12345L,
428                 Text = "hogehoge",
429             };
430             Assert.True(this.tabinfo.IsMuted(post, isHomeTimeline: true));
431         }
432
433         [Fact]
434         public void IsMuted_NotMutingTest()
435         {
436             this.tabinfo.MuteUserIds = new HashSet<long> { 12345L };
437
438             var post = new PostClass
439             {
440                 UserId = 11111L,
441                 Text = "hogehoge",
442             };
443             Assert.False(this.tabinfo.IsMuted(post, isHomeTimeline: true));
444         }
445
446         [Fact]
447         public void IsMuted_RetweetTest()
448         {
449             this.tabinfo.MuteUserIds = new HashSet<long> { 12345L };
450
451             var post = new PostClass
452             {
453                 UserId = 11111L,
454                 RetweetedByUserId = 12345L,
455                 Text = "hogehoge",
456             };
457             Assert.True(this.tabinfo.IsMuted(post, isHomeTimeline: true));
458         }
459
460         [Fact]
461         public void IsMuted_RetweetNotMutingTest()
462         {
463             this.tabinfo.MuteUserIds = new HashSet<long> { 12345L };
464
465             var post = new PostClass
466             {
467                 UserId = 11111L,
468                 RetweetedByUserId = 22222L,
469                 Text = "hogehoge",
470             };
471             Assert.False(this.tabinfo.IsMuted(post, isHomeTimeline: true));
472         }
473
474         [Fact]
475         public void IsMuted_ReplyTest()
476         {
477             this.tabinfo.MuteUserIds = new HashSet<long> { 12345L };
478
479             // ミュート対象のユーザーであってもリプライの場合は対象外とする
480             var post = new PostClass
481             {
482                 UserId = 12345L,
483                 Text = "@foo hogehoge",
484                 IsReply = true,
485             };
486             Assert.False(this.tabinfo.IsMuted(post, isHomeTimeline: true));
487         }
488
489         [Fact]
490         public void IsMuted_NotInHomeTimelineTest()
491         {
492             this.tabinfo.MuteUserIds = new HashSet<long> { 12345L };
493
494             // Recent以外のタブ(検索など)の場合は対象外とする
495             var post = new PostClass
496             {
497                 UserId = 12345L,
498                 Text = "hogehoge",
499             };
500             Assert.False(this.tabinfo.IsMuted(post, isHomeTimeline: false));
501         }
502
503         [Fact]
504         public void IsMuted_MuteTabRulesTest()
505         {
506             this.tabinfo.MuteUserIds = new HashSet<long> { };
507
508             var muteTab = new MuteTabModel();
509             muteTab.AddFilter(new PostFilterRule
510             {
511                 FilterName = "foo",
512                 MoveMatches = true,
513             });
514             this.tabinfo.AddTab(muteTab);
515
516             var post = new PostClass
517             {
518                 UserId = 12345L,
519                 ScreenName = "foo",
520                 Text = "hogehoge",
521             };
522             Assert.True(this.tabinfo.IsMuted(post, isHomeTimeline: true));
523         }
524
525         [Fact]
526         public void IsMuted_MuteTabRules_NotInHomeTimelineTest()
527         {
528             this.tabinfo.MuteUserIds = new HashSet<long> { };
529
530             var muteTab = new MuteTabModel();
531             muteTab.AddFilter(new PostFilterRule
532             {
533                 FilterName = "foo",
534                 MoveMatches = true,
535             });
536             this.tabinfo.AddTab(muteTab);
537
538             // ミュートタブによるミュートはリプライも対象とする
539             var post = new PostClass
540             {
541                 UserId = 12345L,
542                 ScreenName = "foo",
543                 Text = "@hoge hogehoge",
544                 IsReply = true,
545             };
546             Assert.True(this.tabinfo.IsMuted(post, isHomeTimeline: false));
547         }
548
549         [Fact]
550         public void SetReadAllTab_MarkAsReadTest()
551         {
552             var tab1 = new PublicSearchTabModel("search1");
553             var tab2 = new PublicSearchTabModel("search2");
554
555             this.tabinfo.AddTab(tab1);
556             this.tabinfo.AddTab(tab2);
557
558             // search1 に追加するツイート (StatusId: 100, 150, 200; すべて未読)
559             tab1.UnreadManage = true;
560             tab1.AddPostQueue(new PostClass { StatusId = 100L, IsRead = false });
561             tab1.AddPostQueue(new PostClass { StatusId = 150L, IsRead = false });
562             tab1.AddPostQueue(new PostClass { StatusId = 200L, IsRead = false });
563
564             // search2 に追加するツイート (StatusId: 150, 200, 250; すべて未読)
565             tab2.UnreadManage = true;
566             tab2.AddPostQueue(new PostClass { StatusId = 150L, IsRead = false });
567             tab2.AddPostQueue(new PostClass { StatusId = 200L, IsRead = false });
568             tab2.AddPostQueue(new PostClass { StatusId = 250L, IsRead = false });
569
570             this.tabinfo.DistributePosts();
571             this.tabinfo.SubmitUpdate();
572
573             // この時点での各タブの未読件数
574             Assert.Equal(3, tab1.UnreadCount);
575             Assert.Equal(3, tab2.UnreadCount);
576
577             // ... ここまで長い前置き
578
579             // StatusId: 200 を既読にする (search1, search2 両方に含まれる)
580             this.tabinfo.SetReadAllTab(200L, read: true);
581             Assert.Equal(2, tab1.UnreadCount);
582             Assert.Equal(2, tab2.UnreadCount);
583
584             // StatusId: 100 を既読にする (search1 のみに含まれる)
585             this.tabinfo.SetReadAllTab(100L, read: true);
586             Assert.Equal(1, tab1.UnreadCount);
587             Assert.Equal(2, tab2.UnreadCount);
588         }
589
590         [Fact]
591         public void SetReadAllTab_MarkAsUnreadTest()
592         {
593             var tab1 = new PublicSearchTabModel("search1");
594             var tab2 = new PublicSearchTabModel("search2");
595
596             this.tabinfo.AddTab(tab1);
597             this.tabinfo.AddTab(tab2);
598
599             // search1 に追加するツイート (StatusId: 100, 150, 200; すべて既読)
600             tab1.UnreadManage = true;
601             tab1.AddPostQueue(new PostClass { StatusId = 100L, IsRead = true });
602             tab1.AddPostQueue(new PostClass { StatusId = 150L, IsRead = true });
603             tab1.AddPostQueue(new PostClass { StatusId = 200L, IsRead = true });
604
605             // search2 に追加するツイート (StatusId: 150, 200, 250; すべて既読)
606             tab2.UnreadManage = true;
607             tab2.AddPostQueue(new PostClass { StatusId = 150L, IsRead = true });
608             tab2.AddPostQueue(new PostClass { StatusId = 200L, IsRead = true });
609             tab2.AddPostQueue(new PostClass { StatusId = 250L, IsRead = true });
610
611             this.tabinfo.DistributePosts();
612             this.tabinfo.SubmitUpdate();
613
614             // この時点での各タブの未読件数
615             Assert.Equal(0, tab1.UnreadCount);
616             Assert.Equal(0, tab2.UnreadCount);
617
618             // ... ここまで長い前置き
619
620             // StatusId: 200 を未読にする (search1, search2 両方に含まれる)
621             this.tabinfo.SetReadAllTab(200L, read: false);
622             Assert.Equal(1, tab1.UnreadCount);
623             Assert.Equal(1, tab2.UnreadCount);
624
625             // StatusId: 100 を未読にする (search1 のみに含まれる)
626             this.tabinfo.SetReadAllTab(100L, read: false);
627             Assert.Equal(2, tab1.UnreadCount);
628             Assert.Equal(1, tab2.UnreadCount);
629         }
630
631         [Fact]
632         public void SetReadHomeTab_Test()
633         {
634             var homeTab = this.tabinfo.Tabs["Recent"];
635
636             // Recent に追加するツイート (StatusId: 100, 150, 200; すべて未読)
637             homeTab.UnreadManage = true;
638             this.tabinfo.AddPost(new PostClass { StatusId = 100L, IsRead = false });
639             this.tabinfo.AddPost(new PostClass { StatusId = 150L, IsRead = false });
640             this.tabinfo.AddPost(new PostClass { StatusId = 200L, IsRead = false });
641
642             this.tabinfo.DistributePosts();
643             this.tabinfo.SubmitUpdate();
644
645             // この時点でのHomeタブの未読件数
646             Assert.Equal(3, homeTab.UnreadCount);
647
648             // Recent タブのツイートをすべて未読にする
649             this.tabinfo.SetReadHomeTab();
650             Assert.Equal(0, homeTab.UnreadCount);
651         }
652
653         [Fact]
654         public void SetReadHomeTab_ContainsReplyTest()
655         {
656             var homeTab = this.tabinfo.Tabs["Recent"];
657
658             // Recent に追加するツイート (StatusId: 100, 150, 200; すべて未読)
659             // StatusId: 150 は未読だがリプライ属性が付いている
660             homeTab.UnreadManage = true;
661             this.tabinfo.AddPost(new PostClass { StatusId = 100L, IsRead = false });
662             this.tabinfo.AddPost(new PostClass { StatusId = 150L, IsRead = false, IsReply = true });
663             this.tabinfo.AddPost(new PostClass { StatusId = 200L, IsRead = false });
664
665             this.tabinfo.DistributePosts();
666             this.tabinfo.SubmitUpdate();
667
668             // この時点でのHomeタブの未読件数
669             Assert.Equal(3, homeTab.UnreadCount);
670
671             // Recent タブのツイートをすべて未読にする
672             this.tabinfo.SetReadHomeTab();
673
674             // リプライである StatusId: 150 を除いてすべて未読になっている
675             Assert.Equal(1, homeTab.UnreadCount);
676             Assert.Equal(150L, homeTab.NextUnreadId);
677         }
678
679         [Fact]
680         public void SetReadHomeTab_ContainsFilterHitTest()
681         {
682             var homeTab = this.tabinfo.Tabs["Recent"];
683
684             // Recent に追加するツイート (StatusId: 100, 150, 200; すべて未読)
685             homeTab.UnreadManage = true;
686             this.tabinfo.AddPost(new PostClass { StatusId = 100L, IsRead = false });
687             this.tabinfo.AddPost(new PostClass { StatusId = 150L, IsRead = false });
688             this.tabinfo.AddPost(new PostClass { StatusId = 200L, IsRead = false });
689
690             // StatusId: 150 だけ FilterTab の振り分けルールにヒットする (PostClass.FilterHit が true になる)
691             var filterTab = new FilterTabModel("FilterTab");
692             filterTab.AddFilter(TestPostFilterRule.Create(x =>
693                 x.StatusId == 150L ? MyCommon.HITRESULT.Copy : MyCommon.HITRESULT.None));
694             this.tabinfo.AddTab(filterTab);
695
696             this.tabinfo.DistributePosts();
697             this.tabinfo.SubmitUpdate();
698
699             // この時点でのHomeタブの未読件数
700             Assert.Equal(3, homeTab.UnreadCount);
701
702             // Recent タブのツイートをすべて未読にする
703             this.tabinfo.SetReadHomeTab();
704
705             // FilterHit が true である StatusId: 150 を除いてすべて未読になっている
706             Assert.Equal(1, homeTab.UnreadCount);
707             Assert.Equal(150L, homeTab.NextUnreadId);
708         }
709
710         [Fact]
711         public void SubmitUpdate_RemoveSubmit_Test()
712         {
713             var homeTab = this.tabinfo.HomeTab;
714
715             this.tabinfo.AddPost(new PostClass { StatusId = 100L });
716             this.tabinfo.DistributePosts();
717             this.tabinfo.SubmitUpdate();
718
719             Assert.Equal(1, homeTab.AllCount);
720
721             this.tabinfo.RemovePostFromAllTabs(100L, setIsDeleted: true);
722
723             // この時点ではまだ削除されない
724             Assert.Equal(1, homeTab.AllCount);
725
726             this.tabinfo.SubmitUpdate(out _, out _, out _, out var isDeletePost);
727
728             Assert.True(isDeletePost);
729             Assert.Equal(0, homeTab.AllCount);
730             Assert.False(this.tabinfo.Posts.ContainsKey(100L));
731         }
732
733         [Fact]
734         public void SubmitUpdate_RemoveSubmit_NotOrphaned_Test()
735         {
736             var homeTab = this.tabinfo.HomeTab;
737             var favTab = this.tabinfo.FavoriteTab;
738
739             this.tabinfo.AddPost(new PostClass { StatusId = 100L, IsFav = true });
740             this.tabinfo.DistributePosts();
741             this.tabinfo.SubmitUpdate();
742
743             Assert.Equal(1, homeTab.AllCount);
744             Assert.Equal(1, favTab.AllCount);
745
746             // favTab のみ発言を除去 (homeTab には残ったまま)
747             favTab.EnqueueRemovePost(100L, setIsDeleted: false);
748
749             // この時点ではまだ削除されない
750             Assert.Equal(1, homeTab.AllCount);
751             Assert.Equal(1, favTab.AllCount);
752
753             this.tabinfo.SubmitUpdate(out _, out _, out _, out var isDeletePost);
754
755             Assert.True(isDeletePost);
756             Assert.Equal(1, homeTab.AllCount);
757             Assert.Equal(0, favTab.AllCount);
758
759             // homeTab には発言が残っているので Posts からは削除されない
760             Assert.True(this.tabinfo.Posts.ContainsKey(100L));
761         }
762
763         [Fact]
764         public void SubmitUpdate_NotifyPriorityTest()
765         {
766             var homeTab = this.tabinfo.HomeTab;
767             homeTab.UnreadManage = true;
768             homeTab.Notify = true;
769             homeTab.SoundFile = "home.wav";
770
771             var replyTab = this.tabinfo.MentionTab;
772             replyTab.UnreadManage = true;
773             replyTab.Notify = true;
774             replyTab.SoundFile = "reply.wav";
775
776             var dmTab = this.tabinfo.DirectMessageTab;
777             dmTab.UnreadManage = true;
778             dmTab.Notify = true;
779             dmTab.SoundFile = "dm.wav";
780
781             // 通常ツイート
782             this.tabinfo.AddPost(new PostClass { StatusId = 100L, IsRead = false });
783
784             // リプライ
785             this.tabinfo.AddPost(new PostClass { StatusId = 200L, IsReply = true, IsRead = false });
786
787             // DM
788             dmTab.AddPostQueue(new PostClass { StatusId = 300L, IsDm = true, IsRead = false });
789
790             this.tabinfo.DistributePosts();
791
792             this.tabinfo.SubmitUpdate(out var soundFile, out var notifyPosts, out _, out _);
793
794             // DM が最も優先度が高いため DM の通知音が再生される
795             Assert.Equal("dm.wav", soundFile);
796
797             // 通知対象のツイートは 3 件
798             Assert.Equal(3, notifyPosts.Length);
799         }
800
801         [Fact]
802         public void SubmitUpdate_IgnoreEmptySoundPath_Test()
803         {
804             var homeTab = this.tabinfo.HomeTab;
805             homeTab.UnreadManage = true;
806             homeTab.Notify = true;
807             homeTab.SoundFile = "home.wav";
808
809             var replyTab = this.tabinfo.MentionTab;
810             replyTab.UnreadManage = true;
811             replyTab.Notify = true;
812             replyTab.SoundFile = "";
813
814             // 通常ツイート
815             this.tabinfo.AddPost(new PostClass { StatusId = 100L, IsRead = false });
816
817             // リプライ
818             this.tabinfo.AddPost(new PostClass { StatusId = 200L, IsReply = true, IsRead = false });
819
820             this.tabinfo.DistributePosts();
821
822             this.tabinfo.SubmitUpdate(out var soundFile, out var notifyPosts, out _, out _);
823
824             // リプライの方が通知音の優先度が高いが、replyTab.SoundFile が空文字列なので次点の Recent の通知音を鳴らす
825             Assert.Equal("home.wav", soundFile);
826
827             // 通知対象のツイートは 2 件
828             Assert.Equal(2, notifyPosts.Length);
829         }
830
831         [Fact]
832         public void FilterAll_CopyFilterTest()
833         {
834             var homeTab = this.tabinfo.HomeTab;
835
836             var myTab1 = new FilterTabModel("MyTab1");
837             this.tabinfo.AddTab(myTab1);
838
839             var filter = new PostFilterRule
840             {
841                 FilterName = "aaa",
842
843                 // コピーのみ
844                 MoveMatches = false,
845                 MarkMatches = false,
846             };
847             myTab1.AddFilter(filter);
848             myTab1.FilterModified = false;
849
850             this.tabinfo.AddPost(new PostClass { StatusId = 100L, ScreenName = "aaa" });
851             this.tabinfo.AddPost(new PostClass { StatusId = 200L, ScreenName = "bbb" });
852             this.tabinfo.AddPost(new PostClass { StatusId = 300L, ScreenName = "ccc" });
853             this.tabinfo.DistributePosts();
854             this.tabinfo.SubmitUpdate();
855
856             // この時点での振り分け状態
857             Assert.Equal(new[] { 100L, 200L, 300L }, homeTab.StatusIds, AnyOrderComparer<long>.Instance);
858             Assert.Equal(new[] { 100L }, myTab1.StatusIds);
859
860             // フィルタを変更する
861             filter.FilterName = "bbb";
862
863             // フィルタの変更を反映
864             this.tabinfo.FilterAll();
865             this.tabinfo.DistributePosts();
866             this.tabinfo.SubmitUpdate();
867
868             // 期待する動作:
869             //   [statusId: 100] は MyTab1 から取り除かれる
870             //   [statusId: 200] は Recent から MyTab1 にコピーされる
871
872             // 変更後の振り分け状態
873             Assert.Equal(new[] { 100L, 200L, 300L }, homeTab.StatusIds, AnyOrderComparer<long>.Instance);
874             Assert.Equal(new[] { 200L }, myTab1.StatusIds);
875         }
876
877         [Fact]
878         public void FilterAll_CopyAndMarkFilterTest()
879         {
880             var homeTab = this.tabinfo.HomeTab;
881
882             var myTab1 = new FilterTabModel("MyTab1");
883             this.tabinfo.AddTab(myTab1);
884
885             var filter = new PostFilterRule
886             {
887                 FilterName = "aaa",
888
889                 // コピー+マーク
890                 MoveMatches = false,
891                 MarkMatches = true,
892             };
893             myTab1.AddFilter(filter);
894             myTab1.FilterModified = false;
895
896             this.tabinfo.AddPost(new PostClass { StatusId = 100L, ScreenName = "aaa" });
897             this.tabinfo.AddPost(new PostClass { StatusId = 200L, ScreenName = "bbb" });
898             this.tabinfo.AddPost(new PostClass { StatusId = 300L, ScreenName = "ccc" });
899             this.tabinfo.DistributePosts();
900             this.tabinfo.SubmitUpdate();
901
902             // この時点での振り分け状態
903             Assert.Equal(new[] { 100L, 200L, 300L }, homeTab.StatusIds, AnyOrderComparer<long>.Instance);
904             Assert.Equal(new[] { 100L }, myTab1.StatusIds);
905
906             // フィルタを変更する
907             filter.FilterName = "bbb";
908
909             // フィルタの変更を反映
910             this.tabinfo.FilterAll();
911             this.tabinfo.DistributePosts();
912             this.tabinfo.SubmitUpdate();
913
914             // 期待する動作:
915             //   [statusId: 100] は MyTab1 から取り除かれる
916             //   [statusId: 200] は Recent から MyTab1 にコピーされ、マークが付与される
917
918             // 変更後の振り分け状態
919             Assert.Equal(new[] { 100L, 200L, 300L }, homeTab.StatusIds, AnyOrderComparer<long>.Instance);
920             Assert.Equal(new[] { 200L }, myTab1.StatusIds);
921
922             // [statusId: 200] は IsMark が true の状態になる
923             Assert.True(this.tabinfo[200L]!.IsMark);
924         }
925
926         [Fact]
927         public void FilterAll_MoveFilterTest()
928         {
929             var homeTab = this.tabinfo.HomeTab;
930
931             var myTab1 = new FilterTabModel("MyTab1");
932             this.tabinfo.AddTab(myTab1);
933
934             var filter = new PostFilterRule
935             {
936                 FilterName = "aaa",
937
938                 // マッチしたら移動
939                 MoveMatches = true,
940             };
941             myTab1.AddFilter(filter);
942             myTab1.FilterModified = false;
943
944             this.tabinfo.AddPost(new PostClass { StatusId = 100L, ScreenName = "aaa" });
945             this.tabinfo.AddPost(new PostClass { StatusId = 200L, ScreenName = "bbb" });
946             this.tabinfo.AddPost(new PostClass { StatusId = 300L, ScreenName = "ccc" });
947             this.tabinfo.DistributePosts();
948             this.tabinfo.SubmitUpdate();
949
950             // この時点での振り分け状態
951             Assert.Equal(new[] { 200L, 300L }, homeTab.StatusIds, AnyOrderComparer<long>.Instance);
952             Assert.Equal(new[] { 100L }, myTab1.StatusIds);
953
954             // フィルタを変更する
955             filter.FilterName = "bbb";
956
957             // フィルタの変更を反映
958             this.tabinfo.FilterAll();
959             this.tabinfo.DistributePosts();
960             this.tabinfo.SubmitUpdate();
961
962             // 期待する動作:
963             //   [statusId: 100] は MyTab1 から取り除かれて Recent に戻される
964             //   [statusId: 200] は Recent から MyTab1 に移動される
965
966             // 変更後の振り分け状態
967             Assert.Equal(new[] { 100L, 300L }, homeTab.StatusIds, AnyOrderComparer<long>.Instance);
968             Assert.Equal(new[] { 200L }, myTab1.StatusIds);
969         }
970
971         [Fact]
972         public void FilterAll_MoveFilterTest2()
973         {
974             var homeTab = this.tabinfo.HomeTab;
975
976             var myTab1 = new FilterTabModel("MyTab1");
977             var myTab2 = new FilterTabModel("MyTab2");
978             this.tabinfo.AddTab(myTab1);
979             this.tabinfo.AddTab(myTab2);
980
981             var filter1 = new PostFilterRule
982             {
983                 FilterName = "aaa",
984
985                 // マッチしたら移動
986                 MoveMatches = true,
987             };
988             myTab1.AddFilter(filter1);
989             myTab1.FilterModified = false;
990
991             var filter2 = new PostFilterRule
992             {
993                 FilterName = "bbb",
994
995                 // マッチしたら移動
996                 MoveMatches = true,
997             };
998             myTab2.AddFilter(filter2);
999             myTab2.FilterModified = false;
1000
1001             this.tabinfo.AddPost(new PostClass { StatusId = 100L, ScreenName = "aaa" });
1002             this.tabinfo.AddPost(new PostClass { StatusId = 200L, ScreenName = "bbb" });
1003             this.tabinfo.AddPost(new PostClass { StatusId = 300L, ScreenName = "ccc" });
1004             this.tabinfo.DistributePosts();
1005             this.tabinfo.SubmitUpdate();
1006
1007             // この時点での振り分け状態
1008             Assert.Equal(new[] { 300L }, homeTab.StatusIds);
1009             Assert.Equal(new[] { 100L }, myTab1.StatusIds);
1010             Assert.Equal(new[] { 200L }, myTab2.StatusIds);
1011
1012             // MyTab1 のフィルタを変更する
1013             filter1.FilterName = "bbb";
1014
1015             // MyTab2 のフィルタを変更する
1016             filter2.FilterName = "ccc";
1017
1018             // フィルタの変更を反映
1019             this.tabinfo.FilterAll();
1020             this.tabinfo.DistributePosts();
1021             this.tabinfo.SubmitUpdate();
1022
1023             // 期待する動作:
1024             //   [statusId: 100] は MyTab1 から取り除かれて Recent に戻される
1025             //   [statusId: 200] は MyTab1 に移動される
1026             //   [statusId: 200] は MyTab2 から取り除かれるが MyTab1 に移動されているので Recent には戻さない
1027             //   [statusId: 300] は Recent から MyTab2 に移動される
1028
1029             // 変更後の振り分け状態
1030             Assert.Equal(new[] { 100L }, homeTab.StatusIds);
1031             Assert.Equal(new[] { 200L }, myTab1.StatusIds);
1032             Assert.Equal(new[] { 300L }, myTab2.StatusIds);
1033         }
1034
1035         [Fact]
1036         public void FilterAll_ExcludeReplyFilterTest()
1037         {
1038             var homeTab = this.tabinfo.HomeTab;
1039             var replyTab = this.tabinfo.MentionTab;
1040
1041             var filter = new PostFilterRule
1042             {
1043                 // @aaa からのリプライは Reply タブに振り分けない
1044                 ExFilterName = "aaa",
1045             };
1046             replyTab.AddFilter(filter);
1047             replyTab.FilterModified = false;
1048
1049             this.tabinfo.AddPost(new PostClass { StatusId = 100L, ScreenName = "aaa", IsReply = true });
1050             this.tabinfo.AddPost(new PostClass { StatusId = 200L, ScreenName = "bbb", IsReply = true });
1051             this.tabinfo.AddPost(new PostClass { StatusId = 300L, ScreenName = "ccc", IsReply = true });
1052             this.tabinfo.DistributePosts();
1053             this.tabinfo.SubmitUpdate();
1054
1055             // この時点での振り分け状態
1056             Assert.Equal(new[] { 100L, 200L, 300L }, homeTab.StatusIds, AnyOrderComparer<long>.Instance);
1057             Assert.Equal(new[] { 200L, 300L }, replyTab.StatusIds, AnyOrderComparer<long>.Instance);
1058
1059             // [statusId: 100] は IsExcludeReply が true の状態になっている
1060             Assert.True(this.tabinfo[100L]!.IsExcludeReply);
1061
1062             // Reply のフィルタを変更する
1063             filter.ExFilterName = "bbb";
1064
1065             // フィルタの変更を反映
1066             this.tabinfo.FilterAll();
1067             this.tabinfo.DistributePosts();
1068             this.tabinfo.SubmitUpdate();
1069
1070             // 期待する動作:
1071             //   [statusId: 100] は Reply にコピーされ、IsExcludeReply が false になる
1072             //   [statusId: 200] は Reply から取り除かれ、IsExcludeReply が true になる
1073
1074             // 変更後の振り分け状態
1075             Assert.Equal(new[] { 100L, 200L, 300L }, homeTab.StatusIds, AnyOrderComparer<long>.Instance);
1076             Assert.Equal(new[] { 100L, 300L }, replyTab.StatusIds, AnyOrderComparer<long>.Instance);
1077
1078             // [statusId: 100] は IsExcludeReply が false の状態になる
1079             Assert.False(this.tabinfo[100L]!.IsExcludeReply);
1080
1081             // [statusId: 200] は IsExcludeReply が true の状態になる
1082             Assert.True(this.tabinfo[200L]!.IsExcludeReply);
1083         }
1084
1085         [Fact]
1086         public void RefreshOwl_HomeTabTest()
1087         {
1088             var post = new PostClass
1089             {
1090                 StatusId = 100L,
1091                 ScreenName = "aaa",
1092                 UserId = 123L,
1093                 IsOwl = true,
1094             };
1095             this.tabinfo.AddPost(post);
1096             this.tabinfo.DistributePosts();
1097             this.tabinfo.SubmitUpdate();
1098
1099             var followerIds = new HashSet<long> { 123L };
1100             this.tabinfo.RefreshOwl(followerIds);
1101
1102             Assert.False(post.IsOwl);
1103         }
1104
1105         [Fact]
1106         public void RefreshOwl_InnerStoregeTabTest()
1107         {
1108             var tab = new PublicSearchTabModel("search");
1109             this.tabinfo.AddTab(tab);
1110
1111             var post = new PostClass
1112             {
1113                 StatusId = 100L,
1114                 ScreenName = "aaa",
1115                 UserId = 123L,
1116                 IsOwl = true,
1117             };
1118             tab.AddPostQueue(post);
1119             this.tabinfo.DistributePosts();
1120             this.tabinfo.SubmitUpdate();
1121
1122             var followerIds = new HashSet<long> { 123L };
1123             this.tabinfo.RefreshOwl(followerIds);
1124
1125             Assert.False(post.IsOwl);
1126         }
1127
1128         [Fact]
1129         public void RefreshOwl_UnfollowedTest()
1130         {
1131             var post = new PostClass
1132             {
1133                 StatusId = 100L,
1134                 ScreenName = "aaa",
1135                 UserId = 123L,
1136                 IsOwl = false,
1137             };
1138             this.tabinfo.AddPost(post);
1139             this.tabinfo.DistributePosts();
1140             this.tabinfo.SubmitUpdate();
1141
1142             var followerIds = new HashSet<long> { 456L };
1143             this.tabinfo.RefreshOwl(followerIds);
1144
1145             Assert.True(post.IsOwl);
1146         }
1147
1148         private class TestPostFilterRule : PostFilterRule
1149         {
1150             public static PostFilterRule Create(Func<PostClass, MyCommon.HITRESULT> filterDelegate)
1151             {
1152                 return new TestPostFilterRule
1153                 {
1154                     filterDelegate = filterDelegate,
1155                     IsDirty = false,
1156                 };
1157             }
1158         }
1159     }
1160 }