140 lines
4.9 KiB
JavaScript
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);
|
|
});
|