feat: align legacy operator and playout behavior
This commit is contained in:
@@ -0,0 +1,169 @@
|
||||
namespace MBN_STOCK_WEBVIEW.LegacyApplication.Tests;
|
||||
|
||||
public sealed class LegacyCutKeyboardParityTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task ControlNavigationMovesOnlyFocusAndShiftKeepsTheNativeAnchor()
|
||||
{
|
||||
var controller = await CreateReadyControllerAsync();
|
||||
controller.CutPointerDown(2, false, false, 1, 47, 1_000);
|
||||
|
||||
var controlDown = controller.CutKeyDown(
|
||||
LegacyCutKeyboardKey.ArrowDown,
|
||||
control: true,
|
||||
shift: false);
|
||||
var shiftSpace = controller.CutKeyDown(
|
||||
LegacyCutKeyboardKey.Space,
|
||||
control: false,
|
||||
shift: true);
|
||||
var plainDown = controller.CutKeyDown(
|
||||
LegacyCutKeyboardKey.ArrowDown,
|
||||
control: false,
|
||||
shift: false);
|
||||
var shiftUp = controller.CutKeyDown(
|
||||
LegacyCutKeyboardKey.ArrowUp,
|
||||
control: false,
|
||||
shift: true);
|
||||
|
||||
AssertCutState(controlDown, [2], focused: 3);
|
||||
AssertCutState(shiftSpace, [2, 3], focused: 3);
|
||||
AssertCutState(plainDown, [4], focused: 4);
|
||||
AssertCutState(shiftUp, [3, 4], focused: 3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ControlShiftNavigationAddsRangeWithoutDiscardingExistingSelection()
|
||||
{
|
||||
var controller = await CreateReadyControllerAsync();
|
||||
controller.CutPointerDown(1, false, false, 1, 24, 1_000);
|
||||
controller.CutPointerDown(4, true, false, 1, 93, 2_000);
|
||||
controller.CutKeyDown(
|
||||
LegacyCutKeyboardKey.ArrowUp,
|
||||
control: true,
|
||||
shift: false);
|
||||
|
||||
var state = controller.CutKeyDown(
|
||||
LegacyCutKeyboardKey.ArrowUp,
|
||||
control: true,
|
||||
shift: true);
|
||||
|
||||
AssertCutState(state, [1, 2, 3, 4], focused: 2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HomeAndEndUsePhysicalBoundariesIncludingSeparatorRows()
|
||||
{
|
||||
var controller = await CreateReadyControllerAsync();
|
||||
controller.CutPointerDown(2, false, false, 1, 47, 1_000);
|
||||
|
||||
var end = controller.CutKeyDown(
|
||||
LegacyCutKeyboardKey.End,
|
||||
control: false,
|
||||
shift: false);
|
||||
var controlHome = controller.CutKeyDown(
|
||||
LegacyCutKeyboardKey.Home,
|
||||
control: true,
|
||||
shift: false);
|
||||
var shiftSpace = controller.CutKeyDown(
|
||||
LegacyCutKeyboardKey.Space,
|
||||
control: false,
|
||||
shift: true);
|
||||
|
||||
var last = end.CutRows.Count - 1;
|
||||
AssertCutState(end, [last], focused: last);
|
||||
AssertCutState(controlHome, [last], focused: 0);
|
||||
AssertCutState(shiftSpace, Enumerable.Range(0, last + 1), focused: 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ControlSpaceTogglesFocusedRowAndMovesTheAnchor()
|
||||
{
|
||||
var controller = await CreateReadyControllerAsync();
|
||||
controller.CutPointerDown(1, false, false, 1, 24, 1_000);
|
||||
|
||||
var toggledOff = controller.CutKeyDown(
|
||||
LegacyCutKeyboardKey.Space,
|
||||
control: true,
|
||||
shift: false);
|
||||
controller.CutKeyDown(
|
||||
LegacyCutKeyboardKey.ArrowDown,
|
||||
control: true,
|
||||
shift: false);
|
||||
var extended = controller.CutKeyDown(
|
||||
LegacyCutKeyboardKey.Space,
|
||||
control: false,
|
||||
shift: true);
|
||||
|
||||
AssertCutState(toggledOff, [], focused: 1);
|
||||
AssertCutState(extended, [1, 2], focused: 2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task KeyboardSelectionFeedsTheSameNativeDropAuthority()
|
||||
{
|
||||
var controller = await CreateReadyControllerAsync();
|
||||
|
||||
var selected = controller.CutKeyDown(
|
||||
LegacyCutKeyboardKey.Space,
|
||||
control: false,
|
||||
shift: false);
|
||||
var dropped = controller.DropSelectedCutsOnPlaylist();
|
||||
|
||||
AssertCutState(selected, [0], focused: 0);
|
||||
Assert.Equal(
|
||||
LegacyStockCutCatalog.Rows[0].RawLabel,
|
||||
Assert.Single(dropped.Playlist).RawCutLabel);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void KeyboardSelectionIsIgnoredUntilAStockIsSelected()
|
||||
{
|
||||
var controller = new LegacyOperatorController(new OneStockLookup());
|
||||
var before = controller.Current;
|
||||
|
||||
var after = controller.CutKeyDown(
|
||||
LegacyCutKeyboardKey.End,
|
||||
control: false,
|
||||
shift: false);
|
||||
|
||||
Assert.Equal(before.Revision, after.Revision);
|
||||
Assert.DoesNotContain(after.CutRows, row => row.IsFocused || row.IsSelected);
|
||||
}
|
||||
|
||||
private static async Task<LegacyOperatorController> CreateReadyControllerAsync()
|
||||
{
|
||||
var controller = new LegacyOperatorController(new OneStockLookup());
|
||||
await controller.SearchStocksAsync("stock", LegacySearchTrigger.Button);
|
||||
controller.SelectStock(0);
|
||||
return controller;
|
||||
}
|
||||
|
||||
private static void AssertCutState(
|
||||
LegacyOperatorSnapshot snapshot,
|
||||
IEnumerable<int> selected,
|
||||
int focused)
|
||||
{
|
||||
Assert.Equal(
|
||||
selected,
|
||||
snapshot.CutRows
|
||||
.Where(row => row.IsSelected)
|
||||
.Select(row => row.PhysicalIndex));
|
||||
Assert.Equal(
|
||||
focused,
|
||||
Assert.Single(snapshot.CutRows, row => row.IsFocused).PhysicalIndex);
|
||||
}
|
||||
|
||||
private sealed class OneStockLookup : ILegacyStockLookup
|
||||
{
|
||||
public Task<LegacyStockSearchData> SearchAsync(
|
||||
string query,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
Task.FromResult(new LegacyStockSearchData(
|
||||
query,
|
||||
["Stock"],
|
||||
[new LegacyStockIdentity(
|
||||
LegacyDomesticStockMarket.Kospi,
|
||||
"Stock",
|
||||
"000001")]));
|
||||
}
|
||||
}
|
||||
@@ -125,33 +125,25 @@ public sealed class LegacyFixedActionCatalogTests
|
||||
Assert.Equal("", result.Selection.DataCode);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false, false, "")]
|
||||
[InlineData(true, false, "MA5")]
|
||||
[InlineData(false, true, "MA20")]
|
||||
[InlineData(true, true, "MA5,MA20")]
|
||||
public void Materialize_ClosesMovingAverageFlags(bool ma5, bool ma20, string expected)
|
||||
[Fact]
|
||||
public void Materialize_LeavesMovingAverageStateOutOfTheActionIdentity()
|
||||
{
|
||||
var result = LegacyFixedActionCatalog.Default.Materialize(
|
||||
"fixed-136",
|
||||
new DateTimeOffset(2026, 7, 15, 9, 0, 0, TimeSpan.FromHours(9)),
|
||||
new LegacyMovingAverageSelection(ma5, ma20));
|
||||
new DateTimeOffset(2026, 7, 15, 9, 0, 0, TimeSpan.FromHours(9)));
|
||||
|
||||
Assert.Equal(expected, result.Selection.GraphicType);
|
||||
Assert.Equal(new LegacyMovingAverageSelection(ma5, ma20), result.MovingAverages);
|
||||
Assert.Equal(string.Empty, result.Selection.GraphicType);
|
||||
Assert.Equal("", result.Action.Selection.GraphicType);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Materialize_IgnoresMovingAverageFlagsForOtherBuilders()
|
||||
public void Materialize_RetainsGraphicTypeForOtherBuilders()
|
||||
{
|
||||
var result = LegacyFixedActionCatalog.Default.Materialize(
|
||||
"fixed-001",
|
||||
DateTimeOffset.Now,
|
||||
new LegacyMovingAverageSelection(true, true));
|
||||
DateTimeOffset.Now);
|
||||
|
||||
Assert.Equal("1열판기본", result.Selection.GraphicType);
|
||||
Assert.Equal(default, result.MovingAverages);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -166,8 +158,7 @@ public sealed class LegacyFixedActionCatalogTests
|
||||
{
|
||||
var before = catalog.Materialize(
|
||||
action.Id,
|
||||
beforeBoundary,
|
||||
new LegacyMovingAverageSelection(true, true));
|
||||
beforeBoundary);
|
||||
Assert.DoesNotContain('$', before.Selection.GroupCode);
|
||||
Assert.DoesNotContain('$', before.Selection.Subject);
|
||||
Assert.DoesNotContain('$', before.Selection.GraphicType);
|
||||
@@ -187,7 +178,7 @@ public sealed class LegacyFixedActionCatalogTests
|
||||
}
|
||||
else if (action.SupportsMovingAverages)
|
||||
{
|
||||
Assert.Equal("MA5,MA20", before.Selection.GraphicType);
|
||||
Assert.Equal(action.Selection.GraphicType, before.Selection.GraphicType);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -181,9 +181,12 @@ public sealed class LegacyOperatorControllerTests
|
||||
var candleRow = candle.Playlist[1];
|
||||
Assert.Equal("8051", candleRow.ToPlayoutEntry().CutCode);
|
||||
Assert.Equal(
|
||||
"MA5",
|
||||
string.Empty,
|
||||
Assert.IsType<MBN_STOCK_WEBVIEW.Core.Playout.Scenes.LegacySceneSelection>(
|
||||
candleRow.ToPlayoutEntry().Selection).GraphicType);
|
||||
Assert.Equal(
|
||||
"MA5",
|
||||
controller.CreatePlayoutRequest().Playlist[1].Selection!.GraphicType);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -242,17 +245,24 @@ public sealed class LegacyOperatorControllerTests
|
||||
{
|
||||
var controller = await ControllerWithPlaylistAsync(0, 1, 2);
|
||||
var rows = controller.Current.Playlist;
|
||||
controller.SelectPlaylistRow(rows[0].RowId, control: false, shift: false);
|
||||
controller.SelectPlaylistRow(rows[1].RowId, control: true, shift: false);
|
||||
|
||||
var last = controller.SelectPlaylistBoundary(last: true);
|
||||
Assert.True(last.Playlist[0].IsEnabled);
|
||||
Assert.True(last.Playlist[2].IsSelected);
|
||||
Assert.True(last.Playlist[2].IsActive);
|
||||
Assert.Equal(
|
||||
rows.Take(2).Select(row => row.RowId),
|
||||
last.Playlist.Where(row => row.IsSelected).Select(row => row.RowId));
|
||||
|
||||
var toggled = controller.ToggleActivePlaylistRowEnabled();
|
||||
Assert.False(toggled.Playlist[2].IsEnabled);
|
||||
|
||||
var first = controller.SelectPlaylistBoundary(last: false);
|
||||
Assert.True(first.Playlist[0].IsSelected);
|
||||
Assert.False(first.Playlist[2].IsSelected);
|
||||
Assert.True(first.Playlist[0].IsActive);
|
||||
Assert.Equal(
|
||||
rows.Take(2).Select(row => row.RowId),
|
||||
first.Playlist.Where(row => row.IsSelected).Select(row => row.RowId));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -307,7 +317,7 @@ public sealed class LegacyOperatorControllerTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MovingAverageFlagsAreCapturedInFixedCandleSelection()
|
||||
public void FixedCandleIdentityStaysStableWhileF8SamplesCurrentMovingAverages()
|
||||
{
|
||||
var controller = new LegacyOperatorController(Lookup());
|
||||
var candle = LegacyFixedActionCatalog.Default
|
||||
@@ -319,11 +329,23 @@ public sealed class LegacyOperatorControllerTests
|
||||
var state = controller.ActivateFixedAction(candle.Id);
|
||||
|
||||
Assert.Equal(
|
||||
"MA20",
|
||||
string.Empty,
|
||||
Assert.IsType<MBN_STOCK_WEBVIEW.Core.Playout.Scenes.LegacySceneSelection>(
|
||||
Assert.Single(state.Playlist).ToPlayoutEntry().Selection).GraphicType);
|
||||
Assert.Equal(
|
||||
"MA20",
|
||||
Assert.Single(controller.CreatePlayoutRequest().Playlist).Selection!.GraphicType);
|
||||
Assert.False(state.MovingAverages.Ma5);
|
||||
Assert.True(state.MovingAverages.Ma20);
|
||||
|
||||
controller.SetMovingAverages(ma5: true, ma20: false);
|
||||
|
||||
Assert.Equal(
|
||||
"MA5",
|
||||
Assert.Single(controller.CreatePlayoutRequest().Playlist).Selection!.GraphicType);
|
||||
Assert.Equal(
|
||||
string.Empty,
|
||||
Assert.Single(controller.Current.Playlist).PlayoutSelection!.GraphicType);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -356,6 +378,9 @@ public sealed class LegacyOperatorControllerTests
|
||||
Assert.Equal("현재가", row.Subtype);
|
||||
Assert.Equal("005930", row.StockCode);
|
||||
Assert.Equal("5001", row.ToPlayoutEntry().CutCode);
|
||||
Assert.True(row.IsActive);
|
||||
Assert.False(row.IsSelected);
|
||||
Assert.Equal(0, controller.CreatePlayoutRequest().SelectedIndexZeroBased);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -558,6 +583,89 @@ public sealed class LegacyOperatorControllerTests
|
||||
Assert.All(state.Playlist, row => Assert.True(row.IsSelected));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CheckboxActivationChangesF8CandidateWithoutReplacingDeleteSelection()
|
||||
{
|
||||
var controller = await ControllerWithPlaylistAsync(0, 1, 2);
|
||||
var ids = controller.Current.Playlist.Select(row => row.RowId).ToArray();
|
||||
controller.SelectPlaylistRow(ids[0], control: false, shift: false);
|
||||
controller.SelectPlaylistRow(ids[1], control: true, shift: false);
|
||||
|
||||
var state = controller.ActivatePlaylistRow(ids[2]);
|
||||
|
||||
Assert.Equal(
|
||||
ids[..2],
|
||||
state.Playlist.Where(row => row.IsSelected).Select(row => row.RowId));
|
||||
Assert.Equal(ids[2], Assert.Single(state.Playlist, row => row.IsActive).RowId);
|
||||
Assert.Equal(2, controller.CreatePlayoutRequest().SelectedIndexZeroBased);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CheckboxActivationDoesNotMoveTheFollowingShiftSelectionAnchor()
|
||||
{
|
||||
var controller = await ControllerWithPlaylistAsync(0, 1, 2, 4);
|
||||
var ids = controller.Current.Playlist.Select(row => row.RowId).ToArray();
|
||||
controller.SelectPlaylistRow(ids[1], control: false, shift: false);
|
||||
|
||||
controller.ActivatePlaylistRow(ids[3]);
|
||||
var range = controller.SelectPlaylistRow(ids[2], control: false, shift: true);
|
||||
|
||||
Assert.Equal(
|
||||
ids[1..3],
|
||||
range.Playlist.Where(row => row.IsSelected).Select(row => row.RowId));
|
||||
Assert.Equal(ids[2], Assert.Single(range.Playlist, row => row.IsActive).RowId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HomeAndEndChangeOnlyTheF8CandidateAndKeepProgramPageProjection()
|
||||
{
|
||||
var controller = await ControllerWithPlaylistAsync(0, 1, 2, 4);
|
||||
var rows = controller.Current.Playlist;
|
||||
controller.SelectPlaylistRow(rows[1].RowId, control: false, shift: false);
|
||||
controller.SelectPlaylistRow(rows[3].RowId, control: true, shift: false);
|
||||
controller.ReflectSuccessfulPlayout(
|
||||
rows[1].RowId,
|
||||
pageIndexZeroBased: 1,
|
||||
pageCount: 3,
|
||||
synchronizeOperatorSelection: false);
|
||||
|
||||
var home = controller.SelectPlaylistBoundary(last: false);
|
||||
Assert.Equal(0, controller.CreatePlayoutRequest().SelectedIndexZeroBased);
|
||||
Assert.Equal("2/3", home.Playlist[1].PageText);
|
||||
Assert.True(home.Playlist[0].IsActive);
|
||||
Assert.Equal(
|
||||
[rows[1].RowId, rows[3].RowId],
|
||||
home.Playlist.Where(row => row.IsSelected).Select(row => row.RowId));
|
||||
|
||||
var end = controller.SelectPlaylistBoundary(last: true);
|
||||
Assert.Equal(3, controller.CreatePlayoutRequest().SelectedIndexZeroBased);
|
||||
Assert.Equal("2/3", end.Playlist[1].PageText);
|
||||
Assert.True(end.Playlist[3].IsActive);
|
||||
|
||||
var shift = controller.SelectPlaylistRow(
|
||||
rows[2].RowId,
|
||||
control: false,
|
||||
shift: true);
|
||||
Assert.Equal(
|
||||
[rows[1].RowId, rows[2].RowId],
|
||||
shift.Playlist.Where(row => row.IsSelected).Select(row => row.RowId));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FirstAppendedRowIsAnF8CandidateButNotAnImplicitDeleteSelection()
|
||||
{
|
||||
var controller = await ControllerWithPlaylistAsync(0);
|
||||
var before = controller.Current;
|
||||
|
||||
Assert.True(Assert.Single(before.Playlist).IsActive);
|
||||
Assert.DoesNotContain(before.Playlist, row => row.IsSelected);
|
||||
Assert.Equal(0, controller.CreatePlayoutRequest().SelectedIndexZeroBased);
|
||||
|
||||
var after = controller.DeleteSelectedPlaylistRows();
|
||||
Assert.Equal(before.Revision, after.Revision);
|
||||
Assert.Single(after.Playlist);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PlaylistEnabledSupportsIndividualToggleAndAllRows()
|
||||
{
|
||||
@@ -591,6 +699,73 @@ public sealed class LegacyOperatorControllerTests
|
||||
[initial[0].RowId, initial[2].RowId],
|
||||
state.Playlist.Select(row => row.RowId));
|
||||
Assert.DoesNotContain(state.Playlist, row => row.IsSelected);
|
||||
Assert.Equal(initial[2].RowId, Assert.Single(state.Playlist, row => row.IsActive).RowId);
|
||||
Assert.Equal(1, controller.CreatePlayoutRequest().SelectedIndexZeroBased);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DeletePreservesAnIndependentCheckboxCandidateByStableIdentity()
|
||||
{
|
||||
var controller = await ControllerWithPlaylistAsync(0, 1, 2, 4);
|
||||
var initial = controller.Current.Playlist.ToArray();
|
||||
controller.SelectPlaylistRow(initial[0].RowId, control: false, shift: false);
|
||||
controller.SelectPlaylistRow(initial[1].RowId, control: true, shift: false);
|
||||
controller.ActivatePlaylistRow(initial[3].RowId);
|
||||
|
||||
var state = controller.DeleteSelectedPlaylistRows();
|
||||
|
||||
Assert.Equal(
|
||||
[initial[2].RowId, initial[3].RowId],
|
||||
state.Playlist.Select(row => row.RowId));
|
||||
Assert.DoesNotContain(state.Playlist, row => row.IsSelected);
|
||||
Assert.Equal(initial[3].RowId, Assert.Single(state.Playlist, row => row.IsActive).RowId);
|
||||
Assert.Equal(1, controller.CreatePlayoutRequest().SelectedIndexZeroBased);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DeleteActiveCandidateChoosesTheNextSurvivorThenThePreviousAtEnd()
|
||||
{
|
||||
var controller = await ControllerWithPlaylistAsync(0, 1, 2, 4);
|
||||
var initial = controller.Current.Playlist.ToArray();
|
||||
controller.SelectPlaylistRow(initial[1].RowId, control: false, shift: false);
|
||||
|
||||
var afterMiddle = controller.DeleteSelectedPlaylistRows();
|
||||
|
||||
Assert.Equal(initial[2].RowId, Assert.Single(afterMiddle.Playlist, row => row.IsActive).RowId);
|
||||
Assert.Equal(1, controller.CreatePlayoutRequest().SelectedIndexZeroBased);
|
||||
|
||||
controller.SelectPlaylistRow(initial[3].RowId, control: false, shift: false);
|
||||
var afterLast = controller.DeleteSelectedPlaylistRows();
|
||||
|
||||
Assert.Equal(initial[2].RowId, Assert.Single(afterLast.Playlist, row => row.IsActive).RowId);
|
||||
Assert.Equal(1, controller.CreatePlayoutRequest().SelectedIndexZeroBased);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ShiftSelectionAndDeleteAreNotLimitedToTheOriginalHundredRowBuffers()
|
||||
{
|
||||
var controller = new LegacyOperatorController(Lookup());
|
||||
await controller.SearchStocksAsync("삼성", LegacySearchTrigger.Button);
|
||||
controller.SelectStock(0);
|
||||
controller.CutPointerDown(1, false, false, 1, 24, 1_000);
|
||||
for (var index = 0; index < 101; index++)
|
||||
{
|
||||
controller.DropSelectedCutsOnPlaylist();
|
||||
}
|
||||
|
||||
var rows = controller.Current.Playlist;
|
||||
Assert.Equal(101, rows.Count);
|
||||
controller.SelectPlaylistRow(rows[0].RowId, control: false, shift: false);
|
||||
var selected = controller.SelectPlaylistRow(
|
||||
rows[^1].RowId,
|
||||
control: false,
|
||||
shift: true);
|
||||
Assert.Equal(101, selected.Playlist.Count(row => row.IsSelected));
|
||||
|
||||
var deleted = controller.DeleteSelectedPlaylistRows();
|
||||
|
||||
Assert.Empty(deleted.Playlist);
|
||||
Assert.Throws<InvalidOperationException>(() => controller.CreatePlayoutRequest());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -610,6 +785,21 @@ public sealed class LegacyOperatorControllerTests
|
||||
Assert.Equal(unchangedRevision, stillEmpty.Revision);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FirstAppendAfterDeleteAllReestablishesTheVisibleF8Candidate()
|
||||
{
|
||||
var controller = await ControllerWithPlaylistAsync(0, 1);
|
||||
controller.DeleteAllPlaylistRows();
|
||||
|
||||
controller.CutPointerDown(2, false, false, 1, 47, 10_000);
|
||||
var state = controller.DropSelectedCutsOnPlaylist();
|
||||
|
||||
var row = Assert.Single(state.Playlist);
|
||||
Assert.True(row.IsActive);
|
||||
Assert.False(row.IsSelected);
|
||||
Assert.Equal(0, controller.CreatePlayoutRequest().SelectedIndexZeroBased);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task MoveUsesOnlyActiveRowAndCollapsesMultiSelectionToMovedRow()
|
||||
{
|
||||
@@ -626,12 +816,17 @@ public sealed class LegacyOperatorControllerTests
|
||||
Assert.Equal(
|
||||
initial[2].RowId,
|
||||
Assert.Single(moved.Playlist, row => row.IsSelected).RowId);
|
||||
Assert.Equal(initial[2].ToPlayoutEntry(), moved.Playlist[1].ToPlayoutEntry());
|
||||
Assert.Equal(initial[2].PageText, moved.Playlist[1].PageText);
|
||||
Assert.Equal(initial[2].RawCutLabel, moved.Playlist[1].RawCutLabel);
|
||||
Assert.Equal(1, controller.CreatePlayoutRequest().SelectedIndexZeroBased);
|
||||
|
||||
var restored = controller.MoveSelectedPlaylistRows(LegacyPlaylistMoveDirection.Down);
|
||||
Assert.Equal(initial.Select(row => row.RowId), restored.Playlist.Select(row => row.RowId));
|
||||
Assert.Equal(
|
||||
initial[2].RowId,
|
||||
Assert.Single(restored.Playlist, row => row.IsSelected).RowId);
|
||||
Assert.Equal(2, controller.CreatePlayoutRequest().SelectedIndexZeroBased);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -651,7 +846,7 @@ public sealed class LegacyOperatorControllerTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PlaylistDragMovesNoncontiguousSelectionAsOneOrderedBlockAndIsIdempotent()
|
||||
public async Task PlaylistDragMovesOnlySourceRowCollapsesSelectionAndIsIdempotent()
|
||||
{
|
||||
var controller = await ControllerWithPlaylistAsync(0, 1, 2, 4, 5, 6);
|
||||
var initial = controller.Current.Playlist.ToArray();
|
||||
@@ -666,16 +861,19 @@ public sealed class LegacyOperatorControllerTests
|
||||
Assert.Equal(
|
||||
[
|
||||
initial[0].RowId,
|
||||
initial[1].RowId,
|
||||
initial[2].RowId,
|
||||
initial[4].RowId,
|
||||
initial[1].RowId,
|
||||
initial[3].RowId,
|
||||
initial[5].RowId
|
||||
],
|
||||
moved.Playlist.Select(row => row.RowId));
|
||||
Assert.Equal(
|
||||
[initial[1].RowId, initial[3].RowId],
|
||||
moved.Playlist.Where(row => row.IsSelected).Select(row => row.RowId));
|
||||
initial[3].RowId,
|
||||
Assert.Single(moved.Playlist, row => row.IsSelected).RowId);
|
||||
Assert.Equal(
|
||||
initial[3].RowId,
|
||||
Assert.Single(moved.Playlist, row => row.IsActive).RowId);
|
||||
|
||||
var revision = moved.Revision;
|
||||
var duplicate = controller.ReorderPlaylistRows(
|
||||
@@ -686,6 +884,9 @@ public sealed class LegacyOperatorControllerTests
|
||||
Assert.Equal(
|
||||
moved.Playlist.Select(row => row.RowId),
|
||||
duplicate.Playlist.Select(row => row.RowId));
|
||||
Assert.Equal(
|
||||
initial[3].RowId,
|
||||
Assert.Single(duplicate.Playlist, row => row.IsSelected).RowId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -34,13 +34,16 @@ public sealed class LegacyOverseasOperatorControllerTests
|
||||
Assert.Equal("legacy-overseas-00000001", row.RowId);
|
||||
Assert.Equal("FOREIGN_INDEX", row.MarketText);
|
||||
Assert.Equal("나스닥 캔들 20일", row.StockName);
|
||||
Assert.Equal("MA20", row.GraphicType);
|
||||
Assert.Equal("캔들그래프", row.GraphicType);
|
||||
Assert.Equal("PRICE", row.Subtype);
|
||||
Assert.Equal("1/1", row.PageText);
|
||||
Assert.Equal("8040", entry.CutCode);
|
||||
Assert.Equal("FOREIGN_INDEX", selection.GroupCode);
|
||||
Assert.Equal("나스닥", selection.Subject);
|
||||
Assert.Equal("NAS@IXIC|US|0", selection.DataCode);
|
||||
Assert.Equal(
|
||||
"MA20",
|
||||
Assert.Single(controller.CreatePlayoutRequest().Playlist).Selection!.GraphicType);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -72,11 +75,15 @@ public sealed class LegacyOverseasOperatorControllerTests
|
||||
Assert.Equal(2, stockState.Playlist.Count);
|
||||
var stockRow = stockState.Playlist[1];
|
||||
Assert.Equal("8061", stockRow.ToPlayoutEntry().CutCode);
|
||||
Assert.Equal("캔들그래프", stockRow.GraphicType);
|
||||
Assert.Equal("FOREIGN_STOCK", stockRow.PlayoutSelection!.GroupCode);
|
||||
Assert.Equal("Apple", stockRow.PlayoutSelection.Subject);
|
||||
Assert.Equal("MA5,MA20", stockRow.PlayoutSelection.GraphicType);
|
||||
Assert.Equal(string.Empty, stockRow.PlayoutSelection.GraphicType);
|
||||
Assert.Equal("PRICE", stockRow.PlayoutSelection.Subtype);
|
||||
Assert.Equal("NAS@AAPL|US|1", stockRow.PlayoutSelection.DataCode);
|
||||
Assert.Equal(
|
||||
"MA5,MA20",
|
||||
controller.CreatePlayoutRequest().Playlist[1].Selection!.GraphicType);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -0,0 +1,420 @@
|
||||
using System.Text;
|
||||
using MBN_STOCK_WEBVIEW.LegacyApplication;
|
||||
|
||||
namespace MBN_STOCK_WEBVIEW.LegacyApplication.Tests;
|
||||
|
||||
public sealed class LegacyRuntimeStockCutMenuLoaderTests
|
||||
{
|
||||
[Fact]
|
||||
public void TryParse_UsesCp949AndPreservesTrimmedPhysicalOrder()
|
||||
{
|
||||
const string first = "1열판기본_현재가";
|
||||
Assert.Equal(first, LegacyStockCutCatalog.Rows[1].RawLabel);
|
||||
var second = LegacyStockCutCatalog.Rows[0].RawLabel;
|
||||
var bytes = EncodeCp949($" \r\n [종목] \r\n {first} \r\n\r\n - \r\n{second}\r\n{first}\r\n");
|
||||
|
||||
var loaded = LegacyRuntimeStockCutMenuLoader.TryParse(
|
||||
bytes,
|
||||
out var rows,
|
||||
out var failure);
|
||||
|
||||
Assert.True(loaded);
|
||||
Assert.Equal(LegacyRuntimeStockCutMenuFailure.None, failure);
|
||||
Assert.Collection(
|
||||
rows,
|
||||
row => Assert.Equal((0, 1, first, false), Fields(row)),
|
||||
row => Assert.Equal((1, null, string.Empty, true), Fields(row)),
|
||||
row => Assert.Equal((2, 2, second, false), Fields(row)),
|
||||
row => Assert.Equal((3, 3, first, false), Fields(row)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryLoad_ReadsAStandaloneRuntimeFileWithoutBundlingTheAsset()
|
||||
{
|
||||
var directory = Path.Combine(
|
||||
Path.GetTempPath(),
|
||||
$"mbn-stock-cut-menu-{Guid.NewGuid():N}");
|
||||
var path = Path.Combine(directory, "종목.ini");
|
||||
Directory.CreateDirectory(directory);
|
||||
try
|
||||
{
|
||||
var label = LegacyStockCutCatalog.Rows[0].RawLabel;
|
||||
File.WriteAllBytes(path, EncodeCp949($"[종목]\r\n{label}\r\n"));
|
||||
|
||||
var loaded = LegacyRuntimeStockCutMenuLoader.TryLoad(
|
||||
path,
|
||||
out var rows,
|
||||
out var failure);
|
||||
|
||||
Assert.True(loaded);
|
||||
Assert.Equal(LegacyRuntimeStockCutMenuFailure.None, failure);
|
||||
var row = Assert.Single(rows);
|
||||
Assert.Equal((0, 1, label, false), Fields(row));
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(directory, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(50, true, LegacyRuntimeStockCutMenuFailure.None)]
|
||||
[InlineData(51, false, LegacyRuntimeStockCutMenuFailure.TooManyRows)]
|
||||
public void TryParse_EnforcesTheLegacyFiftyPhysicalRowLimit(
|
||||
int rowCount,
|
||||
bool expectedSuccess,
|
||||
LegacyRuntimeStockCutMenuFailure expectedFailure)
|
||||
{
|
||||
var label = LegacyStockCutCatalog.Rows[0].RawLabel;
|
||||
var text = LegacyRuntimeStockCutMenuLoader.StockSectionHeader + "\r\n" +
|
||||
string.Join("\r\n", Enumerable.Repeat(label, rowCount));
|
||||
|
||||
var loaded = LegacyRuntimeStockCutMenuLoader.TryParse(
|
||||
EncodeCp949(text),
|
||||
out var rows,
|
||||
out var failure);
|
||||
|
||||
Assert.Equal(expectedSuccess, loaded);
|
||||
Assert.Equal(expectedFailure, failure);
|
||||
Assert.Equal(expectedSuccess ? rowCount : 0, rows.Count);
|
||||
}
|
||||
|
||||
public static TheoryData<string, LegacyRuntimeStockCutMenuFailure> MalformedMenus => new()
|
||||
{
|
||||
{ string.Empty, LegacyRuntimeStockCutMenuFailure.InvalidFormat },
|
||||
{ "[종목", LegacyRuntimeStockCutMenuFailure.InvalidFormat },
|
||||
{ LegacyStockCutCatalog.Rows[0].RawLabel, LegacyRuntimeStockCutMenuFailure.InvalidFormat },
|
||||
{ "[다른메뉴]\r\n-", LegacyRuntimeStockCutMenuFailure.InvalidFormat },
|
||||
{ "[종목]\r\n[다른메뉴]", LegacyRuntimeStockCutMenuFailure.InvalidFormat },
|
||||
{ "[종목]", LegacyRuntimeStockCutMenuFailure.EmptyMenu },
|
||||
{ "[종목]\r\n-", LegacyRuntimeStockCutMenuFailure.EmptyMenu },
|
||||
{ "[종목]\r\n지원하지않는_컷", LegacyRuntimeStockCutMenuFailure.UnknownCut }
|
||||
};
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(MalformedMenus))]
|
||||
public void TryParse_FailsClosedWithoutReturningPartialRows(
|
||||
string text,
|
||||
LegacyRuntimeStockCutMenuFailure expectedFailure)
|
||||
{
|
||||
var loaded = LegacyRuntimeStockCutMenuLoader.TryParse(
|
||||
EncodeCp949(text),
|
||||
out var rows,
|
||||
out var failure);
|
||||
|
||||
Assert.False(loaded);
|
||||
Assert.Empty(rows);
|
||||
Assert.Equal(expectedFailure, failure);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryParse_RejectsAnIncompleteCp949Sequence()
|
||||
{
|
||||
var loaded = LegacyRuntimeStockCutMenuLoader.TryParse(
|
||||
new byte[] { 0x81 },
|
||||
out var rows,
|
||||
out var failure);
|
||||
|
||||
Assert.False(loaded);
|
||||
Assert.Empty(rows);
|
||||
Assert.Equal(LegacyRuntimeStockCutMenuFailure.InvalidEncoding, failure);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryLoad_MissingOrBlankPathLetsTheCallerUseTheBuiltInCatalog()
|
||||
{
|
||||
var missing = Path.Combine(
|
||||
Path.GetTempPath(),
|
||||
$"missing-cut-menu-{Guid.NewGuid():N}.ini");
|
||||
|
||||
Assert.False(LegacyRuntimeStockCutMenuLoader.TryLoad(
|
||||
missing,
|
||||
out var missingRows,
|
||||
out var missingFailure));
|
||||
Assert.Empty(missingRows);
|
||||
Assert.Equal(LegacyRuntimeStockCutMenuFailure.FileNotFound, missingFailure);
|
||||
|
||||
Assert.False(LegacyRuntimeStockCutMenuLoader.TryLoad(
|
||||
" ",
|
||||
out var blankRows,
|
||||
out var blankFailure));
|
||||
Assert.Empty(blankRows);
|
||||
Assert.Equal(LegacyRuntimeStockCutMenuFailure.InvalidPath, blankFailure);
|
||||
Assert.NotEmpty(LegacyStockCutCatalog.Rows);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryLoad_RejectsAnOversizedFileBeforeAllocatingOrDecodingItsContent()
|
||||
{
|
||||
var path = Path.Combine(
|
||||
Path.GetTempPath(),
|
||||
$"oversized-cut-menu-{Guid.NewGuid():N}.ini");
|
||||
try
|
||||
{
|
||||
using (var stream = new FileStream(path, FileMode.CreateNew, FileAccess.Write))
|
||||
{
|
||||
stream.SetLength(LegacyRuntimeStockCutMenuLoader.MaximumFileBytes + 1L);
|
||||
}
|
||||
|
||||
var loaded = LegacyRuntimeStockCutMenuLoader.TryLoad(
|
||||
path,
|
||||
out var rows,
|
||||
out var failure);
|
||||
|
||||
Assert.False(loaded);
|
||||
Assert.Empty(rows);
|
||||
Assert.Equal(LegacyRuntimeStockCutMenuFailure.FileTooLarge, failure);
|
||||
}
|
||||
finally
|
||||
{
|
||||
File.Delete(path);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResolveFromBaseDirectory_MissingFileReturnsBoundedPathFreeFallback()
|
||||
{
|
||||
var directory = Path.Combine(
|
||||
Path.GetTempPath(),
|
||||
$"mbn-stock-cut-composition-{Guid.NewGuid():N}");
|
||||
Directory.CreateDirectory(directory);
|
||||
try
|
||||
{
|
||||
var composition =
|
||||
LegacyRuntimeStockCutMenuLoader.ResolveFromBaseDirectory(directory);
|
||||
|
||||
Assert.True(composition.UsesBuiltInCatalog);
|
||||
Assert.Equal(LegacyRuntimeStockCutMenuSource.BuiltIn, composition.Source);
|
||||
Assert.Same(LegacyStockCutCatalog.Rows, composition.Rows);
|
||||
Assert.Equal(
|
||||
LegacyRuntimeStockCutMenuFailure.FileNotFound,
|
||||
composition.Failure);
|
||||
Assert.Equal(
|
||||
LegacyRuntimeStockCutMenuLoader.FallbackWarningMessage,
|
||||
composition.WarningMessage);
|
||||
var warning = Assert.IsType<string>(composition.WarningMessage);
|
||||
Assert.InRange(warning.Length, 1, 80);
|
||||
Assert.DoesNotContain(directory, warning,
|
||||
StringComparison.OrdinalIgnoreCase);
|
||||
Assert.DoesNotContain("종목.ini", warning,
|
||||
StringComparison.Ordinal);
|
||||
Assert.DoesNotContain('\\', warning);
|
||||
Assert.DoesNotContain('/', warning);
|
||||
var controller = new LegacyOperatorController(
|
||||
new EmptyStockLookup(),
|
||||
cutRows: composition.Rows);
|
||||
var warningState = controller.ReportWarning(warning);
|
||||
Assert.Equal(LegacyOperatorStatusKind.Warning, warningState.StatusKind);
|
||||
Assert.Equal(composition.WarningMessage, warningState.StatusMessage);
|
||||
|
||||
var resourceDirectory = Path.Combine(directory, "Res");
|
||||
Directory.CreateDirectory(resourceDirectory);
|
||||
File.WriteAllBytes(
|
||||
Path.Combine(resourceDirectory, "종목.ini"),
|
||||
EncodeCp949("[종목]\r\n지원하지않는_컷\r\n"));
|
||||
var invalid =
|
||||
LegacyRuntimeStockCutMenuLoader.ResolveFromBaseDirectory(directory);
|
||||
Assert.True(invalid.UsesBuiltInCatalog);
|
||||
Assert.Same(LegacyStockCutCatalog.Rows, invalid.Rows);
|
||||
Assert.Equal(LegacyRuntimeStockCutMenuFailure.UnknownCut, invalid.Failure);
|
||||
Assert.Equal(composition.WarningMessage, invalid.WarningMessage);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(directory, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResolveFromBaseDirectory_ValidRuntimeFileReturnsOnlyItsRows()
|
||||
{
|
||||
var directory = Path.Combine(
|
||||
Path.GetTempPath(),
|
||||
$"mbn-stock-cut-composition-{Guid.NewGuid():N}");
|
||||
var resourceDirectory = Path.Combine(directory, "Res");
|
||||
Directory.CreateDirectory(resourceDirectory);
|
||||
try
|
||||
{
|
||||
const string label = "1열판기본_현재가";
|
||||
File.WriteAllBytes(
|
||||
Path.Combine(resourceDirectory, "종목.ini"),
|
||||
EncodeCp949($"[종목]\r\n{label}\r\n"));
|
||||
|
||||
var composition =
|
||||
LegacyRuntimeStockCutMenuLoader.ResolveFromBaseDirectory(directory);
|
||||
|
||||
Assert.False(composition.UsesBuiltInCatalog);
|
||||
Assert.Equal(LegacyRuntimeStockCutMenuSource.Packaged, composition.Source);
|
||||
Assert.Equal(LegacyRuntimeStockCutMenuFailure.None, composition.Failure);
|
||||
Assert.Null(composition.WarningMessage);
|
||||
Assert.Equal(label, Assert.Single(composition.Rows).RawLabel);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(directory, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResolveFromCandidates_ExternalOverrideTakesPrecedenceOverPackagedMenu()
|
||||
{
|
||||
var paths = CreateCandidatePaths();
|
||||
try
|
||||
{
|
||||
WriteMenu(paths.ExternalPath, "1열판기본_현재가");
|
||||
WriteMenu(paths.PackagedPath, "1열판기본_예상체결가");
|
||||
|
||||
var composition = LegacyRuntimeStockCutMenuLoader.ResolveFromCandidates(
|
||||
paths.ExternalPath,
|
||||
paths.PackagedBaseDirectory);
|
||||
|
||||
Assert.Equal(
|
||||
LegacyRuntimeStockCutMenuSource.ExternalOverride,
|
||||
composition.Source);
|
||||
Assert.False(composition.UsesBuiltInCatalog);
|
||||
Assert.Null(composition.WarningMessage);
|
||||
Assert.Equal(
|
||||
"1열판기본_현재가",
|
||||
Assert.Single(composition.Rows).RawLabel);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(paths.Root, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResolveFromCandidates_MissingExternalContinuesToPackagedMenu()
|
||||
{
|
||||
var paths = CreateCandidatePaths();
|
||||
try
|
||||
{
|
||||
WriteMenu(paths.PackagedPath, "1열판기본_예상체결가");
|
||||
|
||||
var composition = LegacyRuntimeStockCutMenuLoader.ResolveFromCandidates(
|
||||
paths.ExternalPath,
|
||||
paths.PackagedBaseDirectory);
|
||||
|
||||
Assert.Equal(LegacyRuntimeStockCutMenuSource.Packaged, composition.Source);
|
||||
Assert.False(composition.UsesBuiltInCatalog);
|
||||
Assert.Null(composition.WarningMessage);
|
||||
Assert.Equal(
|
||||
"1열판기본_예상체결가",
|
||||
Assert.Single(composition.Rows).RawLabel);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(paths.Root, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResolveFromCandidates_InvalidExternalFailsClosedWithoutPackagedFallback()
|
||||
{
|
||||
var paths = CreateCandidatePaths();
|
||||
try
|
||||
{
|
||||
WriteMenu(paths.ExternalPath, "지원하지않는_컷");
|
||||
WriteMenu(paths.PackagedPath, "1열판기본_예상체결가");
|
||||
|
||||
var composition = LegacyRuntimeStockCutMenuLoader.ResolveFromCandidates(
|
||||
paths.ExternalPath,
|
||||
paths.PackagedBaseDirectory);
|
||||
|
||||
Assert.Equal(LegacyRuntimeStockCutMenuSource.BuiltIn, composition.Source);
|
||||
Assert.True(composition.UsesBuiltInCatalog);
|
||||
Assert.Same(LegacyStockCutCatalog.Rows, composition.Rows);
|
||||
Assert.Equal(LegacyRuntimeStockCutMenuFailure.UnknownCut, composition.Failure);
|
||||
Assert.Equal(
|
||||
LegacyRuntimeStockCutMenuLoader.FallbackWarningMessage,
|
||||
composition.WarningMessage);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(paths.Root, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResolveFromBaseDirectory_RejectsInvalidOrControlContainingPathsWithoutLeakage()
|
||||
{
|
||||
string?[] invalidPaths =
|
||||
[
|
||||
null,
|
||||
string.Empty,
|
||||
" ",
|
||||
"relative\\package",
|
||||
"C:\\sensitive-root\nprivate",
|
||||
"C:\\sensitive-root\0private"
|
||||
];
|
||||
|
||||
foreach (var invalidPath in invalidPaths)
|
||||
{
|
||||
var composition =
|
||||
LegacyRuntimeStockCutMenuLoader.ResolveFromBaseDirectory(invalidPath);
|
||||
|
||||
Assert.True(composition.UsesBuiltInCatalog);
|
||||
Assert.Equal(LegacyRuntimeStockCutMenuSource.BuiltIn, composition.Source);
|
||||
Assert.Same(LegacyStockCutCatalog.Rows, composition.Rows);
|
||||
Assert.Equal(
|
||||
LegacyRuntimeStockCutMenuFailure.InvalidPath,
|
||||
composition.Failure);
|
||||
Assert.Equal(
|
||||
LegacyRuntimeStockCutMenuLoader.FallbackWarningMessage,
|
||||
composition.WarningMessage);
|
||||
Assert.DoesNotContain("sensitive-root", composition.WarningMessage!,
|
||||
StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
|
||||
private static (int PhysicalIndex, int? DisplayOrdinal, string RawLabel, bool IsSeparator)
|
||||
Fields(LegacyCutCatalogRow row) =>
|
||||
(row.PhysicalIndex, row.DisplayOrdinal, row.RawLabel, row.IsSeparator);
|
||||
|
||||
private static byte[] EncodeCp949(string text)
|
||||
{
|
||||
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
|
||||
var cp949 = Encoding.GetEncoding(
|
||||
949,
|
||||
EncoderFallback.ExceptionFallback,
|
||||
DecoderFallback.ExceptionFallback);
|
||||
return cp949.GetBytes(text);
|
||||
}
|
||||
|
||||
private static (
|
||||
string Root,
|
||||
string ExternalPath,
|
||||
string PackagedBaseDirectory,
|
||||
string PackagedPath) CreateCandidatePaths()
|
||||
{
|
||||
var root = Path.Combine(
|
||||
Path.GetTempPath(),
|
||||
$"mbn-stock-cut-candidates-{Guid.NewGuid():N}");
|
||||
var externalPath = Path.Combine(
|
||||
root,
|
||||
"local",
|
||||
"MBN_STOCK_WEBVIEW",
|
||||
"Res",
|
||||
"종목.ini");
|
||||
var packagedBaseDirectory = Path.Combine(root, "package");
|
||||
return (
|
||||
root,
|
||||
externalPath,
|
||||
packagedBaseDirectory,
|
||||
Path.Combine(packagedBaseDirectory, "Res", "종목.ini"));
|
||||
}
|
||||
|
||||
private static void WriteMenu(string path, string label)
|
||||
{
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(path)!);
|
||||
File.WriteAllBytes(path, EncodeCp949($"[종목]\r\n{label}\r\n"));
|
||||
}
|
||||
|
||||
private sealed class EmptyStockLookup : ILegacyStockLookup
|
||||
{
|
||||
public Task<LegacyStockSearchData> SearchAsync(
|
||||
string query,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
Task.FromResult(new LegacyStockSearchData(query, [], []));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,362 @@
|
||||
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<LegacyCutCatalogRow> 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<ArgumentException>(() => 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<LegacyStockSearchData> 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<LegacyStockSearchData> SearchAsync(
|
||||
string text,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
Task.FromResult(new LegacyStockSearchData(
|
||||
text,
|
||||
["Next stock(NXT)"],
|
||||
[new LegacyStockIdentity(
|
||||
LegacyDomesticStockMarket.NxtKospi,
|
||||
"Next stock(NXT)",
|
||||
"A00001")]));
|
||||
}
|
||||
}
|
||||
@@ -101,6 +101,181 @@ public sealed class LegacyTabActivationParityTests
|
||||
Assert.Single(retried.Theme!.SearchResults);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(LegacyOperatorTabId.KospiIndustry, IndustryMarket.Kospi)]
|
||||
[InlineData(LegacyOperatorTabId.KosdaqIndustry, IndustryMarket.Kosdaq)]
|
||||
public async Task IndustryEachActivation_ReloadsAndClearsUc2Selections(
|
||||
LegacyOperatorTabId tabId,
|
||||
IndustryMarket market)
|
||||
{
|
||||
var service = new CountingIndustryService();
|
||||
var controller = new LegacyOperatorController(
|
||||
Lookup(),
|
||||
industryWorkflow: new LegacyIndustrySelectionWorkflow(service));
|
||||
|
||||
await controller.SelectTabAsync(tabId);
|
||||
controller.SelectIndustry(0, doubleClick: true);
|
||||
var selected = controller.SelectIndustry(1, doubleClick: true);
|
||||
Assert.NotNull(selected.Industry!.SelectedIndustry);
|
||||
Assert.NotNull(selected.Industry.FirstIndustry);
|
||||
Assert.NotNull(selected.Industry.SecondIndustry);
|
||||
|
||||
await controller.SelectTabAsync(tabId);
|
||||
Assert.Single(service.Markets);
|
||||
|
||||
await controller.SelectTabAsync(LegacyOperatorTabId.Index);
|
||||
var reentered = await controller.SelectTabAsync(tabId);
|
||||
|
||||
Assert.Equal([market, market], service.Markets);
|
||||
Assert.Equal(2, reentered.Industry!.Industries.Count);
|
||||
Assert.Null(reentered.Industry.SelectedIndex);
|
||||
Assert.Null(reentered.Industry.SelectedIndustry);
|
||||
Assert.Null(reentered.Industry.FirstIndustry);
|
||||
Assert.Null(reentered.Industry.SecondIndustry);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ComparisonEachActivation_ReloadsPairsAndClearsUc3DraftAndSearch()
|
||||
{
|
||||
var store = new CountingComparisonPairStore();
|
||||
var controller = new LegacyOperatorController(
|
||||
Lookup(),
|
||||
comparisonWorkflow: new LegacyComparisonWorkflowController(
|
||||
new StaticComparisonDataService(),
|
||||
store));
|
||||
|
||||
await controller.SelectTabAsync(LegacyOperatorTabId.Comparison);
|
||||
var searched = await controller.SearchComparisonDomesticAsync("semiconductor");
|
||||
var results = searched.Comparison!.DomesticSearch.Results;
|
||||
controller.ChooseComparisonDomesticResult(results[0].SelectionId);
|
||||
var drafted = controller.ChooseComparisonDomesticResult(results[1].SelectionId);
|
||||
Assert.NotNull(drafted.Comparison!.FirstTarget);
|
||||
Assert.NotNull(drafted.Comparison.SecondTarget);
|
||||
|
||||
await controller.SelectTabAsync(LegacyOperatorTabId.Comparison);
|
||||
Assert.Equal(1, store.ReadCount);
|
||||
|
||||
await controller.SelectTabAsync(LegacyOperatorTabId.Index);
|
||||
var reentered = await controller.SelectTabAsync(LegacyOperatorTabId.Comparison);
|
||||
|
||||
Assert.Equal(2, store.ReadCount);
|
||||
Assert.Equal(LegacyComparisonSearchStatus.Idle, reentered.Comparison!.DomesticSearch.Status);
|
||||
Assert.Empty(reentered.Comparison.DomesticSearch.Results);
|
||||
Assert.Equal(string.Empty, reentered.Comparison.DomesticSearch.Query);
|
||||
Assert.Null(reentered.Comparison.FirstTarget);
|
||||
Assert.Null(reentered.Comparison.SecondTarget);
|
||||
Assert.Empty(reentered.Comparison.SavedPairs);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ComparisonReentry_InvalidatesPairLoadFromDiscardedUc3()
|
||||
{
|
||||
var store = new DelayedComparisonPairStore();
|
||||
var controller = new LegacyOperatorController(
|
||||
Lookup(),
|
||||
comparisonWorkflow: new LegacyComparisonWorkflowController(
|
||||
new StaticComparisonDataService(),
|
||||
store));
|
||||
|
||||
var firstActivation = controller.SelectTabAsync(LegacyOperatorTabId.Comparison);
|
||||
await store.FirstReadStarted.Task.WaitAsync(TimeSpan.FromSeconds(5));
|
||||
controller.SelectTab(LegacyOperatorTabId.Index);
|
||||
controller.SelectTab(LegacyOperatorTabId.Comparison);
|
||||
store.ReleaseFirstRead.TrySetResult();
|
||||
await firstActivation;
|
||||
|
||||
Assert.Equal(LegacyOperatorStatusKind.Neutral, controller.Current.Comparison!.StatusKind);
|
||||
Assert.Equal(string.Empty, controller.Current.Comparison.StatusMessage);
|
||||
Assert.Empty(controller.Current.Comparison.SavedPairs);
|
||||
|
||||
var reloaded = await controller.SelectTabAsync(LegacyOperatorTabId.Comparison);
|
||||
Assert.Equal(2, store.ReadCount);
|
||||
Assert.Equal(LegacyOperatorStatusKind.Information, reloaded.Comparison!.StatusKind);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task OverseasStocksReentry_ResetsUc5StateAndKeepsMainMovingAverages()
|
||||
{
|
||||
var workflow = new LegacyOverseasSelectionWorkflow(
|
||||
new StaticOverseasIndustryService(),
|
||||
new StaticOverseasStockService());
|
||||
var controller = new LegacyOperatorController(Lookup(), overseasWorkflow: workflow);
|
||||
controller.SelectTab(LegacyOperatorTabId.OverseasStocks);
|
||||
controller.SelectOverseasFixedIndex("nasdaq");
|
||||
var industries = await controller.SearchOverseasIndustriesAsync("industry");
|
||||
controller.SelectOverseasIndustry(
|
||||
Assert.Single(industries.Overseas!.Industry.Results).SelectionId);
|
||||
var stocks = await controller.SearchOverseasStocksAsync("stock");
|
||||
controller.SelectOverseasStock(
|
||||
Assert.Single(stocks.Overseas!.Stock.Results).SelectionId);
|
||||
controller.SetMovingAverages(ma5: false, ma20: true);
|
||||
|
||||
controller.SelectTab(LegacyOperatorTabId.Index);
|
||||
var reentered = controller.SelectTab(LegacyOperatorTabId.OverseasStocks);
|
||||
|
||||
Assert.Null(reentered.Overseas!.SelectedFixedIndex);
|
||||
Assert.Equal(LegacyOverseasSearchStatus.Idle, reentered.Overseas.Industry.Status);
|
||||
Assert.Equal(string.Empty, reentered.Overseas.Industry.Query);
|
||||
Assert.Empty(reentered.Overseas.Industry.Results);
|
||||
Assert.Null(reentered.Overseas.Industry.Selected);
|
||||
Assert.Equal(LegacyOverseasSearchStatus.Idle, reentered.Overseas.Stock.Status);
|
||||
Assert.Equal(string.Empty, reentered.Overseas.Stock.Query);
|
||||
Assert.Empty(reentered.Overseas.Stock.Results);
|
||||
Assert.Null(reentered.Overseas.Stock.Selected);
|
||||
Assert.Equal(
|
||||
new LegacyOverseasMovingAverageSelection(false, true),
|
||||
reentered.Overseas.MovingAverages);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TradingHaltReentry_ResetsUc7QuerySelectionAndSort()
|
||||
{
|
||||
var controller = new LegacyOperatorController(
|
||||
Lookup(),
|
||||
tradingHaltWorkflow: new LegacyTradingHaltWorkflowController(
|
||||
new StaticTradingHaltService()));
|
||||
controller.SelectTab(LegacyOperatorTabId.TradingHalt);
|
||||
var searched = await controller.SearchTradingHaltsAsync("halted");
|
||||
controller.SelectTradingHalt(
|
||||
Assert.Single(searched.TradingHalt!.Results).SelectionId);
|
||||
controller.ToggleTradingHaltSort();
|
||||
controller.SetMovingAverages(ma5: false, ma20: true);
|
||||
|
||||
controller.SelectTab(LegacyOperatorTabId.Index);
|
||||
var reentered = controller.SelectTab(LegacyOperatorTabId.TradingHalt);
|
||||
|
||||
Assert.Equal(LegacyTradingHaltRequestStatus.Idle, reentered.TradingHalt!.Status);
|
||||
Assert.Equal(string.Empty, reentered.TradingHalt.Query);
|
||||
Assert.Empty(reentered.TradingHalt.Results);
|
||||
Assert.Null(reentered.TradingHalt.Selected);
|
||||
Assert.Equal(LegacyTradingHaltNameSort.None, reentered.TradingHalt.NameSort);
|
||||
Assert.False(reentered.TradingHalt.Ma5);
|
||||
Assert.True(reentered.TradingHalt.Ma20);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task OverseasReentry_InvalidatesSearchCompletionFromDiscardedUc5()
|
||||
{
|
||||
var service = new DelayedOverseasIndustryService();
|
||||
var controller = new LegacyOperatorController(
|
||||
Lookup(),
|
||||
overseasWorkflow: new LegacyOverseasSelectionWorkflow(
|
||||
service,
|
||||
new StaticOverseasStockService()));
|
||||
controller.SelectTab(LegacyOperatorTabId.OverseasStocks);
|
||||
|
||||
var pending = controller.SearchOverseasIndustriesAsync("old");
|
||||
await service.RequestStarted.Task.WaitAsync(TimeSpan.FromSeconds(5));
|
||||
controller.SelectTab(LegacyOperatorTabId.Index);
|
||||
controller.SelectTab(LegacyOperatorTabId.OverseasStocks);
|
||||
service.ReleaseRequest.TrySetResult();
|
||||
await pending;
|
||||
|
||||
Assert.Equal(LegacyOverseasSearchStatus.Idle, controller.Current.Overseas!.Industry.Status);
|
||||
Assert.Equal(string.Empty, controller.Current.Overseas.Industry.Query);
|
||||
Assert.Empty(controller.Current.Overseas.Industry.Results);
|
||||
}
|
||||
|
||||
private static ILegacyStockLookup Lookup() =>
|
||||
new StaticLookup(new LegacyStockSearchData(string.Empty, [], []));
|
||||
|
||||
@@ -213,4 +388,160 @@ public sealed class LegacyTabActivationParityTests
|
||||
CancellationToken cancellationToken = default) => Task.FromResult(
|
||||
new ExpertPreviewResult(identity, DateTimeOffset.UtcNow, [], false));
|
||||
}
|
||||
|
||||
private sealed class CountingIndustryService : IIndustrySelectionService
|
||||
{
|
||||
public List<IndustryMarket> Markets { get; } = [];
|
||||
|
||||
public Task<IReadOnlyList<IndustrySelection>> GetAsync(
|
||||
IndustryMarket market,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
Markets.Add(market);
|
||||
return Task.FromResult<IReadOnlyList<IndustrySelection>>(
|
||||
[
|
||||
new(market, "Industry 1", "001"),
|
||||
new(market, "Industry 2", "002")
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class StaticComparisonDataService : ILegacyComparisonDataService
|
||||
{
|
||||
private static readonly IReadOnlyList<LegacyComparisonTarget> Domestic =
|
||||
[
|
||||
LegacyComparisonTarget.CreateKrxStock(
|
||||
LegacyComparisonDomesticMarket.Kospi,
|
||||
"Samsung Electronics",
|
||||
"005930"),
|
||||
LegacyComparisonTarget.CreateKrxStock(
|
||||
LegacyComparisonDomesticMarket.Kospi,
|
||||
"SK Hynix",
|
||||
"000660")
|
||||
];
|
||||
|
||||
public Task<IReadOnlyList<LegacyComparisonTarget>> SearchDomesticAsync(
|
||||
string query,
|
||||
int maximumResults,
|
||||
CancellationToken cancellationToken = default) => Task.FromResult(Domestic);
|
||||
|
||||
public Task<IReadOnlyList<LegacyComparisonTarget>> SearchWorldAsync(
|
||||
string query,
|
||||
int maximumResults,
|
||||
CancellationToken cancellationToken = default) => Task.FromResult<
|
||||
IReadOnlyList<LegacyComparisonTarget>>([]);
|
||||
|
||||
public Task<LegacyComparisonTarget?> ResolveStoredTargetAsync(
|
||||
LegacyComparisonStoredTargetReference target,
|
||||
CancellationToken cancellationToken = default) => Task.FromResult<
|
||||
LegacyComparisonTarget?>(null);
|
||||
}
|
||||
|
||||
private sealed class CountingComparisonPairStore : ILegacyComparisonPairStore
|
||||
{
|
||||
public int ReadCount { get; private set; }
|
||||
|
||||
public Task<byte[]?> ReadCp949Async(CancellationToken cancellationToken = default)
|
||||
{
|
||||
ReadCount++;
|
||||
return Task.FromResult<byte[]?>(null);
|
||||
}
|
||||
|
||||
public Task WriteCp949Async(
|
||||
IReadOnlyList<LegacyComparisonPairWriteRow> rows,
|
||||
CancellationToken cancellationToken = default) => Task.CompletedTask;
|
||||
}
|
||||
|
||||
private sealed class DelayedComparisonPairStore : ILegacyComparisonPairStore
|
||||
{
|
||||
public TaskCompletionSource FirstReadStarted { get; } = new(
|
||||
TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
|
||||
public TaskCompletionSource ReleaseFirstRead { get; } = new(
|
||||
TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
|
||||
public int ReadCount { get; private set; }
|
||||
|
||||
public async Task<byte[]?> ReadCp949Async(
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
ReadCount++;
|
||||
if (ReadCount == 1)
|
||||
{
|
||||
FirstReadStarted.TrySetResult();
|
||||
await ReleaseFirstRead.Task.WaitAsync(cancellationToken);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public Task WriteCp949Async(
|
||||
IReadOnlyList<LegacyComparisonPairWriteRow> rows,
|
||||
CancellationToken cancellationToken = default) => Task.CompletedTask;
|
||||
}
|
||||
|
||||
private sealed class StaticOverseasIndustryService : IOverseasIndustryIndexSearchService
|
||||
{
|
||||
public Task<OverseasIndustryIndexSearchResult> SearchAsync(
|
||||
string query,
|
||||
int maximumResults = LegacyOverseasIndustryIndexSearchService.DefaultMaximumResults,
|
||||
CancellationToken cancellationToken = default) => Task.FromResult(
|
||||
new OverseasIndustryIndexSearchResult(
|
||||
query.Trim(),
|
||||
DateTimeOffset.UtcNow,
|
||||
[new OverseasIndustryIndexItem("Semiconductor", "US Semiconductor", "NAS@SOX")],
|
||||
false));
|
||||
}
|
||||
|
||||
private sealed class StaticOverseasStockService : IWorldStockSearchService
|
||||
{
|
||||
public Task<WorldStockSearchResult> SearchAsync(
|
||||
string query,
|
||||
int maximumResults = LegacyWorldStockSearchService.DefaultMaximumResults,
|
||||
CancellationToken cancellationToken = default) => Task.FromResult(
|
||||
new WorldStockSearchResult(
|
||||
query.Trim(),
|
||||
DateTimeOffset.UtcNow,
|
||||
[new WorldStockSearchItem("Apple", "NAS@AAPL", "US")],
|
||||
false));
|
||||
}
|
||||
|
||||
private sealed class DelayedOverseasIndustryService : IOverseasIndustryIndexSearchService
|
||||
{
|
||||
public TaskCompletionSource RequestStarted { get; } = new(
|
||||
TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
|
||||
public TaskCompletionSource ReleaseRequest { get; } = new(
|
||||
TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
|
||||
public async Task<OverseasIndustryIndexSearchResult> SearchAsync(
|
||||
string query,
|
||||
int maximumResults = LegacyOverseasIndustryIndexSearchService.DefaultMaximumResults,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
RequestStarted.TrySetResult();
|
||||
await ReleaseRequest.Task.WaitAsync(cancellationToken);
|
||||
return new OverseasIndustryIndexSearchResult(
|
||||
query.Trim(),
|
||||
DateTimeOffset.UtcNow,
|
||||
[new OverseasIndustryIndexItem("Old", "Old result", "NAS@OLD")],
|
||||
false);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class StaticTradingHaltService : ITradingHaltSelectionService
|
||||
{
|
||||
public Task<TradingHaltSelectionResult> SearchAsync(
|
||||
string query = "",
|
||||
int maximumResults = LegacyTradingHaltSelectionService.DefaultMaximumResults,
|
||||
CancellationToken cancellationToken = default) => Task.FromResult(
|
||||
new TradingHaltSelectionResult(
|
||||
query.Trim(),
|
||||
DateTimeOffset.UtcNow,
|
||||
[new TradingHaltSelection(
|
||||
TradingHaltMarket.Kospi,
|
||||
"005930",
|
||||
"Samsung Electronics")],
|
||||
false));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,6 +89,9 @@ public sealed class LegacyNamedPlaylistOperatorIntegrationTests
|
||||
Assert.Equal("005930", row.StockCode);
|
||||
Assert.Equal("5001", row.SceneAlias);
|
||||
Assert.Equal("코스피", row.PlayoutSelection?.GroupCode);
|
||||
Assert.True(row.IsActive);
|
||||
Assert.False(row.IsSelected);
|
||||
Assert.Equal(0, controller.CreatePlayoutRequest().SelectedIndexZeroBased);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -156,6 +159,45 @@ public sealed class LegacyNamedPlaylistOperatorIntegrationTests
|
||||
saved.NamedPlaylist?.LastMutationOutcome);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task NamedPlaylistIdentityDoesNotChangeWhenMovingAveragesChange()
|
||||
{
|
||||
var persistence = new OperatorNamedPlaylistPersistence([]);
|
||||
var controller = CreateController(persistence);
|
||||
controller.SelectTab(LegacyOperatorTabId.Index);
|
||||
var candle = LegacyFixedActionCatalog.Default
|
||||
.GetActions(LegacyFixedMarket.Index)
|
||||
.First(action => action.SupportsMovingAverages);
|
||||
controller.SetMovingAverages(ma5: false, ma20: false);
|
||||
var added = controller.ActivateFixedAction(candle.Id);
|
||||
var raw = Assert.Single(added.Playlist);
|
||||
Assert.Equal(string.Empty, raw.PlayoutSelection!.GraphicType);
|
||||
|
||||
controller.SetMovingAverages(ma5: true, ma20: false);
|
||||
Assert.Equal(
|
||||
"MA5",
|
||||
Assert.Single(controller.CreatePlayoutRequest().Playlist).Selection!.GraphicType);
|
||||
var listed = await controller.RefreshNamedPlaylistsAsync();
|
||||
controller.SelectNamedPlaylist(
|
||||
Assert.Single(listed.NamedPlaylist!.Definitions).DefinitionId);
|
||||
await controller.SaveCurrentNamedPlaylistAsync();
|
||||
var firstStored = Assert.Single(persistence.LastReplaceItems!).Selection;
|
||||
|
||||
controller.SetMovingAverages(ma5: false, ma20: true);
|
||||
Assert.Equal(
|
||||
"MA20",
|
||||
Assert.Single(controller.CreatePlayoutRequest().Playlist).Selection!.GraphicType);
|
||||
await controller.SaveCurrentNamedPlaylistAsync();
|
||||
var secondStored = Assert.Single(persistence.LastReplaceItems!).Selection;
|
||||
|
||||
Assert.Equal(firstStored, secondStored);
|
||||
Assert.Equal(raw.GraphicType, secondStored.GraphicType);
|
||||
Assert.DoesNotContain("MA5", secondStored.GraphicType, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("MA20", secondStored.GraphicType, StringComparison.Ordinal);
|
||||
Assert.Equal(string.Empty,
|
||||
Assert.Single(controller.Current.Playlist).PlayoutSelection!.GraphicType);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Save_to_opaque_id_selects_and_writes_once_in_one_native_command()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user