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

556 lines
23 KiB
C#

using MMoneyCoderSharp.Data;
namespace MBN_STOCK_WEBVIEW.LegacyApplication.Tests;
public sealed class LegacyTabActivationParityTests
{
[Fact]
public async Task ThemeEachActivation_LoadsOriginalEmptyQueryOncePerActivation()
{
// Original evidence: Control/UC4.cs UC4_Load starts timer1 and its first
// timer1_Tick calls GetThemeByKeyword with the initially empty txts value.
var service = new CountingThemeService();
var controller = new LegacyOperatorController(
Lookup(),
themeWorkflow: new LegacyThemeWorkflow(service));
var first = await controller.SelectTabAsync(LegacyOperatorTabId.Theme);
Assert.Equal(string.Empty, service.Queries.Single());
var firstResultId = Assert.Single(first.Theme!.SearchResults).ResultId;
controller.SetThemeSort("GAIN_DESC");
var selected = await controller.SelectThemeAsync(firstResultId);
Assert.Equal(LegacyThemeSort.GainRateDescending, selected.Theme!.Sort);
Assert.NotNull(selected.Theme.SelectedTheme);
await controller.SelectTabAsync(LegacyOperatorTabId.Theme);
Assert.Single(service.Queries);
await controller.SelectTabAsync(LegacyOperatorTabId.Index);
var second = await controller.SelectTabAsync(LegacyOperatorTabId.Theme);
Assert.Collection(
service.Queries,
query => Assert.Equal(string.Empty, query),
query => Assert.Equal(string.Empty, query));
Assert.Single(second.Theme!.SearchResults);
Assert.Equal(LegacyThemeSort.InputOrder, second.Theme.Sort);
Assert.Null(second.Theme.SelectedResultId);
Assert.Null(second.Theme.SelectedTheme);
Assert.Empty(second.Theme.PreviewItems);
}
[Fact]
public async Task ExpertEachActivation_LoadsOriginalFullListOncePerActivation()
{
// Original evidence: Control/UC6.cs UC6_Load calls Data_Load, whose query
// reads the complete EXPERT_LIST without a filter.
var service = new CountingExpertService();
var controller = new LegacyOperatorController(
Lookup(),
expertWorkflow: new LegacyExpertWorkflowController(service));
var first = await controller.SelectTabAsync(LegacyOperatorTabId.Expert);
Assert.Equal(string.Empty, service.Queries.Single());
Assert.Single(first.Expert!.Search.Results);
await controller.SelectTabAsync(LegacyOperatorTabId.Expert);
Assert.Single(service.Queries);
await controller.SelectTabAsync(LegacyOperatorTabId.Index);
var second = await controller.SelectTabAsync(LegacyOperatorTabId.Expert);
Assert.Collection(
service.Queries,
query => Assert.Equal(string.Empty, query),
query => Assert.Equal(string.Empty, query));
Assert.Single(second.Expert!.Search.Results);
}
[Fact]
public async Task DragSwapThatActivatesTheme_UsesTheSameNativeInitialLoad()
{
var service = new CountingThemeService();
var controller = new LegacyOperatorController(
Lookup(),
themeWorkflow: new LegacyThemeWorkflow(service));
var state = await controller.SwapTabsAsync(
LegacyOperatorTabId.Theme,
LegacyOperatorTabId.Exchange);
Assert.Equal(string.Empty, service.Queries.Single());
Assert.Single(state.Theme!.SearchResults);
Assert.Equal(
LegacyOperatorTabId.Theme,
Assert.Single(state.Tabs!, tab => tab.IsActive).Id);
}
[Fact]
public async Task SupersededThemeActivation_DoesNotCommitAndRetriesWhenNeeded()
{
var service = new DelayedThemeService();
var controller = new LegacyOperatorController(
Lookup(),
themeWorkflow: new LegacyThemeWorkflow(service));
var firstActivation = controller.SelectTabAsync(LegacyOperatorTabId.Theme);
await service.FirstRequestStarted.Task.WaitAsync(TimeSpan.FromSeconds(5));
controller.SelectTab(LegacyOperatorTabId.Index);
service.ReleaseFirstRequest.TrySetResult();
var superseded = await firstActivation;
Assert.Equal(
LegacyOperatorTabId.Index,
Assert.Single(superseded.Tabs!, tab => tab.IsActive).Id);
var retried = await controller.SelectTabAsync(LegacyOperatorTabId.Theme);
Assert.Equal(2, service.CallCount);
Assert.Single(retried.Theme!.SearchResults);
}
[Theory]
[InlineData(LegacyOperatorTabId.KospiIndustry, IndustryMarket.Kospi)]
[InlineData(LegacyOperatorTabId.KosdaqIndustry, IndustryMarket.Kosdaq)]
public async Task IndustryEachActivation_ReloadsAndClearsUc2Selections(
LegacyOperatorTabId tabId,
IndustryMarket market)
{
var service = new CountingIndustryService();
var controller = new LegacyOperatorController(
Lookup(),
industryWorkflow: new LegacyIndustrySelectionWorkflow(service));
await controller.SelectTabAsync(tabId);
controller.SelectIndustry(0, doubleClick: true);
var selected = controller.SelectIndustry(1, doubleClick: true);
Assert.NotNull(selected.Industry!.SelectedIndustry);
Assert.NotNull(selected.Industry.FirstIndustry);
Assert.NotNull(selected.Industry.SecondIndustry);
await controller.SelectTabAsync(tabId);
Assert.Single(service.Markets);
await controller.SelectTabAsync(LegacyOperatorTabId.Index);
var reentered = await controller.SelectTabAsync(tabId);
Assert.Equal([market, market], service.Markets);
Assert.Equal(2, reentered.Industry!.Industries.Count);
Assert.Null(reentered.Industry.SelectedIndex);
Assert.Null(reentered.Industry.SelectedIndustry);
Assert.Null(reentered.Industry.FirstIndustry);
Assert.Null(reentered.Industry.SecondIndustry);
}
[Fact]
public async Task ComparisonEachActivation_ReloadsPairsAndClearsUc3DraftAndSearch()
{
var store = new CountingComparisonPairStore();
var controller = new LegacyOperatorController(
Lookup(),
comparisonWorkflow: new LegacyComparisonWorkflowController(
new StaticComparisonDataService(),
store));
await controller.SelectTabAsync(LegacyOperatorTabId.Comparison);
var searched = await controller.SearchComparisonDomesticAsync("semiconductor");
var results = searched.Comparison!.DomesticSearch.Results;
controller.ChooseComparisonDomesticResult(results[0].SelectionId);
var drafted = controller.ChooseComparisonDomesticResult(results[1].SelectionId);
Assert.NotNull(drafted.Comparison!.FirstTarget);
Assert.NotNull(drafted.Comparison.SecondTarget);
await controller.SelectTabAsync(LegacyOperatorTabId.Comparison);
Assert.Equal(1, store.ReadCount);
await controller.SelectTabAsync(LegacyOperatorTabId.Index);
var reentered = await controller.SelectTabAsync(LegacyOperatorTabId.Comparison);
Assert.Equal(2, store.ReadCount);
Assert.Equal(LegacyComparisonSearchStatus.Idle, reentered.Comparison!.DomesticSearch.Status);
Assert.Empty(reentered.Comparison.DomesticSearch.Results);
Assert.Equal(string.Empty, reentered.Comparison.DomesticSearch.Query);
Assert.Null(reentered.Comparison.FirstTarget);
Assert.Null(reentered.Comparison.SecondTarget);
Assert.Empty(reentered.Comparison.SavedPairs);
}
[Fact]
public async Task ComparisonReentry_InvalidatesPairLoadFromDiscardedUc3()
{
var store = new DelayedComparisonPairStore();
var controller = new LegacyOperatorController(
Lookup(),
comparisonWorkflow: new LegacyComparisonWorkflowController(
new StaticComparisonDataService(),
store));
var firstActivation = controller.SelectTabAsync(LegacyOperatorTabId.Comparison);
await store.FirstReadStarted.Task.WaitAsync(TimeSpan.FromSeconds(5));
controller.SelectTab(LegacyOperatorTabId.Index);
controller.SelectTab(LegacyOperatorTabId.Comparison);
store.ReleaseFirstRead.TrySetResult();
await firstActivation;
Assert.Equal(LegacyOperatorStatusKind.Neutral, controller.Current.Comparison!.StatusKind);
Assert.Equal(string.Empty, controller.Current.Comparison.StatusMessage);
Assert.Empty(controller.Current.Comparison.SavedPairs);
var reloaded = await controller.SelectTabAsync(LegacyOperatorTabId.Comparison);
Assert.Equal(2, store.ReadCount);
Assert.Equal(LegacyOperatorStatusKind.Information, reloaded.Comparison!.StatusKind);
}
[Fact]
public async Task OverseasStocksReentry_ResetsUc5StateAndKeepsMainMovingAverages()
{
var workflow = new LegacyOverseasSelectionWorkflow(
new StaticOverseasIndustryService(),
new StaticOverseasStockService());
var controller = new LegacyOperatorController(Lookup(), overseasWorkflow: workflow);
controller.SelectTab(LegacyOperatorTabId.OverseasStocks);
controller.SelectOverseasFixedIndex("nasdaq");
var industries = await controller.SearchOverseasIndustriesAsync("industry");
controller.SelectOverseasIndustry(
Assert.Single(industries.Overseas!.Industry.Results).SelectionId);
var stocks = await controller.SearchOverseasStocksAsync("stock");
controller.SelectOverseasStock(
Assert.Single(stocks.Overseas!.Stock.Results).SelectionId);
controller.SetMovingAverages(ma5: false, ma20: true);
controller.SelectTab(LegacyOperatorTabId.Index);
var reentered = controller.SelectTab(LegacyOperatorTabId.OverseasStocks);
Assert.Equal("dow-jones", reentered.Overseas!.SelectedFixedIndex?.TargetId);
Assert.Equal(LegacyOverseasSearchStatus.Idle, reentered.Overseas.Industry.Status);
Assert.Equal(string.Empty, reentered.Overseas.Industry.Query);
Assert.Empty(reentered.Overseas.Industry.Results);
Assert.Null(reentered.Overseas.Industry.Selected);
Assert.Equal(LegacyOverseasSearchStatus.Idle, reentered.Overseas.Stock.Status);
Assert.Equal(string.Empty, reentered.Overseas.Stock.Query);
Assert.Empty(reentered.Overseas.Stock.Results);
Assert.Null(reentered.Overseas.Stock.Selected);
Assert.Equal(
new LegacyOverseasMovingAverageSelection(false, true),
reentered.Overseas.MovingAverages);
}
[Fact]
public async Task TradingHaltReentry_ResetsUc7QuerySelectionAndSort()
{
var controller = new LegacyOperatorController(
Lookup(),
tradingHaltWorkflow: new LegacyTradingHaltWorkflowController(
new StaticTradingHaltService()));
controller.SelectTab(LegacyOperatorTabId.TradingHalt);
var searched = await controller.SearchTradingHaltsAsync("halted");
controller.SelectTradingHalt(
Assert.Single(searched.TradingHalt!.Results).SelectionId);
controller.ToggleTradingHaltSort();
controller.SetMovingAverages(ma5: false, ma20: true);
controller.SelectTab(LegacyOperatorTabId.Index);
var reentered = controller.SelectTab(LegacyOperatorTabId.TradingHalt);
Assert.Equal(LegacyTradingHaltRequestStatus.Idle, reentered.TradingHalt!.Status);
Assert.Equal(string.Empty, reentered.TradingHalt.Query);
Assert.Empty(reentered.TradingHalt.Results);
Assert.Null(reentered.TradingHalt.Selected);
Assert.Equal(LegacyTradingHaltNameSort.None, reentered.TradingHalt.NameSort);
Assert.False(reentered.TradingHalt.Ma5);
Assert.True(reentered.TradingHalt.Ma20);
}
[Fact]
public async Task OverseasReentry_InvalidatesSearchCompletionFromDiscardedUc5()
{
var service = new DelayedOverseasIndustryService();
var controller = new LegacyOperatorController(
Lookup(),
overseasWorkflow: new LegacyOverseasSelectionWorkflow(
service,
new StaticOverseasStockService()));
controller.SelectTab(LegacyOperatorTabId.OverseasStocks);
var pending = controller.SearchOverseasIndustriesAsync("old");
await service.RequestStarted.Task.WaitAsync(TimeSpan.FromSeconds(5));
controller.SelectTab(LegacyOperatorTabId.Index);
controller.SelectTab(LegacyOperatorTabId.OverseasStocks);
service.ReleaseRequest.TrySetResult();
await pending;
Assert.Equal(LegacyOverseasSearchStatus.Idle, controller.Current.Overseas!.Industry.Status);
Assert.Equal(string.Empty, controller.Current.Overseas.Industry.Query);
Assert.Empty(controller.Current.Overseas.Industry.Results);
}
private static ILegacyStockLookup Lookup() =>
new StaticLookup(new LegacyStockSearchData(string.Empty, [], []));
private sealed class StaticLookup : ILegacyStockLookup
{
private readonly LegacyStockSearchData _data;
public StaticLookup(LegacyStockSearchData data)
{
_data = data;
}
public Task<LegacyStockSearchData> SearchAsync(
string text,
CancellationToken cancellationToken = default) => Task.FromResult(_data);
}
private sealed class CountingThemeService : IThemeSelectionService
{
private static readonly ThemeSelectionIdentity Identity = new(
ThemeMarket.Krx,
ThemeSession.NotApplicable,
"00000001",
"Theme A");
public List<string> Queries { get; } = [];
public Task<ThemeSearchResult> SearchAsync(
string query,
ThemeSession nxtSession,
int maximumResults = LegacyThemeSelectionService.DefaultMaximumResults,
CancellationToken cancellationToken = default)
{
Queries.Add(query);
return Task.FromResult(CreateThemeResult(query, nxtSession));
}
public Task<ThemePreviewResult> PreviewAsync(
ThemeSelectionIdentity identity,
int maximumItems = LegacyThemeSelectionService.DefaultMaximumPreviewItems,
CancellationToken cancellationToken = default) => Task.FromResult(
new ThemePreviewResult(identity, DateTimeOffset.UtcNow, [], false));
public static ThemeSearchResult CreateThemeResult(
string query,
ThemeSession nxtSession) => new(
query,
nxtSession,
DateTimeOffset.UtcNow,
[new ThemeSelectorItem(Identity, DataSourceKind.Oracle)],
false);
}
private sealed class DelayedThemeService : IThemeSelectionService
{
private int _callCount;
public TaskCompletionSource FirstRequestStarted { get; } = new(
TaskCreationOptions.RunContinuationsAsynchronously);
public TaskCompletionSource ReleaseFirstRequest { get; } = new(
TaskCreationOptions.RunContinuationsAsynchronously);
public int CallCount => Volatile.Read(ref _callCount);
public async Task<ThemeSearchResult> SearchAsync(
string query,
ThemeSession nxtSession,
int maximumResults = LegacyThemeSelectionService.DefaultMaximumResults,
CancellationToken cancellationToken = default)
{
if (Interlocked.Increment(ref _callCount) == 1)
{
FirstRequestStarted.TrySetResult();
await ReleaseFirstRequest.Task.WaitAsync(cancellationToken);
}
return CountingThemeService.CreateThemeResult(query, nxtSession);
}
public Task<ThemePreviewResult> PreviewAsync(
ThemeSelectionIdentity identity,
int maximumItems = LegacyThemeSelectionService.DefaultMaximumPreviewItems,
CancellationToken cancellationToken = default) => Task.FromResult(
new ThemePreviewResult(identity, DateTimeOffset.UtcNow, [], false));
}
private sealed class CountingExpertService : IExpertSelectionService
{
private static readonly ExpertSelectionIdentity Identity = new("0001", "Expert A");
public List<string> Queries { get; } = [];
public Task<ExpertSearchResult> SearchAsync(
string query = "",
int maximumResults = LegacyExpertSelectionService.DefaultMaximumResults,
CancellationToken cancellationToken = default)
{
Queries.Add(query);
return Task.FromResult(new ExpertSearchResult(
query,
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, [], false));
}
private sealed class CountingIndustryService : IIndustrySelectionService
{
public List<IndustryMarket> Markets { get; } = [];
public Task<IReadOnlyList<IndustrySelection>> GetAsync(
IndustryMarket market,
CancellationToken cancellationToken = default)
{
Markets.Add(market);
return Task.FromResult<IReadOnlyList<IndustrySelection>>(
[
new(market, "Industry 1", "001"),
new(market, "Industry 2", "002")
]);
}
}
private sealed class StaticComparisonDataService : ILegacyComparisonDataService
{
private static readonly IReadOnlyList<LegacyComparisonTarget> Domestic =
[
LegacyComparisonTarget.CreateKrxStock(
LegacyComparisonDomesticMarket.Kospi,
"Samsung Electronics",
"005930"),
LegacyComparisonTarget.CreateKrxStock(
LegacyComparisonDomesticMarket.Kospi,
"SK Hynix",
"000660")
];
public Task<IReadOnlyList<LegacyComparisonTarget>> SearchDomesticAsync(
string query,
int maximumResults,
CancellationToken cancellationToken = default) => Task.FromResult(Domestic);
public Task<IReadOnlyList<LegacyComparisonTarget>> SearchWorldAsync(
string query,
int maximumResults,
CancellationToken cancellationToken = default) => Task.FromResult<
IReadOnlyList<LegacyComparisonTarget>>([]);
public Task<LegacyComparisonTarget?> ResolveStoredTargetAsync(
LegacyComparisonStoredTargetReference target,
CancellationToken cancellationToken = default) => Task.FromResult<
LegacyComparisonTarget?>(null);
}
private sealed class CountingComparisonPairStore : ILegacyComparisonPairStore
{
public int ReadCount { get; private set; }
public Task<byte[]?> ReadCp949Async(CancellationToken cancellationToken = default)
{
ReadCount++;
return Task.FromResult<byte[]?>(null);
}
public Task WriteCp949Async(
IReadOnlyList<LegacyComparisonPairWriteRow> rows,
CancellationToken cancellationToken = default) => Task.CompletedTask;
}
private sealed class DelayedComparisonPairStore : ILegacyComparisonPairStore
{
public TaskCompletionSource FirstReadStarted { get; } = new(
TaskCreationOptions.RunContinuationsAsynchronously);
public TaskCompletionSource ReleaseFirstRead { get; } = new(
TaskCreationOptions.RunContinuationsAsynchronously);
public int ReadCount { get; private set; }
public async Task<byte[]?> ReadCp949Async(
CancellationToken cancellationToken = default)
{
ReadCount++;
if (ReadCount == 1)
{
FirstReadStarted.TrySetResult();
await ReleaseFirstRead.Task.WaitAsync(cancellationToken);
}
return null;
}
public Task WriteCp949Async(
IReadOnlyList<LegacyComparisonPairWriteRow> rows,
CancellationToken cancellationToken = default) => Task.CompletedTask;
}
private sealed class StaticOverseasIndustryService : IOverseasIndustryIndexSearchService
{
public Task<OverseasIndustryIndexSearchResult> SearchAsync(
string query,
int maximumResults = LegacyOverseasIndustryIndexSearchService.DefaultMaximumResults,
CancellationToken cancellationToken = default) => Task.FromResult(
new OverseasIndustryIndexSearchResult(
query.Trim(),
DateTimeOffset.UtcNow,
[new OverseasIndustryIndexItem("Semiconductor", "US Semiconductor", "NAS@SOX")],
false));
}
private sealed class StaticOverseasStockService : IOverseasStockSearchService
{
public Task<WorldStockSearchResult> SearchAsync(
string query,
int maximumResults = LegacyOverseasStockSearchService.DefaultMaximumResults,
CancellationToken cancellationToken = default) => Task.FromResult(
new WorldStockSearchResult(
query.Trim(),
DateTimeOffset.UtcNow,
[new WorldStockSearchItem("Apple", "NAS@AAPL", "US")],
false));
}
private sealed class DelayedOverseasIndustryService : IOverseasIndustryIndexSearchService
{
public TaskCompletionSource RequestStarted { get; } = new(
TaskCreationOptions.RunContinuationsAsynchronously);
public TaskCompletionSource ReleaseRequest { get; } = new(
TaskCreationOptions.RunContinuationsAsynchronously);
public async Task<OverseasIndustryIndexSearchResult> SearchAsync(
string query,
int maximumResults = LegacyOverseasIndustryIndexSearchService.DefaultMaximumResults,
CancellationToken cancellationToken = default)
{
RequestStarted.TrySetResult();
await ReleaseRequest.Task.WaitAsync(cancellationToken);
return new OverseasIndustryIndexSearchResult(
query.Trim(),
DateTimeOffset.UtcNow,
[new OverseasIndustryIndexItem("Old", "Old result", "NAS@OLD")],
false);
}
}
private sealed class StaticTradingHaltService : ITradingHaltSelectionService
{
public Task<TradingHaltSelectionResult> SearchAsync(
string query = "",
int maximumResults = LegacyTradingHaltSelectionService.DefaultMaximumResults,
CancellationToken cancellationToken = default) => Task.FromResult(
new TradingHaltSelectionResult(
query.Trim(),
DateTimeOffset.UtcNow,
[new TradingHaltSelection(
TradingHaltMarket.Kospi,
"005930",
"Samsung Electronics")],
false));
}
}