using MMoneyCoderSharp.Data; using MBN_STOCK_WEBVIEW.Core.Playout; using MBN_STOCK_WEBVIEW.Core.Playout.Scenes; namespace MBN_STOCK_WEBVIEW.LegacyApplication.Tests; public sealed class LegacyOperatorPlayoutRequestTests { [Fact] public void EmptyPlaylistCannotCreatePlayoutRequest() { var controller = new LegacyOperatorController(new StaticLookup()); Assert.Throws(controller.CreatePlayoutRequest); } [Fact] public async Task RequestPreservesNativePlaylistIdentityEnabledStateAndSelection() { var controller = new LegacyOperatorController(new StaticLookup()); await controller.SearchStocksAsync("삼성", LegacySearchTrigger.Button); controller.SelectStock(0); AddCut(controller, physicalIndex: 0, timestamp: 1_000); AddCut(controller, physicalIndex: 1, timestamp: 2_000); var rows = controller.Current.Playlist; controller.SetPlaylistRowEnabled(rows[0].RowId, enabled: false); controller.SelectPlaylistRow(rows[1].RowId, control: false, shift: false); var request = controller.CreatePlayoutRequest(); Assert.Equal(1, request.SelectedIndexZeroBased); Assert.Equal(2, request.Playlist.Count); Assert.Equal(rows.Select(row => row.RowId), request.Playlist.Select(entry => entry.EntryId)); Assert.False(request.Playlist[0].IsEnabled); Assert.True(request.Playlist[1].IsEnabled); Assert.All(request.Playlist, entry => Assert.Equal("005930", entry.Selection?.DataCode)); } [Theory] [InlineData(false, false, "")] [InlineData(true, false, "MA5")] [InlineData(false, true, "MA20")] [InlineData(true, true, "MA5,MA20")] public async Task CandleAddedBeforeToggleSamplesCurrentMovingAveragesAtF8Request( bool ma5, bool ma20, string expectedGraphicType) { var controller = new LegacyOperatorController(new StaticLookup()); await controller.SearchStocksAsync("stock", LegacySearchTrigger.Button); controller.SelectStock(0); var candle = LegacyStockCutCatalog.Rows.First(row => !row.IsSeparator && LegacyStockCutCatalog.ResolveCutAlias(row.RawLabel, isNxt: false) == "8051"); AddCut(controller, candle.PhysicalIndex, 1_000); var rawBeforeToggle = Assert.Single(controller.Current.Playlist); var rawGraphicType = rawBeforeToggle.ToPlayoutEntry().Selection!.GraphicType; controller.SetMovingAverages(ma5, ma20); var entry = Assert.Single(controller.CreatePlayoutRequest().Playlist); Assert.Equal(expectedGraphicType, entry.Selection!.GraphicType); Assert.Equal(rawGraphicType, Assert.Single(controller.Current.Playlist).ToPlayoutEntry().Selection!.GraphicType); Assert.Equal( new LegacySceneMovingAverageSelection(ma5, ma20), entry.MovingAverageSelectionSource!.Current); } [Fact] public async Task MovingAverageOverlayDoesNotAlterNonCandleRequest() { var controller = new LegacyOperatorController(new StaticLookup()); await controller.SearchStocksAsync("stock", LegacySearchTrigger.Button); controller.SelectStock(0); var current = LegacyStockCutCatalog.Rows.First(row => !row.IsSeparator && LegacyStockCutCatalog.ResolveCutAlias(row.RawLabel, isNxt: false) == "5001"); AddCut(controller, current.PhysicalIndex, 1_000); var raw = Assert.Single(controller.Current.Playlist).ToPlayoutEntry(); controller.SetMovingAverages(ma5: false, ma20: false); var requested = Assert.Single(controller.CreatePlayoutRequest().Playlist); Assert.Equal(raw.Selection, requested.Selection); Assert.Null(requested.MovingAverageSelectionSource); } [Theory] [InlineData(0)] [InlineData(6)] [InlineData(19)] public async Task RequestAppliesCurrentGlobalDissolveIndexToEveryExistingRow( int globalFadeDuration) { var controller = await CreateControllerWithCutsAsync(3); Assert.All(controller.Current.Playlist, row => Assert.Equal(6, row.FadeDuration)); var request = controller.CreatePlayoutRequest(globalFadeDuration); Assert.All(request.Playlist, entry => Assert.Equal(globalFadeDuration, entry.FadeDuration)); Assert.All(controller.Current.Playlist, row => Assert.Equal(6, row.FadeDuration)); } [Theory] [InlineData(-1)] [InlineData(20)] [InlineData(60)] public async Task RequestRejectsGlobalDissolveIndexOutsideOriginalComboRange( int globalFadeDuration) { var controller = await CreateControllerWithCutsAsync(1); Assert.Throws(() => controller.CreatePlayoutRequest(globalFadeDuration)); } [Theory] [InlineData("5단 표그래프", ScenePageSize.Five)] [InlineData("6종목 현재가", ScenePageSize.Six)] [InlineData("12종목 현재가", ScenePageSize.Twelve)] public void ExactLegacyGraphicTypeFreezesTheOperatorPageSuffix( string graphicType, ScenePageSize expectedPageSize) { var entry = OperatorRow(graphicType, "2/3").ToPlayoutEntry(); Assert.Equal(expectedPageSize, entry.PageNavigation?.PageSize); Assert.Equal(3, entry.PageNavigation?.FrozenPageCount); Assert.True(entry.PageNavigation?.IsEnabled); } [Theory] [InlineData("5단 표그래프 ")] [InlineData("5단표그래프")] [InlineData("five-row")] public void NonCanonicalGraphicTypeExplicitlyDisablesPageNext(string graphicType) { var entry = OperatorRow(graphicType, "1/9").ToPlayoutEntry(); Assert.NotNull(entry.PageNavigation); Assert.False(entry.PageNavigation.IsEnabled); Assert.Null(entry.PageNavigation.PageSize); Assert.Null(entry.PageNavigation.FrozenPageCount); } [Theory] [InlineData("1/0")] [InlineData("1/21")] [InlineData("2/1")] [InlineData("1 /2")] public void CanonicalPagedGraphicRejectsInvalidOperatorPageText(string pageText) { var row = OperatorRow("5단 표그래프", pageText); Assert.Throws(row.ToPlayoutEntry); } [Fact] public async Task NextProgressUpdatesProgramPageWithoutReplacingOperatorSelection() { var controller = await CreateControllerWithCutsAsync(3); var rows = controller.Current.Playlist; controller.SelectPlaylistRow(rows[2].RowId, control: false, shift: false); var state = controller.ReflectSuccessfulPlayout( rows[0].RowId, pageIndexZeroBased: 1, pageCount: 3, synchronizeOperatorSelection: false); Assert.Equal("2/3", state.Playlist[0].PageText); Assert.True(state.Playlist[2].IsSelected); Assert.False(state.Playlist[0].IsSelected); Assert.Equal(2, controller.CreatePlayoutRequest().SelectedIndexZeroBased); } [Fact] public async Task TakeSkipMovesOnlyTheF8CandidateAndPreservesDeleteSelection() { var controller = await CreateControllerWithCutsAsync(2); var rows = controller.Current.Playlist; controller.SelectPlaylistRow(rows[0].RowId, control: false, shift: false); controller.ReflectSuccessfulPlayout( rows[0].RowId, pageIndexZeroBased: 2, pageCount: 3, synchronizeOperatorSelection: false); var state = controller.ReflectSuccessfulPlayout( rows[1].RowId, pageIndexZeroBased: 0, pageCount: 2, synchronizeOperatorSelection: true); Assert.Equal("1/2", state.Playlist[1].PageText); Assert.True(state.Playlist[0].IsSelected); Assert.False(state.Playlist[1].IsSelected); Assert.True(state.Playlist[1].IsActive); Assert.Equal(1, controller.CreatePlayoutRequest().SelectedIndexZeroBased); } [Fact] public async Task SuccessfulF8DoesNotReplaceTheRowsAccumulatedForDelete() { var controller = await CreateControllerWithCutsAsync(3); var rows = controller.Current.Playlist; controller.SelectPlaylistRow(rows[0].RowId, control: false, shift: false); controller.SelectPlaylistRow(rows[2].RowId, control: true, shift: false); var reflected = controller.ReflectSuccessfulPlayout( rows[2].RowId, pageIndexZeroBased: 0, pageCount: 1, synchronizeOperatorSelection: true); Assert.Equal( [rows[0].RowId, rows[2].RowId], reflected.Playlist.Where(row => row.IsSelected).Select(row => row.RowId)); Assert.True(reflected.Playlist[2].IsActive); var deleted = controller.DeleteSelectedPlaylistRows(); Assert.Equal(rows[1].RowId, Assert.Single(deleted.Playlist).RowId); } private static async Task CreateControllerWithCutsAsync(int count) { var controller = new LegacyOperatorController(new StaticLookup()); await controller.SearchStocksAsync("?쇱꽦", LegacySearchTrigger.Button); controller.SelectStock(0); for (var index = 0; index < count; index++) { AddCut(controller, index, 1_000 + (index * 1_000)); } return controller; } private static LegacyOperatorPlaylistRow OperatorRow( string graphicType, string pageText) => new( "row-1", true, "테마", "반도체", graphicType, "현재가", pageText, "00123", "paged-cut", 6, SceneAlias: "5074", PlayoutSelection: new( "테마", "반도체", graphicType, "현재가", "00123")); private static void AddCut( LegacyOperatorController controller, int physicalIndex, long timestamp) { controller.CutPointerDown( physicalIndex, control: false, shift: false, x: 1, y: physicalIndex * 23, timestampMilliseconds: timestamp); controller.DropSelectedCutsOnPlaylist(); } private sealed class StaticLookup : ILegacyStockLookup { public Task SearchAsync( string query, CancellationToken cancellationToken = default) => Task.FromResult(new LegacyStockSearchData( query, ["삼성전자"], [new LegacyStockIdentity( LegacyDomesticStockMarket.Kospi, "삼성전자", "005930")])); } }