velxio/frontend/src/__tests__/drop-slot.test.ts

76 lines
3.0 KiB
TypeScript

/**
* Where a new component lands.
*
* The complaint this fixes: "I add an element and sometimes I cannot even
* see where it went". Two causes — placement at fixed world coordinates
* (off-screen once the canvas is panned, fixed elsewhere by anchoring to the
* visible corner) and a cascade keyed on components.length, which kept
* marching down-right forever even after parts were moved away or deleted.
*
* These lock the rule that replaced it: first FREE slot from the corner.
*/
import { describe, it, expect } from 'vitest';
import { pickDropSlot } from '../utils/dropSlot';
const ORIGIN = { x: 100, y: 50 };
const STEP = 36;
describe('pickDropSlot', () => {
it('drops at the corner when nothing is there', () => {
expect(pickDropSlot(ORIGIN, [], { step: STEP })).toEqual(ORIGIN);
});
it('steps down-right when the corner is taken', () => {
const slot = pickDropSlot(ORIGIN, [ORIGIN], { step: STEP });
expect(slot).toEqual({ x: 100 + STEP, y: 50 + STEP });
});
it('keeps cascading while each slot in turn is taken', () => {
const placed = [ORIGIN, { x: 100 + STEP, y: 50 + STEP }];
expect(pickDropSlot(ORIGIN, placed, { step: STEP })).toEqual({
x: 100 + 2 * STEP,
y: 50 + 2 * STEP,
});
});
it('reclaims a slot whose component was moved away', () => {
// The whole point of not counting components: the user parked the last
// drop somewhere else, so the corner is free again and the next one
// belongs there — not three steps further down the diagonal.
const movedAside = [{ x: 900, y: 700 }];
expect(pickDropSlot(ORIGIN, movedAside, { step: STEP })).toEqual(ORIGIN);
});
it('ignores components that are merely near, not on, the slot', () => {
const nearby = [{ x: ORIGIN.x + STEP * 0.9, y: ORIGIN.y + STEP * 0.9 }];
expect(pickDropSlot(ORIGIN, nearby, { step: STEP })).toEqual(ORIGIN);
});
it('treats a slightly nudged component as still parked', () => {
// A few px of drag should not make the next drop land on top of it.
const nudged = [{ x: ORIGIN.x + 4, y: ORIGIN.y - 3 }];
expect(pickDropSlot(ORIGIN, nudged, { step: STEP })).toEqual({
x: ORIGIN.x + STEP,
y: ORIGIN.y + STEP,
});
});
it('stops cascading instead of walking off the viewport', () => {
// Every slot occupied: it stacks at the last one rather than marching
// out of sight, which is the failure mode being fixed.
const full = Array.from({ length: 40 }, (_, i) => ({
x: ORIGIN.x + i * STEP,
y: ORIGIN.y + i * STEP,
}));
const slot = pickDropSlot(ORIGIN, full, { step: STEP, maxSlots: 12 });
expect(slot).toEqual({ x: ORIGIN.x + 12 * STEP, y: ORIGIN.y + 12 * STEP });
});
it('scales with zoom, since the step arrives in world units', () => {
// The canvas passes 36 screen-px / zoom, so the gap looks the same
// whatever the zoom level.
const zoomedOut = pickDropSlot(ORIGIN, [ORIGIN], { step: 36 / 0.5 });
expect(zoomedOut).toEqual({ x: 100 + 72, y: 50 + 72 });
});
});