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

@@ -39,7 +39,39 @@ public sealed class LegacyOperatorControllerTests
}
[Fact]
public async Task IndustryParentDoubleClickMaterializesDirectChildrenAtomicallyInOrder()
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(
@@ -53,16 +85,15 @@ public sealed class LegacyOperatorControllerTests
new StaticIndustryService(industries)));
await controller.SelectTabAsync(LegacyOperatorTabId.KospiIndustry);
controller.SelectIndustry(0, doubleClick: true);
var rejected = controller.ActivateIndustrySection(0);
Assert.Empty(rejected.Playlist);
controller.SelectIndustry(1, doubleClick: true);
var added = controller.ActivateIndustrySection(0);
Assert.Collection(
added.Playlist,
row => Assert.Equal("5001", row.SceneAlias),
row => Assert.Equal("8018", row.SceneAlias));
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]
@@ -96,11 +127,123 @@ public sealed class LegacyOperatorControllerTests
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(
"GAIN_DESC",
new MBN_STOCK_WEBVIEW.Core.Playout.Scenes.LegacySceneSelection(
"PAGED_THEME",
"AI 테마",
"GAIN_DESC",
"CURRENT",
"00000001"),
Assert.IsType<MBN_STOCK_WEBVIEW.Core.Playout.Scenes.LegacySceneSelection>(
row.ToPlayoutEntry().Selection).GraphicType);
row.ToPlayoutEntry().Selection));
}
[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"),
Assert.IsType<MBN_STOCK_WEBVIEW.Core.Playout.Scenes.LegacySceneSelection>(
row.ToPlayoutEntry().Selection));
}
[Fact]
@@ -962,6 +1105,19 @@ public sealed class LegacyOperatorControllerTests
}
}
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;
@@ -991,17 +1147,30 @@ public sealed class LegacyOperatorControllerTests
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) => Task.FromResult(
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, DataSourceKind.Oracle)],
[new ThemeSelectorItem(
identity,
identity.Market == ThemeMarket.Krx
? DataSourceKind.Oracle
: DataSourceKind.MariaDb)],
false));
}
public Task<ThemePreviewResult> PreviewAsync(
ThemeSelectionIdentity identity,
@@ -1011,7 +1180,7 @@ public sealed class LegacyOperatorControllerTests
PreviewCalls++;
return Task.FromResult(
new ThemePreviewResult(
_identity,
identity,
DateTimeOffset.UtcNow,
_items,
false));