feat: advance legacy UI behavior parity

This commit is contained in:
2026-07-18 10:18:29 +09:00
parent d7ec571343
commit 1302b1b92f
71 changed files with 11244 additions and 1153 deletions

View File

@@ -79,17 +79,59 @@ public sealed class LegacyThemeWorkflowTests
workflow.CreateInitial(ThemeSession.NotApplicable));
}
[Theory]
[InlineData(0, ThemeSession.PreMarket)]
[InlineData(12, ThemeSession.PreMarket)]
[InlineData(13, ThemeSession.AfterMarket)]
[InlineData(23, ThemeSession.AfterMarket)]
public void CurrentNxtSession_UsesTheOriginalNativeLocalHourBoundary(
int hour,
ThemeSession expected)
{
var workflow = new LegacyThemeWorkflow(
new FakeThemeSelectionService(),
() => new DateTimeOffset(2026, 7, 18, hour, 0, 0, TimeSpan.FromHours(9)));
Assert.Equal(expected, workflow.CurrentNxtSession);
}
[Fact]
public async Task SearchAsync_NormalizesQueryPreservesExplicitIdentityAndOrdersRows()
public async Task ResetForTabActivation_RestoresTheNewUc4Defaults()
{
var identity = Identity(
ThemeMarket.Krx,
ThemeSession.NotApplicable,
"AI",
"00000001");
var workflow = new LegacyThemeWorkflow(
ServiceWithTheme(identity, [new(0, "P005930", "삼성전자")]));
var selected = workflow.SelectSort(
await SearchAndSelectAsync(workflow, ThemeSession.AfterMarket),
"GAIN_DESC");
var reset = workflow.ResetForTabActivation(selected);
Assert.Equal(selected.Revision + 1, reset.Revision);
Assert.Equal(string.Empty, reset.Query);
Assert.Equal(ThemeSession.PreMarket, reset.NxtSession);
Assert.Equal(LegacyThemeSort.InputOrder, reset.Sort);
Assert.Empty(reset.SearchResults);
Assert.Null(reset.SelectedResultId);
Assert.Null(reset.SelectedTheme);
Assert.Empty(reset.PreviewItems);
}
[Fact]
public async Task SearchAsync_NormalizesQueryAndPreservesProviderOrderWithinKrxThenNxt()
{
var service = new FakeThemeSelectionService
{
SearchHandler = (query, session) => SearchResult(
query,
session,
Selector(ThemeMarket.Nxt, session, "Robotics", "00000003"),
Selector(ThemeMarket.Krx, ThemeSession.NotApplicable, "Zulu", "00000002"),
Selector(ThemeMarket.Krx, ThemeSession.NotApplicable, "Alpha", "00000001"))
Selector(ThemeMarket.Krx, ThemeSession.NotApplicable, "Alpha", "00000001"),
Selector(ThemeMarket.Nxt, session, "Robotics", "00000003"))
};
var workflow = new LegacyThemeWorkflow(service);
var initial = workflow.CreateInitial();
@@ -110,12 +152,12 @@ public sealed class LegacyThemeWorkflowTests
row =>
{
Assert.Equal("theme-result-001", row.ResultId);
Assert.Equal("Alpha", row.DisplayTitle);
Assert.Equal("Zulu", row.DisplayTitle);
Assert.Equal(ThemeMarket.Krx, row.Market);
Assert.Equal(ThemeSession.NotApplicable, row.Session);
Assert.Equal(DataSourceKind.Oracle, row.Source);
},
row => Assert.Equal("Zulu", row.DisplayTitle),
row => Assert.Equal("Alpha", row.DisplayTitle),
row =>
{
Assert.Equal("theme-result-003", row.ResultId);
@@ -195,8 +237,30 @@ public sealed class LegacyThemeWorkflowTests
session,
Selector(ThemeMarket.Krx, ThemeSession.NotApplicable, "AI", "00000001"),
Selector(ThemeMarket.Krx, ThemeSession.NotApplicable, "AI", "00000002"));
await Assert.ThrowsAsync<LegacyThemeWorkflowDataException>(() =>
workflow.SearchAsync(snapshot, "AI", ThemeSession.PreMarket));
var duplicateTitles = await workflow.SearchAsync(
snapshot,
"AI",
ThemeSession.PreMarket);
Assert.Equal(2, duplicateTitles.SearchResults.Count);
}
[Fact]
public async Task SearchAsync_RejectsProviderRowsThatWouldRequireReordering()
{
var service = new FakeThemeSelectionService
{
SearchHandler = (query, session) => SearchResult(
query,
session,
Selector(ThemeMarket.Nxt, session, "NXT first", "00000002"),
Selector(ThemeMarket.Krx, ThemeSession.NotApplicable, "KRX later", "00000001"))
};
var workflow = new LegacyThemeWorkflow(service);
var exception = await Assert.ThrowsAsync<LegacyThemeWorkflowDataException>(() =>
workflow.SearchAsync(workflow.CreateInitial(), "", ThemeSession.PreMarket));
Assert.Contains("search provider order", exception.Message, StringComparison.Ordinal);
}
[Fact]
@@ -279,6 +343,38 @@ public sealed class LegacyThemeWorkflowTests
Assert.Single(selected.PreviewItems);
}
[Fact]
public async Task SearchAsync_RetainsOnlyACorrelatedSelectionAndPreview()
{
var identity = Identity(
ThemeMarket.Krx,
ThemeSession.NotApplicable,
"AI",
"00000001");
var service = ServiceWithTheme(identity, [new(0, "P005930", "삼성전자")]);
var workflow = new LegacyThemeWorkflow(service);
var selected = workflow.SelectSort(await SearchAndSelectAsync(workflow), "GAIN_DESC");
var refreshed = await workflow.SearchAsync(
selected,
"AI",
ThemeSession.AfterMarket);
Assert.Equal(
new LegacySelectedThemeSummary(
ThemeMarket.Krx,
ThemeSession.NotApplicable,
"AI",
1,
false),
refreshed.SelectedTheme);
Assert.NotNull(refreshed.SelectedResultId);
Assert.Single(refreshed.PreviewItems);
Assert.Equal("삼성전자", refreshed.PreviewItems[0].ItemName);
Assert.Equal(LegacyThemeSort.GainRateDescending, refreshed.Sort);
Assert.Single(service.PreviewCalls);
}
[Fact]
public async Task SelectAsync_RejectsUnknownResultWithoutCallingPreview()
{
@@ -398,7 +494,18 @@ public sealed class LegacyThemeWorkflowTests
Assert.Equal(action.Code, draft.Code);
Assert.Equal(action.Aliases, draft.Aliases);
Assert.Equal("AI", draft.Title);
Assert.Equal($"{action.Label} · {sortLabel}", draft.Detail);
var expectedGraphicType = action.Id switch
{
"five-row-current" or "five-row-expected" => "5단 표그래프",
"six-row-current" => "6종목 현재가",
"twelve-row-current" => "12종목 현재가",
_ => throw new InvalidOperationException()
};
var expectedSubtype =
$"테마-{(action.ValueKind == LegacyThemeValueKind.Expected ? "" : "")}({sortLabel})";
Assert.Equal(expectedGraphicType, draft.PlaylistGraphicType);
Assert.Equal(expectedSubtype, draft.PlaylistSubtype);
Assert.Equal(expectedSubtype, draft.Detail);
Assert.Equal("plate", draft.Category);
Assert.Equal("theme", draft.Market);
Assert.Equal("PAGED_THEME", draft.Selection.GroupCode);
@@ -418,7 +525,7 @@ public sealed class LegacyThemeWorkflowTests
[InlineData("INPUT_ORDER")]
[InlineData("GAIN_DESC")]
[InlineData("GAIN_ASC")]
public async Task MaterializePlaylistDraft_NxtKeepsSessionAndBlocksExpected(
public async Task MaterializePlaylistDraft_NxtKeepsSessionAndExpectedClickShowsOriginalWarning(
string sortId)
{
var identity = Identity(
@@ -427,7 +534,9 @@ public sealed class LegacyThemeWorkflowTests
"Robotics",
"00000002");
var service = ServiceWithTheme(identity, CreateItems(7, ThemeMarket.Nxt));
var workflow = new LegacyThemeWorkflow(service);
var workflow = new LegacyThemeWorkflow(
service,
() => new DateTimeOffset(2026, 7, 18, 14, 0, 0, TimeSpan.FromHours(9)));
var selected = workflow.SelectSort(
await SearchAndSelectAsync(workflow, ThemeSession.AfterMarket),
sortId);
@@ -451,11 +560,38 @@ public sealed class LegacyThemeWorkflowTests
}
var expectedState = workflow.GetActionState(selected, "five-row-expected");
Assert.False(expectedState.IsAvailable);
Assert.Equal("expected-theme-is-krx-only", expectedState.UnavailableReason);
Assert.True(expectedState.IsAvailable);
Assert.Null(expectedState.UnavailableReason);
var exception = Assert.Throws<InvalidOperationException>(() =>
workflow.MaterializePlaylistDraft(selected, "five-row-expected"));
Assert.Contains("expected-price", exception.Message, StringComparison.Ordinal);
Assert.Equal("NXT는 데이터가 없습니다.", exception.Message);
}
[Fact]
public async Task MaterializePlaylistDraft_NxtReevaluatesTheNativeHourAtActionTime()
{
var localNow = new DateTimeOffset(
2026,
7,
18,
10,
0,
0,
TimeSpan.FromHours(9));
var identity = Identity(
ThemeMarket.Nxt,
ThemeSession.PreMarket,
"Robotics",
"00000002");
var workflow = new LegacyThemeWorkflow(
ServiceWithTheme(identity, CreateItems(1, ThemeMarket.Nxt)),
() => localNow);
var selected = await SearchAndSelectAsync(workflow, ThemeSession.PreMarket);
localNow = localNow.AddHours(4);
var draft = workflow.MaterializePlaylistDraft(selected, "five-row-current");
Assert.Equal("AFTER_MARKET", draft.Selection.GraphicType);
}
[Theory]