1
0
Fork 0

feat(ui): default to active queue, scope search toolbar, add double-bezel inspector, optimize queue columns, and achieve 100% pure Korean UI

This commit is contained in:
Yun Chan 2026-09-05 12:25:30 +09:00
parent 455c8b1324
commit 4ca7352dc3
27 changed files with 2477 additions and 659 deletions

View file

@ -0,0 +1,53 @@
using System;
using System.Threading;
using System.Windows;
using System.Windows.Media.Imaging;
using Everything2Everything.App.Views;
using Xunit;
namespace Everything2Everything.Tests;
public class CategoryGlyphsTests
{
[Theory]
[InlineData(".jpg", 80)]
[InlineData(".mp4", 80)]
[InlineData(".pdf", 80)]
[InlineData(".csv", 80)]
public void ForExtension_ReturnsValidImageSource_WithProperPackUri(string ext, int minDimension)
{
RunOnSta(() =>
{
var img = CategoryGlyphs.ForExtension(ext) as BitmapImage;
Assert.NotNull(img);
Assert.Contains("Everything2Everything;component", img.UriSource.OriginalString);
Assert.True(img.PixelWidth >= minDimension, $"Glyph for {ext} should have width >= {minDimension}");
Assert.True(img.PixelHeight >= minDimension, $"Glyph for {ext} should have height >= {minDimension}");
});
}
private static void RunOnSta(Action action)
{
Exception? ex = null;
var thread = new Thread(() =>
{
try
{
if (Application.Current == null)
{
try { _ = new Application(); } catch { }
}
action();
}
catch (Exception e)
{
ex = e;
}
});
thread.SetApartmentState(ApartmentState.STA);
thread.Start();
thread.Join();
if (ex != null) throw new AggregateException("STA failure", ex);
}
}

View file

@ -419,6 +419,541 @@ public class DesignAuditAstTests
Assert.NotNull(cyanBgBrush);
Assert.Equal("#083344", cyanBgBrush.Attribute("Color")?.Value);
}
[Fact]
public void InteractiveElements_MustHave_AutomationPropertiesAutomationId_For_E2E_Reliability()
{
// UI Automation 기반 Headful E2E 테스트 및 접근성(A11y) 신뢰성을 위해 핵심 인터랙티브 요소는 AutomationId가 지정되어야 한다.
var mainFile = Path.Combine(ViewsDir, "MainWindow.xaml");
var doc = XDocument.Parse(File.ReadAllText(mainFile));
var requiredAutomationIds = new[]
{
"ProcessQueueButton",
"AdvancedOptionsExpander",
"OutputFormatCombo",
"ImageLosslessCheck",
"PdfCompressLevelCombo",
"PdfDpiCombo",
"QualitySlider",
"VideoCrfSlider",
"AudioBitrateQuickCombo",
"PdfCompressQuickCombo"
};
var foundAutomationIds = doc.Descendants()
.Select(e => e.Attribute(XName.Get("AutomationId", "clr-namespace:System.Windows.Automation;assembly=PresentationCore"))?.Value
?? e.Attribute("AutomationProperties.AutomationId")?.Value)
.Where(id => !string.IsNullOrEmpty(id))
.ToHashSet();
var missing = requiredAutomationIds.Where(req => !foundAutomationIds.Contains(req)).ToList();
Assert.True(missing.Count == 0,
$"핵심 인터랙티브 요소에 AutomationProperties.AutomationId가 누락되었습니다: {string.Join(", ", missing)}");
}
[Fact]
public void Sidebar_ConflictRuleSection_MustUsePureKoreanLabels()
{
// designpaca & AGENTS.md Rule 3:
// 영문 대문자 'FILE CONFLICT RULE' 및 영문 버튼 'Skip', 'Rename', 'Replace'를 금지하고
// 정제된 한국어 표준 '파일 충돌 해결', '건너뛰기', '이름 변경', '덮어쓰기'로 일관되게 제공해야 한다.
var mainFile = Path.Combine(ViewsDir, "MainWindow.xaml");
var doc = XDocument.Parse(File.ReadAllText(mainFile));
var allTexts = doc.Descendants().Where(e => e.Name.LocalName == "TextBlock")
.Select(t => t.Attribute("Text")?.Value ?? t.Value)
.ToList();
Assert.DoesNotContain(allTexts, t => t.Contains("FILE CONFLICT RULE", StringComparison.OrdinalIgnoreCase));
var skipBtn = doc.Descendants().FirstOrDefault(e => e.Attribute(XName.Get("Name", "http://schemas.microsoft.com/winfx/2006/xaml"))?.Value == "ConflictSkipBtn");
var renameBtn = doc.Descendants().FirstOrDefault(e => e.Attribute(XName.Get("Name", "http://schemas.microsoft.com/winfx/2006/xaml"))?.Value == "ConflictRenameBtn");
var replaceBtn = doc.Descendants().FirstOrDefault(e => e.Attribute(XName.Get("Name", "http://schemas.microsoft.com/winfx/2006/xaml"))?.Value == "ConflictReplaceBtn");
Assert.NotNull(skipBtn);
Assert.NotNull(renameBtn);
Assert.NotNull(replaceBtn);
Assert.Equal("건너뛰기", skipBtn.Attribute("Content")?.Value);
Assert.Equal("이름 변경", renameBtn.Attribute("Content")?.Value);
Assert.Equal("덮어쓰기", replaceBtn.Attribute("Content")?.Value);
}
[Fact]
public void FormatShiftTheme_MustDefine_IslandButtonStyle_And_DoubleBezelStyles()
{
// high-end-visual-design Section 4:
// Island Button (Button-in-Button) 스타일 및 Double-Bezel 카드 쉘 스타일이 선언되어야 한다.
var themeFile = Path.Combine(ViewsDir, "FormatShiftTheme.xaml");
var doc = XDocument.Parse(File.ReadAllText(themeFile));
var keys = doc.Descendants()
.Select(e => e.Attribute(XName.Get("Key", "http://schemas.microsoft.com/winfx/2006/xaml"))?.Value)
.Where(k => k != null)
.ToHashSet();
Assert.Contains("FsIslandPrimaryButtonStyle", keys);
Assert.Contains("FsDoubleBezelShellStyle", keys);
}
[Fact]
public void SegmentedTabs_MustUsePureKorean_And_NotContainEnglishRawLabels()
{
// AGENTS.md Rule 3:
// 상단 내비게이션 탭은 영문 'Active Queue', 'Past Results' 대신 '대기열', '변환 기록'을 사용해야 한다.
var mainFile = Path.Combine(ViewsDir, "MainWindow.xaml");
var doc = XDocument.Parse(File.ReadAllText(mainFile));
var tabActive = doc.Descendants().FirstOrDefault(e => e.Attribute(XName.Get("Name", "http://schemas.microsoft.com/winfx/2006/xaml"))?.Value == "TabActiveBtn");
var tabPast = doc.Descendants().FirstOrDefault(e => e.Attribute(XName.Get("Name", "http://schemas.microsoft.com/winfx/2006/xaml"))?.Value == "TabPastBtn");
Assert.NotNull(tabActive);
Assert.NotNull(tabPast);
var activeText = tabActive.Descendants().Where(e => e.Name.LocalName == "TextBlock").Select(t => t.Attribute("Text")?.Value ?? t.Value).FirstOrDefault();
var pastText = tabPast.Descendants().Where(e => e.Name.LocalName == "TextBlock").Select(t => t.Attribute("Text")?.Value ?? t.Value).FirstOrDefault();
Assert.Equal("대기열", activeText);
Assert.Equal("변환 기록", pastText);
}
[Fact]
public void Sidebar_CombineCheck_MustWrapText_ToPreventClipping()
{
// Design Audit Invariant 4: 컨테이너 오버플로 및 텍스트 생략 방지
// CombineToSingleCheck 내부의 텍스트가 잘리지 않도록 TextBlock에 TextWrapping="Wrap"이 선언되어야 한다.
var mainFile = Path.Combine(ViewsDir, "MainWindow.xaml");
var doc = XDocument.Parse(File.ReadAllText(mainFile));
var combineCheck = doc.Descendants().FirstOrDefault(e => e.Attribute(XName.Get("Name", "http://schemas.microsoft.com/winfx/2006/xaml"))?.Value == "CombineToSingleCheck");
Assert.NotNull(combineCheck);
var childTb = combineCheck.Descendants().FirstOrDefault(e => e.Name.LocalName == "TextBlock");
Assert.NotNull(childTb);
Assert.Equal("Wrap", childTb.Attribute("TextWrapping")?.Value);
}
[Fact]
public void ProcessQueueButton_DefaultText_MustBeCompact_ToPreventOverflow()
{
// 280px 너비의 사이드바 내 Island 버튼(버튼 내 원형 화살표 포함)에서
// '대기 중 — 파일을 드래그하여 추가하세요'와 같은 긴 텍스트는 글자 잘림을 유발하므로
// '파일을 드래그하여 추가' 등 15자 이하의 정갈한 텍스트를 사용해야 한다.
var mainFile = Path.Combine(ViewsDir, "MainWindow.xaml");
var doc = XDocument.Parse(File.ReadAllText(mainFile));
var btn = doc.Descendants().FirstOrDefault(e => e.Attribute(XName.Get("Name", "http://schemas.microsoft.com/winfx/2006/xaml"))?.Value == "ProcessQueueButton");
Assert.NotNull(btn);
var content = btn.Attribute("Content")?.Value;
Assert.NotNull(content);
Assert.True(content.Length <= 15, $"ProcessQueueButton의 텍스트가 너무 길어 잘림이 발생합니다: '{content}' ({content.Length}자)");
Assert.Equal("파일을 드래그하여 추가", content);
}
[Fact]
public void Sidebar_AiTaskAndEncodingCards_MustUseDoubleBezelShell_ForUniformStyle()
{
// high-end-visual-design Section 4:
// 사이드바의 AI 작업 및 상세 인코딩 설정 카드도 FsDoubleBezelShellStyle로 통일되어 일관된 머신 하드웨어 룩을 완성해야 한다.
var mainFile = Path.Combine(ViewsDir, "MainWindow.xaml");
var doc = XDocument.Parse(File.ReadAllText(mainFile));
var aiPanel = doc.Descendants().FirstOrDefault(d => d.Attribute(XName.Get("Name", "http://schemas.microsoft.com/winfx/2006/xaml"))?.Value == "AiTaskPanel");
Assert.NotNull(aiPanel);
var outerBorder = aiPanel.Ancestors().Where(a => a.Name.LocalName == "Border").Skip(1).FirstOrDefault();
Assert.NotNull(outerBorder);
Assert.Equal("{StaticResource FsDoubleBezelShellStyle}", outerBorder.Attribute("Style")?.Value);
}
[Fact]
public void ActiveQueueList_StateText_MustBindToDisplayStateText_ForKoreanLocalization()
{
// AGENTS.md Rule 3:
// 대기열 목록의 상태 표시는 날것의 영문 'queued', 'done' 대신
// 한국어 '대기 중', '변환 완료'를 제공하는 DisplayStateText에 바인딩되어야 한다.
var mainFile = Path.Combine(ViewsDir, "MainWindow.xaml");
var doc = XDocument.Parse(File.ReadAllText(mainFile));
var queueList = doc.Descendants().FirstOrDefault(e => e.Attribute(XName.Get("Name", "http://schemas.microsoft.com/winfx/2006/xaml"))?.Value == "ActiveQueueList");
Assert.NotNull(queueList);
var boundTexts = queueList.Descendants().Where(e => e.Name.LocalName == "TextBlock")
.Select(t => t.Attribute("Text")?.Value)
.Where(v => v != null)
.ToList();
Assert.Contains("{Binding DisplayStateText}", boundTexts);
}
[Fact]
public void MainWindow_Title_MustBe_Everything2Everything_And_NotContainFormatShiftUtility()
{
// 윈도우 타이틀은 레거시 'FormatShift Utility'가 아니라 공식 브랜드명 'Everything2Everything'이어야 한다.
var mainFile = Path.Combine(ViewsDir, "MainWindow.xaml");
var doc = XDocument.Parse(File.ReadAllText(mainFile));
var root = doc.Root;
Assert.NotNull(root);
var title = root.Attribute("Title")?.Value;
Assert.Equal("Everything2Everything", title);
}
[Fact]
public void DropHintOverlay_MustUsePureKorean_And_NotContainEnglishRawLabels()
{
// 드래그앤드롭 오버레이 안내 텍스트는 영문 날것 'Drop to add to queue' 대신 정제된 한국어 '파일을 놓아 대기열에 추가'여야 한다.
var mainFile = Path.Combine(ViewsDir, "MainWindow.xaml");
var doc = XDocument.Parse(File.ReadAllText(mainFile));
var overlay = doc.Descendants().FirstOrDefault(e => e.Attribute(XName.Get("Name", "http://schemas.microsoft.com/winfx/2006/xaml"))?.Value == "DropHintOverlay");
Assert.NotNull(overlay);
var texts = overlay.Descendants().Where(e => e.Name.LocalName == "TextBlock")
.Select(t => t.Attribute("Text")?.Value)
.Where(v => v != null)
.ToList();
Assert.DoesNotContain(texts, t => t!.Contains("Drop to add to queue", StringComparison.OrdinalIgnoreCase));
Assert.Contains(texts, t => t!.Contains("대기열"));
}
[Fact]
public void SmartConversionDeck_MustDirectlyContain_AdvancedOptionsExpander_Below_Presets()
{
// Fluent 2 점진적 공개(Progressive Disclosure) 및 사용자 피드백 원칙:
// 상세 설정 폴드아웃(AdvancedOptionsExpander)은 저장 위치나 AI 작업 카드의 한참 아래가 아니라,
// 프리셋 선택 즉시 펼쳐서 미세조정할 수 있도록 '스마트 변환 덱'(SmartPresetCombo가 있는 첫 번째 카드) 내부에 직결되어야 한다.
var mainFile = Path.Combine(ViewsDir, "MainWindow.xaml");
var doc = XDocument.Parse(File.ReadAllText(mainFile));
var presetCombo = doc.Descendants().FirstOrDefault(e => e.Attribute(XName.Get("Name", "http://schemas.microsoft.com/winfx/2006/xaml"))?.Value == "SmartPresetCombo");
Assert.NotNull(presetCombo);
var expander = doc.Descendants().FirstOrDefault(e => e.Attribute(XName.Get("Name", "http://schemas.microsoft.com/winfx/2006/xaml"))?.Value == "AdvancedOptionsExpander");
Assert.NotNull(expander);
// SmartPresetCombo의 가장 가까운 Border 부모 (스마트 변환 덱 카드)
var deckCard = presetCombo.Ancestors().FirstOrDefault(a => a.Name.LocalName == "Border" && (a.Attribute("Style")?.Value?.Contains("FsCardStyle") == true || a.Attribute("Style")?.Value?.Contains("FsDoubleBezelShellStyle") == true));
Assert.NotNull(deckCard);
// AdvancedOptionsExpander가 바로 그 스마트 변환 덱 카드(deckCard)의 자손이어야 한다.
var isInsideDeck = deckCard.Descendants().Any(d => d == expander);
Assert.True(isInsideDeck, "AdvancedOptionsExpander는 스마트 프리셋 바로 아래에서 펼쳐질 수 있도록 스마트 변환 덱(deckCard) 내부에 위치해야 합니다.");
}
[Fact]
public void SmartConversionDeck_MustContain_AllQuickPanels_For_MediaTypes()
{
// 사용자 피드백: 프리셋만 퉁쳐져 있지 않고 이미지 품질, 비디오 CRF, 오디오 비트레이트, PDF 압축률 등
// 핵심 슬라이더와 선택기가 스마트 변환 덱에 직관적으로 배치되어야 한다.
var mainFile = Path.Combine(ViewsDir, "MainWindow.xaml");
var doc = XDocument.Parse(File.ReadAllText(mainFile));
var requiredPanels = new[] { "QualityPanel", "VideoQuickPanel", "AudioQuickPanel", "PdfQuickPanel" };
foreach (var panelName in requiredPanels)
{
var panel = doc.Descendants().FirstOrDefault(e => e.Attribute(XName.Get("Name", "http://schemas.microsoft.com/winfx/2006/xaml"))?.Value == panelName);
Assert.NotNull(panel);
}
// 각 패널에 연결된 컨트롤 및 슬라이더 확인
var qualitySlider = doc.Descendants().FirstOrDefault(e => e.Attribute("AutomationProperties.AutomationId")?.Value == "QualitySlider");
Assert.NotNull(qualitySlider);
var videoSlider = doc.Descendants().FirstOrDefault(e => e.Attribute("AutomationProperties.AutomationId")?.Value == "VideoCrfSlider");
Assert.NotNull(videoSlider);
var audioCombo = doc.Descendants().FirstOrDefault(e => e.Attribute("AutomationProperties.AutomationId")?.Value == "AudioBitrateQuickCombo");
Assert.NotNull(audioCombo);
var pdfCombo = doc.Descendants().FirstOrDefault(e => e.Attribute("AutomationProperties.AutomationId")?.Value == "PdfCompressQuickCombo");
Assert.NotNull(pdfCombo);
}
[Fact]
public void PastResultsEmpty_MustUseDoubleBezelShell_And_PureKoreanTerminology()
{
// 변환 기록 빈 상태도 FsDoubleBezelShellStyle을 사용하여 DropZoneEmpty와 일관된 머신 룩을 제공해야 하며,
// 버튼 텍스트는 '큐'가 아닌 '대기열' 표준 한국어 용어를 사용해야 한다.
var mainFile = Path.Combine(ViewsDir, "MainWindow.xaml");
var doc = XDocument.Parse(File.ReadAllText(mainFile));
var emptyContainer = doc.Descendants().FirstOrDefault(e => e.Attribute(XName.Get("Name", "http://schemas.microsoft.com/winfx/2006/xaml"))?.Value == "PastResultsEmpty");
Assert.NotNull(emptyContainer);
var bezelShell = emptyContainer.Descendants().FirstOrDefault(e => e.Name.LocalName == "Border" && e.Attribute("Style")?.Value == "{StaticResource FsDoubleBezelShellStyle}");
Assert.NotNull(bezelShell);
var texts = emptyContainer.Descendants().Where(e => e.Name.LocalName == "TextBlock")
.Select(t => t.Attribute("Text")?.Value)
.Where(v => v != null)
.ToList();
Assert.DoesNotContain(texts, t => t!.Contains("큐로"));
Assert.Contains(texts, t => t!.Contains("대기열로"));
}
[Fact]
public void SettingsWindow_Cards_MustUseDoubleBezelShell_ForUniformStyle()
{
// SettingsWindow의 AI 카드 및 외부 도구 카드도 FsDoubleBezelShellStyle 및 FsDoubleBezelCoreStyle을 사용하여
// 앱 전체와 동일한 스위스 미니멀 이중 베젤 아키텍처를 준수해야 한다.
var settingsFile = Path.Combine(ViewsDir, "SettingsWindow.xaml");
var doc = XDocument.Parse(File.ReadAllText(settingsFile));
var shells = doc.Descendants().Where(e => e.Name.LocalName == "Border" && e.Attribute("Style")?.Value == "{StaticResource FsDoubleBezelShellStyle}").ToList();
Assert.True(shells.Count >= 2, $"SettingsWindow는 최소 2개 이상의 이중 베젤 쉘 카드를 포함해야 합니다. (발견된 수: {shells.Count})");
}
[Fact]
public void Sidebar_AllCards_MustUseDoubleBezelShell_ForUnifiedAesthetic()
{
// high-end-visual-design Section 4 & stitch-design-taste:
// 사이드바의 모든 덱(스마트 변환 덱, 저장 위치 및 충돌 해결 덱, AI 작업 덱, 통계 덱)은
// 단일 보더 플랫 카드(FsCardStyle)가 아닌 이중 베젤(FsDoubleBezelShellStyle + FsDoubleBezelCoreStyle)로 통일되어야 한다.
var mainFile = Path.Combine(ViewsDir, "MainWindow.xaml");
var doc = XDocument.Parse(File.ReadAllText(mainFile));
var sidebarScroll = doc.Descendants().FirstOrDefault(e => e.Name.LocalName == "ScrollViewer" && e.Ancestors().Any(a => a.Name.LocalName == "Border" && a.Attribute(XName.Get("Column", "http://schemas.microsoft.com/winfx/2006/xaml"))?.Value == "0" || a.Attribute("Grid.Column")?.Value == "0"));
Assert.NotNull(sidebarScroll);
var stackPanel = sidebarScroll.Descendants().FirstOrDefault(e => e.Name.LocalName == "StackPanel");
Assert.NotNull(stackPanel);
// stackPanel의 직계 자식 카드들
var directChildCards = stackPanel.Elements().Where(e => e.Name.LocalName == "Border").ToList();
Assert.True(directChildCards.Count >= 4, $"사이드바에는 최소 4개 이상의 카드가 있어야 합니다. (발견된 수: {directChildCards.Count})");
foreach (var card in directChildCards)
{
var style = card.Attribute("Style")?.Value;
Assert.Equal("{StaticResource FsDoubleBezelShellStyle}", style);
}
}
[Fact]
public void PreviewPane_MustUsePureKorean_And_NotContainEnglishLabels()
{
// AGENTS.md 규약 3 & stitch-design-taste:
// 우측 미리보기 인스펙터 패널의 헤더, 빈 상태 안내문구, 메타데이터 레이블은
// 날것의 영어(PREVIEW, No selection, Active Queue, Format, Size, Dimensions, Pages) 대신
// 정갈하고 직관적인 한국어(미리보기, 선택된 항목 없음, 대기열, 형식, 크기, 해상도 등)를 사용해야 한다.
var mainFile = Path.Combine(ViewsDir, "MainWindow.xaml");
var doc = XDocument.Parse(File.ReadAllText(mainFile));
var textBlocks = doc.Descendants().Where(e => e.Name.LocalName == "TextBlock").ToList();
var allTexts = textBlocks.Select(t => t.Attribute("Text")?.Value).Where(v => v != null).ToList();
// 영어 레이블 금지
Assert.DoesNotContain(allTexts, t => t == "PREVIEW");
Assert.DoesNotContain(allTexts, t => t == "No selection");
Assert.DoesNotContain(allTexts, t => t != null && t.Contains("Active Queue 항목을 클릭하면"));
Assert.DoesNotContain(allTexts, t => t == "Format");
Assert.DoesNotContain(allTexts, t => t == "Size");
Assert.DoesNotContain(allTexts, t => t == "Dimensions");
Assert.DoesNotContain(allTexts, t => t == "Pages");
// 한국어 레이블 필수
Assert.Contains(allTexts, t => t == "선택된 항목 없음");
Assert.Contains(allTexts, t => t == "형식");
Assert.Contains(allTexts, t => t == "크기");
Assert.Contains(allTexts, t => t != null && t.Contains("해상도"));
}
[Fact]
public void DropZoneEmpty_MustUseDoubleBezelShell_ForLinearTierMachinedLook()
{
// high-end-visual-design Section 4 (Double-Bezel):
// 중앙 메인 빈 상태 드롭존(DropZoneEmpty)도 단일 대시 사각형이 아닌
// FsDoubleBezelShellStyle + FsDoubleBezelCoreStyle 이중 베젤 쉘 아키텍처로 구현되어
// PastResultsEmpty 및 사이드바 덱들과 완벽한 하드웨어 머신 룩 통일감을 형성해야 한다.
var mainFile = Path.Combine(ViewsDir, "MainWindow.xaml");
var doc = XDocument.Parse(File.ReadAllText(mainFile));
var dropZone = doc.Descendants().FirstOrDefault(e => e.Attribute(XName.Get("Name", "http://schemas.microsoft.com/winfx/2006/xaml"))?.Value == "DropZoneEmpty");
Assert.NotNull(dropZone);
var bezelShell = dropZone.Descendants().FirstOrDefault(e => e.Name.LocalName == "Border" && e.Attribute("Style")?.Value == "{StaticResource FsDoubleBezelShellStyle}");
Assert.NotNull(bezelShell);
}
[Fact]
public void MainWindow_CodeBehind_DateLabels_MustUseKorean_And_NotContainEnglishTodayOrYesterday()
{
// AGENTS.md 규약 3 & stitch-design-taste:
// 변환 기록 날짜 헤더(DateTitle)는 영문 'Today', 'Yesterday' 대신 정제된 한국어 '오늘', '어제'를 사용해야 한다.
var codeFile = Path.Combine(ViewsDir, "MainWindow.xaml.cs");
var code = File.ReadAllText(codeFile);
Assert.DoesNotContain("\"Today\"", code);
Assert.DoesNotContain("\"Yesterday\"", code);
Assert.Contains("\"오늘", code);
Assert.Contains("\"어제", code);
}
[Theory]
[InlineData("MainWindow.xaml")]
[InlineData("SettingsWindow.xaml")]
[InlineData("DiagnoseWindow.xaml")]
[InlineData("QuickOptionsWindow.xaml")]
[InlineData("QuickProgressWindow.xaml")]
public void AllWindows_MustInclude_UiControlsDictionary_InResources(string xamlName)
{
// Wpf.Ui Fluent 2 컨트롤(ComboBox, Slider, Button 등)의 다크 테마 룩앤필이
// 독립 실행 및 In-Memory 렌더링 시에도 100% 보장되도록 각 윈도우의 MergedDictionaries는
// <ui:ControlsDictionary/>를 필수로 포함해야 한다.
var file = Path.Combine(ViewsDir, xamlName);
var doc = XDocument.Parse(File.ReadAllText(file));
var hasControlsDict = doc.Descendants().Any(e => e.Name.LocalName == "ControlsDictionary");
Assert.True(hasControlsDict, $"{xamlName}에 <ui:ControlsDictionary/>가 선언되어 있지 않습니다.");
}
[Fact]
public void MainWindow_CodeBehind_MustNotContain_RawEnglish_PastResults()
{
// AGENTS.md 규약 3 & stitch-design-taste:
// 메시지 박스 및 다이얼로그 안내 문구에서도 'Past Results' 날것의 영어를 금지하고
// '변환 기록' 표준 한국어로 안내해야 한다.
var codeFile = Path.Combine(ViewsDir, "MainWindow.xaml.cs");
var code = File.ReadAllText(codeFile);
Assert.DoesNotContain("\"Past Results", code);
}
[Fact]
public void PastResults_MetaLine_MustHave_TextTrimmingCharacterEllipsis_ToPreventOverflow()
{
// AGENTS.md Rule 4 (Overflow Safety):
// 긴 파일 메타데이터(타임스탬프 + 파일수)를 표시하는 TextBlock은
// 좁은 컬럼 폭에서도 오버플로/글리프 잘림 없이 안전하게 축약되어야 한다.
var file = Path.Combine(ViewsDir, "MainWindow.xaml");
var doc = XDocument.Parse(File.ReadAllText(file));
var metaTextBlocks = doc.Descendants()
.Where(e => e.Name.LocalName == "TextBlock" &&
e.Attribute("Text")?.Value == "{Binding MetaLine}")
.ToList();
Assert.NotEmpty(metaTextBlocks);
foreach (var tb in metaTextBlocks)
{
var trimming = tb.Attribute("TextTrimming")?.Value;
Assert.True(trimming == "CharacterEllipsis",
"MetaLine TextBlock은 TextTrimming=\"CharacterEllipsis\"를 지정하여 오버플로를 방지해야 합니다.");
}
}
[Fact]
public void MainWindow_SearchAndFilterToolbar_MustBeScopedToListColumn()
{
// 검색 및 카테고리 필터 바는 미리보기(Inspector) 위로 어색하게 500px 걸쳐있지 않고,
// 대상이 되는 대기열/기록 리스트 컬럼(Grid.Column="0") 내부에 정렬되어야 한다.
var file = Path.Combine(ViewsDir, "MainWindow.xaml");
var doc = XDocument.Parse(File.ReadAllText(file));
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);
// SearchBox를 감싸는 툴바 Border 찾기
var toolbarBorder = searchBox.Ancestors().FirstOrDefault(e => e.Name.LocalName == "Border" &&
e.Attribute("Grid.Row")?.Value == "0");
Assert.NotNull(toolbarBorder);
var colSpan = toolbarBorder.Attribute("Grid.ColumnSpan")?.Value;
Assert.True(string.IsNullOrEmpty(colSpan) || colSpan == "1",
"Search/Filter Toolbar는 Inspector 컬럼을 침범하는 ColumnSpan=\"2\"가 아니어야 하며, 리스트 컬럼(Column 0)에 정합되어야 합니다.");
}
[Fact]
public void ActiveQueueList_Columns_MustProvideAdequateWidthForMetadata()
{
// 대기열 아이템 행 그리드에서 파일 크기 및 상태 컬럼이 과도하게 너비를 차지하여
// 파일명과 메타데이터 문자열이 중간에 잘리지 않도록 컬럼 너비를 정밀 최적화해야 한다.
// SizeText 컬럼 <= 80px, DisplayStateText 컬럼 <= 90px
var file = Path.Combine(ViewsDir, "MainWindow.xaml");
var doc = XDocument.Parse(File.ReadAllText(file));
var queueList = doc.Descendants().FirstOrDefault(e => e.Name.LocalName == "ItemsControl" &&
e.Attribute(XName.Get("Name", "http://schemas.microsoft.com/winfx/2006/xaml"))?.Value == "ActiveQueueList");
Assert.NotNull(queueList);
var colDefs = queueList.Descendants()
.Where(e => e.Name.LocalName == "Grid.ColumnDefinitions" && e.Elements().Count() >= 6)
.FirstOrDefault();
Assert.NotNull(colDefs);
var widths = colDefs.Elements().Select(c => c.Attribute("Width")?.Value).ToList();
Assert.True(widths.Count >= 6, "ActiveQueueList 행 그리드는 최소 6개 컬럼(선택, 아이콘, 본문*, 크기, 상태, 삭제)이어야 합니다.");
// Column 3: SizeText width
var sizeWidth = widths[3];
Assert.True(int.TryParse(sizeWidth, out var sw) && sw <= 80,
$"SizeText 컬럼 폭은 80px 이하여야 메타데이터 컬럼이 오버플로되지 않습니다. 현재: {sizeWidth}");
// Column 4: StateText width
var stateWidth = widths[4];
Assert.True(int.TryParse(stateWidth, out var stw) && stw <= 90,
$"DisplayStateText 컬럼 폭은 90px 이하여야 메타데이터 컬럼이 오버플로되지 않습니다. 현재: {stateWidth}");
}
[Fact]
public void Inspector_PreviewArea_MustUse_DoubleBezelArchitecture()
{
// 미리보기 뷰포트는 단순 평면 박스가 아니라 고품격 machined 하드웨어 느낌의
// FsDoubleBezelShellStyle 및 FsDoubleBezelCoreStyle 이중 베젤(Doppelrand) 구조를 가져야 한다.
var file = Path.Combine(ViewsDir, "MainWindow.xaml");
var doc = XDocument.Parse(File.ReadAllText(file));
var previewArea = doc.Descendants().FirstOrDefault(e =>
e.Attribute(XName.Get("Name", "http://schemas.microsoft.com/winfx/2006/xaml"))?.Value == "PreviewImageArea");
Assert.NotNull(previewArea);
var hasShell = previewArea.Descendants().Any(e =>
e.Attribute("Style")?.Value.Contains("FsDoubleBezelShellStyle") == true);
var hasCore = previewArea.Descendants().Any(e =>
e.Attribute("Style")?.Value.Contains("FsDoubleBezelCoreStyle") == true);
Assert.True(hasShell && hasCore,
"PreviewImageArea는 고품격 Fluent 2 미학을 위해 FsDoubleBezelShellStyle 및 FsDoubleBezelCoreStyle 이중 베젤 구조를 갖추어야 합니다.");
}
[Fact]
public void MainWindow_SearchBox_Width_MustLeaveAdequateRoomForFilterButtons()
{
// 1080p 및 컴팩트 뷰포트에서도 '전체' 및 카테고리 필터 버튼들이
// 검색창에 가려지거나 겹치지 않도록 검색창 너비는 180px 이하여야 한다.
var file = Path.Combine(ViewsDir, "MainWindow.xaml");
var doc = XDocument.Parse(File.ReadAllText(file));
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);
var toolbarGrid = searchBox.Ancestors()
.FirstOrDefault(e => e.Name.LocalName == "Grid" && e.Element(searchBox.Name.Namespace + "Grid.ColumnDefinitions") != null);
Assert.NotNull(toolbarGrid);
var searchBoxCol = toolbarGrid.Element(searchBox.Name.Namespace + "Grid.ColumnDefinitions")?
.Elements().FirstOrDefault();
Assert.NotNull(searchBoxCol);
var widthVal = searchBoxCol.Attribute("Width")?.Value;
Assert.True(int.TryParse(widthVal, out var w) && w <= 180,
$"SearchBox 컬럼 폭은 180px 이하여야 필터 버튼들이 잘리지 않습니다. 현재: {widthVal}");
}
}

View file

@ -249,6 +249,167 @@ public class DesignAuditVisualTreeTests
});
}
[Fact]
public void MainWindow_WhenQueueIsEmpty_ShowsDefaultFormat_AndQualitySlider_AndAdvancedPanel_Visible()
{
// Zero-Void 원칙 및 점진적 공개 원칙: 큐가 비어있더라도 사용자가 변환 옵션과 품질 슬라이더를 사전에 확인하고 조작할 수 있도록
// 기본 출력 포맷(.jpg)과 품질 슬라이더, 해당 포맷의 상세 인코딩 서브패널이 Visible 이어야 한다.
RunOnSta(() =>
{
var engine = Everything2EverythingBootstrap.CreateDefault();
var settings = new FakeSettingsStore();
var window = new MainWindow(engine, settings);
var content = (UIElement)window.Content;
content.Measure(new Size(1280, 960));
content.Arrange(new Rect(0, 0, 1280, 960));
var formatCombo = (ComboBox)window.FindName("OutputFormatCombo");
var qualityPanel = (StackPanel)window.FindName("QualityPanel");
var advancedImagePanel = (StackPanel)window.FindName("AdvancedImagePanel");
Assert.NotNull(formatCombo);
Assert.NotNull(qualityPanel);
Assert.NotNull(advancedImagePanel);
// 큐가 비어있어도 지원 가능한 전체 포맷이 채워져 있어야 한다.
Assert.True(formatCombo.Items.Count > 0, "큐가 비어있어도 전체 지원 포맷이 표시되어야 합니다.");
// 기본 포맷(.jpg)에 맞춰 품질 슬라이더 패널이 표시되어야 한다.
Assert.Equal(Visibility.Visible, qualityPanel.Visibility);
// 기본 포맷에 맞춰 상세 설정 패널이 표시되어야 한다.
Assert.Equal(Visibility.Visible, advancedImagePanel.Visibility);
});
}
[Fact]
public void MainWindow_FormatSwitching_UpdatesQuickPanels_And_AdvancedPanels_Correctly()
{
// 사용자 피드백 대응: 스마트 프리셋 직하단에서 각 미디어 형식(.mp4, .pdf, .mp3, .webp)에 따라
// 전용 퀵 컨트롤(품질/CRF/비트레이트/PDF압축)과 상세 폴드아웃 서브패널이 동적으로 즉시 표시되어야 한다.
RunOnSta(() =>
{
var engine = Everything2EverythingBootstrap.CreateDefault();
var settings = new FakeSettingsStore();
var window = new MainWindow(engine, settings);
var content = (UIElement)window.Content;
content.Measure(new Size(1280, 960));
content.Arrange(new Rect(0, 0, 1280, 960));
var formatCombo = (ComboBox)window.FindName("OutputFormatCombo");
var qualityPanel = (StackPanel)window.FindName("QualityPanel");
var videoQuickPanel = (StackPanel)window.FindName("VideoQuickPanel");
var audioQuickPanel = (StackPanel)window.FindName("AudioQuickPanel");
var pdfQuickPanel = (StackPanel)window.FindName("PdfQuickPanel");
var expander = (Expander)window.FindName("AdvancedOptionsExpander");
var advVideo = (StackPanel)window.FindName("AdvancedVideoPanel");
var advAudio = (StackPanel)window.FindName("AdvancedAudioPanel");
var advPdf = (StackPanel)window.FindName("AdvancedPdfPanel");
var advImage = (StackPanel)window.FindName("AdvancedImagePanel");
Assert.NotNull(formatCombo);
Assert.NotNull(qualityPanel);
Assert.NotNull(videoQuickPanel);
Assert.NotNull(audioQuickPanel);
Assert.NotNull(pdfQuickPanel);
Assert.NotNull(expander);
// 기본은 열려있어야 함 (사용자가 즉시 확인 가능)
Assert.True(expander.IsExpanded);
void SelectExtension(string ext)
{
for (var i = 0; i < formatCombo.Items.Count; i++)
{
if (formatCombo.Items[i] is ComboBoxItem item && (string)item.Tag == ext)
{
formatCombo.SelectedIndex = i;
break;
}
}
}
// 1. 영상 (.mp4) 전환 검증
SelectExtension(".mp4");
Assert.Equal(Visibility.Collapsed, qualityPanel.Visibility);
Assert.Equal(Visibility.Visible, videoQuickPanel.Visibility);
Assert.Equal(Visibility.Visible, advVideo.Visibility);
Assert.Equal(Visibility.Visible, advAudio.Visibility);
Assert.Equal(Visibility.Collapsed, advPdf.Visibility);
// 2. PDF (.pdf) 전환 검증
SelectExtension(".pdf");
Assert.Equal(Visibility.Collapsed, qualityPanel.Visibility);
Assert.Equal(Visibility.Collapsed, videoQuickPanel.Visibility);
Assert.Equal(Visibility.Visible, pdfQuickPanel.Visibility);
Assert.Equal(Visibility.Visible, advPdf.Visibility);
Assert.Equal(Visibility.Collapsed, advVideo.Visibility);
// 3. 오디오 (.mp3) 전환 검증
SelectExtension(".mp3");
Assert.Equal(Visibility.Collapsed, qualityPanel.Visibility);
Assert.Equal(Visibility.Visible, audioQuickPanel.Visibility);
Assert.Equal(Visibility.Visible, advAudio.Visibility);
Assert.Equal(Visibility.Collapsed, advVideo.Visibility);
// 4. 이미지 (.webp) 전환 검증
SelectExtension(".webp");
Assert.Equal(Visibility.Visible, qualityPanel.Visibility);
Assert.Equal(Visibility.Collapsed, videoQuickPanel.Visibility);
Assert.Equal(Visibility.Collapsed, audioQuickPanel.Visibility);
Assert.Equal(Visibility.Collapsed, pdfQuickPanel.Visibility);
Assert.Equal(Visibility.Visible, advImage.Visibility);
});
}
[Fact]
public void MainWindow_RenderToBitmap_SavesVisualVerificationArtifact()
{
RunOnSta(() =>
{
var dir = System.IO.Directory.GetCurrentDirectory();
while (dir != null && !System.IO.File.Exists(System.IO.Path.Combine(dir, "Everything2Everything.slnx")))
{
dir = System.IO.Directory.GetParent(dir)?.FullName;
}
var root = dir ?? throw new System.IO.DirectoryNotFoundException("솔루션 루트를 찾을 수 없습니다.");
var engine = Everything2EverythingBootstrap.CreateDefault();
var settings = new FakeSettingsStore();
var testFiles = new List<string>
{
System.IO.Path.Combine(root, "test_assets", "test_icon.png"),
System.IO.Path.Combine(root, "test_assets", "test_art.png")
};
var window = new MainWindow(engine, settings, testFiles);
window.ApplyTemplate();
var expander = (Expander)window.FindName("AdvancedOptionsExpander");
expander.ApplyTemplate();
var content = (UIElement)window.Content;
content.Measure(new Size(1280, 960));
content.Arrange(new Rect(0, 0, 1280, 960));
content.UpdateLayout();
var rtb = new System.Windows.Media.Imaging.RenderTargetBitmap(1280, 960, 96, 96, PixelFormats.Pbgra32);
rtb.Render(content);
var encoder = new System.Windows.Media.Imaging.PngBitmapEncoder();
encoder.Frames.Add(System.Windows.Media.Imaging.BitmapFrame.Create(rtb));
var outDir = @"C:\Users\encep\.gemini\antigravity\brain\a5abaf02-dd4b-45f7-8890-144e9da36bcc";
var outPath = System.IO.Path.Combine(outDir, "app_rendered_preview.png");
using (var fs = new System.IO.FileStream(outPath, System.IO.FileMode.Create, System.IO.FileAccess.Write, System.IO.FileShare.ReadWrite))
{
encoder.Save(fs);
}
Assert.True(System.IO.File.Exists(outPath));
Assert.True(new System.IO.FileInfo(outPath).Length > 1000);
});
}
private static IEnumerable<T> FindLogicalChildren<T>(object parent) where T : DependencyObject
{
if (parent is ContentControl cc && cc.Content != null)

View file

@ -60,4 +60,24 @@ public class FileInspectorTests
var info = FileInspectorBuilder.Build(dummyPath);
Assert.Equal(expectedCategory, info.Category);
}
[Theory]
[InlineData(FilterCategory.All, "전체")]
[InlineData(FilterCategory.Image, "이미지")]
[InlineData(FilterCategory.Document, "문서")]
[InlineData(FilterCategory.Media, "미디어")]
[InlineData(FilterCategory.Data, "데이터")]
public void ToKoreanLabel_ReturnsRefinedKoreanCategory(FilterCategory category, string expectedLabel)
{
var label = category.ToKoreanLabel();
Assert.Equal(expectedLabel, label);
}
[Fact]
public void Build_DimensionsOrMeta_UsesKoreanCategory()
{
var dummyPath = "C:\\test\\sample.png";
var info = FileInspectorBuilder.Build(dummyPath);
Assert.Equal("PNG · 이미지", info.DimensionsOrMeta);
}
}

View file

@ -34,7 +34,7 @@ public class FormatPresetEngineTests
Assert.True(presets.Count >= 3);
var q85 = presets.First(p => p.Title.Contains("웹 고화질"));
Assert.Contains("Quality 85", q85.SpecChips);
Assert.Contains("품질 85", q85.SpecChips);
var options = new OptionsViewModel();
q85.Apply(options);
@ -42,6 +42,23 @@ public class FormatPresetEngineTests
Assert.True(options.StripMetadata);
}
[Theory]
[InlineData(".jpg", "품질 95")]
[InlineData(".webp", "품질 85")]
[InlineData(".avif", "품질 55")]
public void GetPresetsForExtension_ImagePresets_MustUseKoreanQualityChips(string ext, string expectedChip)
{
var presets = FormatPresetEngine.GetPresetsForExtension(ext);
var hasExpectedChip = presets.Any(p => p.SpecChips.Contains(expectedChip));
Assert.True(hasExpectedChip, $"{ext} 프리셋 스펙 칩에 한국어 '{expectedChip}'이 포함되어야 합니다.");
// 영어 Quality X 칩 금지
foreach (var p in presets)
{
Assert.DoesNotContain(p.SpecChips, c => c.StartsWith("Quality "));
}
}
[Fact]
public void GetPresetsForExtension_Pdf_ReturnsResolutionPresets()
{
@ -71,6 +88,30 @@ public class FormatPresetEngineTests
Assert.Equal("fast", options.VideoPreset);
}
[Theory]
[InlineData(".jpg", 4)]
[InlineData(".png", 3)]
[InlineData(".webp", 4)]
[InlineData(".avif", 3)]
[InlineData(".pdf", 4)]
[InlineData(".mp4", 4)]
[InlineData(".mp3", 4)]
[InlineData(".gif", 2)]
public void GetPresetsForExtension_MajorFormats_ReturnRichPresetSuite(string ext, int minPresetCount)
{
var presets = FormatPresetEngine.GetPresetsForExtension(ext);
Assert.True(presets.Count >= minPresetCount,
$"{ext} 형식의 프리셋 개수가 너무 적습니다 (최소 {minPresetCount}개 이상 필요, 실제: {presets.Count}개).");
foreach (var p in presets)
{
Assert.False(string.IsNullOrWhiteSpace(p.Id), "프리셋 ID가 비어있습니다.");
Assert.False(string.IsNullOrWhiteSpace(p.Title), "프리셋 제목이 비어있습니다.");
Assert.False(string.IsNullOrWhiteSpace(p.Description), "프리셋 설명이 비어있습니다.");
Assert.True(p.SpecChips.Count >= 2, $"프리셋 [{p.Title}]의 스펙 칩 개수가 2개 미만입니다.");
}
}
[Fact]
public void GetPresetsForExtension_UnknownExtension_ReturnsFallbackPresets()
{

View file

@ -65,4 +65,26 @@ public class OptionsViewModelTests
Assert.Equal(NameCollision.Skip, o.OnCollision);
Assert.False(o.VideoPreferGpu);
}
[Fact]
public void OptionsViewModel_HasAdvancedProperties_And_DefaultExpandedIsTrue()
{
// 사용자 피드백 반영: 슬라이더와 상세 옵션을 바로 확인할 수 있도록 기본값으로 펼침(true) 상태여야 한다.
var vm = new OptionsViewModel();
Assert.True(vm.IsAdvancedExpanded);
Assert.Equal(0, vm.PdfCompressLevelIndex);
Assert.Equal(1, vm.PdfDpiIndex);
Assert.False(vm.ImageLossless);
Assert.False(vm.Progressive);
vm.ImageLossless = true;
vm.Progressive = true;
vm.PdfCompressLevelIndex = 1;
vm.PdfDpiIndex = 2;
var options = vm.ToConvertOptions();
Assert.True(options.Webp.Lossless);
Assert.True(options.Jpeg.Progressive);
Assert.Equal(300, options.PdfRender.Dpi);
}
}

View file

@ -195,7 +195,7 @@ public class UnifiedTitleBarTests
try
{
var rtb = new System.Windows.Media.Imaging.RenderTargetBitmap(1280, 720, 96, 96, System.Windows.Media.PixelFormats.Pbgra32);
var rtb = new System.Windows.Media.Imaging.RenderTargetBitmap(1280, 960, 96, 96, System.Windows.Media.PixelFormats.Pbgra32);
rtb.Render(content);
var enc = new System.Windows.Media.Imaging.PngBitmapEncoder();
enc.Frames.Add(System.Windows.Media.Imaging.BitmapFrame.Create(rtb));
@ -313,6 +313,306 @@ public class UnifiedTitleBarTests
});
}
[Fact]
public void MainWindow_PopulatedActiveQueue_DisplaysBatchBarAndItems_AndEnablesLaunchButton()
{
RunOnSta(() =>
{
var engine = Everything2EverythingBootstrap.CreateDefault();
var store = new FakeSettingsStore();
var window = new MainWindow(engine, store);
// Add sample items to ActiveQueue
var q1 = new QueueItem
{
SourcePath = @"C:\Mock\nature_photo.jpg",
FileName = "nature_photo.jpg",
FormatLabel = "JPG",
FormatBrush = System.Windows.Media.Brushes.Coral,
SizeText = "4.2 MB",
MetaLine = "3840x2160 · sRGB · 24-bit",
SourceSizeBytes = 4_404_019,
SelectedOutputExtension = ".webp",
IsSelected = true
};
q1.SetState("queued");
var q2 = new QueueItem
{
SourcePath = @"C:\Mock\quarterly_report.pdf",
FileName = "quarterly_report.pdf",
FormatLabel = "PDF",
FormatBrush = System.Windows.Media.Brushes.IndianRed,
SizeText = "12.8 MB",
MetaLine = "32 페이지 · 텍스트/벡터 포함",
SourceSizeBytes = 13_421_772,
SelectedOutputExtension = ".pdf",
IsSelected = false
};
q2.SetState("50%");
var q3 = new QueueItem
{
SourcePath = @"C:\Mock\keynote_presentation.mp4",
FileName = "keynote_presentation.mp4",
FormatLabel = "MP4",
FormatBrush = System.Windows.Media.Brushes.CornflowerBlue,
SizeText = "156.4 MB",
MetaLine = "1080p60 · H.264 / AAC · 05:22",
SourceSizeBytes = 164_000_000,
SelectedOutputExtension = ".mp4",
IsSelected = false
};
q3.SetState("done");
window.ActiveQueue.Add(q1);
window.ActiveQueue.Add(q2);
window.ActiveQueue.Add(q3);
// Trigger visibility updates via reflection
var updateVisMethod = typeof(MainWindow).GetMethod("UpdateActiveQueueVisibility", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
updateVisMethod?.Invoke(window, null);
var updateBtnMethod = typeof(MainWindow).GetMethod("UpdateProcessQueueButton", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
updateBtnMethod?.Invoke(window, null);
var updateBadgesMethod = typeof(MainWindow).GetMethod("UpdateBadges", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
updateBadgesMethod?.Invoke(window, null);
var setPreviewMetaMethod = typeof(MainWindow).GetMethod("SetPreviewMeta", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
setPreviewMetaMethod?.Invoke(window, new object[] { q1.FileName, q1.SourcePath, q1.FormatLabel, q1.SizeText });
var showPreviewGlyphMethod = typeof(MainWindow).GetMethod("ShowPreviewGlyph", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
showPreviewGlyphMethod?.Invoke(window, new object[] { ".jpg", "선택된 파일: " + q1.FileName });
var activeQueueList = (ItemsControl)window.FindName("ActiveQueueList");
activeQueueList.ItemsSource = window.ActiveQueue;
activeQueueList.ApplyTemplate();
var content = (UIElement)window.Content;
content.Measure(new Size(1280, 960));
content.Arrange(new Rect(0, 0, 1280, 960));
content.UpdateLayout();
var dropZoneEmpty = (FrameworkElement)window.FindName("DropZoneEmpty");
var activeQueueScroll = (FrameworkElement)window.FindName("ActiveQueueScroll");
var batchActionBar = (FrameworkElement)window.FindName("BatchActionBar");
var processBtn = (System.Windows.Controls.Button)window.FindName("ProcessQueueButton");
Assert.NotNull(dropZoneEmpty);
Assert.NotNull(activeQueueScroll);
Assert.NotNull(batchActionBar);
Assert.NotNull(processBtn);
Assert.Equal(Visibility.Collapsed, dropZoneEmpty.Visibility);
Assert.Equal(Visibility.Visible, activeQueueScroll.Visibility);
Assert.Equal(Visibility.Visible, batchActionBar.Visibility);
Assert.Equal("변환 시작 (3개 파일)", processBtn.Content);
Assert.True(processBtn.IsEnabled);
// DisplayStateText check on items
Assert.Equal("대기 중", q1.DisplayStateText);
Assert.Equal("50%", q2.DisplayStateText);
Assert.Equal("변환 완료", q3.DisplayStateText);
try
{
var rtb = new System.Windows.Media.Imaging.RenderTargetBitmap(1280, 960, 96, 96, System.Windows.Media.PixelFormats.Pbgra32);
rtb.Render(content);
var enc = new System.Windows.Media.Imaging.PngBitmapEncoder();
enc.Frames.Add(System.Windows.Media.Imaging.BitmapFrame.Create(rtb));
var outPath = @"C:\Users\encep\.gemini\antigravity\brain\b58fd023-a52b-4f4b-aa23-e6df654aa1fb\.tempmediaStorage\rendered_queue_populated.png";
using var fs = File.Create(outPath);
enc.Save(fs);
}
catch { }
});
}
[Fact]
public void MainWindow_PastResults_MustUseKorean_And_NotContainEnglishOutputOrSessionSavings()
{
RunOnSta(() =>
{
var dg = new DateGroup("오늘 (9월 5일)");
Assert.Contains("절감", dg.SessionSavingsText);
Assert.DoesNotContain("Session Savings", dg.SessionSavingsText);
var entry = new HistoryEntry(
DateTime.Now,
@"C:\photos\sample.png",
"png",
1024 * 1024,
512 * 1024,
1,
null,
ConvertStatus.Success,
null,
new[] { @"C:\photos\sample.webp" });
var row = HistoryRow.From(entry);
Assert.Contains("개", row.MetaLine);
Assert.DoesNotContain("output(s)", row.MetaLine);
});
}
[Fact]
public void MainWindow_TabCommand_DefaultFallback_MustBeActive()
{
RunOnSta(() =>
{
var engine = Everything2EverythingBootstrap.CreateDefault();
var store = new FakeSettingsStore();
var window = new MainWindow(engine, store);
var tabActiveBtn = (System.Windows.Controls.Primitives.ToggleButton)window.FindName("TabActiveBtn");
var tabPastBtn = (System.Windows.Controls.Primitives.ToggleButton)window.FindName("TabPastBtn");
var activeQueueView = (FrameworkElement)window.FindName("ActiveQueueView");
var pastResultsContainer = (FrameworkElement)window.FindName("PastResultsContainer");
// Switch to past first
window.TabCommand.Execute("Past");
Assert.False(tabActiveBtn.IsChecked);
Assert.True(tabPastBtn.IsChecked);
Assert.Equal(Visibility.Collapsed, activeQueueView.Visibility);
Assert.Equal(Visibility.Visible, pastResultsContainer.Visibility);
// Execute with null or empty fallback
window.TabCommand.Execute(null);
Assert.True(tabActiveBtn.IsChecked);
Assert.False(tabPastBtn.IsChecked);
Assert.Equal(Visibility.Visible, activeQueueView.Visibility);
Assert.Equal(Visibility.Collapsed, pastResultsContainer.Visibility);
});
}
[Fact]
public void MainWindow_PastResults_RendersPopulatedAndEmptyState()
{
RunOnSta(() =>
{
var engine = Everything2EverythingBootstrap.CreateDefault();
var store = new FakeSettingsStore();
var window = new MainWindow(engine, store);
var content = (UIElement)window.Content;
content.Measure(new Size(1280, 960));
content.Arrange(new Rect(0, 0, 1280, 960));
content.UpdateLayout();
var updateBadgesMethod = typeof(MainWindow).GetMethod("UpdateBadges", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
// 1. Switch to Past tab when history is empty
window.PastResults.Clear();
updateBadgesMethod?.Invoke(window, null);
window.TabCommand.Execute("Past");
content.Measure(new Size(1280, 960));
content.Arrange(new Rect(0, 0, 1280, 960));
content.UpdateLayout();
var pastResultsContainer = (FrameworkElement)window.FindName("PastResultsContainer");
var pastResultsEmpty = (FrameworkElement)window.FindName("PastResultsEmpty");
var pastResultsView = (FrameworkElement)window.FindName("PastResultsView");
Assert.Equal(Visibility.Visible, pastResultsContainer.Visibility);
Assert.Equal(Visibility.Visible, pastResultsEmpty.Visibility);
Assert.Equal(Visibility.Collapsed, pastResultsView.Visibility);
// Render empty state
try
{
var rtbEmpty = new System.Windows.Media.Imaging.RenderTargetBitmap(1280, 960, 96, 96, System.Windows.Media.PixelFormats.Pbgra32);
rtbEmpty.Render(content);
var enc = new System.Windows.Media.Imaging.PngBitmapEncoder();
enc.Frames.Add(System.Windows.Media.Imaging.BitmapFrame.Create(rtbEmpty));
var outPath = @"C:\Users\encep\.gemini\antigravity\brain\b58fd023-a52b-4f4b-aa23-e6df654aa1fb\.tempmediaStorage\rendered_past_results_empty.png";
using var fs = File.Create(outPath);
enc.Save(fs);
}
catch { }
// 2. Populate Past Results
var dateGroup = new DateGroup("오늘 (9월 5일)");
var entry1 = new HistoryEntry(
DateTime.Now,
@"C:\demo\hero-graphic.png",
"png",
4_250_000,
820_000,
1,
null,
ConvertStatus.Success,
null,
new[] { @"C:\demo\hero-graphic.webp" });
var entry2 = new HistoryEntry(
DateTime.Now.AddMinutes(-12),
@"C:\demo\annual-report.docx",
"docx",
12_800_000,
3_150_000,
1,
null,
ConvertStatus.Success,
null,
new[] { @"C:\demo\annual-report.pdf" });
dateGroup.Add(HistoryRow.From(entry1));
dateGroup.Add(HistoryRow.From(entry2));
window.PastResults.Add(dateGroup);
updateBadgesMethod?.Invoke(window, null);
var pastRow = dateGroup.Entries.FirstOrDefault();
if (pastRow != null)
{
var setPreviewMetaMethod = typeof(MainWindow).GetMethod("SetPreviewMeta", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
setPreviewMetaMethod?.Invoke(window, new object[] { pastRow.FileName, pastRow.SourcePath, pastRow.FormatLabel, pastRow.SizeText });
var showPreviewGlyphMethod = typeof(MainWindow).GetMethod("ShowPreviewGlyph", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
showPreviewGlyphMethod?.Invoke(window, new object[] { ".png", "변환 완료: " + pastRow.FileName });
}
var pastResultsList = (ItemsControl)window.FindName("PastResultsList");
pastResultsList.ItemsSource = window.PastResults;
pastResultsList.ApplyTemplate();
content.Measure(new Size(1280, 960));
content.Arrange(new Rect(0, 0, 1280, 960));
content.UpdateLayout();
Assert.Equal(Visibility.Collapsed, pastResultsEmpty.Visibility);
Assert.Equal(Visibility.Visible, pastResultsView.Visibility);
// Render populated state
try
{
var rtbPopulated = new System.Windows.Media.Imaging.RenderTargetBitmap(1280, 960, 96, 96, System.Windows.Media.PixelFormats.Pbgra32);
rtbPopulated.Render(content);
var enc = new System.Windows.Media.Imaging.PngBitmapEncoder();
enc.Frames.Add(System.Windows.Media.Imaging.BitmapFrame.Create(rtbPopulated));
var outPath = @"C:\Users\encep\.gemini\antigravity\brain\b58fd023-a52b-4f4b-aa23-e6df654aa1fb\.tempmediaStorage\rendered_past_results.png";
using var fs = File.Create(outPath);
enc.Save(fs);
}
catch { }
});
}
[Fact]
public void MainWindow_EnsureHandle_CreatesValidHwnd()
{
RunOnSta(() =>
{
var engine = Everything2EverythingBootstrap.CreateDefault();
var store = new FakeSettingsStore();
var window = new MainWindow(engine, store);
var helper = new System.Windows.Interop.WindowInteropHelper(window);
var hwnd = helper.EnsureHandle();
Assert.NotEqual(IntPtr.Zero, hwnd);
window.Close();
});
}
[Fact]
public void SettingsWindow_TitleBar_MustDisplayProperlyAlignedHeaderAndCloseButton()
{
@ -355,6 +655,18 @@ public class UnifiedTitleBarTests
var closeCenterY = closeBtn.TransformToAncestor(titleBar).Transform(new Point(0, closeBtn.ActualHeight / 2.0)).Y;
Assert.True(Math.Abs(textCenterY - closeCenterY) <= 2.0,
$"Title text CenterY ({textCenterY:F1}px) and CloseButton CenterY ({closeCenterY:F1}px) must match within 2px.");
try
{
var rtb = new System.Windows.Media.Imaging.RenderTargetBitmap(560, 720, 96, 96, System.Windows.Media.PixelFormats.Pbgra32);
rtb.Render(content);
var enc = new System.Windows.Media.Imaging.PngBitmapEncoder();
enc.Frames.Add(System.Windows.Media.Imaging.BitmapFrame.Create(rtb));
var outPath = @"C:\Users\encep\.gemini\antigravity\brain\b58fd023-a52b-4f4b-aa23-e6df654aa1fb\.tempmediaStorage\rendered_settings.png";
using var fs = File.Create(outPath);
enc.Save(fs);
}
catch { }
});
}