feat: advance legacy UI behavior parity
This commit is contained in:
@@ -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));
|
||||
|
||||
Reference in New Issue
Block a user