mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-10-09 05:17:18 +00:00
Moves 290 root test-*.js into tests/unit (177, listed in test-all.sh) and tests/e2e (113, classified in scripts/non-unit-tests.json), per #1981 and PR-D of #1385. Root goes from 348 entries to 48. test-all.sh and test-fixtures/ stay put. The inventory guard now fails if a test reappears in the root or sits in the wrong folder.
Verified independently of the diff: the invoked sets are unchanged (test-all.sh 177 before and after, deploy.yml 96 before and after, both identical as sets), and a full local run of test-all.sh on master and on the branch produced 4702 output lines each whose only differences are absolute paths, stack-trace line numbers shifted by the REPO_ROOT line, the inventory wording and two perf ratios. The guard was mutation-checked: a test back in the root, a unit suite in tests/e2e, and a suite dropped from test-all.sh each make it exit 1. CI run 35246304316 ran 97 suites from tests/e2e and is green.
Follow-up 9335c51d finished the instruction files: no bare root test command is left in AGENTS.md, the squad charters, .github or docs, and every tests/ path they name resolves.
Merged by the interim maintainer without a second human reviewer: CI and the local runs above are the independent checks.
Known and deliberately out of scope: 18 of the 113 files in tests/e2e are invoked by no runner at all, and one of them cannot run anywhere because it requires jsdom, which is not a declared dependency. Tracked separately.
460 lines
14 KiB
JavaScript
460 lines
14 KiB
JavaScript
/* test-drag-manager.js — Unit tests for DragManager (#608 M1) */
|
|
'use strict';
|
|
const REPO_ROOT = require('path').resolve(__dirname, '..', '..');
|
|
|
|
const vm = require('vm');
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const assert = require('assert');
|
|
|
|
// Minimal DOM shim
|
|
function makePanel(id) {
|
|
const listeners = {};
|
|
const style = {};
|
|
const dataset = {};
|
|
const classList = {
|
|
_set: new Set(),
|
|
add(c) { this._set.add(c); },
|
|
remove(c) { this._set.delete(c); },
|
|
contains(c) { return this._set.has(c); }
|
|
};
|
|
let attrs = {};
|
|
const header = {
|
|
_listeners: {},
|
|
addEventListener(ev, fn) {
|
|
if (!this._listeners[ev]) this._listeners[ev] = [];
|
|
this._listeners[ev].push(fn);
|
|
},
|
|
setPointerCapture() {},
|
|
releasePointerCapture() {},
|
|
_fire(ev, data) {
|
|
(this._listeners[ev] || []).forEach(fn => fn(data));
|
|
}
|
|
};
|
|
return {
|
|
id: id,
|
|
style: style,
|
|
dataset: dataset,
|
|
classList: classList,
|
|
querySelector(sel) {
|
|
if (sel === '.panel-header') return header;
|
|
return null;
|
|
},
|
|
getAttribute(k) { return attrs[k] || null; },
|
|
setAttribute(k, v) { attrs[k] = v; },
|
|
removeAttribute(k) {
|
|
delete attrs[k];
|
|
// DOMStringMap reflects removal of data-* attributes in real browsers.
|
|
if (k.startsWith('data-')) delete dataset[k.slice(5).replace(/-([a-z])/g, (_, c) => c.toUpperCase())];
|
|
},
|
|
getBoundingClientRect() {
|
|
return {
|
|
left: parseFloat(style.left) || 0,
|
|
top: parseFloat(style.top) || 0,
|
|
right: (parseFloat(style.left) || 0) + 300,
|
|
bottom: (parseFloat(style.top) || 0) + 200,
|
|
width: 300,
|
|
height: 200
|
|
};
|
|
},
|
|
_header: header
|
|
};
|
|
}
|
|
|
|
// Mock globals
|
|
const storage = {};
|
|
const mockWindow = {
|
|
innerWidth: 1920,
|
|
innerHeight: 1080,
|
|
DragManager: null,
|
|
matchMedia() { return { matches: true, addEventListener() {} }; },
|
|
addEventListener() {}
|
|
};
|
|
const mockDocument = {
|
|
addEventListener(ev, fn) {
|
|
if (!mockDocument._listeners) mockDocument._listeners = {};
|
|
if (!mockDocument._listeners[ev]) mockDocument._listeners[ev] = [];
|
|
mockDocument._listeners[ev].push(fn);
|
|
},
|
|
removeEventListener(ev, fn) {
|
|
if (mockDocument._listeners && mockDocument._listeners[ev]) {
|
|
mockDocument._listeners[ev] = mockDocument._listeners[ev].filter(f => f !== fn);
|
|
}
|
|
},
|
|
querySelectorAll() { return []; }
|
|
};
|
|
const mockLocalStorage = {
|
|
_data: {},
|
|
getItem(k) { return this._data[k] || null; },
|
|
setItem(k, v) { this._data[k] = v; },
|
|
removeItem(k) { delete this._data[k]; },
|
|
clear() { this._data = {}; }
|
|
};
|
|
|
|
// Load DragManager
|
|
const src = fs.readFileSync(path.join(REPO_ROOT, 'public', 'drag-manager.js'), 'utf8');
|
|
const ctx = vm.createContext({
|
|
window: mockWindow,
|
|
document: mockDocument,
|
|
localStorage: mockLocalStorage,
|
|
Math: Math,
|
|
JSON: JSON,
|
|
console: console,
|
|
setTimeout: setTimeout,
|
|
clearTimeout: clearTimeout,
|
|
parseFloat: parseFloat
|
|
});
|
|
vm.runInContext(src, ctx);
|
|
const DragManager = ctx.window.DragManager;
|
|
|
|
let passed = 0;
|
|
let failed = 0;
|
|
|
|
function test(name, fn) {
|
|
try {
|
|
mockLocalStorage.clear();
|
|
fn();
|
|
passed++;
|
|
console.log(' ✓ ' + name);
|
|
} catch (e) {
|
|
failed++;
|
|
console.log(' ✗ ' + name + ': ' + e.message);
|
|
}
|
|
}
|
|
|
|
console.log('DragManager tests:');
|
|
|
|
test('constructor initializes IDLE state', () => {
|
|
const dm = new DragManager();
|
|
assert.strictEqual(dm.state, 'IDLE');
|
|
assert.strictEqual(dm.enabled, true);
|
|
});
|
|
|
|
test('register adds panel', () => {
|
|
const dm = new DragManager();
|
|
const panel = makePanel('testPanel');
|
|
dm.register(panel);
|
|
assert.strictEqual(dm._panels.length, 1);
|
|
});
|
|
|
|
test('register ignores null panel', () => {
|
|
const dm = new DragManager();
|
|
dm.register(null);
|
|
assert.strictEqual(dm._panels.length, 0);
|
|
});
|
|
|
|
test('pointerdown transitions to PENDING', () => {
|
|
const dm = new DragManager();
|
|
const panel = makePanel('p1');
|
|
dm.register(panel);
|
|
panel._header._fire('pointerdown', {
|
|
button: 0, clientX: 100, clientY: 100,
|
|
preventDefault() {},
|
|
target: { closest() { return null; } }
|
|
});
|
|
assert.strictEqual(dm.state, 'PENDING');
|
|
assert.strictEqual(dm.activePanel, panel);
|
|
});
|
|
|
|
test('pointerdown ignores non-left button', () => {
|
|
const dm = new DragManager();
|
|
const panel = makePanel('p1');
|
|
dm.register(panel);
|
|
panel._header._fire('pointerdown', {
|
|
button: 2, clientX: 100, clientY: 100,
|
|
preventDefault() {},
|
|
target: { closest() { return null; } }
|
|
});
|
|
assert.strictEqual(dm.state, 'IDLE');
|
|
});
|
|
|
|
test('pointerdown ignores button clicks', () => {
|
|
const dm = new DragManager();
|
|
const panel = makePanel('p1');
|
|
dm.register(panel);
|
|
panel._header._fire('pointerdown', {
|
|
button: 0, clientX: 100, clientY: 100,
|
|
preventDefault() {},
|
|
target: { closest(sel) { return sel === 'button' ? {} : null; } }
|
|
});
|
|
assert.strictEqual(dm.state, 'IDLE');
|
|
});
|
|
|
|
test('pointermove within dead zone stays PENDING', () => {
|
|
const dm = new DragManager();
|
|
const panel = makePanel('p1');
|
|
dm.register(panel);
|
|
panel._header._fire('pointerdown', {
|
|
button: 0, clientX: 100, clientY: 100,
|
|
preventDefault() {},
|
|
target: { closest() { return null; } }
|
|
});
|
|
panel._header._fire('pointermove', { clientX: 103, clientY: 102 });
|
|
assert.strictEqual(dm.state, 'PENDING');
|
|
assert.ok(!panel.classList.contains('is-dragging'));
|
|
});
|
|
|
|
test('pointermove beyond dead zone transitions to DRAGGING', () => {
|
|
const dm = new DragManager();
|
|
const panel = makePanel('p1');
|
|
panel.setAttribute('data-position', 'bl');
|
|
dm.register(panel);
|
|
panel._header._fire('pointerdown', {
|
|
button: 0, clientX: 100, clientY: 100,
|
|
preventDefault() {},
|
|
target: { closest() { return null; } }
|
|
});
|
|
panel._header._fire('pointermove', { clientX: 110, clientY: 110 });
|
|
assert.strictEqual(dm.state, 'DRAGGING');
|
|
assert.ok(panel.classList.contains('is-dragging'));
|
|
assert.strictEqual(panel.getAttribute('data-position'), null); // removed
|
|
assert.strictEqual(panel.dataset.dragged, 'true');
|
|
});
|
|
|
|
test('pointerup after drag finalizes position', () => {
|
|
const dm = new DragManager();
|
|
const panel = makePanel('p1');
|
|
panel.setAttribute('data-position', 'bl');
|
|
ctx.window.innerWidth = 1920;
|
|
ctx.window.innerHeight = 1080;
|
|
dm.register(panel);
|
|
|
|
panel._header._fire('pointerdown', {
|
|
button: 0, clientX: 100, clientY: 100,
|
|
preventDefault() {},
|
|
target: { closest() { return null; } }
|
|
});
|
|
panel._header._fire('pointermove', { clientX: 200, clientY: 300 });
|
|
panel._header._fire('pointerup', { pointerId: 1 });
|
|
|
|
assert.strictEqual(dm.state, 'IDLE');
|
|
assert.ok(!panel.classList.contains('is-dragging'));
|
|
// Should have persisted
|
|
const saved = JSON.parse(mockLocalStorage.getItem('panel-drag-p1'));
|
|
assert.ok(saved.xPct >= 0);
|
|
assert.ok(saved.yPct >= 0);
|
|
});
|
|
|
|
test('pointerup from PENDING (click) does not finalize', () => {
|
|
const dm = new DragManager();
|
|
const panel = makePanel('p1');
|
|
dm.register(panel);
|
|
panel._header._fire('pointerdown', {
|
|
button: 0, clientX: 100, clientY: 100,
|
|
preventDefault() {},
|
|
target: { closest() { return null; } }
|
|
});
|
|
panel._header._fire('pointerup', { pointerId: 1 });
|
|
assert.strictEqual(dm.state, 'IDLE');
|
|
assert.strictEqual(mockLocalStorage.getItem('panel-drag-p1'), null);
|
|
});
|
|
|
|
test('disable prevents drag', () => {
|
|
const dm = new DragManager();
|
|
dm.disable();
|
|
const panel = makePanel('p1');
|
|
dm.register(panel);
|
|
panel._header._fire('pointerdown', {
|
|
button: 0, clientX: 100, clientY: 100,
|
|
preventDefault() {},
|
|
target: { closest() { return null; } }
|
|
});
|
|
assert.strictEqual(dm.state, 'IDLE');
|
|
});
|
|
|
|
test('snap-to-edge works within threshold', () => {
|
|
const dm = new DragManager();
|
|
const panel = makePanel('p1');
|
|
panel.setAttribute('data-position', 'tl');
|
|
ctx.window.innerWidth = 1920;
|
|
ctx.window.innerHeight = 1080;
|
|
dm.register(panel);
|
|
|
|
// Simulate drag to near top-left edge
|
|
panel._header._fire('pointerdown', {
|
|
button: 0, clientX: 500, clientY: 500,
|
|
preventDefault() {},
|
|
target: { closest() { return null; } }
|
|
});
|
|
panel._header._fire('pointermove', { clientX: 510, clientY: 510 }); // trigger DRAGGING
|
|
|
|
// Panel is now detached; set its position near edge
|
|
panel.style.left = '5px';
|
|
panel.style.top = '10px';
|
|
|
|
panel._header._fire('pointerup', { pointerId: 1 });
|
|
|
|
// Should have snapped to margin (12px)
|
|
assert.strictEqual(panel.style.left, '12px');
|
|
assert.strictEqual(panel.style.top, '12px');
|
|
});
|
|
|
|
test('restorePositions applies saved viewport percentages', () => {
|
|
const dm = new DragManager();
|
|
const panel = makePanel('p2');
|
|
panel.setAttribute('data-position', 'br');
|
|
ctx.window.innerWidth = 1000;
|
|
ctx.window.innerHeight = 800;
|
|
dm.register(panel);
|
|
|
|
mockLocalStorage.setItem('panel-drag-p2', JSON.stringify({ xPct: 0.5, yPct: 0.25 }));
|
|
dm.restorePositions();
|
|
|
|
assert.strictEqual(panel.style.left, '500px');
|
|
assert.strictEqual(panel.style.top, '200px');
|
|
assert.strictEqual(panel.dataset.dragged, 'true');
|
|
assert.strictEqual(panel.getAttribute('data-position'), null);
|
|
});
|
|
|
|
test('handleResize clamps panels inside viewport', () => {
|
|
const dm = new DragManager();
|
|
const panel = makePanel('p3');
|
|
dm.register(panel);
|
|
|
|
// Simulate a dragged panel that's now off-screen
|
|
panel.dataset.dragged = 'true';
|
|
panel.style.left = '1800px';
|
|
panel.style.top = '900px';
|
|
ctx.window.innerWidth = 1000;
|
|
ctx.window.innerHeight = 600;
|
|
|
|
// Need querySelectorAll to return this panel
|
|
const origQSA = mockDocument.querySelectorAll;
|
|
mockDocument.querySelectorAll = function (sel) {
|
|
if (sel === '.live-overlay[data-dragged="true"]') return [panel];
|
|
return [];
|
|
};
|
|
|
|
dm.handleResize();
|
|
mockDocument.querySelectorAll = origQSA;
|
|
|
|
// Should be clamped
|
|
const left = parseFloat(panel.style.left);
|
|
const top = parseFloat(panel.style.top);
|
|
assert.ok(left + 300 <= 1000, 'left clamped: ' + left);
|
|
assert.ok(top + 200 <= 600, 'top clamped: ' + top);
|
|
});
|
|
|
|
test('Escape during drag reverts to corner position', () => {
|
|
const dm = new DragManager();
|
|
const panel = makePanel('esc1');
|
|
panel.setAttribute('data-position', 'bl');
|
|
dm.register(panel);
|
|
|
|
panel._header._fire('pointerdown', {
|
|
button: 0, clientX: 100, clientY: 100,
|
|
preventDefault() {},
|
|
target: { closest() { return null; } }
|
|
});
|
|
panel._header._fire('pointermove', { clientX: 120, clientY: 120 }); // trigger DRAGGING
|
|
assert.strictEqual(dm.state, 'DRAGGING');
|
|
|
|
// Simulate Escape
|
|
dm._handleKeyDown({ key: 'Escape' });
|
|
|
|
assert.strictEqual(dm.state, 'IDLE');
|
|
assert.ok(!panel.classList.contains('is-dragging'));
|
|
assert.strictEqual(panel.dataset.dragged, undefined);
|
|
assert.strictEqual(panel.style.transform, '');
|
|
});
|
|
|
|
test('Escape during drag reverts to saved position', () => {
|
|
const dm = new DragManager();
|
|
const panel = makePanel('esc2');
|
|
ctx.window.innerWidth = 1000;
|
|
ctx.window.innerHeight = 800;
|
|
dm.register(panel);
|
|
|
|
// Pre-save a dragged position
|
|
mockLocalStorage.setItem('panel-drag-esc2', JSON.stringify({ xPct: 0.3, yPct: 0.4 }));
|
|
dm.restorePositions();
|
|
assert.strictEqual(panel.dataset.dragged, 'true');
|
|
|
|
panel._header._fire('pointerdown', {
|
|
button: 0, clientX: 400, clientY: 400,
|
|
preventDefault() {},
|
|
target: { closest() { return null; } }
|
|
});
|
|
panel._header._fire('pointermove', { clientX: 500, clientY: 500 });
|
|
assert.strictEqual(dm.state, 'DRAGGING');
|
|
|
|
dm._handleKeyDown({ key: 'Escape' });
|
|
|
|
assert.strictEqual(dm.state, 'IDLE');
|
|
assert.strictEqual(panel.style.transform, 'none');
|
|
});
|
|
|
|
test('pointercancel during drag finalizes position', () => {
|
|
const dm = new DragManager();
|
|
const panel = makePanel('pc1');
|
|
panel.setAttribute('data-position', 'tl');
|
|
ctx.window.innerWidth = 1920;
|
|
ctx.window.innerHeight = 1080;
|
|
dm.register(panel);
|
|
|
|
panel._header._fire('pointerdown', {
|
|
button: 0, clientX: 100, clientY: 100,
|
|
preventDefault() {},
|
|
target: { closest() { return null; } }
|
|
});
|
|
panel._header._fire('pointermove', { clientX: 200, clientY: 200 });
|
|
assert.strictEqual(dm.state, 'DRAGGING');
|
|
|
|
panel._header._fire('pointercancel', {});
|
|
assert.strictEqual(dm.state, 'IDLE');
|
|
assert.ok(!panel.classList.contains('is-dragging'));
|
|
});
|
|
|
|
test('z-index increments on drag', () => {
|
|
const dm = new DragManager();
|
|
const p1 = makePanel('z1');
|
|
const p2 = makePanel('z2');
|
|
dm.register(p1);
|
|
dm.register(p2);
|
|
|
|
// Drag p1
|
|
p1._header._fire('pointerdown', {
|
|
button: 0, clientX: 100, clientY: 100,
|
|
preventDefault() {},
|
|
target: { closest() { return null; } }
|
|
});
|
|
p1._header._fire('pointermove', { clientX: 110, clientY: 110 });
|
|
const z1 = parseInt(p1.style.zIndex);
|
|
p1._header._fire('pointerup', { pointerId: 1 });
|
|
|
|
// Drag p2
|
|
p2._header._fire('pointerdown', {
|
|
button: 0, clientX: 100, clientY: 100,
|
|
preventDefault() {},
|
|
target: { closest() { return null; } }
|
|
});
|
|
p2._header._fire('pointermove', { clientX: 110, clientY: 110 });
|
|
const z2 = parseInt(p2.style.zIndex);
|
|
p2._header._fire('pointerup', { pointerId: 1 });
|
|
|
|
assert.ok(z2 > z1, 'z2 (' + z2 + ') should be greater than z1 (' + z1 + ')');
|
|
assert.ok(z1 >= 1001, 'z1 should be >= 1001');
|
|
});
|
|
|
|
test('disable mid-drag resets state', () => {
|
|
const dm = new DragManager();
|
|
const panel = makePanel('dis1');
|
|
dm.register(panel);
|
|
|
|
panel._header._fire('pointerdown', {
|
|
button: 0, clientX: 100, clientY: 100,
|
|
preventDefault() {},
|
|
target: { closest() { return null; } }
|
|
});
|
|
panel._header._fire('pointermove', { clientX: 120, clientY: 120 });
|
|
assert.strictEqual(dm.state, 'DRAGGING');
|
|
|
|
dm.disable();
|
|
assert.strictEqual(dm.state, 'IDLE');
|
|
assert.ok(!panel.classList.contains('is-dragging'));
|
|
assert.strictEqual(dm.enabled, false);
|
|
});
|
|
|
|
console.log('\n' + passed + ' passed, ' + failed + ' failed');
|
|
if (failed > 0) process.exit(1);
|