feat(ui): implement 1-click presets, real-time search & filter bar, batch queue actions, and file inspector
This commit is contained in:
parent
41c8a8a94b
commit
b67b3f1b61
17 changed files with 1240 additions and 38 deletions
90
src/Everything2Everything.Tests/BatchQueueActionsTests.cs
Normal file
90
src/Everything2Everything.Tests/BatchQueueActionsTests.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
63
src/Everything2Everything.Tests/FileInspectorTests.cs
Normal file
63
src/Everything2Everything.Tests/FileInspectorTests.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
81
src/Everything2Everything.Tests/PackagingSignatureTests.cs
Normal file
81
src/Everything2Everything.Tests/PackagingSignatureTests.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
71
src/Everything2Everything.Tests/PresetEngineTests.cs
Normal file
71
src/Everything2Everything.Tests/PresetEngineTests.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
60
src/Everything2Everything.Tests/QueueFilterTests.cs
Normal file
60
src/Everything2Everything.Tests/QueueFilterTests.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue