Files
feuerwerk/tests/ui/block-drag.test.ts
adminandClaude Opus 5.5 a5184b3368
game-maker / gate (push) Failing after 3m4s
game-maker / suite (push) Successful in 14m57s
Let the rows of a Variables node be dragged into a new order
The Variables and Properties nodes listed their picked rows in the
inspector, but unlike Start, End and struct rows they could not be
reordered. The drag dots also sat right on the edge of the row and
touched the kind mark.

- buildPickedPortSection gathers each row with its warning line as one
  block and hands the blocks to sortBlocks. A move reorders the ports
  on the node and is one undo step. Properties uses the same section
  and gains the same reordering.
- Grip draws the dots 3 px in from the left edge and moves the kind
  mark 3 px clear of them (DOTS_PAD). The column keeps its width, so
  the name fields do not shift. Rows without dots are unchanged.

Tests:
- block-drag, reorder, variable-choices: 3 files, 13 tests, all green.
  block-drag now expects the mark 11 px in with dots, 0 without.
- e2e 23 on melektaus-atx: ALL PASSED, 32 checks (+3 for dragging vy
  before vx and undoing it). It ran before the padding change, which
  touches no checks there.
- The gate did not run before this commit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 16:08:59 +02:00

93 lines
3.9 KiB
TypeScript

/**
* What a sortable column shows before anything is dragged (#310): dots on every grip, and under the
* pointer the grip lit and the block that would go with it backed.
*/
import { beforeAll, describe, expect, it, vi } from 'vitest';
import { CanvasTextMetrics, FederatedPointerEvent, Graphics } from 'pixi.js';
import { Grip, HStack, Label, sortBlocks, Theme, VStack } from '../../src/ui';
vi.mock('../../src/ui/svg', () => ({ drawSvg: () => undefined, tintSvg: (svg: string) => svg }));
beforeAll(() => {
CanvasTextMetrics.measureText = ((text: string) => ({
width: text.length * 7,
height: 14,
lines: [text],
lineWidths: [text.length * 7],
})) as never;
});
/** Three blocks: one row, two rows, one row. */
function column(sortable = true) {
const body = new VStack();
const blocks = [1, 2, 1].map((size, index) => {
const grip = new Grip(new Label(String(index), { width: 28 }));
const rows = Array.from({ length: size }, (_, i) => {
const row = new HStack();
row.minHeight = Theme.rowHeight;
if (i === 0) row.add(grip);
return body.add(row);
});
return { grip, rows };
});
body.resize(200, 400);
if (sortable) sortBlocks(blocks, () => {});
return { body, blocks };
}
const pointer = (type: string) => Object.assign(new FederatedPointerEvent(null as never), { type });
const backs = (body: VStack) => body.children.filter((c) => c instanceof Graphics);
describe('the grips of a sortable column', () => {
it('show their dots, and only where the rows may be dragged', () => {
expect(column().blocks.map((b) => b.grip.dotsShown)).toEqual(['idle', 'idle', 'idle']);
expect(column(false).blocks.map((b) => b.grip.dotsShown)).toEqual(['none', 'none', 'none']);
});
it('move their mark right of the dots, and leave it at the edge where there are none', () => {
const [sorted] = column().blocks;
expect(sorted!.grip.content.x).toBe(11);
expect(sorted!.grip.content.x + sorted!.grip.content.w).toBe(sorted!.grip.w);
expect(column(false).blocks[0]!.grip.content.x).toBe(0);
});
it('light up under the pointer and back the whole block, behind its rows', () => {
const { body, blocks } = column();
const grip = blocks[1]!.grip;
grip.emit('pointerover', pointer('pointerover'));
expect(grip.dotsShown).toBe('lit');
expect(blocks[0]!.grip.dotsShown).toBe('idle');
const [back] = backs(body);
expect(body.children.indexOf(back!)).toBe(0);
const bounds = (back as Graphics).bounds;
expect([bounds.minY, bounds.maxY]).toEqual([Theme.rowHeight, 3 * Theme.rowHeight]);
});
it('go back to rest when the pointer leaves', () => {
const { body, blocks } = column();
const grip = blocks[1]!.grip;
grip.emit('pointerover', pointer('pointerover'));
grip.emit('pointerout', pointer('pointerout'));
expect(grip.dotsShown).toBe('idle');
expect(backs(body)).toEqual([]);
});
it('let the pointer pass by while a block is dragged, and answer it again after', () => {
const { body, blocks } = column();
const [dragged, , passed] = blocks.map((b) => b.grip);
const at = (type: string, y: number, button = 0) =>
Object.assign(pointer(type), { button, global: { x: 0, y } });
dragged!.emit('pointerdown', at('pointerdown', 0));
dragged!.emit('globalpointermove', at('globalpointermove', 10));
passed!.emit('pointerover', pointer('pointerover'));
expect(blocks.map((b) => b.grip.dotsShown)).toEqual(['lit', 'idle', 'idle']);
// The drop line is a Graphics too; a backing would sit behind the rows.
expect(body.children[0]).not.toBeInstanceOf(Graphics);
expect(blocks.map((b) => b.grip.cursor)).toEqual(['grabbing', 'grabbing', 'grabbing']);
dragged!.emit('pointerupoutside', at('pointerupoutside', 10));
expect(blocks.map((b) => b.grip.dotsShown)).toEqual(['idle', 'idle', 'lit']);
expect(backs(body)).toHaveLength(1);
expect(blocks.map((b) => b.grip.cursor)).toEqual(['grab', 'grab', 'grab']);
});
});