1584 lines
65 KiB
C#
1584 lines
65 KiB
C#
using MBN_STOCK_WEBVIEW.Core.Playout;
|
|
using MBN_STOCK_WEBVIEW.Core.Playout.Scenes;
|
|
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 IndustryComparisonEditChangesOnlyTwoColumnSubjectAndNeverMutatesPlaylistByItself()
|
|
{
|
|
var industries = new[]
|
|
{
|
|
new IndustrySelection(IndustryMarket.Kospi, "전기전자", "013")
|
|
};
|
|
var controller = new LegacyOperatorController(
|
|
Lookup(),
|
|
industryWorkflow: new LegacyIndustrySelectionWorkflow(
|
|
new StaticIndustryService(industries)));
|
|
await controller.SelectTabAsync(LegacyOperatorTabId.KospiIndustry);
|
|
controller.SelectIndustry(0, doubleClick: false);
|
|
|
|
var firstEdit = controller.SetIndustryComparisonText(
|
|
LegacyIndustryComparisonSlot.First,
|
|
"직접 첫째");
|
|
var secondEdit = controller.SetIndustryComparisonText(
|
|
LegacyIndustryComparisonSlot.Second,
|
|
"직접 둘째");
|
|
|
|
Assert.Empty(firstEdit.Playlist);
|
|
Assert.Empty(secondEdit.Playlist);
|
|
Assert.Equal("전기전자",
|
|
controller.ActivateIndustryAction("one-column").Playlist[0].StockName);
|
|
|
|
var pair = controller.ActivateIndustryAction("two-column");
|
|
var pairRow = pair.Playlist[^1];
|
|
Assert.Equal("직접 첫째-직접 둘째", pairRow.StockName);
|
|
Assert.Equal("직접 첫째-직접 둘째", pairRow.PlayoutSelection?.Subject);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task IndustryRootDoubleClickMatchesLegacySilentNoOpWhileChildStillAdds()
|
|
{
|
|
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);
|
|
controller.SelectIndustry(1, doubleClick: true);
|
|
|
|
var rootDoubleClick = controller.ActivateIndustrySection(0);
|
|
|
|
Assert.Empty(rootDoubleClick.Playlist);
|
|
Assert.Null(rootDoubleClick.Dialog);
|
|
|
|
var childDoubleClick = controller.ActivateIndustryAction("one-column");
|
|
Assert.Equal("5001", Assert.Single(childDoubleClick.Playlist).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("테마", row.MarketText);
|
|
Assert.Equal("AI 테마", row.StockName);
|
|
Assert.Equal("5단 표그래프", row.GraphicType);
|
|
Assert.Equal("테마-현재가(상승률순)", row.Subtype);
|
|
Assert.Equal("테마-현재가(상승률순)", row.RawCutLabel);
|
|
Assert.Equal(
|
|
new MBN_STOCK_WEBVIEW.Core.Playout.Scenes.LegacySceneSelection(
|
|
"PAGED_THEME",
|
|
"AI 테마",
|
|
"GAIN_DESC",
|
|
"CURRENT",
|
|
"00000001",
|
|
"AI 테마"),
|
|
Assert.IsType<MBN_STOCK_WEBVIEW.Core.Playout.Scenes.LegacySceneSelection>(
|
|
row.ToPlayoutEntry().Selection));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ThemeSearch_ReportsTheFiniteFiveThousandRowTruncationToTheOperator()
|
|
{
|
|
var controller = new LegacyOperatorController(
|
|
Lookup(),
|
|
themeWorkflow: new LegacyThemeWorkflow(new TruncatedThemeService()));
|
|
await controller.SelectTabAsync(LegacyOperatorTabId.Theme);
|
|
|
|
var searched = await controller.SearchThemesAsync("Theme");
|
|
|
|
Assert.Equal(5_000, searched.Theme!.SearchResults.Count);
|
|
Assert.True(searched.Theme.SearchIsTruncated);
|
|
Assert.Equal(LegacyOperatorStatusKind.Warning, searched.StatusKind);
|
|
Assert.Contains("5,000", searched.StatusMessage, StringComparison.Ordinal);
|
|
Assert.Contains("검색어", searched.StatusMessage, StringComparison.Ordinal);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task NxtExpectedThemeAction_RemainsClickableAndShowsExactWarningWithoutAdding()
|
|
{
|
|
var identity = new ThemeSelectionIdentity(
|
|
ThemeMarket.Nxt,
|
|
ThemeSession.PreMarket,
|
|
"00000002",
|
|
"AI 테마");
|
|
var service = new StaticThemeService(
|
|
identity,
|
|
[new ThemeItemPreview(0, "X005930", "삼성전자")]);
|
|
var controller = new LegacyOperatorController(
|
|
Lookup(),
|
|
timeProvider: new FixedTimeProvider(new DateTimeOffset(
|
|
2026, 7, 18, 10, 0, 0, TimeSpan.FromHours(9))),
|
|
themeWorkflow: new LegacyThemeWorkflow(
|
|
service,
|
|
() => new DateTimeOffset(2026, 7, 18, 10, 0, 0, TimeSpan.FromHours(9))));
|
|
await controller.SelectTabAsync(LegacyOperatorTabId.Theme);
|
|
var searched = await controller.SearchThemesAsync("AI", ThemeSession.AfterMarket);
|
|
Assert.Equal(
|
|
[ThemeSession.PreMarket, ThemeSession.PreMarket],
|
|
service.SearchSessions);
|
|
var resultId = Assert.Single(searched.Theme!.SearchResults).ResultId;
|
|
var selected = await controller.SelectThemeAsync(resultId);
|
|
Assert.True(Assert.Single(
|
|
selected.Theme!.ActionStates,
|
|
state => state.ActionId == "five-row-expected").IsAvailable);
|
|
|
|
var warned = controller.ActivateThemeAction("five-row-expected");
|
|
|
|
Assert.Empty(warned.Playlist);
|
|
Assert.Equal("NXT는 데이터가 없습니다.", warned.Dialog?.Message);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(0, ThemeSession.PreMarket)]
|
|
[InlineData(12, ThemeSession.PreMarket)]
|
|
[InlineData(13, ThemeSession.AfterMarket)]
|
|
[InlineData(23, ThemeSession.AfterMarket)]
|
|
public async Task ThemeActivationAndSearch_DeriveNxtSessionFromControllerLocalTime(
|
|
int localHour,
|
|
ThemeSession expectedSession)
|
|
{
|
|
var identity = new ThemeSelectionIdentity(
|
|
ThemeMarket.Nxt,
|
|
ThemeSession.PreMarket,
|
|
"00000002",
|
|
"AI 테마");
|
|
var service = new StaticThemeService(
|
|
identity,
|
|
[new ThemeItemPreview(0, "X005930", "삼성전자")]);
|
|
var controller = new LegacyOperatorController(
|
|
Lookup(),
|
|
timeProvider: new FixedTimeProvider(new DateTimeOffset(
|
|
2026, 7, 18, localHour, 0, 0, TimeSpan.FromHours(9))),
|
|
themeWorkflow: new LegacyThemeWorkflow(service));
|
|
|
|
await controller.SelectTabAsync(LegacyOperatorTabId.Theme);
|
|
await controller.SearchThemesAsync("AI");
|
|
|
|
Assert.Equal([expectedSession, expectedSession], service.SearchSessions);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task NxtCurrentThemeAction_ProjectsOriginalVisibleColumnsAndKeepsTypedSelection()
|
|
{
|
|
var identity = new ThemeSelectionIdentity(
|
|
ThemeMarket.Nxt,
|
|
ThemeSession.PreMarket,
|
|
"00000002",
|
|
"AI 테마");
|
|
var controller = new LegacyOperatorController(
|
|
Lookup(),
|
|
timeProvider: new FixedTimeProvider(new DateTimeOffset(
|
|
2026, 7, 18, 10, 0, 0, TimeSpan.FromHours(9))),
|
|
themeWorkflow: new LegacyThemeWorkflow(
|
|
new StaticThemeService(
|
|
identity,
|
|
[new ThemeItemPreview(0, "X005930", "삼성전자")]),
|
|
() => new DateTimeOffset(2026, 7, 18, 10, 0, 0, TimeSpan.FromHours(9))));
|
|
var loaded = await controller.SelectTabAsync(LegacyOperatorTabId.Theme);
|
|
await controller.SelectThemeAsync(Assert.Single(loaded.Theme!.SearchResults).ResultId);
|
|
|
|
var added = controller.ActivateThemeAction("six-row-current");
|
|
|
|
var row = Assert.Single(added.Playlist);
|
|
Assert.Equal("테마_NXT", row.MarketText);
|
|
Assert.Equal("AI 테마(NXT)", row.StockName);
|
|
Assert.Equal("6종목 현재가", row.GraphicType);
|
|
Assert.Equal("테마-현재가(입력순)", row.Subtype);
|
|
Assert.Equal("테마-현재가(입력순)", row.RawCutLabel);
|
|
Assert.Equal(
|
|
new MBN_STOCK_WEBVIEW.Core.Playout.Scenes.LegacySceneSelection(
|
|
"PAGED_NXT_THEME",
|
|
"AI 테마(NXT)",
|
|
"PRE_MARKET",
|
|
"INPUT_ORDER",
|
|
"00000002",
|
|
"AI 테마"),
|
|
Assert.IsType<MBN_STOCK_WEBVIEW.Core.Playout.Scenes.LegacySceneSelection>(
|
|
row.ToPlayoutEntry().Selection));
|
|
}
|
|
|
|
[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 ExpertTabSearchPreviewAndPagedActionAddOriginalRowWithNullBuyAmount()
|
|
{
|
|
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.Null(added.Expert!.Preview.Items[0].BuyAmount);
|
|
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(
|
|
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]
|
|
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 TabHoverSwapUsesExpectedPositionsSoDuplicateDeliveryCannotSwapBack()
|
|
{
|
|
var controller = new LegacyOperatorController(Lookup());
|
|
|
|
var first = await controller.SwapTabsAtExpectedPositionsAsync(
|
|
LegacyOperatorTabId.Theme,
|
|
LegacyOperatorTabId.Exchange,
|
|
expectedSourceIndex: 6,
|
|
expectedTargetIndex: 1);
|
|
Assert.Equal(LegacyOperatorTabId.Theme, first.Tabs![1].Id);
|
|
Assert.Equal(LegacyOperatorTabId.Exchange, first.Tabs[6].Id);
|
|
|
|
var revision = first.Revision;
|
|
var duplicate = await controller.SwapTabsAtExpectedPositionsAsync(
|
|
LegacyOperatorTabId.Theme,
|
|
LegacyOperatorTabId.Exchange,
|
|
expectedSourceIndex: 6,
|
|
expectedTargetIndex: 1);
|
|
Assert.Equal(revision, duplicate.Revision);
|
|
Assert.Equal(LegacyOperatorTabId.Theme, duplicate.Tabs![1].Id);
|
|
Assert.Equal(LegacyOperatorTabId.Exchange, duplicate.Tabs[6].Id);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task PlaylistKeyboardCommandsUseActiveRowAndBoundaries()
|
|
{
|
|
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].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].IsActive);
|
|
Assert.Equal(
|
|
rows.Take(2).Select(row => row.RowId),
|
|
first.Playlist.Where(row => row.IsSelected).Select(row => row.RowId));
|
|
}
|
|
|
|
[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);
|
|
var expected = OriginalFixedVisibleSelection(first);
|
|
Assert.Equal(expected.GroupCode, row.MarketText);
|
|
Assert.Equal(expected.Subject, row.StockName);
|
|
Assert.Equal(expected.GraphicType, row.GraphicType);
|
|
Assert.Equal(expected.Subtype, row.Subtype);
|
|
Assert.Equal(expected.DataCode, row.StockCode);
|
|
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 =>
|
|
OriginalFixedVisibleSelection(action).Subject),
|
|
section.Playlist.Skip(1).Select(item => item.StockName));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task EveryAvailableFixedActionUsesOriginalVisibleProjectionCanonicalPlayoutAndInitialPageN()
|
|
{
|
|
var catalog = LegacyFixedActionCatalog.Default;
|
|
var localNow = new DateTimeOffset(2026, 7, 22, 10, 0, 0, TimeSpan.FromHours(9));
|
|
var pagePlans = new RecordingPagePlanProvider();
|
|
var availableActions = catalog.Actions
|
|
.Where(candidate => candidate.Available)
|
|
.ToArray();
|
|
Assert.Equal(322, availableActions.Length);
|
|
var visibleIdentities = new HashSet<
|
|
(string GroupCode, string Subject, string GraphicType, string Subtype, string DataCode)>();
|
|
|
|
foreach (var action in availableActions)
|
|
{
|
|
var controller = new LegacyOperatorController(
|
|
Lookup(),
|
|
timeProvider: new FixedTimeProvider(localNow),
|
|
fixedPagePlanProvider: pagePlans);
|
|
controller.SelectTab(action.Market switch
|
|
{
|
|
LegacyFixedMarket.Overseas => LegacyOperatorTabId.Overseas,
|
|
LegacyFixedMarket.Exchange => LegacyOperatorTabId.Exchange,
|
|
LegacyFixedMarket.Index => LegacyOperatorTabId.Index,
|
|
_ => throw new ArgumentOutOfRangeException()
|
|
});
|
|
|
|
var state = await controller.ActivateFixedActionAsync(action.Id);
|
|
|
|
var row = Assert.Single(state.Playlist);
|
|
var expectedVisible = OriginalFixedVisibleSelection(action);
|
|
Assert.Equal(expectedVisible.GroupCode, row.MarketText);
|
|
Assert.Equal(expectedVisible.Subject, row.StockName);
|
|
Assert.Equal(expectedVisible.GraphicType, row.GraphicType);
|
|
Assert.Equal(expectedVisible.Subtype, row.Subtype);
|
|
Assert.Equal(expectedVisible.DataCode, row.StockCode);
|
|
Assert.Equal(action.Code switch
|
|
{
|
|
"5074" => "1/20",
|
|
"5077" or "5088" => "1/3",
|
|
_ => "1/1"
|
|
}, row.PageText);
|
|
Assert.Equal(action.Label, row.RawCutLabel);
|
|
Assert.Equal(action.Code, row.SceneAlias);
|
|
Assert.True(
|
|
visibleIdentities.Add((
|
|
row.MarketText,
|
|
row.StockName,
|
|
row.GraphicType,
|
|
row.Subtype,
|
|
row.StockCode)),
|
|
$"Duplicate original fixed selection: {action.Id}");
|
|
|
|
var expectedPlayout = catalog.Materialize(action.Id, localNow).Selection;
|
|
var actualPlayout = Assert.IsType<
|
|
MBN_STOCK_WEBVIEW.Core.Playout.Scenes.LegacySceneSelection>(
|
|
row.PlayoutSelection);
|
|
Assert.Equal(expectedPlayout.GroupCode, actualPlayout.GroupCode);
|
|
Assert.Equal(expectedPlayout.Subject, actualPlayout.Subject);
|
|
Assert.Equal(expectedPlayout.GraphicType, actualPlayout.GraphicType);
|
|
Assert.Equal(expectedPlayout.Subtype, actualPlayout.Subtype);
|
|
Assert.Equal(expectedPlayout.DataCode, actualPlayout.DataCode);
|
|
}
|
|
|
|
Assert.Equal(availableActions.Length, visibleIdentities.Count);
|
|
Assert.Equal(78, pagePlans.Entries.Count);
|
|
Assert.Equal(
|
|
78,
|
|
pagePlans.Entries.Select(entry => entry.EntryId).Distinct(StringComparer.Ordinal).Count());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task FixedPagedActionPreservesOriginalOneSlashZeroForEmptyOrFailedPageRead()
|
|
{
|
|
var action = LegacyFixedActionCatalog.Default.Actions
|
|
.First(candidate => candidate.Available && candidate.Code == "5074");
|
|
|
|
var emptyPlans = new RecordingPagePlanProvider { FiveRowItemCount = 0 };
|
|
var emptyController = new LegacyOperatorController(
|
|
Lookup(),
|
|
fixedPagePlanProvider: emptyPlans);
|
|
emptyController.SelectTab(LegacyOperatorTabId.Index);
|
|
var empty = await emptyController.ActivateFixedActionAsync(action.Id);
|
|
Assert.Equal("1/0", Assert.Single(empty.Playlist).PageText);
|
|
Assert.Equal(LegacyOperatorStatusKind.Information, empty.StatusKind);
|
|
|
|
var failedPlans = new RecordingPagePlanProvider
|
|
{
|
|
Failure = new IOException("page read failed")
|
|
};
|
|
var failedController = new LegacyOperatorController(
|
|
Lookup(),
|
|
fixedPagePlanProvider: failedPlans);
|
|
failedController.SelectTab(LegacyOperatorTabId.Index);
|
|
var failed = await failedController.ActivateFixedActionAsync(action.Id);
|
|
Assert.Equal("1/0", Assert.Single(failed.Playlist).PageText);
|
|
Assert.Equal(LegacyOperatorStatusKind.Warning, failed.StatusKind);
|
|
Assert.Null(failed.Dialog);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task FixedPagedActionCancellationDoesNotAppendAPlaylistRow()
|
|
{
|
|
var action = LegacyFixedActionCatalog.Default.Actions
|
|
.First(candidate => candidate.Available && candidate.Code == "5077");
|
|
var controller = new LegacyOperatorController(
|
|
Lookup(),
|
|
fixedPagePlanProvider: new RecordingPagePlanProvider());
|
|
controller.SelectTab(LegacyOperatorTabId.Index);
|
|
using var cancellation = new CancellationTokenSource();
|
|
cancellation.Cancel();
|
|
|
|
await Assert.ThrowsAnyAsync<OperationCanceledException>(() =>
|
|
controller.ActivateFixedActionAsync(action.Id, cancellation.Token));
|
|
|
|
Assert.Empty(controller.Current.Playlist);
|
|
}
|
|
|
|
[Fact]
|
|
public void Fixed8035UsesOriginalIndexFieldsWithoutChangingCanonicalSelection()
|
|
{
|
|
var controller = new LegacyOperatorController(Lookup());
|
|
controller.SelectTab(LegacyOperatorTabId.Index);
|
|
|
|
var state = controller.ActivateFixedAction("fixed-136");
|
|
|
|
var row = Assert.Single(state.Playlist);
|
|
Assert.Equal("코스피", row.MarketText);
|
|
Assert.Equal("지수", row.StockName);
|
|
Assert.Equal("캔들그래프", row.GraphicType);
|
|
Assert.Equal("일봉", row.Subtype);
|
|
Assert.Equal(string.Empty, row.StockCode);
|
|
Assert.Equal("8035", row.SceneAlias);
|
|
var selection = Assert.IsType<
|
|
MBN_STOCK_WEBVIEW.Core.Playout.Scenes.LegacySceneSelection>(
|
|
row.PlayoutSelection);
|
|
Assert.Equal("KOSPI_INDEX", selection.GroupCode);
|
|
Assert.Equal("INDEX", selection.Subject);
|
|
Assert.Equal(string.Empty, selection.GraphicType);
|
|
Assert.Equal("PRICE", selection.Subtype);
|
|
Assert.Equal(string.Empty, selection.DataCode);
|
|
}
|
|
|
|
[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 FixedCandleIdentityStaysStableWhileF8SamplesCurrentMovingAverages()
|
|
{
|
|
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(
|
|
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]
|
|
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);
|
|
Assert.True(row.IsActive);
|
|
Assert.False(row.IsSelected);
|
|
Assert.Equal(0, controller.CreatePlayoutRequest().SelectedIndexZeroBased);
|
|
}
|
|
|
|
[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 PointerSelectionWithoutAppendPermission_NeverAppendsAndResetsDoubleClick()
|
|
{
|
|
var controller = new LegacyOperatorController(Lookup());
|
|
await controller.SearchStocksAsync("삼성", LegacySearchTrigger.Button);
|
|
controller.SelectStock(0);
|
|
|
|
controller.CutPointerDown(
|
|
1, false, false, 20, 45, 1_000, allowAppend: true);
|
|
var onAirSelection = controller.CutPointerDown(
|
|
1, false, false, 20, 45, 1_200, allowAppend: false);
|
|
|
|
Assert.Empty(onAirSelection.Playlist);
|
|
Assert.Equal(
|
|
1,
|
|
Assert.Single(onAirSelection.CutRows, row => row.IsSelected).PhysicalIndex);
|
|
Assert.Equal(
|
|
1,
|
|
Assert.Single(onAirSelection.CutRows, row => row.IsFocused).PhysicalIndex);
|
|
|
|
var firstIdleClick = controller.CutPointerDown(
|
|
1, false, false, 20, 45, 1_400, allowAppend: true);
|
|
Assert.Empty(firstIdleClick.Playlist);
|
|
|
|
var secondIdleClick = controller.CutPointerDown(
|
|
1, false, false, 20, 45, 1_600, allowAppend: true);
|
|
Assert.Single(secondIdleClick.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 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()
|
|
{
|
|
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);
|
|
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]
|
|
public async Task DeleteAllRowsClearsPlaylistAndSelectionState()
|
|
{
|
|
var controller = await ControllerWithPlaylistAsync(0, 1, 2);
|
|
controller.SelectPlaylistRow(
|
|
controller.Current.Playlist[1].RowId,
|
|
control: false,
|
|
shift: false);
|
|
|
|
var requested = controller.DeleteAllPlaylistRows();
|
|
Assert.Equal("송출 리스트를 모두 삭제하겠습니까?", requested.Dialog?.Message);
|
|
Assert.Equal("MmoneyCoder", requested.Dialog?.Caption);
|
|
Assert.True(requested.Dialog?.IsConfirmation);
|
|
Assert.Equal(3, requested.Playlist.Count);
|
|
|
|
var canceled = controller.CancelDialog();
|
|
Assert.Null(canceled.Dialog);
|
|
Assert.Equal(3, canceled.Playlist.Count);
|
|
|
|
controller.DeleteAllPlaylistRows();
|
|
var empty = await controller.ConfirmDialogAsync();
|
|
Assert.Null(empty.Dialog);
|
|
Assert.Empty(empty.Playlist);
|
|
|
|
var unchangedRevision = empty.Revision;
|
|
var requestedEmpty = controller.DeleteAllPlaylistRows();
|
|
Assert.True(requestedEmpty.Dialog?.IsConfirmation);
|
|
var stillEmpty = await controller.ConfirmDialogAsync();
|
|
Assert.Empty(stillEmpty.Playlist);
|
|
Assert.True(stillEmpty.Revision > unchangedRevision);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task FirstAppendAfterDeleteAllReestablishesTheVisibleF8Candidate()
|
|
{
|
|
var controller = await ControllerWithPlaylistAsync(0, 1);
|
|
controller.DeleteAllPlaylistRows();
|
|
await controller.ConfirmDialogAsync();
|
|
|
|
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()
|
|
{
|
|
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);
|
|
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]
|
|
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));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task PlaylistDragMovesOnlySourceRowCollapsesSelectionAndIsIdempotent()
|
|
{
|
|
var controller = await ControllerWithPlaylistAsync(0, 1, 2, 4, 5, 6);
|
|
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 moved = controller.ReorderPlaylistRows(
|
|
initial[3].RowId,
|
|
initial[5].RowId,
|
|
LegacyDragDropPosition.Before);
|
|
|
|
Assert.Equal(
|
|
[
|
|
initial[0].RowId,
|
|
initial[1].RowId,
|
|
initial[2].RowId,
|
|
initial[4].RowId,
|
|
initial[3].RowId,
|
|
initial[5].RowId
|
|
],
|
|
moved.Playlist.Select(row => row.RowId));
|
|
Assert.Equal(
|
|
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(
|
|
initial[3].RowId,
|
|
initial[5].RowId,
|
|
LegacyDragDropPosition.Before);
|
|
Assert.Equal(revision, duplicate.Revision);
|
|
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]
|
|
public async Task PlaylistDragFromUnselectedRowMovesOnlyThatRowAndSelectsIt()
|
|
{
|
|
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);
|
|
|
|
var moved = controller.ReorderPlaylistRows(
|
|
initial[3].RowId,
|
|
initial[0].RowId,
|
|
LegacyDragDropPosition.Before);
|
|
|
|
Assert.Equal(
|
|
[initial[3].RowId, initial[0].RowId, initial[1].RowId, initial[2].RowId],
|
|
moved.Playlist.Select(row => row.RowId));
|
|
Assert.Equal(
|
|
initial[3].RowId,
|
|
Assert.Single(moved.Playlist, row => row.IsSelected).RowId);
|
|
}
|
|
|
|
private static LegacyFixedSelection OriginalFixedVisibleSelection(
|
|
LegacyFixedAction action)
|
|
{
|
|
var separator = action.Label.IndexOf('_');
|
|
var first = separator < 0 ? action.Label : action.Label[..separator];
|
|
var remainder = separator < 0
|
|
? string.Empty
|
|
: action.Label[(separator + 1)..];
|
|
if (action.Market is LegacyFixedMarket.Overseas or LegacyFixedMarket.Exchange)
|
|
{
|
|
return new LegacyFixedSelection(
|
|
action.Market == LegacyFixedMarket.Overseas ? "해외지수" : "환율",
|
|
first,
|
|
action.Section,
|
|
remainder,
|
|
string.Empty);
|
|
}
|
|
|
|
var indexGroup = action.Label.Contains("코스피_NXT", StringComparison.Ordinal)
|
|
? "코스피_NXT"
|
|
: action.Label.Contains("코스닥_NXT", StringComparison.Ordinal)
|
|
? "코스닥_NXT"
|
|
: action.Label.Contains("코스피", StringComparison.Ordinal)
|
|
? "코스피"
|
|
: action.Label.Contains("코스닥", StringComparison.Ordinal)
|
|
? "코스닥"
|
|
: "전체";
|
|
if (action.Section is "5단 표그래프" or "6종목 현재가" or "12종목 현재가")
|
|
{
|
|
return new LegacyFixedSelection(
|
|
indexGroup,
|
|
"지수",
|
|
action.Section,
|
|
action.Label,
|
|
string.Empty);
|
|
}
|
|
|
|
if (string.Equals(action.Section, "매매동향", StringComparison.Ordinal))
|
|
{
|
|
return new LegacyFixedSelection(
|
|
indexGroup,
|
|
"지수",
|
|
first,
|
|
remainder,
|
|
string.Empty);
|
|
}
|
|
|
|
return new LegacyFixedSelection(
|
|
action.Section,
|
|
"지수",
|
|
first,
|
|
remainder,
|
|
string.Empty);
|
|
}
|
|
|
|
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 RecordingPagePlanProvider : ILegacyScenePagePlanProvider
|
|
{
|
|
public List<LegacyPlaylistEntry> Entries { get; } = [];
|
|
|
|
public int FiveRowItemCount { get; init; } = 101;
|
|
|
|
public int SixRowItemCount { get; init; } = 13;
|
|
|
|
public int TwelveRowItemCount { get; init; } = 25;
|
|
|
|
public Exception? Failure { get; init; }
|
|
|
|
public Task<LegacyScenePagePlan> CreatePagePlanAsync(
|
|
LegacyPlaylistEntry entry,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
Entries.Add(entry);
|
|
if (Failure is not null)
|
|
{
|
|
return Task.FromException<LegacyScenePagePlan>(Failure);
|
|
}
|
|
|
|
var (builderKey, pageSize, itemCount) = entry.CutCode switch
|
|
{
|
|
"5074" => ("s5074", ScenePageSize.Five, FiveRowItemCount),
|
|
"5077" => ("s5077", ScenePageSize.Six, SixRowItemCount),
|
|
"5088" => ("s5088", ScenePageSize.Twelve, TwelveRowItemCount),
|
|
_ => throw new InvalidOperationException(
|
|
"Only a paged fixed action may request an initial page plan.")
|
|
};
|
|
var plan = Assert.IsType<ScenePagingPlan>(
|
|
ScenePaging.CreatePlan(itemCount, pageSize).Plan);
|
|
return Task.FromResult(new LegacyScenePagePlan(
|
|
builderKey,
|
|
itemCount,
|
|
pageSize,
|
|
plan.TotalPages,
|
|
plan.AccessibleItemCount,
|
|
plan.ExcludedItemCount > 0));
|
|
}
|
|
}
|
|
|
|
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 FixedTimeProvider(DateTimeOffset localNow) : TimeProvider
|
|
{
|
|
private static readonly TimeZoneInfo KoreaStandardTime = TimeZoneInfo.CreateCustomTimeZone(
|
|
"FixedKst",
|
|
TimeSpan.FromHours(9),
|
|
"Fixed KST",
|
|
"Fixed KST");
|
|
|
|
public override DateTimeOffset GetUtcNow() => localNow.ToUniversalTime();
|
|
|
|
public override TimeZoneInfo LocalTimeZone => KoreaStandardTime;
|
|
}
|
|
|
|
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 List<ThemeSession> SearchSessions { get; } = [];
|
|
|
|
public Task<ThemeSearchResult> SearchAsync(
|
|
string query,
|
|
ThemeSession nxtSession,
|
|
int maximumResults = LegacyThemeSelectionService.DefaultMaximumResults,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
SearchSessions.Add(nxtSession);
|
|
var identity = _identity.Market == ThemeMarket.Nxt
|
|
? _identity with { Session = nxtSession }
|
|
: _identity;
|
|
return Task.FromResult(
|
|
new ThemeSearchResult(
|
|
query,
|
|
nxtSession,
|
|
DateTimeOffset.UtcNow,
|
|
[new ThemeSelectorItem(
|
|
identity,
|
|
identity.Market == ThemeMarket.Krx
|
|
? DataSourceKind.Oracle
|
|
: DataSourceKind.MariaDb)],
|
|
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 TruncatedThemeService : IThemeSelectionService
|
|
{
|
|
public Task<ThemeSearchResult> SearchAsync(
|
|
string query,
|
|
ThemeSession nxtSession,
|
|
int maximumResults = LegacyThemeSelectionService.DefaultMaximumResults,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
Assert.Equal(5_000, maximumResults);
|
|
var rows = Enumerable.Range(0, maximumResults)
|
|
.Select(index => new ThemeSelectorItem(
|
|
new ThemeSelectionIdentity(
|
|
ThemeMarket.Krx,
|
|
ThemeSession.NotApplicable,
|
|
index.ToString("D8", System.Globalization.CultureInfo.InvariantCulture),
|
|
$"Theme {index}"),
|
|
DataSourceKind.Oracle))
|
|
.ToArray();
|
|
return Task.FromResult(new ThemeSearchResult(
|
|
query,
|
|
nxtSession,
|
|
DateTimeOffset.UtcNow,
|
|
rows,
|
|
IsTruncated: true));
|
|
}
|
|
|
|
public Task<ThemePreviewResult> PreviewAsync(
|
|
ThemeSelectionIdentity identity,
|
|
int maximumItems = LegacyThemeSelectionService.DefaultMaximumPreviewItems,
|
|
CancellationToken cancellationToken = default) =>
|
|
Task.FromResult(new ThemePreviewResult(
|
|
identity,
|
|
DateTimeOffset.UtcNow,
|
|
[],
|
|
IsTruncated: 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}",
|
|
index == 1 ? null : 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));
|
|
}
|
|
}
|