Files
MBN_STOCK_WEBVIEW/tests/LegacyParityWeb/legacy-double-click-timing.test.cjs

140 lines
4.9 KiB
JavaScript

const assert = require("node:assert/strict");
const fs = require("node:fs");
const path = require("node:path");
const test = require("node:test");
const app = fs.readFileSync(path.join(
__dirname,
"../../src/MBN_STOCK_WEBVIEW.LegacyParityApp/Web/app.js"), "utf8");
function section(start, end) {
const startIndex = app.indexOf(start);
const endIndex = app.indexOf(end, startIndex);
assert.ok(startIndex >= 0 && endIndex > startIndex,
`Missing or out-of-order source markers: ${start} -> ${end}`);
return app.slice(startIndex, endIndex);
}
function timingHarness() {
let now = 0;
let nextTimer = 1;
const pending = new Map();
const windowObject = {
setTimeout(callback, milliseconds) {
const timer = nextTimer++;
pending.set(timer, { callback, due: now + milliseconds });
return timer;
},
clearTimeout(timer) {
pending.delete(timer);
}
};
const source = section(
"const fallbackDoubleClickTimeMilliseconds",
"function isRovingListNavigationKey");
const api = new Function("window", `
${source}
return {
update: updateDoubleClickTime,
defer: deferLegacySingleClick,
clear: clearDelayedClick
};
`)(windowObject);
return {
...api,
pendingCount: () => pending.size,
advance(milliseconds) {
now += milliseconds;
const due = Array.from(pending.entries())
.filter(([, timer]) => timer.due <= now)
.sort((left, right) => left[1].due - right[1].due);
due.forEach(([id, timer]) => {
if (!pending.delete(id)) return;
timer.callback();
});
}
};
}
test("a 350ms Windows double-click cancels the pending single action", () => {
const harness = timingHarness();
const actions = [];
harness.update({
interactionMetrics: { doubleClickTimeMilliseconds: 500 }
});
const timer = harness.defer(() => actions.push("single"));
harness.advance(350);
assert.deepEqual(actions, [], "the first click must not commit before click two");
assert.equal(harness.pendingCount(), 1);
harness.clear(timer);
actions.push("double");
harness.advance(1000);
assert.deepEqual(actions, ["double"]);
assert.equal(harness.pendingCount(), 0);
});
test("ordinary single-click follows the live Windows interval plus a race margin", () => {
const harness = timingHarness();
let count = 0;
harness.update({
interactionMetrics: { doubleClickTimeMilliseconds: 800 }
});
harness.defer(() => { count += 1; });
harness.advance(849);
assert.equal(count, 0);
harness.advance(1);
assert.equal(count, 1);
harness.update({ interactionMetrics: { doubleClickTimeMilliseconds: 5001 } });
harness.defer(() => { count += 1; });
harness.advance(549);
assert.equal(count, 1, "invalid native data must use the 500ms fallback");
harness.advance(1);
assert.equal(count, 2);
});
test("rerender-prone rows share system timing while GraphE candidates remain double-click only", () => {
assert.doesNotMatch(app, /manualStockResultClickTimer|queueManualFinancialStockSelection/u);
const manualCandidateClick = section(
"const stock = event.target.closest(\"button[data-manual-stock-result-id]\")",
"if (event.target.closest(\"button[data-manual-save]\"))");
assert.match(manualCandidateClick, /focusManualFinancialStock\(stock\)/u);
assert.match(manualCandidateClick, /selectManualFinancialStock\(stock\)/u);
assert.match(app,
/manualViResultClickTimer = deferLegacySingleClick\(function \(\) \{/u);
assert.match(app,
/manualResultClickTimer = deferLegacySingleClick\(function \(\) \{/u);
assert.equal(
(app.match(/comparisonTargetClickTimer = deferLegacySingleClick\(function \(\) \{/gu) || [])
.length,
2);
assert.match(app,
/themeResultClickTimer = deferLegacySingleClick\(function \(\) \{/u);
assert.doesNotMatch(app, /\}, 250\);/u);
const operatorCatalog = section(
"operatorCatalogStockResults.addEventListener(\"dblclick\"",
"operatorCatalogAddStock.addEventListener(\"click\"");
assert.match(operatorCatalog, /if \(\(event\.detail \|\| 1\) > 1\) return;/u);
assert.doesNotMatch(operatorCatalog, /deferLegacySingleClick|setTimeout/u);
const namedPlaylist = section(
"namedPlaylistDefinitions.addEventListener(\"dblclick\"",
"namedPlaylistNewTitle.addEventListener(\"input\"");
assert.match(namedPlaylist, /namedPlaylistSelection\.isPending\(\)/u);
assert.match(namedPlaylist, /namedPlaylistSelection\.defer\(definitionId/u);
assert.doesNotMatch(namedPlaylist, /setTimeout|250/u);
});
test("each native state refreshes timing before rerendering clickable catalogs", () => {
const render = section("function render(state)", "Object.keys(operatorFolderControls)");
const timing = render.indexOf("updateDoubleClickTime(state);");
const firstCatalog = render.indexOf("renderFixedCatalog(state);");
const manual = render.indexOf("renderManualFinancial(state);");
assert.ok(timing >= 0 && firstCatalog > timing && manual > timing);
});