1 // OpenTween - Client of Twitter
2 // Copyright (c) 2014 kim_upsilon (@kim_upsilon) <https://upsilo.net/~upsilon/>
3 // All rights reserved.
5 // This file is part of OpenTween.
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)
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
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.
23 using System.Collections.Generic;
26 using System.Reflection;
28 using System.Text.RegularExpressions;
29 using System.Windows.Forms;
30 using OpenTween.Api.DataModel;
31 using OpenTween.Models;
32 using OpenTween.OpenTweenCustomControl;
33 using OpenTween.Setting;
34 using OpenTween.SocialProtocol;
35 using OpenTween.Thumbnail;
37 using Xunit.Extensions;
41 public class TweenMainTest
43 private record TestContext(
44 SettingManager Settings,
45 TabInformations TabInfo
48 private void UsingTweenMain(Action<TweenMain, TestContext> func)
50 var settings = new SettingManager("");
51 var tabinfo = new TabInformations();
52 using var accounts = new AccountCollection();
53 using var imageCache = new ImageCache();
54 using var iconAssets = new IconAssetsManager();
55 var thumbnailGenerator = new ThumbnailGenerator(new(autoupdate: false));
57 // TabInformation.GetInstance() で取得できるようにする
58 var field = typeof(TabInformations).GetField("Instance",
59 BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.SetField);
60 field.SetValue(null, tabinfo);
62 using var tweenMain = new TweenMain(settings, tabinfo, accounts, imageCache, iconAssets, thumbnailGenerator);
63 var context = new TestContext(settings, tabinfo);
65 func(tweenMain, context);
69 public void Initialize_Test()
70 => this.UsingTweenMain((_, _) => { });
73 public void AddNewTab_FilterTabTest()
75 this.UsingTweenMain((tweenMain, context) =>
77 Assert.Equal(4, tweenMain.ListTab.TabPages.Count);
79 var tab = new FilterTabModel("hoge");
80 context.TabInfo.AddTab(tab);
81 tweenMain.AddNewTab(tab, startup: false);
83 Assert.Equal(5, tweenMain.ListTab.TabPages.Count);
85 var tabPage = tweenMain.ListTab.TabPages[4];
86 Assert.Equal("hoge", tabPage.Text);
87 Assert.Single(tabPage.Controls);
88 Assert.IsType<DetailsListView>(tabPage.Controls[0]);
93 public void AddNewTab_UserTimelineTabTest()
95 this.UsingTweenMain((tweenMain, context) =>
97 Assert.Equal(4, tweenMain.ListTab.TabPages.Count);
99 var tab = new UserTimelineTabModel("hoge", "twitterapi");
100 context.TabInfo.AddTab(tab);
101 tweenMain.AddNewTab(tab, startup: false);
103 Assert.Equal(5, tweenMain.ListTab.TabPages.Count);
105 var tabPage = tweenMain.ListTab.TabPages[4];
106 Assert.Equal("hoge", tabPage.Text);
107 Assert.Equal(2, tabPage.Controls.Count);
108 Assert.IsType<DetailsListView>(tabPage.Controls[0]);
110 var label = Assert.IsType<Label>(tabPage.Controls[1]);
111 Assert.Equal("twitterapi's Timeline", label.Text);
116 public void AddNewTab_ListTimelineTabTest()
118 this.UsingTweenMain((tweenMain, context) =>
120 Assert.Equal(4, tweenMain.ListTab.TabPages.Count);
122 var list = new ListElement
126 Username = "opentween",
129 var tab = new ListTimelineTabModel("hoge", list);
130 context.TabInfo.AddTab(tab);
131 tweenMain.AddNewTab(tab, startup: false);
133 Assert.Equal(5, tweenMain.ListTab.TabPages.Count);
135 var tabPage = tweenMain.ListTab.TabPages[4];
136 Assert.Equal("hoge", tabPage.Text);
137 Assert.Equal(2, tabPage.Controls.Count);
138 Assert.IsType<DetailsListView>(tabPage.Controls[0]);
140 var label = Assert.IsType<Label>(tabPage.Controls[1]);
141 Assert.Equal("@opentween/tetete [Protected]", label.Text);
146 public void AddNewTab_PublicSearchTabTest()
148 this.UsingTweenMain((tweenMain, context) =>
150 Assert.Equal(4, tweenMain.ListTab.TabPages.Count);
152 var tab = new PublicSearchTabModel("hoge")
154 SearchWords = "#OpenTween",
157 context.TabInfo.AddTab(tab);
158 tweenMain.AddNewTab(tab, startup: false);
160 Assert.Equal(5, tweenMain.ListTab.TabPages.Count);
162 var tabPage = tweenMain.ListTab.TabPages[4];
163 Assert.Equal("hoge", tabPage.Text);
164 Assert.Equal(2, tabPage.Controls.Count);
165 Assert.IsType<DetailsListView>(tabPage.Controls[0]);
167 var panel = Assert.IsType<Panel>(tabPage.Controls[1]);
168 Assert.Equal(4, panel.Controls.Count);
170 var comboSearchWord = Assert.IsType<ComboBox>(panel.Controls[0]);
171 Assert.Equal("#OpenTween", comboSearchWord.Text);
173 var comboSearchLang = Assert.IsType<ComboBox>(panel.Controls[1]);
174 Assert.Equal("ja", comboSearchLang.Text);
176 Assert.IsType<Button>(panel.Controls[2]);
177 Assert.IsType<Label>(panel.Controls[3]);
182 public void RemoveSpecifiedTab_Test()
184 this.UsingTweenMain((tweenMain, context) =>
186 Assert.Equal(4, tweenMain.ListTab.TabPages.Count);
188 var tab = new PublicSearchTabModel("hoge")
190 SearchWords = "#OpenTween",
193 context.TabInfo.AddTab(tab);
194 tweenMain.AddNewTab(tab, startup: false);
195 Assert.Equal(5, tweenMain.ListTab.TabPages.Count);
197 var tabPage = tweenMain.ListTab.TabPages[4];
198 var listView = (DetailsListView)tabPage.Controls[0];
199 var searchPanel = (Panel)tabPage.Controls[1];
200 Assert.Equal("hoge", tabPage.Text);
202 tweenMain.RemoveSpecifiedTab("hoge", confirm: false);
204 Assert.Equal(4, tweenMain.ListTab.TabPages.Count);
205 Assert.False(context.TabInfo.ContainsTab("hoge"));
206 Assert.True(tabPage.IsDisposed);
207 Assert.True(listView.IsDisposed);
208 Assert.True(searchPanel.IsDisposed);
213 public void RefreshTimeline_Test()
215 this.UsingTweenMain((tweenMain, context) =>
217 var tabPage = tweenMain.ListTab.TabPages[0];
218 Assert.Equal("Recent", tabPage.Text);
220 var listView = (DetailsListView)tabPage.Controls[0];
221 Assert.Equal(0, listView.VirtualListSize);
223 var post = new PostClass
225 StatusId = new TwitterStatusId("100"),
228 ScreenName = "opentween",
229 CreatedAt = new(2024, 1, 1, 0, 0, 0),
231 context.TabInfo.AddPost(post);
232 context.TabInfo.DistributePosts();
233 tweenMain.RefreshTimeline();
235 Assert.Equal(1, listView.VirtualListSize);
237 var listItem = listView.Items[0];
238 Assert.Equal("opentween", listItem.SubItems[4].Text);
243 public void FormatStatusText_NewLineTest()
245 this.UsingTweenMain((tweenMain, _) =>
247 Assert.Equal("aaa\nbbb", tweenMain.FormatStatusText("aaa\r\nbbb"));
252 public void FormatStatusText_NewLineInDMTest()
254 this.UsingTweenMain((tweenMain, _) =>
257 Assert.Equal("D opentween aaa\nbbb", tweenMain.FormatStatusText("D opentween aaa\r\nbbb"));
262 public void FormatStatusText_SeparateUrlAndFullwidthCharacter_EnabledTest()
264 this.UsingTweenMain((tweenMain, context) =>
266 tweenMain.SeparateUrlAndFullwidthCharacter = true;
267 Assert.Equal("https://example.com/ あああ", tweenMain.FormatStatusText("https://example.com/あああ"));
272 public void FormatStatusText_SeparateUrlAndFullwidthCharacter_DisabledTest()
274 this.UsingTweenMain((tweenMain, context) =>
276 tweenMain.SeparateUrlAndFullwidthCharacter = false;
277 Assert.Equal("https://example.com/あああ", tweenMain.FormatStatusText("https://example.com/あああ"));
282 public void FormatStatusText_ReplaceFullwidthSpace_EnabledTest()
284 this.UsingTweenMain((tweenMain, context) =>
286 context.Settings.Common.WideSpaceConvert = true;
287 Assert.Equal("aaa bbb", tweenMain.FormatStatusText("aaa bbb"));
292 public void FormatStatusText_ReplaceFullwidthSpaceInDM_EnabledTest()
294 this.UsingTweenMain((tweenMain, context) =>
296 context.Settings.Common.WideSpaceConvert = true;
299 Assert.Equal("D opentween aaa bbb", tweenMain.FormatStatusText("D opentween aaa bbb"));
304 public void FormatStatusText_ReplaceFullwidthSpace_DisabledTest()
306 this.UsingTweenMain((tweenMain, context) =>
308 context.Settings.Common.WideSpaceConvert = false;
309 Assert.Equal("aaa bbb", tweenMain.FormatStatusText("aaa bbb"));
314 public void FormatStatusText_UseRecommendedFooter_Test()
316 this.UsingTweenMain((tweenMain, context) =>
318 context.Settings.Local.UseRecommendStatus = true;
319 Assert.Matches(new Regex(@"^aaa \[TWNv\d+\]$"), tweenMain.FormatStatusText("aaa"));
324 public void FormatStatusText_CustomFooterText_Test()
326 this.UsingTweenMain((tweenMain, context) =>
328 context.Settings.Local.StatusText = "foo";
329 Assert.Equal("aaa foo", tweenMain.FormatStatusText("aaa"));
334 public void FormatStatusText_DisableFooterIfSendingDM_Test()
336 this.UsingTweenMain((tweenMain, context) =>
338 context.Settings.Local.StatusText = "foo";
341 Assert.Equal("D opentween aaa", tweenMain.FormatStatusText("D opentween aaa"));
346 public void FormatStatusText_DisableFooterIfContainsUnofficialRT_Test()
348 this.UsingTweenMain((tweenMain, context) =>
350 context.Settings.Local.StatusText = "foo";
352 // 非公式 RT を含む場合はフッターを無効化する
353 Assert.Equal("aaa RT @foo: bbb", tweenMain.FormatStatusText("aaa RT @foo: bbb"));
358 public void FormatStatusText_DisableFooterIfPostByEnterAndPressedShiftKey_Test()
360 this.UsingTweenMain((tweenMain, context) =>
362 context.Settings.Common.PostCtrlEnter = false;
363 context.Settings.Common.PostShiftEnter = false; // Enter で投稿する設定
364 context.Settings.Local.StatusText = "foo";
365 context.Settings.Local.StatusMultiline = false; // 単一行モード
367 // Shift キーが押されている場合はフッターを無効化する
368 Assert.Equal("aaa", tweenMain.FormatStatusText("aaa", modifierKeys: Keys.Shift));
373 public void FormatStatusText_DisableFooterIfPostByEnterAndPressedCtrlKeyAndMultilineMode_Test()
375 this.UsingTweenMain((tweenMain, context) =>
377 context.Settings.Common.PostCtrlEnter = false;
378 context.Settings.Common.PostShiftEnter = false; // Enter で投稿する設定
379 context.Settings.Local.StatusText = "foo";
380 context.Settings.Local.StatusMultiline = true; // 複数行モード
382 // Ctrl キーが押されている場合はフッターを無効化する
383 Assert.Equal("aaa", tweenMain.FormatStatusText("aaa", modifierKeys: Keys.Control));
388 public void FormatStatusText_DisableFooterIfPostByShiftEnterAndPressedControlKey_Test()
390 this.UsingTweenMain((tweenMain, context) =>
392 context.Settings.Common.PostCtrlEnter = false;
393 context.Settings.Common.PostShiftEnter = true; // Shift+Enter で投稿する設定
394 context.Settings.Local.StatusText = "foo";
396 // Ctrl キーが押されている場合はフッターを無効化する
397 Assert.Equal("aaa", tweenMain.FormatStatusText("aaa", modifierKeys: Keys.Control));
402 public void FormatStatusText_EnableFooterIfPostByShiftEnter_Test()
404 this.UsingTweenMain((tweenMain, context) =>
406 context.Settings.Common.PostCtrlEnter = false;
407 context.Settings.Common.PostShiftEnter = true; // Shift+Enter で投稿する設定
408 context.Settings.Local.StatusText = "foo";
410 // Shift+Enter で投稿する場合、Ctrl キーが押されていなければフッターを付ける
411 Assert.Equal("aaa foo", tweenMain.FormatStatusText("aaa", modifierKeys: Keys.Shift));
416 public void FormatStatusText_DisableFooterIfPostByCtrlEnterAndPressedShiftKey_Test()
418 this.UsingTweenMain((tweenMain, context) =>
420 context.Settings.Common.PostCtrlEnter = true; // Ctrl+Enter で投稿する設定
421 context.Settings.Common.PostShiftEnter = false;
422 context.Settings.Local.StatusText = "foo";
424 // Shift キーが押されている場合はフッターを無効化する
425 Assert.Equal("aaa", tweenMain.FormatStatusText("aaa", modifierKeys: Keys.Shift));
430 public void FormatStatusText_EnableFooterIfPostByCtrlEnter_Test()
432 this.UsingTweenMain((tweenMain, context) =>
434 context.Settings.Common.PostCtrlEnter = true; // Ctrl+Enter で投稿する設定
435 context.Settings.Common.PostShiftEnter = false;
436 context.Settings.Local.StatusText = "foo";
438 // Ctrl+Enter で投稿する場合、Shift キーが押されていなければフッターを付ける
439 Assert.Equal("aaa foo", tweenMain.FormatStatusText("aaa", modifierKeys: Keys.Control));
444 public void FormatStatusText_PreventSmsCommand_Test()
446 this.UsingTweenMain((tweenMain, context) =>
448 // 「D+」などから始まる文字列をツイートしようとすると SMS コマンドと誤認されてエラーが返される問題の回避策
449 Assert.Equal("\u200bd+aaaa", tweenMain.FormatStatusText("d+aaaa"));
454 public void GetUrlFromDataObject_XMozUrlTest()
456 var dataBytes = Encoding.Unicode.GetBytes("https://twitter.com/\nTwitter\0");
457 using var memstream = new MemoryStream(dataBytes);
458 var data = new DataObject("text/x-moz-url", memstream);
460 var expected = ("https://twitter.com/", "Twitter");
461 Assert.Equal(expected, TweenMain.GetUrlFromDataObject(data));
465 public void GetUrlFromDataObject_IESiteModeToUrlTest()
467 var dataBytes = Encoding.Unicode.GetBytes("https://twitter.com/\0Twitter\0");
468 using var memstream = new MemoryStream(dataBytes);
469 var data = new DataObject("IESiteModeToUrl", memstream);
471 var expected = ("https://twitter.com/", "Twitter");
472 Assert.Equal(expected, TweenMain.GetUrlFromDataObject(data));
476 public void GetUrlFromDataObject_UniformResourceLocatorWTest()
478 var dataBytes = Encoding.Unicode.GetBytes("https://twitter.com/\0");
479 using var memstream = new MemoryStream(dataBytes);
480 var data = new DataObject("UniformResourceLocatorW", memstream);
482 var expected = ("https://twitter.com/", (string?)null);
483 Assert.Equal(expected, TweenMain.GetUrlFromDataObject(data));
487 public void GetUrlFromDataObject_UnknownFormatTest()
489 using var memstream = new MemoryStream(Array.Empty<byte>());
490 var data = new DataObject("application/x-hogehoge", memstream);
492 Assert.Throws<NotSupportedException>(() => TweenMain.GetUrlFromDataObject(data));
496 public void CreateRetweetUnofficial_UrlTest()
498 var statusText = """<a href="http://t.co/KYi7vMZzRt" title="http://twitter.com/">twitter.com</a>""";
500 Assert.Equal("http://twitter.com/", TweenMain.CreateRetweetUnofficial(statusText, false));
504 public void CreateRetweetUnofficial_MentionTest()
506 var statusText = """<a class="mention" href="https://twitter.com/twitterapi">@TwitterAPI</a>""";
508 Assert.Equal("@TwitterAPI", TweenMain.CreateRetweetUnofficial(statusText, false));
512 public void CreateRetweetUnofficial_HashtagTest()
514 var statusText = """<a class="hashtag" href="https://twitter.com/search?q=%23OpenTween">#OpenTween</a>""";
516 Assert.Equal("#OpenTween", TweenMain.CreateRetweetUnofficial(statusText, false));
520 public void CreateRetweetUnofficial_SingleLineTest()
522 var statusText = "123<br>456<br>789";
524 Assert.Equal("123 456 789", TweenMain.CreateRetweetUnofficial(statusText, false));
528 public void CreateRetweetUnofficial_MultiLineTest()
530 var statusText = "123<br>456<br>789";
532 Assert.Equal("123" + Environment.NewLine + "456" + Environment.NewLine + "789", TweenMain.CreateRetweetUnofficial(statusText, true));
536 public void CreateRetweetUnofficial_DecodeTest()
538 var statusText = "<>"' ";
540 Assert.Equal("<>\"' ", TweenMain.CreateRetweetUnofficial(statusText, false));
544 public void CreateRetweetUnofficial_WithFormatterTest()
546 // TweetFormatterでHTMLに整形 → CreateRetweetUnofficialで復元 までの動作が正しく行えているか
548 var text = "#てすと @TwitterAPI \n http://t.co/KYi7vMZzRt";
549 var entities = new TwitterEntity[]
551 new TwitterEntityHashtag
553 Indices = new[] { 0, 4 },
556 new TwitterEntityMention
558 Indices = new[] { 5, 16 },
560 Name = "Twitter API",
561 ScreenName = "twitterapi",
565 Indices = new[] { 19, 41 },
566 DisplayUrl = "twitter.com",
567 ExpandedUrl = "http://twitter.com/",
568 Url = "http://t.co/KYi7vMZzRt",
572 var html = TweetFormatter.AutoLinkHtml(text, entities);
574 var expected = "#てすと @TwitterAPI " + Environment.NewLine + " http://twitter.com/";
575 Assert.Equal(expected, TweenMain.CreateRetweetUnofficial(html, true));
579 [InlineData("", true)]
580 [InlineData("hoge", false)]
581 [InlineData("@twitterapi ", true)]
582 [InlineData("@twitterapi @opentween ", true)]
583 [InlineData("@twitterapi @opentween hoge", false)]
584 public void TextContainsOnlyMentions_Test(string input, bool expected)
585 => Assert.Equal(expected, TweenMain.TextContainsOnlyMentions(input));