feat: advance legacy UI behavior parity

This commit is contained in:
2026-07-18 10:18:29 +09:00
parent d7ec571343
commit 1302b1b92f
71 changed files with 11244 additions and 1153 deletions

View File

@@ -221,6 +221,111 @@ public sealed class LegacyComparisonWorkflowTests
Assert.False(snapshot.IsBusy);
}
[Fact]
public async Task Uc3_AllowsExactDuplicatePairsAndMovesOrDeletesOnlyTheSelectedStableRow()
{
var data = new FakeDataService { DomesticResults = [Samsung, SkHynix] };
var store = new FakePairStore();
var controller = CreateController(data, store);
var search = await controller.SearchDomesticAsync(string.Empty);
controller.ChooseDomesticResult(search.DomesticSearch.Results[0].SelectionId);
controller.ChooseDomesticResult(search.DomesticSearch.Results[1].SelectionId);
var firstAdd = await controller.AddDraftPairAsync();
var firstRowId = Assert.Single(firstAdd.SavedPairs).RowId;
var secondAdd = await controller.AddDraftPairAsync();
var secondRowId = secondAdd.SavedPairs[1].RowId;
Assert.NotEqual(firstRowId, secondRowId);
Assert.Equal(
new[] { firstRowId, secondRowId },
secondAdd.SavedPairs.Select(pair => pair.RowId));
Assert.All(
secondAdd.SavedPairs,
pair => Assert.Equal("SK하이닉스 ↔ 삼성전자", pair.DisplayLabel));
Assert.Collection(
store.Writes[^1],
row => Assert.Equal(
new LegacyComparisonPairWriteRow(
1,
"SK하이닉스",
"코스피",
"삼성전자",
"코스피"),
row),
row => Assert.Equal(
new LegacyComparisonPairWriteRow(
2,
"SK하이닉스",
"코스피",
"삼성전자",
"코스피"),
row));
controller.SelectSavedPair(secondRowId);
var moved = await controller.MoveSelectedPairAsync(
LegacyComparisonPairMoveDirection.Up);
Assert.Equal(
new[] { secondRowId, firstRowId },
moved.SavedPairs.Select(pair => pair.RowId));
Assert.Equal(secondRowId, moved.SelectedPairId);
Assert.True(moved.SavedPairs[0].IsSelected);
Assert.False(moved.SavedPairs[1].IsSelected);
var deleted = await controller.DeleteSelectedPairAsync();
var remaining = Assert.Single(deleted.SavedPairs);
Assert.Equal(firstRowId, remaining.RowId);
Assert.True(remaining.IsSelected);
Assert.Equal(firstRowId, deleted.SelectedPairId);
Assert.Single(store.Writes[^1]);
}
[Fact]
public async Task Uc3_DuplicatePairSaveRoundTripPreservesPhysicalRowOrder()
{
var data = new FakeDataService();
data.Resolve(Samsung, "삼성전자", "코스피");
data.Resolve(SkHynix, "SK하이닉스", "코스피");
data.Resolve(Apple, "APPLE", string.Empty);
var store = new FakePairStore
{
Bytes = EncodeCp949(
("삼성전자", "코스피", "SK하이닉스", "코스피"),
("삼성전자", "코스피", "SK하이닉스", "코스피"),
("코스피 지수", string.Empty, "APPLE", string.Empty))
};
var controller = CreateController(data, store);
var loaded = await controller.LoadSavedPairsAsync();
var thirdRowId = loaded.SavedPairs[2].RowId;
controller.SelectSavedPair(thirdRowId);
var moved = await controller.MoveSelectedPairAsync(
LegacyComparisonPairMoveDirection.Up);
Assert.Equal(
new[]
{
"삼성전자 ↔ SK하이닉스",
"코스피 지수 ↔ APPLE",
"삼성전자 ↔ SK하이닉스"
},
moved.SavedPairs.Select(pair => pair.DisplayLabel));
Assert.Equal(new[] { 1, 2, 3 }, store.Writes[^1].Select(row => row.RowNumber));
var reloadedStore = new FakePairStore
{
Bytes = EncodeCp949(store.Writes[^1])
};
var reloaded = await CreateController(data, reloadedStore).LoadSavedPairsAsync();
Assert.Equal(
moved.SavedPairs.Select(pair => pair.DisplayLabel),
reloaded.SavedPairs.Select(pair => pair.DisplayLabel));
Assert.Equal(3, reloaded.SavedPairs.Select(pair => pair.RowId).Distinct().Count());
}
[Fact]
public async Task SavedPairMoveAndDelete_PreserveActiveRowAndPersistPhysicalOrder()
{
@@ -424,6 +529,13 @@ public sealed class LegacyComparisonWorkflowTests
return encoding.GetBytes(string.Join("\r\n", lines) + "\r\n");
}
private static byte[] EncodeCp949(IReadOnlyList<LegacyComparisonPairWriteRow> rows) =>
EncodeCp949(rows.Select(row => (
row.FirstName,
row.FirstMarket,
row.SecondName,
row.SecondMarket)).ToArray());
private sealed class FakeDataService : ILegacyComparisonDataService
{
private readonly Dictionary<LegacyComparisonStoredTargetReference, LegacyComparisonTarget>

View File

@@ -59,6 +59,108 @@ public sealed class LegacyOperatorComparisonIntegrationTests
row => Assert.Equal("5087", row.SceneAlias));
}
[Fact]
public async Task DeleteAllComparisonPairs_NoPreservesRowsAndPerformsNoWrite()
{
var (controller, store) = await CreateControllerWithSavedPairAsync();
var writesBeforeRequest = store.Writes.Count;
var requested = await controller.DeleteComparisonPairAsync(all: true);
Assert.Equal("모두 삭제하겠습니까?", requested.Dialog?.Message);
Assert.Equal("MmoneyCoder", requested.Dialog?.Caption);
Assert.True(requested.Dialog?.IsConfirmation);
Assert.Single(requested.Comparison!.SavedPairs);
Assert.Equal(writesBeforeRequest, store.Writes.Count);
var dismissed = controller.DismissDialog();
Assert.True(dismissed.Dialog?.IsConfirmation);
Assert.Equal(writesBeforeRequest, store.Writes.Count);
var canceled = controller.CancelDialog();
Assert.Null(canceled.Dialog);
Assert.Single(canceled.Comparison!.SavedPairs);
Assert.Equal(writesBeforeRequest, store.Writes.Count);
await controller.ConfirmDialogAsync();
Assert.Equal(writesBeforeRequest, store.Writes.Count);
}
[Fact]
public async Task DeleteAllComparisonPairs_YesConsumesConfirmationBeforeSingleWrite()
{
var (controller, store) = await CreateControllerWithSavedPairAsync();
var writesBeforeRequest = store.Writes.Count;
await controller.DeleteComparisonPairAsync(all: true);
var confirmed = await controller.ConfirmDialogAsync();
Assert.Null(confirmed.Dialog);
Assert.Empty(confirmed.Comparison!.SavedPairs);
Assert.Equal(writesBeforeRequest + 1, store.Writes.Count);
Assert.Empty(store.Writes[^1]);
await controller.ConfirmDialogAsync();
Assert.Equal(writesBeforeRequest + 1, store.Writes.Count);
}
[Fact]
public async Task DeleteAllComparisonPairs_ChangedListInvalidatesPendingConfirmation()
{
var (controller, store) = await CreateControllerWithSavedPairAsync();
var writesBeforeRequest = store.Writes.Count;
var requested = await controller.DeleteComparisonPairAsync(all: true);
var rowId = Assert.Single(requested.Comparison!.SavedPairs).RowId;
controller.SelectComparisonPair(rowId);
await controller.DeleteComparisonPairAsync(all: true);
var rejected = await controller.ConfirmDialogAsync();
Assert.Single(rejected.Comparison!.SavedPairs);
Assert.Equal(writesBeforeRequest, store.Writes.Count);
Assert.Equal(LegacyOperatorStatusKind.Error, rejected.StatusKind);
}
[Fact]
public async Task DeleteAllComparisonPairs_EmptyListStillPromptsAndYesDoesNotWrite()
{
var store = new MemoryComparisonPairStore();
var controller = new LegacyOperatorController(
new EmptyStockLookup(),
comparisonWorkflow: new LegacyComparisonWorkflowController(
new StaticComparisonDataService(),
store));
await controller.SelectTabAsync(LegacyOperatorTabId.Comparison);
var requested = await controller.DeleteComparisonPairAsync(all: true);
Assert.Equal("모두 삭제하겠습니까?", requested.Dialog?.Message);
Assert.True(requested.Dialog?.IsConfirmation);
var confirmed = await controller.ConfirmDialogAsync();
Assert.Empty(confirmed.Comparison!.SavedPairs);
Assert.Empty(store.Writes);
}
private static async Task<(LegacyOperatorController Controller,
MemoryComparisonPairStore Store)> CreateControllerWithSavedPairAsync()
{
var store = new MemoryComparisonPairStore();
var controller = new LegacyOperatorController(
new EmptyStockLookup(),
comparisonWorkflow: new LegacyComparisonWorkflowController(
new StaticComparisonDataService(),
store));
await controller.SelectTabAsync(LegacyOperatorTabId.Comparison);
var searched = await controller.SearchComparisonDomesticAsync("반도체");
controller.ChooseComparisonDomesticResult(
searched.Comparison!.DomesticSearch.Results[0].SelectionId);
controller.ChooseComparisonDomesticResult(
searched.Comparison.DomesticSearch.Results[1].SelectionId);
await controller.AddComparisonPairAsync();
return (controller, store);
}
private sealed class EmptyStockLookup : ILegacyStockLookup
{
public Task<LegacyStockSearchData> SearchAsync(