1
0
Fork 0

feat(ui): implement 1-click presets, real-time search & filter bar, batch queue actions, and file inspector

This commit is contained in:
Yun Chan 2026-09-03 17:12:14 +09:00
parent 41c8a8a94b
commit b67b3f1b61
17 changed files with 1240 additions and 38 deletions

View file

@ -0,0 +1,90 @@
using System.Collections.ObjectModel;
using System.IO;
using Everything2Everything.App.Views;
using Xunit;
namespace Everything2Everything.Tests;
/// <summary>
/// 큐 일괄 처리(Batch Actions) TDD 단위 테스트 스위트
/// 전체 선택/해제, 선택 항목 삭제, 완료 항목 정리, 일괄 출력 형식 변경 검증
/// </summary>
public class BatchQueueActionsTests
{
private static QueueItem CreateItem(string path, string outExt = ".jpg") =>
new()
{
SourcePath = path,
FileName = Path.GetFileName(path),
SelectedOutputExtension = outExt,
StateText = "queued",
};
[Fact]
public void BatchChangeOutput_UpdatesEligibleItems()
{
var items = new ObservableCollection<QueueItem>
{
CreateItem("a.png", ".jpg"),
CreateItem("b.png", ".jpg"),
CreateItem("c.mp4", ".mp4"),
};
// .webp 지원 여부에 따라 이미지(a.png, b.png)만 .webp로 일괄 변경
BatchQueueService.BatchChangeOutput(items, ".webp", new[] { ".png", ".jpg" });
Assert.Equal(".webp", items[0].SelectedOutputExtension);
Assert.Equal(".webp", items[1].SelectedOutputExtension);
Assert.Equal(".mp4", items[2].SelectedOutputExtension); // mp4는 제외
}
[Fact]
public void RemoveSelected_RemovesOnlySelectedItems()
{
var items = new ObservableCollection<QueueItem>
{
CreateItem("1.png"),
CreateItem("2.png"),
CreateItem("3.png"),
};
items[0].IsSelected = true;
items[2].IsSelected = true;
BatchQueueService.RemoveSelected(items);
Assert.Single(items);
Assert.Equal("2.png", items[0].FileName);
}
[Fact]
public void ClearCompleted_RemovesOnlyDoneItems()
{
var i1 = CreateItem("1.png"); i1.SetState("done");
var i2 = CreateItem("2.png"); i2.SetState("45%");
var i3 = CreateItem("3.png"); i3.SetState("queued");
var i4 = CreateItem("4.png"); i4.SetState("done");
var items = new ObservableCollection<QueueItem> { i1, i2, i3, i4 };
BatchQueueService.ClearCompleted(items);
Assert.Equal(2, items.Count);
Assert.All(items, item => Assert.False(item.IsDone));
}
[Fact]
public void SelectAll_TogglesAllItems()
{
var items = new ObservableCollection<QueueItem>
{
CreateItem("1.png"),
CreateItem("2.png"),
};
BatchQueueService.SetSelectionAll(items, true);
Assert.All(items, item => Assert.True(item.IsSelected));
BatchQueueService.SetSelectionAll(items, false);
Assert.All(items, item => Assert.False(item.IsSelected));
}
}

View file

@ -227,4 +227,36 @@ public class DesignAuditAstTests
Assert.True(paddings.Count == 1,
$"SettingsWindow 푸터 버튼들의 패딩이 서로 다릅니다 (동일 위계 버튼은 패딩 통일 필수): {string.Join(", ", paddings)}");
}
[Fact]
public void MainWindow_MustHave_Presets_Search_Batch_And_Inspector_Elements()
{
var mainFile = Path.Combine(ViewsDir, "MainWindow.xaml");
var doc = XDocument.Parse(File.ReadAllText(mainFile));
// 1. Quick Presets (PresetCommand 바인딩 버튼 존재)
var presetButtons = doc.Descendants()
.Where(e => (e.Name.LocalName == "Button" || e.Name.LocalName == "ToggleButton") &&
e.Attribute("Command")?.Value.Contains("PresetCommand") == true)
.ToList();
Assert.True(presetButtons.Count >= 4, "빠른 최적화 프리셋 버튼 4개(웹/고화질/문서/모바일)가 MainWindow.xaml에 선언되어야 합니다.");
// 2. SearchBox (검색창 존재)
var searchBox = doc.Descendants()
.FirstOrDefault(e => e.Name.LocalName == "TextBox" &&
e.Attribute(XName.Get("Name", "http://schemas.microsoft.com/winfx/2006/xaml"))?.Value == "SearchBox");
Assert.NotNull(searchBox);
// 3. BatchActionBar (일괄 작업 툴바 존재)
var batchBar = doc.Descendants()
.FirstOrDefault(e => e.Attribute(XName.Get("Name", "http://schemas.microsoft.com/winfx/2006/xaml"))?.Value == "BatchActionBar");
Assert.NotNull(batchBar);
// 4. Right Inspector Open Button (PreviewOpenFileCommand)
var openButtons = doc.Descendants()
.Where(e => e.Name.LocalName == "Button" &&
e.Attribute("Command")?.Value.Contains("PreviewOpenFileCommand") == true)
.ToList();
Assert.NotEmpty(openButtons);
}
}

View file

@ -0,0 +1,63 @@
using System.IO;
using Everything2Everything.Core.Filters;
using Everything2Everything.Core.Inspector;
using Xunit;
namespace Everything2Everything.Tests;
/// <summary>
/// 파일 인스펙터(Inspector & Metadata Card) TDD 단위 테스트 스위트
/// 선택된 파일의 세부 메타데이터, 카테고리, 변환 가능 대상 형식 카운트, 탐색기 연동 정보 검증
/// </summary>
public class FileInspectorTests
{
[Fact]
public void Build_ValidTextFile_ExtractsMetadataAndTargetFormats()
{
var tempFile = Path.GetTempFileName() + ".txt";
File.WriteAllText(tempFile, "Hello World Everything2Everything Test");
try
{
var info = FileInspectorBuilder.Build(tempFile);
Assert.Equal(Path.GetFileName(tempFile), info.FileName);
Assert.Equal(tempFile, info.FullPath);
Assert.Equal(FilterCategory.Document, info.Category);
Assert.True(info.FileSizeBytes > 0);
Assert.NotEmpty(info.FormattedSize);
Assert.True(info.CanOpen);
Assert.True(info.CanReveal);
}
finally
{
if (File.Exists(tempFile)) File.Delete(tempFile);
}
}
[Fact]
public void Build_NonExistentFile_ReturnsSafeDefaults()
{
var ghostFile = "C:\\path\\does_not_exist\\nonexistent.png";
var info = FileInspectorBuilder.Build(ghostFile);
Assert.Equal("nonexistent.png", info.FileName);
Assert.Equal(FilterCategory.Image, info.Category);
Assert.Equal(0, info.FileSizeBytes);
Assert.Equal("0 B", info.FormattedSize);
Assert.False(info.CanOpen);
Assert.False(info.CanReveal);
}
[Theory]
[InlineData(".png", FilterCategory.Image)]
[InlineData(".mp4", FilterCategory.Media)]
[InlineData(".docx", FilterCategory.Document)]
[InlineData(".json", FilterCategory.Data)]
public void Build_IdentifiesCategoryAccurately(string ext, FilterCategory expectedCategory)
{
var dummyPath = "C:\\test\\sample" + ext;
var info = FileInspectorBuilder.Build(dummyPath);
Assert.Equal(expectedCategory, info.Category);
}
}

View file

@ -0,0 +1,81 @@
using System;
using System.IO;
using System.Text.RegularExpressions;
using System.Xml.Linq;
using Xunit;
namespace Everything2Everything.Tests;
public class PackagingSignatureTests
{
private static string FindRepoRoot()
{
var current = AppDomain.CurrentDomain.BaseDirectory;
while (!string.IsNullOrEmpty(current))
{
if (File.Exists(Path.Combine(current, "Everything2Everything.slnx")) ||
File.Exists(Path.Combine(current, "AGENTS.md")))
{
return current;
}
var parent = Directory.GetParent(current);
if (parent == null) break;
current = parent.FullName;
}
return Path.GetFullPath(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "..", "..", "..", "..", ".."));
}
[Fact]
public void AppxManifest_Publisher_Matches_DevCert_Subject()
{
var repoRoot = FindRepoRoot();
var manifestPath = Path.Combine(repoRoot, "packaging", "Package.appxmanifest");
Assert.True(File.Exists(manifestPath), $"Package.appxmanifest must exist at {manifestPath}");
var doc = XDocument.Load(manifestPath);
var ns = doc.Root?.GetDefaultNamespace() ?? XNamespace.None;
var identity = doc.Root?.Element(ns + "Identity");
Assert.NotNull(identity);
var publisher = identity.Attribute("Publisher")?.Value;
Assert.Equal("CN=Everything2EverythingDev", publisher);
}
[Fact]
public void EnvExample_Contains_CodeSigning_Variables()
{
var repoRoot = FindRepoRoot();
var envExamplePath = Path.Combine(repoRoot, ".env.example");
Assert.True(File.Exists(envExamplePath), ".env.example template file must exist");
var content = File.ReadAllText(envExamplePath);
Assert.Contains("CODE_SIGN_PFX_PATH", content);
Assert.Contains("CODE_SIGN_PFX_PASSWORD", content);
Assert.Contains("CODE_SIGN_THUMBPRINT", content);
}
[Fact]
public void PublishReleaseScript_Enforces_Signing_Flag()
{
var repoRoot = FindRepoRoot();
var publishScript = Path.Combine(repoRoot, "tools", "Publish-Release.ps1");
Assert.True(File.Exists(publishScript), "Publish-Release.ps1 must exist");
var content = File.ReadAllText(publishScript);
// BuildMsix must be invoked with -Sign flag
Assert.Contains("BuildMsix.ps1", content);
Assert.Matches(@"(?i)-Sign\b", content);
}
[Fact]
public void InstallCmd_Exists_For_OneClick_Elevation()
{
var repoRoot = FindRepoRoot();
var installCmdPath = Path.Combine(repoRoot, "packaging", "Install.cmd");
Assert.True(File.Exists(installCmdPath), "packaging/Install.cmd must exist for 1-click end-user installation");
var content = File.ReadAllText(installCmdPath);
Assert.Contains("powershell", content, StringComparison.OrdinalIgnoreCase);
Assert.Contains("TrustedPeople", content, StringComparison.OrdinalIgnoreCase);
}
}

View file

@ -0,0 +1,71 @@
using Everything2Everything.App.ViewModels;
using Everything2Everything.Core.Presets;
using Xunit;
namespace Everything2Everything.Tests;
/// <summary>
/// 1-클릭 빠른 최적화 프리셋 시스템 TDD 단위 테스트 스위트
/// 웹 최적화, 무손실 보존, 문서 PDF 보관, 모바일 공유 프리셋의 파라미터 튜닝 및 추천 확장자 검증
/// </summary>
public class PresetEngineTests
{
[Fact]
public void Apply_WebOptimized_SetsWebPAndOptimalCompression()
{
var options = new OptionsViewModel();
var targetExt = ConversionPreset.Apply(PresetType.WebOptimized, options, ".png");
Assert.Equal(".webp", targetExt);
Assert.Equal(80, options.ImageQuality);
Assert.True(options.StripMetadata);
}
[Fact]
public void Apply_HighQualityLossless_SetsMaxQualityAndFlacOrPng()
{
var options = new OptionsViewModel();
var audioTarget = ConversionPreset.Apply(PresetType.HighQualityLossless, options, ".wav");
Assert.Equal(".flac", audioTarget);
Assert.Equal(320, options.AudioBitrateKbps);
var imgTarget = ConversionPreset.Apply(PresetType.HighQualityLossless, options, ".jpg");
Assert.Equal(".png", imgTarget);
Assert.Equal(100, options.ImageQuality);
Assert.False(options.StripMetadata);
}
[Fact]
public void Apply_DocumentPdf_SetsPdfTargetForDocuments()
{
var options = new OptionsViewModel();
var docxTarget = ConversionPreset.Apply(PresetType.DocumentPdf, options, ".docx");
Assert.Equal(".pdf", docxTarget);
var hwpTarget = ConversionPreset.Apply(PresetType.DocumentPdf, options, ".hwp");
Assert.Equal(".pdf", hwpTarget);
}
[Fact]
public void Apply_MobileShare_SetsLightweightVideoAndAudio()
{
var options = new OptionsViewModel();
var videoTarget = ConversionPreset.Apply(PresetType.MobileShare, options, ".mkv");
Assert.Equal(".mp4", videoTarget);
Assert.Equal(26, options.VideoCrf);
Assert.Equal(128, options.AudioBitrateKbps);
Assert.Equal("veryfast", options.VideoPreset);
}
[Theory]
[InlineData(PresetType.WebOptimized, "웹 최적화", "WebP · 80% 압축 · 메타데이터 제거")]
[InlineData(PresetType.HighQualityLossless, "초고화질 보존", "무손실 PNG/FLAC · 100% 품질")]
[InlineData(PresetType.DocumentPdf, "문서 PDF 보관", "표준 PDF/A 문서 변환")]
[InlineData(PresetType.MobileShare, "모바일 공유", "MP4 H.264 · 가벼운 용량 전송")]
public void Presets_HaveUserFacingTitleAndDescription(PresetType type, string expectedTitle, string expectedDesc)
{
var info = ConversionPreset.GetInfo(type);
Assert.Equal(expectedTitle, info.Title);
Assert.Equal(expectedDesc, info.Description);
}
}

View file

@ -0,0 +1,60 @@
using Everything2Everything.Core.Filters;
using Xunit;
namespace Everything2Everything.Tests;
/// <summary>
/// 실시간 큐 및 히스토리 검색 & 카테고리 필터 TDD 단위 테스트 스위트
/// 파일명/확장자 검색 및 멀티미디어/문서/이미지/데이터 카테고리 분류 로직 검증
/// </summary>
public class QueueFilterTests
{
[Theory]
[InlineData("photo.jpg", FilterCategory.Image)]
[InlineData("vector.svg", FilterCategory.Image)]
[InlineData("report.docx", FilterCategory.Document)]
[InlineData("hwp_doc.hwp", FilterCategory.Document)]
[InlineData("document.pdf", FilterCategory.Document)]
[InlineData("movie.mp4", FilterCategory.Media)]
[InlineData("podcast.mp3", FilterCategory.Media)]
[InlineData("audio.wav", FilterCategory.Media)]
[InlineData("dataset.csv", FilterCategory.Data)]
[InlineData("config.json", FilterCategory.Data)]
public void GetCategory_ClassifiesCorrectCategory(string filename, FilterCategory expected)
{
var category = QueueFilterMatcher.GetCategory(filename);
Assert.Equal(expected, category);
}
[Theory]
[InlineData("photo.jpg", "", FilterCategory.All, true)]
[InlineData("photo.jpg", "photo", FilterCategory.All, true)]
[InlineData("photo.jpg", "PHOTO", FilterCategory.All, true)]
[InlineData("photo.jpg", ".jpg", FilterCategory.All, true)]
[InlineData("photo.jpg", "video", FilterCategory.All, false)]
[InlineData("photo.jpg", "", FilterCategory.Image, true)]
[InlineData("photo.jpg", "", FilterCategory.Document, false)]
[InlineData("report.docx", "rep", FilterCategory.Document, true)]
[InlineData("report.docx", "rep", FilterCategory.Image, false)]
[InlineData("video.mp4", "vid", FilterCategory.Media, true)]
public void Matches_FiltersBySearchTextAndCategory(string filename, string query, FilterCategory category, bool expected)
{
var matches = QueueFilterMatcher.Matches(filename, query, category);
Assert.Equal(expected, matches);
}
[Fact]
public void FilterCollection_ReturnsMatchingItemsOnly()
{
var items = new[]
{
new Everything2Everything.App.Views.QueueItem { SourcePath = "C:\\a.png", FileName = "a.png" },
new Everything2Everything.App.Views.QueueItem { SourcePath = "C:\\b.docx", FileName = "b.docx" },
new Everything2Everything.App.Views.QueueItem { SourcePath = "C:\\c.mp4", FileName = "c.mp4" },
};
var filtered = QueueFilterMatcher.Filter(items, item => item.FileName, "", FilterCategory.Image).ToList();
Assert.Single(filtered);
Assert.Equal("a.png", filtered[0].FileName);
}
}