feat: align legacy operator and playout behavior

This commit is contained in:
2026-07-15 21:04:05 +09:00
parent 06a16e5741
commit 83398044c6
77 changed files with 6377 additions and 391 deletions

View File

@@ -38,6 +38,81 @@ public sealed class LegacyBridgeProtocolTests
Assert.Equal(1_400, pointer.TimestampMilliseconds);
}
[Theory]
[InlineData(49, true)]
[InlineData(50, false)]
public void ParsePointer_UsesTheFullFiftyRowRuntimeMenuBoundary(
int physicalIndex,
bool expected)
{
var json =
"{\"type\":\"cut-pointer-down\",\"payload\":{" +
$"\"physicalIndex\":{physicalIndex},\"control\":false," +
"\"shift\":false,\"x\":0,\"y\":0,\"timestampMilliseconds\":0}}";
Assert.Equal(expected, LegacyBridgeProtocol.TryParseIntent(
json,
out var parsed,
out _));
if (expected)
{
Assert.Equal(
physicalIndex,
Assert.IsType<LegacyCutPointerDownIntent>(parsed).PhysicalIndex);
}
}
[Theory]
[InlineData("arrow-up", LegacyCutKeyboardKey.ArrowUp)]
[InlineData("arrow-down", LegacyCutKeyboardKey.ArrowDown)]
[InlineData("home", LegacyCutKeyboardKey.Home)]
[InlineData("end", LegacyCutKeyboardKey.End)]
[InlineData("space", LegacyCutKeyboardKey.Space)]
public void ParseCutKeyDown_AcceptsOnlyClosedKeysAndModifiers(
string key,
LegacyCutKeyboardKey expected)
{
var json =
"{\"type\":\"cut-key-down\",\"payload\":{" +
$"\"key\":\"{key}\",\"control\":true,\"shift\":false}}}}";
Assert.True(LegacyBridgeProtocol.TryParseIntent(json, out var parsed, out _));
var intent = Assert.IsType<LegacyCutKeyDownIntent>(parsed);
Assert.Equal(expected, intent.Key);
Assert.True(intent.Control);
Assert.False(intent.Shift);
}
[Theory]
[InlineData("page-down", "\"control\":false,\"shift\":false")]
[InlineData("ArrowUp", "\"control\":false,\"shift\":false")]
[InlineData("home", "\"control\":0,\"shift\":false")]
[InlineData("home", "\"control\":false,\"shift\":false,\"physicalIndex\":0")]
public void ParseCutKeyDown_RejectsUnknownOrAdditionalAuthority(
string key,
string fields)
{
var json =
"{\"type\":\"cut-key-down\",\"payload\":{" +
$"\"key\":\"{key}\",{fields}}}}}";
Assert.False(LegacyBridgeProtocol.TryParseIntent(json, out _, out _));
}
[Fact]
public void ParseClearCutSelection_AcceptsOnlyAnEmptyPayload()
{
Assert.True(LegacyBridgeProtocol.TryParseIntent(
"{\"type\":\"clear-cut-selection\",\"payload\":{}}",
out var parsed,
out _));
Assert.IsType<LegacyClearCutSelectionIntent>(parsed);
Assert.False(LegacyBridgeProtocol.TryParseIntent(
"{\"type\":\"clear-cut-selection\",\"payload\":{\"physicalIndex\":0}}",
out _,
out _));
}
[Fact]
public void ParsePlaylistSelection_PreservesModifiersAndRejectsUnsafeAuthority()
{
@@ -61,6 +136,27 @@ public sealed class LegacyBridgeProtocolTests
out _));
}
[Fact]
public void ParsePlaylistActivation_AcceptsOnlyOneOpaqueRowId()
{
Assert.True(LegacyBridgeProtocol.TryParseIntent(
"{\"type\":\"activate-playlist-row\",\"payload\":{\"rowId\":\"legacy-stock-00000003\"}}",
out var parsed,
out _));
Assert.Equal(
"legacy-stock-00000003",
Assert.IsType<LegacyActivatePlaylistRowIntent>(parsed).RowId);
Assert.False(LegacyBridgeProtocol.TryParseIntent(
"{\"type\":\"activate-playlist-row\",\"payload\":{\"rowId\":\"../row\"}}",
out _,
out _));
Assert.False(LegacyBridgeProtocol.TryParseIntent(
"{\"type\":\"activate-playlist-row\",\"payload\":{\"rowId\":\"legacy-stock-00000003\",\"enabled\":false}}",
out _,
out _));
}
[Fact]
public void ParsePlaylistReorder_UsesOnlyOpaqueRowsAndClosedDropPosition()
{
@@ -427,7 +523,7 @@ public sealed class LegacyBridgeProtocolTests
"삼성",
[new LegacyStockResultRow(0, "삼성전자")],
0,
[new LegacyCutViewRow(1, 2, "1열판기본_현재가", false, true)],
[new LegacyCutViewRow(1, 2, "1열판기본_현재가", false, true, true)],
[playlistRow],
null,
string.Empty,
@@ -450,10 +546,45 @@ public sealed class LegacyBridgeProtocolTests
Assert.Equal(
"1열판기본_현재가",
payload.GetProperty("cutRows")[0].GetProperty("rawLabel").GetString());
Assert.True(payload.GetProperty("cutRows")[0].GetProperty("isFocused").GetBoolean());
Assert.False(payload.GetProperty("playlist")[0].GetProperty("isSelected").GetBoolean());
Assert.False(payload.GetProperty("playlist")[0].GetProperty("isActive").GetBoolean());
Assert.True(payload.GetProperty("isBusy").GetBoolean());
}
[Fact]
public void SerializedState_PreservesInjectedRuntimeCutMenuOrderAndSeparators()
{
var controller = new LegacyOperatorController(
new EmptyStockLookup(),
cutRows:
[
new LegacyCutCatalogRow(
0,
1,
LegacyStockCutCatalog.Rows[1].RawLabel,
IsSeparator: false),
new LegacyCutCatalogRow(1, null, string.Empty, IsSeparator: true),
new LegacyCutCatalogRow(
2,
2,
LegacyStockCutCatalog.Rows[0].RawLabel,
IsSeparator: false)
]);
var json = LegacyBridgeProtocol.SerializeState(controller.Current);
using var document = JsonDocument.Parse(json);
var rows = document.RootElement.GetProperty("payload").GetProperty("cutRows");
Assert.Equal(3, rows.GetArrayLength());
Assert.Equal("1열판기본_현재가", rows[0].GetProperty("rawLabel").GetString());
Assert.Equal(0, rows[0].GetProperty("physicalIndex").GetInt32());
Assert.True(rows[1].GetProperty("isSeparator").GetBoolean());
Assert.Equal(string.Empty, rows[1].GetProperty("rawLabel").GetString());
Assert.Equal("1열판기본_예상체결가", rows[2].GetProperty("rawLabel").GetString());
Assert.Equal(2, rows[2].GetProperty("displayOrdinal").GetInt32());
}
[Fact]
public async Task SerializedNamedPlaylist_ProjectsOnlyOpaqueListAndSafetyState()
{
@@ -540,4 +671,12 @@ public sealed class LegacyBridgeProtocolTests
string programCode,
CancellationToken cancellationToken = default) => Task.CompletedTask;
}
private sealed class EmptyStockLookup : ILegacyStockLookup
{
public Task<LegacyStockSearchData> SearchAsync(
string query,
CancellationToken cancellationToken = default) =>
Task.FromResult(new LegacyStockSearchData(query, [], []));
}
}

View File

@@ -65,9 +65,25 @@ public sealed class LegacyPlayoutBridgeTests
Assert.IsType<LegacyChooseBackgroundIntent>(choose);
}
[Theory]
[InlineData(0)]
[InlineData(6)]
[InlineData(19)]
public void FadeDuration_AcceptsOriginalComboIndexRange(int duration)
{
Assert.True(LegacyBridgeProtocol.TryParseIntent(
"{\"type\":\"set-fade-duration\",\"payload\":{\"duration\":" +
duration + "}}",
out var intent,
out var error), error);
Assert.Equal(duration,
Assert.IsType<LegacySetFadeDurationIntent>(intent).Duration);
}
[Theory]
[InlineData("-1")]
[InlineData("61")]
[InlineData("20")]
[InlineData("60")]
[InlineData("1.5")]
[InlineData("\"6\"")]
[InlineData("null")]
@@ -124,7 +140,10 @@ public sealed class LegacyPlayoutBridgeTests
BackgroundEnabled: true,
BackgroundFileName: "기본.vrv",
CanChangeBackground: false,
Message: "준비 완료");
Message: "준비 완료")
{
IsTakeOutCompletionPending = true
};
using var document = JsonDocument.Parse(
LegacyBridgeProtocol.SerializeState(snapshot, playout: playout));
@@ -137,7 +156,8 @@ public sealed class LegacyPlayoutBridgeTests
Assert.Equal("pageNext", projected.GetProperty("nextKind").GetString());
Assert.Equal(2, projected.GetProperty("pageCount").GetInt32());
Assert.Equal("기본.vrv", projected.GetProperty("backgroundFileName").GetString());
Assert.False(projected.TryGetProperty("currentEntryId", out _));
Assert.Equal("entry-1", projected.GetProperty("currentEntryId").GetString());
Assert.True(projected.GetProperty("isTakeOutCompletionPending").GetBoolean());
Assert.False(projected.TryGetProperty("builderKey", out _));
}
}