Files
Tenney95-ComfyUI-NodeAligner/tests/node_info.test.cjs
T

594 lines
29 KiB
JavaScript

const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const vm = require('node:vm');
class Element {
constructor(tagName = 'div') {
this.tagName = tagName.toUpperCase();
this.children = [];
this.style = {};
this.listeners = new Map();
this.classList = new Set();
this.classList.contains = this.classList.has.bind(this.classList);
}
appendChild(child) {
child.parentNode = this;
this.children.push(child);
return child;
}
append(...children) { children.forEach(child => this.appendChild(child)); }
remove() {
if (this.parentNode) this.parentNode.children.splice(this.parentNode.children.indexOf(this), 1);
this.parentNode = null;
}
attachShadow() { return this.shadowRoot = new Element('shadow-root'); }
setAttribute(name, value) { this[name] = value; }
addEventListener(type, handler) {
if (!this.listeners.has(type)) this.listeners.set(type, new Set());
this.listeners.get(type).add(handler);
}
removeEventListener(type, handler) { this.listeners.get(type)?.delete(handler); }
listenerCount(type) { return this.listeners.get(type)?.size || 0; }
dispatch(type, data = {}) {
const event = { target: this, currentTarget: this, button: 0, clientX: 200, clientY: 100,
preventDefault() {}, stopPropagation() {}, composedPath: () => [this], ...data };
for (const handler of [...this.listeners.get(type) || []]) handler(event);
}
querySelectorAll(selector) {
const matches = [];
const visit = element => {
for (const child of element.children) {
if (selector.startsWith('#') ? child.id === selector.slice(1)
: selector.startsWith('.') ? child.classList.has(selector.slice(1))
: child.tagName.toLowerCase() === selector) matches.push(child);
visit(child);
}
};
visit(this);
return matches;
}
querySelector(selector) { return this.querySelectorAll(selector)[0] || null; }
set innerHTML(html) {
this.children = [];
for (const match of html.matchAll(/<button\s+id="([^"]+)"/g)) {
const button = new Element('button');
button.id = match[1];
this.appendChild(button);
}
}
getBoundingClientRect() {
const left = Number.parseFloat(this.style.left) || 20;
const top = Number.parseFloat(this.style.top) || 20;
return { left, top, width: 298, height: 40, right: left + 298, bottom: top + 40 };
}
get offsetLeft() { return this.getBoundingClientRect().left; }
get offsetTop() { return this.getBoundingClientRect().top; }
get parentElement() { return this.parentNode; }
closest(selector) {
for (let element = this; element; element = element.parentElement) {
if (selector.startsWith('.') && element.classList.has(selector.slice(1))) return element;
}
return null;
}
contains(target) { return target === this || this.children.some(child => child.contains(target)); }
}
class LGraphNode {
constructor(x = 0, y = 0, width = 100, height = 100) {
this.pos = [x, y];
this.size = [width, height];
this.resizeCount = 0;
}
setSize(size) {
this.resizeCount++;
this.size = [...size];
}
}
function load(nodes = []) {
const document = new Element('document');
document.body = new Element('body');
document.createElement = tag => new Element(tag);
document.getElementById = id => document.body.querySelector(`#${id}`);
document.querySelector = selector => selector === 'canvas#graph-canvas' ? null : document.body.querySelector(selector);
const window = new Element('window');
Object.assign(window, { innerWidth: 1600, innerHeight: 900, LGraphNode });
const storage = new Map();
const writes = [];
const history = [];
const frames = new Map();
let nextFrame = 0;
const canvas = {
selectedItems: new Set(nodes),
selected_nodes: Object.fromEntries(nodes.map((node, index) => [index, node])),
ds: { offset: [0, 0], scale: 2 },
canvas: new Element('canvas'),
graph: { beforeChange() { history.push('before'); }, afterChange() { history.push('after'); } },
setDirty() {},
};
const canvasParent = document.body.appendChild(new Element('div'));
canvasParent.appendChild(canvas.canvas);
const extensions = [];
const app = { canvas, graph: canvas.graph, registerExtension(extension) { extensions.push(extension); } };
const context = vm.createContext({
app, document, window, LGraphNode, LGraphCanvas: { active_canvas: canvas }, console,
localStorage: {
getItem: key => storage.get(key) ?? null,
setItem(key, value) { storage.set(key, value); writes.push(key); },
},
setTimeout() {}, clearTimeout() {},
requestAnimationFrame(callback) { frames.set(++nextFrame, callback); return nextFrame; },
cancelAnimationFrame(id) { frames.delete(id); },
});
const source = fs.readFileSync(path.join(__dirname, '..', 'node_info.js'), 'utf8')
.replace(/^import\s+.*?;\s*$/gm, '');
vm.runInContext(`${source}\nglobalThis.manager = ButtonManager;`, context);
const flushFrames = () => {
const pending = [...frames.values()];
frames.clear();
pending.forEach(callback => callback());
};
return { manager: context.manager, app, canvas, context, extensions, document, window, writes, history, storage, flushFrames };
}
let failures = 0;
let checks = 0;
function test(name, check) {
checks++;
try { check(); console.log(`PASS ${name}`); }
catch (error) { failures++; console.error(`FAIL ${name}: ${error.message}`); }
}
test('registers with ComfyUI and initializes only once', () => {
const { manager, extensions, document } = load();
assert.equal(extensions.length, 1);
assert.equal(typeof extensions[0].setup, 'function');
extensions[0].setup();
manager.init();
assert.equal(document.body.querySelectorAll('#alignment-buttons').length, 1);
for (const button of manager.buttonContainer.shadowRoot.querySelectorAll('.custom-button')) {
assert.ok(button.title && button.title !== 'undefined');
assert.equal(button['aria-label'], button.title);
}
});
test('uses the current canvas selectedItems and excludes groups and reroutes', () => {
const node = new LGraphNode();
const { manager, canvas, context } = load([node]);
canvas.selectedItems.add({ pos: [5, 5], size: [100, 100] });
canvas.selected_nodes = { stale: new LGraphNode() };
context.LGraphCanvas.active_canvas = null;
assert.deepEqual([...manager.getSelectedNodes()], [node]);
canvas.selectedItems.clear();
assert.equal(manager.getSelectedNodes().length, 0);
});
test('supports the legacy selected_nodes object', () => {
const node = new LGraphNode();
const { manager, canvas } = load([node]);
delete canvas.selectedItems;
assert.deepEqual([...manager.getSelectedNodes()], [node]);
});
test('records one graph change around a toolbar action', () => {
const first = new LGraphNode(10, 20);
const second = new LGraphNode(100, 80);
const { manager, history, canvas } = load([first, second]);
const canvasChanges = [];
canvas.graph.beforeChange = () => { assert.equal(second.pos[0], 100); history.push('before'); };
canvas.graph.afterChange = () => { assert.equal(second.pos[0], 10); history.push('after'); };
canvas.emitBeforeChange = () => canvasChanges.push('before');
canvas.emitAfterChange = () => canvasChanges.push('after');
manager.runAction(manager.alignLeft);
assert.deepEqual(history, ['before', 'after']);
assert.deepEqual(canvasChanges, ['before', 'after']);
});
test('toolbar actions are safe without a canvas', () => {
const { manager, app, context } = load();
app.canvas = null;
context.LGraphCanvas.active_canvas = null;
assert.equal(manager.getSelectedNodes().length, 0);
for (const button of manager.getButtons().filter(button => button.action)) manager.runAction(button.action);
});
test('resetting an empty canvas does not enter workflow history', () => {
const { manager, canvas, history } = load();
manager.runAction(manager.alignLeft);
assert.deepEqual([...canvas.ds.offset], [80, 140]);
assert.equal(canvas.ds.scale, 1);
assert.deepEqual(history, []);
});
test('equal sizes use setSize, round to five and preserve the other dimension', () => {
for (const [action, expected] of [
['equalWidth', [[305, 203], [305, 251]]],
['equalMinWidth', [[210, 203], [210, 251]]],
['equalHeight', [[211, 250], [303, 250]]],
['equalMinHeight', [[211, 205], [303, 205]]],
]) {
const nodes = [new LGraphNode(0, 0, 211, 203), new LGraphNode(0, 0, 303, 251)];
const { manager } = load(nodes);
manager.runAction(manager[action]);
assert.deepEqual(nodes.map(node => node.size), expected, action);
assert.deepEqual(nodes.map(node => node.resizeCount), [1, 1], action);
}
});
test('preserves the six alignment operations', () => {
for (const [action, axis, positions] of [
['alignLeft', 0, [10, 10]], ['alignRight', 0, [250, 200]],
['alignTop', 1, [20, 20]], ['alignBottom', 1, [100, 80]],
['alignCenterVertically', 0, [130, 105]],
['alignCenterHorizontally', 1, [60, 50]],
]) {
const nodes = [new LGraphNode(10, 20, 100, 100), new LGraphNode(200, 80, 150, 120)];
const { manager } = load(nodes);
manager.runAction(manager[action]);
assert.deepEqual(nodes.map(node => node.pos[axis]), positions, action);
}
});
for (const count of [4, 8]) {
for (const [action, axis, anchor] of [
['alignLeft', 0, 0], ['alignRight', 0, 1],
['alignTop', 1, 0], ['alignBottom', 1, 1],
['alignCenterVertically', 0, 0.5], ['alignCenterHorizontally', 1, 0.5],
]) {
test(`${action} aligns all ${count} selected nodes in one transaction`, () => {
const nodes = Array.from({ length: count }, (_, index) => {
const position = index === count - 1 ? 420 : 100;
return axis === 0
? new LGraphNode(position, index * 250, 100 + index * 10, 80 + index * 7)
: new LGraphNode(index * 250, position, 80 + index * 7, 100 + index * 10);
});
const minimum = Math.min(...nodes.map(node => node.pos[axis]));
const maximum = Math.max(...nodes.map(node => node.pos[axis] + node.size[axis]));
const target = minimum + (maximum - minimum) * anchor;
const otherPositions = nodes.map(node => node.pos[1 - axis]);
const sizes = nodes.map(node => [...node.size]);
const { manager, history } = load(nodes);
manager.runAction(manager[action]);
assert.deepEqual(nodes.map(node => node.pos[axis] + node.size[axis] * anchor), nodes.map(() => target));
assert.deepEqual(nodes.map(node => node.pos[1 - axis]), otherPositions);
assert.deepEqual(nodes.map(node => node.size), sizes);
assert.deepEqual(history, ['before', 'after']);
});
}
}
test('preserves horizontal and vertical distribution', () => {
const horizontal = [new LGraphNode(0, 0, 100), new LGraphNode(200, 0, 50), new LGraphNode(400, 0, 150)];
const first = load(horizontal);
first.manager.runAction(first.manager.horizontalDistribution);
assert.deepEqual(horizontal.map(node => node.pos[0]), [0, 225, 400]);
const vertical = [new LGraphNode(0, 0, 100, 100), new LGraphNode(0, 200, 100, 150), new LGraphNode(0, 500, 100, 200)];
const second = load(vertical);
second.manager.runAction(second.manager.verticalDistribution);
assert.deepEqual(vertical.map(node => node.pos[1]), [0, 225, 500]);
});
test('preserves grid tracks, selection order and repeated alignment or distribution', () => {
const grid = [0, 300, 900].flatMap(y => [0, 400, 1000].map(x => [x, y, 100, 100]));
const layouts = [
{
name: 'irregular 2x3',
data: [[100, 100, 120, 80], [630, 110, 180, 100], [1210, 90, 100, 120],
[120, 520, 160, 110], [610, 500, 120, 140], [1200, 530, 200, 90]],
expected: {
alignLeft: [0, [100, 630, 1210, 100, 630, 1210]],
alignRight: [0, [160, 630, 1300, 120, 690, 1200]],
alignCenterVertically: [0, [130, 620, 1250, 110, 650, 1200]],
alignTop: [1, [100, 100, 100, 520, 520, 520]],
alignBottom: [1, [130, 110, 90, 530, 500, 550]],
alignCenterHorizontally: [1, [110, 100, 90, 515, 500, 525]],
horizontalDistribution: [0, [100, 625, 1210, 120, 680, 1200]],
verticalDistribution: [1, [100, 110, 90, 520, 500, 530]],
},
},
{ name: '3x3', data: grid,
horizontal: [0, 500, 1000, 0, 500, 1000, 0, 500, 1000],
vertical: [0, 0, 0, 450, 450, 450, 900, 900, 900] },
{ name: '3x3 missing center', data: grid.filter((_, index) => index !== 4),
horizontal: [0, 500, 1000, 0, 1000, 0, 500, 1000],
vertical: [0, 0, 0, 450, 450, 900, 900, 900] },
];
for (const layout of layouts) {
const indices = layout.data.map((_, index) => index);
const orders = [indices, [...indices].reverse(),
indices.filter(index => index % 2 === 0).reverse().concat(indices.filter(index => index % 2))];
const expected = layout.expected || {
alignLeft: [0, layout.data.map(node => node[0])],
alignRight: [0, layout.data.map(node => node[0])],
alignCenterVertically: [0, layout.data.map(node => node[0])],
alignTop: [1, layout.data.map(node => node[1])],
alignBottom: [1, layout.data.map(node => node[1])],
alignCenterHorizontally: [1, layout.data.map(node => node[1])],
horizontalDistribution: [0, layout.horizontal],
verticalDistribution: [1, layout.vertical],
};
for (const [action, [axis, positions]] of Object.entries(expected)) {
for (const order of orders) {
const nodes = layout.data.map(node => new LGraphNode(...node));
const { manager, history } = load(order.map(index => nodes[index]));
const message = `${layout.name}, ${action}, selection ${order}`;
manager.runAction(manager[action]);
assert.deepEqual(nodes.map(node => node.pos[axis]), positions, message);
assert.deepEqual(nodes.map(node => node.pos[1 - axis]), layout.data.map(node => node[1 - axis]), message);
assert.deepEqual(nodes.map(node => node.size), layout.data.map(node => node.slice(2)), message);
assert.deepEqual(history, ['before', 'after'], message);
const first = nodes.map(node => [...node.pos]);
manager.runAction(manager[action]);
assert.deepEqual(nodes.map(node => node.pos), first, `${message}, repeated action`);
}
}
}
});
test('keeps a tall singleton from changing the spacing of other columns', () => {
const data = [[0, 0, 100, 100], [0, 300, 100, 100],
[500, 0, 100, 150], [500, 500, 100, 100], [1000, 100, 100, 2000]];
for (const order of [[0, 1, 2, 3, 4], [4, 3, 2, 1, 0], [2, 4, 1, 3, 0]]) {
const nodes = data.map(node => new LGraphNode(...node));
const { manager } = load(order.map(index => nodes[index]));
manager.runAction(manager.verticalDistribution);
assert.deepEqual(nodes.map(node => node.pos), data.map(node => node.slice(0, 2)));
}
});
test('matches offset and incomplete grid rows using the top and left reference nodes', () => {
const layouts = [
{ name: 'screenshot 2x3', data: [[90, 110], [590, 110], [1515, 190], [90, 470], [590, 550], [1175, 550]],
xs: [90, 590, 1515, 90, 590, 1515], ys: [110, 110, 110, 470, 470, 470] },
{ name: 'screenshot missing B', data: [[90, 110], [1515, 190], [90, 470], [590, 550], [1175, 550]],
xs: [90, 1515, 90, 590, 1515], ys: [110, 110, 470, 470, 470] },
{ name: 'two incomplete rows', data: [[90, 110], [590, 110], [590, 470], [1515, 470]],
xs: [90, 590, 590, 1515], ys: [110, 110, 470, 470] },
];
for (const layout of layouts) {
const indices = layout.data.map((_, index) => index);
const orders = [indices, [...indices].reverse(),
indices.filter(index => index % 2 === 0).reverse().concat(indices.filter(index => index % 2))];
for (const order of orders) {
for (const sequence of [['alignLeft', 'alignTop'], ['alignTop', 'alignLeft']]) {
const nodes = layout.data.map(([x, y]) => new LGraphNode(x, y, 280, 100));
const { manager, history } = load(order.map(index => nodes[index]));
for (const action of [...sequence, ...sequence]) {
const axis = action === 'alignLeft' ? 0 : 1;
const other = nodes.map(node => node.pos[1 - axis]);
const start = history.length;
const message = `${layout.name}, ${sequence}, selection ${order}, ${action}`;
manager.runAction(manager[action]);
assert.deepEqual(nodes.map(node => node.pos[axis]), axis === 0 ? layout.xs : layout.ys, message);
assert.deepEqual(nodes.map(node => node.pos[1 - axis]), other, message);
assert.deepEqual(nodes.map(node => node.size), nodes.map(() => [280, 100]), message);
assert.deepEqual(history.slice(start), ['before', 'after'], message);
}
}
}
}
});
test('expands overlapping heterogeneous nodes forward with a stable minimum gap', () => {
for (const axis of [0, 1]) {
const nodes = [[0, 0, 600], [10, 5, 50], [20, -4, 800]].map(([position, other, size]) =>
axis === 0 ? new LGraphNode(position, other, size, 100) : new LGraphNode(other, position, 100, size));
const { manager } = load(nodes);
const action = axis === 0 ? manager.horizontalDistribution : manager.verticalDistribution;
manager.runAction(action);
assert.deepEqual(nodes.map(node => node.pos[axis]), [0, 620, 690]);
assert.deepEqual(nodes.map(node => node.pos[1 - axis]), [0, 5, -4]);
assert.deepEqual(nodes.map(node => node.size[axis]), [600, 50, 800]);
const first = nodes.map(node => [...node.pos]);
manager.runAction(action);
assert.deepEqual(nodes.map(node => node.pos), first);
}
});
test('all toolbar actions are safe with empty and single-node selections', () => {
for (const nodes of [[], [new LGraphNode(120, 80, 235, 155)]]) {
const { manager, history } = load(nodes);
for (const button of manager.getButtons().filter(button => button.action)) manager.runAction(button.action);
assert.deepEqual(nodes.map(node => [...node.pos, ...node.size]), nodes.length ? [[120, 80, 235, 155]] : []);
if (!nodes.length) assert.deepEqual(history, []);
}
});
test('keeps heterogeneous single columns together and separates overlapping column bounds', () => {
const singleColumn = [[0, 0, 400, 100], [0, 150, 400, 100], [10, 400, 200, 100], [10, 600, 200, 100]];
for (const [action, axis, expected] of [
['alignLeft', 0, [0, 0, 0, 0]], ['verticalDistribution', 1, [0, 200, 400, 600]],
]) {
const nodes = singleColumn.map(node => new LGraphNode(...node));
const { manager } = load(nodes);
manager.runAction(manager[action]);
assert.deepEqual(nodes.map(node => node.pos[axis]), expected, action);
assert.deepEqual(nodes.map(node => node.pos[1 - axis]), singleColumn.map(node => node[1 - axis]), action);
const first = nodes.map(node => [...node.pos]);
manager.runAction(manager[action]);
assert.deepEqual(nodes.map(node => node.pos), first, `${action}, repeated action`);
}
const overlapping = [[50, 0, 100, 100], [-100, 300, 400, 100], [250, 0, 100, 100], [-50, 300, 700, 100]];
for (const axis of [0, 1]) {
for (const [action, anchor] of axis === 0
? [['alignLeft', 0], ['alignRight', 1], ['alignCenterVertically', 0.5]]
: [['alignTop', 0], ['alignBottom', 1], ['alignCenterHorizontally', 0.5]]) {
const data = overlapping.map(node => axis === 0 ? node : [node[1], node[0], node[3], node[2]]);
const nodes = data.map(node => new LGraphNode(...node));
const { manager } = load(nodes);
manager.runAction(manager[action]);
const aligned = nodes.map(node => node.pos[axis] + node.size[axis] * anchor);
assert.equal(aligned[0], aligned[1], action);
assert.equal(aligned[2], aligned[3], action);
const firstEnd = Math.max(...nodes.slice(0, 2).map(node => node.pos[axis] + node.size[axis]));
const secondStart = Math.min(...nodes.slice(2).map(node => node.pos[axis]));
assert.ok(secondStart - firstEnd >= 20, `${action}, overlapping track bounds`);
if (anchor === 0) assert.deepEqual(nodes.map(node => node.pos[axis]), [50, 50, 513.5, 513.5], action);
assert.deepEqual(nodes.map(node => node.pos[1 - axis]), data.map(node => node[1 - axis]), action);
const first = nodes.map(node => [...node.pos]);
manager.runAction(manager[action]);
assert.deepEqual(nodes.map(node => node.pos), first, `${action}, repeated action`);
}
}
const xs = [-20, -20, 20, 20, 540, 520, 540, 500];
const widths = [300, 180, 360, 360, 300, 300, 300, 360];
const ys = [10, 290, 570, 840, -10, 290, 560, 830];
const heights = [180, 140, 180, 100, 180, 180, 140, 140];
const nodes = xs.map((x, index) => new LGraphNode(x, ys[index], widths[index], heights[index]));
const { manager } = load(nodes);
manager.runAction(manager.alignCenterVertically);
const centers = nodes.map(node => node.pos[0] + node.size[0] / 2);
assert.deepEqual(centers.slice(0, 4), Array(4).fill(centers[0]));
assert.deepEqual(centers.slice(4), Array(4).fill(centers[4]));
assert.ok(centers[0] < centers[4]);
assert.deepEqual(nodes.map(node => node.pos[1]), ys);
const first = nodes.map(node => [...node.pos]);
manager.runAction(manager.alignCenterVertically);
assert.deepEqual(nodes.map(node => node.pos), first, '2-column center alignment, repeated action');
});
test('keeps wide and narrow tracks separate after repeated right alignment', () => {
const xs = [0, 0, 0, 650, 650, 650, 650, 1280, 1300];
const widths = [900, 900, 900, 900, 900, 600, 180, 180, 180];
let expected;
for (const order of [[0, 1, 2, 3, 4, 5, 6, 7, 8], [8, 7, 6, 5, 4, 3, 2, 1, 0], [6, 2, 8, 0, 5, 3, 7, 1, 4]]) {
const nodes = xs.map((x, index) => new LGraphNode(x, index * 200, widths[index], 100));
const { manager } = load(order.map(index => nodes[index]));
manager.runAction(manager.alignRight);
const rights = nodes.map(node => node.pos[0] + node.size[0]);
assert.deepEqual(rights.slice(0, 3), Array(3).fill(rights[0]));
assert.deepEqual(rights.slice(3, 7), Array(4).fill(rights[3]));
assert.deepEqual(rights.slice(7), Array(2).fill(rights[7]));
assert.ok(rights[0] < rights[3] && rights[3] < rights[7]);
const first = nodes.map(node => [...node.pos]);
if (expected) assert.deepEqual(first, expected, 'selection order');
expected = first;
manager.runAction(manager.alignRight);
assert.deepEqual(nodes.map(node => node.pos), first, 'repeated right alignment');
}
});
test('uses rendered bounds for alignment and distribution without changing stored sizes', () => {
for (const axis of [0, 1]) {
const actions = axis === 0
? ['alignRight', 'alignCenterVertically', 'horizontalDistribution']
: ['alignBottom', 'alignCenterHorizontally', 'verticalDistribution'];
const expected = [[820, 750, 800], [410, 375, 400], [0, 365, 800]];
for (const [index, action] of actions.entries()) {
const nodes = [0, 300, 800].map((position, nodeIndex) => {
const node = axis === 0 ? new LGraphNode(position, 0) : new LGraphNode(0, position);
const rendered = [180, 250, 200][nodeIndex];
node.renderingSize = axis === 0 ? [rendered, 100] : [100, rendered];
return node;
});
const { manager } = load(nodes);
manager.runAction(manager[action]);
assert.deepEqual(nodes.map(node => node.pos[axis]), expected[index], action);
assert.deepEqual(nodes.map(node => node.pos[1 - axis]), [0, 0, 0], action);
assert.deepEqual(nodes.map(node => node.size), [[100, 100], [100, 100], [100, 100]], action);
}
}
});
test('drag listeners exist only during dragging and ordinary mouseup never saves', () => {
const { manager, document, writes } = load();
manager.init();
const dividers = manager.buttonContainer.shadowRoot.querySelectorAll('.divider');
assert.equal(dividers.length, 3);
assert.equal(document.listenerCount('mousemove'), 0);
assert.equal(document.listenerCount('mouseup'), 0);
const initialWrites = writes.length;
manager.onDragEnd();
assert.equal(writes.length, initialWrites);
for (const divider of dividers) {
divider.dispatch('mousedown');
assert.equal(document.listenerCount('mousemove'), 1);
assert.equal(document.listenerCount('mouseup'), 1);
document.dispatch('mousemove', { clientX: 230, clientY: 120 });
document.dispatch('mouseup');
assert.equal(document.listenerCount('mousemove'), 0);
assert.equal(document.listenerCount('mouseup'), 0);
}
assert.equal(writes.length - initialWrites, 3);
});
test('temporary toolbar dragging does not overwrite the pinned position', () => {
const { manager, document, writes, storage } = load();
manager.init();
const divider = manager.buttonContainer.shadowRoot.querySelector('.divider');
divider.dispatch('mousedown');
document.dispatch('mousemove', { clientX: 350, clientY: 220 });
document.dispatch('mouseup');
const pinned = storage.get('NodeAlignerButtonContainerPosition');
manager.isPermanent = false;
manager.setPosition(700, 500);
const initialWrites = writes.length;
divider.dispatch('mousedown');
document.dispatch('mousemove', { clientX: 220, clientY: 120 });
document.dispatch('mouseup');
assert.equal(writes.length, initialWrites);
assert.equal(storage.get('NodeAlignerButtonContainerPosition'), pinned);
manager.buttonContainer.shadowRoot.querySelector('#pin-mode').dispatch('click');
for (const [side, value] of Object.entries(JSON.parse(pinned))) {
assert.equal(manager.buttonContainer.style[side], value);
}
});
test('document pointerup updates the temporary toolbar after selection and clamps its position', () => {
const nodes = [new LGraphNode(), new LGraphNode()];
const { manager, document, canvas, writes, flushFrames } = load(nodes);
manager.init();
manager.isPermanent = false;
manager.hide();
const initialWrites = writes.length;
const release = target => document.dispatch('pointerup', {
target, clientX: 30, clientY: 10,
composedPath: () => [target, target.parentElement, document],
});
release(canvas.canvas);
assert.equal(manager.isShow, false);
flushFrames();
assert.equal(manager.isShow, true);
assert.equal(manager.buttonContainer.style.left, '0px');
assert.equal(manager.buttonContainer.style.top, '0px');
assert.equal(writes.length, initialWrites);
canvas.selectedItems.clear();
document.dispatch('pointerup', { composedPath: () => [manager.buttonContainer] });
flushFrames();
assert.equal(manager.isShow, true);
release(canvas.canvas);
flushFrames();
assert.equal(manager.isShow, false);
const overlay = canvas.canvas.parentElement.appendChild(new Element('div'));
nodes.forEach(node => canvas.selectedItems.add(node));
const sidebar = canvas.canvas.parentElement.appendChild(new Element('div'));
const workflowTab = sidebar.appendChild(new Element('button'));
release(workflowTab);
flushFrames();
assert.equal(manager.isShow, false);
overlay.classList.add('lg-node');
release(overlay.appendChild(new Element('textarea')));
flushFrames();
assert.equal(manager.isShow, true);
});
test('destroy removes the toolbar and active drag listeners', () => {
const { manager, document } = load();
manager.init();
manager.buttonContainer.shadowRoot.querySelector('.divider').dispatch('mousedown');
manager.destroy();
assert.equal(document.listenerCount('mousemove'), 0);
assert.equal(document.listenerCount('mouseup'), 0);
assert.equal(document.listenerCount('pointerup'), 0);
assert.equal(document.listenerCount('pointerdown'), 0);
assert.equal(document.body.querySelectorAll('#alignment-buttons').length, 0);
assert.equal(manager.isInitialized, false);
manager.init();
assert.equal(document.body.querySelectorAll('#alignment-buttons').length, 1);
assert.equal(document.listenerCount('pointerup'), 1);
});
process.exitCode = failures ? 1 : 0;
console.log(`${checks - failures}/${checks} checks passed`);