228 lines
7.2 KiB
C#
228 lines
7.2 KiB
C#
using VideoDownloader.Browser.Tabs;
|
|
|
|
namespace VideoDownloader.Tests.Browser;
|
|
|
|
// Phase A 탭 아키텍처: TabManager 순수 로직(뷰 팩토리 주입 — WebView2 없이 검증).
|
|
public class TabManagerTests
|
|
{
|
|
private static TabManager NewManager() => new(() => new TabItem());
|
|
|
|
[Fact]
|
|
public void NewTab은_탭을_추가하고_활성화한다()
|
|
{
|
|
var m = NewManager();
|
|
var t = m.NewTab("https://a.com");
|
|
Assert.Single(m.Tabs);
|
|
Assert.Same(t, m.ActiveTab);
|
|
Assert.Equal("https://a.com", t.Url);
|
|
}
|
|
|
|
[Fact]
|
|
public void 두_번째_탭은_이전_탭을_비활성화하고_활성화된다()
|
|
{
|
|
var m = NewManager();
|
|
var t1 = m.NewTab("https://a.com");
|
|
var t2 = m.NewTab("https://b.com");
|
|
Assert.Same(t2, m.ActiveTab);
|
|
Assert.False(t1.IsActive);
|
|
Assert.True(t2.IsActive);
|
|
}
|
|
|
|
[Fact]
|
|
public void Activate는_활성_탭을_바꾸고_이벤트를_발생시킨다()
|
|
{
|
|
var m = NewManager();
|
|
var t1 = m.NewTab();
|
|
var t2 = m.NewTab();
|
|
var fired = 0;
|
|
m.ActiveChanged += _ => fired++;
|
|
m.Activate(t1);
|
|
Assert.Same(t1, m.ActiveTab);
|
|
Assert.True(t1.IsActive);
|
|
Assert.False(t2.IsActive);
|
|
Assert.Equal(1, fired);
|
|
}
|
|
|
|
[Fact]
|
|
public void CloseTab은_제거하고_이웃_탭을_활성화한다()
|
|
{
|
|
var m = NewManager();
|
|
m.NewTab("https://a.com"); // index 0
|
|
var t2 = m.NewTab("https://b.com"); // index 1 (active)
|
|
var t3 = m.NewTab("https://c.com"); // index 2
|
|
m.Activate(t2);
|
|
|
|
m.CloseTab(t2);
|
|
Assert.Equal(2, m.Tabs.Count);
|
|
Assert.DoesNotContain(t2, m.Tabs);
|
|
// 닫힌 탭 오른쪽이 있으면 그 탭(t3) 활성화 (Chrome 동작)
|
|
Assert.Same(t3, m.ActiveTab);
|
|
}
|
|
|
|
[Fact]
|
|
public void 마지막_탭을_닫으면_새_빈_탭으로_대체된다()
|
|
{
|
|
var m = NewManager();
|
|
var only = m.NewTab("https://solo.com");
|
|
m.CloseTab(only);
|
|
Assert.Single(m.Tabs);
|
|
Assert.NotSame(only, m.ActiveTab);
|
|
Assert.Same(m.ActiveTab, m.Tabs[0]);
|
|
}
|
|
|
|
[Fact]
|
|
public void 닫힌_탭은_스택에_쌓이고_ReopenClosedTab으로_원래_위치에_복원된다()
|
|
{
|
|
var m = NewManager();
|
|
var t1 = m.NewTab("https://a.com");
|
|
m.NewTab("https://b.com");
|
|
m.NewTab("https://c.com"); // index 2, active
|
|
m.CloseTab(m.Tabs[2]); // 마지막 인덱스 탭 닫기
|
|
|
|
var reopened = m.ReopenClosedTab();
|
|
Assert.NotNull(reopened);
|
|
Assert.Equal("https://c.com", reopened!.Url);
|
|
Assert.Equal(3, m.Tabs.Count);
|
|
Assert.Equal(2, m.Tabs.IndexOf(reopened)); // 원래 인덱스 복원
|
|
Assert.Same(reopened, m.ActiveTab);
|
|
Assert.Empty(m.ClosedStack);
|
|
}
|
|
|
|
[Fact]
|
|
public void 닫힌_탭_스택은_최대_25개다()
|
|
{
|
|
var m = NewManager();
|
|
for (int i = 0; i < 30; i++)
|
|
{
|
|
var t = m.NewTab($"https://x{i}.com");
|
|
m.CloseTab(t);
|
|
}
|
|
Assert.Equal(25, m.ClosedStack.Count);
|
|
Assert.Equal("https://x29.com", m.ClosedStack.First().Url); // LIFO 최신 우선
|
|
}
|
|
|
|
[Fact]
|
|
public void ActivateNext_Previous는_순환한다()
|
|
{
|
|
var m = NewManager();
|
|
var t1 = m.NewTab("https://a.com");
|
|
var t2 = m.NewTab("https://b.com");
|
|
m.Activate(t1);
|
|
|
|
m.ActivateNext();
|
|
Assert.Same(t2, m.ActiveTab);
|
|
m.ActivateNext(); // 순환
|
|
Assert.Same(t1, m.ActiveTab);
|
|
m.ActivatePrevious();
|
|
Assert.Same(t2, m.ActiveTab);
|
|
}
|
|
|
|
[Fact]
|
|
public void ActivateAt와_ActivateLast가_동작한다()
|
|
{
|
|
var m = NewManager();
|
|
m.NewTab("a"); m.NewTab("b"); var last = m.NewTab("c");
|
|
m.ActivateAt(0);
|
|
Assert.Same(m.Tabs[0], m.ActiveTab);
|
|
m.ActivateLast();
|
|
Assert.Same(last, m.ActiveTab);
|
|
}
|
|
|
|
[Fact]
|
|
public void Move는_탭_순서를_바꾼다()
|
|
{
|
|
var m = NewManager();
|
|
var t1 = m.NewTab("a"); var t2 = m.NewTab("b"); var t3 = m.NewTab("c");
|
|
m.Move(t1, 2); // a 를 끝으로
|
|
Assert.Equal(2, m.Tabs.IndexOf(t1));
|
|
Assert.Equal(0, m.Tabs.IndexOf(t2));
|
|
Assert.Equal(1, m.Tabs.IndexOf(t3));
|
|
}
|
|
|
|
[Fact]
|
|
public void Pin은_탭을_좌측_끝으로_이동시키고_핀_표시한다()
|
|
{
|
|
var m = NewManager();
|
|
m.NewTab("a"); m.NewTab("b");
|
|
var t3 = m.NewTab("c");
|
|
m.SetPinned(t3, true);
|
|
Assert.True(t3.IsPinned);
|
|
Assert.Equal(0, m.Tabs.IndexOf(t3)); // 핀 탭은 좌측 정렬
|
|
m.SetPinned(t3, false);
|
|
Assert.False(t3.IsPinned);
|
|
}
|
|
|
|
[Fact]
|
|
public void CaptureSession은_URL과_활성_인덱스와_핀을_담는다()
|
|
{
|
|
var m = NewManager();
|
|
m.NewTab("https://a.com");
|
|
var t2 = m.NewTab("https://b.com");
|
|
m.NewTab("https://c.com");
|
|
m.SetPinned(t2, true); // b 가 좌측으로 이동 → 순서: b,a,c
|
|
m.Activate(t2);
|
|
|
|
var s = m.CaptureSession();
|
|
Assert.Equal(new[] { "https://b.com", "https://a.com", "https://c.com" }, s.Urls);
|
|
Assert.Equal(0, s.ActiveIndex);
|
|
Assert.Equal(new[] { true, false, false }, s.Pinned);
|
|
}
|
|
|
|
[Fact]
|
|
public void RestoreSession은_저장된_탭들을_재생성한다()
|
|
{
|
|
var m = NewManager();
|
|
// CaptureSession 불변식: 핀 탭이 먼저 오는 순서로 저장됨
|
|
var snap = new SessionSnapshot(
|
|
new() { "https://b.com", "https://a.com" }, 0, new() { true, false });
|
|
m.RestoreSession(snap);
|
|
Assert.Equal(2, m.Tabs.Count);
|
|
Assert.Equal("https://b.com", m.ActiveTab!.Url);
|
|
Assert.True(m.Tabs[0].IsPinned); // 핀 탭이 좌측
|
|
Assert.False(m.Tabs[1].IsPinned);
|
|
}
|
|
|
|
[Fact]
|
|
public void RestoreSession은_기존_탭을_치환한다()
|
|
{
|
|
var m = NewManager();
|
|
m.NewTab("https://old.com");
|
|
m.RestoreSession(new SessionSnapshot(new() { "https://new.com" }, 0, new() { false }));
|
|
Assert.Single(m.Tabs);
|
|
Assert.Equal("https://new.com", m.Tabs[0].Url);
|
|
}
|
|
}
|
|
|
|
// 세션 JSON 저장소(T1-10) 분리 테스트
|
|
public class TabSessionStoreTests : IDisposable
|
|
{
|
|
private readonly string _path = Path.Combine(Path.GetTempPath(), $"session_{Guid.NewGuid():N}.json");
|
|
|
|
[Fact]
|
|
public void 저장_후_로드_라운드트립()
|
|
{
|
|
var snap = new SessionSnapshot(
|
|
new() { "https://a.com", "https://b.com", "https://c.com" }, 2, new() { true, false, false });
|
|
TabSessionStore.Save(_path, snap);
|
|
var loaded = TabSessionStore.Load(_path);
|
|
Assert.NotNull(loaded);
|
|
Assert.Equal(snap.Urls, loaded!.Urls);
|
|
Assert.Equal(2, loaded.ActiveIndex);
|
|
Assert.Equal(snap.Pinned, loaded.Pinned);
|
|
}
|
|
|
|
[Fact]
|
|
public void 파일_없으면_null()
|
|
{
|
|
Assert.Null(TabSessionStore.Load(_path));
|
|
}
|
|
|
|
[Fact]
|
|
public void 손상된_JSON이면_null()
|
|
{
|
|
File.WriteAllText(_path, "{ broken");
|
|
Assert.Null(TabSessionStore.Load(_path));
|
|
}
|
|
|
|
public void Dispose() { try { File.Delete(_path); } catch { } }
|
|
}
|