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 tabinfo.AddDefaultTabs();
64 using var tweenMain = new TweenMain(settings, tabinfo, accounts, imageCache, iconAssets, thumbnailGenerator);
65 var context = new TestContext(settings, tabinfo);
67 func(tweenMain, context);
71 public void Initialize_Test()
72 => this.UsingTweenMain((_, _) => { });
75 public void AddNewTab_FilterTabTest()
77 this.UsingTweenMain((tweenMain, context) =>
79 Assert.Equal(4, tweenMain.ListTab.TabPages.Count);
81 var tab = new FilterTabModel("hoge");
82 context.TabInfo.AddTab(tab);
83 tweenMain.AddNewTab(tab, startup: false);
85 Assert.Equal(5, tweenMain.ListTab.TabPages.Count);
87 var tabPage = tweenMain.ListTab.TabPages[4];
88 Assert.Equal("hoge", tabPage.Text);
89 Assert.Single(tabPage.Controls);
90 Assert.IsType<DetailsListView>(tabPage.Controls[0]);
95 public void AddNewTab_UserTimelineTabTest()
97 this.UsingTweenMain((tweenMain, context) =>
99 Assert.Equal(4, tweenMain.ListTab.TabPages.Count);
101 var tab = new UserTimelineTabModel("hoge", "twitterapi");
102 context.TabInfo.AddTab(tab);
103 tweenMain.AddNewTab(tab, startup: false);
105 Assert.Equal(5, tweenMain.ListTab.TabPages.Count);
107 var tabPage = tweenMain.ListTab.TabPages[4];
108 Assert.Equal("hoge", tabPage.Text);
109 Assert.Equal(2, tabPage.Controls.Count);
110 Assert.IsType<DetailsListView>(tabPage.Controls[0]);
112 var label = Assert.IsType<Label>(tabPage.Controls[1]);
113 Assert.Equal("twitterapi's Timeline", label.Text);
118 public void AddNewTab_ListTimelineTabTest()
120 this.UsingTweenMain((tweenMain, context) =>
122 Assert.Equal(4, tweenMain.ListTab.TabPages.Count);
124 var list = new ListElement
128 Username = "opentween",
131 var tab = new ListTimelineTabModel("hoge", list);
132 context.TabInfo.AddTab(tab);
133 tweenMain.AddNewTab(tab, startup: false);
135 Assert.Equal(5, tweenMain.ListTab.TabPages.Count);
137 var tabPage = tweenMain.ListTab.TabPages[4];
138 Assert.Equal("hoge", tabPage.Text);
139 Assert.Equal(2, tabPage.Controls.Count);
140 Assert.IsType<DetailsListView>(tabPage.Controls[0]);
142 var label = Assert.IsType<Label>(tabPage.Controls[1]);
143 Assert.Equal("@opentween/tetete [Protected]", label.Text);
148 public void AddNewTab_PublicSearchTabTest()
150 this.UsingTweenMain((tweenMain, context) =>
152 Assert.Equal(4, tweenMain.ListTab.TabPages.Count);
154 var tab = new PublicSearchTabModel("hoge")
156 SearchWords = "#OpenTween",
159 context.TabInfo.AddTab(tab);
160 tweenMain.AddNewTab(tab, startup: false);
162 Assert.Equal(5, tweenMain.ListTab.TabPages.Count);
164 var tabPage = tweenMain.ListTab.TabPages[4];
165 Assert.Equal("hoge", tabPage.Text);
166 Assert.Equal(2, tabPage.Controls.Count);
167 Assert.IsType<DetailsListView>(tabPage.Controls[0]);
169 var panel = Assert.IsType<Panel>(tabPage.Controls[1]);
170 Assert.Equal(4, panel.Controls.Count);
172 var comboSearchWord = Assert.IsType<ComboBox>(panel.Controls[0]);
173 Assert.Equal("#OpenTween", comboSearchWord.Text);
175 var comboSearchLang = Assert.IsType<ComboBox>(panel.Controls[1]);
176 Assert.Equal("ja", comboSearchLang.Text);
178 Assert.IsType<Button>(panel.Controls[2]);
179 Assert.IsType<Label>(panel.Controls[3]);
184 public void RemoveSpecifiedTab_Test()
186 this.UsingTweenMain((tweenMain, context) =>
188 Assert.Equal(4, tweenMain.ListTab.TabPages.Count);
190 var tab = new PublicSearchTabModel("hoge")
192 SearchWords = "#OpenTween",
195 context.TabInfo.AddTab(tab);
196 tweenMain.AddNewTab(tab, startup: false);
197 Assert.Equal(5, tweenMain.ListTab.TabPages.Count);
199 var tabPage = tweenMain.ListTab.TabPages[4];
200 var listView = (DetailsListView)tabPage.Controls[0];
201 var searchPanel = (Panel)tabPage.Controls[1];
202 Assert.Equal("hoge", tabPage.Text);
204 tweenMain.RemoveSpecifiedTab("hoge", confirm: false);
206 Assert.Equal(4, tweenMain.ListTab.TabPages.Count);
207 Assert.False(context.TabInfo.ContainsTab("hoge"));
208 Assert.True(tabPage.IsDisposed);
209 Assert.True(listView.IsDisposed);
210 Assert.True(searchPanel.IsDisposed);
215 public void RefreshTimeline_Test()
217 this.UsingTweenMain((tweenMain, context) =>
219 var tabPage = tweenMain.ListTab.TabPages[0];
220 Assert.Equal("Recent", tabPage.Text);
222 var listView = (DetailsListView)tabPage.Controls[0];
223 Assert.Equal(0, listView.VirtualListSize);
225 var post = new PostClass
227 StatusId = new TwitterStatusId("100"),
230 ScreenName = "opentween",
231 CreatedAt = new(2024, 1, 1, 0, 0, 0),
233 context.TabInfo.AddPost(post);
234 context.TabInfo.DistributePosts();
235 tweenMain.RefreshTimeline();
237 Assert.Equal(1, listView.VirtualListSize);
239 var listItem = listView.Items[0];
240 Assert.Equal("opentween", listItem.SubItems[4].Text);
245 public void FormatStatusText_NewLineTest()
247 this.UsingTweenMain((tweenMain, _) =>
249 Assert.Equal("aaa\nbbb", tweenMain.FormatStatusText("aaa\r\nbbb"));
254 public void FormatStatusText_NewLineInDMTest()
256 this.UsingTweenMain((tweenMain, _) =>
259 Assert.Equal("D opentween aaa\nbbb", tweenMain.FormatStatusText("D opentween aaa\r\nbbb"));
264 public void FormatStatusText_SeparateUrlAndFullwidthCharacter_EnabledTest()
266 this.UsingTweenMain((tweenMain, context) =>
268 tweenMain.SeparateUrlAndFullwidthCharacter = true;
269 Assert.Equal("https://example.com/ あああ", tweenMain.FormatStatusText("https://example.com/あああ"));
274 public void FormatStatusText_SeparateUrlAndFullwidthCharacter_DisabledTest()
276 this.UsingTweenMain((tweenMain, context) =>
278 tweenMain.SeparateUrlAndFullwidthCharacter = false;
279 Assert.Equal("https://example.com/あああ", tweenMain.FormatStatusText("https://example.com/あああ"));
284 public void FormatStatusText_ReplaceFullwidthSpace_EnabledTest()
286 this.UsingTweenMain((tweenMain, context) =>
288 context.Settings.Common.WideSpaceConvert = true;
289 Assert.Equal("aaa bbb", tweenMain.FormatStatusText("aaa bbb"));
294 public void FormatStatusText_ReplaceFullwidthSpaceInDM_EnabledTest()
296 this.UsingTweenMain((tweenMain, context) =>
298 context.Settings.Common.WideSpaceConvert = true;
301 Assert.Equal("D opentween aaa bbb", tweenMain.FormatStatusText("D opentween aaa bbb"));
306 public void FormatStatusText_ReplaceFullwidthSpace_DisabledTest()
308 this.UsingTweenMain((tweenMain, context) =>
310 context.Settings.Common.WideSpaceConvert = false;
311 Assert.Equal("aaa bbb", tweenMain.FormatStatusText("aaa bbb"));
316 public void FormatStatusText_UseRecommendedFooter_Test()
318 this.UsingTweenMain((tweenMain, context) =>
320 context.Settings.Local.UseRecommendStatus = true;
321 Assert.Matches(new Regex(@"^aaa \[TWNv\d+\]$"), tweenMain.FormatStatusText("aaa"));
326 public void FormatStatusText_CustomFooterText_Test()
328 this.UsingTweenMain((tweenMain, context) =>
330 context.Settings.Local.StatusText = "foo";
331 Assert.Equal("aaa foo", tweenMain.FormatStatusText("aaa"));
336 public void FormatStatusText_DisableFooterIfSendingDM_Test()
338 this.UsingTweenMain((tweenMain, context) =>
340 context.Settings.Local.StatusText = "foo";
343 Assert.Equal("D opentween aaa", tweenMain.FormatStatusText("D opentween aaa"));
348 public void FormatStatusText_DisableFooterIfContainsUnofficialRT_Test()
350 this.UsingTweenMain((tweenMain, context) =>
352 context.Settings.Local.StatusText = "foo";
354 // 非公式 RT を含む場合はフッターを無効化する
355 Assert.Equal("aaa RT @foo: bbb", tweenMain.FormatStatusText("aaa RT @foo: bbb"));
360 public void FormatStatusText_DisableFooterIfPostByEnterAndPressedShiftKey_Test()
362 this.UsingTweenMain((tweenMain, context) =>
364 context.Settings.Common.PostCtrlEnter = false;
365 context.Settings.Common.PostShiftEnter = false; // Enter で投稿する設定
366 context.Settings.Local.StatusText = "foo";
367 context.Settings.Local.StatusMultiline = false; // 単一行モード
369 // Shift キーが押されている場合はフッターを無効化する
370 Assert.Equal("aaa", tweenMain.FormatStatusText("aaa", modifierKeys: Keys.Shift));
375 public void FormatStatusText_DisableFooterIfPostByEnterAndPressedCtrlKeyAndMultilineMode_Test()
377 this.UsingTweenMain((tweenMain, context) =>
379 context.Settings.Common.PostCtrlEnter = false;
380 context.Settings.Common.PostShiftEnter = false; // Enter で投稿する設定
381 context.Settings.Local.StatusText = "foo";
382 context.Settings.Local.StatusMultiline = true; // 複数行モード
384 // Ctrl キーが押されている場合はフッターを無効化する
385 Assert.Equal("aaa", tweenMain.FormatStatusText("aaa", modifierKeys: Keys.Control));
390 public void FormatStatusText_DisableFooterIfPostByShiftEnterAndPressedControlKey_Test()
392 this.UsingTweenMain((tweenMain, context) =>
394 context.Settings.Common.PostCtrlEnter = false;
395 context.Settings.Common.PostShiftEnter = true; // Shift+Enter で投稿する設定
396 context.Settings.Local.StatusText = "foo";
398 // Ctrl キーが押されている場合はフッターを無効化する
399 Assert.Equal("aaa", tweenMain.FormatStatusText("aaa", modifierKeys: Keys.Control));
404 public void FormatStatusText_EnableFooterIfPostByShiftEnter_Test()
406 this.UsingTweenMain((tweenMain, context) =>
408 context.Settings.Common.PostCtrlEnter = false;
409 context.Settings.Common.PostShiftEnter = true; // Shift+Enter で投稿する設定
410 context.Settings.Local.StatusText = "foo";
412 // Shift+Enter で投稿する場合、Ctrl キーが押されていなければフッターを付ける
413 Assert.Equal("aaa foo", tweenMain.FormatStatusText("aaa", modifierKeys: Keys.Shift));
418 public void FormatStatusText_DisableFooterIfPostByCtrlEnterAndPressedShiftKey_Test()
420 this.UsingTweenMain((tweenMain, context) =>
422 context.Settings.Common.PostCtrlEnter = true; // Ctrl+Enter で投稿する設定
423 context.Settings.Common.PostShiftEnter = false;
424 context.Settings.Local.StatusText = "foo";
426 // Shift キーが押されている場合はフッターを無効化する
427 Assert.Equal("aaa", tweenMain.FormatStatusText("aaa", modifierKeys: Keys.Shift));
432 public void FormatStatusText_EnableFooterIfPostByCtrlEnter_Test()
434 this.UsingTweenMain((tweenMain, context) =>
436 context.Settings.Common.PostCtrlEnter = true; // Ctrl+Enter で投稿する設定
437 context.Settings.Common.PostShiftEnter = false;
438 context.Settings.Local.StatusText = "foo";
440 // Ctrl+Enter で投稿する場合、Shift キーが押されていなければフッターを付ける
441 Assert.Equal("aaa foo", tweenMain.FormatStatusText("aaa", modifierKeys: Keys.Control));
446 public void FormatStatusText_PreventSmsCommand_Test()
448 this.UsingTweenMain((tweenMain, context) =>
450 // 「D+」などから始まる文字列をツイートしようとすると SMS コマンドと誤認されてエラーが返される問題の回避策
451 Assert.Equal("\u200bd+aaaa", tweenMain.FormatStatusText("d+aaaa"));
456 public void GetUrlFromDataObject_XMozUrlTest()
458 var dataBytes = Encoding.Unicode.GetBytes("https://twitter.com/\nTwitter\0");
459 using var memstream = new MemoryStream(dataBytes);
460 var data = new DataObject("text/x-moz-url", memstream);
462 var expected = ("https://twitter.com/", "Twitter");
463 Assert.Equal(expected, TweenMain.GetUrlFromDataObject(data));
467 public void GetUrlFromDataObject_IESiteModeToUrlTest()
469 var dataBytes = Encoding.Unicode.GetBytes("https://twitter.com/\0Twitter\0");
470 using var memstream = new MemoryStream(dataBytes);
471 var data = new DataObject("IESiteModeToUrl", memstream);
473 var expected = ("https://twitter.com/", "Twitter");
474 Assert.Equal(expected, TweenMain.GetUrlFromDataObject(data));
478 public void GetUrlFromDataObject_UniformResourceLocatorWTest()
480 var dataBytes = Encoding.Unicode.GetBytes("https://twitter.com/\0");
481 using var memstream = new MemoryStream(dataBytes);
482 var data = new DataObject("UniformResourceLocatorW", memstream);
484 var expected = ("https://twitter.com/", (string?)null);
485 Assert.Equal(expected, TweenMain.GetUrlFromDataObject(data));
489 public void GetUrlFromDataObject_UnknownFormatTest()
491 using var memstream = new MemoryStream(Array.Empty<byte>());
492 var data = new DataObject("application/x-hogehoge", memstream);
494 Assert.Throws<NotSupportedException>(() => TweenMain.GetUrlFromDataObject(data));
498 public void CreateRetweetUnofficial_UrlTest()
500 var statusText = """<a href="http://t.co/KYi7vMZzRt" title="http://twitter.com/">twitter.com</a>""";
502 Assert.Equal("http://twitter.com/", TweenMain.CreateRetweetUnofficial(statusText, false));
506 public void CreateRetweetUnofficial_MentionTest()
508 var statusText = """<a class="mention" href="https://twitter.com/twitterapi">@TwitterAPI</a>""";
510 Assert.Equal("@TwitterAPI", TweenMain.CreateRetweetUnofficial(statusText, false));
514 public void CreateRetweetUnofficial_HashtagTest()
516 var statusText = """<a class="hashtag" href="https://twitter.com/search?q=%23OpenTween">#OpenTween</a>""";
518 Assert.Equal("#OpenTween", TweenMain.CreateRetweetUnofficial(statusText, false));
522 public void CreateRetweetUnofficial_SingleLineTest()
524 var statusText = "123<br>456<br>789";
526 Assert.Equal("123 456 789", TweenMain.CreateRetweetUnofficial(statusText, false));
530 public void CreateRetweetUnofficial_MultiLineTest()
532 var statusText = "123<br>456<br>789";
534 Assert.Equal("123" + Environment.NewLine + "456" + Environment.NewLine + "789", TweenMain.CreateRetweetUnofficial(statusText, true));
538 public void CreateRetweetUnofficial_DecodeTest()
540 var statusText = "<>"' ";
542 Assert.Equal("<>\"' ", TweenMain.CreateRetweetUnofficial(statusText, false));
546 public void CreateRetweetUnofficial_WithFormatterTest()
548 // TweetFormatterでHTMLに整形 → CreateRetweetUnofficialで復元 までの動作が正しく行えているか
550 var text = "#てすと @TwitterAPI \n http://t.co/KYi7vMZzRt";
551 var entities = new TwitterEntity[]
553 new TwitterEntityHashtag
555 Indices = new[] { 0, 4 },
558 new TwitterEntityMention
560 Indices = new[] { 5, 16 },
562 Name = "Twitter API",
563 ScreenName = "twitterapi",
567 Indices = new[] { 19, 41 },
568 DisplayUrl = "twitter.com",
569 ExpandedUrl = "http://twitter.com/",
570 Url = "http://t.co/KYi7vMZzRt",
574 var html = TweetFormatter.AutoLinkHtml(text, entities);
576 var expected = "#てすと @TwitterAPI " + Environment.NewLine + " http://twitter.com/";
577 Assert.Equal(expected, TweenMain.CreateRetweetUnofficial(html, true));
581 [InlineData("", true)]
582 [InlineData("hoge", false)]
583 [InlineData("@twitterapi ", true)]
584 [InlineData("@twitterapi @opentween ", true)]
585 [InlineData("@twitterapi @opentween hoge", false)]
586 public void TextContainsOnlyMentions_Test(string input, bool expected)
587 => Assert.Equal(expected, TweenMain.TextContainsOnlyMentions(input));