"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" ]); });