feat: advance legacy UI behavior parity
This commit is contained in:
@@ -9,7 +9,7 @@ public sealed class LegacyOverseasSelectionWorkflowTests
|
||||
new(2026, 7, 15, 9, 30, 0, TimeSpan.FromHours(9));
|
||||
|
||||
[Fact]
|
||||
public void InitialSnapshotIsTypedImmutableAndContainsClosedCatalogs()
|
||||
public void InitialSnapshotIsTypedImmutableAndKeepsFarPointFirstFixedRowActive()
|
||||
{
|
||||
var workflow = CreateWorkflow();
|
||||
|
||||
@@ -18,7 +18,7 @@ public sealed class LegacyOverseasSelectionWorkflowTests
|
||||
Assert.Equal(0, snapshot.Revision);
|
||||
Assert.Equal(10, snapshot.FixedIndexTargets.Count);
|
||||
Assert.Equal(12, snapshot.Actions.Count);
|
||||
Assert.Null(snapshot.SelectedFixedIndex);
|
||||
Assert.Equal("dow-jones", snapshot.SelectedFixedIndex?.TargetId);
|
||||
Assert.Equal(LegacyOverseasSearchStatus.Idle, snapshot.Industry.Status);
|
||||
Assert.Equal(LegacyOverseasSearchStatus.Idle, snapshot.Stock.Status);
|
||||
Assert.Empty(snapshot.Industry.Results);
|
||||
@@ -68,6 +68,8 @@ public sealed class LegacyOverseasSelectionWorkflowTests
|
||||
Assert.Equal(
|
||||
["overseas-stock-00000001-0001", "overseas-stock-00000001-0002"],
|
||||
stockState.Stock.Results.Select(row => row.SelectionId));
|
||||
Assert.Equal("Alpha", industryState.Industry.Selected?.KoreanName);
|
||||
Assert.Equal("Apple", stockState.Stock.Selected?.InputName);
|
||||
Assert.Equal(2, workflow.Current.Industry.ResultCount);
|
||||
Assert.Equal(2, workflow.Current.Stock.ResultCount);
|
||||
|
||||
@@ -81,6 +83,28 @@ public sealed class LegacyOverseasSelectionWorkflowTests
|
||||
Assert.Throws<NotSupportedException>(() => stockRows.RemoveAt(0));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FirstInsertedSearchRowsBecomeTheActiveSourcesAtTheFullCompatibilityBound()
|
||||
{
|
||||
var industry = new RecordingIndustryService(
|
||||
[new OverseasIndustryIndexItem("Alpha", "Alpha Industry", "NAS@ALPHA")]);
|
||||
var stock = new RecordingStockService(
|
||||
[new WorldStockSearchItem("Apple", "NAS@AAPL", "US")]);
|
||||
var workflow = new LegacyOverseasSelectionWorkflow(industry, stock);
|
||||
|
||||
var industries = await workflow.SearchIndustriesAsync("alpha");
|
||||
var stocks = await workflow.SearchStocksAsync("apple");
|
||||
|
||||
Assert.Equal("Alpha", industries.Industry.Selected?.KoreanName);
|
||||
Assert.Equal("Apple", stocks.Stock.Selected?.InputName);
|
||||
Assert.Equal(
|
||||
[("alpha", LegacyOverseasSelectionWorkflow.DefaultMaximumResults)],
|
||||
industry.Calls);
|
||||
Assert.Equal(
|
||||
[("apple", LegacyOverseasStockSearchService.MaximumResults)],
|
||||
stock.Calls);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SelectionUsesCurrentOpaqueIdsAndARefreshInvalidatesOldIds()
|
||||
{
|
||||
@@ -108,8 +132,8 @@ public sealed class LegacyOverseasSelectionWorkflowTests
|
||||
await workflow.SearchIndustriesAsync("next");
|
||||
await workflow.SearchStocksAsync("next");
|
||||
|
||||
Assert.Null(workflow.Current.Industry.Selected);
|
||||
Assert.Null(workflow.Current.Stock.Selected);
|
||||
Assert.Equal("Beta", workflow.Current.Industry.Selected?.KoreanName);
|
||||
Assert.Equal("Taiwan Semi", workflow.Current.Stock.Selected?.InputName);
|
||||
Assert.Throws<KeyNotFoundException>(() => workflow.SelectIndustry(oldIndustryId));
|
||||
Assert.Throws<KeyNotFoundException>(() => workflow.SelectStock(oldStockId));
|
||||
Assert.StartsWith(
|
||||
@@ -145,7 +169,7 @@ public sealed class LegacyOverseasSelectionWorkflowTests
|
||||
Assert.Equal([pair.Value.Alias], draft.Aliases);
|
||||
Assert.Equal(0, draft.PageSize);
|
||||
Assert.Equal("1/1", draft.InitialPageText);
|
||||
Assert.Equal($"독일 DAX 30 캔들 {pair.Value.Days}일", draft.Title);
|
||||
Assert.Equal("독일 DAX 30", draft.Title);
|
||||
Assert.Equal("chart", draft.Category);
|
||||
Assert.Equal("overseas", draft.Market);
|
||||
Assert.Equal(LegacyOverseasActionSource.FixedIndex, draft.Source);
|
||||
@@ -160,6 +184,10 @@ public sealed class LegacyOverseasSelectionWorkflowTests
|
||||
Assert.Equal(new LegacyOverseasMovingAverageSelection(true, false), draft.MovingAverages);
|
||||
Assert.True(draft.IsEnabled);
|
||||
Assert.Equal(6, draft.FadeDuration);
|
||||
Assert.Equal(
|
||||
new LegacyOverseasPlaylistVisibleFields(
|
||||
"해외지수", "독일 DAX 30", "캔들그래프", $"{pair.Value.Days}일", "1/1"),
|
||||
draft.VisibleFields);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -189,6 +217,10 @@ public sealed class LegacyOverseasSelectionWorkflowTests
|
||||
"US Semiconductor|NAS@SOX|US|0"),
|
||||
draft.Selection);
|
||||
Assert.Equal(default, draft.MovingAverages);
|
||||
Assert.Equal(
|
||||
new LegacyOverseasPlaylistVisibleFields(
|
||||
"해외업종", "반도체", "1열판기본", string.Empty, "1/1"),
|
||||
draft.VisibleFields);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -213,6 +245,10 @@ public sealed class LegacyOverseasSelectionWorkflowTests
|
||||
"TWS@2330|TW|1"),
|
||||
current.Selection);
|
||||
Assert.Equal(default, current.MovingAverages);
|
||||
Assert.Equal(
|
||||
new LegacyOverseasPlaylistVisibleFields(
|
||||
"해외종목", "Taiwan Semi", "1열판기본", string.Empty, "1/1"),
|
||||
current.VisibleFields);
|
||||
|
||||
var expectedAliases = new Dictionary<string, string>(StringComparer.Ordinal)
|
||||
{
|
||||
@@ -232,6 +268,11 @@ public sealed class LegacyOverseasSelectionWorkflowTests
|
||||
Assert.Equal("PRICE", candle.Selection.Subtype);
|
||||
Assert.Equal("TWS@2330|TW|1", candle.Selection.DataCode);
|
||||
Assert.Equal(new LegacyOverseasMovingAverageSelection(true, true), candle.MovingAverages);
|
||||
Assert.Equal(
|
||||
new LegacyOverseasPlaylistVisibleFields(
|
||||
"해외종목", "Taiwan Semi", "캔들그래프",
|
||||
$"{LegacyOverseasActionCatalog.GetAction(pair.Key).PeriodDays}일", "1/1"),
|
||||
candle.VisibleFields);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -261,8 +302,9 @@ public sealed class LegacyOverseasSelectionWorkflowTests
|
||||
{
|
||||
var workflow = CreateWorkflow();
|
||||
|
||||
Assert.Throws<LegacyOverseasWorkflowException>(() =>
|
||||
workflow.MaterializePlaylistDraft("index-candle-5d"));
|
||||
Assert.Equal(
|
||||
"DJI@DJI",
|
||||
workflow.MaterializePlaylistDraft("index-candle-5d").Selection.DataCode.Split('|')[0]);
|
||||
Assert.Throws<LegacyOverseasWorkflowException>(() =>
|
||||
workflow.MaterializePlaylistDraft("industry-current"));
|
||||
Assert.Throws<LegacyOverseasWorkflowException>(() =>
|
||||
@@ -280,6 +322,38 @@ public sealed class LegacyOverseasSelectionWorkflowTests
|
||||
Assert.Equal(typeof(string), parameter.ParameterType);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task NonUsTwSearchHitMaterializesAnExactAllNationIdentity()
|
||||
{
|
||||
var workflow = CreateWorkflow(stocks: [new("London Stock", "LNS@LON", "GB")]);
|
||||
var searched = await workflow.SearchStocksAsync("london");
|
||||
workflow.SelectStock(Assert.Single(searched.Stock.Results).SelectionId);
|
||||
|
||||
var availability = workflow.GetActionAvailability("stock-current");
|
||||
|
||||
Assert.True(availability.IsAvailable);
|
||||
Assert.Null(availability.Reason);
|
||||
var current = workflow.MaterializePlaylistDraft("stock-current");
|
||||
var candle = workflow.MaterializePlaylistDraft("stock-candle-5d");
|
||||
Assert.Equal("LNS@LON|GB|1", current.Selection.DataCode);
|
||||
Assert.Equal("LNS@LON|GB|1", candle.Selection.DataCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ThreeLetterNationRowsRemainVisibleButFailClosedBeforePlaylistMaterialization()
|
||||
{
|
||||
var workflow = CreateWorkflow(stocks: [new("Unsupported", "LNS@UNSUPPORTED", "GBR")]);
|
||||
|
||||
var searched = await workflow.SearchStocksAsync("unsupported");
|
||||
|
||||
Assert.Equal("Unsupported", searched.Stock.Selected?.InputName);
|
||||
var availability = workflow.GetActionAvailability("stock-current");
|
||||
Assert.False(availability.IsAvailable);
|
||||
Assert.Equal("선택한 해외종목의 국가 코드는 두 글자여야 합니다.", availability.Reason);
|
||||
Assert.Throws<LegacyOverseasWorkflowException>(() =>
|
||||
workflow.MaterializePlaylistDraft("stock-current"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SlowerIndustryResponseCannotReplaceOrInvalidateNewerState()
|
||||
{
|
||||
@@ -361,7 +435,7 @@ public sealed class LegacyOverseasSelectionWorkflowTests
|
||||
{
|
||||
var invalid = StockResult(
|
||||
"query",
|
||||
new WorldStockSearchItem("London", "LNS@BAD", "GB"));
|
||||
new WorldStockSearchItem("London", "LNS@BAD", "gB"));
|
||||
var workflow = new LegacyOverseasSelectionWorkflow(
|
||||
new RecordingIndustryService([]),
|
||||
new QueueStockService(invalid));
|
||||
@@ -465,13 +539,13 @@ public sealed class LegacyOverseasSelectionWorkflowTests
|
||||
}
|
||||
|
||||
private sealed class RecordingStockService(
|
||||
IReadOnlyList<WorldStockSearchItem> items) : IWorldStockSearchService
|
||||
IReadOnlyList<WorldStockSearchItem> items) : IOverseasStockSearchService
|
||||
{
|
||||
public List<(string Query, int MaximumResults)> Calls { get; } = [];
|
||||
|
||||
public Task<WorldStockSearchResult> SearchAsync(
|
||||
string query,
|
||||
int maximumResults = LegacyWorldStockSearchService.DefaultMaximumResults,
|
||||
int maximumResults = LegacyOverseasStockSearchService.DefaultMaximumResults,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
@@ -500,13 +574,13 @@ public sealed class LegacyOverseasSelectionWorkflowTests
|
||||
}
|
||||
|
||||
private sealed class QueueStockService(
|
||||
params WorldStockSearchResult[] results) : IWorldStockSearchService
|
||||
params WorldStockSearchResult[] results) : IOverseasStockSearchService
|
||||
{
|
||||
private readonly Queue<WorldStockSearchResult> _results = new(results);
|
||||
|
||||
public Task<WorldStockSearchResult> SearchAsync(
|
||||
string query,
|
||||
int maximumResults = LegacyWorldStockSearchService.DefaultMaximumResults,
|
||||
int maximumResults = LegacyOverseasStockSearchService.DefaultMaximumResults,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
@@ -534,14 +608,14 @@ public sealed class LegacyOverseasSelectionWorkflowTests
|
||||
_pending[query].SetResult(result);
|
||||
}
|
||||
|
||||
private sealed class ControlledStockService : IWorldStockSearchService
|
||||
private sealed class ControlledStockService : IOverseasStockSearchService
|
||||
{
|
||||
private readonly Dictionary<string, TaskCompletionSource<WorldStockSearchResult>> _pending =
|
||||
new(StringComparer.Ordinal);
|
||||
|
||||
public Task<WorldStockSearchResult> SearchAsync(
|
||||
string query,
|
||||
int maximumResults = LegacyWorldStockSearchService.DefaultMaximumResults,
|
||||
int maximumResults = LegacyOverseasStockSearchService.DefaultMaximumResults,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var source = new TaskCompletionSource<WorldStockSearchResult>(
|
||||
|
||||
Reference in New Issue
Block a user