270 lines
9.9 KiB
JavaScript
270 lines
9.9 KiB
JavaScript
"use strict";
|
|
|
|
const assert = require("node:assert/strict");
|
|
const fs = require("node:fs");
|
|
const path = require("node:path");
|
|
const test = require("node:test");
|
|
|
|
const repositoryRoot = path.resolve(__dirname, "..", "..");
|
|
const harness = fs.readFileSync(path.join(
|
|
repositoryRoot, "scripts", "Test-LegacyPackageInput.mjs"), "utf8");
|
|
const runner = fs.readFileSync(path.join(
|
|
repositoryRoot, "scripts", "Invoke-LegacyPackageInputSmoke.ps1"), "utf8");
|
|
|
|
function section(source, startMarker, endMarker) {
|
|
const start = source.indexOf(startMarker);
|
|
assert.notEqual(start, -1, "missing start marker: " + startMarker);
|
|
const end = source.indexOf(endMarker, start + startMarker.length);
|
|
assert.ok(end > start, "missing end marker: " + endMarker);
|
|
return source.slice(start, end);
|
|
}
|
|
|
|
function assertInOrder(source, markers) {
|
|
let cursor = 0;
|
|
for (const marker of markers) {
|
|
const index = source.indexOf(marker, cursor);
|
|
assert.ok(index >= cursor, "missing or out-of-order marker: " + marker);
|
|
cursor = index + marker.length;
|
|
}
|
|
}
|
|
|
|
test("DryRun PROGRAM uses the original all, mouse, Space, and NEXT split", () => {
|
|
const sequence = section(
|
|
harness,
|
|
"async function executeDryRunPlayoutSequence",
|
|
"function recordFullUiDbCheck");
|
|
assertInOrder(sequence, [
|
|
"PROGRAM clear every enabled row",
|
|
"snapshot.state.playlist.every(row => row.isEnabled === false)",
|
|
"snapshot.state.playout.nextKind === \"endOfPlaylist\"",
|
|
"PROGRAM restore every enabled row",
|
|
"snapshot.state.playlist.every(row => row.isEnabled === true)",
|
|
"pointerClickDisabledPlaylistCheckbox(",
|
|
"checkboxAriaDisabled === true",
|
|
"pressKey(\"Space\"",
|
|
"[\"toggle-active-playlist-enabled\"]",
|
|
"DryRun NEXT skips the Space-disabled future row",
|
|
"currentEntryId === nextEntryId"
|
|
]);
|
|
assert.match(sequence, /\["set-all-playlist-enabled"\]/u);
|
|
assert.match(sequence, /setAllIntentCount: 2/u);
|
|
assert.match(sequence, /toggleActiveIntentCount: 1/u);
|
|
assert.doesNotMatch(sequence, /send\("set-playlist-enabled"/u);
|
|
});
|
|
|
|
test("F8 and Escape are physical one-shot keys only in known DryRun phases", () => {
|
|
const authorization = section(
|
|
harness,
|
|
"function authorizeDryRunPlayoutShortcut",
|
|
"async function setSearchText");
|
|
assert.match(authorization, /new Set\(\["F8", "Escape"\]\)/u);
|
|
assert.match(authorization,
|
|
/definitionName === "F8" \? "idle" : "program"/u);
|
|
assert.match(authorization, /playout\.outcomeUnknown === true/u);
|
|
assert.match(authorization, /playout\.isPlayCompletionPending === true/u);
|
|
assert.match(authorization, /playout\.isTakeOutCompletionPending === true/u);
|
|
assert.match(authorization, /authorization\.consumed === true/u);
|
|
assert.match(authorization, /authorization\.consumed = true/u);
|
|
|
|
const sequence = section(
|
|
harness,
|
|
"async function executeDryRunPlayoutSequence",
|
|
"function recordFullUiDbCheck");
|
|
assertInOrder(sequence, [
|
|
"const f8Authorization = authorizeDryRunPlayoutShortcut(",
|
|
"\"rapid DryRun F8 after row selection\"",
|
|
"takeInIntents.length !== 1",
|
|
"const escapeAuthorization = authorizeDryRunPlayoutShortcut(",
|
|
"\"DryRun Escape TAKE OUT after NEXT\"",
|
|
"takeOutIntents.length !== 1",
|
|
"finalPhase = current.state.playout.phase"
|
|
]);
|
|
});
|
|
|
|
test("ThemeA button and existing UC6 Enter expose a bounded blank stock list", () => {
|
|
const blankSearch = section(
|
|
harness,
|
|
"async function verifyBlankOperatorCatalogStockSearch",
|
|
"async function addOperatorCatalogStock");
|
|
assertInOrder(blankSearch, [
|
|
"`${label} blank query`",
|
|
"trigger === \"button\"",
|
|
"pressKey(\"Enter\"",
|
|
"currentCatalog?.stockQuery === \"\"",
|
|
"resultCount > 1 && resultCount <= 10_000",
|
|
"statusMessage === \"종목 검색을 완료했습니다.\"",
|
|
"operatorCatalogStockResultCount === resultCount",
|
|
"messages[0].emptyStockQuery !== true",
|
|
"resultCount !== 10_000 || !truncationVisible",
|
|
"draftRowsPreserved: true"
|
|
]);
|
|
|
|
const theme = section(
|
|
harness,
|
|
"async function createReadEditDeleteTheme",
|
|
"async function searchAndSelectMainExpert");
|
|
assertInOrder(theme, [
|
|
"`${label} ThemeA editor`",
|
|
"addOperatorCatalogStockByDoubleClick("
|
|
]);
|
|
|
|
const expert = section(
|
|
harness,
|
|
"async function createReadEditDeleteExpert",
|
|
"async function executeOperatorCatalogCrud");
|
|
assertInOrder(expert, [
|
|
"snapshot.state.operatorCatalog?.mode === \"edit\"",
|
|
"`${label} existing UC6 editor`",
|
|
"addOperatorCatalogStockByDoubleClick("
|
|
]);
|
|
});
|
|
|
|
test("KRX ThemeA create physically round-trips NXT while preserving the name", () => {
|
|
const allowed = section(
|
|
harness,
|
|
"const fullUiDbAllowedOutboundMessageTypes",
|
|
"const playoutIntentMessageTypes");
|
|
assert.match(allowed, /"change-create-theme-market"/u);
|
|
|
|
const roundTrip = section(
|
|
harness,
|
|
"async function verifyKrxThemeCreateMarketRoundTrip",
|
|
"async function createReadEditDeleteTheme");
|
|
assertInOrder(roundTrip, [
|
|
"operatorCatalogEditorMarketValue !== \"krx\"",
|
|
"draftRows?.some(row => row.rowId === draftRowId)",
|
|
"switch editor market to NXT",
|
|
"newThemeMarket === \"nxt\"",
|
|
"editorName === title",
|
|
"draftRows?.length === 0",
|
|
"statusKind === \"warning\"",
|
|
"operatorCatalogNameValue === title",
|
|
"change-create-theme-market",
|
|
"switch editor market back to KRX",
|
|
"newThemeMarket === \"krx\"",
|
|
"operatorCatalogNameValue === title",
|
|
"removedDraftRowId: draftRowId"
|
|
]);
|
|
|
|
const create = section(
|
|
harness,
|
|
"async function createReadEditDeleteTheme",
|
|
"async function searchAndSelectMainExpert");
|
|
assertInOrder(create, [
|
|
"let added = await addOperatorCatalogStockByDoubleClick(",
|
|
"await verifyKrxThemeCreateMarketRoundTrip(",
|
|
"duplicate recheck after market round-trip",
|
|
"name available after market round-trip",
|
|
"stock re-add after market round-trip",
|
|
"let expectedSavedRows = 1"
|
|
]);
|
|
});
|
|
|
|
test("playlist Shift-click deletion is one physical Windows exchange and restores three rows", () => {
|
|
const setup = section(
|
|
harness,
|
|
"async function executeSafeSequence",
|
|
"async function executeDryRunPlayoutSequence");
|
|
assertInOrder(setup, [
|
|
'"stage first disposable Shift-delete row"',
|
|
"await sleep(legacyCutDoubleClickWindowMilliseconds + 100)",
|
|
'"stage second disposable Shift-delete row"'
|
|
]);
|
|
assert.match(harness, /const legacyCutDoubleClickWindowMilliseconds = 500;/u);
|
|
|
|
const request = section(
|
|
harness,
|
|
"async function requestWindowsInput(",
|
|
"async function installProbe");
|
|
assertInOrder(request, [
|
|
"beforeOrder.length !== 5",
|
|
"reorderedOrder.at(-2) !== rangeStartRowId",
|
|
"const afterOrder = reorderedOrder.filter",
|
|
'operation: "row-header-drag-home-shift-range-delete-restore"',
|
|
"expectedRangeSelectedRowIds: [rangeStartRowId, rangeEndRowId]",
|
|
"mouseDownCalls: 4",
|
|
"shiftKeyDownCalls: 1",
|
|
"deleteKeyCalls: 1",
|
|
'"delete-selected-playlist-rows"',
|
|
"selections[1].shift !== true",
|
|
"shiftRangeDeletedRowIds = [rangeStartRowId, rangeEndRowId]"
|
|
]);
|
|
assert.doesNotMatch(request, /Input\.dispatchMouseEvent/u);
|
|
|
|
const native = section(
|
|
runner,
|
|
"public static void SendShiftClickRangeThenDeleteAndRestore(",
|
|
"public static void SendApplicationClick(");
|
|
assertInOrder(native, [
|
|
"SendKeyboard(VirtualKeyShift, false)",
|
|
"SendMouseButton(MouseLeftDown)",
|
|
"SendKeyboard(VirtualKeyShift, true)",
|
|
"SendKeyboard(VirtualKeyDelete, false)",
|
|
"SendKeyboard(VirtualKeyDelete, true)",
|
|
"SendMouseMove(restore.X, restore.Y)"
|
|
]);
|
|
});
|
|
|
|
test("PList fresh readback uses the second physical double-click exchange", () => {
|
|
const refresh = section(
|
|
harness,
|
|
"function namedPlaylistCommandSequence",
|
|
"async function requestNamedPlaylistLoadDoubleClick");
|
|
assertInOrder(refresh, [
|
|
"Number.isSafeInteger(sequence)",
|
|
'result?.command === "refresh-named-playlists"',
|
|
"result.succeeded === true",
|
|
"result.sequence > baselineSequence",
|
|
'listFreshness === "fresh"'
|
|
]);
|
|
|
|
const request = section(
|
|
harness,
|
|
"async function requestNamedPlaylistLoadDoubleClick",
|
|
"function namedPlaylistDeleteWriteCount");
|
|
assertInOrder(request, [
|
|
"configuration.windowsInputExchangeCount !== 2",
|
|
'current.dom.namedTitle !== "DB 재생목록 불러오기"',
|
|
"must start on the freshly read, unselected saved row",
|
|
"const beforeContent = playlistContentProjection(current)",
|
|
'operation: "named-playlist-load-double-click"',
|
|
"mouseDownCalls: 2",
|
|
"doubleClickIntervalMilliseconds: 100",
|
|
'snapshot.state.commandResult?.command === "load-named-playlist-by-id"',
|
|
"JSON.stringify(playlistContentProjection(snapshot))",
|
|
'["select-named-playlist", "load-named-playlist-by-id"]',
|
|
"loads.length !== 1",
|
|
"selections.length !== 1",
|
|
"approvedDatabaseWriteMessages.length !== beforeApprovedWriteCount",
|
|
"startedUnselected: true",
|
|
"selectionIntentCount: selections.length",
|
|
"contentRestored: true"
|
|
]);
|
|
assert.doesNotMatch(request, /pointerDoubleClick|Input\.dispatchMouseEvent/u);
|
|
|
|
const crud = section(
|
|
harness,
|
|
"async function executeNamedPlaylistCrud",
|
|
"const manualFinancialScreens");
|
|
assertInOrder(crud, [
|
|
"PList reopen for fresh readback",
|
|
"completedNamedPlaylistRefreshAfter(snapshot, readbackRefreshBaseline)",
|
|
"matches[0].isSelected !== true",
|
|
"requestNamedPlaylistLoadDoubleClick(",
|
|
'"PList physical saved-row double-click load"',
|
|
'"PList physical double-click fresh readback"',
|
|
"PList reopen for delete",
|
|
"completedNamedPlaylistRefreshAfter(snapshot, deleteRefreshBaseline)"
|
|
]);
|
|
assert.doesNotMatch(crud, /PList select fresh readback|PList load fresh rows/u);
|
|
|
|
const native = section(
|
|
runner,
|
|
"public static void SendApplicationDoubleClick(",
|
|
"public static IntPtr ReadForegroundWindow(");
|
|
assert.match(native, /for \(int click = 0; click < 2; click\+\+\)/u);
|
|
assert.match(native, /SendMouseButton\(MouseLeftDown\)/u);
|
|
assert.match(native, /SendMouseButton\(MouseLeftUp\)/u);
|
|
assert.doesNotMatch(native, /Input\.dispatchMouseEvent/u);
|
|
});
|