[200] 의 패널은 코드로 Border 를 쌓는 비가상화 렌더고 SheetMe 는 가상화 ListBox 다. 골격은 SheetMe 것을 지키고 겉모습과 조작 표면만 옮겼다 — 통째로 베끼면 1,067 컨트롤 서식에서 검색어 한 글자마다 수천 비주얼을 다시 만들게 되고, [200] 에 대응물이 없는 컨테이너 자식 재귀 표시가 사라진다. 구조 - 페이지를 별도 섹션이 아니라 아웃라인의 최상위 노드로. 접기·복제·삭제·컨트롤 수·현재 강조 - 맨 아래 '+ 페이지 추가' 행 - 목록 순서를 뒤집었다 — 이제 맨 위가 z-order 최상위(캔버스에서 위에 겹쳐 보이는 것이 목록 위) - 들여쓰기 12 / 32 / +16 - 레이어 패널을 LayerPanelView 로 분리(MainView.xaml 이 1,000줄을 넘겼고, 진단 렌더러가 이 패널만 따로 찍어 라이트·다크·선택 상태를 검사할 수 있어야 한다) 선택 표시 - IsSelected 가 아니라 Highlight 3상태(None/Blue/Navy)가 배경을 그린다. 그룹 전원 선택 시 폴더는 파랑, 멤버는 서브트리 배경이 되고 맞닿는 모서리를 각지게 해 하나의 블록으로 이어진다 - 밴드 위에서는 아이콘·보조 텍스트 색도 함께 뒤집는다. 안 그러면 눈·자물쇠가 1.6:1 로 묻혀 선택한 행에서만 잠금 상태가 안 보이는 상태가 된다 - 신설 토큰 3종(B.LayerSel / B.LayerSubtree / B.OnSelMuted) — [200] 원본값은 각각 흰 글자 3.00:1, 다크 밴드 1.03:1, 라이트 밴드 1.14:1 로 이 저장소가 이미 기각한 값이다 기능 - 드래그로 z-order 재정렬(삽입선 어도너). 임의 index 이동 연산 MoveWithinParent 신설 - 페이지 복제 — 컨트롤 이름을 전부 새로 발급하고 그룹도 복제본끼리 다시 묶는다 ([200] 은 GroupId 를 그대로 둬서 복제 그룹이 원본과 gid 를 공유한다) - 페이지 헤더 클릭 → 캔버스가 그 페이지로 이동(ScrollToPage) - 데이터 배선 배지 — 값이 조회·데이터소스에서 오는 컨트롤 표시 - 검색칩에 테두리(라이트에서 B.Input=B.Panel 이라 경계 대비가 1.00:1 이었다) 이식하지 않은 것: 컨트롤 행 인라인 개명. [200] 이 고치는 값은 표시 전용 별칭이지만 SheetMe 의 컨트롤 이름은 외부 참조 키라 목록에서 무심코 고치면 배선이 끊긴다. 더블클릭은 요청대로 캔버스 이동을 유지하고, 그룹 행에서만 이름 편집으로 간다. 동반 수정: 눈·자물쇠 토글에 되돌리기 지점 추가(400ms 코얼레스), 그룹 이름 비우기 경로가 스냅숏 없이 지워지던 것, 숨김 흐리기를 행 전체가 아니라 이름 줄에만. 편집 스모크의 레이어 검사 19 → 42건. 회귀: 테스트 124/124, 편집 스모크 실패 0, DB 왕복 1,271건 diff 0/예외 0, 종이 렌더 P062 바이트 동일. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
727 lines
44 KiB
C#
727 lines
44 KiB
C#
using System.IO;
|
|
using System.Windows;
|
|
using SheetMe.Core.Serialization;
|
|
using SheetMe.Designer.DataBusiness;
|
|
using SheetMe.Designer.Services;
|
|
using SheetMe.Designer.ViewModels;
|
|
using SheetMe.Designer.ViewModels.Controls;
|
|
|
|
namespace SheetMe.Designer.Diagnostics;
|
|
|
|
/// <summary>
|
|
/// 편집 코어 자동 검증(--edit-smoke) — 실제 마우스 없이 InteractionController 에 포인터 시퀀스를 주입해
|
|
/// 선택/이동/리사이즈/마퀴/복사/붙여넣기/삭제/Undo/저장왕복을 검사한다.
|
|
/// </summary>
|
|
public static class EditSmoke
|
|
{
|
|
#region Methods
|
|
/// <summary>스모크 실행 — 결과를 리포트 파일에 기록, 실패 있으면 1 반환</summary>
|
|
public static int Run(string reportPath)
|
|
{
|
|
var lines = new List<string>();
|
|
var failed = 0;
|
|
|
|
void Check(string name, bool condition, string? detail = null)
|
|
{
|
|
if (!condition)
|
|
{
|
|
failed++;
|
|
}
|
|
lines.Add($"{(condition ? "PASS" : "FAIL")} {name}{(condition || detail is null ? "" : " — " + detail)}");
|
|
}
|
|
|
|
try
|
|
{
|
|
var business = new FormDesignDataBusiness();
|
|
var serializer = new LegacyXmlSerializer();
|
|
var designer = new DesignerViewModel(business.CreateNew());
|
|
var none = new PointerContext(false, false, false, 1);
|
|
var alt = new PointerContext(false, false, true, 1);
|
|
var ctrl = new PointerContext(true, false, false, 1);
|
|
|
|
ControlViewModel Find(string type)
|
|
=> designer.Pages[0].Controls.First(c => c.Type == type);
|
|
|
|
// 1) 팔레트 배치
|
|
designer.AddControlAt("TextBox", new Point(100, 100));
|
|
designer.AddControlAt("Label", new Point(400, 60));
|
|
designer.AddControlAt("CheckBox", new Point(200, 260));
|
|
Check("배치: VM/모델 3개", designer.Pages[0].Controls.Count == 3
|
|
&& designer.Document.Pages[0].Controls.Count == 3);
|
|
Check("배치: 신규가 선택됨", designer.Selection.Items.Count == 1);
|
|
|
|
// 2) 클릭 선택 — 컨트롤 중심(핸들 반경 밖)
|
|
var textBox = Find("TextBox");
|
|
var clickPoint = new Point(textBox.X + textBox.Width / 2, textBox.Y + textBox.Height / 2);
|
|
designer.Interaction.PointerDown(clickPoint, none);
|
|
designer.Interaction.PointerUp(clickPoint, none);
|
|
Check("클릭 선택: Primary=TextBox", designer.Selection.Primary == textBox);
|
|
|
|
// 3) 드래그 이동(Alt=자유 이동 — 결정적 검증)
|
|
var origX = textBox.X;
|
|
var origY = textBox.Y;
|
|
designer.Interaction.PointerDown(clickPoint, none);
|
|
designer.Interaction.PointerMove(new Point(clickPoint.X + 33, clickPoint.Y + 47), alt);
|
|
designer.Interaction.PointerUp(new Point(clickPoint.X + 33, clickPoint.Y + 47), alt);
|
|
Check("드래그 이동 +33,+47", textBox.X == origX + 33 && textBox.Y == origY + 47,
|
|
$"실제 ({textBox.X},{textBox.Y}) 기대 ({origX + 33},{origY + 47})");
|
|
|
|
// 4) Undo → 위치 원복(VM 재생성되므로 재조회)
|
|
designer.Undo.Undo();
|
|
var textBoxAfterUndo = Find("TextBox");
|
|
Check("Undo: 위치 원복", textBoxAfterUndo.X == origX && textBoxAfterUndo.Y == origY,
|
|
$"실제 ({textBoxAfterUndo.X},{textBoxAfterUndo.Y})");
|
|
|
|
// 5) Redo → 이동 재적용
|
|
designer.Undo.Redo();
|
|
var textBoxAfterRedo = Find("TextBox");
|
|
Check("Redo: 이동 재적용", textBoxAfterRedo.X == origX + 33 && textBoxAfterRedo.Y == origY + 47);
|
|
|
|
// 6) 리사이즈 — SE 핸들(4번) 드래그
|
|
var target = Find("TextBox");
|
|
designer.Selection.SetSingle(target);
|
|
var bbox = designer.SelectionWorldBounds()!.Value;
|
|
var seHandle = new Point(bbox.Right, bbox.Bottom);
|
|
var origW = target.Width;
|
|
var origH = target.Height;
|
|
designer.Interaction.PointerDown(seHandle, none);
|
|
designer.Interaction.PointerMove(new Point(seHandle.X + 40, seHandle.Y + 18), alt);
|
|
designer.Interaction.PointerUp(new Point(seHandle.X + 40, seHandle.Y + 18), alt);
|
|
Check("리사이즈 +40,+18", target.Width == origW + 40 && target.Height == origH + 18,
|
|
$"실제 ({target.Width},{target.Height}) 기대 ({origW + 40},{origH + 18})");
|
|
|
|
// 7) 마퀴 다중선택 — 전체를 덮는 사각형
|
|
designer.Selection.Clear();
|
|
designer.Interaction.PointerDown(new Point(1, 1), none);
|
|
designer.Interaction.PointerMove(new Point(700, 700), none);
|
|
designer.Interaction.PointerUp(new Point(700, 700), none);
|
|
Check("마퀴: 3개 선택", designer.Selection.Items.Count == 3,
|
|
$"실제 {designer.Selection.Items.Count}");
|
|
|
|
// 8) Ctrl+클릭 토글 해제
|
|
var label = Find("Label");
|
|
var labelPoint = new Point(label.X + 3, label.Y + 3);
|
|
designer.Interaction.PointerDown(labelPoint, ctrl);
|
|
designer.Interaction.PointerUp(labelPoint, ctrl);
|
|
Check("Ctrl+클릭: 토글 해제 → 2개", designer.Selection.Items.Count == 2);
|
|
|
|
// 9) 복사/붙여넣기
|
|
designer.Selection.SetSingle(Find("CheckBox"));
|
|
designer.CopySelection();
|
|
designer.Paste();
|
|
Check("붙여넣기: 4개 + 새 Id", designer.Pages[0].Controls.Count == 4
|
|
&& designer.Pages[0].Controls.Count(c => c.Type == "CheckBox") == 2
|
|
&& designer.Pages[0].Controls.Where(c => c.Type == "CheckBox").Select(c => c.Id).Distinct().Count() == 2);
|
|
|
|
// 10) 삭제
|
|
designer.DeleteSelection();
|
|
Check("삭제: 3개", designer.Pages[0].Controls.Count == 3);
|
|
|
|
// 11) z-order — TextBox 맨 뒤로 (모델 마지막 = VM 첫번째)
|
|
designer.Selection.SetSingle(Find("TextBox"));
|
|
designer.ReorderSelection(toFront: false);
|
|
Check("맨 뒤로: 모델 끝/VM 처음",
|
|
designer.Document.Pages[0].Controls[^1].Type == "TextBox"
|
|
&& designer.Pages[0].Controls[0].Type == "TextBox");
|
|
|
|
// 12) 넛지
|
|
var nudgeTarget = Find("Label");
|
|
designer.Selection.SetSingle(nudgeTarget);
|
|
var beforeNudgeX = nudgeTarget.X;
|
|
designer.NudgeSelection(1, 0);
|
|
designer.NudgeSelection(1, 0);
|
|
Check("넛지 +2", nudgeTarget.X == beforeNudgeX + 2);
|
|
|
|
// 13) 저장 → 재로드 → 재직렬화 동일(왕복 안정)
|
|
var tempPath = Path.Combine(Path.GetTempPath(), "sheetme_editsmoke.xml");
|
|
business.SaveXmlFile(designer.Document, tempPath);
|
|
var reloaded = business.OpenXmlFile(tempPath);
|
|
var xmlA = serializer.Write(designer.Document);
|
|
var xmlB = serializer.Write(reloaded);
|
|
Check("저장/재로드 왕복 동일", xmlA == xmlB);
|
|
|
|
// 14) 신규 컨트롤 XML 에 레거시 필수 요소 포함
|
|
Check("XML: AQN/Location/Name 포함",
|
|
xmlA.Contains("M.EMR.UserControl.TextBox,") && xmlA.Contains("<Property name=\"Location\">")
|
|
&& xmlA.Contains("<Property name=\"Name\">"));
|
|
|
|
// 15) 인스펙터 — 선택 시 행 구성 + Text 커밋
|
|
var inspectorTarget = Find("Label");
|
|
designer.Selection.SetSingle(inspectorTarget);
|
|
Check("인스펙터: 행 생성", designer.Inspector.Rows.Count > 5,
|
|
$"행 {designer.Inspector.Rows.Count}");
|
|
var textRow = designer.Inspector.Rows
|
|
.OfType<ViewModels.Inspector.MultilineTextRowViewModel>()
|
|
.FirstOrDefault(r => r.Label == "텍스트");
|
|
Check("인스펙터: 텍스트 행 존재", textRow is not null);
|
|
if (textRow is not null)
|
|
{
|
|
textRow.ValueText = "수정된 라벨";
|
|
Check("인스펙터: Text 커밋 → 모델 반영",
|
|
inspectorTarget.Model.Props.GetText("Text") == "수정된 라벨"
|
|
&& inspectorTarget.Text == "수정된 라벨");
|
|
Check("인스펙터: 커밋이 Undo 스텝", designer.Undo.CanUndo);
|
|
}
|
|
|
|
// 16) 인스펙터 — 이름 변경
|
|
var nameRow = designer.Inspector.Rows
|
|
.OfType<ViewModels.Inspector.TextRowViewModel>()
|
|
.FirstOrDefault(r => r.Label == "이름");
|
|
if (nameRow is not null)
|
|
{
|
|
nameRow.ValueText = "lblTitle";
|
|
Check("인스펙터: 개명 반영", inspectorTarget.Id == "lblTitle");
|
|
}
|
|
|
|
// 17) 페이지 추가/삭제 + 용지 크기
|
|
designer.AddPage();
|
|
Check("페이지 추가: 2페이지", designer.Pages.Count == 2
|
|
&& designer.Document.Pages.Count == 2);
|
|
designer.SelectedPage = designer.Pages[1];
|
|
designer.ApplyPaperSize(794, 1123);
|
|
Check("용지 크기 변경(A4)", designer.Pages[1].WidthDip == 794 && designer.Pages[1].HeightDip == 1123);
|
|
designer.RemoveSelectedPageForSmoke();
|
|
Check("페이지 삭제: 1페이지", designer.Pages.Count == 1);
|
|
|
|
// 18) 컨테이너 드롭 — Panel 배치 후 내부에 TextBox 드롭
|
|
designer.AddControlAt("Panel", new Point(400, 400));
|
|
var panel = designer.Pages[0].Controls.OfType<PanelViewModel>().First();
|
|
designer.AddControlAt("TextBox", new Point(panel.X + 30, panel.Y + 30));
|
|
Check("컨테이너 드롭: 자식 추가", panel.Model.Children.Count == 1
|
|
&& panel.Children.Count == 1
|
|
&& panel.Model.Children[0].Type == "TextBox");
|
|
|
|
// 19) 컨테이너 자식 직접 편집 — 히트테스트/이동/삭제
|
|
var panelVm = designer.Pages[0].Controls.OfType<PanelViewModel>().First();
|
|
var child = panelVm.Children[0];
|
|
var childWorld = designer.WorldBoundsOf(child);
|
|
var childHit = designer.HitTestControl(new Point(childWorld.X + childWorld.Width / 2, childWorld.Y + childWorld.Height / 2));
|
|
Check("자식 히트테스트", childHit == child, $"실제 {childHit?.Id}");
|
|
|
|
designer.Selection.SetSingle(child);
|
|
var childOrigX = child.X;
|
|
var center = new Point(childWorld.X + childWorld.Width / 2, childWorld.Y + childWorld.Height / 2);
|
|
designer.Interaction.PointerDown(center, none);
|
|
designer.Interaction.PointerMove(new Point(center.X + 15, center.Y + 10), alt);
|
|
designer.Interaction.PointerUp(new Point(center.X + 15, center.Y + 10), alt);
|
|
Check("자식 이동(+15,+10 상대좌표)", child.X == childOrigX + 15,
|
|
$"실제 X={child.X} 기대 {childOrigX + 15}");
|
|
|
|
designer.Selection.SetSingle(child);
|
|
designer.DeleteSelection();
|
|
Check("자식 삭제", panelVm.Children.Count == 0 && panelVm.Model.Children.Count == 0);
|
|
|
|
// 20) 그룹/해제 — 그룹 단위 클릭 선택
|
|
designer.Selection.Set(new[] { Find("TextBox"), Find("Label") });
|
|
designer.GroupSelection();
|
|
designer.Selection.Clear();
|
|
var member = Find("TextBox");
|
|
var memberPoint = new Point(member.X + member.Width / 2, member.Y + member.Height / 2);
|
|
designer.Interaction.PointerDown(memberPoint, none);
|
|
designer.Interaction.PointerUp(memberPoint, none);
|
|
Check("그룹 클릭 → 그룹 전체 선택", designer.Selection.Items.Count == 2,
|
|
$"실제 {designer.Selection.Items.Count}");
|
|
designer.UngroupSelection();
|
|
designer.Selection.Clear();
|
|
designer.Interaction.PointerDown(memberPoint, none);
|
|
designer.Interaction.PointerUp(memberPoint, none);
|
|
Check("그룹 해제 후 단일 선택", designer.Selection.Items.Count == 1);
|
|
|
|
// 20-1) 인라인 텍스트 편집 — 더블클릭 요청 이벤트 + 커밋
|
|
var inlineTarget = Find("Label");
|
|
ControlViewModel? inlineRequested = null;
|
|
designer.InlineEditRequested += vm => inlineRequested = vm;
|
|
var inlineWorld = designer.WorldBoundsOf(inlineTarget);
|
|
var inlineCenter = new Point(inlineWorld.X + inlineWorld.Width / 2, inlineWorld.Y + inlineWorld.Height / 2);
|
|
designer.Interaction.PointerDown(inlineCenter, new PointerContext(false, false, false, 2));
|
|
designer.Interaction.PointerUp(inlineCenter, new PointerContext(false, false, false, 2));
|
|
Check("더블클릭 → 인라인 편집 요청", inlineRequested == inlineTarget,
|
|
$"실제 {inlineRequested?.Id ?? "null"}");
|
|
designer.CommitInlineText(inlineTarget, "인라인 수정됨");
|
|
Check("인라인 커밋 → 모델/Undo 반영",
|
|
inlineTarget.Text == "인라인 수정됨"
|
|
&& inlineTarget.Model.Props.GetText("Text") == "인라인 수정됨"
|
|
&& designer.Undo.CanUndo);
|
|
|
|
// 20-2) 폰트 일괄 변경 — 집계 + 적용
|
|
var usage = designer.CollectFontUsage();
|
|
Check("폰트 집계: 조합 존재", usage.Count >= 2, $"실제 {usage.Count}");
|
|
var applied = designer.ApplyFontBulk(usage, new Core.Serialization.LegacyFont
|
|
{
|
|
Family = "돋움",
|
|
SizePt = 12,
|
|
Bold = true,
|
|
});
|
|
Check("폰트 일괄 적용: 대상 수", applied >= 2, $"실제 {applied}");
|
|
var fontTarget = Find("Label");
|
|
Check("폰트 일괄 적용: 모델 반영",
|
|
fontTarget.Model.Props.GetText("Font") == "돋움, 12pt, style=Bold"
|
|
&& fontTarget.EffectiveFont.Family == "돋움" && fontTarget.EffectiveFont.Bold);
|
|
designer.Undo.Undo();
|
|
Check("폰트 일괄 적용: Undo 1스텝 원복",
|
|
Find("Label").Model.Props.GetText("Font") != "돋움, 12pt, style=Bold");
|
|
|
|
// 20-2-1) 레이어 목록 — 그룹 폴더/계층 평탄화/검색·타입 필터/그룹 위생.
|
|
// 뒤 단계들이 designer 의 컨트롤 개수를 세므로 별도 문서에서 검사한다.
|
|
{
|
|
var lay = new DesignerViewModel(business.CreateNew());
|
|
lay.AddControlAt("TextBox", new Point(100, 100));
|
|
lay.AddControlAt("Label", new Point(100, 200));
|
|
lay.AddControlAt("CheckBox", new Point(100, 300));
|
|
ControlViewModel Pick(string type) => lay.Pages[0].Controls.First(c => c.Type == type);
|
|
|
|
lay.Selection.Set(new[] { Pick("TextBox"), Pick("Label") });
|
|
lay.GroupSelection();
|
|
|
|
var groupRow = lay.LayerRows.OfType<LayerGroupViewModel>().FirstOrDefault();
|
|
Check("레이어: 그룹 폴더 행 1개", lay.LayerRows.OfType<LayerGroupViewModel>().Count() == 1);
|
|
Check("레이어: 폴더 멤버 2 + 기본 이름",
|
|
groupRow is { Members.Count: 2 } && groupRow.Name.StartsWith("그룹"),
|
|
$"실제 {groupRow?.Members.Count} / {groupRow?.Name}");
|
|
|
|
// 아웃라인은 페이지 헤더로 시작하고 '페이지 추가' 행으로 끝난다
|
|
Check("레이어: 첫 행은 페이지 헤더", lay.LayerRows.FirstOrDefault() is LayerPageViewModel);
|
|
Check("레이어: 마지막 행은 페이지 추가", lay.LayerRows.LastOrDefault() is LayerAddPageViewModel);
|
|
Check("레이어: 페이지 헤더는 선택 대상 아님",
|
|
lay.LayerRows.OfType<ILayerRow>().Where(r => r is LayerPageViewModel or LayerAddPageViewModel)
|
|
.All(r => !r.IsSelectableRow));
|
|
|
|
// 폴더 바로 뒤에 멤버가 들여쓰기된 채 이어져야 한다(평탄화 시퀀스)
|
|
var folderIndex = lay.LayerRows.IndexOf(groupRow!);
|
|
Check("레이어: 폴더 뒤에 멤버가 들여쓰여 이어짐",
|
|
folderIndex >= 0
|
|
&& lay.LayerRows.Skip(folderIndex + 1).Take(2)
|
|
.OfType<ControlViewModel>().Count(c => c.LayerIndent == 32) == 2);
|
|
Check("레이어: 페이지 직속 컨트롤 들여쓰기 12",
|
|
lay.LayerRows.OfType<ControlViewModel>()
|
|
.Where(c => c.Model.GroupId is null && c.Parent is null)
|
|
.All(c => c.LayerIndent == 12));
|
|
|
|
// 목록 맨 위 = z-order 최상위 (모델 리스트 index 0)
|
|
var topControl = lay.LayerRows.OfType<ControlViewModel>().First();
|
|
Check("레이어: 목록 맨 위가 z-order 최상위",
|
|
ReferenceEquals(lay.Document.Pages[0].Controls[0], topControl.Model),
|
|
$"실제 맨 위 {topControl.Id} / 모델 첫째 {lay.Document.Pages[0].Controls[0].Id}");
|
|
|
|
// 접으면 멤버 행이 빠지고, 펴면 되돌아온다
|
|
var expandedCount = lay.LayerRows.Count;
|
|
groupRow!.IsExpanded = false;
|
|
lay.RebuildLayerRows();
|
|
Check("레이어: 접으면 멤버 행 제거", lay.LayerRows.Count == expandedCount - 2,
|
|
$"실제 {lay.LayerRows.Count} 기대 {expandedCount - 2}");
|
|
groupRow = lay.LayerRows.OfType<LayerGroupViewModel>().First();
|
|
groupRow.IsExpanded = true;
|
|
lay.RebuildLayerRows();
|
|
Check("레이어: 펴면 멤버 행 복귀", lay.LayerRows.Count == expandedCount);
|
|
|
|
// 그룹 일괄 숨김이 전 멤버에 퍼진다
|
|
groupRow = lay.LayerRows.OfType<LayerGroupViewModel>().First();
|
|
groupRow.IsHiddenFlag = true;
|
|
Check("레이어: 그룹 숨김 → 전 멤버", groupRow.Members.All(m => m.IsHiddenFlag));
|
|
groupRow.IsHiddenFlag = false;
|
|
Check("레이어: 그룹 표시 복귀", groupRow.Members.All(m => !m.IsHiddenFlag));
|
|
|
|
// 그룹 이름은 문서가 소유 — 비우면 기본값으로 되돌아간다
|
|
groupRow.Name = "머리글 묶음";
|
|
Check("레이어: 그룹 개명", lay.GroupNameOf(groupRow.GroupId) == "머리글 묶음");
|
|
groupRow.Name = string.Empty;
|
|
Check("레이어: 이름 비우면 기본값", lay.GroupNameOf(groupRow.GroupId) == "그룹");
|
|
|
|
// 컨테이너 자식이 계층으로 보이고, 접으면 목록에서 빠진다
|
|
lay.AddControlAt("Panel", new Point(300, 600));
|
|
var layerPanel = lay.Pages[0].Controls.OfType<PanelViewModel>().First();
|
|
lay.AddControlAt("Label", new Point(layerPanel.X + 20, layerPanel.Y + 20));
|
|
lay.RebuildLayerRows();
|
|
Check("레이어: 컨테이너 자식이 목록에 나옴",
|
|
lay.LayerRows.OfType<ControlViewModel>().Any(c => ReferenceEquals(c.Parent, layerPanel)));
|
|
Check("레이어: 자식 들여쓰기 = 부모+16",
|
|
lay.LayerRows.OfType<ControlViewModel>()
|
|
.Where(c => ReferenceEquals(c.Parent, layerPanel))
|
|
.All(c => c.LayerIndent == layerPanel.LayerIndent + 16));
|
|
var withChild = lay.LayerRows.Count;
|
|
layerPanel.IsLayerExpanded = false;
|
|
lay.RebuildLayerRows();
|
|
Check("레이어: 컨테이너 접으면 자식 제거", lay.LayerRows.Count == withChild - 1);
|
|
layerPanel.IsLayerExpanded = true;
|
|
lay.RebuildLayerRows();
|
|
|
|
// 검색은 자손이 걸리면 조상도 남긴다 — 부모가 빠지면 자식이 통째로 사라진다
|
|
var childId = layerPanel.Children[0].Id;
|
|
lay.LayerFilter = childId;
|
|
Check("레이어: 검색 시 조상 보존",
|
|
lay.LayerRows.Contains(layerPanel)
|
|
&& lay.LayerRows.OfType<ControlViewModel>().Any(c => c.Id == childId));
|
|
lay.LayerFilter = "존재하지않는이름____";
|
|
Check("레이어: 0건이면 빈 목록 안내", lay.IsLayerListEmpty && lay.LayerEmptyText.Contains("검색"));
|
|
lay.LayerFilter = string.Empty;
|
|
|
|
// 타입 필터
|
|
lay.SetLayerTypeFilter(new[] { "CheckBox" });
|
|
Check("레이어: 타입 필터 적용",
|
|
lay.HasLayerTypeFilter
|
|
&& lay.LayerRows.OfType<ControlViewModel>().Any()
|
|
&& lay.LayerRows.OfType<ControlViewModel>().All(c => c.Type == "CheckBox"));
|
|
lay.SetLayerTypeFilter(Array.Empty<string>());
|
|
Check("레이어: 타입 필터 해제", !lay.HasLayerTypeFilter);
|
|
|
|
// 붙여넣은 복제본은 원본 그룹에 끼지 않는다(GroupId 재매핑)
|
|
groupRow = lay.LayerRows.OfType<LayerGroupViewModel>().First();
|
|
var sourceGroupId = groupRow.Members[0].Model.GroupId;
|
|
lay.Selection.SetSingle(groupRow.Members[0]);
|
|
lay.CopySelection();
|
|
lay.Paste();
|
|
Check("레이어: 붙여넣기가 원본 그룹에 끼지 않음",
|
|
lay.Selection.Items[0].Model.GroupId != sourceGroupId,
|
|
$"원본 {sourceGroupId} 복제 {lay.Selection.Items[0].Model.GroupId}");
|
|
|
|
// 멤버가 1개로 줄면 그룹은 자동 해제되고 이름도 정리된다
|
|
groupRow = lay.LayerRows.OfType<LayerGroupViewModel>().First();
|
|
lay.Selection.SetSingle(groupRow.Members[1]);
|
|
lay.DeleteSelection();
|
|
Check("레이어: 멤버 1개 남으면 그룹 자동 해제",
|
|
!lay.LayerRows.OfType<LayerGroupViewModel>().Any()
|
|
&& lay.Pages[0].Controls.All(c => c.Model.GroupId is null)
|
|
&& lay.Document.GroupNames.Count == 0);
|
|
|
|
// 그룹은 편집 전용 상태다 — 저장 XML 에 흔적이 남으면 왕복 불변식이 깨진다
|
|
lay.Selection.Set(lay.Pages[0].Controls.Take(2).ToList());
|
|
lay.GroupSelection();
|
|
Check("레이어: 그룹은 XML 에 나가지 않음",
|
|
!serializer.Write(lay.Document).Contains("GroupId", StringComparison.OrdinalIgnoreCase));
|
|
|
|
// 강조 3상태 — 그룹 전체 선택이면 폴더=Blue, 멤버=Navy 이고 연속 블록으로 이어진다
|
|
groupRow = lay.LayerRows.OfType<LayerGroupViewModel>().First();
|
|
lay.Selection.Set(groupRow.Members.ToList(), groupRow.Members[0]);
|
|
lay.UpdateLayerHighlight();
|
|
Check("레이어: 그룹 전원 선택 → 폴더 Blue", groupRow.Highlight == LayerHighlight.Blue,
|
|
$"실제 {groupRow.Highlight}");
|
|
Check("레이어: 그룹 전원 선택 → 멤버 Navy",
|
|
groupRow.Members.All(m => m.Highlight == LayerHighlight.Navy));
|
|
Check("레이어: 폴더와 멤버가 이어진 블록",
|
|
groupRow.MergeBottom && groupRow.Members[0].MergeTop);
|
|
|
|
lay.Selection.SetSingle(groupRow.Members[0]);
|
|
lay.UpdateLayerHighlight();
|
|
Check("레이어: 멤버 하나만 선택 → 그 행만 Blue, 폴더 무강조",
|
|
groupRow.Members[0].Highlight == LayerHighlight.Blue
|
|
&& groupRow.Highlight == LayerHighlight.None);
|
|
|
|
// 강조 계산이 페이지 헤더에서 끊긴다(블록이 페이지를 넘지 않는다)
|
|
Check("레이어: 페이지 헤더는 항상 무강조",
|
|
lay.LayerRows.OfType<LayerPageViewModel>().All(p => p.Highlight == LayerHighlight.None));
|
|
|
|
// 페이지 복제 — 컨트롤 이름 유일성과 그룹 분리가 지켜져야 한다
|
|
var beforePages = lay.Pages.Count;
|
|
var beforeIds = lay.Pages[0].Controls.Select(c => c.Id).ToHashSet(StringComparer.Ordinal);
|
|
var copied = lay.DuplicatePage(lay.Pages[0]);
|
|
Check("레이어: 페이지 복제 — 장수 +1", lay.Pages.Count == beforePages + 1 && copied is not null);
|
|
if (copied is not null)
|
|
{
|
|
Check("레이어: 페이지 복제 — 컨트롤 수 동일",
|
|
copied.Controls.Count == lay.Pages[0].Controls.Count);
|
|
Check("레이어: 페이지 복제 — 이름이 전부 새로 발급됨",
|
|
copied.Controls.All(c => !beforeIds.Contains(c.Id)));
|
|
Check("레이어: 페이지 복제 — 그룹이 원본과 분리됨",
|
|
copied.Controls.Where(c => c.Model.GroupId is not null)
|
|
.All(c => lay.Pages[0].Controls.All(o => o.Model.GroupId != c.Model.GroupId)));
|
|
}
|
|
Check("레이어: 전 페이지가 아웃라인에 나옴",
|
|
lay.LayerRows.OfType<LayerPageViewModel>().Count() == lay.Pages.Count);
|
|
|
|
// 페이지 접기 — 그 페이지 컨트롤 행만 빠진다
|
|
var pageHeader = lay.LayerRows.OfType<LayerPageViewModel>().First();
|
|
var beforeCollapse = lay.LayerRows.Count;
|
|
pageHeader.IsExpanded = false;
|
|
lay.RebuildLayerRows();
|
|
Check("레이어: 페이지 접기 — 행 감소", lay.LayerRows.Count < beforeCollapse);
|
|
Check("레이어: 페이지 접어도 헤더는 남음",
|
|
lay.LayerRows.OfType<LayerPageViewModel>().Count() == lay.Pages.Count);
|
|
lay.LayerRows.OfType<LayerPageViewModel>().First().IsExpanded = true;
|
|
lay.RebuildLayerRows();
|
|
Check("레이어: 페이지 펴기 — 행 복귀", lay.LayerRows.Count == beforeCollapse);
|
|
|
|
// 드래그 재정렬 — 목록에서 아래 행을 맨 위 행 '위'로 옮기면 z-order 최상위가 된다
|
|
var order = lay.LayerRows.OfType<ControlViewModel>()
|
|
.Where(c => c.Parent is null && ReferenceEquals(lay.PageOf(c), lay.Pages[0])).ToList();
|
|
if (order.Count >= 2)
|
|
{
|
|
var mover = order[^1];
|
|
var anchor = order[0];
|
|
lay.MoveControlNextTo(mover, anchor, above: true);
|
|
Check("레이어: 드래그로 맨 위에 놓으면 z-order 최상위",
|
|
ReferenceEquals(lay.Pages[0].Model.Controls[0], mover.Model),
|
|
$"실제 {lay.Pages[0].Model.Controls[0].Id} 기대 {mover.Id}");
|
|
Check("레이어: 재정렬이 되돌리기 스텝", lay.Undo.CanUndo);
|
|
}
|
|
|
|
// 눈·자물쇠도 되돌릴 수 있어야 한다
|
|
var flagTarget = lay.LayerRows.OfType<ControlViewModel>().First();
|
|
lay.Undo.Snapshot();
|
|
var depthBefore = lay.Undo.CanUndo;
|
|
flagTarget.IsLockedFlag = true;
|
|
lay.Undo.Undo();
|
|
Check("레이어: 잠금 토글이 Undo 로 원복",
|
|
lay.Pages.SelectMany(p => p.Controls).All(c => !c.Model.Locked) && depthBefore);
|
|
|
|
// 데이터 배선 배지
|
|
var badgeTarget = lay.Pages[0].Controls[0];
|
|
Check("레이어: 배선 없는 컨트롤은 배지 없음", !badgeTarget.HasDataBinding);
|
|
badgeTarget.Model.Props.SetText("DataTableField", "DT.Item(\"ChtNum\")");
|
|
Check("레이어: DataTableField 있으면 배지", badgeTarget.HasDataBinding);
|
|
badgeTarget.Model.Props.SetText("DataTableField", string.Empty);
|
|
}
|
|
|
|
// 20-3) 문서 간 복사/붙여넣기 — A 서식에서 복사 → B 서식에 붙여넣기(클립보드 공유)
|
|
var designerB = new DesignerViewModel(business.CreateNew());
|
|
designer.Selection.SetSingle(Find("TextBox"));
|
|
designer.CopySelection();
|
|
var beforePasteB = designerB.Pages[0].Controls.Count;
|
|
designerB.Paste();
|
|
Check("문서 간 붙여넣기: B 문서에 추가", designerB.Pages[0].Controls.Count == beforePasteB + 1
|
|
&& designerB.Pages[0].Controls.Any(c => c.Type == "TextBox"),
|
|
$"실제 {designerB.Pages[0].Controls.Count}");
|
|
Check("문서 간 붙여넣기: A 문서 불변", designer.Pages[0].Controls.Count(c => c.Type == "TextBox") == 1);
|
|
|
|
// 20-4) 배치 명령 — 같은 크기/간격 균등/페이지 가운데/잘라내기
|
|
designer.AddControlAt("Label", new Point(100, 500));
|
|
designer.AddControlAt("Label", new Point(300, 520));
|
|
designer.AddControlAt("Label", new Point(560, 540));
|
|
var arrangeTargets = designer.Pages[0].Controls.Where(c => c.Y >= 480).ToList();
|
|
designer.Selection.Set(arrangeTargets, arrangeTargets[0]);
|
|
|
|
designer.SizeToControl("Both");
|
|
Check("같은 크기로: 기준에 통일",
|
|
arrangeTargets.All(c => c.Width == arrangeTargets[0].Width && c.Height == arrangeTargets[0].Height));
|
|
|
|
designer.AdjustSpacing("HorizEqual");
|
|
var sorted = arrangeTargets.OrderBy(c => c.X).ToList();
|
|
var gap1 = sorted[1].X - (sorted[0].X + sorted[0].Width);
|
|
var gap2 = sorted[2].X - (sorted[1].X + sorted[1].Width);
|
|
Check("가로 간격 균등", Math.Abs(gap1 - gap2) <= 1, $"gap1={gap1} gap2={gap2}");
|
|
|
|
designer.AdjustSpacing("HorizConcat");
|
|
sorted = arrangeTargets.OrderBy(c => c.X).ToList();
|
|
Check("가로 붙이기: 간격 0", sorted[1].X == sorted[0].X + sorted[0].Width
|
|
&& sorted[2].X == sorted[1].X + sorted[1].Width);
|
|
|
|
designer.CenterInPage("H");
|
|
var bboxAfterCenter = designer.SelectionWorldBounds()!.Value;
|
|
var pageW = designer.Pages[0].WidthDip;
|
|
Check("페이지 가로 가운데", Math.Abs((bboxAfterCenter.X + bboxAfterCenter.Width / 2) - pageW / 2) <= 1,
|
|
$"중심 {bboxAfterCenter.X + bboxAfterCenter.Width / 2} 기대 {pageW / 2}");
|
|
|
|
var beforeCut = designer.Pages[0].Controls.Count;
|
|
designer.Selection.Set(new[] { arrangeTargets[0] }, arrangeTargets[0]);
|
|
designer.CutSelection();
|
|
Check("잘라내기: 삭제됨", designer.Pages[0].Controls.Count == beforeCut - 1);
|
|
designer.Paste();
|
|
Check("잘라내기 → 붙여넣기 복원", designer.Pages[0].Controls.Count == beforeCut);
|
|
|
|
// 남은 배치 테스트 컨트롤 정리
|
|
designer.Selection.Set(designer.Pages[0].Controls.Where(c => c.Y >= 480).ToList());
|
|
designer.DeleteSelection();
|
|
|
|
// 20-5) 탭순서 편집 — 모드 진입 → 역순 클릭 → 완료 적용(레거시 규약 0,100,200…)
|
|
designer.AddControlAt("TextBox", new Point(100, 620));
|
|
designer.AddControlAt("TextBox", new Point(300, 620));
|
|
designer.AddControlAt("TextBox", new Point(500, 620));
|
|
var tabTargets = designer.Pages[0].Controls
|
|
.Where(c => c.Type == "TextBox" && c.Y >= 600)
|
|
.OrderBy(c => c.X)
|
|
.ToList();
|
|
|
|
designer.ToggleTabOrderMode(); // 시작
|
|
Check("탭순서: 모드 진입 + 배지 생성", designer.IsTabOrderMode
|
|
&& designer.Overlay.TabBadges.Count >= 3,
|
|
$"배지 {designer.Overlay.TabBadges.Count}");
|
|
|
|
// 오른쪽→왼쪽 역순으로 클릭(입력 순서 지정)
|
|
for (var i = tabTargets.Count - 1; i >= 0; i--)
|
|
{
|
|
var world = designer.WorldBoundsOf(tabTargets[i]);
|
|
designer.Interaction.PointerDown(new Point(world.X + world.Width / 2, world.Y + world.Height / 2), none);
|
|
designer.Interaction.PointerUp(new Point(world.X + world.Width / 2, world.Y + world.Height / 2), none);
|
|
}
|
|
Check("탭순서: 배지 순번 표시", designer.Overlay.TabBadges.Count(b => b.IsAssigned) == 3);
|
|
|
|
designer.ToggleTabOrderMode(); // 완료(적용)
|
|
Check("탭순서: 적용 — 클릭 순서 0/100/200", !designer.IsTabOrderMode
|
|
&& tabTargets[2].Model.Props.GetText("TabIndex") == "0"
|
|
&& tabTargets[1].Model.Props.GetText("TabIndex") == "100"
|
|
&& tabTargets[0].Model.Props.GetText("TabIndex") == "200",
|
|
$"실제 {tabTargets[2].Model.Props.GetText("TabIndex")}/{tabTargets[1].Model.Props.GetText("TabIndex")}/{tabTargets[0].Model.Props.GetText("TabIndex")}");
|
|
Check("탭순서: 미클릭 컨트롤 뒤 순번(999999+)",
|
|
Find("Label").Model.Props.GetText("TabIndex")?.StartsWith("1000") == true);
|
|
designer.Undo.Undo();
|
|
Check("탭순서: Undo 원복",
|
|
designer.Pages[0].Controls.First(c => c.Type == "TextBox" && c.Y >= 600).Model.Props.GetText("TabIndex") is null or "0");
|
|
|
|
// 정리
|
|
designer.Selection.Set(designer.Pages[0].Controls.Where(c => c.Y >= 600).ToList());
|
|
designer.DeleteSelection();
|
|
|
|
// 20-6) 전체 속성 그리드(고급) — 토글 → raw 편집 → 속성 추가
|
|
var advTarget = Find("TextBox");
|
|
designer.Selection.SetSingle(advTarget);
|
|
var toggleRow = designer.Inspector.Rows
|
|
.OfType<ViewModels.Inspector.ToggleAdvancedRowViewModel>().FirstOrDefault();
|
|
Check("고급: 토글 행 존재", toggleRow is not null);
|
|
toggleRow!.ToggleCommand!.Execute(null, EventArgs.Empty);
|
|
var advRows = designer.Inspector.Rows
|
|
.OfType<ViewModels.Inspector.TextRowViewModel>()
|
|
.Where(r => r.Label is "LocationOnBase" or "TabIndex" or "DisplaySequence")
|
|
.ToList();
|
|
Check("고급: raw 속성 행 노출", designer.Inspector.Rows
|
|
.OfType<ViewModels.Inspector.SectionRowViewModel>()
|
|
.Any(r => r.Label.StartsWith("전체 속성")));
|
|
|
|
var addRow = designer.Inspector.Rows
|
|
.OfType<ViewModels.Inspector.AddPropertyRowViewModel>().FirstOrDefault();
|
|
Check("고급: 속성 추가 행 존재", addRow is not null);
|
|
addRow!.KeyText = "EnterTabYon";
|
|
addRow.AddCommand!.Execute(null, EventArgs.Empty);
|
|
Check("고급: 속성 추가됨", advTarget.Model.Props.Contains("EnterTabYon"));
|
|
var enterTabRow = designer.Inspector.Rows
|
|
.OfType<ViewModels.Inspector.TextRowViewModel>()
|
|
.FirstOrDefault(r => r.Label == "EnterTabYon");
|
|
Check("고급: 추가 속성 행 노출", enterTabRow is not null);
|
|
enterTabRow!.ValueText = "True";
|
|
Check("고급: raw 값 커밋", advTarget.Model.Props.GetText("EnterTabYon") == "True");
|
|
|
|
// 21) JSON 왕복(부 포맷) — 편집 결과 유지
|
|
var jsonSerializer = new Core.Serialization.FormJsonSerializer();
|
|
var json = jsonSerializer.Write(designer.Document);
|
|
var fromJson = jsonSerializer.Read(json);
|
|
Check("JSON 왕복 — XML 동일", serializer.Write(fromJson) == serializer.Write(designer.Document));
|
|
|
|
// 22) Spread 판정 — 복제/이름변경 가드의 근거 술어(가드 자체는 MessageBox 를 띄우므로 무인 검증 불가)
|
|
var spreadElement = new Core.Models.ControlElement { Type = "Spread", Id = "Spread1" };
|
|
var panelWithSpread = new Core.Models.ControlElement { Type = "Panel", Id = "Panel9" };
|
|
panelWithSpread.Children.Add(new Core.Models.ControlElement { Type = "Label", Id = "L9" });
|
|
panelWithSpread.Children.Add(spreadElement.Clone());
|
|
var plainPanel = new Core.Models.ControlElement { Type = "Panel", Id = "Panel8" };
|
|
plainPanel.Children.Add(new Core.Models.ControlElement { Type = "Label", Id = "L8" });
|
|
|
|
Check("Spread 판정: 자신", DesignerViewModel.ContainsSpread(spreadElement));
|
|
Check("Spread 판정: 컨테이너 자손", DesignerViewModel.ContainsSpread(panelWithSpread));
|
|
Check("Spread 판정: 무관 컨트롤 false", !DesignerViewModel.ContainsSpread(plainPanel));
|
|
Check("Spread 판정: Placeholder 오탐 없음",
|
|
!DesignerViewModel.ContainsSpread(new Core.Models.ControlElement { Type = "Placeholder", Id = "P1" }));
|
|
|
|
// 붙여넣기 필터 — 표만 걸러내고 나머지는 통과시켜야 한다(전체 거부는 정상 작업까지 막는다)
|
|
var mixed = new[] { plainPanel, spreadElement, panelWithSpread,
|
|
new Core.Models.ControlElement { Type = "Label", Id = "L7" } };
|
|
var (pastable, blockedSpreads) = DesignerViewModel.FilterPastable(mixed);
|
|
Check("붙여넣기 필터: 표 2개 차단", blockedSpreads == 2, $"차단 {blockedSpreads}");
|
|
Check("붙여넣기 필터: 나머지 2개 통과", pastable.Count == 2, $"통과 {pastable.Count}");
|
|
Check("붙여넣기 필터: 통과분에 표 없음", pastable.All(e => !DesignerViewModel.ContainsSpread(e)));
|
|
Check("붙여넣기 필터: 표 없으면 전량 통과",
|
|
DesignerViewModel.FilterPastable(new[] { plainPanel }) is { BlockedSpreads: 0, Pastable.Count: 1 });
|
|
|
|
// 23) 쿼리 편집기 — XAML 로드/바인딩과 두 모드(SQL/상용구)를 실제로 생성해 확인
|
|
var sqlEditor = new Views.QueryEditorWindow("스모크", "SELECT 1 FROM DUAL");
|
|
var variableItems = (sqlEditor.FindName("VariableList") as System.Windows.Controls.ListBox)?
|
|
.ItemsSource?.Cast<object>().Count() ?? 0;
|
|
Check("쿼리 편집기: 치환 변수 목록 바인딩",
|
|
variableItems == Core.Catalog.LegacyQueryVariableCatalog.All.Count,
|
|
$"바인딩 {variableItems}건 / 카탈로그 {Core.Catalog.LegacyQueryVariableCatalog.All.Count}건");
|
|
Check("쿼리 편집기: 변수 패널 표시",
|
|
(sqlEditor.FindName("VariablePane") as UIElement)?.Visibility == Visibility.Visible);
|
|
sqlEditor.Close();
|
|
|
|
var wordEditor = new Views.QueryEditorWindow("상용구 수정", "문구", showVariables: false);
|
|
Check("상용구 모드: 변수 패널 숨김",
|
|
(wordEditor.FindName("VariablePane") as UIElement)?.Visibility == Visibility.Collapsed);
|
|
Check("상용구 모드: 제목에 '쿼리' 없음", !wordEditor.Title.Contains("쿼리"), wordEditor.Title);
|
|
wordEditor.Close();
|
|
|
|
// 24) 로그 마스킹 — 접속 문자열이 예외 메시지에 섞여 로그로 나가는 것을 막는 마지막 방어선
|
|
// 픽스처에는 절대 실제 자격증명을 쓰지 않는다 — 소스에 박히면 비밀 스캔이 영구히 걸리고,
|
|
// 저장소 자체가 유출 경로가 된다.
|
|
const string fakeUser = "SAMPLE_ACCOUNT";
|
|
const string fakePassword = "SAMPLE_SECRET_VALUE";
|
|
var secretish = $"ORA-12545: Data Source=(HOST=1.2.3.4);User Id={fakeUser};Password={fakePassword};";
|
|
var redacted = AppLog.Redact(secretish);
|
|
Check("마스킹: 비밀번호 제거", !redacted.Contains(fakePassword, StringComparison.Ordinal), redacted);
|
|
Check("마스킹: 계정 제거", !redacted.Contains(fakeUser, StringComparison.Ordinal), redacted);
|
|
Check("마스킹: 진단 문맥은 보존", redacted.Contains("ORA-12545", StringComparison.Ordinal), redacted);
|
|
Check("마스킹: 키 이름은 남김(형태 파악용)", redacted.Contains("Password=", StringComparison.Ordinal), redacted);
|
|
Check("마스킹: 빈 문자열 안전", AppLog.Redact(string.Empty) == string.Empty);
|
|
|
|
// LogManager 는 ConcurrentQueue 배치 기록이고 flush API 가 없다(6.0.0 에 HandleShutdown 미존재).
|
|
// 기록 직후에는 파일이 아직 없을 수 있으므로 잠시 폴링한다.
|
|
AppLog.Info("edit-smoke 로그 배선 확인");
|
|
var logWritten = false;
|
|
for (var wait = 0; wait < 30 && !logWritten; wait++)
|
|
{
|
|
logWritten = Directory.Exists(AppLog.LogPath)
|
|
&& Directory.GetFiles(AppLog.LogPath, "*", SearchOption.AllDirectories).Length > 0;
|
|
if (!logWritten)
|
|
{
|
|
System.Threading.Thread.Sleep(100);
|
|
}
|
|
}
|
|
Check("로그: 파일 생성됨", logWritten, AppLog.LogPath);
|
|
|
|
// 25) 기동 인자 파싱 — 레거시 관용 파싱(PadRight 후 split) 동형
|
|
Check("인자: 표준 3필드",
|
|
StartupArguments.ParseRaw("MSYS,,S999") is { UidCod: "MSYS", ShtCod: "S999", ComNum: null });
|
|
Check("인자: ComNum 포함",
|
|
StartupArguments.ParseRaw("MSYS,12345,S999") is { ComNum: 12345, ShtCod: "S999" });
|
|
Check("인자: UidCod 단독",
|
|
StartupArguments.ParseRaw("MSYS") is { UidCod: "MSYS", ShtCod: "" });
|
|
Check("인자: 빈 문자열",
|
|
StartupArguments.ParseRaw("") is { UidCod: "", ShtCod: "" });
|
|
Check("인자: 필드 초과해도 index 안전",
|
|
StartupArguments.ParseRaw("MSYS,,S999,X,Y") is { ShtCod: "S999" });
|
|
Check("인자: 비숫자 ComNum 관용 처리",
|
|
StartupArguments.ParseRaw("MSYS,abc,S999") is { ComNum: null, ShtCod: "S999" });
|
|
Check("인자: 진단 플래그는 HIS 인자가 아님",
|
|
StartupArguments.Parse(new[] { "--db-smoke", "r.txt" }) is null);
|
|
Check("인자: 인자 없음",
|
|
StartupArguments.Parse(Array.Empty<string>()) is null);
|
|
|
|
// 26) 더티 추적 — 종료 가드의 판단 근거(별도 문서로 격리해 위 단계와 간섭 없게)
|
|
var dirtyDoc = new DesignerViewModel(business.CreateNew());
|
|
Check("더티: 초기 상태는 clean", !dirtyDoc.Undo.IsDirty);
|
|
dirtyDoc.AddControlAt("Label", new Point(50, 50));
|
|
Check("더티: 편집 후 dirty", dirtyDoc.Undo.IsDirty);
|
|
dirtyDoc.Undo.MarkSaved();
|
|
Check("더티: 저장 표시 후 clean", !dirtyDoc.Undo.IsDirty);
|
|
dirtyDoc.AddControlAt("Label", new Point(90, 90));
|
|
Check("더티: 저장 후 편집하면 dirty", dirtyDoc.Undo.IsDirty);
|
|
dirtyDoc.Undo.Undo();
|
|
Check("더티: 저장 지점으로 Undo 하면 clean", !dirtyDoc.Undo.IsDirty);
|
|
dirtyDoc.Undo.Undo();
|
|
Check("더티: 저장 지점보다 더 되돌리면 dirty", dirtyDoc.Undo.IsDirty);
|
|
dirtyDoc.Undo.Redo();
|
|
Check("더티: Redo 로 저장 지점 복귀하면 clean", !dirtyDoc.Undo.IsDirty);
|
|
// 저장 지점보다 얕은 상태에서 새 편집 → 그 지점은 되돌아갈 수 없으므로 영구 dirty
|
|
dirtyDoc.Undo.Undo();
|
|
dirtyDoc.AddControlAt("CheckBox", new Point(120, 120));
|
|
dirtyDoc.Undo.Undo();
|
|
Check("더티: 분기 후에는 저장 지점 소실로 계속 dirty", dirtyDoc.Undo.IsDirty);
|
|
|
|
// 25) 연속 Undo 로 빈 문서까지
|
|
var guard = 0;
|
|
while (designer.Undo.CanUndo && guard++ < 80)
|
|
{
|
|
designer.Undo.Undo();
|
|
}
|
|
Check("전체 Undo → 빈 문서", designer.Document.Pages[0].Controls.Count == 0
|
|
&& designer.Document.Pages.Count == 1,
|
|
$"실제 컨트롤 {designer.Document.Pages[0].Controls.Count} 페이지 {designer.Document.Pages.Count}");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
failed++;
|
|
lines.Add("EXCEPTION " + ex);
|
|
}
|
|
|
|
lines.Add($"결과: 실패 {failed}건");
|
|
File.WriteAllText(reportPath, string.Join(Environment.NewLine, lines));
|
|
return failed == 0 ? 0 : 1;
|
|
}
|
|
#endregion
|
|
}
|