using MMoneyCoderSharp.Data; namespace MBN_STOCK_WEBVIEW.LegacyApplication.Tests; public sealed class LegacyStockCutInteractionParityTests { [Fact] public async Task EmptySearchButtonPreservesResultsStockAndCutSelection() { var lookup = new QueryLookup(); var controller = new LegacyOperatorController(lookup); await controller.SearchStocksAsync("first", LegacySearchTrigger.Button); controller.SelectStock(0); controller.CutPointerDown(1, control: false, shift: false, x: 2, y: 24, timestampMilliseconds: 1_000); var state = await controller.SearchStocksAsync( string.Empty, LegacySearchTrigger.Button); Assert.Equal(1, lookup.CallCount); Assert.Equal(string.Empty, state.SearchText); Assert.Equal("First stock", Assert.Single(state.SearchResults).DisplayName); Assert.Equal(0, state.SelectedStockIndex); Assert.Equal( [1], state.CutRows.Where(row => row.IsSelected).Select(row => row.PhysicalIndex)); Assert.Empty(state.Playlist); } [Fact] public async Task EmptyEnterQueriesAndClearsOnlyTheStockResultSelection() { var lookup = new QueryLookup(); var controller = new LegacyOperatorController(lookup); await controller.SearchStocksAsync("first", LegacySearchTrigger.Button); controller.SelectStock(0); controller.CutPointerDown(1, control: false, shift: false, x: 2, y: 24, timestampMilliseconds: 1_000); var state = await controller.SearchStocksAsync( string.Empty, LegacySearchTrigger.Enter); Assert.Equal(2, lookup.CallCount); Assert.Null(state.SelectedStockIndex); Assert.Equal("All stock", Assert.Single(state.SearchResults).DisplayName); Assert.Equal( [1], state.CutRows.Where(row => row.IsSelected).Select(row => row.PhysicalIndex)); } [Fact] public async Task RetainedCutSelectionUsesTheNewlySelectedStockAtDropTime() { var lookup = new QueryLookup(); var controller = new LegacyOperatorController(lookup); await controller.SearchStocksAsync("first", LegacySearchTrigger.Button); controller.SelectStock(0); controller.CutPointerDown(1, control: false, shift: false, x: 2, y: 24, timestampMilliseconds: 1_000); await controller.SearchStocksAsync("second", LegacySearchTrigger.Button); controller.SelectStock(0); var state = controller.DropSelectedCutsOnPlaylist(); var row = Assert.Single(state.Playlist); Assert.Equal("Second stock", row.StockName); Assert.Equal("000002", row.StockCode); Assert.Equal(LegacyStockCutCatalog.Rows[1].RawLabel, row.RawCutLabel); } [Fact] public async Task OrdinaryMarketSeparatorIsIgnoredWithoutOpeningTheNxtWarning() { var controller = new LegacyOperatorController(new QueryLookup()); await controller.SearchStocksAsync("first", LegacySearchTrigger.Button); controller.SelectStock(0); controller.CutPointerDown(3, control: false, shift: false, x: 2, y: 70, timestampMilliseconds: 1_000); var state = controller.DropSelectedCutsOnPlaylist(); Assert.Empty(state.Playlist); Assert.Null(state.Dialog); } [Fact] public async Task InjectedRuntimeMenuIsCopiedAndDrivesViewSelectionAndDropByPhysicalOrder() { var injected = RuntimeMenu(); var expectedFirstLabel = injected[0].RawLabel; var expectedLastLabel = injected[2].RawLabel; var controller = new LegacyOperatorController( new QueryLookup(), cutRows: injected); injected[0] = injected[0] with { RawLabel = LegacyStockCutCatalog.Rows[2].RawLabel }; Assert.Equal( [expectedFirstLabel, string.Empty, expectedLastLabel], controller.Current.CutRows.Select(row => row.RawLabel)); await controller.SearchStocksAsync("first", LegacySearchTrigger.Button); controller.SelectStock(0); controller.CutPointerDown(0, control: false, shift: false, x: 1, y: 1, timestampMilliseconds: 1_000); controller.CutPointerDown(2, control: true, shift: false, x: 1, y: 47, timestampMilliseconds: 2_000); var dropped = controller.DropSelectedCutsOnPlaylist(); Assert.Equal( [0, 2], dropped.CutRows.Where(row => row.IsSelected).Select(row => row.PhysicalIndex)); Assert.Equal( [expectedFirstLabel, expectedLastLabel], dropped.Playlist.Select(row => row.RawCutLabel)); var revision = dropped.Revision; var invalidIndex = controller.CutPointerDown( 3, control: false, shift: false, x: 1, y: 70, timestampMilliseconds: 3_000); Assert.Equal(revision, invalidIndex.Revision); } [Fact] public async Task InjectedRuntimeMenuPreservesTheNxtCurrentLabelRuleAfterReorder() { var menu = RuntimeMenu(); var controller = new LegacyOperatorController( new NxtLookup(), cutRows: menu); await controller.SearchStocksAsync("nxt", LegacySearchTrigger.Button); controller.SelectStock(0); controller.CutPointerDown(2, control: false, shift: false, x: 1, y: 47, timestampMilliseconds: 1_000); var warning = controller.DropSelectedCutsOnPlaylist(); Assert.Equal("NXT는 제공하지 않습니다.", warning.Dialog?.Message); Assert.Empty(warning.Playlist); controller.DismissDialog(); controller.CutPointerDown(0, control: false, shift: false, x: 1, y: 1, timestampMilliseconds: 2_000); var accepted = controller.DropSelectedCutsOnPlaylist(); Assert.Equal("N5001", Assert.Single(accepted.Playlist).ToPlayoutEntry().CutCode); } [Fact] public async Task FiftyRowRuntimeMenuCanSelectAndDropItsLastPhysicalRow() { var rows = Enumerable.Range(0, LegacyRuntimeStockCutMenuLoader.MaximumRows) .Select(index => new LegacyCutCatalogRow( index, index + 1, LegacyStockCutCatalog.Rows[1].RawLabel, IsSeparator: false)) .ToArray(); var controller = new LegacyOperatorController(new QueryLookup(), cutRows: rows); await controller.SearchStocksAsync("first", LegacySearchTrigger.Button); controller.SelectStock(0); controller.CutPointerDown( LegacyRuntimeStockCutMenuLoader.MaximumRows - 1, control: false, shift: false, x: 1, y: 1, timestampMilliseconds: 1_000); var state = controller.DropSelectedCutsOnPlaylist(); Assert.Equal( LegacyRuntimeStockCutMenuLoader.MaximumRows - 1, Assert.Single(state.CutRows, row => row.IsSelected).PhysicalIndex); Assert.Equal( LegacyStockCutCatalog.Rows[1].RawLabel, Assert.Single(state.Playlist).RawCutLabel); } [Fact] public async Task ConsecutiveShiftGesturesKeepTheOriginalNativeListAnchor() { var controller = new LegacyOperatorController(new QueryLookup()); await controller.SearchStocksAsync("first", LegacySearchTrigger.Button); controller.SelectStock(0); controller.CutPointerDown(0, control: false, shift: false, 1, 1, 1_000); var first = controller.CutPointerDown( 2, control: false, shift: true, 1, 47, 2_000); var second = controller.CutPointerDown( 4, control: false, shift: true, 1, 93, 3_000); Assert.Equal( [0, 1, 2], first.CutRows.Where(row => row.IsSelected).Select(row => row.PhysicalIndex)); Assert.Equal( [0, 1, 2, 3, 4], second.CutRows.Where(row => row.IsSelected).Select(row => row.PhysicalIndex)); } [Fact] public async Task EmptyCutListAreaClearsSelectionSoAStaleDropAddsNothing() { var controller = new LegacyOperatorController(new QueryLookup()); await controller.SearchStocksAsync("first", LegacySearchTrigger.Button); controller.SelectStock(0); controller.CutPointerDown(1, false, false, 1, 24, 1_000); var cleared = controller.ClearCutSelection(); var dropped = controller.DropSelectedCutsOnPlaylist(); Assert.DoesNotContain(cleared.CutRows, row => row.IsSelected); Assert.Empty(dropped.Playlist); } [Theory] [InlineData(0, 2, 0, 1, 2)] [InlineData(3, 1, 1, 2, 3)] public async Task EmptyCutListAreaPreservesNativeAnchorForFollowingShift( int anchor, int target, params int[] expected) { var controller = new LegacyOperatorController(new QueryLookup()); await controller.SearchStocksAsync("first", LegacySearchTrigger.Button); controller.SelectStock(0); controller.CutPointerDown(anchor, false, false, 1, 1, 1_000); controller.ClearCutSelection(); var state = controller.CutPointerDown(target, false, true, 2, 2, 2_000); Assert.Equal( expected, state.CutRows.Where(row => row.IsSelected).Select(row => row.PhysicalIndex)); } [Fact] public void EmptyCutListAreaIsIgnoredUntilAStockIsSelected() { var controller = new LegacyOperatorController(new QueryLookup()); var revision = controller.Current.Revision; var state = controller.ClearCutSelection(); Assert.Equal(revision, state.Revision); } [Fact] public async Task BlankClickResetsLegacyAnchorEvenAfterControlClearsTheLastRow() { var controller = new LegacyOperatorController(new QueryLookup()); await controller.SearchStocksAsync("first", LegacySearchTrigger.Button); controller.SelectStock(0); controller.CutPointerDown(1, false, false, 1, 24, 1_000); controller.CutPointerDown(1, true, false, 1, 24, 2_000); controller.ClearCutSelection(); controller.CutPointerDown(2, false, true, 2, 47, 3_000); var state = controller.CutPointerDown(2, false, false, 2, 47, 3_400); Assert.Empty(state.Playlist); Assert.Equal( [2], state.CutRows.Where(row => row.IsSelected).Select(row => row.PhysicalIndex)); } [Theory] [InlineData("physical-index")] [InlineData("display-ordinal")] [InlineData("unknown-label")] [InlineData("separator-shape")] [InlineData("too-many")] public void InjectedRuntimeMenuRejectsInvalidAuthority(string invalidCase) { var baseline = RuntimeMenu(); IReadOnlyList rows = invalidCase switch { "physical-index" => [baseline[0] with { PhysicalIndex = 1 }], "display-ordinal" => [baseline[0] with { DisplayOrdinal = 2 }], "unknown-label" => [baseline[0] with { RawLabel = "지원하지않는_컷" }], "separator-shape" => [new LegacyCutCatalogRow(0, 1, "-", IsSeparator: true)], "too-many" => Enumerable.Range( 0, LegacyRuntimeStockCutMenuLoader.MaximumRows + 1) .Select(index => new LegacyCutCatalogRow( index, index + 1, LegacyStockCutCatalog.Rows[0].RawLabel, IsSeparator: false)) .ToArray(), _ => throw new ArgumentOutOfRangeException(nameof(invalidCase)) }; Assert.Throws(() => new LegacyOperatorController( new QueryLookup(), cutRows: rows)); } private static LegacyCutCatalogRow[] RuntimeMenu() => [ new LegacyCutCatalogRow( 0, 1, LegacyStockCutCatalog.Rows[1].RawLabel, IsSeparator: false), new LegacyCutCatalogRow(1, null, string.Empty, IsSeparator: true), new LegacyCutCatalogRow( 2, 2, LegacyStockCutCatalog.Rows[0].RawLabel, IsSeparator: false) ]; private sealed class QueryLookup : ILegacyStockLookup { public int CallCount { get; private set; } public Task SearchAsync( string text, CancellationToken cancellationToken = default) { CallCount++; var (name, code) = text switch { "first" => ("First stock", "000001"), "second" => ("Second stock", "000002"), _ => ("All stock", "000003") }; return Task.FromResult(new LegacyStockSearchData( text, [name], [new LegacyStockIdentity( LegacyDomesticStockMarket.Kospi, name, code)])); } } private sealed class NxtLookup : ILegacyStockLookup { public Task SearchAsync( string text, CancellationToken cancellationToken = default) => Task.FromResult(new LegacyStockSearchData( text, ["Next stock(NXT)"], [new LegacyStockIdentity( LegacyDomesticStockMarket.NxtKospi, "Next stock(NXT)", "A00001")])); } }