Files
MBN_STOCK_WEBVIEW/tests/MBN_STOCK_WEBVIEW.LegacyApplication.Tests/LegacyOperatorControllerTests.cs

781 lines
31 KiB
C#

using MBN_STOCK_WEBVIEW.LegacyApplication;
using MMoneyCoderSharp.Data;
namespace MBN_STOCK_WEBVIEW.LegacyApplication.Tests;
public sealed class LegacyOperatorControllerTests
{
[Fact]
public async Task IndustryTabLoadsSelectorRotatesPairAndAddsTrustedActions()
{
var industries = Enumerable.Range(1, 11)
.Select(index => new IndustrySelection(
IndustryMarket.Kospi,
$"업종{index}",
index.ToString("D3", System.Globalization.CultureInfo.InvariantCulture)))
.ToArray();
var workflow = new LegacyIndustrySelectionWorkflow(
new StaticIndustryService(industries));
var controller = new LegacyOperatorController(
Lookup(),
industryWorkflow: workflow);
var loaded = await controller.SelectTabAsync(LegacyOperatorTabId.KospiIndustry);
Assert.Equal(11, loaded.Industry?.Industries.Count);
controller.SelectIndustry(0, doubleClick: true);
var pair = controller.SelectIndustry(1, doubleClick: true);
Assert.Equal("업종2", pair.Industry?.FirstIndustry?.IndustryName);
Assert.Equal("업종1", pair.Industry?.SecondIndustry?.IndustryName);
var twoColumn = controller.ActivateIndustryAction("two-column");
var pairRow = Assert.Single(twoColumn.Playlist);
Assert.Equal("8018", pairRow.ToPlayoutEntry().CutCode);
Assert.Equal("업종2-업종1", pairRow.StockName);
var paged = controller.ActivateIndustryAction("five-row");
Assert.Equal("1/3", paged.Playlist[1].PageText);
Assert.Equal("5074", paged.Playlist[1].ToPlayoutEntry().CutCode);
}
[Fact]
public async Task IndustryParentDoubleClickMaterializesDirectChildrenAtomicallyInOrder()
{
var industries = Enumerable.Range(1, 3)
.Select(index => new IndustrySelection(
IndustryMarket.Kospi,
$"업종{index}",
index.ToString("D3", System.Globalization.CultureInfo.InvariantCulture)))
.ToArray();
var controller = new LegacyOperatorController(
Lookup(),
industryWorkflow: new LegacyIndustrySelectionWorkflow(
new StaticIndustryService(industries)));
await controller.SelectTabAsync(LegacyOperatorTabId.KospiIndustry);
controller.SelectIndustry(0, doubleClick: true);
var rejected = controller.ActivateIndustrySection(0);
Assert.Empty(rejected.Playlist);
controller.SelectIndustry(1, doubleClick: true);
var added = controller.ActivateIndustrySection(0);
Assert.Collection(
added.Playlist,
row => Assert.Equal("5001", row.SceneAlias),
row => Assert.Equal("8018", row.SceneAlias));
}
[Fact]
public async Task ThemeTabSearchPreviewSortAndActionRemainCSharpAuthoritative()
{
var identity = new ThemeSelectionIdentity(
ThemeMarket.Krx,
ThemeSession.NotApplicable,
"00000001",
"AI 테마");
var service = new StaticThemeService(
identity,
Enumerable.Range(0, 13)
.Select(index => new ThemeItemPreview(
index,
$"P{index + 1:000000}",
$"종목{index + 1}"))
.ToArray());
var controller = new LegacyOperatorController(
Lookup(),
themeWorkflow: new LegacyThemeWorkflow(service));
await controller.SelectTabAsync(LegacyOperatorTabId.Theme);
var searched = await controller.SearchThemesAsync("AI", ThemeSession.PreMarket);
var resultId = Assert.Single(searched.Theme!.SearchResults).ResultId;
var selected = await controller.SelectThemeAsync(resultId);
Assert.Equal(13, selected.Theme?.PreviewItems.Count);
controller.SetThemeSort("GAIN_DESC");
var added = controller.ActivateThemeAction("five-row-current");
var row = Assert.Single(added.Playlist);
Assert.Equal("5074", row.ToPlayoutEntry().CutCode);
Assert.Equal("1/3", row.PageText);
Assert.Equal("AI 테마", row.StockName);
Assert.Equal(
"GAIN_DESC",
Assert.IsType<MBN_STOCK_WEBVIEW.Core.Playout.Scenes.LegacySceneSelection>(
row.ToPlayoutEntry().Selection).GraphicType);
}
[Fact]
public async Task Theme_result_double_click_awaits_preview_and_adds_fixed_current_action_once()
{
var identity = new ThemeSelectionIdentity(
ThemeMarket.Krx,
ThemeSession.NotApplicable,
"00000001",
"AI theme");
var service = new StaticThemeService(
identity,
Enumerable.Range(0, 7)
.Select(index => new ThemeItemPreview(
index,
$"P{index + 1:000000}",
$"Stock {index + 1}"))
.ToArray());
var controller = new LegacyOperatorController(
Lookup(),
themeWorkflow: new LegacyThemeWorkflow(service));
var loaded = await controller.SelectTabAsync(LegacyOperatorTabId.Theme);
var resultId = Assert.Single(loaded.Theme!.SearchResults).ResultId;
var activated = await controller.ActivateThemeResultAsync(resultId);
Assert.Equal(1, service.PreviewCalls);
Assert.Equal(resultId, activated.Theme?.SelectedResultId);
var row = Assert.Single(activated.Playlist);
Assert.Equal("5074", row.ToPlayoutEntry().CutCode);
Assert.Equal("1/2", row.PageText);
Assert.Equal("INPUT_ORDER", Assert.IsType<
MBN_STOCK_WEBVIEW.Core.Playout.Scenes.LegacySceneSelection>(
row.ToPlayoutEntry().Selection).GraphicType);
Assert.Equal("activate-theme-result", activated.CommandResult?.Command);
Assert.True(activated.CommandResult?.Succeeded);
}
[Fact]
public async Task ExpertTabSearchPreviewAndPagedActionAddOriginalRow()
{
var controller = new LegacyOperatorController(
Lookup(),
expertWorkflow: new LegacyExpertWorkflowController(new StaticExpertService()));
await controller.SelectTabAsync(LegacyOperatorTabId.Expert);
var search = await controller.SearchExpertsAsync("전문가");
var selectionId = Assert.Single(search.Expert!.Search.Results).SelectionId;
await controller.SelectExpertAsync(selectionId);
var added = controller.ActivateExpertAction(
LegacyExpertWorkflowController.ActionId);
var row = Assert.Single(added.Playlist);
Assert.Equal("5074", row.ToPlayoutEntry().CutCode);
Assert.Equal("1/2", row.PageText);
Assert.Equal("전문가", row.MarketText);
}
[Fact]
public async Task TradingHaltTabAddsCurrentAndCandleWithGlobalMovingAverages()
{
var controller = new LegacyOperatorController(
Lookup(),
tradingHaltWorkflow: new LegacyTradingHaltWorkflowController(
new StaticTradingHaltService()));
await controller.SelectTabAsync(LegacyOperatorTabId.TradingHalt);
var search = await controller.SearchTradingHaltsAsync("");
var selectionId = Assert.Single(search.TradingHalt!.Results).SelectionId;
controller.SelectTradingHalt(selectionId);
var current = controller.ActivateTradingHaltAction(
LegacyTradingHaltWorkflowController.CurrentActionId);
Assert.Equal("5001", Assert.Single(current.Playlist).ToPlayoutEntry().CutCode);
controller.SetMovingAverages(ma5: true, ma20: false);
var candle = controller.ActivateTradingHaltAction(
LegacyTradingHaltWorkflowController.CandleActionId);
var candleRow = candle.Playlist[1];
Assert.Equal("8051", candleRow.ToPlayoutEntry().CutCode);
Assert.Equal(
"MA5",
Assert.IsType<MBN_STOCK_WEBVIEW.Core.Playout.Scenes.LegacySceneSelection>(
candleRow.ToPlayoutEntry().Selection).GraphicType);
}
[Fact]
public void Tabs_StartInOriginalOrderAndSwapByIdentity()
{
var controller = new LegacyOperatorController(Lookup());
Assert.Equal(
["해외", "환율", "지수", "코스피", "코스닥", "비교", "테마", "해외종목", "전문가", "정지"],
controller.Current.Tabs!.Select(tab => tab.Label));
Assert.Equal(
LegacyOperatorTabId.Overseas,
Assert.Single(controller.Current.Tabs!, tab => tab.IsActive).Id);
var selected = controller.SelectTab(LegacyOperatorTabId.Theme);
Assert.Equal(
LegacyOperatorTabId.Theme,
Assert.Single(selected.Tabs!, tab => tab.IsActive).Id);
var swapped = controller.SwapTabs(
LegacyOperatorTabId.Theme,
LegacyOperatorTabId.Exchange);
Assert.Equal(LegacyOperatorTabId.Theme, swapped.Tabs![1].Id);
Assert.Equal(LegacyOperatorTabId.Exchange, swapped.Tabs[6].Id);
Assert.Equal(
LegacyOperatorTabId.Theme,
Assert.Single(swapped.Tabs, tab => tab.IsActive).Id);
}
[Fact]
public async Task PlaylistKeyboardCommandsUseActiveRowAndBoundaries()
{
var controller = await ControllerWithPlaylistAsync(0, 1, 2);
var rows = controller.Current.Playlist;
var last = controller.SelectPlaylistBoundary(last: true);
Assert.True(last.Playlist[0].IsEnabled);
Assert.True(last.Playlist[2].IsSelected);
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);
}
[Fact]
public async Task FixedCatalogLeafAndSectionAreMaterializedOnlyByCSharp()
{
var controller = new LegacyOperatorController(Lookup());
var first = LegacyFixedActionCatalog.Default.GetActions(LegacyFixedMarket.Overseas)[0];
var leaf = controller.ActivateFixedAction(first.Id);
var row = Assert.Single(leaf.Playlist);
Assert.Equal("해외지수", row.MarketText);
Assert.Equal(first.Label, row.StockName);
Assert.Equal(first.Code, row.ToPlayoutEntry().CutCode);
Assert.Equal(
first.Selection.Subject,
Assert.IsType<MBN_STOCK_WEBVIEW.Core.Playout.Scenes.LegacySceneSelection>(
row.ToPlayoutEntry().Selection).Subject);
var firstSection = LegacyFixedActionCatalog.Default
.GetSections(LegacyFixedMarket.Overseas)[0];
var section = await controller.ActivateFixedSection(0);
Assert.Equal(1 + firstSection.Actions.Count, section.Playlist.Count);
Assert.Equal(
firstSection.Actions.Select(action => action.Label),
section.Playlist.Skip(1).Select(item => item.StockName));
}
[Fact]
public void FixedCatalogRejectsCrossTabAndUnavailableActionsAtomically()
{
var controller = new LegacyOperatorController(Lookup());
var exchange = LegacyFixedActionCatalog.Default
.GetActions(LegacyFixedMarket.Exchange)[0];
var crossTab = controller.ActivateFixedAction(exchange.Id);
Assert.Empty(crossTab.Playlist);
Assert.Equal(LegacyOperatorStatusKind.Error, crossTab.StatusKind);
var unavailable = LegacyFixedActionCatalog.Default.Actions
.First(action => !action.Available);
controller.SelectTab(unavailable.Market switch
{
LegacyFixedMarket.Overseas => LegacyOperatorTabId.Overseas,
LegacyFixedMarket.Exchange => LegacyOperatorTabId.Exchange,
LegacyFixedMarket.Index => LegacyOperatorTabId.Index,
_ => throw new InvalidOperationException()
});
var blocked = controller.ActivateFixedAction(unavailable.Id);
Assert.Empty(blocked.Playlist);
Assert.Equal(unavailable.Prerequisite, blocked.Dialog?.Message);
}
[Fact]
public void MovingAverageFlagsAreCapturedInFixedCandleSelection()
{
var controller = new LegacyOperatorController(Lookup());
var candle = LegacyFixedActionCatalog.Default
.GetActions(LegacyFixedMarket.Index)
.First(action => action.SupportsMovingAverages);
controller.SelectTab(LegacyOperatorTabId.Index);
controller.SetMovingAverages(ma5: false, ma20: true);
var state = controller.ActivateFixedAction(candle.Id);
Assert.Equal(
"MA20",
Assert.IsType<MBN_STOCK_WEBVIEW.Core.Playout.Scenes.LegacySceneSelection>(
Assert.Single(state.Playlist).ToPlayoutEntry().Selection).GraphicType);
Assert.False(state.MovingAverages.Ma5);
Assert.True(state.MovingAverages.Ma20);
}
[Fact]
public async Task EmptyButtonIsNoOpButEmptyEnterSearches()
{
var lookup = Lookup();
var controller = new LegacyOperatorController(lookup);
await controller.SearchStocksAsync(string.Empty, LegacySearchTrigger.Button);
Assert.Equal(0, lookup.CallCount);
await controller.SearchStocksAsync(string.Empty, LegacySearchTrigger.Enter);
Assert.Equal(1, lookup.CallCount);
}
[Fact]
public async Task ExactPointerDoubleClick_AppendsOneLegacyRow()
{
var controller = new LegacyOperatorController(Lookup());
await controller.SearchStocksAsync("삼성", LegacySearchTrigger.Button);
controller.SelectStock(0);
controller.CutPointerDown(1, false, false, 20, 45, 1_000);
var state = controller.CutPointerDown(1, false, false, 20, 45, 1_400);
var row = Assert.Single(state.Playlist);
Assert.Equal("코스피", row.MarketText);
Assert.Equal("삼성전자", row.StockName);
Assert.Equal("1열판기본", row.GraphicType);
Assert.Equal("현재가", row.Subtype);
Assert.Equal("005930", row.StockCode);
Assert.Equal("5001", row.ToPlayoutEntry().CutCode);
}
[Fact]
public async Task ExactPointerDoubleClick_AllowsZeroBasedWebTimestamp()
{
var controller = new LegacyOperatorController(Lookup());
await controller.SearchStocksAsync("삼성", LegacySearchTrigger.Button);
controller.SelectStock(0);
controller.CutPointerDown(1, false, false, 20, 45, 0);
var state = controller.CutPointerDown(1, false, false, 20, 45, 499);
Assert.Single(state.Playlist);
}
[Fact]
public async Task DifferentPointerPosition_DoesNotCountAsLegacyDoubleClick()
{
var controller = new LegacyOperatorController(Lookup());
await controller.SearchStocksAsync("삼성", LegacySearchTrigger.Button);
controller.SelectStock(0);
controller.CutPointerDown(1, false, false, 20, 45, 1_000);
var state = controller.CutPointerDown(1, false, false, 21, 45, 1_200);
Assert.Empty(state.Playlist);
}
[Fact]
public async Task NativeSelectionAndLegacyDoubleClickRowsStaySeparateAtShiftEdges()
{
var controller = new LegacyOperatorController(Lookup());
await controller.SearchStocksAsync("삼성", LegacySearchTrigger.Button);
controller.SelectStock(0);
var firstShift = controller.CutPointerDown(2, false, true, 20, 69, 1_000);
Assert.Equal(
[0, 1, 2],
firstShift.CutRows.Where(row => row.IsSelected).Select(row => row.PhysicalIndex));
var noDoubleClick = controller.CutPointerDown(2, false, false, 20, 69, 1_400);
Assert.Empty(noDoubleClick.Playlist);
var sameRowShift = controller.CutPointerDown(2, false, true, 21, 69, 2_000);
Assert.Equal(
[2],
sameRowShift.CutRows.Where(row => row.IsSelected).Select(row => row.PhysicalIndex));
var stillNoDoubleClick = controller.CutPointerDown(2, false, false, 21, 69, 2_400);
Assert.Empty(stillNoDoubleClick.Playlist);
}
[Fact]
public async Task ControlAndShiftSelection_DropsInPhysicalOrderAndKeepsDuplicates()
{
var controller = new LegacyOperatorController(Lookup());
await controller.SearchStocksAsync("삼성", LegacySearchTrigger.Button);
controller.SelectStock(0);
controller.CutPointerDown(0, false, false, 1, 1, 1_000);
controller.CutPointerDown(2, true, false, 1, 47, 2_000);
controller.CutPointerDown(1, true, true, 1, 24, 3_000);
var first = controller.DropSelectedCutsOnPlaylist();
var second = controller.DropSelectedCutsOnPlaylist();
Assert.Equal(3, first.Playlist.Count);
Assert.Equal(6, second.Playlist.Count);
Assert.Equal(
["예상체결가", "현재가", "시간외단일가"],
first.Playlist.Select(row => row.Subtype));
}
[Fact]
public async Task SameRowShiftStillDropsTheNativeListViewSelection()
{
var controller = new LegacyOperatorController(Lookup());
await controller.SearchStocksAsync("삼성", LegacySearchTrigger.Button);
controller.SelectStock(0);
controller.CutPointerDown(1, false, false, 1, 24, 1_000);
controller.CutPointerDown(1, false, true, 2, 24, 2_000);
var state = controller.DropSelectedCutsOnPlaylist();
Assert.Equal("현재가", Assert.Single(state.Playlist).Subtype);
}
[Fact]
public async Task NxtOnlyAllowsCurrentAndShowsOriginalWarning()
{
var controller = new LegacyOperatorController(Lookup());
await controller.SearchStocksAsync("넥스트", LegacySearchTrigger.Button);
controller.SelectStock(1);
controller.CutPointerDown(0, false, false, 1, 1, 1_000);
var warning = controller.DropSelectedCutsOnPlaylist();
Assert.Empty(warning.Playlist);
Assert.Equal("NXT는 제공하지 않습니다.", warning.Dialog?.Message);
controller.DismissDialog();
controller.CutPointerDown(1, false, false, 1, 24, 2_000);
var accepted = controller.DropSelectedCutsOnPlaylist();
Assert.Equal("N5001", Assert.Single(accepted.Playlist).ToPlayoutEntry().CutCode);
}
[Fact]
public async Task NxtMultiSelection_PausesAtEachOriginalModalBeforeContinuing()
{
var controller = new LegacyOperatorController(Lookup());
await controller.SearchStocksAsync("넥스트", LegacySearchTrigger.Button);
controller.SelectStock(1);
controller.CutPointerDown(0, false, false, 1, 1, 1_000);
controller.CutPointerDown(1, true, false, 1, 24, 2_000);
controller.CutPointerDown(2, true, false, 1, 47, 3_000);
var firstWarning = controller.DropSelectedCutsOnPlaylist();
Assert.Empty(firstWarning.Playlist);
Assert.Equal("NXT는 제공하지 않습니다.", firstWarning.Dialog?.Message);
var secondWarning = controller.DismissDialog();
Assert.Equal("N5001", Assert.Single(secondWarning.Playlist).ToPlayoutEntry().CutCode);
Assert.Equal("NXT는 제공하지 않습니다.", secondWarning.Dialog?.Message);
var finished = controller.DismissDialog();
Assert.Single(finished.Playlist);
Assert.Null(finished.Dialog);
}
[Fact]
public async Task NxtSeparatorStillShowsOriginalWarning()
{
var controller = new LegacyOperatorController(Lookup());
await controller.SearchStocksAsync("넥스트", LegacySearchTrigger.Button);
controller.SelectStock(1);
controller.CutPointerDown(3, false, false, 1, 70, 1_000);
var warning = controller.DropSelectedCutsOnPlaylist();
Assert.Empty(warning.Playlist);
Assert.Equal("NXT는 제공하지 않습니다.", warning.Dialog?.Message);
}
[Fact]
public async Task NewSearchClearsStockSelectionButNotCutCatalogOrPlaylist()
{
var controller = new LegacyOperatorController(Lookup());
await controller.SearchStocksAsync("삼성", LegacySearchTrigger.Button);
controller.SelectStock(0);
controller.CutPointerDown(1, false, false, 1, 24, 1_000);
controller.DropSelectedCutsOnPlaylist();
var state = await controller.SearchStocksAsync("다시", LegacySearchTrigger.Button);
Assert.Null(state.SelectedStockIndex);
Assert.Equal(33, state.CutRows.Count);
Assert.Single(state.Playlist);
}
[Fact]
public async Task PlaylistPlainControlAndControlShiftSelectionMatchMainFormRules()
{
var controller = await ControllerWithPlaylistAsync(0, 1, 2, 4, 5);
var ids = controller.Current.Playlist.Select(row => row.RowId).ToArray();
controller.SelectPlaylistRow(ids[1], control: false, shift: false);
controller.SelectPlaylistRow(ids[3], control: true, shift: false);
controller.SelectPlaylistRow(ids[3], control: true, shift: false);
var state = controller.SelectPlaylistRow(ids[4], control: true, shift: true);
Assert.Equal(
[ids[1], ids[3], ids[4]],
state.Playlist.Where(row => row.IsSelected).Select(row => row.RowId));
}
[Fact]
public async Task PlaylistShiftUsesPreviousActiveRowAndMovesItsRangeAnchor()
{
var controller = await ControllerWithPlaylistAsync(0, 1, 2, 4, 5);
var ids = controller.Current.Playlist.Select(row => row.RowId).ToArray();
controller.SelectPlaylistRow(ids[1], control: false, shift: false);
var firstRange = controller.SelectPlaylistRow(ids[3], control: false, shift: true);
Assert.Equal(
ids[1..4],
firstRange.Playlist.Where(row => row.IsSelected).Select(row => row.RowId));
var movedRange = controller.SelectPlaylistRow(ids[4], control: false, shift: true);
Assert.Equal(
ids[3..5],
movedRange.Playlist.Where(row => row.IsSelected).Select(row => row.RowId));
}
[Fact]
public async Task FirstPlaylistShiftStartsAtOriginalZeroCursor()
{
var controller = await ControllerWithPlaylistAsync(0, 1, 2);
var ids = controller.Current.Playlist.Select(row => row.RowId).ToArray();
var state = controller.SelectPlaylistRow(ids[2], control: false, shift: true);
Assert.All(state.Playlist, row => Assert.True(row.IsSelected));
}
[Fact]
public async Task PlaylistEnabledSupportsIndividualToggleAndAllRows()
{
var controller = await ControllerWithPlaylistAsync(0, 1, 2);
var ids = controller.Current.Playlist.Select(row => row.RowId).ToArray();
controller.SetPlaylistRowEnabled(ids[0], false);
var toggled = controller.TogglePlaylistRowEnabled(ids[1]);
Assert.False(toggled.Playlist[0].IsEnabled);
Assert.False(toggled.Playlist[1].IsEnabled);
Assert.True(toggled.Playlist[2].IsEnabled);
var disabled = controller.SetAllPlaylistRowsEnabled(false);
Assert.All(disabled.Playlist, row => Assert.False(row.IsEnabled));
var enabled = controller.SetAllPlaylistRowsEnabled(true);
Assert.All(enabled.Playlist, row => Assert.True(row.IsEnabled));
}
[Fact]
public async Task DeleteSelectedRowsPreservesRemainingPhysicalOrderAndClearsSelection()
{
var controller = await ControllerWithPlaylistAsync(0, 1, 2, 4);
var initial = controller.Current.Playlist.ToArray();
controller.SelectPlaylistRow(initial[1].RowId, control: false, shift: false);
controller.SelectPlaylistRow(initial[3].RowId, control: true, shift: false);
var state = controller.DeleteSelectedPlaylistRows();
Assert.Equal(
[initial[0].RowId, initial[2].RowId],
state.Playlist.Select(row => row.RowId));
Assert.DoesNotContain(state.Playlist, row => row.IsSelected);
}
[Fact]
public async Task DeleteAllRowsClearsPlaylistAndSelectionState()
{
var controller = await ControllerWithPlaylistAsync(0, 1, 2);
controller.SelectPlaylistRow(
controller.Current.Playlist[1].RowId,
control: false,
shift: false);
var empty = controller.DeleteAllPlaylistRows();
Assert.Empty(empty.Playlist);
var unchangedRevision = empty.Revision;
var stillEmpty = controller.DeleteAllPlaylistRows();
Assert.Equal(unchangedRevision, stillEmpty.Revision);
}
[Fact]
public async Task MoveUsesOnlyActiveRowAndCollapsesMultiSelectionToMovedRow()
{
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[2].RowId, control: true, shift: false);
var moved = controller.MoveSelectedPlaylistRows(LegacyPlaylistMoveDirection.Up);
Assert.Equal(
[initial[0].RowId, initial[2].RowId, initial[1].RowId, initial[3].RowId],
moved.Playlist.Select(row => row.RowId));
Assert.Equal(
initial[2].RowId,
Assert.Single(moved.Playlist, row => row.IsSelected).RowId);
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);
}
[Fact]
public async Task MoveAtBoundaryIsNoOp()
{
var controller = await ControllerWithPlaylistAsync(0, 1);
var first = controller.Current.Playlist[0];
controller.SelectPlaylistRow(first.RowId, control: false, shift: false);
var before = controller.Current;
var after = controller.MoveSelectedPlaylistRows(LegacyPlaylistMoveDirection.Up);
Assert.Equal(before.Revision, after.Revision);
Assert.Equal(
before.Playlist.Select(row => row.RowId),
after.Playlist.Select(row => row.RowId));
}
private static async Task<LegacyOperatorController> ControllerWithPlaylistAsync(
params int[] physicalCutRows)
{
var controller = new LegacyOperatorController(Lookup());
await controller.SearchStocksAsync("삼성", LegacySearchTrigger.Button);
controller.SelectStock(0);
var timestamp = 1_000L;
foreach (var physicalCutRow in physicalCutRows)
{
controller.CutPointerDown(
physicalCutRow,
control: false,
shift: false,
x: 1,
y: physicalCutRow * 23,
timestampMilliseconds: timestamp);
controller.DropSelectedCutsOnPlaylist();
timestamp += 1_000;
}
return controller;
}
private static FakeLookup Lookup() => new(new LegacyStockSearchData(
"query",
["삼성전자", "넥스트(NXT)"],
[
new LegacyStockIdentity(LegacyDomesticStockMarket.Kospi, "삼성전자", "005930"),
new LegacyStockIdentity(LegacyDomesticStockMarket.NxtKospi, "넥스트(NXT)", "A00001")
]));
private sealed class FakeLookup : ILegacyStockLookup
{
private readonly LegacyStockSearchData _result;
public FakeLookup(LegacyStockSearchData result)
{
_result = result;
}
public int CallCount { get; private set; }
public Task<LegacyStockSearchData> SearchAsync(
string query,
CancellationToken cancellationToken = default)
{
CallCount++;
return Task.FromResult(_result);
}
}
private sealed class StaticIndustryService : IIndustrySelectionService
{
private readonly IReadOnlyList<IndustrySelection> _rows;
public StaticIndustryService(IReadOnlyList<IndustrySelection> rows)
{
_rows = rows;
}
public Task<IReadOnlyList<IndustrySelection>> GetAsync(
IndustryMarket market,
CancellationToken cancellationToken = default) => Task.FromResult(_rows);
}
private sealed class StaticThemeService : IThemeSelectionService
{
private readonly ThemeSelectionIdentity _identity;
private readonly IReadOnlyList<ThemeItemPreview> _items;
public StaticThemeService(
ThemeSelectionIdentity identity,
IReadOnlyList<ThemeItemPreview> items)
{
_identity = identity;
_items = items;
}
public int PreviewCalls { get; private set; }
public Task<ThemeSearchResult> SearchAsync(
string query,
ThemeSession nxtSession,
int maximumResults = LegacyThemeSelectionService.DefaultMaximumResults,
CancellationToken cancellationToken = default) => Task.FromResult(
new ThemeSearchResult(
query,
nxtSession,
DateTimeOffset.UtcNow,
[new ThemeSelectorItem(_identity, DataSourceKind.Oracle)],
false));
public Task<ThemePreviewResult> PreviewAsync(
ThemeSelectionIdentity identity,
int maximumItems = LegacyThemeSelectionService.DefaultMaximumPreviewItems,
CancellationToken cancellationToken = default)
{
PreviewCalls++;
return Task.FromResult(
new ThemePreviewResult(
_identity,
DateTimeOffset.UtcNow,
_items,
false));
}
}
private sealed class StaticExpertService : IExpertSelectionService
{
private static readonly ExpertSelectionIdentity Identity = new("0001", "전문가A");
public Task<ExpertSearchResult> SearchAsync(
string query = "",
int maximumResults = LegacyExpertSelectionService.DefaultMaximumResults,
CancellationToken cancellationToken = default) => Task.FromResult(
new ExpertSearchResult(
query.Trim(),
DateTimeOffset.UtcNow,
[new ExpertSelectorItem(Identity, DataSourceKind.Oracle)],
false));
public Task<ExpertPreviewResult> PreviewAsync(
ExpertSelectionIdentity identity,
int maximumItems = LegacyExpertSelectionService.DefaultMaximumPreviewItems,
CancellationToken cancellationToken = default) => Task.FromResult(
new ExpertPreviewResult(
Identity,
DateTimeOffset.UtcNow,
Enumerable.Range(1, 6)
.Select(index => new ExpertRecommendationPreview(
index,
index.ToString("D6", System.Globalization.CultureInfo.InvariantCulture),
$"추천{index}",
10_000m + index))
.ToArray(),
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", "삼성전자")],
false));
}
}