feat: restore legacy drag events and Tornado launch integration
This commit is contained in:
@@ -213,6 +213,30 @@ public sealed class LegacyOperatorControllerTests
|
||||
Assert.Single(swapped.Tabs, tab => tab.IsActive).Id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TabHoverSwapUsesExpectedPositionsSoDuplicateDeliveryCannotSwapBack()
|
||||
{
|
||||
var controller = new LegacyOperatorController(Lookup());
|
||||
|
||||
var first = await controller.SwapTabsAtExpectedPositionsAsync(
|
||||
LegacyOperatorTabId.Theme,
|
||||
LegacyOperatorTabId.Exchange,
|
||||
expectedSourceIndex: 6,
|
||||
expectedTargetIndex: 1);
|
||||
Assert.Equal(LegacyOperatorTabId.Theme, first.Tabs![1].Id);
|
||||
Assert.Equal(LegacyOperatorTabId.Exchange, first.Tabs[6].Id);
|
||||
|
||||
var revision = first.Revision;
|
||||
var duplicate = await controller.SwapTabsAtExpectedPositionsAsync(
|
||||
LegacyOperatorTabId.Theme,
|
||||
LegacyOperatorTabId.Exchange,
|
||||
expectedSourceIndex: 6,
|
||||
expectedTargetIndex: 1);
|
||||
Assert.Equal(revision, duplicate.Revision);
|
||||
Assert.Equal(LegacyOperatorTabId.Theme, duplicate.Tabs![1].Id);
|
||||
Assert.Equal(LegacyOperatorTabId.Exchange, duplicate.Tabs[6].Id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PlaylistKeyboardCommandsUseActiveRowAndBoundaries()
|
||||
{
|
||||
@@ -626,6 +650,65 @@ public sealed class LegacyOperatorControllerTests
|
||||
after.Playlist.Select(row => row.RowId));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PlaylistDragMovesNoncontiguousSelectionAsOneOrderedBlockAndIsIdempotent()
|
||||
{
|
||||
var controller = await ControllerWithPlaylistAsync(0, 1, 2, 4, 5, 6);
|
||||
var initial = controller.Current.Playlist.ToArray();
|
||||
|
||||
controller.SelectPlaylistRow(initial[1].RowId, control: false, shift: false);
|
||||
controller.SelectPlaylistRow(initial[3].RowId, control: true, shift: false);
|
||||
var moved = controller.ReorderPlaylistRows(
|
||||
initial[3].RowId,
|
||||
initial[5].RowId,
|
||||
LegacyDragDropPosition.Before);
|
||||
|
||||
Assert.Equal(
|
||||
[
|
||||
initial[0].RowId,
|
||||
initial[2].RowId,
|
||||
initial[4].RowId,
|
||||
initial[1].RowId,
|
||||
initial[3].RowId,
|
||||
initial[5].RowId
|
||||
],
|
||||
moved.Playlist.Select(row => row.RowId));
|
||||
Assert.Equal(
|
||||
[initial[1].RowId, initial[3].RowId],
|
||||
moved.Playlist.Where(row => row.IsSelected).Select(row => row.RowId));
|
||||
|
||||
var revision = moved.Revision;
|
||||
var duplicate = controller.ReorderPlaylistRows(
|
||||
initial[3].RowId,
|
||||
initial[5].RowId,
|
||||
LegacyDragDropPosition.Before);
|
||||
Assert.Equal(revision, duplicate.Revision);
|
||||
Assert.Equal(
|
||||
moved.Playlist.Select(row => row.RowId),
|
||||
duplicate.Playlist.Select(row => row.RowId));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PlaylistDragFromUnselectedRowMovesOnlyThatRowAndSelectsIt()
|
||||
{
|
||||
var controller = await ControllerWithPlaylistAsync(0, 1, 2, 4);
|
||||
var initial = controller.Current.Playlist.ToArray();
|
||||
controller.SelectPlaylistRow(initial[0].RowId, control: false, shift: false);
|
||||
controller.SelectPlaylistRow(initial[1].RowId, control: true, shift: false);
|
||||
|
||||
var moved = controller.ReorderPlaylistRows(
|
||||
initial[3].RowId,
|
||||
initial[0].RowId,
|
||||
LegacyDragDropPosition.Before);
|
||||
|
||||
Assert.Equal(
|
||||
[initial[3].RowId, initial[0].RowId, initial[1].RowId, initial[2].RowId],
|
||||
moved.Playlist.Select(row => row.RowId));
|
||||
Assert.Equal(
|
||||
initial[3].RowId,
|
||||
Assert.Single(moved.Playlist, row => row.IsSelected).RowId);
|
||||
}
|
||||
|
||||
private static async Task<LegacyOperatorController> ControllerWithPlaylistAsync(
|
||||
params int[] physicalCutRows)
|
||||
{
|
||||
|
||||
@@ -59,6 +59,77 @@ public sealed class LegacyOperatorCatalogWorkflowTests
|
||||
Assert.Equal(LegacyOperatorCatalogMode.List, deleted.Mode);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(ThemeMarket.Krx)]
|
||||
[InlineData(ThemeMarket.Nxt)]
|
||||
public async Task ThemeA_name_check_is_read_only_and_scoped_to_the_selected_market(
|
||||
ThemeMarket market)
|
||||
{
|
||||
var fixture = new Fixture();
|
||||
var workflow = fixture.CreateWorkflow();
|
||||
await workflow.OpenAsync(LegacyOperatorCatalogEntity.Theme);
|
||||
var creating = await workflow.BeginCreateThemeAsync(market);
|
||||
Assert.True(creating.CanCheckThemeName);
|
||||
|
||||
fixture.ThemePersistence.NameExists = false;
|
||||
var available = await workflow.CheckThemeNameAvailabilityAsync("AI 반도체");
|
||||
|
||||
Assert.Equal(LegacyThemeNameAvailability.Available, available.ThemeNameAvailability);
|
||||
Assert.Equal("AI 반도체", available.CheckedThemeName);
|
||||
Assert.Equal("사용 가능한 테마명입니다.", available.StatusMessage);
|
||||
Assert.Equal(market, fixture.ThemePersistence.LastNameCheckMarket);
|
||||
Assert.Equal("AI 반도체", fixture.ThemePersistence.LastCheckedName);
|
||||
Assert.Equal(0, fixture.ThemePersistence.CreateCalls);
|
||||
Assert.Equal(0, fixture.ThemePersistence.DeleteCalls);
|
||||
|
||||
fixture.ThemePersistence.NameExists = true;
|
||||
var duplicate = await workflow.CheckThemeNameAvailabilityAsync("AI 반도체");
|
||||
|
||||
Assert.Equal(LegacyThemeNameAvailability.Duplicate, duplicate.ThemeNameAvailability);
|
||||
Assert.Equal("이미 사용중인 테마명입니다.", duplicate.StatusMessage);
|
||||
Assert.Equal(2, fixture.ThemePersistence.NameExistsCalls);
|
||||
Assert.Equal(0, fixture.ThemePersistence.CreateCalls);
|
||||
Assert.Equal(0, fixture.ThemePersistence.DeleteCalls);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(ThemeMarket.Krx)]
|
||||
[InlineData(ThemeMarket.Nxt)]
|
||||
public async Task ThemeA_edit_name_check_preserves_original_self_duplicate_behavior(
|
||||
ThemeMarket market)
|
||||
{
|
||||
var fixture = new Fixture();
|
||||
var item = ThemeItem(market, "AI 반도체");
|
||||
fixture.ThemeSelection.SearchItems = [item];
|
||||
fixture.ThemePersistence.NameExists = true;
|
||||
var workflow = fixture.CreateWorkflow();
|
||||
await workflow.OpenThemeForEditAsync(item.Identity);
|
||||
|
||||
var result = await workflow.CheckThemeNameAvailabilityAsync("AI 반도체");
|
||||
|
||||
Assert.Equal(LegacyThemeNameAvailability.Duplicate, result.ThemeNameAvailability);
|
||||
Assert.Equal(market, fixture.ThemePersistence.LastNameCheckMarket);
|
||||
Assert.Equal("AI 반도체", fixture.ThemePersistence.LastCheckedName);
|
||||
Assert.Equal(0, fixture.ThemePersistence.CreateCalls);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ThemeA_name_check_reports_indeterminate_when_exact_query_fails()
|
||||
{
|
||||
var fixture = new Fixture();
|
||||
var workflow = fixture.CreateWorkflow();
|
||||
await workflow.OpenAsync(LegacyOperatorCatalogEntity.Theme);
|
||||
await workflow.BeginCreateThemeAsync(ThemeMarket.Krx);
|
||||
fixture.ThemePersistence.NameExistsException = new InvalidOperationException("offline");
|
||||
|
||||
var result = await workflow.CheckThemeNameAvailabilityAsync("새 테마");
|
||||
|
||||
Assert.Equal(LegacyThemeNameAvailability.Indeterminate, result.ThemeNameAvailability);
|
||||
Assert.Equal("새 테마", result.CheckedThemeName);
|
||||
Assert.Equal(LegacyOperatorCatalogStatusKind.Error, result.StatusKind);
|
||||
Assert.Equal(0, fixture.ThemePersistence.CreateCalls);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EList_preserves_buy_amount_and_CSharp_owned_reorder_in_one_create()
|
||||
{
|
||||
@@ -103,6 +174,47 @@ public sealed class LegacyOperatorCatalogWorkflowTests
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EList_drag_reorder_moves_to_arbitrary_position_and_duplicate_is_no_op()
|
||||
{
|
||||
var fixture = new Fixture();
|
||||
fixture.StockSearch.Items =
|
||||
[
|
||||
new StockSearchItem(StockMarket.Kospi, DataSourceKind.Oracle, "First", "000001"),
|
||||
new StockSearchItem(StockMarket.Kospi, DataSourceKind.Oracle, "Second", "000002"),
|
||||
new StockSearchItem(StockMarket.Kospi, DataSourceKind.Oracle, "Third", "000003")
|
||||
];
|
||||
var workflow = fixture.CreateWorkflow();
|
||||
await workflow.OpenAsync(LegacyOperatorCatalogEntity.Expert);
|
||||
await workflow.BeginCreateExpertAsync();
|
||||
var stocks = await workflow.SearchStocksAsync("stock");
|
||||
foreach (var stock in stocks.StockResults)
|
||||
{
|
||||
workflow.SelectStock(stock.ResultId);
|
||||
workflow.AddSelectedStock(10_000);
|
||||
}
|
||||
|
||||
var initial = workflow.Current.DraftRows.ToArray();
|
||||
var moved = workflow.ReorderDraftRow(
|
||||
initial[0].RowId,
|
||||
initial[2].RowId,
|
||||
LegacyDragDropPosition.After);
|
||||
Assert.Equal(
|
||||
[initial[1].RowId, initial[2].RowId, initial[0].RowId],
|
||||
moved.DraftRows.Select(row => row.RowId));
|
||||
Assert.Equal([0, 1, 2], moved.DraftRows.Select(row => row.Position));
|
||||
|
||||
var revision = moved.Revision;
|
||||
var duplicate = workflow.ReorderDraftRow(
|
||||
initial[0].RowId,
|
||||
initial[2].RowId,
|
||||
LegacyDragDropPosition.After);
|
||||
Assert.Equal(revision, duplicate.Revision);
|
||||
Assert.Equal(
|
||||
moved.DraftRows.Select(row => row.RowId),
|
||||
duplicate.DraftRows.Select(row => row.RowId));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Uc4_and_Uc6_edit_entries_resolve_only_the_native_selected_identity()
|
||||
{
|
||||
@@ -197,27 +309,29 @@ public sealed class LegacyOperatorCatalogWorkflowTests
|
||||
|
||||
private sealed class FakeThemeSelection : IThemeSelectionService
|
||||
{
|
||||
internal int SearchCalls { get; private set; }
|
||||
|
||||
internal int PreviewCalls { get; private set; }
|
||||
|
||||
internal IReadOnlyList<ThemeSelectorItem> SearchItems { get; set; } =
|
||||
[
|
||||
ThemeItem(ThemeMarket.Krx, "AI 반도체")
|
||||
];
|
||||
|
||||
public Task<ThemeSearchResult> SearchAsync(
|
||||
string query,
|
||||
ThemeSession nxtSession,
|
||||
int maximumResults = LegacyThemeSelectionService.DefaultMaximumResults,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
Task.FromResult(new ThemeSearchResult(
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
SearchCalls++;
|
||||
return Task.FromResult(new ThemeSearchResult(
|
||||
query,
|
||||
nxtSession,
|
||||
DateTimeOffset.UtcNow,
|
||||
[
|
||||
new ThemeSelectorItem(
|
||||
new ThemeSelectionIdentity(
|
||||
ThemeMarket.Krx,
|
||||
ThemeSession.NotApplicable,
|
||||
"00000001",
|
||||
"AI 반도체"),
|
||||
DataSourceKind.Oracle)
|
||||
],
|
||||
SearchItems,
|
||||
false));
|
||||
}
|
||||
|
||||
public Task<ThemePreviewResult> PreviewAsync(
|
||||
ThemeSelectionIdentity identity,
|
||||
@@ -270,8 +384,31 @@ public sealed class LegacyOperatorCatalogWorkflowTests
|
||||
|
||||
internal Exception? CreateException { get; set; }
|
||||
|
||||
internal int NameExistsCalls { get; private set; }
|
||||
|
||||
internal bool NameExists { get; set; }
|
||||
|
||||
internal Exception? NameExistsException { get; set; }
|
||||
|
||||
internal ThemeMarket? LastNameCheckMarket { get; private set; }
|
||||
|
||||
internal string? LastCheckedName { get; private set; }
|
||||
|
||||
public bool CanMutate => true;
|
||||
|
||||
public Task<bool> ThemeNameExistsAsync(
|
||||
ThemeMarket market,
|
||||
string themeTitle,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
NameExistsCalls++;
|
||||
LastNameCheckMarket = market;
|
||||
LastCheckedName = themeTitle;
|
||||
return NameExistsException is null
|
||||
? Task.FromResult(NameExists)
|
||||
: Task.FromException<bool>(NameExistsException);
|
||||
}
|
||||
|
||||
public Task<string> SuggestNextCodeAsync(
|
||||
ThemeMarket market,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
@@ -391,4 +528,15 @@ public sealed class LegacyOperatorCatalogWorkflowTests
|
||||
Guid.NewGuid(),
|
||||
kind,
|
||||
DateTimeOffset.UtcNow));
|
||||
|
||||
private static ThemeSelectorItem ThemeItem(ThemeMarket market, string title) =>
|
||||
new(
|
||||
new ThemeSelectionIdentity(
|
||||
market,
|
||||
market == ThemeMarket.Nxt
|
||||
? ThemeSession.PreMarket
|
||||
: ThemeSession.NotApplicable,
|
||||
market == ThemeMarket.Nxt ? "N0000001" : "00000001",
|
||||
title),
|
||||
market == ThemeMarket.Nxt ? DataSourceKind.MariaDb : DataSourceKind.Oracle);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user