feat: align legacy operator and playout behavior

This commit is contained in:
2026-07-15 21:04:05 +09:00
parent 06a16e5741
commit 83398044c6
77 changed files with 6377 additions and 391 deletions

View File

@@ -1,4 +1,6 @@
using MMoneyCoderSharp.Data;
using MBN_STOCK_WEBVIEW.Core.Playout;
using MBN_STOCK_WEBVIEW.Core.Playout.Scenes;
namespace MBN_STOCK_WEBVIEW.LegacyApplication.Tests;
@@ -36,6 +38,229 @@ public sealed class LegacyOperatorPlayoutRequestTests
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<ArgumentOutOfRangeException>(() =>
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<InvalidOperationException>(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<LegacyOperatorController> 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,