using MBN_STOCK_WEBVIEW.LegacyApplication; namespace MBN_STOCK_WEBVIEW.LegacyApplication.Tests; public sealed class LegacyOperatorControllerTests { [Fact] public async Task EmptyButtonIsNoOpButEmptyEnterSearches() { var lookup = Lookup(); var controller = new LegacyOperatorController(lookup); await controller.SearchStocksAsync(string.Empty, LegacySearchTrigger.Button); Assert.Equal(0, lookup.CallCount); await controller.SearchStocksAsync(string.Empty, LegacySearchTrigger.Enter); Assert.Equal(1, lookup.CallCount); } [Fact] public async Task ExactPointerDoubleClick_AppendsOneLegacyRow() { var controller = new LegacyOperatorController(Lookup()); await controller.SearchStocksAsync("삼성", LegacySearchTrigger.Button); controller.SelectStock(0); controller.CutPointerDown(1, false, false, 20, 45, 1_000); var state = controller.CutPointerDown(1, false, false, 20, 45, 1_400); var row = Assert.Single(state.Playlist); Assert.Equal("코스피", row.MarketText); Assert.Equal("삼성전자", row.StockName); Assert.Equal("1열판기본", row.GraphicType); Assert.Equal("현재가", row.Subtype); Assert.Equal("005930", row.StockCode); Assert.Equal("5001", row.ToPlayoutEntry().CutCode); } [Fact] public async Task ExactPointerDoubleClick_AllowsZeroBasedWebTimestamp() { var controller = new LegacyOperatorController(Lookup()); await controller.SearchStocksAsync("삼성", LegacySearchTrigger.Button); controller.SelectStock(0); controller.CutPointerDown(1, false, false, 20, 45, 0); var state = controller.CutPointerDown(1, false, false, 20, 45, 499); Assert.Single(state.Playlist); } [Fact] public async Task DifferentPointerPosition_DoesNotCountAsLegacyDoubleClick() { var controller = new LegacyOperatorController(Lookup()); await controller.SearchStocksAsync("삼성", LegacySearchTrigger.Button); controller.SelectStock(0); controller.CutPointerDown(1, false, false, 20, 45, 1_000); var state = controller.CutPointerDown(1, false, false, 21, 45, 1_200); Assert.Empty(state.Playlist); } [Fact] public async Task NativeSelectionAndLegacyDoubleClickRowsStaySeparateAtShiftEdges() { var controller = new LegacyOperatorController(Lookup()); await controller.SearchStocksAsync("삼성", LegacySearchTrigger.Button); controller.SelectStock(0); var firstShift = controller.CutPointerDown(2, false, true, 20, 69, 1_000); Assert.Equal( [0, 1, 2], firstShift.CutRows.Where(row => row.IsSelected).Select(row => row.PhysicalIndex)); var noDoubleClick = controller.CutPointerDown(2, false, false, 20, 69, 1_400); Assert.Empty(noDoubleClick.Playlist); var sameRowShift = controller.CutPointerDown(2, false, true, 21, 69, 2_000); Assert.Equal( [2], sameRowShift.CutRows.Where(row => row.IsSelected).Select(row => row.PhysicalIndex)); var stillNoDoubleClick = controller.CutPointerDown(2, false, false, 21, 69, 2_400); Assert.Empty(stillNoDoubleClick.Playlist); } [Fact] public async Task ControlAndShiftSelection_DropsInPhysicalOrderAndKeepsDuplicates() { var controller = new LegacyOperatorController(Lookup()); await controller.SearchStocksAsync("삼성", LegacySearchTrigger.Button); controller.SelectStock(0); controller.CutPointerDown(0, false, false, 1, 1, 1_000); controller.CutPointerDown(2, true, false, 1, 47, 2_000); controller.CutPointerDown(1, true, true, 1, 24, 3_000); var first = controller.DropSelectedCutsOnPlaylist(); var second = controller.DropSelectedCutsOnPlaylist(); Assert.Equal(3, first.Playlist.Count); Assert.Equal(6, second.Playlist.Count); Assert.Equal( ["예상체결가", "현재가", "시간외단일가"], first.Playlist.Select(row => row.Subtype)); } [Fact] public async Task SameRowShiftStillDropsTheNativeListViewSelection() { var controller = new LegacyOperatorController(Lookup()); await controller.SearchStocksAsync("삼성", LegacySearchTrigger.Button); controller.SelectStock(0); controller.CutPointerDown(1, false, false, 1, 24, 1_000); controller.CutPointerDown(1, false, true, 2, 24, 2_000); var state = controller.DropSelectedCutsOnPlaylist(); Assert.Equal("현재가", Assert.Single(state.Playlist).Subtype); } [Fact] public async Task NxtOnlyAllowsCurrentAndShowsOriginalWarning() { var controller = new LegacyOperatorController(Lookup()); await controller.SearchStocksAsync("넥스트", LegacySearchTrigger.Button); controller.SelectStock(1); controller.CutPointerDown(0, false, false, 1, 1, 1_000); var warning = controller.DropSelectedCutsOnPlaylist(); Assert.Empty(warning.Playlist); Assert.Equal("NXT는 제공하지 않습니다.", warning.Dialog?.Message); controller.DismissDialog(); controller.CutPointerDown(1, false, false, 1, 24, 2_000); var accepted = controller.DropSelectedCutsOnPlaylist(); Assert.Equal("N5001", Assert.Single(accepted.Playlist).ToPlayoutEntry().CutCode); } [Fact] public async Task NxtMultiSelection_PausesAtEachOriginalModalBeforeContinuing() { var controller = new LegacyOperatorController(Lookup()); await controller.SearchStocksAsync("넥스트", LegacySearchTrigger.Button); controller.SelectStock(1); controller.CutPointerDown(0, false, false, 1, 1, 1_000); controller.CutPointerDown(1, true, false, 1, 24, 2_000); controller.CutPointerDown(2, true, false, 1, 47, 3_000); var firstWarning = controller.DropSelectedCutsOnPlaylist(); Assert.Empty(firstWarning.Playlist); Assert.Equal("NXT는 제공하지 않습니다.", firstWarning.Dialog?.Message); var secondWarning = controller.DismissDialog(); Assert.Equal("N5001", Assert.Single(secondWarning.Playlist).ToPlayoutEntry().CutCode); Assert.Equal("NXT는 제공하지 않습니다.", secondWarning.Dialog?.Message); var finished = controller.DismissDialog(); Assert.Single(finished.Playlist); Assert.Null(finished.Dialog); } [Fact] public async Task NxtSeparatorStillShowsOriginalWarning() { var controller = new LegacyOperatorController(Lookup()); await controller.SearchStocksAsync("넥스트", LegacySearchTrigger.Button); controller.SelectStock(1); controller.CutPointerDown(3, false, false, 1, 70, 1_000); var warning = controller.DropSelectedCutsOnPlaylist(); Assert.Empty(warning.Playlist); Assert.Equal("NXT는 제공하지 않습니다.", warning.Dialog?.Message); } [Fact] public async Task NewSearchClearsStockSelectionButNotCutCatalogOrPlaylist() { var controller = new LegacyOperatorController(Lookup()); await controller.SearchStocksAsync("삼성", LegacySearchTrigger.Button); controller.SelectStock(0); controller.CutPointerDown(1, false, false, 1, 24, 1_000); controller.DropSelectedCutsOnPlaylist(); var state = await controller.SearchStocksAsync("다시", LegacySearchTrigger.Button); Assert.Null(state.SelectedStockIndex); Assert.Equal(33, state.CutRows.Count); Assert.Single(state.Playlist); } private static FakeLookup Lookup() => new(new LegacyStockSearchData( "query", ["삼성전자", "넥스트(NXT)"], [ new LegacyStockIdentity(LegacyDomesticStockMarket.Kospi, "삼성전자", "005930"), new LegacyStockIdentity(LegacyDomesticStockMarket.NxtKospi, "넥스트(NXT)", "A00001") ])); private sealed class FakeLookup : ILegacyStockLookup { private readonly LegacyStockSearchData _result; public FakeLookup(LegacyStockSearchData result) { _result = result; } public int CallCount { get; private set; } public Task SearchAsync( string query, CancellationToken cancellationToken = default) { CallCount++; return Task.FromResult(_result); } } }