Files
MBN_STOCK_WEBVIEW/scripts/Test-LegacyPackageInput.mjs

4806 lines
200 KiB
JavaScript

import fs from "node:fs";
import path from "node:path";
import { createHash, randomBytes } from "node:crypto";
const exactTargetUrl = "https://legacy-parity.mbn.local/index.html";
const ctrlModifier = 2;
const shiftModifier = 8;
const defaultObservationTimeoutMilliseconds = 45_000;
const minimumObservationTimeoutMilliseconds = 5_000;
const maximumObservationTimeoutMilliseconds = 900_000;
const acceptedArgumentNames = new Set([
"profile",
"scope",
"port",
"output",
"screenshot",
"windows-input-request",
"windows-input-ack",
"timeout-ms"
]);
const readOnlyForbiddenControlIds = Object.freeze([
"playout-prepare",
"playout-take-in",
"playout-next",
"playout-take-out",
"named-playlist-create-button",
"named-playlist-delete-button",
"named-playlist-confirmation-yes",
"operator-catalog-save",
"operator-catalog-delete"
]);
const readOnlyAllowedOutboundMessageTypes = Object.freeze([
"ready",
"search-stocks",
"select-stock",
"cut-pointer-down",
"cut-key-down",
"drop-selected-cuts",
"select-tab",
"hover-swap-tabs",
"activate-playlist-row",
"select-playlist-row",
"reorder-playlist-rows",
"select-playlist-boundary",
"refresh-named-playlists"
]);
const fullUiDbAllowedOutboundMessageTypes = Object.freeze([
...readOnlyAllowedOutboundMessageTypes,
"activate-fixed-action",
"activate-fixed-section",
"select-industry",
"double-click-industry",
"swap-industries",
"set-industry-comparison-text",
"activate-industry-action",
"activate-industry-section",
"search-overseas-industries",
"search-overseas-stocks",
"select-overseas-fixed-index",
"select-overseas-industry",
"select-overseas-stock",
"activate-overseas-action",
"search-comparison-domestic",
"search-comparison-world",
"select-comparison-domestic",
"select-comparison-world",
"select-comparison-fixed",
"choose-comparison-domestic",
"choose-comparison-world",
"choose-comparison-fixed",
"swap-comparison-targets",
"clear-comparison-targets",
"add-comparison-pair",
"select-comparison-pair",
"move-comparison-pair",
"delete-selected-comparison-pair",
"delete-all-comparison-pairs",
"activate-comparison-action",
"activate-comparison-section",
"search-themes",
"select-theme",
"activate-theme-result",
"set-theme-sort",
"activate-theme-action",
"begin-uc4-theme-catalog",
"edit-uc4-selected-theme",
"delete-uc4-selected-theme",
"search-experts",
"select-expert",
"activate-expert-action",
"begin-uc6-expert-catalog",
"edit-uc6-selected-expert",
"delete-uc6-selected-expert",
"search-trading-halts",
"select-trading-halt",
"toggle-trading-halt-sort",
"activate-trading-halt-action",
"open-manual-financial",
"close-manual-financial",
"search-manual-financial",
"find-manual-financial",
"toggle-manual-financial-name-sort",
"load-manual-financial",
"activate-manual-financial-result",
"begin-manual-financial-create",
"search-manual-financial-stocks",
"select-manual-financial-stock",
"save-manual-financial",
"delete-manual-financial",
"activate-manual-financial-action",
"open-manual-list",
"close-manual-list",
"refresh-manual-list",
"set-manual-net-sell-cell",
"search-manual-vi",
"select-manual-vi-result",
"add-manual-vi-result",
"move-manual-vi-item",
"delete-manual-vi-item",
"delete-all-manual-vi-items",
"confirm-manual-list",
"open-operator-catalog",
"close-operator-catalog",
"search-operator-catalog",
"select-operator-catalog-result",
"begin-create-theme-catalog",
"begin-create-expert-catalog",
"search-operator-catalog-stocks",
"select-operator-catalog-stock",
"add-operator-catalog-stock",
"activate-operator-catalog-stock",
"move-operator-catalog-draft-row",
"reorder-operator-catalog-draft-row",
"remove-operator-catalog-draft-row",
"select-operator-catalog-draft-row",
"remove-active-operator-catalog-draft-row",
"clear-operator-catalog-draft-rows",
"set-operator-catalog-draft-buy-amount",
"check-operator-catalog-theme-name",
"save-operator-catalog",
"confirm-native-dialog",
"cancel-native-dialog",
"dismiss-dialog",
"select-named-playlist",
"load-named-playlist-by-id",
"create-named-playlist",
"save-current-named-playlist-to",
"delete-selected-named-playlist",
"move-selected-playlist-rows",
"delete-selected-playlist-rows",
"delete-all-playlist-rows",
"set-all-playlist-enabled",
"set-moving-averages"
]);
const playoutIntentMessageTypes = Object.freeze([
"prepare-playout",
"take-in",
"next-playout",
"take-out",
"choose-background",
"toggle-background"
]);
const databaseWriteIntentMessageTypes = Object.freeze([
"create-named-playlist",
"delete-selected-named-playlist",
"save-current-named-playlist-to",
"save-manual-financial",
"delete-manual-financial",
"delete-all-manual-financial",
"import-manual-lists",
"confirm-manual-list",
"add-comparison-pair",
"move-comparison-pair",
"delete-selected-comparison-pair",
"delete-all-comparison-pairs",
"save-operator-catalog",
"delete-operator-catalog",
"delete-uc4-selected-theme",
"delete-uc6-selected-expert"
]);
const forbiddenCdpMethods = new Set([
"Browser.close",
"Page.close",
"Runtime.terminateExecution",
"Target.closeTarget"
]);
class HarnessFailure extends Error {
constructor(category, message) {
super(`${category}: ${message}`);
this.name = "HarnessFailure";
this.category = category;
}
}
function failKnown(message) {
throw new HarnessFailure("KNOWN_FAILURE", message);
}
function failUnknown(message) {
throw new HarnessFailure("OUTCOME_UNKNOWN", message);
}
function parseArguments(argv) {
if (argv.length === 0 || argv.length % 2 !== 0) {
failKnown(
"Usage: node scripts/Test-LegacyPackageInput.mjs --port <port> " +
"--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] [--timeout-ms <milliseconds>]");
}
const values = new Map();
for (let index = 0; index < argv.length; index += 2) {
const name = argv[index];
const value = argv[index + 1];
if (!name?.startsWith("--") || value === undefined || value.length === 0) {
failKnown("Arguments must be non-empty --name value pairs.");
}
const key = name.slice(2);
if (!acceptedArgumentNames.has(key)) {
failKnown(`Unknown argument --${key}.`);
}
if (values.has(key)) {
failKnown(`Argument --${key} was supplied more than once.`);
}
values.set(key, value);
}
if (!values.has("port") || !values.has("output") ||
!values.has("screenshot") || !values.has("windows-input-request") ||
!values.has("windows-input-ack")) {
failKnown(
"--port, --output, --screenshot, --windows-input-request, and " +
"--windows-input-ack are required.");
}
const port = Number(values.get("port"));
if (!Number.isInteger(port) || port < 1 || port > 65_535) {
failKnown("--port must be an integer from 1 through 65535.");
}
const outputPath = path.resolve(values.get("output"));
if (!path.isAbsolute(values.get("output")) ||
path.extname(outputPath).toLowerCase() !== ".json") {
failKnown("--output must be an absolute .json path.");
}
const screenshotPath = path.resolve(values.get("screenshot"));
if (!path.isAbsolute(values.get("screenshot")) ||
path.extname(screenshotPath).toLowerCase() !== ".png") {
failKnown("--screenshot must be an absolute .png path.");
}
if (outputPath.toLowerCase() === screenshotPath.toLowerCase()) {
failKnown("--output and --screenshot must name different files.");
}
const windowsInputRequestPath = path.resolve(values.get("windows-input-request"));
const windowsInputAckPath = path.resolve(values.get("windows-input-ack"));
for (const [name, candidate] of [
["--windows-input-request", windowsInputRequestPath],
["--windows-input-ack", windowsInputAckPath]
]) {
if (!path.isAbsolute(values.get(name.slice(2))) ||
path.extname(candidate).toLowerCase() !== ".json") {
failKnown(`${name} must be an absolute .json path.`);
}
}
const uniqueEvidencePaths = new Set([
outputPath.toLowerCase(),
screenshotPath.toLowerCase(),
windowsInputRequestPath.toLowerCase(),
windowsInputAckPath.toLowerCase()
]);
if (uniqueEvidencePaths.size !== 4) {
failKnown("All result, screenshot, Windows-input request, and ack paths must differ.");
}
const timeoutMilliseconds = values.has("timeout-ms")
? Number(values.get("timeout-ms"))
: defaultObservationTimeoutMilliseconds;
if (!Number.isInteger(timeoutMilliseconds) ||
timeoutMilliseconds < minimumObservationTimeoutMilliseconds ||
timeoutMilliseconds > maximumObservationTimeoutMilliseconds) {
failKnown(
`--timeout-ms must be an integer from ${minimumObservationTimeoutMilliseconds} ` +
`through ${maximumObservationTimeoutMilliseconds}.`);
}
if (fs.existsSync(outputPath) || fs.existsSync(screenshotPath) ||
fs.existsSync(windowsInputRequestPath) || fs.existsSync(windowsInputAckPath)) {
failKnown("An evidence path already exists; evidence is never overwritten.");
}
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
fs.mkdirSync(path.dirname(screenshotPath), { recursive: true });
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.");
}
const scope = values.get("scope") || "all";
if (!new Set(["all", "plist", "graphe", "catalog", "screens"]).has(scope)) {
failKnown("--scope must be all, plist, graphe, catalog, or screens.");
}
if (profile !== "full-ui-db" && scope !== "all") {
failKnown("A non-DB-write run requires --scope all.");
}
return {
profile,
scope,
port,
outputPath,
screenshotPath,
windowsInputRequestPath,
windowsInputAckPath,
timeoutMilliseconds
};
}
function assertWithinHardDeadline(label) {
if (externalCancellationRequested && !cleanupMode) {
failUnknown(`The wrapper requested cancellation ${label}; no action is retried.`);
}
const deadline = cleanupMode
? cleanupDeadlineEpochMilliseconds
: hardDeadlineEpochMilliseconds;
if (Date.now() >= deadline) {
failUnknown(`The global input deadline expired ${label}; no action is retried.`);
}
}
async function sleep(milliseconds) {
assertWithinHardDeadline("before waiting");
const activeDeadline = cleanupMode
? cleanupDeadlineEpochMilliseconds
: hardDeadlineEpochMilliseconds;
const remaining = activeDeadline - Date.now();
await new Promise(resolve => setTimeout(resolve, Math.min(milliseconds, remaining)));
assertWithinHardDeadline("after waiting");
}
function sha256(bytes) {
return createHash("sha256").update(bytes).digest("hex").toUpperCase();
}
function errorProjection(error) {
return {
category: error instanceof HarnessFailure ? error.category : "HARNESS_ERROR",
name: error?.name || "Error",
message: String(error?.message || error)
};
}
const configuration = parseArguments(process.argv.slice(2));
const fullUiDbProfile = configuration.profile === "full-ui-db";
const dryRunPlayoutProfile = configuration.profile === "dry-run-playout";
const forbiddenControlIds = fullUiDbProfile
? Object.freeze([
"playout-background-file",
"playout-prepare",
"playout-take-in",
"playout-next",
"playout-take-out"
])
: dryRunPlayoutProfile
? Object.freeze(readOnlyForbiddenControlIds.filter(id =>
!new Set(["playout-take-in", "playout-next", "playout-take-out"]).has(id)))
: readOnlyForbiddenControlIds;
const allowedOutboundMessageTypes = fullUiDbProfile
? fullUiDbAllowedOutboundMessageTypes
: dryRunPlayoutProfile
? Object.freeze([
...readOnlyAllowedOutboundMessageTypes,
"take-in",
"next-playout",
"take-out"
])
: readOnlyAllowedOutboundMessageTypes;
const hardDeadlineEpochMilliseconds = Date.now() + configuration.timeoutMilliseconds;
const startedAt = new Date().toISOString();
const evidence = {
schemaVersion: 1,
profile: configuration.profile,
scope: configuration.scope,
result: "RUNNING",
startedAt,
completedAt: null,
target: {
expectedUrl: exactTargetUrl,
port: configuration.port,
timeoutMilliseconds: configuration.timeoutMilliseconds,
id: null,
url: null
},
safety: {
requiredMode: "dryRun",
requiredPhase: "idle",
forbiddenControlIds,
allowedOutboundMessageTypes,
appCloseRequested: false,
playoutIntentIssued: false,
databaseWriteIntentIssued: false,
approvedDatabaseWriteMessages: [],
observedOutboundMessageTypes: [],
blockedOutboundMessages: [],
stateViolations: [],
externalCancellationRequested: false
},
fixture: {
query: "삼성",
stockDisplayName: "삼성출판사",
databaseStockDisplayName: "1Q 삼성전자선물단일종목레버리지",
namedPlaylistTitle: fullUiDbProfile
? `CDX_P_${Date.now()}_${randomBytes(3).toString("hex")}`
: null,
krxThemeTitle: fullUiDbProfile
? `CDX_K_${Date.now()}_${randomBytes(3).toString("hex")}`
: null,
nxtThemeTitle: fullUiDbProfile
? `CDX_N_${Date.now()}_${randomBytes(3).toString("hex")}`
: null,
expertTitle: fullUiDbProfile
? `CDX_E_${Date.now()}_${randomBytes(3).toString("hex")}`
: null
},
fullUiDb: fullUiDbProfile ? {
result: "PENDING",
screenChecks: [],
databaseChecks: [],
screenInventory: [],
cleanupVerified: false
} : null,
dryRunPlayout: dryRunPlayoutProfile ? {
result: "PENDING",
stagedEntryId: null,
selectedEntryId: null,
nextEntryId: null,
programCutSelection: null,
playlistCountBeforeSelection: null,
playlistCountAfterDoubleClick: null
} : null,
checkpoints: [],
inputs: [],
warnings: [],
windowsInput: {
required: true,
requestPath: configuration.windowsInputRequestPath,
acknowledgementPath: configuration.windowsInputAckPath,
requestSha256: null,
acknowledgementSha256: null,
result: "PENDING"
},
screenshot: null,
finalSnapshot: null,
error: null
};
let socket = null;
let nextRequestId = 1;
const pendingRequests = new Map();
let externalCancellationRequested = false;
let cancellationInputBuffer = "";
let pointerIsPressed = false;
let pointerReleasePoint = null;
let pointerReleaseModifiers = 0;
let dragInterceptionEnabled = false;
let activeInterceptedDragData = null;
let probeInstalled = false;
let cleanupMode = false;
let cleanupDeadlineEpochMilliseconds = 0;
let expectedJavaScriptDialog = null;
let pendingDragIntercept = null;
const unexpectedJavaScriptDialogs = [];
process.stdin.setEncoding("utf8");
process.stdin.on("data", chunk => {
cancellationInputBuffer += chunk;
if (!cancellationInputBuffer.split(/\r?\n/u).some(line => line === "CANCEL")) {
if (cancellationInputBuffer.length > 64) cancellationInputBuffer = "";
return;
}
externalCancellationRequested = true;
evidence.safety.externalCancellationRequested = true;
for (const pending of pendingRequests.values()) {
clearTimeout(pending.timer);
pending.reject(new HarnessFailure(
"OUTCOME_UNKNOWN",
`Wrapper cancellation interrupted ${pending.method}; the command is not retried.`));
}
pendingRequests.clear();
if (pendingDragIntercept) {
clearTimeout(pendingDragIntercept.timer);
pendingDragIntercept.reject(new HarnessFailure(
"OUTCOME_UNKNOWN",
"Wrapper cancellation interrupted the intercepted drag; it is not retried."));
pendingDragIntercept = null;
}
});
if (typeof process.stdin.unref === "function") process.stdin.unref();
async function discoverExactTarget() {
const deadline = Date.now() + Math.min(
configuration.timeoutMilliseconds,
30_000);
const discoveryUrl = `http://127.0.0.1:${configuration.port}/json/list`;
let lastFailure = null;
while (Date.now() < deadline) {
try {
const response = await fetch(discoveryUrl, {
redirect: "error",
signal: AbortSignal.timeout(1_000)
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const targets = await response.json();
if (!Array.isArray(targets)) {
failKnown("CDP discovery did not return a target array.");
}
const exactTargets = targets.filter(target =>
target?.type === "page" && target.url === exactTargetUrl);
if (exactTargets.length > 1) {
failKnown("More than one exact LegacyParityApp WebView target exists.");
}
if (exactTargets.length === 1) {
return exactTargets[0];
}
} catch (error) {
if (error instanceof HarnessFailure) throw error;
lastFailure = String(error?.message || error);
}
await sleep(100);
}
failKnown(`The exact package WebView target was not found at ${discoveryUrl}` +
(lastFailure ? ` (${lastFailure}).` : "."));
}
function validateWebSocketTarget(target) {
if (!target?.id || !target.webSocketDebuggerUrl) {
failKnown("The exact target does not expose a WebSocket debugger URL.");
}
const endpoint = new URL(target.webSocketDebuggerUrl);
if (endpoint.protocol !== "ws:" || endpoint.hostname !== "127.0.0.1" ||
Number(endpoint.port) !== configuration.port ||
endpoint.pathname !== `/devtools/page/${target.id}`) {
failKnown("The CDP endpoint is not the exact loopback page endpoint.");
}
return endpoint;
}
async function connect(endpoint) {
if (typeof WebSocket !== "function") {
failKnown("This Node runtime does not provide the WebSocket API.");
}
socket = new WebSocket(endpoint.href);
await new Promise((resolve, reject) => {
const timer = setTimeout(
() => reject(new HarnessFailure("OUTCOME_UNKNOWN", "CDP socket open timed out.")),
5_000);
socket.addEventListener("open", () => {
clearTimeout(timer);
resolve();
}, { once: true });
socket.addEventListener("error", () => {
clearTimeout(timer);
reject(new HarnessFailure("OUTCOME_UNKNOWN", "CDP socket failed to open."));
}, { once: true });
});
socket.addEventListener("message", event => {
const message = JSON.parse(String(event.data));
if (message.method === "Input.dragIntercepted") {
const pending = pendingDragIntercept;
if (pending) {
pendingDragIntercept = null;
clearTimeout(pending.timer);
pending.resolve(message.params?.data || null);
}
return;
}
if (message.method === "Page.javascriptDialogOpening") {
const dialog = message.params || {};
const expected = expectedJavaScriptDialog;
if (!expected || dialog.type !== expected.type ||
(expected.message && dialog.message !== expected.message)) {
unexpectedJavaScriptDialogs.push({
type: dialog.type || null,
message: dialog.message || null,
at: new Date().toISOString()
});
return;
}
expectedJavaScriptDialog = null;
void rpc("Page.handleJavaScriptDialog", {
accept: expected.accept === true
}).then(expected.resolve, expected.reject);
return;
}
if (!Object.hasOwn(message, "id")) return;
const pending = pendingRequests.get(message.id);
if (!pending) return;
pendingRequests.delete(message.id);
clearTimeout(pending.timer);
if (message.error) {
pending.reject(new HarnessFailure(
"KNOWN_FAILURE",
`${pending.method} failed: ${message.error.message || "CDP error"}`));
} else {
pending.resolve(message.result);
}
});
socket.addEventListener("close", () => {
for (const pending of pendingRequests.values()) {
clearTimeout(pending.timer);
pending.reject(new HarnessFailure(
"OUTCOME_UNKNOWN",
`CDP socket closed while waiting for ${pending.method}.`));
}
pendingRequests.clear();
if (pendingDragIntercept) {
clearTimeout(pendingDragIntercept.timer);
pendingDragIntercept.reject(new HarnessFailure(
"OUTCOME_UNKNOWN",
"CDP socket closed while waiting for the intercepted drag."));
pendingDragIntercept = null;
}
});
}
function waitForDragIntercept(label) {
if (pendingDragIntercept) {
failKnown("A second intercepted drag was requested before the first completed.");
}
const promise = new Promise((resolve, reject) => {
const timer = setTimeout(() => {
pendingDragIntercept = null;
reject(new HarnessFailure(
"KNOWN_FAILURE",
`${label} did not expose Chromium's intercepted drag data.`));
}, 5_000);
pendingDragIntercept = { resolve, reject, timer };
});
// A pointer or transport failure can happen before the caller reaches its
// await. Attach a handler immediately; the caller still observes the same
// rejection when it awaits this promise during structured cleanup.
void promise.catch(() => {});
return promise;
}
function cancelPendingDragIntercept(message) {
const pending = pendingDragIntercept;
if (!pending) return;
pendingDragIntercept = null;
clearTimeout(pending.timer);
pending.reject(new HarnessFailure("KNOWN_FAILURE", message));
}
function rpc(method, params = {}, timeoutMilliseconds = 10_000) {
assertWithinHardDeadline(`before ${method}`);
if (cleanupMode && method !== "Input.dispatchMouseEvent" &&
method !== "Input.dispatchDragEvent" &&
method !== "Input.setInterceptDrags" && method !== "Runtime.evaluate") {
failKnown(`Cleanup refused unexpected CDP method ${method}.`);
}
if (forbiddenCdpMethods.has(method)) {
evidence.safety.appCloseRequested = true;
failKnown(`The harness refused forbidden CDP method ${method}.`);
}
if (!socket || socket.readyState !== WebSocket.OPEN) {
failUnknown(`CDP socket is not open for ${method}.`);
}
const id = nextRequestId++;
return new Promise((resolve, reject) => {
const activeDeadline = cleanupMode
? cleanupDeadlineEpochMilliseconds
: hardDeadlineEpochMilliseconds;
const remaining = activeDeadline - Date.now();
const effectiveTimeout = cleanupMode
? Math.min(timeoutMilliseconds, 2_000, remaining)
: Math.min(timeoutMilliseconds, remaining);
const timer = setTimeout(() => {
pendingRequests.delete(id);
reject(new HarnessFailure(
"OUTCOME_UNKNOWN",
`${method} timed out; the command is not retried.`));
}, effectiveTimeout);
pendingRequests.set(id, { method, resolve, reject, timer });
socket.send(JSON.stringify({ id, method, params }));
});
}
async function evaluate(expression) {
const response = await rpc("Runtime.evaluate", {
expression,
awaitPromise: true,
returnByValue: true,
userGesture: false
});
if (response.exceptionDetails) {
const description = response.exceptionDetails.exception?.description ||
response.exceptionDetails.text || "Runtime evaluation failed.";
failKnown(description);
}
return response.result?.value;
}
function probeSnapshotExpression() {
return `(() => {
const probe = globalThis.__legacyPackageInputProbe;
const state = probe?.states?.at(-1) || null;
const pointerEvents = probe?.pointerEvents || [];
const lastPointerDown = [...pointerEvents].reverse().find(event =>
event.type === "pointerdown") || null;
const activePointerId = Number.isInteger(lastPointerDown?.pointerId)
? lastPointerDown.pointerId
: null;
const capturedCutPhysicalIndices = activePointerId === null
? []
: Array.from(document.querySelectorAll("#cut-list .cut-row"))
.filter(row => typeof row.hasPointerCapture === "function" &&
row.hasPointerCapture(activePointerId))
.map(row => Number(row.dataset.physicalIndex));
const capturedPlaylistRowIds = activePointerId === null
? []
: Array.from(document.querySelectorAll(
"#playlist-rows .playlist-data-row .playlist-row-header"))
.filter(header => typeof header.hasPointerCapture === "function" &&
header.hasPointerCapture(activePointerId))
.map(header => header.closest(".playlist-data-row")?.dataset?.rowId || null)
.filter(Boolean);
const active = document.activeElement;
const rowForActive = active?.closest?.(".playlist-data-row") || null;
const cutDragFeedback = document.getElementById("cut-drag-feedback");
const cutDragFeedbackBounds = cutDragFeedback?.getBoundingClientRect() || null;
const visibleModal = Array.from(document.querySelectorAll(
"[role='dialog'], [role='alertdialog']"))
.filter(element => !element.hidden && !element.closest("[hidden]")).at(-1) || null;
const focusables = visibleModal ? Array.from(visibleModal.querySelectorAll(
"button:not([disabled]), input:not([disabled]):not([type='hidden']), " +
"select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex='-1'])"))
.filter(element => !element.hidden && !element.closest("[hidden]") &&
getComputedStyle(element).display !== "none" &&
getComputedStyle(element).visibility !== "hidden") : [];
return {
capturedAt: new Date().toISOString(),
devicePixelRatio: window.devicePixelRatio,
stateCount: probe?.stateCount ?? null,
busyStateCount: probe?.busyStateCount ?? null,
safety: {
postMessageWrapped: probe?.postMessageWrapper != null &&
window.chrome?.webview?.postMessage === probe.postMessageWrapper,
outboundMessages: (probe?.outboundMessages || []).map(message => ({ ...message })),
approvedDatabaseWriteMessages:
(probe?.approvedDatabaseWriteMessages || []).map(message => ({ ...message })),
blockedOutboundMessages: (probe?.blockedOutboundMessages || [])
.map(message => ({ ...message })),
stateViolations: (probe?.stateViolations || []).map(violation => ({
...violation,
reasons: [...violation.reasons]
}))
},
pointerEvents: pointerEvents.slice(-40).map(event => ({ ...event })),
state: state ? {
revision: state.revision,
isBusy: state.isBusy,
searchText: state.searchText,
searchResults: (state.searchResults || []).map(row => ({
index: row.index,
displayName: row.displayName
})),
selectedStockIndex: state.selectedStockIndex,
cutRows: (state.cutRows || []).map(row => ({
physicalIndex: row.physicalIndex,
displayOrdinal: row.displayOrdinal,
rawLabel: row.rawLabel,
isSeparator: row.isSeparator,
isSelected: row.isSelected,
isFocused: row.isFocused
})),
playlist: (state.playlist || []).map(row => ({
rowId: row.rowId,
isEnabled: row.isEnabled,
isSelected: row.isSelected,
isActive: row.isActive,
marketText: row.marketText,
stockName: row.stockName,
graphicType: row.graphicType,
subtype: row.subtype,
pageText: row.pageText
})),
dialog: state.dialog,
statusMessage: state.statusMessage,
statusKind: state.statusKind,
commandResult: state.commandResult,
tabs: (state.tabs || []).map(tab => ({ ...tab })),
movingAverages: state.movingAverages ? { ...state.movingAverages } : null,
fixedCatalog: state.fixedCatalog ? structuredClone(state.fixedCatalog) : null,
industry: state.industry ? structuredClone(state.industry) : null,
overseas: state.overseas ? structuredClone(state.overseas) : null,
comparison: state.comparison ? structuredClone(state.comparison) : null,
theme: state.theme ? structuredClone(state.theme) : null,
expert: state.expert ? structuredClone(state.expert) : null,
tradingHalt: state.tradingHalt ? structuredClone(state.tradingHalt) : null,
manualFinancial: state.manualFinancial
? structuredClone(state.manualFinancial) : null,
manualLists: state.manualLists ? structuredClone(state.manualLists) : null,
operatorCatalog: state.operatorCatalog
? structuredClone(state.operatorCatalog) : null,
fixedSectionBatch: state.fixedSectionBatch
? structuredClone(state.fixedSectionBatch) : null,
namedPlaylist: state.namedPlaylist ? {
revision: state.namedPlaylist.revision,
definitions: (state.namedPlaylist.definitions || []).map(definition => ({
definitionId: definition.definitionId,
title: definition.title,
isSelected: definition.isSelected
})),
selectedDefinitionId: state.namedPlaylist.selectedDefinitionId,
selectedTitle: state.namedPlaylist.selectedTitle,
rowCount: state.namedPlaylist.rowCount,
isWriteInProgress: state.namedPlaylist.isWriteInProgress,
isWriteQuarantined: state.namedPlaylist.isWriteQuarantined,
canMutate: state.namedPlaylist.canMutate,
canCreate: state.namedPlaylist.canCreate,
canLoad: state.namedPlaylist.canLoad,
canSave: state.namedPlaylist.canSave,
canDelete: state.namedPlaylist.canDelete,
lastMutationOutcome: state.namedPlaylist.lastMutationOutcome,
statusMessage: state.namedPlaylist.statusMessage,
statusKind: state.namedPlaylist.statusKind
} : null,
interactionMetrics: state.interactionMetrics,
playout: state.playout ? {
mode: state.playout.mode,
connectionState: state.playout.connectionState,
phase: state.playout.phase,
isConnected: state.playout.isConnected,
isCommandAvailable: state.playout.isCommandAvailable,
liveTakeInAllowed: state.playout.liveTakeInAllowed,
isPlayCompletionPending: state.playout.isPlayCompletionPending,
isTakeOutCompletionPending: state.playout.isTakeOutCompletionPending,
outcomeUnknown: state.playout.outcomeUnknown,
preparedCode: state.playout.preparedCode,
onAirCode: state.playout.onAirCode,
currentEntryId: state.playout.currentEntryId,
currentCueIndexZeroBased: state.playout.currentCueIndexZeroBased,
pageIndexZeroBased: state.playout.pageIndexZeroBased,
pageCount: state.playout.pageCount,
nextKind: state.playout.nextKind,
isBusy: state.playout.isBusy,
refreshActive: state.playout.refreshActive,
message: state.playout.message
} : null
} : null,
dom: {
readyState: document.readyState,
url: location.href,
viewport: {
width: document.documentElement.clientWidth,
height: document.documentElement.clientHeight
},
bodyBusy: document.body.getAttribute("aria-busy"),
connectionText: (document.getElementById("playout-connection-state")?.textContent || "").trim(),
playoutStatusText: (document.getElementById("playout-status")?.textContent || "").trim(),
operatorStatusText: (document.getElementById("operator-status")?.textContent || "").trim(),
legacyDialogHidden: document.getElementById("legacy-dialog")?.hidden === true,
legacyDialogText: (document.getElementById("legacy-dialog-message")?.textContent || "").trim(),
stockOptions: Array.from(document.querySelectorAll(
"#stock-results button[data-result-index]"))
.map(button => ({
index: Number(button.dataset.resultIndex),
text: (button.textContent || "").trim(),
selected: button.getAttribute("aria-selected") === "true",
tabIndex: button.tabIndex,
focused: button === active
})),
cuts: Array.from(document.querySelectorAll("#cut-list .cut-row"))
.map(row => ({
physicalIndex: Number(row.dataset.physicalIndex),
selected: row.getAttribute("aria-selected") === "true",
tabIndex: row.tabIndex,
focused: row === active,
dragging: row.classList.contains("dragging")
})),
playlist: Array.from(document.querySelectorAll(
"#playlist-rows .playlist-data-row"))
.map(row => ({
rowId: row.dataset.rowId,
selected: row.getAttribute("aria-selected") === "true",
active: row.dataset.active === "true",
current: row.getAttribute("aria-current") === "true",
enabled: row.querySelector("input[type='checkbox']")?.checked === true,
dragging: row.classList.contains("dragging"),
dragBefore: row.classList.contains("drag-before"),
dragAfter: row.classList.contains("drag-after"),
rowHeaderText: (row.querySelector(".playlist-row-header")?.textContent || "").trim(),
rowHeaderWidth: row.querySelector(".playlist-row-header")
?.getBoundingClientRect().width ?? null,
rowHeaderDragEnabled: row.querySelector(".playlist-row-header")
?.dataset?.dragEnabled === "true",
focused: row === active || row.contains(active)
})),
cutDropHighlighted: document.getElementById("playlist-drop-zone")
?.classList.contains("cut-drag-over") === true,
cutDragFeedback: cutDragFeedback ? {
hidden: cutDragFeedback.hidden === true,
dropAllowed: cutDragFeedback.dataset.dropAllowed || null,
left: cutDragFeedbackBounds?.left ?? null,
top: cutDragFeedbackBounds?.top ?? null,
width: cutDragFeedbackBounds?.width ?? null,
height: cutDragFeedbackBounds?.height ?? null
} : null,
cutPointerCapture: {
pointerId: activePointerId,
physicalIndices: capturedCutPhysicalIndices
},
playlistPointerCapture: {
pointerId: activePointerId,
rowIds: capturedPlaylistRowIds
},
namedModalHidden: document.getElementById("named-playlist-modal")?.hidden === true,
namedConfirmationHidden: document.getElementById("named-playlist-confirmation")?.hidden === true,
manualFinancialModalHidden:
document.getElementById("manual-financial-modal")?.hidden === true,
manualListModalHidden:
document.getElementById("manual-list-modal")?.hidden === true,
operatorCatalogModalHidden:
document.getElementById("operator-catalog-modal")?.hidden === true,
namedTitle: (document.getElementById("named-playlist-title")?.textContent || "").trim(),
namedConfirmationMessage: (document.getElementById(
"named-playlist-confirmation-message")?.textContent || "").trim(),
namedConfirmDisabled: document.getElementById(
"named-playlist-confirm-button")?.disabled === true,
namedDefinitionCount: document.querySelectorAll(
"#named-playlist-definitions button[data-named-playlist-definition-id]").length,
activeElement: active ? {
id: active.id || null,
tagName: active.tagName,
resultIndex: active.dataset?.resultIndex ?? null,
physicalIndex: active.dataset?.physicalIndex ?? null,
playlistRowId: rowForActive?.dataset?.rowId ?? null
} : null,
visibleModalRole: visibleModal?.getAttribute("role") || null,
visibleModalLabelledBy: visibleModal?.getAttribute("aria-labelledby") || null,
activeInsideVisibleModal: visibleModal ? visibleModal.contains(active) : false,
modalFocusableIds: focusables.map(element => element.id || null)
}
};
})()`;
}
async function readSnapshot() {
return await evaluate(probeSnapshotExpression());
}
function assertSafety(
snapshot,
label,
allowUiBusy = true,
allowLegacyDialog = false,
allowActiveDryRun = false) {
if (!snapshot?.state?.playout) {
failKnown(`Native playout state is missing at ${label}.`);
}
const state = snapshot.state;
const playout = state.playout;
if (snapshot.safety?.postMessageWrapped !== true) {
failUnknown(`The outbound WebView safety gate is not installed at ${label}.`);
}
if (unexpectedJavaScriptDialogs.length > 0 || expectedJavaScriptDialog) {
failUnknown(`An unexpected or unresolved JavaScript dialog exists at ${label}.`);
}
const outboundMessages = snapshot.safety.outboundMessages || [];
const approvedDatabaseWriteMessages =
snapshot.safety.approvedDatabaseWriteMessages || [];
const blockedOutboundMessages = snapshot.safety.blockedOutboundMessages || [];
const stateViolations = snapshot.safety.stateViolations || [];
evidence.safety.observedOutboundMessageTypes = outboundMessages
.map(message => message.type);
evidence.safety.approvedDatabaseWriteMessages = approvedDatabaseWriteMessages
.map(message => ({ ...message }));
evidence.safety.blockedOutboundMessages = blockedOutboundMessages
.map(message => ({ ...message }));
evidence.safety.stateViolations = stateViolations.map(violation => ({
...violation,
reasons: [...violation.reasons]
}));
evidence.safety.playoutIntentIssued = outboundMessages.some(message =>
playoutIntentMessageTypes.includes(message.type)) ||
blockedOutboundMessages.some(message => message.category === "playout");
evidence.safety.databaseWriteIntentIssued =
approvedDatabaseWriteMessages.length > 0 ||
blockedOutboundMessages.some(message => message.category === "database-write");
if (blockedOutboundMessages.length > 0) {
failKnown(`The outbound WebView safety gate blocked an unapproved message at ${label}.`);
}
if (stateViolations.length > 0) {
failUnknown(`A transient unsafe playout state was observed before ${label}.`);
}
if (playout.mode !== "dryRun") {
failKnown(`Expected DryRun at ${label}, received ${String(playout.mode)}.`);
}
if (!allowActiveDryRun &&
(playout.phase !== "idle" || playout.preparedCode != null ||
playout.onAirCode != null)) {
failKnown(`Playout left IDLE at ${label}.`);
}
if (playout.outcomeUnknown === true ||
(!allowActiveDryRun && playout.isPlayCompletionPending === true) ||
(!allowActiveDryRun && playout.isTakeOutCompletionPending === true) ||
(!allowActiveDryRun && playout.isBusy === true) ||
(!allowActiveDryRun && playout.refreshActive === true)) {
failUnknown(`Playout became busy, pending, refreshing, or unknown at ${label}.`);
}
if (!allowUiBusy && state.isBusy === true) {
failKnown(`The operator UI is still busy at ${label}.`);
}
if (snapshot.dom.url !== exactTargetUrl || snapshot.dom.readyState !== "complete") {
failKnown(`The exact package document is not ready at ${label}.`);
}
if (snapshot.dom.connectionText !== "DRY RUN") {
failKnown(`The visible connection badge is not DRY RUN at ${label}.`);
}
if (!snapshot.dom.legacyDialogHidden && !allowLegacyDialog) {
failKnown(`A legacy alert is visible at ${label}: ${snapshot.dom.legacyDialogText}`);
}
}
async function waitFor(label, predicate, options = {}) {
const timeoutMilliseconds = options.timeoutMilliseconds ||
Math.min(configuration.timeoutMilliseconds, 60_000);
const deadline = Date.now() + timeoutMilliseconds;
let lastSnapshot = null;
while (Date.now() < deadline) {
lastSnapshot = await readSnapshot();
if (lastSnapshot?.state?.playout) {
assertSafety(
lastSnapshot,
label,
options.allowUiBusy !== false,
options.allowLegacyDialog === true,
options.allowActiveDryRun === true);
}
if (predicate(lastSnapshot)) return lastSnapshot;
await sleep(100);
}
failUnknown(`Timed out waiting for ${label}; no action was retried. Last snapshot: ` +
JSON.stringify(lastSnapshot));
}
async function checkpoint(name, details = null) {
const snapshot = await readSnapshot();
assertSafety(snapshot, name, false);
evidence.checkpoints.push({ name, details, snapshot });
return snapshot;
}
async function elementGeometry(expression, label, verticalFraction = 0.5) {
const geometry = await evaluate(`(() => {
const element = ${expression};
if (!element) return null;
element.scrollIntoView({ block: "nearest", inline: "nearest" });
const rectangle = element.getBoundingClientRect();
return {
x: rectangle.left + rectangle.width / 2,
y: rectangle.top + rectangle.height * ${Number(verticalFraction)},
left: rectangle.left,
top: rectangle.top,
right: rectangle.right,
bottom: rectangle.bottom,
width: rectangle.width,
height: rectangle.height,
disabled: element.disabled === true,
ariaDisabled: element.getAttribute("aria-disabled")
};
})()`);
if (!geometry || geometry.disabled || geometry.ariaDisabled === "true" ||
!Number.isFinite(geometry.x) || !Number.isFinite(geometry.y) ||
geometry.width <= 0 || geometry.height <= 0 ||
geometry.right <= 0 || geometry.bottom <= 0) {
failKnown(`${label} is missing, disabled, or outside the rendered viewport.`);
}
return geometry;
}
async function readWorkspaceLayout() {
return await evaluate(`(() => {
function rectangle(element) {
if (!element) return null;
const bounds = element.getBoundingClientRect();
return {
left: bounds.left,
top: bounds.top,
right: bounds.right,
bottom: bounds.bottom,
width: bounds.width,
height: bounds.height
};
}
const shell = document.querySelector(".operator-shell");
const region = document.getElementById("workspace-region");
const navigation = document.getElementById("workspace-navigation");
const surface = document.querySelector(".workspace-surface");
const toggle = document.getElementById("workspace-nav-toggle");
const stockTab = document.getElementById("workspace-stock-tab");
const settingsTab = document.getElementById("workspace-settings-tab");
const marketTabs = Array.from(
document.querySelectorAll("#market-tabs > button[data-tab-id]"));
const stock = document.getElementById("stock-workspace");
const catalog = document.getElementById("catalog-workspace");
const settings = document.getElementById("settings-workspace");
const schedule = document.querySelector(".playlist-panel");
const scheduleBounds = schedule?.getBoundingClientRect() || null;
const scheduleHit = scheduleBounds && scheduleBounds.width > 0 && scheduleBounds.height > 0
? document.elementFromPoint(
scheduleBounds.left + Math.min(scheduleBounds.width - 2, scheduleBounds.width / 2),
scheduleBounds.top + Math.min(scheduleBounds.height - 2, 90))
: null;
return {
viewport: {
width: document.documentElement.clientWidth,
height: document.documentElement.clientHeight
},
shell: rectangle(shell),
region: rectangle(region),
navigation: rectangle(navigation),
surface: rectangle(surface),
schedule: rectangle(schedule),
scheduleHitOwned: Boolean(schedule && scheduleHit && schedule.contains(scheduleHit)),
activeWorkspace: region?.dataset?.activeWorkspace || null,
navigationExpanded: toggle?.getAttribute("aria-expanded") === "true",
navigationCollapsedClass: region?.classList.contains("nav-collapsed") === true,
title: (document.getElementById("workspace-title")?.textContent || "").trim(),
activeMarketTab: region?.dataset?.activeMarketTab || null,
currentMenuItemCount: document.querySelectorAll(
"#workspace-navigation .workspace-nav-item[aria-current='page']").length,
marketTabs: marketTabs.map(button => ({
tabId: button.dataset.tabId,
label: (button.querySelector(".workspace-nav-label")?.textContent || "").trim(),
current: button.getAttribute("aria-current") || null,
focused: document.activeElement === button,
hasIcon: Boolean(button.querySelector("svg"))
})),
stock: {
hidden: stock?.hidden === true,
inert: stock?.hasAttribute("inert") === true,
ariaHidden: stock?.getAttribute("aria-hidden") || null,
tabCurrent: stockTab?.getAttribute("aria-current") || null,
width: stock?.getBoundingClientRect().width ?? null,
height: stock?.getBoundingClientRect().height ?? null
},
catalog: {
hidden: catalog?.hidden === true,
inert: catalog?.hasAttribute("inert") === true,
ariaHidden: catalog?.getAttribute("aria-hidden") || null,
width: catalog?.getBoundingClientRect().width ?? null,
height: catalog?.getBoundingClientRect().height ?? null
},
settings: {
hidden: settings?.hidden === true,
inert: settings?.hasAttribute("inert") === true,
ariaHidden: settings?.getAttribute("aria-hidden") || null,
tabCurrent: settingsTab?.getAttribute("aria-current") || null,
width: settings?.getBoundingClientRect().width ?? null,
height: settings?.getBoundingClientRect().height ?? null
}
};
})()`);
}
function workspaceLayoutMatches(layout, workspace, expanded = null) {
if (!layout || layout.activeWorkspace !== workspace) return false;
if (expanded !== null && layout.navigationExpanded !== expanded) return false;
if (layout.scheduleHitOwned !== true || layout.currentMenuItemCount !== 1 ||
layout.marketTabs?.length !== 10 ||
layout.marketTabs.some(tab => tab.hasIcon !== true)) return false;
const visible = view => view?.hidden === false && view.inert === false &&
view.ariaHidden === "false" && view.width > 0 && view.height > 0;
const hidden = view => view?.hidden === true && view.inert === true &&
view.ariaHidden === "true";
const currentMarkets = layout.marketTabs.filter(tab => tab.current === "page");
if (workspace === "stock") {
return visible(layout.stock) && hidden(layout.catalog) && hidden(layout.settings) &&
layout.stock.tabCurrent === "page" && layout.settings.tabCurrent === "false" &&
currentMarkets.length === 0;
}
if (workspace === "settings") {
return hidden(layout.stock) && hidden(layout.catalog) && visible(layout.settings) &&
layout.stock.tabCurrent === "false" && layout.settings.tabCurrent === "page" &&
currentMarkets.length === 0;
}
if (workspace === "catalog") {
return hidden(layout.stock) && visible(layout.catalog) && hidden(layout.settings) &&
layout.stock.tabCurrent === "false" && layout.settings.tabCurrent === "false" &&
currentMarkets.length === 1 && currentMarkets[0].tabId === layout.activeMarketTab;
}
return false;
}
async function waitForWorkspaceLayout(label, predicate) {
const deadline = Math.min(Date.now() + 5_000, hardDeadlineEpochMilliseconds);
let lastLayout = null;
while (Date.now() < deadline) {
lastLayout = await readWorkspaceLayout();
if (predicate(lastLayout)) return lastLayout;
await sleep(25);
}
failKnown(`Timed out waiting for ${label}. Last layout: ${JSON.stringify(lastLayout)}`);
}
function assertWorkspaceAndScheduleGeometry(layout, label) {
if (!layout?.shell || !layout.region || !layout.navigation || !layout.surface ||
!layout.schedule || layout.schedule.width <= 0 || layout.schedule.height <= 0 ||
layout.scheduleHitOwned !== true) {
failKnown(`${label} does not expose the complete workspace and fixed schedule geometry: ` +
JSON.stringify(layout));
}
const widthRatio = layout.region.width / layout.schedule.width;
const workspaceShare = layout.region.width / layout.shell.width;
if (!Number.isFinite(widthRatio) || widthRatio < 1.85 || widthRatio > 2.15 ||
!Number.isFinite(workspaceShare) || workspaceShare < 0.62 || workspaceShare > 0.70 ||
Math.abs(layout.schedule.right - layout.shell.right) > 1.5 ||
Math.abs(layout.schedule.top - layout.shell.top) > 1.5 ||
Math.abs(layout.schedule.bottom - layout.shell.bottom) > 1.5) {
failKnown(`${label} does not preserve the approximately 2:1 workspace/schedule split.`);
}
return { widthRatio, workspaceShare };
}
function assertFixedSchedule(baseline, current, label) {
for (const edge of ["left", "top", "right", "bottom", "width", "height"]) {
if (Math.abs(baseline.schedule[edge] - current.schedule[edge]) > 1.5) {
failKnown(`${label} moved or resized the always-visible schedule at ${edge}.`);
}
}
assertWorkspaceAndScheduleGeometry(current, label);
}
async function ensureWorkspace(workspace, label) {
let layout = await readWorkspaceLayout();
if (!workspaceLayoutMatches(layout, workspace)) {
let expression = null;
if (workspace === "stock") {
expression = 'document.getElementById("workspace-stock-tab")';
} else if (workspace === "settings") {
expression = 'document.getElementById("workspace-settings-tab")';
} else if (workspace === "catalog") {
const activeTabId = layout.activeMarketTab || layout.marketTabs?.[0]?.tabId;
if (activeTabId) {
expression = `document.querySelector('#market-tabs > button[data-tab-id="${activeTabId}"]')`;
}
}
if (!expression) failKnown(`${label} requested an unknown workspace.`);
await pointerClick(expression, label);
layout = await waitForWorkspaceLayout(label, candidate =>
workspaceLayoutMatches(candidate, workspace));
}
assertWorkspaceAndScheduleGeometry(layout, label);
return layout;
}
async function verifyWorkspaceNavigationAndLayout(preflight) {
const baseline = await ensureWorkspace("stock", "workspace stock preflight");
if (baseline.navigationExpanded !== true || baseline.navigationCollapsedClass === true) {
failKnown("The workspace menu was not initially expanded.");
}
const firstMarket = baseline.marketTabs[0];
if (!firstMarket?.tabId || !firstMarket.label) {
failKnown("The first graphic menu item is missing its native identity or label.");
}
const baselineRatio = assertWorkspaceAndScheduleGeometry(
baseline,
"expanded stock workspace preflight");
await pointerClick(
'document.getElementById("workspace-nav-toggle")',
"collapse workspace menu");
const collapsed = await waitForWorkspaceLayout("collapsed workspace menu", layout =>
workspaceLayoutMatches(layout, "stock", false) &&
layout.navigationCollapsedClass === true);
if (collapsed.navigation.width >= baseline.navigation.width - 100 ||
collapsed.surface.width <= baseline.surface.width + 100) {
failKnown("The pointer collapse did not transfer menu width to the active workspace.");
}
assertFixedSchedule(baseline, collapsed, "collapsed workspace menu");
const beforeGraphicSelection = await readSnapshot();
assertSafety(beforeGraphicSelection, "first graphic native selection preflight", false);
await pointerClick(
`document.querySelector('#market-tabs > button[data-tab-id="${firstMarket.tabId}"]')`,
"select first graphic screen from collapsed menu");
const catalog = await waitForWorkspaceLayout("first graphic workspace", layout =>
workspaceLayoutMatches(layout, "catalog", false) &&
layout.title === firstMarket.label && layout.activeMarketTab === firstMarket.tabId);
assertFixedSchedule(baseline, catalog, "first graphic workspace transition");
let nativeState = await waitFor("first graphic native selection", snapshot =>
snapshot.stateCount >= beforeGraphicSelection.stateCount + 2 &&
snapshot.busyStateCount > beforeGraphicSelection.busyStateCount &&
snapshot.state.isBusy === false &&
snapshot.state.tabs?.some(tab => tab.id === firstMarket.tabId && tab.isActive) === true,
{ timeoutMilliseconds: 60_000, allowUiBusy: true });
await pointerClick(
'document.getElementById("workspace-nav-toggle")',
"expand workspace menu");
const expandedCatalog = await waitForWorkspaceLayout("expanded catalog workspace", layout =>
workspaceLayoutMatches(layout, "catalog", true) &&
layout.navigationCollapsedClass === false);
if (Math.abs(expandedCatalog.navigation.width - baseline.navigation.width) > 1.5 ||
Math.abs(expandedCatalog.surface.width - baseline.surface.width) > 1.5) {
failKnown("The pointer expand did not restore the original menu/workspace widths.");
}
assertFixedSchedule(baseline, expandedCatalog, "expanded catalog workspace");
const secondMarket = baseline.marketTabs[1];
if (!secondMarket?.tabId || secondMarket.tabId === firstMarket.tabId) {
failKnown("The second graphic menu item is unavailable for vertical drag verification.");
}
const swappedMarketTabs = await dragMarketTab(
firstMarket.tabId,
secondMarket.tabId,
"vertically reorder graphic menu items");
assertFixedSchedule(baseline, swappedMarketTabs.layout,
"vertical graphic menu reorder");
const restoredMarketTabs = await dragMarketTab(
firstMarket.tabId,
secondMarket.tabId,
"restore graphic menu item order");
assertFixedSchedule(baseline, restoredMarketTabs.layout,
"restored graphic menu order");
if (!restoredMarketTabs.layout.marketTabs.every(
(tab, index) => tab.tabId === baseline.marketTabs[index].tabId)) {
failKnown("The vertical graphic menu drag did not restore the original native order.");
}
const beforeSettingsMessageCount =
restoredMarketTabs.completed.safety.outboundMessages.length;
await pointerClick(
'document.getElementById("workspace-settings-tab")',
"open settings workspace");
const settings = await waitForWorkspaceLayout("settings workspace", layout =>
workspaceLayoutMatches(layout, "settings", true) && layout.title === "설정");
assertFixedSchedule(baseline, settings, "settings workspace transition");
nativeState = await readSnapshot();
assertSafety(nativeState, "settings workspace local transition", false);
if (nativeState.safety.outboundMessages.length !== beforeSettingsMessageCount) {
failKnown("The local Settings menu emitted a native WebView message.");
}
await pointerClick(
'document.getElementById("workspace-stock-tab")',
"return to stock workspace");
const restoredStock = await waitForWorkspaceLayout("restored stock workspace", layout =>
workspaceLayoutMatches(layout, "stock", true) && layout.title === "종목·컷");
assertFixedSchedule(baseline, restoredStock, "restored stock workspace");
if (await evaluate('document.activeElement?.id === "workspace-stock-tab"') !== true) {
failKnown("The pointer-selected stock menu item did not retain keyboard focus.");
}
await pressKey("ArrowDown", "focus first graphic menu item");
if (await evaluate(
`document.activeElement?.dataset?.tabId === ${JSON.stringify(firstMarket.tabId)}`) !== true) {
failKnown("ArrowDown did not move focus to the first graphic menu item.");
}
const focusOnlyStock = await readWorkspaceLayout();
if (!workspaceLayoutMatches(focusOnlyStock, "stock", true)) {
failKnown("ArrowDown changed the workspace before explicit keyboard activation.");
}
assertFixedSchedule(baseline, focusOnlyStock, "keyboard graphic focus");
const beforeKeyboardGraphicSelection = await readSnapshot();
assertSafety(beforeKeyboardGraphicSelection,
"keyboard graphic native selection preflight", false);
await pressKey("Enter", "activate first graphic screen from keyboard");
const keyboardCatalog = await waitForWorkspaceLayout("keyboard graphic workspace", layout =>
workspaceLayoutMatches(layout, "catalog", true) &&
layout.title === firstMarket.label && layout.activeMarketTab === firstMarket.tabId);
assertFixedSchedule(baseline, keyboardCatalog, "keyboard graphic activation");
await waitFor("keyboard graphic native selection", snapshot =>
snapshot.stateCount >= beforeKeyboardGraphicSelection.stateCount + 2 &&
snapshot.busyStateCount > beforeKeyboardGraphicSelection.busyStateCount &&
snapshot.state.isBusy === false &&
snapshot.state.tabs?.some(tab => tab.id === firstMarket.tabId && tab.isActive) === true,
{ timeoutMilliseconds: 60_000, allowUiBusy: true });
await pressKey("ArrowUp", "focus stock workspace menu item");
if (await evaluate('document.activeElement?.id === "workspace-stock-tab"') !== true) {
failKnown("ArrowUp did not return focus to the stock workspace menu item.");
}
await pressKey("Enter", "activate stock workspace from keyboard");
const keyboardStock = await waitForWorkspaceLayout("keyboard stock workspace", layout =>
workspaceLayoutMatches(layout, "stock", true) && layout.title === "종목·컷");
assertFixedSchedule(baseline, keyboardStock, "keyboard stock activation");
await pressKey("ArrowUp", "wrap focus to settings menu item");
if (await evaluate('document.activeElement?.id === "workspace-settings-tab"') !== true) {
failKnown("ArrowUp did not wrap focus from Stock to Settings.");
}
const focusOnlyStockBeforeSettings = await readWorkspaceLayout();
if (!workspaceLayoutMatches(focusOnlyStockBeforeSettings, "stock", true)) {
failKnown("Focusing Settings changed the workspace before keyboard activation.");
}
await pressKey("Enter", "activate settings workspace from keyboard");
const keyboardSettings = await waitForWorkspaceLayout("keyboard settings workspace", layout =>
workspaceLayoutMatches(layout, "settings", true) && layout.title === "설정");
assertFixedSchedule(baseline, keyboardSettings, "keyboard settings activation");
await pointerClick(
'document.getElementById("workspace-stock-tab")',
"restore stock workspace after keyboard navigation");
const finalStock = await waitForWorkspaceLayout("final stock workspace", layout =>
workspaceLayoutMatches(layout, "stock", true) && layout.title === "종목·컷");
assertFixedSchedule(baseline, finalStock, "final stock workspace");
const after = await readSnapshot();
assertSafety(after, "workspace navigation/layout input", false);
const navigationMessages = after.safety.outboundMessages.slice(
preflight.safety.outboundMessages.length);
const selectTabMessages = navigationMessages.filter(
message => message.type === "select-tab");
const hoverSwapMessages = navigationMessages.filter(
message => message.type === "hover-swap-tabs");
if (navigationMessages.length !== 4 || selectTabMessages.length !== 2 ||
hoverSwapMessages.length !== 2 ||
navigationMessages.some(message =>
message.type !== "select-tab" && message.type !== "hover-swap-tabs") ||
after.safety.blockedOutboundMessages.length !==
preflight.safety.blockedOutboundMessages.length) {
failKnown("Sidebar navigation emitted duplicate or unexpected native messages.");
}
await checkpoint("workspace-navigation-layout", {
widthRatio: Number(baselineRatio.widthRatio.toFixed(4)),
workspaceShare: Number(baselineRatio.workspaceShare.toFixed(4)),
expandedNavigationWidth: baseline.navigation.width,
collapsedNavigationWidth: collapsed.navigation.width,
schedule: baseline.schedule,
graphicMenuCount: baseline.marketTabs.length,
pointerTransitions: ["collapse", firstMarket.tabId, "expand", "settings", "stock"],
verticalMenuDrag: [
`${firstMarket.tabId} -> ${secondMarket.tabId}`,
`${firstMarket.tabId} -> ${secondMarket.tabId} (restore)`
],
keyboardTransitions: [
"ArrowDown", `Enter ${firstMarket.tabId}`, "ArrowUp", "Enter stock",
"ArrowUp wrap", "Enter settings"
],
nativeNavigationMessages: navigationMessages.map(message => message.type)
});
}
async function playlistDropGeometry(label) {
const geometry = await evaluate(`(() => {
const zone = document.getElementById("playlist-drop-zone");
if (!zone) return null;
const zoneRectangle = zone.getBoundingClientRect();
const headerRectangle = zone.querySelector(".playlist-header")
?.getBoundingClientRect() || null;
const viewport = {
width: document.documentElement.clientWidth,
height: document.documentElement.clientHeight
};
const inset = 8;
const left = Math.max(0, zoneRectangle.left + inset);
const right = Math.min(viewport.width - 1, zoneRectangle.right - inset);
const top = Math.max(
0,
(headerRectangle ? headerRectangle.bottom : zoneRectangle.top) + inset);
const bottom = Math.min(viewport.height - 1, zoneRectangle.bottom - inset);
const fractions = [0.5, 0.25, 0.75];
const points = right > left && bottom > top
? fractions.map(fraction => {
const x = left + (right - left) / 2;
const y = top + (bottom - top) * fraction;
const hit = document.elementFromPoint(x, y);
return {
x,
y,
hitId: hit?.id || null,
hitClass: hit?.className || null,
insideDropZone: hit === zone || zone.contains(hit)
};
})
: [];
return {
zone: {
left: zoneRectangle.left,
top: zoneRectangle.top,
right: zoneRectangle.right,
bottom: zoneRectangle.bottom,
width: zoneRectangle.width,
height: zoneRectangle.height
},
headerBottom: headerRectangle?.bottom ?? null,
viewport,
visibleInterior: { left, top, right, bottom },
points
};
})()`);
if (!geometry || geometry.zone.width <= 0 || geometry.zone.height <= 0 ||
geometry.points.length === 0 ||
geometry.points.some(point => !Number.isFinite(point.x) ||
!Number.isFinite(point.y) || point.insideDropZone !== true)) {
failKnown(`${label} has no verified visible interior drop point.`);
}
return geometry;
}
async function moveMouse(point, modifiers = 0, buttons = 0) {
await rpc("Input.dispatchMouseEvent", {
type: "mouseMoved",
x: point.x,
y: point.y,
// Edge releases explicit pointer capture if a CDP mouseMoved event says
// no button while its buttons bitmask says the left button is held.
// Keep both fields consistent with the physical state during a drag.
button: (buttons & 1) === 1 ? "left" : "none",
buttons,
modifiers,
pointerType: "mouse"
});
}
async function pressMouse(point, modifiers = 0) {
if (pointerIsPressed) failKnown("A second pointer press was attempted before release.");
await moveMouse(point, modifiers, 0);
await rpc("Input.dispatchMouseEvent", {
type: "mousePressed",
x: point.x,
y: point.y,
button: "left",
buttons: 1,
clickCount: 1,
modifiers,
pointerType: "mouse"
});
pointerIsPressed = true;
pointerReleasePoint = point;
pointerReleaseModifiers = modifiers;
}
async function releaseMouse(point, modifiers = 0) {
if (!pointerIsPressed) failKnown("A pointer release was attempted without a press.");
await rpc("Input.dispatchMouseEvent", {
type: "mouseReleased",
x: point.x,
y: point.y,
button: "left",
buttons: 0,
clickCount: 1,
modifiers,
pointerType: "mouse"
});
pointerIsPressed = false;
pointerReleasePoint = null;
pointerReleaseModifiers = 0;
}
async function cleanupDragInputSession(cancelActiveDrag = true) {
const failures = [];
const point = pointerReleasePoint || { x: 1, y: 1 };
if (cancelActiveDrag && activeInterceptedDragData) {
try {
await rpc("Input.dispatchDragEvent", {
type: "dragCancel",
x: point.x,
y: point.y,
data: activeInterceptedDragData
});
activeInterceptedDragData = null;
} catch (error) {
failures.push(`drag cancel: ${error.message}`);
}
}
if (pointerIsPressed) {
try {
await releaseMouse(point, pointerReleaseModifiers);
} catch (error) {
failures.push(`pointer release: ${error.message}`);
}
}
if (dragInterceptionEnabled) {
try {
await rpc("Input.setInterceptDrags", { enabled: false });
dragInterceptionEnabled = false;
} catch (error) {
failures.push(`drag interception disable: ${error.message}`);
}
}
if (failures.length > 0) {
failUnknown(`Input cleanup did not complete (${failures.join("; ")}).`);
}
}
async function waitForPointerReady(
label,
allowLegacyDialog = false,
allowActiveDryRun = false) {
await waitFor(`${label} pointer readiness`, snapshot =>
snapshot.state?.isBusy === false && snapshot.dom.bodyBusy === "false",
{
allowUiBusy: true,
allowLegacyDialog,
allowActiveDryRun
});
}
async function pointerClick(expression, label, modifiers = 0, options = {}) {
if (options.skipPointerReady !== true) {
await waitForPointerReady(
label,
options.allowLegacyDialog === true,
options.allowActiveDryRun === true);
}
const forbiddenId = await evaluate(`(() => {
const element = ${expression};
return element?.closest?.("button")?.id || element?.id || null;
})()`);
if (forbiddenControlIds.includes(forbiddenId)) {
failKnown(`The harness refused forbidden control #${forbiddenId}.`);
}
const point = await elementGeometry(expression, label);
evidence.inputs.push({ type: "pointer-click", label, modifiers, point });
await pressMouse(point, modifiers);
await sleep(25);
await releaseMouse(point, modifiers);
return point;
}
async function dragMarketTab(sourceTabId, targetTabId, label) {
await waitForPointerReady(label);
const beforeLayout = await readWorkspaceLayout();
const beforeOrder = beforeLayout.marketTabs.map(tab => tab.tabId);
const sourceIndex = beforeOrder.indexOf(sourceTabId);
const targetIndex = beforeOrder.indexOf(targetTabId);
if (sourceIndex < 0 || targetIndex < 0 || sourceIndex === targetIndex) {
failKnown(`${label} does not have two distinct visible market menu items.`);
}
const expectedOrder = beforeOrder.slice();
expectedOrder[sourceIndex] = targetTabId;
expectedOrder[targetIndex] = sourceTabId;
const source = await elementGeometry(
`document.querySelector('#market-tabs > button[data-tab-id="${sourceTabId}"]')`,
`${label} source`);
const target = await elementGeometry(
`document.querySelector('#market-tabs > button[data-tab-id="${targetTabId}"]')`,
`${label} target`);
const before = await readSnapshot();
assertSafety(before, `${label} preflight`, false);
evidence.inputs.push({
type: "market-tab-drag",
label,
sourceTabId,
targetTabId,
source,
target,
beforeOrder,
expectedOrder
});
let dragData = null;
let dragCompleted = false;
let operationError = null;
let intercepted = null;
// Record the attempt before awaiting the acknowledgement. If Chromium
// applies the command but its response is lost, bounded outer cleanup must
// still send and confirm the idempotent disable command once.
dragInterceptionEnabled = true;
try {
await rpc("Input.setInterceptDrags", { enabled: true });
intercepted = waitForDragIntercept(label);
await pressMouse(source, 0);
pointerReleasePoint = source;
const direction = target.y >= source.y ? 1 : -1;
await moveMouse({ x: source.x, y: source.y + direction * 10 }, 0, 1);
dragData = await intercepted;
activeInterceptedDragData = dragData;
const marketItem = dragData?.items?.find(item => item.mimeType === "text/x-mbn-tab");
if (!marketItem || marketItem.data !== sourceTabId) {
failKnown(`${label} did not preserve the closed market identity in drag data.`);
}
await rpc("Input.dispatchDragEvent", {
type: "dragEnter",
x: target.x,
y: target.y,
data: dragData
});
await rpc("Input.dispatchDragEvent", {
type: "dragOver",
x: target.x,
y: target.y,
data: dragData
});
await sleep(100);
await rpc("Input.dispatchDragEvent", {
type: "drop",
x: target.x,
y: target.y,
data: dragData
});
activeInterceptedDragData = null;
dragCompleted = true;
await releaseMouse(target, 0);
} catch (error) {
operationError = error;
throw error;
} finally {
if (pendingDragIntercept) {
cancelPendingDragIntercept(
`${label} ended before Chromium supplied intercepted drag data.`);
}
if (intercepted) await intercepted.catch(() => null);
try {
await cleanupDragInputSession(!dragCompleted);
} catch (cleanupError) {
if (!operationError) throw cleanupError;
// Keep the original operation failure. The outer cleanup switches to a
// bounded cleanup mode and makes one final release/disable attempt.
}
}
const completed = await waitFor(`${label} native swap`, snapshot => {
const order = snapshot.state.tabs?.map(tab => tab.id) || [];
return snapshot.safety.outboundMessages.length ===
before.safety.outboundMessages.length + 1 &&
snapshot.safety.outboundMessages.at(-1)?.type === "hover-swap-tabs" &&
order.length === expectedOrder.length &&
order.every((tabId, index) => tabId === expectedOrder[index]);
}, { timeoutMilliseconds: 60_000, allowUiBusy: true });
const completedLayout = await waitForWorkspaceLayout(`${label} menu order`, layout =>
layout.marketTabs.length === expectedOrder.length &&
layout.marketTabs.every((tab, index) => tab.tabId === expectedOrder[index]));
assertWorkspaceAndScheduleGeometry(completedLayout, `${label} menu order`);
return { completed, layout: completedLayout, beforeOrder, expectedOrder };
}
const keyDefinitions = Object.freeze({
ArrowDown: { key: "ArrowDown", code: "ArrowDown", virtualKey: 40 },
ArrowUp: { key: "ArrowUp", code: "ArrowUp", virtualKey: 38 },
Backspace: { key: "Backspace", code: "Backspace", virtualKey: 8 },
End: { key: "End", code: "End", virtualKey: 35 },
Enter: { key: "Enter", code: "Enter", virtualKey: 13 },
Escape: { key: "Escape", code: "Escape", virtualKey: 27 },
Home: { key: "Home", code: "Home", virtualKey: 36 },
Space: { key: " ", code: "Space", virtualKey: 32 },
Tab: { key: "Tab", code: "Tab", virtualKey: 9 },
SelectAll: { key: "a", code: "KeyA", virtualKey: 65 }
});
async function pressKey(definitionName, label, modifiers = 0) {
const definition = keyDefinitions[definitionName];
if (!definition) failKnown(`Unknown key definition ${definitionName}.`);
if (new Set(["F2", "F3", "F8"]).has(definition.key)) {
failKnown(`The harness refused playout shortcut ${definition.key}.`);
}
if (definition.key === "Escape") {
const modalState = await evaluate(`(() => ({
named: document.getElementById("named-playlist-modal")?.hidden === false,
confirmation: document.getElementById("named-playlist-confirmation")?.hidden === false
}))()`);
if (!modalState?.named && !modalState?.confirmation) {
failKnown("Escape is allowed only while the named-playlist modal owns input.");
}
}
evidence.inputs.push({
type: "key",
label,
key: definition.key,
modifiers
});
const common = {
key: definition.key,
code: definition.code,
windowsVirtualKeyCode: definition.virtualKey,
nativeVirtualKeyCode: definition.virtualKey,
modifiers
};
await rpc("Input.dispatchKeyEvent", { type: "keyDown", ...common });
await rpc("Input.dispatchKeyEvent", { type: "keyUp", ...common });
}
async function setSearchText(text) {
await ensureWorkspace("stock", "show stock workspace before stock search");
await pointerClick(
'document.getElementById("stock-search")',
"focus stock search");
await pressKey("SelectAll", "select existing stock query", ctrlModifier);
await pressKey("Backspace", "clear existing stock query");
evidence.inputs.push({ type: "text", label: "stock query", text });
await rpc("Input.insertText", { text });
}
async function replaceText(expression, text, label) {
await pointerClick(expression, `focus ${label}`);
const focused = await evaluate(`document.activeElement === (${expression})`);
if (focused !== true) failKnown(`${label} did not receive pointer focus.`);
await pressKey("SelectAll", `select existing ${label}`, ctrlModifier);
await pressKey("Backspace", `clear existing ${label}`);
evidence.inputs.push({ type: "text", label, text });
await rpc("Input.insertText", { text });
const actual = await evaluate(`(${expression})?.value ?? null`);
if (actual !== text) {
failKnown(`${label} did not retain the exact typed value.`);
}
}
async function selectOptionValue(expression, value, label) {
await pointerClick(expression, `focus ${label}`);
const selection = await evaluate(`(() => {
const select = ${expression};
if (!(select instanceof HTMLSelectElement)) return null;
const targetIndex = [...select.options].findIndex(option => option.value === ${JSON.stringify(value)});
return { targetIndex, currentValue: select.value };
})()`);
if (!selection || selection.targetIndex < 0) {
failKnown(`${label} does not contain the requested option.`);
}
await pressKey("Home", `${label} first option`);
for (let index = 0; index < selection.targetIndex; index++) {
await pressKey("ArrowDown", `${label} next option ${index + 1}`);
}
await pressKey("Enter", `${label} commit option`);
const actual = await evaluate(`(${expression})?.value ?? null`);
if (actual !== value) failKnown(`${label} did not select the requested value.`);
}
async function respondToNativeDialog(label, confirmation, accept) {
const opened = await waitFor(`${label} dialog`, snapshot =>
snapshot.state?.dialog?.isConfirmation === confirmation &&
snapshot.dom.legacyDialogHidden === false,
{
timeoutMilliseconds: 30_000,
allowUiBusy: true,
allowLegacyDialog: true
});
if (confirmation && accept !== true && accept !== false) {
failKnown(`${label} confirmation response is not explicit.`);
}
await pointerClick(
confirmation && accept === false
? 'document.getElementById("legacy-dialog-no")'
: 'document.getElementById("legacy-dialog-ok")',
`${label} ${confirmation ? (accept ? "yes" : "no") : "ok"}`,
0,
{ allowLegacyDialog: true });
await waitFor(`${label} dialog close`, snapshot =>
snapshot.state?.dialog == null && snapshot.dom.legacyDialogHidden === true,
{
timeoutMilliseconds: 60_000,
allowUiBusy: true,
allowLegacyDialog: true
});
return opened;
}
async function pointerDoubleClick(expression, label, modifiers = 0) {
await waitForPointerReady(label);
const point = await elementGeometry(expression, label);
evidence.inputs.push({ type: "pointer-double-click", label, modifiers, point });
await moveMouse(point, modifiers, 0);
for (const clickCount of [1, 2]) {
await rpc("Input.dispatchMouseEvent", {
type: "mousePressed",
x: point.x,
y: point.y,
button: "left",
buttons: 1,
clickCount,
modifiers,
pointerType: "mouse"
});
await rpc("Input.dispatchMouseEvent", {
type: "mouseReleased",
x: point.x,
y: point.y,
button: "left",
buttons: 0,
clickCount,
modifiers,
pointerType: "mouse"
});
if (clickCount === 1) await sleep(50);
}
}
async function pointerClickWithJavaScriptDialog(
expression,
label,
type,
message,
accept) {
if (expectedJavaScriptDialog) {
failUnknown(`Another JavaScript dialog was pending before ${label}.`);
}
await waitForPointerReady(label);
let resolveDialog;
let rejectDialog;
const handled = new Promise((resolve, reject) => {
resolveDialog = resolve;
rejectDialog = reject;
});
expectedJavaScriptDialog = {
type,
message,
accept: accept === true,
resolve: resolveDialog,
reject: rejectDialog
};
try {
await pointerClick(expression, label, 0, { skipPointerReady: true });
await Promise.race([
handled,
new Promise((_, reject) => setTimeout(
() => reject(new HarnessFailure(
"OUTCOME_UNKNOWN",
`Timed out waiting for the JavaScript dialog at ${label}.`)),
10_000))
]);
} catch (error) {
if (expectedJavaScriptDialog) expectedJavaScriptDialog = null;
throw error;
}
}
function selectedPhysicalIndices(snapshot) {
return snapshot.state.cutRows
.filter(row => row.isSelected)
.map(row => row.physicalIndex);
}
function selectedPlaylistIds(snapshot) {
return snapshot.state.playlist
.filter(row => row.isSelected)
.map(row => row.rowId);
}
function activePlaylistId(snapshot) {
return snapshot.state.playlist.find(row => row.isActive)?.rowId || null;
}
function focusedCutIndex(snapshot) {
return snapshot.state.cutRows.find(row => row.isFocused)?.physicalIndex ?? null;
}
function assertExactArray(actual, expected, label) {
if (actual.length !== expected.length ||
actual.some((value, index) => value !== expected[index])) {
failKnown(`${label}: expected ${JSON.stringify(expected)}, received ${JSON.stringify(actual)}.`);
}
}
function cssDragSize(snapshot) {
const metrics = snapshot.state.interactionMetrics;
const devicePixelRatio = snapshot.devicePixelRatio;
if (!metrics || metrics.coordinateSpace !== "host-dip" ||
!Number.isInteger(metrics.cutDragWidthDips) || metrics.cutDragWidthDips <= 0 ||
!Number.isInteger(metrics.cutDragHeightDips) || metrics.cutDragHeightDips <= 0) {
failKnown("Valid host-DIP cut drag metrics are missing.");
}
const hostScale = Number.isFinite(metrics.hostRasterizationScale) &&
metrics.hostRasterizationScale > 0 ? metrics.hostRasterizationScale : 1;
const deviceScale = Number.isFinite(devicePixelRatio) && devicePixelRatio > 0
? devicePixelRatio : hostScale;
const candidateZoom = deviceScale / hostScale;
const browserZoom = Number.isFinite(candidateZoom) &&
candidateZoom >= 0.25 && candidateZoom <= 5 ? candidateZoom : 1;
return {
width: metrics.cutDragWidthDips / browserZoom,
height: metrics.cutDragHeightDips / browserZoom
};
}
async function dragCutsToPlaylist(physicalIndex, modifiers, expectedAppendCount, label) {
const before = await readSnapshot();
assertSafety(before, `${label} preflight`, false);
const beforeCount = before.state.playlist.length;
const beforeRevision = before.state.revision;
const sourceExpression =
`document.querySelector('#cut-list .cut-row[data-physical-index="${physicalIndex}"]')`;
const source = await elementGeometry(sourceExpression, `${label} source`);
const dropGeometry = await playlistDropGeometry(`${label} target`);
const dragInput = {
type: "cut-drag",
label,
physicalIndex,
modifiers,
source,
dropGeometry,
targetSamples: [],
acceptedTarget: null,
expectedAppendCount
};
evidence.inputs.push(dragInput);
await pressMouse(source, modifiers);
try {
const held = await waitFor(`${label} pointer-down receipt`, snapshot =>
snapshot.state.revision > beforeRevision &&
focusedCutIndex(snapshot) === physicalIndex,
{ allowUiBusy: false });
const dragSize = cssDragSize(held);
const inside = {
x: source.x - dragSize.width / 2,
y: source.y - dragSize.height / 2
};
const threshold = {
x: Math.min(source.right - 4, source.x + Math.max(8, dragSize.width + 2)),
y: source.y
};
pointerReleasePoint = source;
pointerReleaseModifiers = modifiers;
await moveMouse(inside, modifiers, 1);
await sleep(30);
const beforeThreshold = await readSnapshot();
const sourceBeforeThreshold = beforeThreshold.dom.cuts.find(row =>
row.physicalIndex === physicalIndex);
if (!sourceBeforeThreshold || sourceBeforeThreshold.dragging ||
beforeThreshold.dom.cutDropHighlighted ||
beforeThreshold.dom.cutDragFeedback?.hidden !== true) {
failKnown(`${label} started inside the original drag rectangle.`);
}
await moveMouse(threshold, modifiers, 1);
await sleep(50);
const thresholdSnapshot = await readSnapshot();
const thresholdSource = thresholdSnapshot.dom.cuts.find(row =>
row.physicalIndex === physicalIndex);
const capturedAtThreshold = thresholdSnapshot.dom.cutPointerCapture
?.physicalIndices?.includes(physicalIndex) === true;
dragInput.threshold = {
point: threshold,
dragging: thresholdSource?.dragging === true,
captured: capturedAtThreshold,
feedback: thresholdSnapshot.dom.cutDragFeedback,
pointerEvents: thresholdSnapshot.pointerEvents.slice(-12)
};
if (!thresholdSource?.dragging) {
failKnown(
`${label} did not cross the Windows drag threshold on the source row; ` +
`pointer diagnostics: ${JSON.stringify(dragInput.threshold.pointerEvents)}.`);
}
if (!capturedAtThreshold) {
failKnown(
`${label} crossed the drag threshold but WebView2 did not retain pointer ` +
`capture on the source row; pointer diagnostics: ` +
`${JSON.stringify(dragInput.threshold.pointerEvents)}.`);
}
if (thresholdSnapshot.dom.cutDropHighlighted) {
failKnown(`${label} highlighted the remote drop zone while still on the source row.`);
}
const thresholdFeedback = thresholdSnapshot.dom.cutDragFeedback;
if (!thresholdFeedback || thresholdFeedback.hidden ||
thresholdFeedback.dropAllowed !== "false" ||
!Number.isFinite(thresholdFeedback.left) ||
!Number.isFinite(thresholdFeedback.top) ||
Math.abs(thresholdFeedback.left - threshold.x) > 280 ||
Math.abs(thresholdFeedback.top - threshold.y) > 80) {
failKnown(`${label} did not show blocked drag feedback beside the real pointer.`);
}
let overTarget = null;
let acceptedTarget = null;
for (const candidate of dropGeometry.points) {
await moveMouse(candidate, modifiers, 1);
await sleep(50);
const sample = await readSnapshot();
const sourceAtCandidate = sample.dom.cuts.find(row =>
row.physicalIndex === physicalIndex);
const sampleEvidence = {
point: candidate,
dragging: sourceAtCandidate?.dragging === true,
highlighted: sample.dom.cutDropHighlighted,
feedback: sample.dom.cutDragFeedback,
captured: sample.dom.cutPointerCapture?.physicalIndices
?.includes(physicalIndex) === true,
pointerEvents: sample.pointerEvents.slice(-12)
};
dragInput.targetSamples.push(sampleEvidence);
if (sampleEvidence.dragging && sampleEvidence.highlighted &&
sampleEvidence.captured && sampleEvidence.feedback?.hidden === false &&
sampleEvidence.feedback?.dropAllowed === "true") {
overTarget = sample;
acceptedTarget = candidate;
break;
}
}
if (!overTarget || !acceptedTarget) {
failKnown(
`${label} did not enter the real captured-pointer drop state; samples: ` +
`${JSON.stringify(dragInput.targetSamples)}.`);
}
dragInput.acceptedTarget = acceptedTarget;
await releaseMouse(acceptedTarget, modifiers);
} finally {
if (pointerIsPressed) {
const safePoint = pointerReleasePoint || source;
await releaseMouse(safePoint, pointerReleaseModifiers);
}
}
const completed = await waitFor(`${label} append`, snapshot =>
snapshot.state.isBusy !== true &&
snapshot.state.playlist.length === beforeCount + expectedAppendCount,
{ allowUiBusy: false });
if (!completed.dom.cuts.every(row => !row.dragging) ||
completed.dom.cutDropHighlighted ||
completed.dom.cutDragFeedback?.hidden !== true) {
failKnown(`${label} left drag visuals active after release.`);
}
return completed;
}
async function rowCellGeometry(rowId, label) {
return await elementGeometry(`(() => {
const row = Array.from(document.querySelectorAll(
"#playlist-rows .playlist-data-row"))
.find(candidate => candidate.dataset.rowId === ${JSON.stringify(rowId)});
return row?.children?.item(3) || null;
})()`, label);
}
async function playlistRowHeaderGeometry(rowId, label, verticalFraction = 0.5) {
return await elementGeometry(`(() => {
const row = Array.from(document.querySelectorAll(
"#playlist-rows .playlist-data-row"))
.find(candidate => candidate.dataset.rowId === ${JSON.stringify(rowId)});
return row?.querySelector(".playlist-row-header") || null;
})()`, label, verticalFraction);
}
async function playlistCheckboxGeometry(rowId, label) {
return await elementGeometry(`(() => {
const row = Array.from(document.querySelectorAll(
"#playlist-rows .playlist-data-row"))
.find(candidate => candidate.dataset.rowId === ${JSON.stringify(rowId)});
return row?.querySelector("input[type='checkbox']") || null;
})()`, label);
}
async function pointerClickPlaylistRow(rowId, label, modifiers = 0) {
const point = await rowCellGeometry(rowId, label);
evidence.inputs.push({ type: "pointer-click", label, modifiers, point, rowId });
await pressMouse(point, modifiers);
await sleep(25);
await releaseMouse(point, modifiers);
}
async function dragPlaylistRow(sourceRowId, targetRowId, label) {
const source = await playlistRowHeaderGeometry(sourceRowId, `${label} source`);
const target = await playlistRowHeaderGeometry(targetRowId, `${label} target`, 0.75);
evidence.inputs.push({
type: "playlist-row-drag",
label,
sourceRowId,
targetRowId,
position: "after",
source,
target
});
await pressMouse(source, 0);
try {
pointerReleasePoint = source;
await moveMouse({ x: source.x + 8, y: source.y }, 0, 1);
await sleep(50);
await moveMouse({
x: (source.x + target.x) / 2,
y: (source.y + target.y) / 2
}, 0, 1);
await sleep(50);
await moveMouse(target, 0, 1);
await sleep(75);
const overTarget = await readSnapshot();
const sourceDom = overTarget.dom.playlist.find(row => row.rowId === sourceRowId);
const targetDom = overTarget.dom.playlist.find(row => row.rowId === targetRowId);
const captured = overTarget.dom.playlistPointerCapture?.rowIds
?.includes(sourceRowId) === true;
if (!sourceDom?.dragging || !targetDom?.dragAfter || !captured) {
failKnown(`${label} did not produce the captured-pointer row-header drag state.`);
}
await releaseMouse(target, 0);
} finally {
if (pointerIsPressed) {
await releaseMouse(pointerReleasePoint || source, 0);
}
}
}
async function attemptCheckboxOriginDrag(sourceRowId, targetRowId, label) {
const source = await playlistCheckboxGeometry(sourceRowId, `${label} checkbox`);
const target = await playlistRowHeaderGeometry(targetRowId, `${label} target`, 0.75);
evidence.inputs.push({
type: "playlist-checkbox-drag-attempt",
label,
sourceRowId,
targetRowId,
source,
target
});
await pressMouse(source, 0);
try {
pointerReleasePoint = source;
await moveMouse({ x: source.x + 8, y: source.y }, 0, 1);
await sleep(50);
await moveMouse(target, 0, 1);
await sleep(75);
const during = await readSnapshot();
if (during.dom.playlist.some(row =>
row.dragging || row.dragBefore || row.dragAfter)) {
failKnown(`${label} exposed a row drag visual from a checkbox origin.`);
}
await releaseMouse(target, 0);
} finally {
if (pointerIsPressed) {
await releaseMouse(pointerReleasePoint || source, 0);
}
}
}
function readSmallJsonFile(filePath, label) {
let stat;
try {
stat = fs.lstatSync(filePath);
} catch (_) {
failKnown(`${label} is unavailable.`);
}
if (!stat.isFile() || stat.isSymbolicLink() || stat.size <= 0 || stat.size > 16_384) {
failKnown(`${label} is not a small regular file.`);
}
try {
const bytes = fs.readFileSync(filePath);
if (bytes.length !== stat.size) failKnown(`${label} changed while it was read.`);
return {
value: JSON.parse(bytes.toString("utf8")),
bytes
};
} catch (_) {
failKnown(`${label} is not valid UTF-8 JSON.`);
}
}
async function requestWindowsInput(
current,
sourceRowId,
targetRowId,
expectedFirstRowId,
label) {
assertSafety(current, `${label} preflight`, false);
const preflightSignature = JSON.stringify({
revision: current.state.revision,
order: current.state.playlist.map(row => row.rowId),
selected: selectedPlaylistIds(current),
active: activePlaylistId(current),
focused: current.dom.activeElement?.playlistRowId ?? null
});
await sleep(350);
let stabilized = await readSnapshot();
assertSafety(stabilized, `${label} stabilized preflight`, false);
const stabilizedSignature = JSON.stringify({
revision: stabilized.state.revision,
order: stabilized.state.playlist.map(row => row.rowId),
selected: selectedPlaylistIds(stabilized),
active: activePlaylistId(stabilized),
focused: stabilized.dom.activeElement?.playlistRowId ?? null
});
if (stabilizedSignature !== preflightSignature) {
failKnown(`${label} state changed before the one-shot request was published.`);
}
current = stabilized;
const source = await playlistRowHeaderGeometry(sourceRowId, `${label} source`);
const target = await playlistRowHeaderGeometry(targetRowId, `${label} target`, 0.75);
await sleep(100);
const geometryGuard = await readSnapshot();
const guardedSource = await playlistRowHeaderGeometry(
sourceRowId, `${label} guarded source`);
const guardedTarget = await playlistRowHeaderGeometry(
targetRowId, `${label} guarded target`, 0.75);
const geometryStable = ["x", "y", "left", "top", "right", "bottom"]
.every(key => Math.abs(source[key] - guardedSource[key]) <= 0.5 &&
Math.abs(target[key] - guardedTarget[key]) <= 0.5);
const guardSignature = JSON.stringify({
revision: geometryGuard.state.revision,
order: geometryGuard.state.playlist.map(row => row.rowId),
selected: selectedPlaylistIds(geometryGuard),
active: activePlaylistId(geometryGuard),
focused: geometryGuard.dom.activeElement?.playlistRowId ?? null
});
if (!geometryStable || guardSignature !== stabilizedSignature) {
failKnown(`${label} DOM geometry or native state changed before publication.`);
}
current = geometryGuard;
const token = randomBytes(16).toString("hex").toUpperCase();
const beforeOrder = current.state.playlist.map(row => row.rowId);
const afterOrder = beforeOrder.filter(rowId => rowId !== sourceRowId);
const targetIndex = afterOrder.indexOf(targetRowId);
if (targetIndex < 0 || sourceRowId === targetRowId ||
new Set(beforeOrder).size !== beforeOrder.length) {
failKnown(`${label} row identities are not safe for a one-row move.`);
}
afterOrder.splice(targetIndex + 1, 0, sourceRowId);
if (afterOrder[0] !== expectedFirstRowId) {
failKnown(`${label} computed an unexpected Home boundary row.`);
}
const request = {
schemaVersion: 1,
token,
targetUrl: exactTargetUrl,
operation: "row-header-drag-after-then-home",
sourceRowId,
targetRowId,
position: "after",
source: { x: source.x, y: source.y },
target: { x: target.x, y: target.y },
viewport: { ...current.dom.viewport },
devicePixelRatio: current.devicePixelRatio,
beforeOrder,
afterOrder,
expectedSelectedRowId: sourceRowId,
expectedActiveRowIdAfterHome: expectedFirstRowId,
expectedFocusedRowId: sourceRowId,
mouseDownCalls: 1,
mouseUpCalls: 1,
homeKeyCalls: 1
};
const requestBytes = Buffer.from(`${JSON.stringify(request, null, 2)}\n`, "utf8");
fs.writeFileSync(configuration.windowsInputRequestPath, requestBytes, { flag: "wx" });
evidence.windowsInput.requestSha256 = sha256(requestBytes);
evidence.inputs.push({
type: "windows-send-input-request",
label,
sourceRowId,
targetRowId,
position: "after"
});
while (!fs.existsSync(configuration.windowsInputAckPath)) {
await sleep(50);
}
const acknowledgement = readSmallJsonFile(
configuration.windowsInputAckPath,
"Windows input acknowledgement");
const ack = acknowledgement.value;
if (!ack || ack.schemaVersion !== 1 || ack.token !== token ||
ack.result !== "PASS" || ack.operation !== request.operation ||
ack.foregroundValidated !== true || ack.packageIdentityValidated !== true ||
ack.mouseDownCalls !== 1 || ack.mouseUpCalls !== 1 ||
ack.homeKeyCalls !== 1 || ack.inputRetryCount !== 0) {
failKnown("Windows input acknowledgement does not match the one-shot request.");
}
evidence.windowsInput.acknowledgementSha256 = sha256(acknowledgement.bytes);
const completed = await waitFor(label, snapshot =>
JSON.stringify(snapshot.state.playlist.map(row => row.rowId)) ===
JSON.stringify(afterOrder) &&
JSON.stringify(selectedPlaylistIds(snapshot)) === JSON.stringify([sourceRowId]) &&
activePlaylistId(snapshot) === expectedFirstRowId &&
snapshot.dom.activeElement?.playlistRowId === sourceRowId,
{
timeoutMilliseconds: Math.min(configuration.timeoutMilliseconds, 15_000),
allowUiBusy: false
});
evidence.windowsInput.result = "PASS";
evidence.windowsInput.beforeOrder = beforeOrder;
evidence.windowsInput.afterOrder = afterOrder;
evidence.windowsInput.activeRowIdAfterHome = expectedFirstRowId;
await checkpoint("windows-sendinput-row-header-drag-and-home", {
sourceRowId,
targetRowId,
beforeOrder,
afterOrder,
expectedFirstRowId
});
return completed;
}
async function installProbe() {
const token = `legacy-package-input-${Date.now()}-${Math.random().toString(16).slice(2)}`;
const installed = await evaluate(`(() => {
if (!window.chrome?.webview) {
throw new Error("WebView2 bridge is unavailable.");
}
if (globalThis.__legacyPackageInputProbe) {
throw new Error("A legacy package input probe is already installed; restart the app.");
}
const probe = {
token: ${JSON.stringify(token)},
stateCount: 0,
busyStateCount: 0,
states: [],
listener: null,
pointerEventTypes: [
"pointerdown",
"gotpointercapture",
"pointermove",
"pointerup",
"pointercancel",
"lostpointercapture"
],
pointerEvents: [],
pointerListener: null,
originalPostMessage: null,
postMessageWrapper: null,
outboundMessages: [],
approvedDatabaseWriteMessages: [],
blockedOutboundMessages: [],
stateViolations: []
};
const allowActiveDryRun = ${JSON.stringify(dryRunPlayoutProfile)};
probe.listener = event => {
if (event.data?.type !== "state" || !event.data.payload) return;
const payload = event.data.payload;
probe.stateCount += 1;
if (payload.isBusy === true) probe.busyStateCount += 1;
probe.states.push(structuredClone(payload));
if (probe.states.length > 750) probe.states.shift();
const playout = payload.playout;
const reasons = [];
if (!playout) {
reasons.push("missing-playout");
} else {
if (playout.mode !== "dryRun") reasons.push("mode");
if (!allowActiveDryRun && playout.phase !== "idle") reasons.push("phase");
if (!allowActiveDryRun && playout.preparedCode != null) reasons.push("prepared-code");
if (!allowActiveDryRun && playout.onAirCode != null) reasons.push("on-air-code");
if (playout.isConnected === true) reasons.push("connected");
if (playout.outcomeUnknown === true) reasons.push("outcome-unknown");
if (!allowActiveDryRun && playout.isPlayCompletionPending === true) {
reasons.push("play-pending");
}
if (!allowActiveDryRun && playout.isTakeOutCompletionPending === true) {
reasons.push("take-out-pending");
}
if (!allowActiveDryRun && playout.isBusy === true) reasons.push("playout-busy");
if (!allowActiveDryRun && playout.refreshActive === true) reasons.push("refresh-active");
}
if (reasons.length > 0) {
probe.stateViolations.push({
at: new Date().toISOString(),
revision: payload.revision ?? null,
reasons
});
}
};
probe.pointerListener = event => {
const target = event.target instanceof Element ? event.target : null;
const cut = target?.closest?.("#cut-list .cut-row") || null;
const playlistRow = target?.closest?.("#playlist-rows .playlist-data-row") || null;
const capturedCuts = Number.isInteger(event.pointerId)
? Array.from(document.querySelectorAll("#cut-list .cut-row"))
.filter(row => typeof row.hasPointerCapture === "function" &&
row.hasPointerCapture(event.pointerId))
.map(row => Number(row.dataset.physicalIndex))
: [];
probe.pointerEvents.push({
type: event.type,
pointerId: event.pointerId,
pointerType: event.pointerType,
clientX: event.clientX,
clientY: event.clientY,
button: event.button,
buttons: event.buttons,
ctrlKey: event.ctrlKey,
shiftKey: event.shiftKey,
targetId: target?.id || null,
targetClass: target?.className || null,
cutPhysicalIndex: cut ? Number(cut.dataset.physicalIndex) : null,
playlistRowId: playlistRow?.dataset?.rowId || null,
capturedCutPhysicalIndices: capturedCuts,
timeStamp: event.timeStamp
});
if (probe.pointerEvents.length > 200) probe.pointerEvents.shift();
};
const allowedTypes = new Set(${JSON.stringify(allowedOutboundMessageTypes)});
const playoutTypes = new Set(${JSON.stringify(playoutIntentMessageTypes)});
const databaseWriteTypes = new Set(${JSON.stringify(databaseWriteIntentMessageTypes)});
probe.originalPostMessage = window.chrome.webview.postMessage;
probe.postMessageWrapper = message => {
const type = typeof message?.type === "string" ? message.type : null;
const record = {
type,
at: new Date().toISOString(),
allowAppend: type === "cut-pointer-down"
? message?.payload?.allowAppend ?? null
: null
};
probe.outboundMessages.push(record);
if (probe.outboundMessages.length > 250) probe.outboundMessages.shift();
if (!type || !allowedTypes.has(type)) {
const blocked = {
...record,
category: playoutTypes.has(type)
? "playout"
: databaseWriteTypes.has(type)
? "database-write"
: "unapproved"
};
probe.blockedOutboundMessages.push(blocked);
return;
}
if (databaseWriteTypes.has(type)) {
probe.approvedDatabaseWriteMessages.push(record);
}
return probe.originalPostMessage.call(window.chrome.webview, message);
};
try {
window.chrome.webview.postMessage = probe.postMessageWrapper;
if (window.chrome.webview.postMessage !== probe.postMessageWrapper) {
throw new Error("The outbound WebView safety gate could not be installed.");
}
globalThis.__legacyPackageInputProbe = probe;
window.chrome.webview.addEventListener("message", probe.listener);
probe.pointerEventTypes.forEach(type =>
document.addEventListener(type, probe.pointerListener, true));
window.chrome.webview.postMessage({ type: "ready", payload: {} });
} catch (error) {
window.chrome.webview.postMessage = probe.originalPostMessage;
delete globalThis.__legacyPackageInputProbe;
throw error;
}
return { token: probe.token };
})()`);
if (installed?.token !== token) failKnown("The state observer was not installed exactly once.");
}
async function removeProbe() {
if (!socket || socket.readyState !== WebSocket.OPEN) return;
const removed = await evaluate(`(() => {
const probe = globalThis.__legacyPackageInputProbe;
if (!probe) return false;
if (probe.listener) {
window.chrome.webview.removeEventListener("message", probe.listener);
}
if (probe.pointerListener) {
probe.pointerEventTypes.forEach(type =>
document.removeEventListener(type, probe.pointerListener, true));
}
if (probe.originalPostMessage) {
if (window.chrome?.webview?.postMessage !== probe.postMessageWrapper) {
throw new Error("The outbound WebView safety gate identity changed.");
}
window.chrome.webview.postMessage = probe.originalPostMessage;
}
delete globalThis.__legacyPackageInputProbe;
return true;
})()`);
if (removed !== true) {
failUnknown("The outbound WebView safety probe was not restored exactly once.");
}
probeInstalled = false;
}
async function captureScreenshot() {
const response = await rpc("Page.captureScreenshot", {
format: "png",
fromSurface: true,
captureBeyondViewport: false
}, 20_000);
if (!response?.data) failKnown("CDP did not return screenshot data.");
const bytes = Buffer.from(response.data, "base64");
fs.writeFileSync(configuration.screenshotPath, bytes, { flag: "wx" });
evidence.screenshot = {
path: configuration.screenshotPath,
bytes: bytes.length,
sha256: sha256(bytes)
};
}
async function executeSafeSequence() {
await rpc("Runtime.enable");
await rpc("Page.enable");
await installProbe();
probeInstalled = true;
const preflight = await waitFor("DryRun IDLE preflight", snapshot =>
snapshot?.state?.playout?.mode === "dryRun" &&
snapshot.state.playout.phase === "idle" &&
snapshot.state.isBusy !== true &&
snapshot.dom.connectionText === "DRY RUN",
{ allowUiBusy: false });
assertSafety(preflight, "DryRun IDLE preflight", false);
if (preflight.state.playlist.length !== 0) {
failKnown("The package input test requires an initially empty in-memory playlist.");
}
if (!preflight.dom.namedModalHidden || !preflight.dom.namedConfirmationHidden) {
failKnown("A child modal was already open at preflight.");
}
await checkpoint("preflight");
await verifyWorkspaceNavigationAndLayout(preflight);
await setSearchText(evidence.fixture.query);
await pointerClick(
'document.getElementById("stock-search-button")',
"submit stock search");
let current = await waitFor("stock search result", snapshot =>
snapshot?.state?.isBusy === false &&
snapshot.state.searchText === evidence.fixture.query &&
snapshot.state.searchResults.some(row =>
row.displayName === evidence.fixture.stockDisplayName),
{ timeoutMilliseconds: 60_000, allowUiBusy: true });
const exactStocks = current.state.searchResults.filter(row =>
row.displayName === evidence.fixture.stockDisplayName);
if (exactStocks.length !== 1) {
failKnown(`Expected exactly one ${evidence.fixture.stockDisplayName} row, received ${exactStocks.length}.`);
}
const exactStock = exactStocks[0];
const exactStockExpression =
`document.querySelector('#stock-results button[data-result-index="${exactStock.index}"]')`;
await pointerClick(exactStockExpression, `select ${evidence.fixture.stockDisplayName}`);
current = await waitFor("exact stock selection", snapshot =>
snapshot.state.isBusy === false &&
snapshot.state.selectedStockIndex === exactStock.index,
{ allowUiBusy: false });
const selectedPosition = current.state.searchResults.findIndex(row =>
row.index === exactStock.index);
const forward = selectedPosition < current.state.searchResults.length - 1;
const firstKey = forward ? "ArrowDown" : "ArrowUp";
const returnKey = forward ? "ArrowUp" : "ArrowDown";
const neighbor = current.state.searchResults[selectedPosition + (forward ? 1 : -1)];
if (!neighbor) failKnown("The exact stock has no adjacent keyboard result.");
await pressKey(firstKey, "move stock result selection to adjacent row");
current = await waitFor("adjacent stock keyboard selection", snapshot =>
snapshot.state.selectedStockIndex === neighbor.index &&
snapshot.dom.stockOptions.filter(row => row.selected).length === 1 &&
snapshot.dom.stockOptions.some(row =>
row.index === neighbor.index && row.selected && row.tabIndex === 0 && row.focused),
{ allowUiBusy: false });
await pressKey(returnKey, "return stock result selection");
current = await waitFor("returned stock keyboard selection", snapshot =>
snapshot.state.selectedStockIndex === exactStock.index &&
snapshot.dom.stockOptions.filter(row => row.selected).length === 1 &&
snapshot.dom.stockOptions.some(row =>
row.index === exactStock.index && row.selected && row.tabIndex === 0 && row.focused),
{ allowUiBusy: false });
await checkpoint("stock-search-and-keyboard", {
resultCount: current.state.searchResults.length,
exactStockIndex: exactStock.index
});
const firstThreeCuts = current.state.cutRows.slice(0, 3);
if (firstThreeCuts.length !== 3 ||
firstThreeCuts.some((row, index) =>
row.physicalIndex !== index || row.isSeparator === true)) {
failKnown("The first three physical cuts are not the expected non-separator fixture.");
}
assertExactArray(
firstThreeCuts.map(row => row.rawLabel),
[
"1열판기본_예상체결가",
"1열판기본_현재가",
"1열판기본_시간외단일가"
],
"first three physical cut labels");
await pointerClick(
'document.querySelector("#cut-list .cut-row[data-physical-index=\'0\']")',
"select first cut");
current = await waitFor("first cut selection", snapshot =>
focusedCutIndex(snapshot) === 0 &&
JSON.stringify(selectedPhysicalIndices(snapshot)) === JSON.stringify([0]),
{ allowUiBusy: false });
await pressKey("ArrowDown", "move cut focus without changing selection", ctrlModifier);
current = await waitFor("Ctrl ArrowDown cut focus", snapshot =>
focusedCutIndex(snapshot) === 1 &&
JSON.stringify(selectedPhysicalIndices(snapshot)) === JSON.stringify([0]),
{ allowUiBusy: false });
await pressKey("Space", "select focused cut with Space");
current = await waitFor("Space cut selection", snapshot =>
focusedCutIndex(snapshot) === 1 &&
JSON.stringify(selectedPhysicalIndices(snapshot)) === JSON.stringify([1]),
{ allowUiBusy: false });
await pressKey("ArrowDown", "extend cut selection with Shift ArrowDown", shiftModifier);
current = await waitFor("Shift ArrowDown cut range", snapshot =>
focusedCutIndex(snapshot) === 2 &&
JSON.stringify(selectedPhysicalIndices(snapshot)) === JSON.stringify([1, 2]),
{ allowUiBusy: false });
await checkpoint("cut-keyboard", {
selectedPhysicalIndices: selectedPhysicalIndices(current),
focusedPhysicalIndex: focusedCutIndex(current)
});
current = await dragCutsToPlaylist(
2,
ctrlModifier | shiftModifier,
2,
"multi-cut captured-pointer drag");
assertExactArray(
current.state.playlist.map(row => row.stockName),
[evidence.fixture.stockDisplayName, evidence.fixture.stockDisplayName],
"two-cut playlist stock order");
assertExactArray(
current.state.playlist.map(row => `${row.graphicType}|${row.subtype}`),
["1열판기본|현재가", "1열판기본|시간외단일가"],
"two-cut playlist mapping");
await checkpoint("multi-cut-drag", {
playlistRowIds: current.state.playlist.map(row => row.rowId)
});
current = await dragCutsToPlaylist(
1,
0,
1,
"single-cut captured-pointer drag");
if (current.state.playlist.length !== 3) {
failKnown("The two cut drags did not create exactly three in-memory rows.");
}
assertExactArray(
current.state.playlist.map(row => `${row.graphicType}|${row.subtype}`),
[
"1열판기본|현재가",
"1열판기본|시간외단일가",
"1열판기본|현재가"
],
"three-cut playlist mapping");
assertExactArray(
current.dom.playlist.map(row => row.rowHeaderText),
["1", "2", "3"],
"playlist row-header ordinals");
if (current.dom.playlist.some(row =>
!Number.isFinite(row.rowHeaderWidth) ||
Math.abs(row.rowHeaderWidth - 30) > 0.5 ||
row.rowHeaderDragEnabled !== true)) {
failKnown("Playlist row-header geometry or drag availability differs from FarPoint.");
}
await checkpoint("single-cut-drag", {
playlistRowIds: current.state.playlist.map(row => row.rowId)
});
const [rowA, rowB, rowC] = current.state.playlist.map(row => row.rowId);
await pointerClickPlaylistRow(rowA, "select playlist row A");
current = await waitFor("playlist row A selection", snapshot =>
activePlaylistId(snapshot) === rowA &&
JSON.stringify(selectedPlaylistIds(snapshot)) === JSON.stringify([rowA]),
{ allowUiBusy: false });
await pointerClickPlaylistRow(rowB, "Ctrl-select playlist row B", ctrlModifier);
current = await waitFor("playlist A+B selection", snapshot =>
activePlaylistId(snapshot) === rowB &&
JSON.stringify(selectedPlaylistIds(snapshot)) === JSON.stringify([rowA, rowB]),
{ allowUiBusy: false });
await dragPlaylistRow(rowA, rowC, "single-source playlist row drag");
current = await waitFor("single-source playlist reorder", snapshot =>
JSON.stringify(snapshot.state.playlist.map(row => row.rowId)) ===
JSON.stringify([rowB, rowC, rowA]),
{ allowUiBusy: false });
assertExactArray(selectedPlaylistIds(current), [rowA], "post-drag deletion selection");
if (activePlaylistId(current) !== rowA ||
current.dom.activeElement?.playlistRowId !== rowA) {
failKnown("Playlist drag did not retain the source as active and focused.");
}
await checkpoint("playlist-single-source-drag", {
beforeOrder: [rowA, rowB, rowC],
afterOrder: [rowB, rowC, rowA]
});
assertExactArray(
current.dom.playlist.map(row => row.rowHeaderText),
["1", "2", "3"],
"reordered playlist row-header ordinals");
const orderBeforeCheckboxAttempt = current.state.playlist.map(row => row.rowId);
const enabledBeforeCheckboxAttempt = current.state.playlist.map(row => ({
rowId: row.rowId,
enabled: row.isEnabled
}));
await attemptCheckboxOriginDrag(rowB, rowA, "checkbox-origin drag rejection");
current = await waitFor("checkbox-origin drag rejection", snapshot =>
snapshot.state.isBusy === false && activePlaylistId(snapshot) === rowB,
{ allowUiBusy: false });
assertExactArray(
current.state.playlist.map(row => row.rowId),
orderBeforeCheckboxAttempt,
"checkbox-origin playlist order");
if (JSON.stringify(current.state.playlist.map(row => ({
rowId: row.rowId,
enabled: row.isEnabled
}))) !== JSON.stringify(enabledBeforeCheckboxAttempt)) {
failKnown("Checkbox-origin drag changed a row's enabled state.");
}
assertExactArray(selectedPlaylistIds(current), [rowA], "checkbox-origin deletion selection");
if (current.dom.playlist.some(row => row.dragging || row.dragBefore || row.dragAfter)) {
failKnown("Checkbox-origin drag left playlist drag visuals active.");
}
await checkpoint("playlist-checkbox-drag-rejected");
const focusBeforeBoundary = current.dom.activeElement;
if (focusBeforeBoundary?.playlistRowId !== rowB) {
failKnown("Checkbox-origin pointer did not leave focus inside source row B.");
}
await pressKey("End", "select last playlist boundary without moving focus");
current = await waitFor("playlist End boundary", snapshot =>
activePlaylistId(snapshot) === rowA,
{ allowUiBusy: false });
assertExactArray(selectedPlaylistIds(current), [rowA], "End deletion selection");
if (JSON.stringify(current.dom.activeElement) !== JSON.stringify(focusBeforeBoundary)) {
failKnown("End moved DOM focus instead of only changing the F8 candidate.");
}
await pressKey("Home", "select first playlist boundary without moving focus");
current = await waitFor("playlist Home boundary", snapshot =>
activePlaylistId(snapshot) === rowB,
{ allowUiBusy: false });
assertExactArray(selectedPlaylistIds(current), [rowA], "Home deletion selection");
if (JSON.stringify(current.dom.activeElement) !== JSON.stringify(focusBeforeBoundary)) {
failKnown("Home moved DOM focus instead of only changing the F8 candidate.");
}
await checkpoint("playlist-keyboard-focus-split");
current = await requestWindowsInput(
current,
rowB,
rowA,
rowC,
"Windows SendInput row-header drag and Home key");
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 &&
snapshot.dom.activeInsideVisibleModal === true,
{ timeoutMilliseconds: 60_000, allowUiBusy: true });
if (current.dom.namedTitle !== "DB 재생목록 불러오기") {
failKnown(`Unexpected load-modal title ${current.dom.namedTitle}.`);
}
await pressKey("Escape", "child load modal Escape no-op");
await sleep(100);
current = await readSnapshot();
assertSafety(current, "load modal Escape", false);
if (current.dom.namedModalHidden || !current.dom.namedConfirmationHidden ||
!current.dom.activeInsideVisibleModal) {
failKnown("Escape closed the read-only named-playlist child modal.");
}
const focusableCount = current.dom.modalFocusableIds.length;
if (focusableCount < 2) failKnown("The named-playlist modal has fewer than two focusable controls.");
for (let index = 0; index <= focusableCount; index += 1) {
await pressKey("Tab", `load modal Shift+Tab cycle ${index + 1}`, shiftModifier);
const tabSnapshot = await readSnapshot();
assertSafety(tabSnapshot, `load modal Shift+Tab cycle ${index + 1}`, false);
if (!tabSnapshot.dom.activeInsideVisibleModal) {
failKnown("Shift+Tab escaped the named-playlist modal focus trap.");
}
}
await pointerClick(
'document.getElementById("named-playlist-cancel-button")',
"close load modal with Cancel");
current = await waitFor("load modal close and focus restore", snapshot =>
snapshot.dom.namedModalHidden === true &&
snapshot.dom.activeElement?.id === "db-load-button",
{ allowUiBusy: false });
await checkpoint("named-playlist-load-modal-focus", {
definitionCount: current.state.namedPlaylist?.definitions?.length ?? 0,
focusableCount
});
await pointerClick(
'document.getElementById("db-save-button")',
"open DB playlist save modal");
current = await waitFor("read-only named playlist save modal", snapshot =>
snapshot.state.isBusy === false && snapshot.dom.namedModalHidden === false &&
snapshot.dom.namedConfirmationHidden === true &&
snapshot.dom.activeInsideVisibleModal === true,
{ timeoutMilliseconds: 60_000, allowUiBusy: true });
if (current.dom.namedTitle !== "DB 재생목록 저장") {
failKnown(`Unexpected save-modal title ${current.dom.namedTitle}.`);
}
if (current.dom.namedConfirmDisabled) {
evidence.warnings.push(
"Nested save confirmation was skipped because no existing definition was safely selectable.");
await pointerClick(
'document.getElementById("named-playlist-cancel-button")',
"close save modal without an existing definition");
current = await waitFor("save modal fixture skip close", snapshot =>
snapshot.dom.namedModalHidden === true &&
snapshot.dom.activeElement?.id === "db-save-button",
{ allowUiBusy: false });
} else {
const namedBeforeConfirmation = {
revision: current.state.namedPlaylist?.revision ?? null,
lastMutationOutcome: current.state.namedPlaylist?.lastMutationOutcome ?? null,
isWriteInProgress: current.state.namedPlaylist?.isWriteInProgress ?? null
};
await pointerClick(
'document.getElementById("named-playlist-confirm-button")',
"open save confirmation without confirming write");
current = await waitFor("nested save confirmation", snapshot =>
snapshot.dom.namedModalHidden === false &&
snapshot.dom.namedConfirmationHidden === false &&
snapshot.dom.namedConfirmationMessage === "저장하겠습니까?" &&
snapshot.dom.activeElement?.id === "named-playlist-confirmation-yes",
{ allowUiBusy: false });
await pressKey("Escape", "nested save confirmation Escape no-op");
await sleep(100);
current = await readSnapshot();
assertSafety(current, "nested confirmation Escape", false);
if (current.dom.namedConfirmationHidden ||
current.dom.activeElement?.id !== "named-playlist-confirmation-yes") {
failKnown("Escape changed the nested save confirmation.");
}
await pressKey("Tab", "move nested confirmation focus to No");
current = await readSnapshot();
if (current.dom.activeElement?.id !== "named-playlist-confirmation-no") {
failKnown("Tab did not move nested confirmation focus from Yes to No.");
}
await pressKey("Tab", "wrap nested confirmation focus to Yes");
current = await readSnapshot();
if (current.dom.activeElement?.id !== "named-playlist-confirmation-yes") {
failKnown("Tab did not wrap nested confirmation focus back to Yes.");
}
await pointerClick(
'document.getElementById("named-playlist-confirmation-no")',
"decline save confirmation");
current = await waitFor("declined save confirmation close", snapshot =>
snapshot.dom.namedModalHidden === true &&
snapshot.dom.namedConfirmationHidden === true &&
snapshot.dom.activeElement?.id === "db-save-button",
{ allowUiBusy: false });
const namedAfterConfirmation = {
revision: current.state.namedPlaylist?.revision ?? null,
lastMutationOutcome: current.state.namedPlaylist?.lastMutationOutcome ?? null,
isWriteInProgress: current.state.namedPlaylist?.isWriteInProgress ?? null
};
if (JSON.stringify(namedAfterConfirmation) !==
JSON.stringify(namedBeforeConfirmation)) {
failKnown("Declining the save confirmation changed named-playlist mutation state.");
}
}
await checkpoint("named-playlist-save-confirmation-no-write");
current = await checkpoint("final", {
expectedPlaylistOrder: [rowC, rowA, rowB]
});
assertExactArray(
current.state.playlist.map(row => row.rowId),
[rowC, rowA, rowB],
"final playlist order");
assertExactArray(selectedPlaylistIds(current), [rowB], "final deletion selection");
if (!current.dom.namedModalHidden || !current.dom.namedConfirmationHidden) {
failKnown("A named-playlist modal remained open at final state.");
}
evidence.finalSnapshot = current;
}
async function executeDryRunPlayoutSequence() {
if (!dryRunPlayoutProfile) return;
let current = await readSnapshot();
assertSafety(current, "DryRun playout sequence preflight", false);
if (current.state.playlist.length < 3 ||
current.state.playlist.some(row => row.isEnabled !== true)) {
failKnown("DryRun NEXT validation requires three enabled in-memory rows.");
}
const originalSelectedIds = selectedPlaylistIds(current);
const originalActiveId = activePlaylistId(current);
if (originalSelectedIds.length !== 1 || !originalActiveId) {
failKnown("DryRun playout preflight lacks the exact legacy selection split.");
}
// The preceding interaction sequence deliberately leaves two distinct current-
// quote rows adjacent. After-hours/expected quote tables are session-dependent;
// the original application also rejects those rows when no exact quote exists.
// Use the two current rows so this check isolates NEXT row semantics from market
// session data availability.
const stagedEntryId = current.state.playlist[2].rowId;
const selectedEntryId = current.state.playlist[0].rowId;
const nextEntryId = current.state.playlist[1].rowId;
const playlistCount = current.state.playlist.length;
evidence.dryRunPlayout.stagedEntryId = stagedEntryId;
evidence.dryRunPlayout.selectedEntryId = selectedEntryId;
evidence.dryRunPlayout.nextEntryId = nextEntryId;
evidence.dryRunPlayout.playlistCountBeforeSelection = playlistCount;
// Move the F8 candidate away from the target first. If the following rapid row
// click were dropped, TAKE IN would remain on this distinct staged row and the
// selected-row assertion below would fail deterministically.
await pointerClickPlaylistRow(
stagedEntryId,
"stage alternate playlist row before rapid DryRun TAKE IN");
current = await waitFor("alternate DryRun row staging", snapshot =>
activePlaylistId(snapshot) === stagedEntryId &&
JSON.stringify(selectedPlaylistIds(snapshot)) ===
JSON.stringify([stagedEntryId]),
{ allowUiBusy: false });
// Keep the real user timing: release the row click and immediately press TAKE IN,
// without waiting for the row-selection state receipt. The native ordered intent
// gate must preserve this input order instead of silently dropping F8/TAKE IN.
await pointerClickPlaylistRow(
selectedEntryId,
"rapid select playlist row before DryRun TAKE IN");
await pointerClick(
'document.getElementById("playout-take-in")',
"rapid DryRun TAKE IN after row selection",
0,
{ skipPointerReady: true });
current = await waitFor("rapid selected-row DryRun TAKE IN", snapshot =>
snapshot.state.playout.phase === "program" &&
snapshot.state.playout.currentEntryId === selectedEntryId &&
snapshot.state.playout.isBusy !== true &&
snapshot.state.isBusy !== true,
{ allowUiBusy: true, allowActiveDryRun: true });
if (current.state.playout.nextKind !== "playlistNext") {
failKnown("DryRun TAKE IN did not expose playlist NEXT from the selected row.");
}
const selectableCut = current.state.cutRows.find(row =>
row.isSeparator !== true && row.isSelected !== true);
if (!selectableCut) {
failKnown("No alternate cut exists for PROGRAM selection validation.");
}
const cutExpression =
`document.querySelector('#cut-list .cut-row[data-physical-index="${selectableCut.physicalIndex}"]')`;
await pointerClick(
cutExpression,
"select alternate left cut during PROGRAM",
0,
{ allowActiveDryRun: true });
current = await waitFor("PROGRAM left cut selection", snapshot =>
snapshot.state.playout.phase === "program" &&
snapshot.state.playlist.length === playlistCount &&
snapshot.state.cutRows.some(row =>
row.physicalIndex === selectableCut.physicalIndex &&
row.isSelected === true && row.isFocused === true),
{ allowUiBusy: false, allowActiveDryRun: true });
evidence.dryRunPlayout.programCutSelection = selectableCut.physicalIndex;
await pointerClick(
cutExpression,
"PROGRAM protected double-click first press",
0,
{ allowActiveDryRun: true });
await pointerClick(
cutExpression,
"PROGRAM protected double-click second press",
0,
{ allowActiveDryRun: true });
await sleep(150);
current = await readSnapshot();
assertSafety(current, "PROGRAM protected double-click", false, false, true);
const recentProgramCutMessages = current.safety.outboundMessages
.filter(message => message.type === "cut-pointer-down")
.slice(-3);
if (current.state.playlist.length !== playlistCount ||
recentProgramCutMessages.length !== 3 ||
recentProgramCutMessages.some(message => message.allowAppend !== false)) {
failKnown("PROGRAM cut selection changed the playlist or retained append authority.");
}
evidence.dryRunPlayout.playlistCountAfterDoubleClick =
current.state.playlist.length;
await pointerClick(
'document.getElementById("playout-next")',
"DryRun NEXT from selected TAKE IN row",
0,
{ allowActiveDryRun: true });
const revisionBeforeNext = current.state.revision;
current = await waitFor("DryRun NEXT selected-row successor", snapshot => {
if (snapshot.state.revision > revisionBeforeNext &&
snapshot.state.playout.isBusy !== true &&
snapshot.state.isBusy !== true &&
snapshot.state.statusKind === "error") {
failKnown(
`DryRun NEXT was safely rejected before playout: ${snapshot.state.statusMessage}`);
}
return snapshot.state.playout.phase === "program" &&
snapshot.state.playout.currentEntryId === nextEntryId &&
snapshot.state.playout.isBusy !== true &&
snapshot.state.isBusy !== true;
},
{ allowUiBusy: true, allowActiveDryRun: true });
await pointerClick(
'document.getElementById("playout-take-out")',
"DryRun TAKE OUT after NEXT",
0,
{ allowActiveDryRun: true });
current = await waitFor("DryRun TAKE OUT IDLE", snapshot =>
snapshot.state.playout.phase === "idle" &&
snapshot.state.playout.currentEntryId == null &&
snapshot.state.playout.onAirCode == null &&
snapshot.state.playout.isBusy !== true &&
snapshot.state.isBusy !== true,
{ allowUiBusy: true, allowActiveDryRun: true });
await pointerClickPlaylistRow(
originalSelectedIds[0],
"restore deletion selection after DryRun playout");
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 playlist row after DryRun playout");
current = await waitFor("restore playlist state after DryRun playout", snapshot =>
snapshot.state.playout.phase === "idle" &&
JSON.stringify(selectedPlaylistIds(snapshot)) ===
JSON.stringify(originalSelectedIds) &&
activePlaylistId(snapshot) === originalActiveId,
{ allowUiBusy: false });
evidence.dryRunPlayout.result = "PASS";
await checkpoint("dry-run-playout-selection-next", {
stagedEntryId,
selectedEntryId,
nextEntryId,
programCutPhysicalIndex: selectableCut.physicalIndex,
playlistCount
});
}
function recordFullUiDbCheck(collection, name, details = null) {
if (!evidence.fullUiDb) return;
evidence.fullUiDb[collection].push({
name,
at: new Date().toISOString(),
details
});
}
function namedDefinitionExpression(title) {
return `Array.from(document.querySelectorAll(
"#named-playlist-definitions button[data-named-playlist-definition-id]"))
.find(button => button.textContent === ${JSON.stringify(title)})`;
}
async function executeNamedPlaylistCrud() {
const title = evidence.fixture.namedPlaylistTitle;
let current = await readSnapshot();
if (current.state.namedPlaylist.definitions.some(row => row.title === title)) {
failKnown("The generated PList fixture already exists before mutation.");
}
await pointerClick('document.getElementById("db-save-button")',
"PList open save dialog");
current = await waitFor("PList fresh definition read", snapshot =>
snapshot.dom.namedModalHidden === false && snapshot.state.isBusy === false,
{ timeoutMilliseconds: 60_000, allowUiBusy: true });
if (current.state.namedPlaylist.definitions.some(row => row.title === title)) {
failKnown("The generated PList fixture appeared during the preflight read.");
}
await replaceText('document.getElementById("named-playlist-new-title")',
title, "PList unique title");
await pointerClick('document.getElementById("named-playlist-create-button")',
"PList create definition");
current = await waitFor("PList definition create commit", snapshot =>
snapshot.state.isBusy === false &&
snapshot.state.namedPlaylist.definitions.some(row => row.title === title) &&
snapshot.state.namedPlaylist.lastMutationOutcome &&
snapshot.state.namedPlaylist.lastMutationOutcome.startsWith("committed"),
{ timeoutMilliseconds: 60_000, allowUiBusy: true });
const created = current.state.namedPlaylist.definitions.find(row => row.title === title);
if (!created || current.state.namedPlaylist.selectedDefinitionId !== created.definitionId) {
await pointerClick(namedDefinitionExpression(title),
"PList select created definition");
current = await waitFor("PList created definition selection", snapshot =>
snapshot.state.isBusy === false &&
snapshot.state.namedPlaylist.selectedTitle === title,
{ timeoutMilliseconds: 60_000, allowUiBusy: true });
}
await pointerClick('document.getElementById("named-playlist-confirm-button")',
"PList request row save");
await waitFor("PList save confirmation", snapshot =>
snapshot.dom.namedConfirmationHidden === false &&
snapshot.state.isBusy === false,
{ allowUiBusy: false });
await pointerClickWithJavaScriptDialog(
'document.getElementById("named-playlist-confirmation-yes")',
"PList confirm row save",
"alert",
"저장되었습니다",
true);
current = await waitFor("PList row save commit", snapshot =>
snapshot.state.isBusy === false && snapshot.dom.namedModalHidden === true &&
snapshot.state.namedPlaylist.selectedTitle === title &&
snapshot.state.namedPlaylist.rowCount === 3 &&
snapshot.state.namedPlaylist.lastMutationOutcome &&
snapshot.state.namedPlaylist.lastMutationOutcome.startsWith("committed"),
{ timeoutMilliseconds: 60_000, allowUiBusy: true });
recordFullUiDbCheck("databaseChecks", "PList create/save", {
rowCount: current.state.namedPlaylist.rowCount
});
await pointerClick('document.getElementById("db-load-button")',
"PList reopen for fresh readback");
current = await waitFor("PList fresh readback", snapshot =>
snapshot.state.isBusy === false && snapshot.dom.namedModalHidden === false &&
snapshot.state.namedPlaylist.definitions.some(row => row.title === title),
{ timeoutMilliseconds: 60_000, allowUiBusy: true });
await pointerClick(namedDefinitionExpression(title),
"PList select fresh readback");
current = await waitFor("PList fresh selected readback", snapshot =>
snapshot.state.isBusy === false &&
snapshot.state.namedPlaylist.selectedTitle === title,
{ timeoutMilliseconds: 60_000, allowUiBusy: true });
await pointerClick('document.getElementById("named-playlist-confirm-button")',
"PList load fresh rows");
current = await waitFor("PList load close", snapshot =>
snapshot.state.isBusy === false && snapshot.dom.namedModalHidden === true &&
snapshot.state.playlist.length === 3,
{ timeoutMilliseconds: 60_000, allowUiBusy: true });
recordFullUiDbCheck("databaseChecks", "PList fresh readback", {
rowCount: current.state.playlist.length
});
await pointerClick('document.getElementById("db-load-button")',
"PList reopen for delete");
await waitFor("PList delete list read", snapshot =>
snapshot.state.isBusy === false && snapshot.dom.namedModalHidden === false &&
snapshot.state.namedPlaylist.definitions.some(row => row.title === title),
{ timeoutMilliseconds: 60_000, allowUiBusy: true });
await pointerClick(namedDefinitionExpression(title), "PList select delete target");
await waitFor("PList delete target selection", snapshot =>
snapshot.state.isBusy === false && snapshot.state.namedPlaylist.selectedTitle === title,
{ timeoutMilliseconds: 60_000, allowUiBusy: true });
await pointerClick('document.getElementById("named-playlist-delete-button")',
"PList open delete confirmation");
await waitFor("PList delete confirmation", snapshot =>
snapshot.dom.namedConfirmationHidden === false && snapshot.state.isBusy === false,
{ allowUiBusy: false });
await pointerClick('document.getElementById("named-playlist-confirmation-yes")',
"PList confirm delete");
current = await waitFor("PList delete and absence", snapshot =>
snapshot.state.isBusy === false &&
!snapshot.state.namedPlaylist.definitions.some(row => row.title === title) &&
snapshot.state.namedPlaylist.lastMutationOutcome &&
snapshot.state.namedPlaylist.lastMutationOutcome.startsWith("committed"),
{ timeoutMilliseconds: 60_000, allowUiBusy: true });
await pointerClick('document.getElementById("named-playlist-cancel-button")',
"PList close after delete");
await waitFor("PList final close", snapshot =>
snapshot.dom.namedModalHidden === true && snapshot.state.isBusy === false,
{ allowUiBusy: false });
recordFullUiDbCheck("databaseChecks", "PList delete/absence");
}
const manualFinancialScreens = Object.freeze([
"revenue-composition",
"growth-metrics",
"sales",
"operating-profit"
]);
function manualScreenButtonExpression(screen) {
return `document.querySelector('button[data-manual-screen="${screen}"]')`;
}
async function openManualFinancial(screen, label) {
await ensureWorkspace("stock", `${label} stock workspace`);
await pointerClick(manualScreenButtonExpression(screen), label);
return await waitFor(`${label} open`, snapshot =>
snapshot.state.isBusy === false &&
snapshot.state.manualFinancial?.screen === screen &&
snapshot.dom.manualFinancialModalHidden === false,
{ timeoutMilliseconds: 60_000, allowUiBusy: true });
}
async function closeManualFinancial(label) {
await pointerClick('document.getElementById("manual-financial-close")', label);
await waitFor(`${label} complete`, snapshot =>
snapshot.state.isBusy === false && snapshot.state.manualFinancial == null &&
snapshot.dom.manualFinancialModalHidden === true,
{ allowUiBusy: false });
}
async function searchManualFinancialIdentity(screen, stockName, label) {
await replaceText('document.getElementById("manual-financial-list-query")',
stockName, `${label} list query`);
await pointerClick('document.querySelector("button[data-manual-list-search]")',
`${label} list search`);
return await waitFor(`${label} list result`, snapshot =>
snapshot.state.isBusy === false &&
snapshot.state.manualFinancial?.screen === screen &&
snapshot.state.manualFinancial.query === stockName,
{ timeoutMilliseconds: 60_000, allowUiBusy: true });
}
async function preflightManualFinancialIdentities() {
const stockName = evidence.fixture.databaseStockDisplayName;
for (const screen of manualFinancialScreens) {
let current = await openManualFinancial(screen,
`GraphE ${screen} preflight open`);
current = await searchManualFinancialIdentity(
screen, stockName, `GraphE ${screen} preflight`);
if (current.state.manualFinancial.rows.some(row => row.stockName === stockName)) {
failKnown(`GraphE ${screen} fixture stock already exists before any GraphE write.`);
}
await closeManualFinancial(`GraphE ${screen} preflight close`);
}
recordFullUiDbCheck("screenChecks", "GraphE four-screen identity preflight");
}
async function fillManualFinancialRecord(screen) {
if (screen === "revenue-composition") {
await replaceText('document.querySelector("[data-manual-base-date]")',
"2026Q3", "GraphE revenue base date");
const percentages = ["10", "20", "30", "20", "20"];
for (let index = 0; index < 5; index += 1) {
await replaceText(
`document.querySelector('[data-manual-revenue-label="${index}"]')`,
`항목${index + 1}`,
`GraphE revenue label ${index + 1}`);
await replaceText(
`document.querySelector('[data-manual-revenue-percentage="${index}"]')`,
percentages[index],
`GraphE revenue percentage ${index + 1}`);
}
return;
}
if (screen === "growth-metrics") {
for (let index = 0; index < 4; index += 1) {
await replaceText(
`document.querySelector('[data-manual-growth-period="${index}"]')`,
`2${index + 3}Q${index + 1}`,
`GraphE growth period ${index + 1}`);
}
const series = [
"salesGrowth",
"operatingProfitGrowth",
"netAssetGrowth",
"netIncomeGrowth"
];
for (let seriesIndex = 0; seriesIndex < series.length; seriesIndex += 1) {
for (let index = 0; index < 4; index += 1) {
await replaceText(
`document.querySelector('[data-manual-growth-series="${series[seriesIndex]}"]` +
`[data-manual-growth-value="${index}"]')`,
String(seriesIndex * 4 + index + 1),
`GraphE growth value ${seriesIndex + 1}-${index + 1}`);
}
}
return;
}
for (let index = 0; index < 6; index += 1) {
await replaceText(
`document.querySelector('[data-manual-quarter-label="${index}"]')`,
`${index + 1}Q`,
`GraphE ${screen} quarter ${index + 1}`);
await replaceText(
`document.querySelector('[data-manual-quarter-value="${index}"]')`,
String((index + 1) * 10),
`GraphE ${screen} value ${index + 1}`);
}
}
async function executeManualFinancialCrud() {
const stockName = evidence.fixture.databaseStockDisplayName;
await preflightManualFinancialIdentities();
for (const screen of manualFinancialScreens) {
let current = await openManualFinancial(screen, `GraphE ${screen} create open`);
await pointerClick('document.querySelector("button[data-manual-create]")',
`GraphE ${screen} new input`);
await waitFor(`GraphE ${screen} create form`, snapshot =>
snapshot.state.isBusy === false &&
snapshot.state.manualFinancial?.screen === screen &&
snapshot.state.manualFinancial.selectedRecord == null,
{ allowUiBusy: false });
await replaceText('document.getElementById("manual-financial-stock-name")',
stockName, `GraphE ${screen} stock name`);
await pointerClick('document.querySelector("button[data-manual-stock-search]")',
`GraphE ${screen} verify stock`);
current = await waitFor(`GraphE ${screen} stock candidates`, snapshot =>
snapshot.state.isBusy === false &&
snapshot.state.manualFinancial?.stockCandidates?.some(row => row.name === stockName),
{ timeoutMilliseconds: 60_000, allowUiBusy: true });
const candidates = current.state.manualFinancial.stockCandidates
.filter(row => row.name === stockName);
if (candidates.length !== 1) {
failKnown(`GraphE ${screen} fixture stock is not unique in the master.`);
}
await pointerClick(
`document.querySelector('[data-manual-stock-result-id="${candidates[0].resultId}"]')`,
`GraphE ${screen} select verified stock`);
await waitFor(`GraphE ${screen} verified stock`, snapshot =>
snapshot.state.isBusy === false &&
snapshot.state.manualFinancial?.verifiedStock?.name === stockName,
{ allowUiBusy: false });
await fillManualFinancialRecord(screen);
await pointerClick('document.querySelector("button[data-manual-save]")',
`GraphE ${screen} save`);
current = await waitFor(`GraphE ${screen} create commit and readback`, snapshot =>
snapshot.state.isBusy === false &&
snapshot.state.manualFinancial?.status === "writeCommitted" &&
snapshot.state.manualFinancial?.selectedRecord?.stockName === stockName &&
snapshot.state.manualFinancial?.canDelete === true &&
snapshot.state.manualFinancial?.writesQuarantined !== true,
{ timeoutMilliseconds: 60_000, allowUiBusy: true });
recordFullUiDbCheck("databaseChecks", `GraphE ${screen} create/save`, {
status: current.state.manualFinancial.status
});
await closeManualFinancial(`GraphE ${screen} close after create`);
await openManualFinancial(screen, `GraphE ${screen} fresh reopen`);
current = await searchManualFinancialIdentity(
screen, stockName, `GraphE ${screen} fresh readback`);
const rows = current.state.manualFinancial.rows
.filter(row => row.stockName === stockName);
if (rows.length !== 1) {
failKnown(`GraphE ${screen} fresh readback is not unique.`);
}
await replaceText('document.getElementById("manual-financial-find-query")',
stockName, `GraphE ${screen} find query`);
await pointerClick('document.querySelector("button[data-manual-find-direction=down]")',
`GraphE ${screen} find down`);
await waitFor(`GraphE ${screen} find down result`, snapshot =>
snapshot.state.isBusy === false &&
snapshot.state.manualFinancial?.selectedRecord?.stockName === stockName,
{ timeoutMilliseconds: 60_000, allowUiBusy: true });
await pointerClick('document.querySelector("button[data-manual-find-direction=up]")',
`GraphE ${screen} find up`);
await waitFor(`GraphE ${screen} find up result`, snapshot =>
snapshot.state.isBusy === false &&
snapshot.state.manualFinancial?.selectedRecord?.stockName === stockName,
{ timeoutMilliseconds: 60_000, allowUiBusy: true });
await pointerClick('document.querySelector("button[data-manual-name-sort]")',
`GraphE ${screen} toggle name sort`);
await waitFor(`GraphE ${screen} sort complete`, snapshot =>
snapshot.state.isBusy === false &&
snapshot.state.manualFinancial?.nameSortDirection === "descending",
{ allowUiBusy: false });
recordFullUiDbCheck("databaseChecks", `GraphE ${screen} fresh readback`, {
rows: rows.length
});
const actionIds = await clickAllEnabledPlaylistActions(
"button[data-manual-action-id]", "manualActionId", `GraphE ${screen} action`);
const beforeDoubleClick = await readSnapshot();
await pointerDoubleClick(dataButtonExpression("manualResultId", rows[0].resultId),
`GraphE ${screen} row double-click`);
await waitFor(`GraphE ${screen} row double-click add`, snapshot =>
snapshot.state.playlist.length === beforeDoubleClick.state.playlist.length + 1 &&
snapshot.state.manualFinancial == null &&
snapshot.dom.manualFinancialModalHidden === true,
{ timeoutMilliseconds: 60_000, allowUiBusy: true });
recordFullUiDbCheck("screenChecks", `GraphE ${screen} actions and row double-click`, {
actionButtons: actionIds.length
});
await openManualFinancial(screen, `GraphE ${screen} delete reopen`);
current = await searchManualFinancialIdentity(
screen, stockName, `GraphE ${screen} delete fresh search`);
const deleteRows = current.state.manualFinancial.rows
.filter(row => row.stockName === stockName);
if (deleteRows.length !== 1) {
failKnown(`GraphE ${screen} delete target is not unique after fresh reopen.`);
}
await pointerClick(
dataButtonExpression("manualResultId", deleteRows[0].resultId),
`GraphE ${screen} select delete target`);
await waitFor(`GraphE ${screen} delete target loaded`, snapshot =>
snapshot.state.isBusy === false &&
snapshot.state.manualFinancial?.selectedRecord?.stockName === stockName &&
snapshot.state.manualFinancial?.canDelete === true,
{ timeoutMilliseconds: 60_000, allowUiBusy: true });
await pointerClickWithJavaScriptDialog(
'document.querySelector("button[data-manual-delete]")',
`GraphE ${screen} delete`,
"confirm",
"선택한 수동 재무 데이터를 삭제하시겠습니까?",
true);
current = await waitFor(`GraphE ${screen} delete commit`, snapshot =>
snapshot.state.isBusy === false &&
snapshot.state.manualFinancial?.status === "writeCommitted" &&
snapshot.state.manualFinancial?.writesQuarantined !== true &&
!snapshot.state.manualFinancial.rows.some(row => row.stockName === stockName),
{ timeoutMilliseconds: 60_000, allowUiBusy: true });
await pointerClickWithJavaScriptDialog(
'document.querySelector("button[data-manual-delete-all]")',
`GraphE ${screen} delete-all cancel branch`,
"confirm",
"현재 화면의 수동 재무 데이터를 모두 삭제하시겠습니까?",
false);
recordFullUiDbCheck("databaseChecks", `GraphE ${screen} delete/absence`);
await closeManualFinancial(`GraphE ${screen} close after delete`);
}
if (configuration.scope === "graphe") {
await clearScreenValidationPlaylist();
}
}
async function selectMainTab(tabId, label) {
const isLoaded = snapshot =>
snapshot.state.isBusy === false &&
snapshot.state.tabs?.some(tab => tab.id === tabId && tab.isActive) === true &&
(tabId !== "theme" || snapshot.state.theme != null) &&
(tabId !== "expert" || snapshot.state.expert != null);
const layout = await readWorkspaceLayout();
const before = await readSnapshot();
assertSafety(before, `${label} preflight`);
const alreadyActive = before.state.tabs?.some(
tab => tab.id === tabId && tab.isActive) === true;
const selectionRequired = !alreadyActive || layout.activeWorkspace !== "catalog" ||
layout.activeMarketTab !== tabId;
if (selectionRequired) {
await pointerClick(
`document.querySelector('#market-tabs > button[data-tab-id="${tabId}"]')`,
label);
}
const loaded = await waitFor(`${label} load`, snapshot =>
(!selectionRequired ||
(snapshot.stateCount >= before.stateCount + 2 &&
snapshot.busyStateCount > before.busyStateCount)) &&
isLoaded(snapshot),
{ timeoutMilliseconds: 60_000, allowUiBusy: true });
const loadedLayout = await waitForWorkspaceLayout(`${label} workspace`, candidate =>
workspaceLayoutMatches(candidate, "catalog") &&
candidate.activeMarketTab === tabId);
assertWorkspaceAndScheduleGeometry(loadedLayout, `${label} workspace`);
return loaded;
}
async function openThemeCatalogList(label) {
await selectMainTab("theme", `${label} select ThemeA tab`);
await pointerClick('document.getElementById("uc4-theme-add")',
`${label} open ThemeA catalog`);
return await waitFor(`${label} ThemeA catalog list`, snapshot =>
snapshot.state.isBusy === false &&
snapshot.state.operatorCatalog?.isOpen === true &&
snapshot.state.operatorCatalog?.entity === "theme" &&
snapshot.state.operatorCatalog?.mode === "list" &&
snapshot.state.operatorCatalog?.schemaValidated === true &&
snapshot.state.operatorCatalog?.writesQuarantined !== true &&
snapshot.dom.operatorCatalogModalHidden === false,
{ timeoutMilliseconds: 60_000, allowUiBusy: true });
}
async function closeOperatorCatalog(label, useEditorCancel = false) {
await pointerClick(
useEditorCancel
? 'document.getElementById("operator-catalog-cancel")'
: 'document.getElementById("operator-catalog-close")',
label);
return await waitFor(`${label} complete`, snapshot =>
snapshot.state.isBusy === false &&
snapshot.state.operatorCatalog == null &&
snapshot.dom.operatorCatalogModalHidden === true,
{ timeoutMilliseconds: 60_000, allowUiBusy: true });
}
async function searchOperatorCatalog(query, label) {
await replaceText('document.getElementById("operator-catalog-query")',
query, `${label} query`);
const before = await readSnapshot();
await pointerClick('document.getElementById("operator-catalog-search")',
`${label} submit`);
return await waitFor(`${label} results`, snapshot =>
snapshot.state.isBusy === false &&
snapshot.state.revision > before.state.revision &&
snapshot.state.operatorCatalog?.isOpen === true &&
snapshot.state.operatorCatalog?.mode === "list" &&
snapshot.state.operatorCatalog?.query === query &&
snapshot.state.operatorCatalog?.writesQuarantined !== true,
{ timeoutMilliseconds: 60_000, allowUiBusy: true });
}
async function requireExactCatalogResult(query, label, expectedCount) {
const current = await searchOperatorCatalog(query, label);
const matches = current.state.operatorCatalog.results
.filter(row => row.displayName === query);
if (matches.length !== expectedCount) {
failKnown(`${label} expected ${expectedCount} exact catalog result(s), ` +
`received ${matches.length}.`);
}
return { current, matches };
}
async function addOperatorCatalogStock(
query,
expectedDisplayName,
label) {
const before = await readSnapshot();
const beforeRows = before.state.operatorCatalog?.draftRows || [];
await replaceText('document.getElementById("operator-catalog-stock-query")',
query, `${label} query`);
const beforeSearch = await readSnapshot();
await pointerClick('document.getElementById("operator-catalog-stock-search")',
`${label} search`);
const searched = await waitFor(`${label} stock results`, snapshot =>
snapshot.state.isBusy === false &&
snapshot.state.revision > beforeSearch.state.revision &&
snapshot.state.operatorCatalog?.stockQuery === query &&
snapshot.state.operatorCatalog?.writesQuarantined !== true,
{ timeoutMilliseconds: 60_000, allowUiBusy: true });
const matches = searched.state.operatorCatalog.stockResults.filter(row =>
row.displayName === expectedDisplayName && row.isCompatible === true);
if (matches.length !== 1) {
failKnown(`${label} requires one exact compatible stock result, received ` +
`${matches.length}.`);
}
const resultId = matches[0].resultId;
await pointerClick(
`document.querySelector('button[data-operator-catalog-stock-result-id="${resultId}"]')`,
`${label} select`);
await waitFor(`${label} selected`, snapshot =>
snapshot.state.isBusy === false &&
snapshot.state.operatorCatalog?.selectedStockResultId === resultId &&
snapshot.state.operatorCatalog?.canAddStock === true,
{ timeoutMilliseconds: 30_000, allowUiBusy: true });
await pointerClick('document.getElementById("operator-catalog-add-stock")',
`${label} add`);
const added = await waitFor(`${label} added`, snapshot =>
snapshot.state.isBusy === false &&
snapshot.state.operatorCatalog?.draftRows?.length === beforeRows.length + 1 &&
snapshot.state.operatorCatalog?.selectedStockResultId == null,
{ allowUiBusy: false });
const addedRows = added.state.operatorCatalog.draftRows.filter(row =>
!beforeRows.some(existing => existing.rowId === row.rowId));
if (addedRows.length !== 1) {
failKnown(`${label} did not add exactly one new draft row.`);
}
return { current: added, row: addedRows[0] };
}
async function setOperatorCatalogBuyAmount(rowId, amount, label) {
const expression =
`document.querySelector('input[data-operator-catalog-buy-amount-row-id="${rowId}"]')`;
await replaceText(expression, String(amount), label);
await pointerClick('document.getElementById("operator-catalog-stock-query")',
`${label} commit`);
return await waitFor(`${label} state`, snapshot =>
snapshot.state.isBusy === false &&
snapshot.state.operatorCatalog?.draftRows?.some(row =>
row.rowId === rowId && row.buyAmount === amount) === true,
{ allowUiBusy: false });
}
async function searchAndSelectMainTheme(title, expectedDisplayTitle, label) {
await selectMainTab("theme", `${label} select ThemeA tab`);
await replaceText('document.getElementById("theme-search")',
title, `${label} query`);
const beforeSearch = await readSnapshot();
await pointerClick('document.querySelector("button[data-theme-search]")',
`${label} search`);
const searched = await waitFor(`${label} results`, snapshot =>
snapshot.state.isBusy === false &&
snapshot.state.revision > beforeSearch.state.revision &&
snapshot.state.theme?.query === title,
{ timeoutMilliseconds: 60_000, allowUiBusy: true });
const matches = searched.state.theme.searchResults.filter(row =>
row.displayTitle === expectedDisplayTitle);
if (matches.length !== 1) {
failKnown(`${label} expected one exact ThemeA result, received ${matches.length}.`);
}
const resultId = matches[0].resultId;
await pointerClick(
`document.querySelector('button[data-theme-result-id="${resultId}"]')`,
`${label} select`);
const selected = await waitFor(`${label} preview`, snapshot =>
snapshot.state.isBusy === false &&
snapshot.state.theme?.selectedResultId === resultId &&
snapshot.state.theme?.selectedTheme?.displayTitle === expectedDisplayTitle,
{ timeoutMilliseconds: 60_000, allowUiBusy: true });
return { current: selected, resultId };
}
async function assertMainThemeAbsent(title, expectedDisplayTitle, label) {
await selectMainTab("theme", `${label} select ThemeA tab`);
await replaceText('document.getElementById("theme-search")',
title, `${label} query`);
const beforeSearch = await readSnapshot();
await pointerClick('document.querySelector("button[data-theme-search]")',
`${label} search`);
const current = await waitFor(`${label} results`, snapshot =>
snapshot.state.isBusy === false &&
snapshot.state.revision > beforeSearch.state.revision &&
snapshot.state.theme?.query === title,
{ timeoutMilliseconds: 60_000, allowUiBusy: true });
if (current.state.theme.searchResults.some(row =>
row.displayTitle === expectedDisplayTitle)) {
failKnown(`${label} still returned the deleted ThemeA row.`);
}
return current;
}
async function createReadEditDeleteTheme(
title,
market,
stockQuery,
stockDisplayName) {
const label = `ThemeA ${market.toUpperCase()} ${title}`;
await openThemeCatalogList(`${label} create`);
await requireExactCatalogResult(title, `${label} preflight`, 0);
await selectOptionValue(
'document.getElementById("operator-catalog-new-market")',
market,
`${label} market`);
await pointerClick('document.getElementById("operator-catalog-new")',
`${label} new`);
await waitFor(`${label} create editor`, snapshot =>
snapshot.state.isBusy === false &&
snapshot.state.operatorCatalog?.entity === "theme" &&
snapshot.state.operatorCatalog?.mode === "create" &&
snapshot.state.operatorCatalog?.newThemeMarket === market &&
snapshot.state.operatorCatalog?.codeLabel?.length > 0 &&
snapshot.state.operatorCatalog?.writesQuarantined !== true,
{ timeoutMilliseconds: 60_000, allowUiBusy: true });
await replaceText('document.getElementById("operator-catalog-name")',
title, `${label} name`);
await pointerClick('document.getElementById("operator-catalog-name-check")',
`${label} duplicate check`);
await respondToNativeDialog(`${label} duplicate check`, false, true);
await waitFor(`${label} name available`, snapshot =>
snapshot.state.isBusy === false &&
snapshot.state.operatorCatalog?.checkedThemeName === title &&
snapshot.state.operatorCatalog?.themeNameAvailability === "available" &&
snapshot.state.operatorCatalog?.writesQuarantined !== true,
{ timeoutMilliseconds: 60_000, allowUiBusy: true });
const added = await addOperatorCatalogStock(
stockQuery, stockDisplayName, `${label} stock`);
await pointerClick('document.getElementById("operator-catalog-delete-all")',
`${label} Delete All cancel branch`);
await respondToNativeDialog(`${label} Delete All cancel branch`, true, false);
const afterCancel = await waitFor(`${label} draft preserved`, snapshot =>
snapshot.state.isBusy === false &&
snapshot.state.operatorCatalog?.draftRows?.length === 1 &&
snapshot.state.operatorCatalog?.draftRows?.[0]?.rowId === added.row.rowId,
{ allowUiBusy: false });
if (afterCancel.state.operatorCatalog.canSave !== true) {
failKnown(`${label} is not saveable after the cancelled Delete All branch.`);
}
await pointerClick('document.getElementById("operator-catalog-save")',
`${label} save`);
await waitFor(`${label} create commit`, snapshot =>
snapshot.state.isBusy === false &&
snapshot.state.operatorCatalog == null &&
snapshot.dom.operatorCatalogModalHidden === true,
{ timeoutMilliseconds: 60_000, allowUiBusy: true });
recordFullUiDbCheck("databaseChecks", `${label} create/save`);
await openThemeCatalogList(`${label} fresh read`);
const fresh = await requireExactCatalogResult(title, `${label} fresh read`, 1);
const catalogResultId = fresh.matches[0].resultId;
await pointerClick(
`document.querySelector('button[data-operator-catalog-result-id="${catalogResultId}"]')`,
`${label} fresh read select`);
const loaded = await waitFor(`${label} fresh read editor`, snapshot =>
snapshot.state.isBusy === false &&
snapshot.state.operatorCatalog?.mode === "edit" &&
snapshot.state.operatorCatalog?.selectedResultId === catalogResultId &&
snapshot.state.operatorCatalog?.editorName === title &&
snapshot.state.operatorCatalog?.draftRows?.length === 1 &&
snapshot.state.operatorCatalog?.writesQuarantined !== true,
{ timeoutMilliseconds: 60_000, allowUiBusy: true });
recordFullUiDbCheck("databaseChecks", `${label} fresh readback`, {
codeLabel: loaded.state.operatorCatalog.codeLabel,
rows: loaded.state.operatorCatalog.draftRows.length
});
await closeOperatorCatalog(`${label} close fresh read`, true);
const expectedDisplayTitle = market === "nxt" ? `${title}(NXT)` : title;
const selected = await searchAndSelectMainTheme(
title, expectedDisplayTitle, `${label} UC4`);
if (selected.current.state.theme.previewItems.length !== 1) {
failKnown(`${label} UC4 preview did not contain the saved stock.`);
}
await pointerClick('document.getElementById("uc4-theme-edit")',
`${label} UC4 edit`);
await waitFor(`${label} UC4 edit loaded`, snapshot =>
snapshot.state.isBusy === false &&
snapshot.state.operatorCatalog?.entity === "theme" &&
snapshot.state.operatorCatalog?.mode === "edit" &&
snapshot.state.operatorCatalog?.editorName === title &&
snapshot.state.operatorCatalog?.draftRows?.length === 1,
{ timeoutMilliseconds: 60_000, allowUiBusy: true });
await closeOperatorCatalog(`${label} UC4 edit cancel`, true);
recordFullUiDbCheck("screenChecks", `${label} UC4 select/edit`);
await pointerClick('document.getElementById("uc4-theme-delete")',
`${label} UC4 delete`);
await respondToNativeDialog(`${label} UC4 delete`, true, true);
await waitFor(`${label} delete commit`, snapshot =>
snapshot.state.isBusy === false &&
snapshot.state.operatorCatalog == null &&
snapshot.state.theme?.selectedTheme == null &&
!snapshot.state.theme?.searchResults?.some(row =>
row.displayTitle === expectedDisplayTitle),
{ timeoutMilliseconds: 60_000, allowUiBusy: true });
await assertMainThemeAbsent(title, expectedDisplayTitle,
`${label} fresh absence`);
recordFullUiDbCheck("databaseChecks", `${label} delete/absence`);
}
async function searchAndSelectMainExpert(title, label) {
await selectMainTab("expert", `${label} select EList tab`);
await replaceText('document.getElementById("expert-search")',
title, `${label} query`);
const beforeSearch = await readSnapshot();
await pointerClick('document.querySelector("button[data-expert-search]")',
`${label} search`);
const searched = await waitFor(`${label} results`, snapshot =>
snapshot.state.isBusy === false &&
snapshot.state.revision > beforeSearch.state.revision &&
snapshot.state.expert?.search?.query === title,
{ timeoutMilliseconds: 60_000, allowUiBusy: true });
const matches = searched.state.expert.search.results.filter(row =>
row.expertName === title);
if (matches.length !== 1) {
failKnown(`${label} expected one exact EList result, received ${matches.length}.`);
}
const selectionId = matches[0].selectionId;
await pointerClick(
`document.querySelector('button[data-expert-selection-id="${selectionId}"]')`,
`${label} select`);
const selected = await waitFor(`${label} preview`, snapshot =>
snapshot.state.isBusy === false &&
snapshot.state.expert?.selectedExpert?.selectionId === selectionId &&
snapshot.state.expert?.selectedExpert?.expertName === title,
{ timeoutMilliseconds: 60_000, allowUiBusy: true });
return { current: selected, selectionId };
}
async function assertMainExpertAbsent(title, label) {
await selectMainTab("expert", `${label} select EList tab`);
await replaceText('document.getElementById("expert-search")',
title, `${label} query`);
const beforeSearch = await readSnapshot();
await pointerClick('document.querySelector("button[data-expert-search]")',
`${label} search`);
const current = await waitFor(`${label} results`, snapshot =>
snapshot.state.isBusy === false &&
snapshot.state.revision > beforeSearch.state.revision &&
snapshot.state.expert?.search?.query === title,
{ timeoutMilliseconds: 60_000, allowUiBusy: true });
if (current.state.expert.search.results.some(row => row.expertName === title)) {
failKnown(`${label} still returned the deleted EList row.`);
}
return current;
}
async function createReadEditDeleteExpert(title) {
const label = `EList ${title}`;
await assertMainExpertAbsent(title, `${label} preflight`);
await pointerClick('document.getElementById("uc6-expert-add")',
`${label} UC6 add`);
await waitFor(`${label} create editor`, snapshot =>
snapshot.state.isBusy === false &&
snapshot.state.operatorCatalog?.isOpen === true &&
snapshot.state.operatorCatalog?.entity === "expert" &&
snapshot.state.operatorCatalog?.mode === "create" &&
snapshot.state.operatorCatalog?.draftRows?.length === 0 &&
snapshot.state.operatorCatalog?.writesQuarantined !== true &&
snapshot.dom.operatorCatalogModalHidden === false,
{ timeoutMilliseconds: 60_000, allowUiBusy: true });
await replaceText('document.getElementById("operator-catalog-name")',
title, `${label} name`);
await pointerClick('document.getElementById("operator-catalog-save")',
`${label} create save`);
await waitFor(`${label} create commit`, snapshot =>
snapshot.state.isBusy === false &&
snapshot.state.operatorCatalog == null &&
snapshot.dom.operatorCatalogModalHidden === true,
{ timeoutMilliseconds: 60_000, allowUiBusy: true });
recordFullUiDbCheck("databaseChecks", `${label} create/save`);
const created = await searchAndSelectMainExpert(title, `${label} fresh create read`);
if (created.current.state.expert.preview.items.length !== 0) {
failKnown(`${label} create readback unexpectedly contained recommendations.`);
}
recordFullUiDbCheck("databaseChecks", `${label} fresh create readback`, {
rows: 0
});
await pointerClick('document.getElementById("uc6-expert-edit")',
`${label} UC6 edit`);
await waitFor(`${label} edit loaded`, snapshot =>
snapshot.state.isBusy === false &&
snapshot.state.operatorCatalog?.entity === "expert" &&
snapshot.state.operatorCatalog?.mode === "edit" &&
snapshot.state.operatorCatalog?.editorName === title &&
snapshot.state.operatorCatalog?.draftRows?.length === 0 &&
snapshot.state.operatorCatalog?.writesQuarantined !== true,
{ timeoutMilliseconds: 60_000, allowUiBusy: true });
const first = await addOperatorCatalogStock(
"삼성출판사", "삼성출판사", `${label} first stock`);
const second = await addOperatorCatalogStock(
"삼성전자", "삼성전자", `${label} second stock`);
await setOperatorCatalogBuyAmount(first.row.rowId, 101,
`${label} first buy amount`);
await setOperatorCatalogBuyAmount(second.row.rowId, 202,
`${label} second buy amount`);
await pointerClick(
`document.querySelector('button[data-operator-catalog-move-row-id="${second.row.rowId}"]` +
`[data-operator-catalog-move-direction="up"]')`,
`${label} second row Up`);
await waitFor(`${label} second row Up state`, snapshot =>
snapshot.state.operatorCatalog?.draftRows?.[0]?.rowId === second.row.rowId &&
snapshot.state.operatorCatalog?.draftRows?.[1]?.rowId === first.row.rowId,
{ allowUiBusy: false });
await pointerClick(
`document.querySelector('button[data-operator-catalog-move-row-id="${second.row.rowId}"]` +
`[data-operator-catalog-move-direction="down"]')`,
`${label} second row Down`);
await waitFor(`${label} second row Down state`, snapshot =>
snapshot.state.operatorCatalog?.draftRows?.[0]?.rowId === first.row.rowId &&
snapshot.state.operatorCatalog?.draftRows?.[1]?.rowId === second.row.rowId,
{ allowUiBusy: false });
await pointerClick(
`document.querySelector('button[data-operator-catalog-remove-row-id="${second.row.rowId}"]')`,
`${label} second row Delete`);
await waitFor(`${label} second row Delete state`, snapshot =>
snapshot.state.operatorCatalog?.draftRows?.length === 1 &&
snapshot.state.operatorCatalog?.draftRows?.[0]?.rowId === first.row.rowId,
{ allowUiBusy: false });
const readded = await addOperatorCatalogStock(
"삼성전자", "삼성전자", `${label} second stock re-add`);
await setOperatorCatalogBuyAmount(readded.row.rowId, 202,
`${label} re-added buy amount`);
const readyToSave = await readSnapshot();
if (readyToSave.state.operatorCatalog?.canSave !== true ||
readyToSave.state.operatorCatalog?.draftRows?.length !== 2) {
failKnown(`${label} edit is not saveable with exactly two recommendations.`);
}
await pointerClick('document.getElementById("operator-catalog-save")',
`${label} edit save`);
await respondToNativeDialog(`${label} edit save`, false, true);
const closed = await waitFor(`${label} edit commit`, snapshot =>
snapshot.state.isBusy === false &&
snapshot.state.operatorCatalog == null &&
snapshot.dom.operatorCatalogModalHidden === true,
{ timeoutMilliseconds: 60_000, allowUiBusy: true });
if (closed.state.expert?.selectedExpert?.expertName !== title ||
closed.state.expert?.preview?.items?.length !== 2) {
failKnown(`${label} known-committed edit did not refresh the two-row preview.`);
}
recordFullUiDbCheck("screenChecks", `${label} UC6 add/edit/up/down/delete`);
recordFullUiDbCheck("databaseChecks", `${label} edit/save`, { rows: 2 });
const fresh = await searchAndSelectMainExpert(title, `${label} fresh edit read`);
const previewItems = fresh.current.state.expert.preview.items;
if (previewItems.length !== 2 ||
previewItems[0].stockName !== "삼성출판사" ||
previewItems[0].buyAmount !== 101 ||
previewItems[1].stockName !== "삼성전자" ||
previewItems[1].buyAmount !== 202) {
failKnown(`${label} fresh readback did not match the exact saved recommendations.`);
}
recordFullUiDbCheck("databaseChecks", `${label} fresh edit readback`, {
rows: previewItems.length
});
await pointerClick('document.getElementById("uc6-expert-delete")',
`${label} UC6 delete`);
await respondToNativeDialog(`${label} UC6 delete`, true, true);
// Original UC6 removes the search row but deliberately retains the selected
// expert and preview until the next search. The following fresh search is the
// authoritative database-absence check.
await waitFor(`${label} delete commit`, snapshot =>
snapshot.state.isBusy === false &&
snapshot.state.operatorCatalog == null &&
!snapshot.state.expert?.search?.results?.some(row => row.expertName === title),
{ timeoutMilliseconds: 60_000, allowUiBusy: true });
await assertMainExpertAbsent(title, `${label} fresh absence`);
recordFullUiDbCheck("databaseChecks", `${label} delete/absence`);
}
async function executeOperatorCatalogCrud() {
await createReadEditDeleteTheme(
evidence.fixture.krxThemeTitle,
"krx",
"삼성출판사",
"삼성출판사");
await createReadEditDeleteTheme(
evidence.fixture.nxtThemeTitle,
"nxt",
"삼성전자",
"삼성전자(NXT)");
await createReadEditDeleteExpert(evidence.fixture.expertTitle);
}
async function captureVisibleScreenInventory(tabId) {
const buttons = await evaluate(`Array.from(document.querySelectorAll("button"))
.filter(button => {
const rectangle = button.getBoundingClientRect();
return !button.hidden && !button.closest("[hidden]") &&
rectangle.width > 0 && rectangle.height > 0 &&
getComputedStyle(button).display !== "none" &&
getComputedStyle(button).visibility !== "hidden";
})
.map(button => ({
id: button.id || null,
text: (button.textContent || "").trim(),
disabled: button.disabled === true,
data: Object.fromEntries(Object.entries(button.dataset))
}))`);
evidence.fullUiDb.screenInventory.push({
tabId,
at: new Date().toISOString(),
buttons
});
}
function dataButtonExpression(datasetKey, value) {
return `Array.from(document.querySelectorAll("button"))
.find(button => button.dataset[${JSON.stringify(datasetKey)}] ===
${JSON.stringify(value)})`;
}
function modalButtonByTextExpression(text) {
return `Array.from(document.querySelectorAll(
"#manual-list-workspace button:not([disabled])"))
.find(button => button.textContent.trim() === ${JSON.stringify(text)})`;
}
async function waitForTabModel(tabId, stateProperty, label) {
await selectMainTab(tabId, label);
return await waitFor(`${label} model`, snapshot =>
snapshot.state.isBusy === false && snapshot.state[stateProperty] != null,
{ timeoutMilliseconds: 60_000, allowUiBusy: true });
}
async function clickAndWaitForPlaylistAdd(expression, label, minimumAdded = 1) {
const before = await readSnapshot();
const beforeCount = before.state.playlist.length;
await pointerClick(expression, label);
return await waitFor(`${label} playlist add`, snapshot =>
snapshot.state.isBusy === false &&
snapshot.state.playlist.length >= beforeCount + minimumAdded,
{ timeoutMilliseconds: 60_000, allowUiBusy: true });
}
async function doubleClickAndWaitForPlaylistAdd(expression, label, minimumAdded = 1) {
const before = await readSnapshot();
const beforeCount = before.state.playlist.length;
await pointerDoubleClick(expression, label);
return await waitFor(`${label} playlist add`, snapshot =>
snapshot.state.isBusy === false &&
snapshot.state.playlist.length >= beforeCount + minimumAdded,
{ timeoutMilliseconds: 60_000, allowUiBusy: true });
}
async function clickAllEnabledPlaylistActions(selector, datasetKey, label) {
const actionIds = await evaluate(`Array.from(document.querySelectorAll(
${JSON.stringify(selector)}))
.filter(button => !button.disabled)
.map(button => button.dataset[${JSON.stringify(datasetKey)}])`);
if (!Array.isArray(actionIds) || actionIds.length === 0 ||
actionIds.some(value => typeof value !== "string" || value.length === 0)) {
failKnown(`${label} exposed no enabled action buttons.`);
}
for (const actionId of actionIds) {
await clickAndWaitForPlaylistAdd(
dataButtonExpression(datasetKey, actionId),
`${label} ${actionId}`);
}
return actionIds;
}
async function doubleClickAllEnabledPlaylistActions(selector, datasetKey, label) {
const actionIds = await evaluate(`Array.from(document.querySelectorAll(
${JSON.stringify(selector)}))
.filter(button => !button.disabled)
.map(button => button.dataset[${JSON.stringify(datasetKey)}])`);
if (!Array.isArray(actionIds) || actionIds.length === 0 ||
actionIds.some(value => typeof value !== "string" || value.length === 0)) {
failKnown(`${label} exposed no enabled action buttons.`);
}
for (const actionId of actionIds) {
await doubleClickAndWaitForPlaylistAdd(
dataButtonExpression(datasetKey, actionId),
`${label} ${actionId}`);
}
return actionIds;
}
async function verifyFixedAndIndustryScreens() {
await waitForTabModel("overseas", "fixedCatalog", "UC1 overseas tab");
await pointerClick('document.getElementById("catalog-expand-all")',
"UC1 Expand off");
if (await evaluate('document.getElementById("catalog-expand-all").checked') !== false) {
failKnown("UC1 Expand did not close the fixed tree.");
}
await pointerClick('document.getElementById("catalog-expand-all")',
"UC1 Expand on");
if (await evaluate('document.getElementById("catalog-expand-all").checked') !== true) {
failKnown("UC1 Expand did not reopen the fixed tree.");
}
await doubleClickAndWaitForPlaylistAdd(dataButtonExpression("actionId", "fixed-001"),
"UC1 overseas leaf");
const beforeSection = await readSnapshot();
await pointerDoubleClick(
`document.querySelector('summary[data-section-index="1"]')`,
"UC1 fixed three-row section");
await waitFor("UC1 fixed section batch", snapshot =>
snapshot.state.isBusy === false &&
snapshot.state.playlist.length === beforeSection.state.playlist.length + 3,
{ timeoutMilliseconds: 60_000, allowUiBusy: true });
await waitForTabModel("exchange", "fixedCatalog", "UC1 exchange tab");
await doubleClickAndWaitForPlaylistAdd(dataButtonExpression("actionId", "fixed-079"),
"UC1 exchange leaf");
await waitForTabModel("index", "fixedCatalog", "UC1 index tab");
await doubleClickAndWaitForPlaylistAdd(dataButtonExpression("actionId", "fixed-125"),
"UC1 index leaf");
recordFullUiDbCheck("screenChecks", "UC1 Expand, leaf, and parent-section buttons");
for (const tabId of ["kospiIndustry", "kosdaqIndustry"]) {
await waitForTabModel(tabId, "industry", `UC2 ${tabId} tab`);
await pointerClick(dataButtonExpression("industryIndex", "0"),
`UC2 ${tabId} first industry`);
await waitFor(`UC2 ${tabId} first selection`, snapshot =>
snapshot.state.isBusy === false && snapshot.state.industry?.selectedIndex === 0,
{ timeoutMilliseconds: 60_000, allowUiBusy: true });
await pointerDoubleClick(dataButtonExpression("industryIndex", "0"),
`UC2 ${tabId} first industry double-click`);
await waitFor(`UC2 ${tabId} first comparison slot`, snapshot =>
snapshot.state.isBusy === false &&
Boolean(snapshot.state.industry?.firstName),
{ timeoutMilliseconds: 60_000, allowUiBusy: true });
await pointerDoubleClick(dataButtonExpression("industryIndex", "1"),
`UC2 ${tabId} second industry double-click`);
const rotated = await waitFor(`UC2 ${tabId} comparison slots`, snapshot =>
snapshot.state.isBusy === false &&
Boolean(snapshot.state.industry?.firstName) &&
Boolean(snapshot.state.industry?.secondName),
{ timeoutMilliseconds: 60_000, allowUiBusy: true });
const firstName = rotated.state.industry.firstName;
const secondName = rotated.state.industry.secondName;
await pointerClick(dataButtonExpression("industrySwap", "true"),
`UC2 ${tabId} swap`);
await waitFor(`UC2 ${tabId} swapped slots`, snapshot =>
snapshot.state.industry?.firstName === secondName &&
snapshot.state.industry?.secondName === firstName,
{ allowUiBusy: false });
await doubleClickAndWaitForPlaylistAdd(
dataButtonExpression("industryActionId", "one-column"),
`UC2 ${tabId} one-column action`);
}
recordFullUiDbCheck("screenChecks", "UC2 KOSPI/KOSDAQ select, double-click, swap, action");
}
function comparisonBaseline(snapshot) {
return snapshot.state.comparison.savedPairs.map(row => ({
rowId: row.rowId,
displayLabel: row.displayLabel,
first: comparisonTargetIdentity(row.first),
second: comparisonTargetIdentity(row.second)
}));
}
function comparisonTargetIdentity(target) {
return target == null ? null : {
displayName: target.displayName,
kind: target.kind,
metadata: target.metadata
};
}
async function verifyComparisonScreen() {
let current = await waitForTabModel("comparison", "comparison", "UC3 comparison tab");
const baseline = comparisonBaseline(current);
await replaceText('document.getElementById("comparison-domestic-search")',
"삼성", "UC3 domestic query");
await pointerClick(dataButtonExpression("comparisonSearch", "domestic"),
"UC3 domestic search");
current = await waitFor("UC3 domestic results", snapshot =>
snapshot.state.isBusy === false &&
snapshot.state.comparison?.domesticSearch?.query === "삼성" &&
snapshot.state.comparison.domesticSearch.results.length > 1,
{ timeoutMilliseconds: 60_000, allowUiBusy: true });
const domesticId = current.state.comparison.domesticSearch.results[0].selectionId;
const secondDomesticId = current.state.comparison.domesticSearch.results[1].selectionId;
await pointerClick(dataButtonExpression("comparisonSelectionId", domesticId),
"UC3 domestic result single-click");
await waitFor("UC3 domestic result selected", snapshot =>
snapshot.state.comparison?.domesticSearch?.selectedResultId === domesticId,
{ timeoutMilliseconds: 60_000, allowUiBusy: true });
await replaceText('document.getElementById("comparison-world-search")',
"3M", "UC3 world query");
await pointerClick(dataButtonExpression("comparisonSearch", "world"),
"UC3 world search");
current = await waitFor("UC3 world results", snapshot =>
snapshot.state.isBusy === false &&
snapshot.state.comparison?.worldSearch?.query === "3M" &&
snapshot.state.comparison?.worldSearch?.status === "ready" &&
snapshot.state.comparison.worldSearch.results.length > 0,
{ timeoutMilliseconds: 60_000, allowUiBusy: true });
if (current.state.comparison.firstTarget != null ||
current.state.comparison.secondTarget != null) {
failKnown("UC3 fresh tab activation did not start with empty draft targets.");
}
await pointerDoubleClick(dataButtonExpression("comparisonSelectionId", domesticId),
"UC3 choose first domestic target");
await waitFor("UC3 first domestic target", snapshot =>
snapshot.state.comparison?.firstTarget != null,
{ timeoutMilliseconds: 60_000, allowUiBusy: true });
await pointerDoubleClick(dataButtonExpression("comparisonSelectionId", secondDomesticId),
"UC3 choose second domestic target");
current = await waitFor("UC3 second domestic target", snapshot =>
snapshot.state.comparison?.firstTarget != null &&
snapshot.state.comparison?.secondTarget != null,
{ timeoutMilliseconds: 60_000, allowUiBusy: true });
const firstTargetIdentity = comparisonTargetIdentity(
current.state.comparison.firstTarget);
const secondTargetIdentity = comparisonTargetIdentity(
current.state.comparison.secondTarget);
await pointerClick(dataButtonExpression("comparisonSwap", "true"), "UC3 swap draft");
await waitFor("UC3 swapped draft", snapshot =>
JSON.stringify(comparisonTargetIdentity(snapshot.state.comparison?.firstTarget)) ===
JSON.stringify(secondTargetIdentity) &&
JSON.stringify(comparisonTargetIdentity(snapshot.state.comparison?.secondTarget)) ===
JSON.stringify(firstTargetIdentity),
{ allowUiBusy: false });
await pointerClick(dataButtonExpression("comparisonSwap", "true"),
"UC3 restore draft order");
await waitFor("UC3 restored draft", snapshot =>
JSON.stringify(comparisonTargetIdentity(snapshot.state.comparison?.firstTarget)) ===
JSON.stringify(firstTargetIdentity) &&
JSON.stringify(comparisonTargetIdentity(snapshot.state.comparison?.secondTarget)) ===
JSON.stringify(secondTargetIdentity),
{ allowUiBusy: false });
await pointerClick(dataButtonExpression("comparisonAdd", "true"),
"UC3 add first temporary comparison pair");
current = await waitFor("UC3 first temporary pair commit", snapshot =>
snapshot.state.isBusy === false &&
snapshot.state.comparison?.savedPairs?.length === baseline.length + 1 &&
snapshot.state.comparison?.selectedPairId != null,
{ timeoutMilliseconds: 60_000, allowUiBusy: true });
const firstTemporaryRowId = current.state.comparison.selectedPairId;
await pointerClick(dataButtonExpression("comparisonAdd", "true"),
"UC3 add second temporary comparison pair");
current = await waitFor("UC3 second temporary pair commit", snapshot =>
snapshot.state.isBusy === false &&
snapshot.state.comparison?.savedPairs?.length === baseline.length + 2 &&
snapshot.state.comparison?.selectedPairId != null &&
snapshot.state.comparison.selectedPairId !== firstTemporaryRowId,
{ timeoutMilliseconds: 60_000, allowUiBusy: true });
const secondTemporaryRowId = current.state.comparison.selectedPairId;
await pointerClick(dataButtonExpression("comparisonMove", "up"),
"UC3 second temporary pair UP");
await waitFor("UC3 second temporary pair UP commit", snapshot =>
snapshot.state.isBusy === false &&
snapshot.state.comparison?.savedPairs?.[baseline.length]?.rowId ===
secondTemporaryRowId,
{ timeoutMilliseconds: 60_000, allowUiBusy: true });
await pointerClick(dataButtonExpression("comparisonMove", "down"),
"UC3 second temporary pair DOWN");
await waitFor("UC3 second temporary pair DOWN commit", snapshot =>
snapshot.state.isBusy === false &&
snapshot.state.comparison?.savedPairs?.at(-1)?.rowId === secondTemporaryRowId,
{ timeoutMilliseconds: 60_000, allowUiBusy: true });
await pointerDoubleClick(dataButtonExpression("comparisonTargetId", "kospi"),
"UC3 fixed target double-click");
await waitFor("UC3 fixed target draft", snapshot =>
snapshot.state.comparison?.selectedFixedTargetId === "kospi" &&
snapshot.state.comparison?.firstTarget?.displayName === "코스피 지수",
{ timeoutMilliseconds: 60_000, allowUiBusy: true });
await pointerClick(dataButtonExpression("comparisonDelete", "all"),
"UC3 DEL ALL");
await respondToNativeDialog("UC3 DEL ALL cancel", true, false);
const comparisonActions = await doubleClickAllEnabledPlaylistActions(
"button[data-comparison-action-id]", "comparisonActionId", "UC3 action");
await pointerClick(dataButtonExpression("comparisonDelete", "selected"),
"UC3 delete second temporary pair");
current = await waitFor("UC3 second temporary pair delete commit", snapshot =>
snapshot.state.isBusy === false &&
snapshot.state.comparison?.savedPairs?.length === baseline.length + 1 &&
!snapshot.state.comparison.savedPairs.some(
row => row.rowId === secondTemporaryRowId),
{ timeoutMilliseconds: 60_000, allowUiBusy: true });
await pointerClick(dataButtonExpression("comparisonPairId", firstTemporaryRowId),
"UC3 select first temporary pair");
await waitFor("UC3 first temporary pair selected", snapshot =>
snapshot.state.comparison?.selectedPairId === firstTemporaryRowId,
{ timeoutMilliseconds: 60_000, allowUiBusy: true });
await pointerClick(dataButtonExpression("comparisonDelete", "selected"),
"UC3 delete first temporary pair");
current = await waitFor("UC3 first temporary pair delete commit", snapshot =>
snapshot.state.isBusy === false &&
snapshot.state.comparison?.savedPairs?.length === baseline.length &&
!snapshot.state.comparison.savedPairs.some(
row => row.rowId === firstTemporaryRowId),
{ timeoutMilliseconds: 60_000, allowUiBusy: true });
if (JSON.stringify(comparisonBaseline(current)) !== JSON.stringify(baseline)) {
failKnown("UC3 baseline changed after deleting the temporary pair.");
}
await waitForTabModel("overseas", "fixedCatalog", "UC3 fresh-read detour");
current = await waitForTabModel("comparison", "comparison", "UC3 fresh-read return");
if (JSON.stringify(comparisonBaseline(current)) !== JSON.stringify(baseline)) {
failKnown("UC3 persisted baseline did not survive fresh re-entry exactly.");
}
recordFullUiDbCheck("screenChecks", "UC3 all control buttons and temporary pair restore", {
baselineRows: baseline.length,
actionButtons: comparisonActions.length
});
recordFullUiDbCheck("databaseChecks", "UC3 local pair save/fresh read/delete/restore", {
rows: baseline.length
});
}
async function verifyThemeScreen() {
let current = await waitForTabModel("theme", "theme", "UC4 theme tab");
await replaceText('document.getElementById("theme-search")', "",
"UC4 theme query");
await pointerClick(dataButtonExpression("themeSearch", "true"), "UC4 theme search");
current = await waitFor("UC4 theme search results", snapshot =>
snapshot.state.isBusy === false &&
snapshot.state.theme?.searchResults?.length > 0,
{ timeoutMilliseconds: 60_000, allowUiBusy: true });
const resultId = current.state.theme.searchResults[0].resultId;
await pointerClick(dataButtonExpression("themeResultId", resultId),
"UC4 theme row single-click");
await waitFor("UC4 theme preview", snapshot =>
snapshot.state.isBusy === false &&
snapshot.state.theme?.selectedResultId === resultId &&
snapshot.state.theme?.selectedTheme != null &&
snapshot.state.theme?.previewItems?.length > 0,
{ timeoutMilliseconds: 60_000, allowUiBusy: true });
for (const sortId of ["GAIN_DESC", "GAIN_ASC", "INPUT_ORDER"]) {
await pointerClick(
`document.querySelector('input[data-theme-sort][value="${sortId}"]')`,
`UC4 theme sort ${sortId}`);
await waitFor(`UC4 theme sort ${sortId} state`, snapshot =>
snapshot.state.theme?.sortId === sortId,
{ allowUiBusy: false });
}
const beforeDouble = await readSnapshot();
await pointerDoubleClick(dataButtonExpression("themeResultId", resultId),
"UC4 theme row double-click");
await waitFor("UC4 theme double-click add", snapshot =>
snapshot.state.playlist.length === beforeDouble.state.playlist.length + 1,
{ timeoutMilliseconds: 60_000, allowUiBusy: true });
const actions = await clickAllEnabledPlaylistActions(
"button[data-theme-action-id]", "themeActionId", "UC4 action");
recordFullUiDbCheck("screenChecks", "UC4 search, row, sort, double-click, all actions", {
actionButtons: actions.length
});
}
async function verifyOverseasScreen() {
let current = await waitForTabModel("overseasStocks", "overseas", "UC5 overseas tab");
const fixedTarget = current.state.overseas.fixedIndexTargets[1] ||
current.state.overseas.fixedIndexTargets[0];
await pointerClick(dataButtonExpression("overseasTargetId", fixedTarget.targetId),
"UC5 fixed index row");
await waitFor("UC5 fixed index selected", snapshot =>
snapshot.state.overseas?.selectedFixedIndexId === fixedTarget.targetId,
{ timeoutMilliseconds: 60_000, allowUiBusy: true });
const fixedActions = await clickAllEnabledPlaylistActions(
"button[data-overseas-action-id]", "overseasActionId", "UC5 fixed action");
await replaceText('document.getElementById("overseas-industry-search")', "",
"UC5 industry query");
await pointerClick(dataButtonExpression("overseasSearch", "industry"),
"UC5 industry search");
current = await waitFor("UC5 industry results", snapshot =>
snapshot.state.isBusy === false &&
snapshot.state.overseas?.industry?.results?.length > 0,
{ timeoutMilliseconds: 60_000, allowUiBusy: true });
const industryId = current.state.overseas.industry.results[0].selectionId;
await pointerClick(dataButtonExpression("overseasSelectionId", industryId),
"UC5 industry result");
await waitFor("UC5 industry selected", snapshot =>
snapshot.state.overseas?.industry?.selectedId === industryId,
{ timeoutMilliseconds: 60_000, allowUiBusy: true });
const industryActionIds = current.state.overseas.actions
.filter(action => action.source === "industry")
.map(action => action.actionId);
for (const actionId of industryActionIds) {
await clickAndWaitForPlaylistAdd(dataButtonExpression("overseasActionId", actionId),
`UC5 industry action ${actionId}`);
}
await replaceText('document.getElementById("overseas-stock-search")', "",
"UC5 stock query");
await pointerClick(dataButtonExpression("overseasSearch", "stock"),
"UC5 stock search");
current = await waitFor("UC5 stock results", snapshot =>
snapshot.state.isBusy === false &&
snapshot.state.overseas?.stock?.results?.length > 0,
{ timeoutMilliseconds: 60_000, allowUiBusy: true });
const stockId = current.state.overseas.stock.results[0].selectionId;
await pointerClick(dataButtonExpression("overseasSelectionId", stockId),
"UC5 stock result");
current = await waitFor("UC5 stock selected", snapshot =>
snapshot.state.overseas?.stock?.selectedId === stockId,
{ timeoutMilliseconds: 60_000, allowUiBusy: true });
const stockActionIds = current.state.overseas.actions
.filter(action => action.source === "stock")
.map(action => action.actionId);
for (const actionId of stockActionIds) {
await clickAndWaitForPlaylistAdd(dataButtonExpression("overseasActionId", actionId),
`UC5 stock action ${actionId}`);
}
recordFullUiDbCheck("screenChecks", "UC5 fixed, industry, stock search/select/actions", {
fixedActions: fixedActions.length,
industryActions: industryActionIds.length,
stockActions: stockActionIds.length
});
}
async function verifyExpertAndTradingHaltScreens() {
let current = await waitForTabModel("expert", "expert", "UC6 expert tab");
await replaceText('document.getElementById("expert-search")', "", "UC6 expert query");
const expertSearchRevision = current.state.revision;
await pointerClick(dataButtonExpression("expertSearch", "true"), "UC6 expert search");
current = await waitFor("UC6 expert results", snapshot =>
snapshot.state.isBusy === false &&
snapshot.state.revision > expertSearchRevision &&
snapshot.state.expert?.search?.results?.length > 0,
{ timeoutMilliseconds: 60_000, allowUiBusy: true });
const expertId = current.state.expert.search.results[0].selectionId;
await pointerClick(dataButtonExpression("expertSelectionId", expertId),
"UC6 expert row");
await waitFor("UC6 expert preview", snapshot =>
snapshot.state.isBusy === false &&
snapshot.state.expert?.selectedExpert?.selectionId === expertId &&
snapshot.state.expert?.preview?.items?.length > 0,
{ timeoutMilliseconds: 60_000, allowUiBusy: true });
const expertActions = await clickAllEnabledPlaylistActions(
"button[data-expert-action-id]", "expertActionId", "UC6 action");
recordFullUiDbCheck("screenChecks", "UC6 search, row, action", {
actionButtons: expertActions.length
});
current = await waitForTabModel("tradingHalt", "tradingHalt", "UC7 trading-halt tab");
await replaceText('document.getElementById("halt-search")', "", "UC7 halt query");
const haltSearchRevision = current.state.revision;
await pointerClick(dataButtonExpression("haltSearch", "true"), "UC7 halt search");
current = await waitFor("UC7 halt results", snapshot =>
snapshot.state.isBusy === false &&
snapshot.state.revision > haltSearchRevision &&
snapshot.state.tradingHalt?.status === "ready" &&
snapshot.state.tradingHalt?.results?.length > 0,
{ timeoutMilliseconds: 60_000, allowUiBusy: true });
const beforeSort = current.state.tradingHalt.nameSort;
await pointerClick(dataButtonExpression("haltSort", "true"), "UC7 halt sort");
current = await waitFor("UC7 halt sort state", snapshot =>
snapshot.state.tradingHalt?.nameSort !== beforeSort,
{ allowUiBusy: false });
const haltId = current.state.tradingHalt.results[0].selectionId;
await pointerClick(dataButtonExpression("haltSelectionId", haltId), "UC7 halt row");
await waitFor("UC7 halt selection", snapshot =>
snapshot.state.tradingHalt?.selected?.selectionId === haltId,
{ timeoutMilliseconds: 60_000, allowUiBusy: true });
const haltActions = await clickAllEnabledPlaylistActions(
"button[data-halt-action-id]", "haltActionId", "UC7 action");
recordFullUiDbCheck("screenChecks", "UC7 search, sort, row, all actions", {
actionButtons: haltActions.length
});
}
async function verifyGraphEActivationButtons() {
await executeManualFinancialCrud();
}
async function openFixedManualList(actionId, screen, audience, label) {
await waitForTabModel("index", "fixedCatalog", `${label} index tab`);
await pointerDoubleClick(dataButtonExpression("actionId", actionId), `${label} open`);
return await waitFor(`${label} loaded`, snapshot =>
snapshot.state.isBusy === false &&
snapshot.state.manualLists?.isOpen === true &&
snapshot.state.manualLists?.screen === screen &&
(audience == null || snapshot.state.manualLists?.audience === audience) &&
snapshot.dom.manualListModalHidden === false &&
snapshot.state.manualLists?.writesQuarantined !== true,
{ timeoutMilliseconds: 60_000, allowUiBusy: true });
}
async function closeManualList(label) {
await pointerClick('document.getElementById("manual-list-close")', label);
await waitFor(`${label} complete`, snapshot =>
snapshot.state.isBusy === false &&
snapshot.state.manualLists?.isOpen === false &&
snapshot.dom.manualListModalHidden === true,
{ timeoutMilliseconds: 60_000, allowUiBusy: true });
}
function netSellProjection(snapshot) {
return snapshot.state.manualLists.netRows.map(row => ({
leftName: row.leftName,
leftAmount: row.leftAmount,
rightName: row.rightName,
rightAmount: row.rightAmount
}));
}
function viProjection(snapshot) {
return snapshot.state.manualLists.viItems.map(row => ({
index: row.index,
name: row.name
}));
}
async function verifyManualListButtonsAndPersistence() {
let current = await openFixedManualList(
"fixed-245", "net-sell", "individual", "FSell individual");
const individualBaseline = netSellProjection(current);
if (individualBaseline.length !== 5 || current.state.manualLists.canSave !== true) {
failKnown("FSell individual baseline is not the original five-row saveable state.");
}
await pointerClickWithJavaScriptDialog(
modalButtonByTextExpression("원본 1회 가져오기"),
"FSell import cancel branch",
"confirm",
"원본 파일을 현재 빈 저장소로 한 번만 가져옵니다. 기존 파일은 덮어쓰지 않습니다.",
false);
const beforeRefresh = await readSnapshot();
await pointerClick(modalButtonByTextExpression("재조회"), "FSell refresh");
await waitFor("FSell refresh result", snapshot =>
snapshot.state.isBusy === false &&
snapshot.state.revision > beforeRefresh.state.revision &&
snapshot.state.manualLists?.netRowsAreFresh === true,
{ timeoutMilliseconds: 60_000, allowUiBusy: true });
for (const [label, audience] of [["외국인", "foreign"], ["기관", "institution"],
["개인", "individual"]]) {
await pointerClick(modalButtonByTextExpression(label), `FSell ${label} tab`);
await waitFor(`FSell ${label} tab state`, snapshot =>
snapshot.state.isBusy === false &&
snapshot.state.manualLists?.audience === audience &&
snapshot.state.manualLists?.netRows?.length === 5,
{ timeoutMilliseconds: 60_000, allowUiBusy: true });
}
await closeManualList("FSell cancel button");
await openFixedManualList("fixed-246", "net-sell", "foreign", "FSell foreign entry");
await closeManualList("FSell foreign close");
await openFixedManualList("fixed-247", "net-sell", "institution",
"FSell institution entry");
await closeManualList("FSell institution close");
current = await openFixedManualList(
"fixed-245", "net-sell", "individual", "FSell save reopen");
const beforeFsellSaveCount = current.state.playlist.length;
await pointerClick(modalButtonByTextExpression("확인"), "FSell confirm save");
await waitFor("FSell save, readback, add, close", snapshot =>
snapshot.state.isBusy === false &&
snapshot.state.manualLists?.isOpen === false &&
snapshot.dom.manualListModalHidden === true &&
snapshot.state.playlist.length === beforeFsellSaveCount + 1,
{ timeoutMilliseconds: 60_000, allowUiBusy: true });
current = await openFixedManualList(
"fixed-245", "net-sell", "individual", "FSell fresh read");
if (JSON.stringify(netSellProjection(current)) !== JSON.stringify(individualBaseline)) {
failKnown("FSell fresh read did not preserve the exact five-row baseline.");
}
await closeManualList("FSell fresh read close");
recordFullUiDbCheck("screenChecks", "FSell all entry/tab/import/refresh/cancel/confirm buttons");
recordFullUiDbCheck("databaseChecks", "FSell trusted file save/fresh read", { rows: 5 });
current = await openFixedManualList("fixed-262", "vi", null, "VIList entry");
const viBaseline = viProjection(current);
if (viBaseline.length === 0 || current.state.manualLists.canSave !== true) {
failKnown("VIList baseline is empty or not saveable.");
}
const beforeViRefresh = await readSnapshot();
await pointerClick(modalButtonByTextExpression("재조회"), "VIList refresh");
await waitFor("VIList refresh result", snapshot =>
snapshot.state.isBusy === false &&
snapshot.state.revision > beforeViRefresh.state.revision &&
snapshot.state.manualLists?.viItemsAreFresh === true,
{ timeoutMilliseconds: 60_000, allowUiBusy: true });
await replaceText(
'document.querySelector("#manual-list-workspace .manual-list-search input")',
"삼성출판사", "VIList stock query");
await pointerClick(modalButtonByTextExpression("검색"), "VIList search");
current = await waitFor("VIList search results", snapshot =>
snapshot.state.isBusy === false &&
snapshot.state.manualLists?.searchResults?.some(row => row.displayName === "삼성출판사"),
{ timeoutMilliseconds: 60_000, allowUiBusy: true });
const viResult = current.state.manualLists.searchResults
.find(row => row.displayName === "삼성출판사");
const viResultExpression = `Array.from(document.querySelectorAll(
"#manual-list-workspace .manual-list-search-results button"))
.find(button => button.textContent.startsWith("삼성출판사 ·"))`;
await pointerClick(viResultExpression, "VIList result single-click");
await waitFor("VIList result selected", snapshot =>
snapshot.state.manualLists?.selectedSearchResultId === viResult.resultId,
{ timeoutMilliseconds: 60_000, allowUiBusy: true });
await pointerDoubleClick(viResultExpression, "VIList result double-click add");
current = await waitFor("VIList temporary item add", snapshot =>
snapshot.state.manualLists?.viItems?.length === viBaseline.length + 1,
{ timeoutMilliseconds: 60_000, allowUiBusy: true });
const temporaryItemId = current.state.manualLists.viItems.at(-1).itemId;
await pointerClick(
'document.querySelector("#manual-list-workspace tbody tr:last-child button:nth-of-type(1)")',
"VIList temporary item UP");
await waitFor("VIList temporary item UP state", snapshot =>
snapshot.state.manualLists?.viItems?.at(-2)?.itemId === temporaryItemId,
{ allowUiBusy: false });
await pointerClick(
`Array.from(document.querySelectorAll("#manual-list-workspace tbody tr"))
.find((row, index) => index === ${viBaseline.length - 1})
?.querySelector("button:nth-of-type(2)")`,
"VIList temporary item DOWN");
await waitFor("VIList temporary item DOWN state", snapshot =>
snapshot.state.manualLists?.viItems?.at(-1)?.itemId === temporaryItemId,
{ allowUiBusy: false });
await pointerClick(
'document.querySelector("#manual-list-workspace tbody tr:last-child button:nth-of-type(3)")',
"VIList temporary item delete");
await waitFor("VIList temporary item delete state", snapshot =>
snapshot.state.manualLists?.viItems?.length === viBaseline.length &&
!snapshot.state.manualLists.viItems.some(row => row.itemId === temporaryItemId),
{ allowUiBusy: false });
await pointerClickWithJavaScriptDialog(
modalButtonByTextExpression("전체 삭제"),
"VIList delete-all cancel branch",
"confirm",
"VI 목록을 모두 지우시겠습니까?",
false);
const beforeViSave = await readSnapshot();
await pointerClick(modalButtonByTextExpression("확인"), "VIList confirm save");
await waitFor("VIList save, readback, add, close", snapshot =>
snapshot.state.isBusy === false &&
snapshot.state.manualLists?.isOpen === false &&
snapshot.dom.manualListModalHidden === true &&
snapshot.state.playlist.length === beforeViSave.state.playlist.length + 1,
{ timeoutMilliseconds: 60_000, allowUiBusy: true });
current = await openFixedManualList("fixed-262", "vi", null, "VIList fresh read");
if (JSON.stringify(viProjection(current)) !== JSON.stringify(viBaseline)) {
failKnown("VIList fresh read did not preserve the exact baseline.");
}
await closeManualList("VIList fresh read close");
recordFullUiDbCheck("screenChecks", "VIList search/select/double-click/up/down/delete/all/refresh/confirm buttons");
recordFullUiDbCheck("databaseChecks", "VIList trusted file save/fresh read", {
rows: viBaseline.length
});
}
async function clearScreenValidationPlaylist() {
const current = await readSnapshot();
if (current.state.playlist.length === 0) return;
await pointerClickWithJavaScriptDialog(
'document.getElementById("playlist-delete-all")',
"screen validation playlist cleanup",
"confirm",
"송출 리스트를 모두 삭제하겠습니까?",
true);
await waitFor("screen validation playlist cleanup state", snapshot =>
snapshot.state.isBusy === false && snapshot.state.playlist.length === 0,
{ allowUiBusy: false });
}
async function executeScreenInventory() {
await verifyFixedAndIndustryScreens();
await verifyComparisonScreen();
await verifyThemeScreen();
await verifyOverseasScreen();
await verifyExpertAndTradingHaltScreens();
await verifyGraphEActivationButtons();
await verifyManualListButtonsAndPersistence();
for (const [tabId, stateProperty] of [
["overseas", "fixedCatalog"], ["exchange", "fixedCatalog"],
["index", "fixedCatalog"], ["kospiIndustry", "industry"],
["kosdaqIndustry", "industry"], ["comparison", "comparison"],
["theme", "theme"], ["overseasStocks", "overseas"],
["expert", "expert"], ["tradingHalt", "tradingHalt"]
]) {
await waitForTabModel(tabId, stateProperty, `final screen inventory ${tabId}`);
await captureVisibleScreenInventory(tabId);
}
await clearScreenValidationPlaylist();
recordFullUiDbCheck("screenChecks", "UC1-UC7 final packaged button inventory");
}
async function executeFullUiDbSequence() {
if (!fullUiDbProfile) return;
if (configuration.scope === "all" || configuration.scope === "plist") {
await executeNamedPlaylistCrud();
}
if (configuration.scope === "all" || configuration.scope === "graphe") {
await executeManualFinancialCrud();
}
if (configuration.scope === "all" || configuration.scope === "catalog") {
await executeOperatorCatalogCrud();
}
if (configuration.scope === "all" || configuration.scope === "screens") {
await executeScreenInventory();
}
evidence.fullUiDb.result = "PASS";
evidence.fullUiDb.cleanupVerified = true;
const current = await readSnapshot();
if (!current.dom.namedModalHidden || !current.dom.namedConfirmationHidden ||
!current.dom.manualFinancialModalHidden || !current.dom.manualListModalHidden ||
!current.dom.operatorCatalogModalHidden) {
failKnown("A child modal remained open after the full UI/DB sequence.");
}
evidence.finalSnapshot = current;
}
try {
const target = await discoverExactTarget();
const endpoint = validateWebSocketTarget(target);
evidence.target.id = target.id;
evidence.target.url = target.url;
await connect(endpoint);
await executeSafeSequence();
await executeDryRunPlayoutSequence();
await executeFullUiDbSequence();
await captureScreenshot();
const sealedSnapshot = await readSnapshot();
assertSafety(sealedSnapshot, "post-screenshot safety seal", false);
evidence.finalSnapshot = sealedSnapshot;
evidence.result = evidence.warnings.length === 0 ? "PASS" : "PASS_WITH_WARNING";
} catch (error) {
evidence.result = "FAIL";
evidence.error = errorProjection(error);
process.exitCode = 1;
if (socket?.readyState === WebSocket.OPEN) {
try {
evidence.finalSnapshot = await readSnapshot();
} catch (snapshotError) {
evidence.warnings.push(`Failure snapshot unavailable: ${snapshotError.message}`);
}
try {
await captureScreenshot();
} catch (screenshotError) {
evidence.warnings.push(`Failure screenshot unavailable: ${screenshotError.message}`);
}
}
} finally {
cleanupMode = true;
cleanupDeadlineEpochMilliseconds = Date.now() + 5_000;
const cleanupFailures = [];
if ((pointerIsPressed || dragInterceptionEnabled || activeInterceptedDragData) &&
socket?.readyState === WebSocket.OPEN) {
try {
await cleanupDragInputSession(true);
} catch (inputCleanupError) {
evidence.warnings.push(`Input cleanup failed: ${inputCleanupError.message}`);
cleanupFailures.push(`input cleanup: ${inputCleanupError.message}`);
}
} else if ((pointerIsPressed || dragInterceptionEnabled || activeInterceptedDragData) &&
socket?.readyState !== WebSocket.OPEN) {
evidence.warnings.push(
"Input cleanup failed: the CDP socket closed before drag/pointer restoration.");
cleanupFailures.push("input cleanup: CDP socket closed");
}
if (socket?.readyState === WebSocket.OPEN) {
try {
await removeProbe();
} catch (probeError) {
evidence.warnings.push(`Probe cleanup failed: ${probeError.message}`);
cleanupFailures.push(`probe restore: ${probeError.message}`);
}
socket.close();
} else if (probeInstalled) {
evidence.warnings.push(
"Probe cleanup failed: the CDP socket was closed before bridge restoration.");
cleanupFailures.push("probe restore: CDP socket closed");
}
if (cleanupFailures.length > 0 && evidence.result !== "FAIL") {
evidence.result = "FAIL";
evidence.error = {
category: "OUTCOME_UNKNOWN",
name: "CleanupFailure",
message: "The one-shot input cleanup did not complete."
};
process.exitCode = 1;
}
evidence.completedAt = new Date().toISOString();
fs.writeFileSync(
configuration.outputPath,
`${JSON.stringify(evidence, null, 2)}\n`,
{ flag: "wx" });
}
if (evidence.result === "FAIL") {
console.error(JSON.stringify({
result: evidence.result,
output: configuration.outputPath,
screenshot: evidence.screenshot?.path || null,
error: evidence.error
}));
} else {
console.log(JSON.stringify({
result: evidence.result,
output: configuration.outputPath,
screenshot: evidence.screenshot
}));
}