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

@@ -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]