638 lines
26 KiB
JavaScript
638 lines
26 KiB
JavaScript
"use strict";
|
|
|
|
const assert = require("node:assert/strict");
|
|
const fs = require("node:fs");
|
|
const path = require("node:path");
|
|
const test = require("node:test");
|
|
|
|
const repositoryRoot = path.resolve(__dirname, "..", "..");
|
|
const webRoot = path.join(repositoryRoot, "src", "MBN_STOCK_WEBVIEW.LegacyParityApp", "Web");
|
|
const app = fs.readFileSync(path.join(webRoot, "app.js"), "utf8");
|
|
const playout = fs.readFileSync(path.join(webRoot, "playout-ui.js"), "utf8");
|
|
const markup = fs.readFileSync(path.join(webRoot, "index.html"), "utf8");
|
|
|
|
test("legacy shell exposes the original stock, cut, playlist and playout controls", () => {
|
|
for (const id of [
|
|
"stock-search",
|
|
"stock-search-button",
|
|
"stock-results",
|
|
"market-tabs",
|
|
"category-tree",
|
|
"catalog-expand-all",
|
|
"moving-average-5",
|
|
"moving-average-20",
|
|
"cut-list",
|
|
"playlist-drop-zone",
|
|
"playlist-rows",
|
|
"playlist-enable-all",
|
|
"playlist-move-up",
|
|
"playlist-move-down",
|
|
"playlist-delete-selected",
|
|
"playlist-delete-all",
|
|
"db-save-button",
|
|
"db-load-button",
|
|
"named-playlist-modal",
|
|
"named-playlist-definitions",
|
|
"named-playlist-new-title",
|
|
"named-playlist-create-button",
|
|
"named-playlist-delete-button",
|
|
"named-playlist-confirm-button",
|
|
"legacy-dialog"
|
|
]) {
|
|
assert.match(markup, new RegExp("id=\\\"" + id + "\\\""));
|
|
}
|
|
assert.match(markup, /TAKE IN/);
|
|
assert.match(markup, /TAKE OUT/);
|
|
assert.match(markup, /NEXT/);
|
|
assert.match(markup, /class="connection-state"[^>]*>미연결<\/div>/);
|
|
assert.match(markup, /class="active"[^>]*data-tab-id="overseas">해외<\/button>/);
|
|
assert.match(markup, /data-tab-id="tradingHalt">정지<\/button>/);
|
|
});
|
|
|
|
test("web layer sends intent and does not own scene, DB identity or playlist state", () => {
|
|
assert.doesNotMatch(app, /(?:sceneCode|cutCode|stockCode|dataCode|builderKey)/);
|
|
assert.doesNotMatch(app, /(?:localStorage|sessionStorage|playlist\.push|fetch\s*\()/);
|
|
assert.doesNotMatch(app, /\b(?:5001|N5001|5006|5011|5086|8035|8061|8040|8046|8051|8056|8003|5037)\b/);
|
|
});
|
|
|
|
test("legacy stock double click is represented by pointer-down facts", () => {
|
|
assert.match(app, /cut-pointer-down/);
|
|
assert.match(app, /timestampMilliseconds/);
|
|
assert.match(app, /event\.ctrlKey/);
|
|
assert.match(app, /event\.shiftKey/);
|
|
const cutFactory = app.slice(app.indexOf("function createCutElement"), app.indexOf("function renderCuts"));
|
|
assert.doesNotMatch(cutFactory, /dblclick/);
|
|
});
|
|
|
|
test("cut rows are reconciled in place so a state reply cannot cancel native drag", () => {
|
|
assert.match(app, /dataset\.physicalIndex/);
|
|
assert.match(app, /function createCutElement/);
|
|
assert.doesNotMatch(app, /cutList\.replaceChildren/);
|
|
});
|
|
|
|
test("state rendering preserves stock and playlist focus like native controls", () => {
|
|
assert.match(app, /dataset\.resultIndex/);
|
|
assert.match(app, /dataset\.rowId/);
|
|
assert.doesNotMatch(app, /stockResults\.replaceChildren/);
|
|
assert.doesNotMatch(app, /playlistRows\.replaceChildren/);
|
|
});
|
|
|
|
test("playlist controls send intent for selection, enabled state, movement and deletion", () => {
|
|
for (const intent of [
|
|
"select-playlist-row",
|
|
"set-playlist-enabled",
|
|
"set-all-playlist-enabled",
|
|
"reorder-playlist-rows",
|
|
"move-selected-playlist-rows",
|
|
"delete-selected-playlist-rows",
|
|
"delete-all-playlist-rows"
|
|
]) {
|
|
assert.match(app, new RegExp("send\\(\\\"" + intent + "\\\""));
|
|
}
|
|
assert.match(app, /control: event\.ctrlKey/);
|
|
assert.match(app, /shift: event\.shiftKey/);
|
|
assert.match(app, /event\.key === "Delete"/);
|
|
assert.match(app, /toggle-active-playlist-enabled/);
|
|
assert.match(app, /select-playlist-boundary/);
|
|
assert.match(app, /event\.key === "Home"/);
|
|
assert.match(app, /event\.key === "End"/);
|
|
assert.match(app, /row\.isSelected/);
|
|
assert.match(app, /enabled\.disabled = state\.isBusy === true/);
|
|
assert.match(app, /playlistEnableAll\.indeterminate/);
|
|
});
|
|
|
|
test("playlist reorder reconciles keyed DOM nodes without discarding focus", () => {
|
|
assert.match(app, /const existingRows = new Map\(\)/);
|
|
assert.match(app, /existingRows\.get\(row\.rowId\)/);
|
|
assert.match(app, /playlistRows\.insertBefore\(element, elementAtPosition\)/);
|
|
assert.match(app, /const retainedRowIds = new Set/);
|
|
assert.doesNotMatch(app, /playlistRows\.replaceChild/);
|
|
});
|
|
|
|
test("playlist native drag preserves a multi-selection and rejects checkbox drag", () => {
|
|
class MiniClassList {
|
|
constructor() { this.names = new Set(); }
|
|
add(...names) { names.forEach((name) => this.names.add(name)); }
|
|
remove(...names) { names.forEach((name) => this.names.delete(name)); }
|
|
contains(name) { return this.names.has(name); }
|
|
toggle(name, force) {
|
|
if (force === undefined ? !this.names.has(name) : force) this.names.add(name);
|
|
else this.names.delete(name);
|
|
}
|
|
}
|
|
|
|
class MiniElement extends EventTarget {
|
|
constructor(tagName) {
|
|
super();
|
|
this.tagName = tagName.toUpperCase();
|
|
this.dataset = {};
|
|
this.children = [];
|
|
this.parentElement = null;
|
|
this.attributes = new Map();
|
|
this.classList = new MiniClassList();
|
|
this.listeners = new Map();
|
|
}
|
|
|
|
set className(value) {
|
|
this.classList = new MiniClassList();
|
|
String(value).split(/\s+/).filter(Boolean)
|
|
.forEach((name) => this.classList.add(name));
|
|
}
|
|
|
|
get className() { return [...this.classList.names].join(" "); }
|
|
appendChild(child) {
|
|
child.parentElement = this;
|
|
this.children.push(child);
|
|
return child;
|
|
}
|
|
contains(candidate) {
|
|
return candidate === this || this.children.some((child) => child.contains(candidate));
|
|
}
|
|
closest(selector) {
|
|
const tags = selector.split(",").map((value) => value.trim().toUpperCase());
|
|
return tags.includes(this.tagName)
|
|
? this
|
|
: this.parentElement?.closest(selector) || null;
|
|
}
|
|
focus() {}
|
|
addEventListener(type, listener, options) {
|
|
if (!this.listeners.has(type)) this.listeners.set(type, []);
|
|
this.listeners.get(type).push(listener);
|
|
super.addEventListener(type, listener, options);
|
|
}
|
|
invoke(type, event) {
|
|
for (const listener of this.listeners.get(type) || []) {
|
|
listener.call(this, event);
|
|
}
|
|
}
|
|
getBoundingClientRect() { return { top: 0, height: 20 }; }
|
|
setAttribute(name, value) { this.attributes.set(name, String(value)); }
|
|
getAttribute(name) { return this.attributes.get(name) ?? null; }
|
|
querySelectorAll(selector) {
|
|
const descendants = [];
|
|
const visit = (element) => {
|
|
element.children.forEach((child) => {
|
|
descendants.push(child);
|
|
visit(child);
|
|
});
|
|
};
|
|
visit(this);
|
|
if (selector === '.playlist-data-row[aria-selected="true"]') {
|
|
return descendants.filter((element) =>
|
|
element.classList.contains("playlist-data-row") &&
|
|
element.getAttribute("aria-selected") === "true");
|
|
}
|
|
if (selector === ".dragging, .drag-before, .drag-after") {
|
|
return descendants.filter((element) =>
|
|
["dragging", "drag-before", "drag-after"]
|
|
.some((name) => element.classList.contains(name)));
|
|
}
|
|
return [];
|
|
}
|
|
}
|
|
|
|
class MiniDataTransfer {
|
|
constructor() { this.values = new Map(); }
|
|
setData(type, value) { this.values.set(type, value); }
|
|
getData(type) { return this.values.get(type) || ""; }
|
|
}
|
|
|
|
const sourceStart = app.indexOf("function clearPlaylistDragVisuals");
|
|
const sourceEnd = app.indexOf("function renderPlaylist", sourceStart);
|
|
assert.ok(sourceStart >= 0 && sourceEnd > sourceStart);
|
|
const createHarness = new Function("document", "playlistRows", "send", "playlistDragMime", `
|
|
let draggedPlaylistRowId = null;
|
|
let deferredPlaylistPlainSelectionRowId = null;
|
|
let playlistDragBlockedRowId = null;
|
|
let playlistMutationLocked = false;
|
|
function dragDropPosition(event, element) {
|
|
const bounds = element.getBoundingClientRect();
|
|
return event.clientY < bounds.top + bounds.height / 2 ? "before" : "after";
|
|
}
|
|
${app.slice(sourceStart, sourceEnd)}
|
|
return {
|
|
createPlaylistElement,
|
|
onPlaylistDragStart,
|
|
state: () => ({ draggedPlaylistRowId, deferredPlaylistPlainSelectionRowId })
|
|
};
|
|
`);
|
|
const document = { createElement: (tagName) => new MiniElement(tagName) };
|
|
const playlistRows = new MiniElement("div");
|
|
const sent = [];
|
|
const harness = createHarness(
|
|
document, playlistRows, (type, payload) => sent.push({ type, payload }),
|
|
"text/x-mbn-playlist-row");
|
|
const source = harness.createPlaylistElement({ rowId: "row-a" });
|
|
source.setAttribute("aria-selected", "true");
|
|
playlistRows.appendChild(source);
|
|
const selectedPeer = harness.createPlaylistElement({ rowId: "row-b" });
|
|
selectedPeer.setAttribute("aria-selected", "true");
|
|
playlistRows.appendChild(selectedPeer);
|
|
const target = harness.createPlaylistElement({ rowId: "row-c" });
|
|
target.setAttribute("aria-selected", "false");
|
|
playlistRows.appendChild(target);
|
|
|
|
const checkbox = source.children[0].children[0];
|
|
const blockedTransfer = new MiniDataTransfer();
|
|
let checkboxDragPrevented = false;
|
|
source.invoke("pointerdown", {
|
|
button: 0,
|
|
currentTarget: source,
|
|
target: checkbox,
|
|
ctrlKey: false,
|
|
shiftKey: false
|
|
});
|
|
harness.onPlaylistDragStart({
|
|
currentTarget: source,
|
|
// Chromium reports the draggable row as dragstart.target even though
|
|
// pointerdown originated on the checkbox.
|
|
target: source,
|
|
dataTransfer: blockedTransfer,
|
|
preventDefault: () => { checkboxDragPrevented = true; }
|
|
});
|
|
assert.equal(checkboxDragPrevented, true);
|
|
assert.equal(blockedTransfer.getData("text/x-mbn-playlist-row"), "");
|
|
|
|
const pointerDown = new Event("pointerdown", { cancelable: true });
|
|
Object.assign(pointerDown, { button: 0, ctrlKey: false, shiftKey: false });
|
|
source.dispatchEvent(pointerDown);
|
|
assert.equal(harness.state().deferredPlaylistPlainSelectionRowId, "row-a");
|
|
assert.deepEqual(sent, []);
|
|
|
|
const transfer = new MiniDataTransfer();
|
|
const dragStart = new Event("dragstart", { cancelable: true });
|
|
Object.assign(dragStart, { dataTransfer: transfer });
|
|
source.dispatchEvent(dragStart);
|
|
assert.equal(harness.state().deferredPlaylistPlainSelectionRowId, null);
|
|
assert.equal(transfer.getData("text/x-mbn-playlist-row"), "row-a");
|
|
assert.deepEqual(sent, []);
|
|
|
|
const dragOver = new Event("dragover", { cancelable: true });
|
|
Object.assign(dragOver, { dataTransfer: transfer, clientY: 1 });
|
|
target.dispatchEvent(dragOver);
|
|
assert.equal(dragOver.defaultPrevented, true);
|
|
|
|
const drop = new Event("drop", { cancelable: true });
|
|
Object.assign(drop, { dataTransfer: transfer, clientY: 1 });
|
|
target.dispatchEvent(drop);
|
|
assert.deepEqual(sent, [{
|
|
type: "reorder-playlist-rows",
|
|
payload: { sourceRowId: "row-a", targetRowId: "row-c", position: "before" }
|
|
}]);
|
|
});
|
|
|
|
test("playlist drop zone accepts only the exact selected-cut drag token", () => {
|
|
class MiniDataTransfer {
|
|
constructor() { this.values = new Map(); }
|
|
get types() { return [...this.values.keys()]; }
|
|
setData(type, value) { this.values.set(type, value); }
|
|
getData(type) { return this.values.get(type) || ""; }
|
|
}
|
|
|
|
const sourceStart = app.indexOf("function hasDragType");
|
|
const sourceEnd = app.indexOf(
|
|
'playlistMoveUp.addEventListener("click"', sourceStart);
|
|
assert.ok(sourceStart >= 0 && sourceEnd > sourceStart);
|
|
const playlistDropZone = new EventTarget();
|
|
const sent = [];
|
|
new Function(
|
|
"playlistDropZone",
|
|
"send",
|
|
"selectedCutsDragToken",
|
|
app.slice(sourceStart, sourceEnd))(
|
|
playlistDropZone,
|
|
(type, payload) => sent.push({ type, payload }),
|
|
"legacy-selected-cuts");
|
|
|
|
const playlistTransfer = new MiniDataTransfer();
|
|
playlistTransfer.setData("text/x-mbn-playlist-row", "row-a");
|
|
const playlistDrop = new Event("drop", { cancelable: true, bubbles: true });
|
|
Object.assign(playlistDrop, { dataTransfer: playlistTransfer });
|
|
playlistDropZone.dispatchEvent(playlistDrop);
|
|
assert.equal(playlistDrop.defaultPrevented, false);
|
|
assert.deepEqual(sent, []);
|
|
|
|
const externalText = new MiniDataTransfer();
|
|
externalText.setData("text/plain", "external-text");
|
|
const externalDrop = new Event("drop", { cancelable: true, bubbles: true });
|
|
Object.assign(externalDrop, { dataTransfer: externalText });
|
|
playlistDropZone.dispatchEvent(externalDrop);
|
|
assert.equal(externalDrop.defaultPrevented, false);
|
|
assert.deepEqual(sent, []);
|
|
|
|
const cutTransfer = new MiniDataTransfer();
|
|
cutTransfer.setData("text/plain", "legacy-selected-cuts");
|
|
const cutOver = new Event("dragover", { cancelable: true });
|
|
Object.assign(cutOver, { dataTransfer: cutTransfer });
|
|
playlistDropZone.dispatchEvent(cutOver);
|
|
assert.equal(cutOver.defaultPrevented, true);
|
|
const cutDrop = new Event("drop", { cancelable: true, bubbles: true });
|
|
Object.assign(cutDrop, { dataTransfer: cutTransfer });
|
|
playlistDropZone.dispatchEvent(cutDrop);
|
|
assert.equal(cutDrop.defaultPrevented, true);
|
|
assert.deepEqual(sent, [{ type: "drop-selected-cuts", payload: {} }]);
|
|
});
|
|
|
|
test("operator catalog drag rejects an interactive child pointer origin", () => {
|
|
class MiniDataTransfer {
|
|
constructor() { this.values = new Map(); }
|
|
setData(type, value) { this.values.set(type, value); }
|
|
getData(type) { return this.values.get(type) || ""; }
|
|
}
|
|
|
|
const sourceStart = app.indexOf("function clearOperatorCatalogDragVisuals");
|
|
const sourceEnd = app.indexOf("function renderOperatorCatalog", sourceStart);
|
|
assert.ok(sourceStart >= 0 && sourceEnd > sourceStart);
|
|
const createHarness = new Function(
|
|
"operatorCatalogDraftRows",
|
|
"send",
|
|
"operatorCatalogDragMime",
|
|
`
|
|
let draggedOperatorCatalogRowId = null;
|
|
let operatorCatalogDragBlockedRowId = null;
|
|
let operatorCatalogMutationLocked = false;
|
|
function dragDropPosition(event, element) {
|
|
const bounds = element.getBoundingClientRect();
|
|
return event.clientY < bounds.top + bounds.height / 2 ? "before" : "after";
|
|
}
|
|
${app.slice(sourceStart, sourceEnd)}
|
|
return {
|
|
onOperatorCatalogPointerDown,
|
|
onOperatorCatalogDragStart,
|
|
state: () => ({ draggedOperatorCatalogRowId, operatorCatalogDragBlockedRowId })
|
|
};
|
|
`);
|
|
const harness = createHarness(
|
|
{ querySelectorAll: () => [] },
|
|
() => {},
|
|
"text/x-mbn-operator-catalog-row");
|
|
const row = {
|
|
dataset: { operatorCatalogRowId: "catalog-draft-row-a" },
|
|
classList: { add() {}, remove() {} },
|
|
closest: () => null
|
|
};
|
|
const input = { closest: () => input };
|
|
harness.onOperatorCatalogPointerDown({
|
|
button: 0,
|
|
currentTarget: row,
|
|
target: input
|
|
});
|
|
const blockedTransfer = new MiniDataTransfer();
|
|
let prevented = false;
|
|
harness.onOperatorCatalogDragStart({
|
|
currentTarget: row,
|
|
target: row,
|
|
dataTransfer: blockedTransfer,
|
|
preventDefault: () => { prevented = true; }
|
|
});
|
|
assert.equal(prevented, true);
|
|
assert.equal(
|
|
blockedTransfer.getData("text/x-mbn-operator-catalog-row"),
|
|
"");
|
|
assert.deepEqual(harness.state(), {
|
|
draggedOperatorCatalogRowId: null,
|
|
operatorCatalogDragBlockedRowId: null
|
|
});
|
|
|
|
harness.onOperatorCatalogPointerDown({
|
|
button: 0,
|
|
currentTarget: row,
|
|
target: row
|
|
});
|
|
const allowedTransfer = new MiniDataTransfer();
|
|
harness.onOperatorCatalogDragStart({
|
|
currentTarget: row,
|
|
target: row,
|
|
dataTransfer: allowedTransfer,
|
|
preventDefault: () => { throw new Error("non-interactive drag was rejected"); }
|
|
});
|
|
assert.equal(
|
|
allowedTransfer.getData("text/x-mbn-operator-catalog-row"),
|
|
"catalog-draft-row-a");
|
|
});
|
|
|
|
test("market tabs send closed identity intents and reconcile C# order", () => {
|
|
assert.match(app, /send\("select-tab"/);
|
|
assert.match(app, /send\("hover-swap-tabs"/);
|
|
assert.match(app, /expectedSourceIndex: sourceIndex/);
|
|
assert.match(app, /expectedTargetIndex: targetIndex/);
|
|
assert.doesNotMatch(app, /send\("swap-tabs"/);
|
|
assert.match(app, /dataset\.tabId/);
|
|
assert.match(app, /const existingTabs = new Map\(\)/);
|
|
assert.match(app, /marketTabs\.insertBefore/);
|
|
assert.doesNotMatch(app, /selectedTab\s*=/);
|
|
});
|
|
|
|
test("fixed catalog renders C# view and sends opaque leaf or section intent", () => {
|
|
assert.match(app, /state\.fixedCatalog/);
|
|
assert.match(app, /dataset\.actionId/);
|
|
assert.match(app, /dataset\.sectionIndex/);
|
|
assert.match(app, /send\("activate-fixed-action"/);
|
|
assert.match(app, /send\("activate-fixed-section"/);
|
|
assert.match(app, /send\("activate-industry-section"/);
|
|
assert.match(app, /send\("activate-comparison-section"/);
|
|
assert.match(app, /send\("set-moving-averages"/);
|
|
assert.doesNotMatch(app, /builderKey|sceneCode|cutCode|dataCode/);
|
|
});
|
|
|
|
test("fixed-262 ignores a single click and emits exactly one intent on double click", () => {
|
|
const doubleClickStart = app.indexOf('categoryTree.addEventListener("dblclick"');
|
|
const clickStart = app.indexOf(
|
|
'categoryTree.addEventListener("click"', doubleClickStart + 1);
|
|
const changeStart = app.indexOf(
|
|
'categoryTree.addEventListener("change"', clickStart + 1);
|
|
assert.ok(doubleClickStart >= 0 && clickStart > doubleClickStart && changeStart > clickStart);
|
|
|
|
class FixedActionElement extends EventTarget {
|
|
constructor() {
|
|
super();
|
|
this.dataset = { actionId: "fixed-262" };
|
|
this.disabled = false;
|
|
}
|
|
|
|
closest(selector) {
|
|
return selector === "button[data-action-id]" ? this : null;
|
|
}
|
|
}
|
|
|
|
const categoryTree = new FixedActionElement();
|
|
const sent = [];
|
|
const send = (type, payload) => sent.push({ type, payload });
|
|
new Function("categoryTree", "send", app.slice(doubleClickStart, clickStart))(
|
|
categoryTree, send);
|
|
new Function("categoryTree", "send", app.slice(clickStart, changeStart))(
|
|
categoryTree, send);
|
|
|
|
categoryTree.dispatchEvent(new Event("click", { bubbles: true, cancelable: true }));
|
|
assert.deepEqual(sent, []);
|
|
|
|
categoryTree.dispatchEvent(new Event("dblclick", { bubbles: true, cancelable: true }));
|
|
assert.deepEqual(sent, [{
|
|
type: "activate-fixed-action",
|
|
payload: { actionId: "fixed-262" }
|
|
}]);
|
|
});
|
|
|
|
test("industry workspace sends index and opaque action intents only", () => {
|
|
assert.match(app, /state\.industry/);
|
|
assert.match(app, /dataset\.industryIndex/);
|
|
assert.match(app, /dataset\.industryActionId/);
|
|
assert.match(app, /send\("select-industry"/);
|
|
assert.match(app, /send\("double-click-industry"/);
|
|
assert.match(app, /send\("swap-industries"/);
|
|
assert.match(app, /send\("activate-industry-action"/);
|
|
});
|
|
|
|
test("comparison workspace sends opaque target, pair and action intents", () => {
|
|
for (const intent of [
|
|
"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"
|
|
]) assert.match(app, new RegExp("\\\"" + intent + "\\\""));
|
|
assert.match(app, /dataset\.comparisonSelectionId/);
|
|
assert.match(app, /dataset\.comparisonPairId/);
|
|
assert.match(app, /dataset\.comparisonActionId/);
|
|
assert.doesNotMatch(app, /send\([^\n]*stockCode/);
|
|
});
|
|
|
|
test("theme workspace sends query, opaque selection, sort and action intents", () => {
|
|
assert.match(app, /state\.theme/);
|
|
assert.match(app, /dataset\.themeResultId/);
|
|
assert.match(app, /dataset\.themeActionId/);
|
|
assert.match(app, /send\("search-themes"/);
|
|
assert.match(app, /send\("select-theme"/);
|
|
assert.match(app, /send\("set-theme-sort"/);
|
|
assert.match(app, /send\("activate-theme-action"/);
|
|
});
|
|
|
|
test("expert and trading halt workspaces send opaque selection and action intents", () => {
|
|
for (const intent of [
|
|
"search-experts", "select-expert", "activate-expert-action",
|
|
"search-trading-halts", "select-trading-halt", "toggle-trading-halt-sort",
|
|
"activate-trading-halt-action"
|
|
]) assert.match(app, new RegExp("send\\(\\\"" + intent + "\\\""));
|
|
assert.match(app, /dataset\.expertSelectionId/);
|
|
assert.match(app, /dataset\.haltSelectionId/);
|
|
});
|
|
|
|
test("theme expert and trading halt actions preserve original single click wiring", () => {
|
|
const doubleClickStart = app.indexOf('categoryTree.addEventListener("dblclick"');
|
|
const clickStart = app.indexOf('categoryTree.addEventListener("click"', doubleClickStart + 1);
|
|
const changeStart = app.indexOf('categoryTree.addEventListener("change"', clickStart + 1);
|
|
assert.ok(doubleClickStart >= 0 && clickStart > doubleClickStart && changeStart > clickStart);
|
|
const doubleClickDelegate = app.slice(doubleClickStart, clickStart);
|
|
const clickDelegate = app.slice(clickStart, changeStart);
|
|
for (const intent of [
|
|
"activate-theme-action",
|
|
"activate-expert-action",
|
|
"activate-trading-halt-action"
|
|
]) {
|
|
assert.match(clickDelegate, new RegExp("send\\(\\\"" + intent + "\\\""));
|
|
assert.doesNotMatch(doubleClickDelegate, new RegExp("send\\(\\\"" + intent + "\\\""));
|
|
assert.equal(app.split('send("' + intent + '"').length - 1, 1);
|
|
}
|
|
assert.match(doubleClickDelegate, /send\("double-click-industry"/);
|
|
for (const intent of [
|
|
"choose-comparison-domestic", "choose-comparison-world", "choose-comparison-fixed"
|
|
]) {
|
|
assert.match(doubleClickDelegate, new RegExp("send\\(.*\\\"" + intent + "\\\"", "s"));
|
|
assert.doesNotMatch(clickDelegate, new RegExp("send\\(.*\\\"" + intent + "\\\"", "s"));
|
|
}
|
|
for (const intent of [
|
|
"select-comparison-domestic", "select-comparison-world", "select-comparison-fixed"
|
|
]) {
|
|
assert.match(clickDelegate, new RegExp("send\\(.*\\\"" + intent + "\\\"", "s"));
|
|
assert.doesNotMatch(doubleClickDelegate, new RegExp("send\\(.*\\\"" + intent + "\\\"", "s"));
|
|
}
|
|
assert.match(clickDelegate, /send\("select-theme"/);
|
|
assert.match(clickDelegate, /send\("select-expert"/);
|
|
assert.match(clickDelegate, /send\("select-trading-halt"/);
|
|
});
|
|
|
|
test("named DB playlists and playout use separate closed native intents", () => {
|
|
for (const intent of [
|
|
"refresh-named-playlists", "select-named-playlist",
|
|
"load-selected-named-playlist", "create-named-playlist",
|
|
"save-current-named-playlist", "delete-selected-named-playlist"
|
|
]) assert.match(app, new RegExp("\\\"" + intent + "\\\""));
|
|
assert.match(app, /state\.namedPlaylist/);
|
|
assert.match(app, /dataset\.namedPlaylistDefinitionId/);
|
|
assert.match(app, /definitionId: definition\.dataset\.namedPlaylistDefinitionId/);
|
|
assert.match(app, /dbLoadButton\.disabled = busy \|\| !named/);
|
|
assert.match(app, /named\.isWriteQuarantined/);
|
|
assert.doesNotMatch(app, /programCode|listText/i);
|
|
|
|
for (const label of ["PREPARE", "TAKE IN", "NEXT", "TAKE OUT"]) {
|
|
assert.match(markup, new RegExp("<button[^>]*disabled[^>]*>[^<]*" + label));
|
|
}
|
|
for (const intent of [
|
|
"prepare-playout", "take-in", "next-playout", "take-out",
|
|
"choose-background", "toggle-background", "set-fade-duration"
|
|
]) assert.match(playout, new RegExp("\\\"" + intent + "\\\""));
|
|
assert.match(playout, /playout\.phase !== "prepared"/);
|
|
assert.match(playout, /playout\.isPlayCompletionPending === true/);
|
|
assert.match(playout, /playout\.outcomeUnknown !== true/);
|
|
assert.doesNotMatch(playout, /fetch\s*\(|localStorage|sessionStorage|sceneCode|filePath/);
|
|
});
|
|
|
|
test("GraphE preserves name header sorting and directional find in native authority", () => {
|
|
for (const intent of [
|
|
"find-manual-financial", "toggle-manual-financial-name-sort"
|
|
]) assert.match(app, new RegExp("\\\"" + intent + "\\\""));
|
|
assert.match(app, /id = "manual-financial-find-query"/);
|
|
assert.match(app, /dataset\.manualFindDirection/);
|
|
assert.match(app, /dataset\.manualNameSort/);
|
|
assert.match(app, /dataset\.manualGeneration/);
|
|
assert.match(app, /generation: generation/);
|
|
assert.match(app, /manual-financial-find-query[\s\S]*down\.click\(\)/);
|
|
assert.doesNotMatch(app, /(?:INPUT_PIE|INPUT_GROW|INPUT_SELL|INPUT_PROFIT)[\s\S]*manual-financial-find-query/);
|
|
});
|
|
|
|
test("FSell and VI dialogs send opaque native intents and never own file or stock authority", () => {
|
|
for (const id of ["manual-list-modal", "manual-list-workspace", "manual-list-close"]) {
|
|
assert.match(markup, new RegExp("id=\\\"" + id + "\\\""));
|
|
}
|
|
for (const screen of ["individual", "foreign", "institution", "vi"]) {
|
|
assert.match(markup, new RegExp("data-manual-list-screen=\\\"" + screen + "\\\""));
|
|
}
|
|
for (const intent of [
|
|
"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",
|
|
"import-manual-lists"
|
|
]) assert.match(app, new RegExp("\\\"" + intent + "\\\""));
|
|
for (const obsoleteIntent of ["save-manual-net-sell", "add-manual-net-sell-cut", "save-manual-vi"]) {
|
|
assert.doesNotMatch(app, new RegExp("\\\"" + obsoleteIntent + "\\\""));
|
|
}
|
|
|
|
const start = app.indexOf("function renderManualLists");
|
|
const end = app.indexOf("function openNamedPlaylist", start);
|
|
const manualLists = app.slice(start, end);
|
|
assert.match(manualLists, /rowId: row\.rowId/);
|
|
assert.match(manualLists, /resultId: row\.resultId/);
|
|
assert.match(manualLists, /itemId: row\.itemId/);
|
|
assert.match(manualLists, /addEventListener\("click"[\s\S]*setTimeout[\s\S]*send\("select-manual-vi-result"/);
|
|
assert.match(manualLists, /addEventListener\("dblclick"[\s\S]*send\("add-manual-vi-result"/);
|
|
assert.doesNotMatch(manualLists, /(?:rowVersion|stockCode|item\.code|filePath|directory|localStorage|fetch\s*\()/i);
|
|
});
|
|
|
|
test("separator rows preserve the original drag gesture", () => {
|
|
assert.match(app, /element\.draggable = true/);
|
|
assert.doesNotMatch(app, /draggable = !row\.isSeparator/);
|
|
});
|
|
|
|
test("a C# busy snapshot freezes controls instead of silently accepting queued input", () => {
|
|
const styles = fs.readFileSync(path.join(webRoot, "styles.css"), "utf8");
|
|
assert.match(app, /state\.isBusy === true/);
|
|
assert.match(app, /classList\.toggle\("busy"/);
|
|
assert.match(styles, /body\.busy \.operator-shell \{ pointer-events: none; \}/);
|
|
});
|