230 lines
8.1 KiB
JavaScript
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"
|
|
]);
|
|
});
|