1
0
Fork 0

feat(ui): elevate cockpit ergonomics, add search watermark, queue selection styling, and shortcut bindings

This commit is contained in:
Yun Chan 2026-09-05 12:44:23 +09:00
parent 4ca7352dc3
commit 388d6707d0
6 changed files with 462 additions and 23 deletions

View file

@ -947,6 +947,137 @@ public class DesignAuditAstTests
Assert.True(int.TryParse(widthVal, out var w) && w <= 180,
$"SearchBox 컬럼 폭은 180px 이하여야 필터 버튼들이 잘리지 않습니다. 현재: {widthVal}");
}
[Fact]
public void BatchActionBar_MustInclude_LiveCockpitSummary()
{
// BatchActionBar는 단순 버튼 나열이 아니라 큐의 총 파일 개수 및 총 데이터 용량을 한눈에 보여주는
// QueueSummaryCountText 및 QueueSummarySizeText 콕핏 텔레메트리 요소를 포함해야 한다.
var file = Path.Combine(ViewsDir, "MainWindow.xaml");
var doc = XDocument.Parse(File.ReadAllText(file));
var batchBar = doc.Descendants().FirstOrDefault(e =>
e.Attribute(XName.Get("Name", "http://schemas.microsoft.com/winfx/2006/xaml"))?.Value == "BatchActionBar");
Assert.NotNull(batchBar);
var summaryCount = batchBar.Descendants().FirstOrDefault(e =>
e.Attribute(XName.Get("Name", "http://schemas.microsoft.com/winfx/2006/xaml"))?.Value == "QueueSummaryCountText");
var summarySize = batchBar.Descendants().FirstOrDefault(e =>
e.Attribute(XName.Get("Name", "http://schemas.microsoft.com/winfx/2006/xaml"))?.Value == "QueueSummarySizeText");
Assert.NotNull(summaryCount);
Assert.NotNull(summarySize);
}
[Fact]
public void Inspector_MustInclude_ConversionPipelineCard()
{
// 미리보기 패널(Inspector)은 원본과 대상 형식 간 변환 경로 및 예상 절감 효과를 즉시 시각화하는
// ConversionPipelineCard 컨테이너를 포함해야 한다.
var file = Path.Combine(ViewsDir, "MainWindow.xaml");
var doc = XDocument.Parse(File.ReadAllText(file));
var pipelineCard = doc.Descendants().FirstOrDefault(e =>
e.Attribute(XName.Get("Name", "http://schemas.microsoft.com/winfx/2006/xaml"))?.Value == "ConversionPipelineCard");
Assert.NotNull(pipelineCard);
var sourceText = pipelineCard.Descendants().FirstOrDefault(e =>
e.Attribute(XName.Get("Name", "http://schemas.microsoft.com/winfx/2006/xaml"))?.Value == "PipelineSourceText");
var targetText = pipelineCard.Descendants().FirstOrDefault(e =>
e.Attribute(XName.Get("Name", "http://schemas.microsoft.com/winfx/2006/xaml"))?.Value == "PipelineTargetText");
Assert.NotNull(sourceText);
Assert.NotNull(targetText);
}
[Fact]
public void PastResults_MustInclude_TelemetryHeaderBar()
{
// 변환 기록 뷰(PastResultsContainer)는 단순 리스트 노출 전에 누적 변환 수량 및 총 절감량을 요약하고
// 빠른 내보내기 액션을 제공하는 PastResultsHeaderBar 툴바를 포함해야 한다.
var file = Path.Combine(ViewsDir, "MainWindow.xaml");
var doc = XDocument.Parse(File.ReadAllText(file));
var container = doc.Descendants().FirstOrDefault(e =>
e.Attribute(XName.Get("Name", "http://schemas.microsoft.com/winfx/2006/xaml"))?.Value == "PastResultsContainer");
Assert.NotNull(container);
var headerBar = container.Descendants().FirstOrDefault(e =>
e.Attribute(XName.Get("Name", "http://schemas.microsoft.com/winfx/2006/xaml"))?.Value == "PastResultsHeaderBar");
Assert.NotNull(headerBar);
}
[Fact]
public void Sidebar_OutputFormatHint_MustWrapText()
{
// 320px 좁은 사이드바 폭에서도 설명 문구가 '자동 필...'처럼 잘리지 않도록
// OutputFormatHint는 TextWrapping="Wrap"을 선언해야 한다.
var file = Path.Combine(ViewsDir, "MainWindow.xaml");
var doc = XDocument.Parse(File.ReadAllText(file));
var hint = doc.Descendants().FirstOrDefault(e =>
e.Attribute(XName.Get("Name", "http://schemas.microsoft.com/winfx/2006/xaml"))?.Value == "OutputFormatHint");
Assert.NotNull(hint);
var textWrapping = hint.Attribute("TextWrapping")?.Value;
Assert.Equal("Wrap", textWrapping);
}
[Fact]
public void SearchBox_MustInclude_WatermarkPlaceholder_And_ClearButton()
{
// 검색창은 플레이스홀더 워터마크(SearchPlaceholderText)와 입력 내용 원클릭 지우기 버튼(SearchClearButton)을 포함해야 한다.
var file = Path.Combine(ViewsDir, "MainWindow.xaml");
var doc = XDocument.Parse(File.ReadAllText(file));
var placeholder = doc.Descendants().FirstOrDefault(e =>
e.Attribute(XName.Get("Name", "http://schemas.microsoft.com/winfx/2006/xaml"))?.Value == "SearchPlaceholderText");
var clearBtn = doc.Descendants().FirstOrDefault(e =>
e.Attribute(XName.Get("Name", "http://schemas.microsoft.com/winfx/2006/xaml"))?.Value == "SearchClearButton");
Assert.NotNull(placeholder);
Assert.NotNull(clearBtn);
}
[Fact]
public void Inspector_ActionButtons_MustBind_IsEnabled_To_Selection()
{
// 파일이 선택되지 않았을 때 무의미한 클릭을 방지하기 위해
// 미리보기 하단의 열기(PreviewOpenButton) 및 폴더보기(PreviewRevealButton) 버튼은 IsEnabled 바인딩 또는 제어를 가져야 한다.
var file = Path.Combine(ViewsDir, "MainWindow.xaml");
var doc = XDocument.Parse(File.ReadAllText(file));
var openBtn = doc.Descendants().FirstOrDefault(e =>
e.Attribute(XName.Get("Name", "http://schemas.microsoft.com/winfx/2006/xaml"))?.Value == "PreviewOpenButton");
var revealBtn = doc.Descendants().FirstOrDefault(e =>
e.Attribute(XName.Get("Name", "http://schemas.microsoft.com/winfx/2006/xaml"))?.Value == "PreviewRevealButton");
Assert.NotNull(openBtn);
Assert.NotNull(revealBtn);
var openEnabled = openBtn.Attribute("IsEnabled")?.Value;
var revealEnabled = revealBtn.Attribute("IsEnabled")?.Value;
Assert.NotNull(openEnabled);
Assert.NotNull(revealEnabled);
}
[Fact]
public void Window_InputBindings_MustInclude_SelectAll_And_Delete()
{
// Raycast / Linear 급 생산성을 위해 전체 선택(Ctrl+A) 및 선택 항목 삭제(Delete) 단축키가 Window.InputBindings에 등록되어야 한다.
var file = Path.Combine(ViewsDir, "MainWindow.xaml");
var doc = XDocument.Parse(File.ReadAllText(file));
var inputBindings = doc.Descendants().FirstOrDefault(e => e.Name.LocalName == "Window.InputBindings");
Assert.NotNull(inputBindings);
var keyBindings = inputBindings.Elements().Where(e => e.Name.LocalName == "KeyBinding").ToList();
bool hasCtrlA = keyBindings.Any(kb => kb.Attribute("Key")?.Value == "A" && kb.Attribute("Modifiers")?.Value == "Ctrl");
bool hasDelete = keyBindings.Any(kb => kb.Attribute("Key")?.Value == "Delete");
Assert.True(hasCtrlA, "Window.InputBindings에 Ctrl+A 단축키가 등록되어야 합니다.");
Assert.True(hasDelete, "Window.InputBindings에 Delete 단축키가 등록되어야 합니다.");
}
}

View file

@ -416,6 +416,19 @@ public class UnifiedTitleBarTests
Assert.Equal("50%", q2.DisplayStateText);
Assert.Equal("변환 완료", q3.DisplayStateText);
var summaryCount = (TextBlock)window.FindName("QueueSummaryCountText");
var summarySize = (TextBlock)window.FindName("QueueSummarySizeText");
Assert.NotNull(summaryCount);
Assert.NotNull(summarySize);
Assert.Equal("총 3개 항목", summaryCount.Text);
var pipeSource = (TextBlock)window.FindName("PipelineSourceText");
var pipeTarget = (TextBlock)window.FindName("PipelineTargetText");
Assert.NotNull(pipeSource);
Assert.NotNull(pipeTarget);
Assert.Equal("JPG · 4.2 MB", pipeSource.Text);
Assert.Equal("JPG", pipeTarget.Text);
try
{
var rtb = new System.Windows.Media.Imaging.RenderTargetBitmap(1280, 960, 96, 96, System.Windows.Media.PixelFormats.Pbgra32);
@ -583,6 +596,10 @@ public class UnifiedTitleBarTests
Assert.Equal(Visibility.Collapsed, pastResultsEmpty.Visibility);
Assert.Equal(Visibility.Visible, pastResultsView.Visibility);
var pastTotalCount = (TextBlock)window.FindName("PastTotalCountText");
Assert.NotNull(pastTotalCount);
Assert.Equal("2개 파일", pastTotalCount.Text);
// Render populated state
try
{