using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Paca.Browser;
using Paca.Browser.NewTab;
using Paca.App.Browser.Features;
using Paca.Tests.Infrastructure;
using Xunit;
namespace Paca.Tests.Browser;
///
/// 크롬 스타일 홈 (새 탭 / NTP) 기능 단위 및 통합 테스트:
/// 1. NtpModels & NtpStore — 바로가기 CRUD, 순서 정렬, 방문기록 기반 자주 찾는 사이트 추출, 맞춤설정(테마/배경/위젯) 영속화
/// 2. NewTabPageBuilder — 로고, 검색창(검색엔진 연동), 바로가기 그리드, "+ 바로가기 추가" 모달, 맞춤설정 서랍, 시계/카드, WebMessage 양방향 브릿지
/// 3. XSS 방어 및 안전한 HTML 생성 검증
/// 4. 크롬 새 탭 URL 식별 (paca://newtab, about:newtab, chrome://newtab)
///
public class ChromeNewTabPageTests : IDisposable
{
private readonly string _testDataDir;
public ChromeNewTabPageTests()
{
_testDataDir = Path.Combine(Path.GetTempPath(), "PacaNtpTest_" + Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(_testDataDir);
NtpStore.CustomDataDir = _testDataDir;
}
public void Dispose()
{
NtpStore.CustomDataDir = null;
try
{
if (Directory.Exists(_testDataDir))
Directory.Delete(_testDataDir, recursive: true);
}
catch { }
}
[Fact]
public void 저장된_바로가기가_없으면_기본_프리셋_바로가기_목록을_반환한다()
{
var shortcuts = NtpStore.LoadShortcuts();
Assert.NotNull(shortcuts);
Assert.NotEmpty(shortcuts);
Assert.Contains(shortcuts, s => s.Title.Contains("Google") || s.Url.Contains("google.com"));
Assert.Contains(shortcuts, s => s.Title.Contains("YouTube") || s.Url.Contains("youtube.com"));
Assert.Contains(shortcuts, s => s.Title.Contains("Naver") || s.Url.Contains("naver.com"));
Assert.Contains(shortcuts, s => s.Title.Contains("GitHub") || s.Url.Contains("github.com"));
}
[Fact]
public void 바로가기_추가_수정_삭제_기능이_정상_동작하고_JSON_파일에_저장된다()
{
// 1. 추가
var added = NtpStore.AddShortcut("테스트 사이트", "https://example.org/test");
Assert.NotNull(added);
Assert.Equal("테스트 사이트", added.Title);
Assert.Equal("https://example.org/test", added.Url);
Assert.False(string.IsNullOrWhiteSpace(added.Id));
var loaded = NtpStore.LoadShortcuts();
Assert.Contains(loaded, s => s.Id == added.Id && s.Title == "테스트 사이트");
// 2. 수정
var updated = NtpStore.EditShortcut(added.Id, "수정된 사이트", "https://updated.org");
Assert.True(updated);
var loadedAfterEdit = NtpStore.LoadShortcuts();
var editedItem = loadedAfterEdit.First(s => s.Id == added.Id);
Assert.Equal("수정된 사이트", editedItem.Title);
Assert.Equal("https://updated.org", editedItem.Url);
// 3. 삭제
var deleted = NtpStore.RemoveShortcut(added.Id);
Assert.True(deleted);
var loadedAfterDelete = NtpStore.LoadShortcuts();
Assert.DoesNotContain(loadedAfterDelete, s => s.Id == added.Id);
}
[Fact]
public void 바로가기_URL에_프로토콜이_없으면_https가_자동_부착된다()
{
var added = NtpStore.AddShortcut("깃허브", "github.com/myrepo");
Assert.StartsWith("https://", added.Url);
Assert.Equal("https://github.com/myrepo", added.Url);
}
[Fact]
public void 바로가기_순서_재정렬이_정상_반영된다()
{
var shortcuts = NtpStore.LoadShortcuts();
Assert.True(shortcuts.Count >= 3);
var id0 = shortcuts[0].Id;
var id1 = shortcuts[1].Id;
var id2 = shortcuts[2].Id;
// 순서 변경: id2, id0, id1
var newOrder = new List { id2, id0, id1 };
var success = NtpStore.ReorderShortcuts(newOrder);
Assert.True(success);
var reordered = NtpStore.LoadShortcuts();
Assert.Equal(id2, reordered[0].Id);
Assert.Equal(id0, reordered[1].Id);
Assert.Equal(id1, reordered[2].Id);
}
[Fact]
public void 맞춤설정_테마_배경_위젯_설정_저장_및_로드가_정상_동작한다()
{
var defaultSettings = NtpStore.LoadSettings();
Assert.NotNull(defaultSettings);
Assert.Equal("custom", defaultSettings.ShortcutsMode);
var customSettings = new NtpSettings
{
ShortcutsMode = "frequent",
ThemeColor = "ocean",
Wallpaper = "aurora",
CustomWallpaperUrl = "https://images.unsplash.com/photo-test",
ShowRecentCards = false,
ShowDownloadsCard = true,
ShowClock = true,
SearchEngineName = "Naver"
};
NtpStore.SaveSettings(customSettings);
var reloaded = NtpStore.LoadSettings();
Assert.Equal("frequent", reloaded.ShortcutsMode);
Assert.Equal("ocean", reloaded.ThemeColor);
Assert.Equal("aurora", reloaded.Wallpaper);
Assert.Equal("https://images.unsplash.com/photo-test", reloaded.CustomWallpaperUrl);
Assert.False(reloaded.ShowRecentCards);
Assert.True(reloaded.ShowDownloadsCard);
Assert.True(reloaded.ShowClock);
Assert.Equal("Naver", reloaded.SearchEngineName);
}
[Fact]
public void 방문_기록으로부터_자주_방문한_사이트_목록을_도메인별_빈도_기준으로_추출한다()
{
var history = new List
{
new("유튜브 메인", "https://youtube.com", DateTime.Now.AddHours(-1)),
new("유튜브 영상1", "https://youtube.com/watch?v=1", DateTime.Now.AddHours(-2)),
new("유튜브 영상2", "https://youtube.com/watch?v=2", DateTime.Now.AddHours(-3)),
new("네이버 메인", "https://naver.com", DateTime.Now.AddHours(-4)),
new("네이버 뉴스", "https://news.naver.com/article", DateTime.Now.AddHours(-5)),
new("구글 검색", "https://google.com/search?q=test", DateTime.Now.AddHours(-6)),
new("내부 설정", "about:blank", DateTime.Now.AddHours(-7)),
new("무효 URL", "javascript:void(0)", DateTime.Now.AddHours(-8)),
};
var topSites = NtpStore.GetTopSitesFromHistory(history, max: 4);
Assert.NotEmpty(topSites);
Assert.True(topSites.Count <= 4);
// 빈도 1위: youtube.com (3회)
Assert.Contains("youtube.com", topSites[0].Url);
// 빈도 2위: naver.com (2회)
Assert.Contains("naver.com", topSites[1].Url);
// 무효 URL이나 about:blank 는 제외됨
Assert.DoesNotContain(topSites, s => s.Url.StartsWith("about:") || s.Url.StartsWith("javascript:"));
}
[Fact]
public void NewTabPageBuilder는_크롬_홈화면_핵심_요소들을_모두_포함한_HTML을_생성한다()
{
var shortcuts = new List
{
new("1", "구글", "https://google.com", IsCustom: true),
new("2", "유튜브", "https://youtube.com", IsCustom: true)
};
var settings = new NtpSettings
{
ShortcutsMode = "custom",
ThemeColor = "default",
Wallpaper = "default",
ShowRecentCards = true,
ShowClock = true,
SearchEngineName = "Google"
};
var recentHistory = new List
{
new("최근 페이지 1", "https://example.com/1", DateTime.Now.AddMinutes(-10))
};
var recentDownloads = new List
{
new("video.mp4", "https://sample.com/video.mp4", 1024 * 1024 * 50, true, DateTime.Now.AddMinutes(-5))
};
var html = NewTabPageBuilder.BuildNtp(
shortcuts,
settings,
recentHistory,
recentDownloads,
isDark: true,
accentColor: "#6c5ce7",
fontStack: "Malgun Gothic, sans-serif");
Assert.NotNull(html);
Assert.NotEmpty(html);
// 1. 크롬 로고 / 타이틀 브랜드
Assert.Contains("google-logo", html);
Assert.Contains("Google", html);
// 2. 옴니박스 검색창 (검색엔진 플레이스홀더 및 입력창)
Assert.Contains("id=\"search-input\"", html);
Assert.Contains("Google에서 검색하거나 URL을 입력하세요", html);
// 3. 바로가기 그리드 & 바로가기 추가 버튼
Assert.Contains("class=\"shortcut-tile\"", html);
Assert.Contains("구글", html);
Assert.Contains("유튜브", html);
Assert.Contains("add-shortcut-btn", html);
Assert.Contains("바로가기 추가", html);
// 4. 바로가기 추가/수정 모달 다이얼로그
Assert.Contains("id=\"shortcut-modal\"", html);
Assert.Contains("id=\"modal-title-input\"", html);
Assert.Contains("id=\"modal-url-input\"", html);
// 5. 맞춤설정 (Customize Chrome) 플로팅 버튼 & 슬라이드 서랍
Assert.Contains("id=\"customize-btn\"", html);
Assert.Contains("맞춤설정", html);
Assert.Contains("id=\"customize-drawer\"", html);
Assert.Contains("배경", html);
Assert.Contains("테마 색상", html);
// 6. 시계 및 인사말 위젯
Assert.Contains("id=\"clock-widget\"", html);
Assert.Contains("id=\"clock-time\"", html);
// 7. 최근 활동 카드 (방문 기록 & 다운로드)
Assert.Contains("id=\"recent-cards\"", html);
Assert.Contains("최근 방문한 페이지", html);
Assert.Contains("최근 다운로드", html);
Assert.Contains("video.mp4", html);
// 8. WebView2 WebMessage 브릿지 스크립트
Assert.Contains("window.chrome.webview.postMessage", html);
Assert.Contains("ntp:search", html);
Assert.Contains("ntp:navigate", html);
Assert.Contains("ntp:addShortcut", html);
Assert.Contains("ntp:editShortcut", html);
Assert.Contains("ntp:removeShortcut", html);
Assert.Contains("ntp:saveSettings", html);
}
[Fact]
public void NewTabPageBuilder는_사용자_입력_특수문자_XSS를_이스케이프_한다()
{
var xssShortcuts = new List
{
new("xss1", "", "https://test.com/\"'onmouseover='alert(1)")
};
var settings = new NtpSettings();
var html = NewTabPageBuilder.BuildNtp(xssShortcuts, settings, isDark: true);
// 스크립트 태그가 그대로 실행 코드로 노출되지 않아야 함
Assert.DoesNotContain("", html);
Assert.Contains("<script>alert('xss')</script>", html);
}
[Theory]
[InlineData("paca://newtab", true)]
[InlineData("about:newtab", true)]
[InlineData("chrome://newtab", true)]
[InlineData("paca://home", true)]
[InlineData("https://www.google.com", false)]
[InlineData("https://example.com", false)]
public void IsNewTabUrl은_크롬_및_파카_새탭_스킴을_정확히_판정한다(string url, bool expected)
{
var isNewTab = NewTabPageBuilder.IsNewTabUrl(url);
Assert.Equal(expected, isNewTab);
}
[Fact]
public void MainWindow_초기화_시_새탭_웹메시지_수신_이벤트가_바인딩되어_있다()
{
var cs = SourceTree.MainWindowCs;
Assert.Contains("_views.NtpMessageReceived +=", cs);
Assert.Contains("HandleNtpMessage", cs);
}
}