fix: complete legacy operator parity workflows
This commit is contained in:
@@ -42,9 +42,11 @@ const readOnlyAllowedOutboundMessageTypes = Object.freeze([
|
||||
"hover-swap-tabs",
|
||||
"activate-playlist-row",
|
||||
"select-playlist-row",
|
||||
"move-selected-playlist-rows",
|
||||
"reorder-playlist-rows",
|
||||
"select-playlist-boundary",
|
||||
"delete-selected-playlist-rows",
|
||||
"set-playlist-enabled",
|
||||
"refresh-named-playlists"
|
||||
]);
|
||||
const fullUiDbAllowedOutboundMessageTypes = Object.freeze([
|
||||
@@ -209,7 +211,7 @@ function failUnknown(message) {
|
||||
}
|
||||
|
||||
function windowsInputExchangePath(basePath, exchangeIndex) {
|
||||
if (!Number.isSafeInteger(exchangeIndex) || exchangeIndex < 1 || exchangeIndex > 2) {
|
||||
if (!Number.isSafeInteger(exchangeIndex) || exchangeIndex < 1 || exchangeIndex > 3) {
|
||||
failKnown("The Windows input exchange index is outside the closed contract.");
|
||||
}
|
||||
if (exchangeIndex === 1) return basePath;
|
||||
@@ -220,6 +222,7 @@ function windowsInputExchangePath(basePath, exchangeIndex) {
|
||||
}
|
||||
|
||||
function expectedWindowsInputExchangeCount(profile, scope) {
|
||||
if (profile === "dry-run-playout") return 3;
|
||||
return profile === "full-ui-db" && new Set(["all", "plist"]).has(scope) ? 2 : 1;
|
||||
}
|
||||
|
||||
@@ -230,7 +233,7 @@ function parseArguments(argv) {
|
||||
"--output <absolute-result.json> --screenshot <absolute-screenshot.png> " +
|
||||
"--windows-input-request <absolute-request.json> " +
|
||||
"--windows-input-ack <absolute-ack.json> " +
|
||||
"[--profile read-only|dry-run-playout|full-ui-db] " +
|
||||
"[--profile read-only|dry-run-playout|full-ui-db|program-playlist-db] " +
|
||||
"[--scope all|plist|graphe|catalog|screens] " +
|
||||
"[--recover-named-playlist-title <CDX_P fixture>] " +
|
||||
"[--timeout-ms <milliseconds>]");
|
||||
@@ -324,8 +327,14 @@ function parseArguments(argv) {
|
||||
fs.mkdirSync(path.dirname(windowsInputRequestPath), { recursive: true });
|
||||
fs.mkdirSync(path.dirname(windowsInputAckPath), { recursive: true });
|
||||
const profile = values.get("profile") || "read-only";
|
||||
if (!new Set(["read-only", "dry-run-playout", "full-ui-db"]).has(profile)) {
|
||||
failKnown("--profile must be read-only, dry-run-playout, or full-ui-db.");
|
||||
if (!new Set([
|
||||
"read-only",
|
||||
"dry-run-playout",
|
||||
"full-ui-db",
|
||||
"program-playlist-db"
|
||||
]).has(profile)) {
|
||||
failKnown(
|
||||
"--profile must be read-only, dry-run-playout, full-ui-db, or program-playlist-db.");
|
||||
}
|
||||
const scope = values.get("scope") || "all";
|
||||
if (!new Set(["all", "plist", "graphe", "catalog", "screens"]).has(scope)) {
|
||||
@@ -410,6 +419,9 @@ function errorProjection(error) {
|
||||
const configuration = parseArguments(process.argv.slice(2));
|
||||
const fullUiDbProfile = configuration.profile === "full-ui-db";
|
||||
const dryRunPlayoutProfile = configuration.profile === "dry-run-playout";
|
||||
const programPlaylistDbProfile = configuration.profile === "program-playlist-db";
|
||||
const databaseWriteProfile = fullUiDbProfile || programPlaylistDbProfile;
|
||||
const activeDryRunProfile = dryRunPlayoutProfile || programPlaylistDbProfile;
|
||||
const forbiddenControlIds = fullUiDbProfile
|
||||
? Object.freeze([
|
||||
"playout-background-file",
|
||||
@@ -418,15 +430,36 @@ const forbiddenControlIds = fullUiDbProfile
|
||||
"playout-next",
|
||||
"playout-take-out"
|
||||
])
|
||||
: programPlaylistDbProfile
|
||||
? Object.freeze([
|
||||
"playout-background-file",
|
||||
"playout-background-none",
|
||||
"playout-prepare",
|
||||
"playout-next",
|
||||
"db-load-button"
|
||||
])
|
||||
: dryRunPlayoutProfile
|
||||
? Object.freeze(readOnlyForbiddenControlIds.filter(id =>
|
||||
!new Set(["playout-take-in", "playout-next", "playout-take-out"]).has(id)))
|
||||
: readOnlyForbiddenControlIds;
|
||||
const allowedOutboundMessageTypes = fullUiDbProfile
|
||||
? fullUiDbAllowedOutboundMessageTypes
|
||||
: programPlaylistDbProfile
|
||||
? Object.freeze([
|
||||
...readOnlyAllowedOutboundMessageTypes,
|
||||
"select-named-playlist",
|
||||
"create-named-playlist",
|
||||
"save-current-named-playlist-to",
|
||||
"delete-selected-named-playlist",
|
||||
"dismiss-dialog",
|
||||
"take-in",
|
||||
"take-out"
|
||||
])
|
||||
: dryRunPlayoutProfile
|
||||
? Object.freeze([
|
||||
...readOnlyAllowedOutboundMessageTypes,
|
||||
"choose-background",
|
||||
"toggle-background",
|
||||
"take-in",
|
||||
"next-playout",
|
||||
"take-out",
|
||||
@@ -468,7 +501,7 @@ const evidence = {
|
||||
query: "삼성",
|
||||
stockDisplayName: "삼성출판사",
|
||||
databaseStockDisplayName: "1Q 삼성전자선물단일종목레버리지",
|
||||
namedPlaylistTitle: fullUiDbProfile
|
||||
namedPlaylistTitle: databaseWriteProfile
|
||||
? `CDX_P_${Date.now()}_${randomBytes(3).toString("hex")}`
|
||||
: null,
|
||||
krxThemeTitle: fullUiDbProfile
|
||||
@@ -508,8 +541,22 @@ const evidence = {
|
||||
playlistCountAfterDoubleClick: null,
|
||||
tailAppendedEntryId: null,
|
||||
programEnableParity: null,
|
||||
backgroundShortcutParity: null,
|
||||
shortcutParity: null
|
||||
} : null,
|
||||
programPlaylistDb: programPlaylistDbProfile ? {
|
||||
result: "PENDING",
|
||||
title: null,
|
||||
definitionId: null,
|
||||
currentEntryId: null,
|
||||
rowCount: null,
|
||||
createWriteCount: 0,
|
||||
saveWriteCount: 0,
|
||||
deleteWriteCount: 0,
|
||||
freshDefinitionReadbackVerified: false,
|
||||
exactAbsenceVerified: false,
|
||||
finalPhase: null
|
||||
} : null,
|
||||
checkpoints: [],
|
||||
inputs: [],
|
||||
warnings: [],
|
||||
@@ -935,6 +982,9 @@ function probeSnapshotExpression() {
|
||||
nextKind: state.playout.nextKind,
|
||||
isBusy: state.playout.isBusy,
|
||||
refreshActive: state.playout.refreshActive,
|
||||
backgroundEnabled: state.playout.backgroundEnabled,
|
||||
backgroundFileName: state.playout.backgroundFileName,
|
||||
canChangeBackground: state.playout.canChangeBackground,
|
||||
message: state.playout.message
|
||||
} : null
|
||||
} : null,
|
||||
@@ -1030,6 +1080,14 @@ function probeSnapshotExpression() {
|
||||
document.getElementById("playlist-enable-all")?.indeterminate === true,
|
||||
disabled: document.getElementById("playlist-enable-all")?.disabled === true
|
||||
},
|
||||
backgroundFileDisabled:
|
||||
document.getElementById("playout-background-file")?.disabled === true,
|
||||
backgroundNoneChecked:
|
||||
document.getElementById("playout-background-none")?.checked === true,
|
||||
backgroundNoneDisabled:
|
||||
document.getElementById("playout-background-none")?.disabled === true,
|
||||
backgroundNameValue:
|
||||
document.getElementById("playout-background-name")?.value ?? null,
|
||||
operatorCatalogStockQueryValue:
|
||||
document.getElementById("operator-catalog-stock-query")?.value ?? null,
|
||||
operatorCatalogNameValue:
|
||||
@@ -2094,8 +2152,8 @@ async function setSearchText(text) {
|
||||
await rpc("Input.insertText", { text });
|
||||
}
|
||||
|
||||
async function replaceText(expression, text, label) {
|
||||
await pointerClick(expression, `focus ${label}`);
|
||||
async function replaceText(expression, text, label, options = {}) {
|
||||
await pointerClick(expression, `focus ${label}`, 0, options);
|
||||
const focused = await evaluate(`document.activeElement === (${expression})`);
|
||||
if (focused !== true) failKnown(`${label} did not receive pointer focus.`);
|
||||
await pressKey("SelectAll", `select existing ${label}`, ctrlModifier);
|
||||
@@ -2128,14 +2186,15 @@ async function selectOptionValue(expression, value, label) {
|
||||
if (actual !== value) failKnown(`${label} did not select the requested value.`);
|
||||
}
|
||||
|
||||
async function respondToNativeDialog(label, confirmation, accept) {
|
||||
async function respondToNativeDialog(label, confirmation, accept, options = {}) {
|
||||
const opened = await waitFor(`${label} dialog`, snapshot =>
|
||||
snapshot.state?.dialog?.isConfirmation === confirmation &&
|
||||
snapshot.dom.legacyDialogHidden === false,
|
||||
{
|
||||
timeoutMilliseconds: 30_000,
|
||||
allowUiBusy: true,
|
||||
allowLegacyDialog: true
|
||||
allowLegacyDialog: true,
|
||||
allowActiveDryRun: options.allowActiveDryRun === true
|
||||
});
|
||||
if (confirmation && accept !== true && accept !== false) {
|
||||
failKnown(`${label} confirmation response is not explicit.`);
|
||||
@@ -2146,13 +2205,17 @@ async function respondToNativeDialog(label, confirmation, accept) {
|
||||
: 'document.getElementById("legacy-dialog-ok")',
|
||||
`${label} ${confirmation ? (accept ? "yes" : "no") : "ok"}`,
|
||||
0,
|
||||
{ allowLegacyDialog: true });
|
||||
{
|
||||
allowLegacyDialog: true,
|
||||
allowActiveDryRun: options.allowActiveDryRun === true
|
||||
});
|
||||
await waitFor(`${label} dialog close`, snapshot =>
|
||||
snapshot.state?.dialog == null && snapshot.dom.legacyDialogHidden === true,
|
||||
{
|
||||
timeoutMilliseconds: 60_000,
|
||||
allowUiBusy: true,
|
||||
allowLegacyDialog: true
|
||||
allowLegacyDialog: true,
|
||||
allowActiveDryRun: options.allowActiveDryRun === true
|
||||
});
|
||||
return opened;
|
||||
}
|
||||
@@ -2162,9 +2225,14 @@ async function pointerClickWithNativeDialog(
|
||||
label,
|
||||
confirmation,
|
||||
message,
|
||||
accept) {
|
||||
await pointerClick(expression, label);
|
||||
const opened = await respondToNativeDialog(label, confirmation, accept);
|
||||
accept,
|
||||
options = {}) {
|
||||
await pointerClick(expression, label, 0, options);
|
||||
const opened = await respondToNativeDialog(
|
||||
label,
|
||||
confirmation,
|
||||
accept,
|
||||
options);
|
||||
if (opened.state?.dialog?.message !== message ||
|
||||
opened.dom.legacyDialogText !== message) {
|
||||
failKnown(`${label} native dialog text did not match the original.`);
|
||||
@@ -2318,9 +2386,15 @@ function cssDragSize(snapshot) {
|
||||
};
|
||||
}
|
||||
|
||||
async function dragCutsToPlaylist(physicalIndex, modifiers, expectedAppendCount, label) {
|
||||
async function dragCutsToPlaylist(
|
||||
physicalIndex,
|
||||
modifiers,
|
||||
expectedAppendCount,
|
||||
label,
|
||||
options = {}) {
|
||||
const before = await readSnapshot();
|
||||
assertSafety(before, `${label} preflight`, false);
|
||||
const allowActiveDryRun = options.allowActiveDryRun === true;
|
||||
assertSafety(before, `${label} preflight`, false, false, allowActiveDryRun);
|
||||
const beforeCount = before.state.playlist.length;
|
||||
const beforeRevision = before.state.revision;
|
||||
const sourceExpression =
|
||||
@@ -2345,7 +2419,7 @@ async function dragCutsToPlaylist(physicalIndex, modifiers, expectedAppendCount,
|
||||
const held = await waitFor(`${label} pointer-down receipt`, snapshot =>
|
||||
snapshot.state.revision > beforeRevision &&
|
||||
focusedCutIndex(snapshot) === physicalIndex,
|
||||
{ allowUiBusy: false });
|
||||
{ allowUiBusy: false, allowActiveDryRun });
|
||||
const dragSize = cssDragSize(held);
|
||||
const inside = {
|
||||
x: source.x - dragSize.width / 2,
|
||||
@@ -2450,7 +2524,7 @@ async function dragCutsToPlaylist(physicalIndex, modifiers, expectedAppendCount,
|
||||
const completed = await waitFor(`${label} append`, snapshot =>
|
||||
snapshot.state.isBusy !== true &&
|
||||
snapshot.state.playlist.length === beforeCount + expectedAppendCount,
|
||||
{ allowUiBusy: false });
|
||||
{ allowUiBusy: false, allowActiveDryRun });
|
||||
if (!completed.dom.cuts.every(row => !row.dragging) ||
|
||||
completed.dom.cutDropHighlighted ||
|
||||
completed.dom.cutDragFeedback?.hidden !== true) {
|
||||
@@ -2887,6 +2961,108 @@ async function requestWindowsInput(
|
||||
return completed;
|
||||
}
|
||||
|
||||
async function requestPhysicalBackgroundShortcuts() {
|
||||
if (!dryRunPlayoutProfile) return;
|
||||
|
||||
let current = await readSnapshot();
|
||||
assertSafety(current, "physical F2/F3 preflight", false);
|
||||
if (current.state.playout.backgroundEnabled !== false ||
|
||||
current.state.playout.canChangeBackground !== true ||
|
||||
current.dom.backgroundFileDisabled !== false ||
|
||||
current.dom.backgroundNoneChecked !== true ||
|
||||
current.dom.backgroundNoneDisabled !== false ||
|
||||
current.dom.backgroundNameValue !== "배경없음") {
|
||||
failKnown("The physical F2/F3 preflight is not exact IDLE background-none state.");
|
||||
}
|
||||
|
||||
const f2OutboundSequence = current.safety.outboundMessages.length;
|
||||
const f2Token = randomBytes(16).toString("hex").toUpperCase();
|
||||
await publishWindowsInputRequest({
|
||||
schemaVersion: 1,
|
||||
exchangeIndex: 2,
|
||||
token: f2Token,
|
||||
targetUrl: exactTargetUrl,
|
||||
operation: "background-f2-picker-cancel",
|
||||
f2KeyCalls: 1,
|
||||
escapeKeyCalls: 1,
|
||||
inputRetryCount: 0
|
||||
}, "Windows SendInput F2 picker and one Escape cancellation", {
|
||||
f2KeyCalls: 1,
|
||||
escapeKeyCalls: 1,
|
||||
pickerUniqueNew: true,
|
||||
pickerOwnerLinked: true,
|
||||
pickerSameProcess: true,
|
||||
pickerClass: "#32770",
|
||||
pickerTitle: "다음 장면 배경 선택",
|
||||
pickerModal: true,
|
||||
pickerClosedAfterEscape: true
|
||||
});
|
||||
current = await waitFor("physical F2 picker cancellation receipt", snapshot =>
|
||||
snapshot.state.isBusy !== true &&
|
||||
snapshot.state.playout.phase === "idle" &&
|
||||
snapshot.state.playout.backgroundEnabled === false &&
|
||||
snapshot.state.playout.canChangeBackground === true &&
|
||||
snapshot.dom.backgroundNoneChecked === true &&
|
||||
snapshot.dom.backgroundNameValue === "배경없음" &&
|
||||
outboundMessagesAfter(snapshot, f2OutboundSequence).length === 1,
|
||||
{ timeoutMilliseconds: 15_000, allowUiBusy: false });
|
||||
assertExactOutboundTypes(
|
||||
current,
|
||||
f2OutboundSequence,
|
||||
["choose-background"],
|
||||
"physical F2 picker cancellation");
|
||||
|
||||
const f3OutboundSequence = current.safety.outboundMessages.length;
|
||||
const f3Revision = current.state.revision;
|
||||
const f3Token = randomBytes(16).toString("hex").toUpperCase();
|
||||
await publishWindowsInputRequest({
|
||||
schemaVersion: 1,
|
||||
exchangeIndex: 3,
|
||||
token: f3Token,
|
||||
targetUrl: exactTargetUrl,
|
||||
operation: "background-f3-none",
|
||||
f3KeyCalls: 1,
|
||||
inputRetryCount: 0
|
||||
}, "Windows SendInput F3 fail-closed background-none", {
|
||||
f3KeyCalls: 1
|
||||
});
|
||||
current = await waitFor("physical F3 background-none receipt", snapshot =>
|
||||
snapshot.state.revision > f3Revision &&
|
||||
snapshot.state.isBusy !== true &&
|
||||
snapshot.state.statusKind === "error" &&
|
||||
snapshot.state.playout.phase === "idle" &&
|
||||
snapshot.state.playout.backgroundEnabled === false &&
|
||||
snapshot.state.playout.backgroundFileName === "" &&
|
||||
snapshot.state.playout.canChangeBackground === true &&
|
||||
snapshot.dom.backgroundNoneChecked === true &&
|
||||
snapshot.dom.backgroundNoneDisabled === false &&
|
||||
snapshot.dom.backgroundNameValue === "배경없음" &&
|
||||
outboundMessagesAfter(snapshot, f3OutboundSequence).length === 1,
|
||||
{ timeoutMilliseconds: 15_000, allowUiBusy: false });
|
||||
assertExactOutboundTypes(
|
||||
current,
|
||||
f3OutboundSequence,
|
||||
["toggle-background"],
|
||||
"physical F3 background-none fail-closed result");
|
||||
|
||||
evidence.dryRunPlayout.backgroundShortcutParity = {
|
||||
f2IntentCount: 1,
|
||||
f2KeyCalls: 1,
|
||||
escapeKeyCalls: 1,
|
||||
pickerOwnedAndCancelled: true,
|
||||
f3IntentCount: 1,
|
||||
f3KeyCalls: 1,
|
||||
defaultAssetAbsentFailClosed: true,
|
||||
finalBackgroundEnabled: false,
|
||||
finalBackgroundNoneChecked: true,
|
||||
finalPhase: current.state.playout.phase
|
||||
};
|
||||
await checkpoint("windows-sendinput-f2-cancel-f3-background-none", {
|
||||
defaultAssetAbsentFailClosed: true,
|
||||
finalBackgroundEnabled: false
|
||||
});
|
||||
}
|
||||
|
||||
async function installProbe() {
|
||||
const token = `legacy-package-input-${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
||||
const installed = await evaluate(`(() => {
|
||||
@@ -2920,7 +3096,7 @@ async function installProbe() {
|
||||
blockedOutboundMessages: [],
|
||||
stateViolations: []
|
||||
};
|
||||
const allowActiveDryRun = ${JSON.stringify(dryRunPlayoutProfile)};
|
||||
const allowActiveDryRun = ${JSON.stringify(activeDryRunProfile)};
|
||||
probe.listener = event => {
|
||||
if (event.data?.type !== "state" || !event.data.payload) return;
|
||||
const payload = event.data.payload;
|
||||
@@ -3013,8 +3189,15 @@ async function installProbe() {
|
||||
: null,
|
||||
definitionId: new Set([
|
||||
"select-named-playlist",
|
||||
"load-named-playlist-by-id"
|
||||
"load-named-playlist-by-id",
|
||||
"save-current-named-playlist-to"
|
||||
]).has(type) ? message?.payload?.definitionId ?? null : null,
|
||||
title: type === "create-named-playlist"
|
||||
? message?.payload?.title ?? null
|
||||
: null,
|
||||
requestId: typeof message?.payload?.requestId === "string"
|
||||
? message.payload.requestId
|
||||
: null,
|
||||
emptyStockQuery: type === "search-operator-catalog-stocks"
|
||||
? message?.payload?.query === ""
|
||||
: null,
|
||||
@@ -3300,6 +3483,79 @@ async function executeSafeSequence() {
|
||||
["1", "2", "3"],
|
||||
"reordered playlist row-header ordinals");
|
||||
|
||||
await pointerClick(
|
||||
'document.getElementById("playlist-move-up")',
|
||||
"move selected playlist row with Up button");
|
||||
current = await waitFor("playlist Up button reorder", snapshot =>
|
||||
JSON.stringify(snapshot.state.playlist.map(row => row.rowId)) ===
|
||||
JSON.stringify([rowB, rowA, rowC]) &&
|
||||
activePlaylistId(snapshot) === rowA &&
|
||||
JSON.stringify(selectedPlaylistIds(snapshot)) === JSON.stringify([rowA]),
|
||||
{ allowUiBusy: false });
|
||||
await pointerClick(
|
||||
'document.getElementById("playlist-move-down")',
|
||||
"restore selected playlist row with Down button");
|
||||
current = await waitFor("playlist Down button reorder", snapshot =>
|
||||
JSON.stringify(snapshot.state.playlist.map(row => row.rowId)) ===
|
||||
JSON.stringify([rowB, rowC, rowA]) &&
|
||||
activePlaylistId(snapshot) === rowA &&
|
||||
JSON.stringify(selectedPlaylistIds(snapshot)) === JSON.stringify([rowA]),
|
||||
{ allowUiBusy: false });
|
||||
await checkpoint("playlist-move-buttons", {
|
||||
restoredOrder: [rowB, rowC, rowA]
|
||||
});
|
||||
|
||||
const rowBCheckboxExpression =
|
||||
`document.querySelector('.playlist-data-row[data-row-id="${rowB}"] input[type="checkbox"]')`;
|
||||
await pointerClick(rowBCheckboxExpression, "disable playlist row with its checkbox");
|
||||
current = await waitFor("individual playlist checkbox disabled row", snapshot =>
|
||||
activePlaylistId(snapshot) === rowB &&
|
||||
snapshot.state.playlist.some(row => row.rowId === rowB && row.isEnabled === false),
|
||||
{ allowUiBusy: false });
|
||||
await pointerClick(rowBCheckboxExpression, "restore playlist row with its checkbox");
|
||||
current = await waitFor("individual playlist checkbox restored row", snapshot =>
|
||||
activePlaylistId(snapshot) === rowB &&
|
||||
snapshot.state.playlist.some(row => row.rowId === rowB && row.isEnabled === true),
|
||||
{ allowUiBusy: false });
|
||||
await checkpoint("playlist-individual-checkbox", { rowId: rowB });
|
||||
|
||||
await sleep(legacyCutDoubleClickWindowMilliseconds + 100);
|
||||
current = await dragCutsToPlaylist(
|
||||
1,
|
||||
0,
|
||||
1,
|
||||
"stage direct selected-delete button row");
|
||||
if (current.state.playlist.length !== 4) {
|
||||
failKnown("The direct selected-delete button fixture did not append exactly one row.");
|
||||
}
|
||||
const directDeleteRowId = current.state.playlist.at(-1)?.rowId;
|
||||
if (!directDeleteRowId || directDeleteRowId === rowA ||
|
||||
directDeleteRowId === rowB || directDeleteRowId === rowC) {
|
||||
failKnown("The direct selected-delete button fixture has no unique tail row.");
|
||||
}
|
||||
await pointerClickPlaylistRow(directDeleteRowId, "select direct button delete fixture");
|
||||
current = await waitFor("direct button delete fixture selection", snapshot =>
|
||||
activePlaylistId(snapshot) === directDeleteRowId &&
|
||||
JSON.stringify(selectedPlaylistIds(snapshot)) === JSON.stringify([directDeleteRowId]),
|
||||
{ allowUiBusy: false });
|
||||
await pointerClick(
|
||||
'document.getElementById("playlist-delete-selected")',
|
||||
"delete selected playlist row with button");
|
||||
current = await waitFor("selected-delete button removed exact fixture", snapshot =>
|
||||
JSON.stringify(snapshot.state.playlist.map(row => row.rowId)) ===
|
||||
JSON.stringify([rowB, rowC, rowA]) &&
|
||||
!snapshot.state.playlist.some(row => row.rowId === directDeleteRowId),
|
||||
{ allowUiBusy: false });
|
||||
await pointerClickPlaylistRow(rowA, "restore row A selection after button delete");
|
||||
current = await waitFor("row A selection restored after button delete", snapshot =>
|
||||
activePlaylistId(snapshot) === rowA &&
|
||||
JSON.stringify(selectedPlaylistIds(snapshot)) === JSON.stringify([rowA]),
|
||||
{ allowUiBusy: false });
|
||||
await checkpoint("playlist-selected-delete-button", {
|
||||
deletedRowId: directDeleteRowId,
|
||||
restoredOrder: [rowB, rowC, rowA]
|
||||
});
|
||||
|
||||
const orderBeforeCheckboxAttempt = current.state.playlist.map(row => row.rowId);
|
||||
const enabledBeforeCheckboxAttempt = current.state.playlist.map(row => ({
|
||||
rowId: row.rowId,
|
||||
@@ -3347,6 +3603,7 @@ async function executeSafeSequence() {
|
||||
}
|
||||
await checkpoint("playlist-keyboard-focus-split");
|
||||
|
||||
await sleep(legacyCutDoubleClickWindowMilliseconds + 100);
|
||||
current = await dragCutsToPlaylist(
|
||||
1,
|
||||
0,
|
||||
@@ -3379,9 +3636,10 @@ async function executeSafeSequence() {
|
||||
rowE,
|
||||
"Windows SendInput drag, Home, Shift range, Delete, and restore");
|
||||
|
||||
await pointerClick(
|
||||
'document.getElementById("db-load-button")',
|
||||
"open DB playlist load modal");
|
||||
if (!programPlaylistDbProfile) {
|
||||
await pointerClick(
|
||||
'document.getElementById("db-load-button")',
|
||||
"open DB playlist load modal");
|
||||
current = await waitFor("read-only named playlist load modal", snapshot =>
|
||||
snapshot.state.isBusy === false && snapshot.dom.namedModalHidden === false &&
|
||||
snapshot.dom.namedConfirmationHidden === true &&
|
||||
@@ -3494,7 +3752,8 @@ async function executeSafeSequence() {
|
||||
failKnown("Declining the save confirmation changed named-playlist mutation state.");
|
||||
}
|
||||
}
|
||||
await checkpoint("named-playlist-save-confirmation-no-write");
|
||||
await checkpoint("named-playlist-save-confirmation-no-write");
|
||||
}
|
||||
|
||||
current = await checkpoint("final", {
|
||||
expectedPlaylistOrder: [rowC, rowA, rowB]
|
||||
@@ -3513,6 +3772,8 @@ async function executeSafeSequence() {
|
||||
async function executeDryRunPlayoutSequence() {
|
||||
if (!dryRunPlayoutProfile) return;
|
||||
|
||||
await requestPhysicalBackgroundShortcuts();
|
||||
|
||||
let current = await readSnapshot();
|
||||
assertSafety(current, "DryRun playout sequence preflight", false);
|
||||
if (current.state.playlist.length < 3 ||
|
||||
@@ -3628,6 +3889,78 @@ async function executeDryRunPlayoutSequence() {
|
||||
{ allowUiBusy: false, allowActiveDryRun: true });
|
||||
evidence.dryRunPlayout.programCutSelection = selectableCut.physicalIndex;
|
||||
|
||||
const dragCut = current.state.cutRows.find(row =>
|
||||
row.isSeparator !== true && row.physicalIndex !== selectableCut.physicalIndex);
|
||||
if (!dragCut) {
|
||||
failKnown("No distinct cut exists for PROGRAM drag tail-append validation.");
|
||||
}
|
||||
const programDragIntentBaseline = current.safety.outboundMessageCount;
|
||||
current = await dragCutsToPlaylist(
|
||||
dragCut.physicalIndex,
|
||||
0,
|
||||
1,
|
||||
"PROGRAM captured-pointer safe-tail drag",
|
||||
{ allowActiveDryRun: true });
|
||||
const programDragAppendedEntryId = current.state.playlist.at(-1)?.rowId;
|
||||
if (!programDragAppendedEntryId ||
|
||||
current.state.playout.phase !== "program" ||
|
||||
current.state.playout.currentEntryId !== selectedEntryId ||
|
||||
current.state.playlist.length !== playlistCount + 1) {
|
||||
failKnown("PROGRAM drag did not append one unique row after the on-air prefix.");
|
||||
}
|
||||
assertExactOutboundTypes(
|
||||
current,
|
||||
programDragIntentBaseline,
|
||||
["cut-pointer-down", "drop-selected-cuts"],
|
||||
"PROGRAM captured-pointer tail append");
|
||||
|
||||
await pointerClickPlaylistRow(
|
||||
programDragAppendedEntryId,
|
||||
"select PROGRAM drag-appended future tail");
|
||||
current = await waitFor("PROGRAM drag-appended tail selection", snapshot =>
|
||||
snapshot.state.playout.phase === "program" &&
|
||||
activePlaylistId(snapshot) === programDragAppendedEntryId &&
|
||||
JSON.stringify(selectedPlaylistIds(snapshot)) ===
|
||||
JSON.stringify([programDragAppendedEntryId]),
|
||||
{ allowUiBusy: false, allowActiveDryRun: true });
|
||||
const programDragDeleteBaseline = current.safety.outboundMessageCount;
|
||||
await pointerClick(
|
||||
'document.getElementById("playlist-delete-selected")',
|
||||
"delete PROGRAM drag-appended future tail",
|
||||
0,
|
||||
{ allowActiveDryRun: true });
|
||||
current = await waitFor("PROGRAM drag-appended tail cleanup", snapshot =>
|
||||
snapshot.state.playout.phase === "program" &&
|
||||
snapshot.state.playout.currentEntryId === selectedEntryId &&
|
||||
snapshot.state.playlist.length === playlistCount &&
|
||||
!snapshot.state.playlist.some(row => row.rowId === programDragAppendedEntryId),
|
||||
{ allowUiBusy: false, allowActiveDryRun: true });
|
||||
assertExactOutboundTypes(
|
||||
current,
|
||||
programDragDeleteBaseline,
|
||||
["delete-selected-playlist-rows"],
|
||||
"PROGRAM drag-appended tail cleanup");
|
||||
evidence.dryRunPlayout.programDragParity = {
|
||||
cutPhysicalIndex: dragCut.physicalIndex,
|
||||
appendedEntryId: programDragAppendedEntryId,
|
||||
appendIntentCount: 1,
|
||||
cleanupDeleteIntentCount: 1,
|
||||
onAirEntryPreserved: true
|
||||
};
|
||||
|
||||
await sleep(legacyCutDoubleClickWindowMilliseconds + 100);
|
||||
await pointerClick(
|
||||
cutExpression,
|
||||
"restore alternate cut before PROGRAM double-click",
|
||||
0,
|
||||
{ allowActiveDryRun: true });
|
||||
current = await waitFor("PROGRAM alternate cut restored", snapshot =>
|
||||
snapshot.state.playout.phase === "program" &&
|
||||
snapshot.state.cutRows.some(row =>
|
||||
row.physicalIndex === selectableCut.physicalIndex &&
|
||||
row.isSelected === true && row.isFocused === true),
|
||||
{ allowUiBusy: false, allowActiveDryRun: true });
|
||||
|
||||
await pointerClick(
|
||||
cutExpression,
|
||||
"PROGRAM safe-tail double-click first press",
|
||||
@@ -4023,6 +4356,40 @@ function namedPlaylistDeleteWriteCount(snapshot) {
|
||||
.filter(message => message.type === "delete-selected-named-playlist").length;
|
||||
}
|
||||
|
||||
async function waitForNamedPlaylistDeleteDispatch(
|
||||
label,
|
||||
baselineWriteCount,
|
||||
options = {}) {
|
||||
const deadline = Date.now() + 5_000;
|
||||
let lastSnapshot = null;
|
||||
while (Date.now() < deadline) {
|
||||
lastSnapshot = await readSnapshot();
|
||||
assertSafety(
|
||||
lastSnapshot,
|
||||
label,
|
||||
true,
|
||||
false,
|
||||
options.allowActiveDryRun === true);
|
||||
if (options.recovery === true) {
|
||||
assertNamedPlaylistRecoverySafe(
|
||||
lastSnapshot,
|
||||
label,
|
||||
baselineWriteCount,
|
||||
options.baselineCommandSequence ?? null);
|
||||
}
|
||||
|
||||
const writeCount = namedPlaylistDeleteWriteCount(lastSnapshot) - baselineWriteCount;
|
||||
if (writeCount > 1 || writeCount < 0) {
|
||||
failUnknown(`${label} emitted an invalid delete-intent count; no action is retried.`);
|
||||
}
|
||||
if (writeCount === 1) return lastSnapshot;
|
||||
await sleep(50);
|
||||
}
|
||||
|
||||
failKnown(`${label} produced no delete intent, so no database delete was started. ` +
|
||||
`Last snapshot: ${JSON.stringify(lastSnapshot)}`);
|
||||
}
|
||||
|
||||
function assertNamedPlaylistRecoverySafe(
|
||||
snapshot,
|
||||
label,
|
||||
@@ -4159,6 +4526,13 @@ async function recoverNamedPlaylistFixture() {
|
||||
await pointerClick(
|
||||
'document.getElementById("named-playlist-confirmation-yes")',
|
||||
"PList recovery confirm exact delete once");
|
||||
await waitForNamedPlaylistDeleteDispatch(
|
||||
"PList recovery physical delete confirmation",
|
||||
beforeDeleteWriteCount,
|
||||
{
|
||||
recovery: true,
|
||||
baselineCommandSequence: beforeDeleteCommandSequence
|
||||
});
|
||||
current = await waitForNamedPlaylistRecovery(
|
||||
"PList recovery committed exact absence",
|
||||
snapshot => {
|
||||
@@ -4322,8 +4696,12 @@ async function executeNamedPlaylistCrud() {
|
||||
await waitFor("PList delete confirmation", snapshot =>
|
||||
snapshot.dom.namedConfirmationHidden === false && snapshot.state.isBusy === false,
|
||||
{ allowUiBusy: false });
|
||||
const beforeDeleteWriteCount = namedPlaylistDeleteWriteCount(await readSnapshot());
|
||||
await pointerClick('document.getElementById("named-playlist-confirmation-yes")',
|
||||
"PList confirm delete");
|
||||
await waitForNamedPlaylistDeleteDispatch(
|
||||
"PList physical delete confirmation",
|
||||
beforeDeleteWriteCount);
|
||||
current = await waitFor("PList delete and absence", snapshot =>
|
||||
snapshot.state.isBusy === false &&
|
||||
!snapshot.state.namedPlaylist.definitions.some(row => row.title === title) &&
|
||||
@@ -4338,6 +4716,271 @@ async function executeNamedPlaylistCrud() {
|
||||
recordFullUiDbCheck("databaseChecks", "PList delete/absence");
|
||||
}
|
||||
|
||||
async function executeProgramNamedPlaylistCrud() {
|
||||
if (!programPlaylistDbProfile) return;
|
||||
|
||||
const activeOptions = { allowActiveDryRun: true };
|
||||
const title = evidence.fixture.namedPlaylistTitle;
|
||||
let current = await readSnapshot();
|
||||
assertSafety(current, "PROGRAM named-playlist preflight", false);
|
||||
if (typeof title !== "string" || title.length !== 26 ||
|
||||
!/^CDX_P_[0-9]{13}_[0-9a-f]{6}$/.test(title) ||
|
||||
current.state.playlist.length !== 3 ||
|
||||
current.state.namedPlaylist.definitions.some(row => row.title === title)) {
|
||||
failKnown("PROGRAM named-playlist preflight is not the exact three-row unique fixture.");
|
||||
}
|
||||
const originalSelectedIds = selectedPlaylistIds(current);
|
||||
const originalActiveId = activePlaylistId(current);
|
||||
if (originalSelectedIds.length !== 1 || !originalActiveId) {
|
||||
failKnown("PROGRAM named-playlist preflight lacks the exact selection split.");
|
||||
}
|
||||
|
||||
const currentEntryId = current.state.playlist[0].rowId;
|
||||
await pointerClickPlaylistRow(
|
||||
currentEntryId,
|
||||
"select PROGRAM named-playlist current row");
|
||||
current = await waitFor("PROGRAM named-playlist current-row selection", snapshot =>
|
||||
snapshot.state.playout.phase === "idle" &&
|
||||
activePlaylistId(snapshot) === currentEntryId,
|
||||
{ allowUiBusy: false });
|
||||
const baselineWriteCount = current.safety.approvedDatabaseWriteMessages.length;
|
||||
await pointerClick(
|
||||
'document.getElementById("playout-take-in")',
|
||||
"enter DryRun PROGRAM for named-playlist persistence");
|
||||
current = await waitFor("DryRun PROGRAM named-playlist entry", snapshot =>
|
||||
snapshot.state.playout.phase === "program" &&
|
||||
snapshot.state.playout.currentEntryId === currentEntryId &&
|
||||
snapshot.state.playout.isBusy !== true && snapshot.state.isBusy !== true,
|
||||
{ timeoutMilliseconds: 60_000, allowUiBusy: true, allowActiveDryRun: true });
|
||||
|
||||
const saveRefreshBaseline = namedPlaylistCommandSequence(current);
|
||||
await pointerClick(
|
||||
'document.getElementById("db-save-button")',
|
||||
"PROGRAM open named-playlist save dialog",
|
||||
0,
|
||||
activeOptions);
|
||||
current = await waitFor("PROGRAM fresh named-playlist definition read", snapshot =>
|
||||
snapshot.state.playout.phase === "program" &&
|
||||
snapshot.state.playout.currentEntryId === currentEntryId &&
|
||||
snapshot.dom.namedModalHidden === false &&
|
||||
completedNamedPlaylistRefreshAfter(snapshot, saveRefreshBaseline),
|
||||
{
|
||||
timeoutMilliseconds: 60_000,
|
||||
allowUiBusy: true,
|
||||
allowActiveDryRun: true
|
||||
});
|
||||
if (current.state.namedPlaylist.definitions.some(row => row.title === title)) {
|
||||
failKnown("PROGRAM generated named-playlist fixture already exists after fresh read.");
|
||||
}
|
||||
|
||||
await replaceText(
|
||||
'document.getElementById("named-playlist-new-title")',
|
||||
title,
|
||||
"PROGRAM named-playlist unique title",
|
||||
activeOptions);
|
||||
await pointerClick(
|
||||
'document.getElementById("named-playlist-create-button")',
|
||||
"PROGRAM create named-playlist definition",
|
||||
0,
|
||||
activeOptions);
|
||||
current = await waitFor("PROGRAM named-playlist create commit", snapshot =>
|
||||
snapshot.state.playout.phase === "program" &&
|
||||
snapshot.state.playout.currentEntryId === currentEntryId &&
|
||||
snapshot.state.namedPlaylist.definitions.filter(row => row.title === title).length === 1 &&
|
||||
snapshot.state.namedPlaylist.lastMutationOutcome?.startsWith("committed") === true,
|
||||
{
|
||||
timeoutMilliseconds: 60_000,
|
||||
allowUiBusy: true,
|
||||
allowActiveDryRun: true
|
||||
});
|
||||
const created = current.state.namedPlaylist.definitions.find(row => row.title === title);
|
||||
if (!created) failUnknown("PROGRAM named-playlist create returned no exact definition.");
|
||||
if (current.state.namedPlaylist.selectedDefinitionId !== created.definitionId) {
|
||||
await pointerClick(
|
||||
namedDefinitionExpression(title),
|
||||
"PROGRAM select created named-playlist definition",
|
||||
0,
|
||||
activeOptions);
|
||||
current = await waitFor("PROGRAM created definition selection", snapshot =>
|
||||
snapshot.state.playout.phase === "program" &&
|
||||
snapshot.state.namedPlaylist.selectedDefinitionId === created.definitionId,
|
||||
{ allowUiBusy: false, allowActiveDryRun: true });
|
||||
}
|
||||
|
||||
await pointerClick(
|
||||
'document.getElementById("named-playlist-confirm-button")',
|
||||
"PROGRAM request current-row save",
|
||||
0,
|
||||
activeOptions);
|
||||
await waitFor("PROGRAM named-playlist save confirmation", snapshot =>
|
||||
snapshot.state.playout.phase === "program" &&
|
||||
snapshot.dom.namedConfirmationHidden === false,
|
||||
{ allowUiBusy: false, allowActiveDryRun: true });
|
||||
await pointerClickWithNativeDialog(
|
||||
'document.getElementById("named-playlist-confirmation-yes")',
|
||||
"PROGRAM confirm named-playlist row save",
|
||||
false,
|
||||
"저장되었습니다",
|
||||
true,
|
||||
activeOptions);
|
||||
current = await waitFor("PROGRAM named-playlist save commit", snapshot =>
|
||||
snapshot.state.playout.phase === "program" &&
|
||||
snapshot.state.playout.currentEntryId === currentEntryId &&
|
||||
snapshot.dom.namedModalHidden === true &&
|
||||
snapshot.state.namedPlaylist.selectedTitle === title &&
|
||||
snapshot.state.namedPlaylist.rowCount === 3 &&
|
||||
snapshot.state.namedPlaylist.documentFreshness === "fresh" &&
|
||||
snapshot.state.namedPlaylist.lastMutationOutcome?.startsWith("committed") === true,
|
||||
{
|
||||
timeoutMilliseconds: 60_000,
|
||||
allowUiBusy: true,
|
||||
allowActiveDryRun: true
|
||||
});
|
||||
const savedRowCount = current.state.namedPlaylist.rowCount;
|
||||
|
||||
const readbackRefreshBaseline = namedPlaylistCommandSequence(current);
|
||||
await pointerClick(
|
||||
'document.getElementById("db-save-button")',
|
||||
"PROGRAM reopen save dialog for fresh readback",
|
||||
0,
|
||||
activeOptions);
|
||||
current = await waitFor("PROGRAM named-playlist fresh readback", snapshot => {
|
||||
const matches = snapshot.state.namedPlaylist?.definitions
|
||||
?.filter(row => row.title === title) || [];
|
||||
return snapshot.state.playout.phase === "program" &&
|
||||
snapshot.state.playout.currentEntryId === currentEntryId &&
|
||||
snapshot.dom.namedModalHidden === false &&
|
||||
completedNamedPlaylistRefreshAfter(snapshot, readbackRefreshBaseline) &&
|
||||
matches.length === 1 &&
|
||||
snapshot.state.namedPlaylist.listFreshness === "fresh" &&
|
||||
snapshot.state.namedPlaylist.documentFreshness === "none" &&
|
||||
snapshot.state.namedPlaylist.rowCount === 0;
|
||||
}, {
|
||||
timeoutMilliseconds: 60_000,
|
||||
allowUiBusy: true,
|
||||
allowActiveDryRun: true
|
||||
});
|
||||
const readback = current.state.namedPlaylist.definitions.find(row => row.title === title);
|
||||
if (!readback || typeof readback.definitionId !== "string" ||
|
||||
readback.definitionId.length === 0) {
|
||||
failKnown("PROGRAM fresh readback did not retain the created definition identity.");
|
||||
}
|
||||
await pointerClick(
|
||||
namedDefinitionExpression(title),
|
||||
"PROGRAM select fresh delete target",
|
||||
0,
|
||||
activeOptions);
|
||||
await waitFor("PROGRAM fresh delete target selection", snapshot =>
|
||||
snapshot.state.playout.phase === "program" &&
|
||||
snapshot.state.namedPlaylist.selectedDefinitionId === readback.definitionId,
|
||||
{ allowUiBusy: false, allowActiveDryRun: true });
|
||||
await pointerClick(
|
||||
'document.getElementById("named-playlist-delete-button")',
|
||||
"PROGRAM open named-playlist delete confirmation",
|
||||
0,
|
||||
activeOptions);
|
||||
await waitFor("PROGRAM named-playlist delete confirmation", snapshot =>
|
||||
snapshot.state.playout.phase === "program" &&
|
||||
snapshot.dom.namedConfirmationHidden === false,
|
||||
{ allowUiBusy: false, allowActiveDryRun: true });
|
||||
const beforeProgramDeleteWriteCount = namedPlaylistDeleteWriteCount(await readSnapshot());
|
||||
await pointerClick(
|
||||
'document.getElementById("named-playlist-confirmation-yes")',
|
||||
"PROGRAM confirm named-playlist delete",
|
||||
0,
|
||||
activeOptions);
|
||||
await waitForNamedPlaylistDeleteDispatch(
|
||||
"PROGRAM physical named-playlist delete confirmation",
|
||||
beforeProgramDeleteWriteCount,
|
||||
activeOptions);
|
||||
current = await waitFor("PROGRAM named-playlist delete and absence", snapshot =>
|
||||
snapshot.state.playout.phase === "program" &&
|
||||
snapshot.state.playout.currentEntryId === currentEntryId &&
|
||||
snapshot.state.namedPlaylist.definitions.every(row => row.title !== title) &&
|
||||
snapshot.state.namedPlaylist.lastMutationOutcome?.startsWith("committed") === true,
|
||||
{
|
||||
timeoutMilliseconds: 60_000,
|
||||
allowUiBusy: true,
|
||||
allowActiveDryRun: true
|
||||
});
|
||||
await pointerClick(
|
||||
'document.getElementById("named-playlist-cancel-button")',
|
||||
"PROGRAM close named-playlist dialog after delete",
|
||||
0,
|
||||
activeOptions);
|
||||
current = await waitFor("PROGRAM named-playlist dialog final close", snapshot =>
|
||||
snapshot.state.playout.phase === "program" &&
|
||||
snapshot.dom.namedModalHidden === true &&
|
||||
snapshot.dom.namedConfirmationHidden === true,
|
||||
{ allowUiBusy: false, allowActiveDryRun: true });
|
||||
|
||||
const writes = current.safety.approvedDatabaseWriteMessages.slice(baselineWriteCount);
|
||||
const writeCounts = type => writes.filter(message => message.type === type).length;
|
||||
const createWriteCount = writeCounts("create-named-playlist");
|
||||
const saveWriteCount = writeCounts("save-current-named-playlist-to");
|
||||
const deleteWriteCount = writeCounts("delete-selected-named-playlist");
|
||||
if (writes.length !== 3 || createWriteCount !== 1 || saveWriteCount !== 1 ||
|
||||
deleteWriteCount !== 1) {
|
||||
failUnknown("PROGRAM named-playlist CRUD did not emit exactly one create/save/delete write.");
|
||||
}
|
||||
|
||||
await pointerClick(
|
||||
'document.getElementById("playout-take-out")',
|
||||
"leave DryRun PROGRAM after named-playlist CRUD",
|
||||
0,
|
||||
activeOptions);
|
||||
current = await waitFor("PROGRAM named-playlist final IDLE", snapshot =>
|
||||
snapshot.state.playout.phase === "idle" &&
|
||||
snapshot.state.playout.currentEntryId == null &&
|
||||
snapshot.state.playout.outcomeUnknown !== true &&
|
||||
snapshot.state.namedPlaylist.definitions.every(row => row.title !== title) &&
|
||||
snapshot.state.isBusy !== true && snapshot.state.playout.isBusy !== true,
|
||||
{
|
||||
timeoutMilliseconds: 60_000,
|
||||
allowUiBusy: true,
|
||||
// TAKE OUT was already sent exactly once. Permit the known DryRun PROGRAM
|
||||
// state while its state receipt crosses the WebView boundary; the predicate
|
||||
// still requires the final sealed state to be IDLE.
|
||||
allowActiveDryRun: true
|
||||
});
|
||||
|
||||
await pointerClickPlaylistRow(
|
||||
originalSelectedIds[0],
|
||||
"restore deletion selection after PROGRAM named-playlist CRUD");
|
||||
await pointerClick(`(() => {
|
||||
const row = Array.from(document.querySelectorAll(
|
||||
"#playlist-rows .playlist-data-row"))
|
||||
.find(candidate => candidate.dataset.rowId === ${JSON.stringify(originalActiveId)});
|
||||
return row?.querySelector(".playlist-row-header") || null;
|
||||
})()`, "restore active row after PROGRAM named-playlist CRUD");
|
||||
current = await waitFor("restore playlist state after PROGRAM named-playlist CRUD", snapshot =>
|
||||
snapshot.state.playout.phase === "idle" &&
|
||||
JSON.stringify(selectedPlaylistIds(snapshot)) ===
|
||||
JSON.stringify(originalSelectedIds) &&
|
||||
activePlaylistId(snapshot) === originalActiveId,
|
||||
{ allowUiBusy: false });
|
||||
|
||||
evidence.programPlaylistDb = {
|
||||
result: "PASS",
|
||||
title,
|
||||
saveDefinitionId: created.definitionId,
|
||||
definitionId: readback.definitionId,
|
||||
currentEntryId,
|
||||
rowCount: savedRowCount,
|
||||
createWriteCount,
|
||||
saveWriteCount,
|
||||
deleteWriteCount,
|
||||
freshDefinitionReadbackVerified: true,
|
||||
exactAbsenceVerified: true,
|
||||
finalPhase: current.state.playout.phase
|
||||
};
|
||||
await checkpoint("program-named-playlist-create-save-readback-delete", {
|
||||
title,
|
||||
currentEntryId,
|
||||
rowCount: 3
|
||||
});
|
||||
}
|
||||
|
||||
const manualFinancialScreens = Object.freeze([
|
||||
"revenue-composition",
|
||||
"growth-metrics",
|
||||
@@ -6263,6 +6906,7 @@ try {
|
||||
await connect(endpoint);
|
||||
await executeSafeSequence();
|
||||
await executeDryRunPlayoutSequence();
|
||||
await executeProgramNamedPlaylistCrud();
|
||||
await executeFullUiDbSequence();
|
||||
await captureScreenshot();
|
||||
const sealedSnapshot = await readSnapshot();
|
||||
|
||||
Reference in New Issue
Block a user