Files
MBN_STOCK_WEBVIEW/tests/Web/market-nav-reorder.test.cjs

230 lines
8.1 KiB
JavaScript

"use strict";
const test = require("node:test");
const assert = require("node:assert/strict");
const reorder = require("../../Web/market-nav-reorder.js");
class FakeClassList {
constructor(values = []) { this.values = new Set(values); }
add(value) { this.values.add(value); }
remove(value) { this.values.delete(value); }
contains(value) { return this.values.has(value); }
}
class FakeButton {
constructor(market, active = false) {
this.dataset = { market };
this.parentElement = null;
this.classList = new FakeClassList(active ? ["active"] : []);
this.attributes = new Map();
this.draggable = false;
this.focusCount = 0;
}
closest(selector) { return selector === "button[data-market]" ? this : null; }
setAttribute(name, value) { this.attributes.set(name, String(value)); }
focus() { this.focusCount += 1; }
}
class FakeNav {
constructor(markets, activeMarket = "compare") {
this.children = markets.map(market => new FakeButton(market, market === activeMarket));
for (const child of this.children) child.parentElement = this;
this.listeners = new Map();
}
addEventListener(type, callback) {
const callbacks = this.listeners.get(type) || [];
callbacks.push(callback);
this.listeners.set(type, callbacks);
}
removeEventListener(type, callback) {
this.listeners.set(type, (this.listeners.get(type) || []).filter(value => value !== callback));
}
append(child) {
this.children = this.children.filter(value => value !== child);
this.children.push(child);
child.parentElement = this;
}
prepend(child) {
this.children = this.children.filter(value => value !== child);
this.children.unshift(child);
child.parentElement = this;
}
dispatch(type, event) {
event.currentTarget = this;
for (const callback of this.listeners.get(type) || []) callback(event);
}
button(market) { return this.children.find(button => button.dataset.market === market); }
}
class FakeDataTransfer {
constructor() {
this.values = new Map();
this.effectAllowed = "none";
this.dropEffect = "none";
}
setData(type, value) { this.values.set(type, value); }
getData(type) { return this.values.get(type) || ""; }
}
function eventFor(target, values = {}) {
return {
target,
altKey: false,
ctrlKey: false,
metaKey: false,
shiftKey: false,
repeat: false,
defaultPrevented: false,
propagationStopped: false,
preventDefault() { this.defaultPrevented = true; },
stopPropagation() { this.propagationStopped = true; },
...values
};
}
function createNav(activeMarket = "compare") {
return new FakeNav([reorder.fixedMarket, ...reorder.originalMarkets], activeMarket);
}
test("dashboard is fixed while original market identities swap by ID", () => {
const nav = createNav();
const result = reorder.swapOrder(
reorder.readOrder(nav),
"exchange",
"expert");
assert.equal(result.changed, true);
assert.equal(reorder.applyOrder(nav, result.order), true);
assert.equal(nav.children[0].dataset.market, "dashboard");
assert.equal(nav.children[2].dataset.market, "expert");
assert.equal(nav.children[9].dataset.market, "exchange");
assert.equal(nav.button("compare").classList.contains("active"), true);
});
test("Alt+Arrow provides adjacent reorder and honors first/last boundaries", () => {
const nav = createNav("overseas");
const controller = reorder.createController({ nav });
assert.equal(controller.mount(), true);
const first = nav.button("overseas");
const firstBoundary = eventFor(first, { key: "ArrowUp", altKey: true });
nav.dispatch("keydown", firstBoundary);
assert.deepEqual(controller.readOrder(), reorder.originalMarkets);
assert.equal(firstBoundary.defaultPrevented, true);
const last = nav.button("halted");
const lastBoundary = eventFor(last, { key: "ArrowDown", altKey: true });
nav.dispatch("keydown", lastBoundary);
assert.deepEqual(controller.readOrder(), reorder.originalMarkets);
const move = eventFor(first, { key: "ArrowDown", altKey: true });
nav.dispatch("keydown", move);
assert.deepEqual(controller.readOrder().slice(0, 3), ["exchange", "overseas", "index"]);
assert.equal(nav.button("overseas").classList.contains("active"), true);
assert.equal(first.focusCount, 1);
});
test("drag swap preserves active state and existing click behavior", () => {
const nav = createNav("compare");
let clicked = null;
nav.addEventListener("click", event => {
clicked = event.target.closest("button[data-market]")?.dataset.market ?? null;
});
const controller = reorder.createController({ nav });
assert.equal(controller.mount(), true);
const source = nav.button("compare");
const target = nav.button("overseas");
const dataTransfer = new FakeDataTransfer();
nav.dispatch("dragstart", eventFor(source, { dataTransfer }));
const over = eventFor(target, { dataTransfer });
nav.dispatch("dragover", over);
assert.equal(over.defaultPrevented, true);
nav.dispatch("drop", eventFor(target, { dataTransfer }));
assert.equal(controller.readOrder()[0], "compare");
assert.equal(controller.readOrder()[5], "overseas");
assert.equal(nav.children[0].dataset.market, "dashboard");
assert.equal(source.classList.contains("active"), true);
const click = eventFor(source);
nav.dispatch("click", click);
assert.equal(clicked, "compare");
assert.equal(click.defaultPrevented, false);
});
test("malformed, external, dashboard and mismatched drags fail closed", () => {
const nav = createNav();
const controller = reorder.createController({ nav });
controller.mount();
const initial = [...controller.readOrder()];
const dashboardStart = eventFor(nav.button("dashboard"), {
dataTransfer: new FakeDataTransfer()
});
nav.dispatch("dragstart", dashboardStart);
assert.equal(dashboardStart.defaultPrevented, true);
const source = nav.button("exchange");
const target = nav.button("expert");
const malformed = new FakeDataTransfer();
nav.dispatch("dragstart", eventFor(source, { dataTransfer: malformed }));
malformed.setData(reorder.dragMime, "mbn-market-nav:v1:dashboard");
nav.dispatch("drop", eventFor(target, { dataTransfer: malformed }));
assert.deepEqual(controller.readOrder(), initial);
const dashboardTarget = new FakeDataTransfer();
nav.dispatch("dragstart", eventFor(source, { dataTransfer: dashboardTarget }));
nav.dispatch("drop", eventFor(nav.button("dashboard"), { dataTransfer: dashboardTarget }));
assert.deepEqual(controller.readOrder(), initial);
const external = new FakeDataTransfer();
external.setData(reorder.dragMime, reorder.createDragToken("exchange"));
nav.dispatch("drop", eventFor(target, { dataTransfer: external }));
assert.deepEqual(controller.readOrder(), initial);
assert.equal(reorder.parseDragToken("mbn-market-nav:v1:unknown"), null);
assert.equal(reorder.parseDragToken(" mbn-market-nav:v1:exchange"), null);
});
test("input, modal/playout lock and malformed DOM cannot reorder", () => {
const nav = createNav();
let blocked = true;
const controller = reorder.createController({ nav, isBlocked: () => blocked });
assert.equal(controller.mount(), true);
const initial = [...controller.readOrder()];
const locked = eventFor(nav.button("exchange"), {
key: "ArrowDown",
altKey: true
});
nav.dispatch("keydown", locked);
assert.deepEqual(controller.readOrder(), initial);
blocked = false;
const input = { closest: () => null };
const typing = eventFor(input, { key: "ArrowDown", altKey: true });
nav.dispatch("keydown", typing);
assert.equal(typing.defaultPrevented, false);
assert.deepEqual(controller.readOrder(), initial);
const malformedNav = createNav();
malformedNav.button("expert").dataset.market = "compare";
const malformedController = reorder.createController({ nav: malformedNav });
assert.equal(malformedController.mount(), false);
});
test("reorder state is session-only and has no persistence surface", () => {
assert.equal("storageKey" in reorder, false);
assert.equal("save" in reorder, false);
assert.deepEqual(reorder.originalMarkets, [
"overseas", "exchange", "index", "kospi", "kosdaq",
"compare", "theme", "foreign-stock", "expert", "halted"
]);
});