Edge handles on a selected Section/Container/Columns write padding via the per-device setter as one undo burst; Alt = opposite side, Shift = all four, Ctrl/Cmd snaps to 8px, Escape cancels. Refs #56
115 lines
5.9 KiB
TypeScript
115 lines
5.9 KiB
TypeScript
import { describe, test, expect, afterEach } from 'vitest';
|
|
import React from 'react';
|
|
import { renderEditorHarness, EditorHarness } from '../test-utils/editorHarness';
|
|
import { DeviceProvider } from '../state/DeviceContext';
|
|
import { SpacingHandles } from './SpacingHandles';
|
|
import { insertBlock } from '../panels/left/blocks-registry';
|
|
import { closeContinuousBurst } from '../panels/right/styles/shared';
|
|
import type { DeviceMode } from '../types';
|
|
|
|
let h: EditorHarness;
|
|
afterEach(() => { h?.unmount(); closeContinuousBurst(); });
|
|
|
|
function seed(style: Record<string, string>, device: DeviceMode = 'desktop') {
|
|
h = renderEditorHarness({ wrap: (tree) => <DeviceProvider initialDevice={device}>{tree}</DeviceProvider> });
|
|
h.mountChild(<SpacingHandles />);
|
|
let id = '';
|
|
h.act(() => { id = insertBlock(h.query, h.actions, 'section', { parentId: 'ROOT', select: false })!; });
|
|
h.act(() => { h.actions.setProp(id, (p: any) => { p.style = { ...p.style, ...style }; }); });
|
|
// Start from a clean history so the drag's undo steps are countable.
|
|
h.act(() => { h.actions.history.clear(); });
|
|
h.act(() => { h.actions.selectNode(id); });
|
|
return id;
|
|
}
|
|
const props = (id: string) => h.query.node(id).get().data.props;
|
|
const handle = (side: string) => document.querySelector(`.spacing-handle[data-side="${side}"]`) as HTMLElement | null;
|
|
const ptr = (type: string, init: Record<string, unknown> = {}) =>
|
|
new MouseEvent(type, { bubbles: true, cancelable: true, button: 0, ...init });
|
|
const down = (side: string, y: number, x = 0) => h.act(() => { handle(side)!.dispatchEvent(ptr('pointerdown', { clientY: y, clientX: x })); });
|
|
const move = (y: number, init: Record<string, unknown> = {}, x = 0) => h.act(() => { document.body.dispatchEvent(ptr('pointermove', { clientY: y, clientX: x, ...init })); });
|
|
const up = () => h.act(() => { document.body.dispatchEvent(ptr('pointerup')); });
|
|
const esc = () => h.act(() => { document.body.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true, cancelable: true })); });
|
|
/** Undo steps recorded since history.clear(). */
|
|
const undoAll = () => { let n = 0; while (h.query.history.canUndo()) { h.act(() => { h.actions.history.undo(); }); n++; if (n > 50) break; } return n; };
|
|
|
|
describe('SpacingHandles (real editor)', () => {
|
|
test('renders four handles for a selected Section, none when nothing/non-container is selected', () => {
|
|
const id = seed({ paddingTop: '40px' });
|
|
expect(['top', 'bottom', 'left', 'right'].every((s) => handle(s))).toBe(true);
|
|
h.act(() => { h.actions.clearEvents(); });
|
|
expect(document.querySelector('.spacing-handles')).toBeNull();
|
|
let hid = '';
|
|
h.act(() => { hid = insertBlock(h.query, h.actions, 'heading', { parentId: 'ROOT', select: true })!; });
|
|
expect(document.querySelector('.spacing-handles')).toBeNull();
|
|
expect(id).toBeTruthy();
|
|
});
|
|
|
|
test('top handle +24px => paddingTop 64px in ONE undo step', () => {
|
|
const id = seed({ paddingTop: '40px' });
|
|
down('top', 100);
|
|
move(110); move(120); move(124);
|
|
expect(document.querySelector('.spacing-readout')?.textContent).toBe('64px');
|
|
up();
|
|
expect(props(id).style.paddingTop).toBe('64px');
|
|
expect(document.querySelector('.spacing-readout')).toBeNull();
|
|
expect(undoAll()).toBe(1);
|
|
expect(props(id).style.paddingTop).toBe('40px');
|
|
});
|
|
|
|
test('bottom handle dragged up grows paddingBottom', () => {
|
|
const id = seed({ paddingBottom: '10px' });
|
|
down('bottom', 100); move(80); up();
|
|
expect(props(id).style.paddingBottom).toBe('30px');
|
|
});
|
|
|
|
test('Alt changes top and bottom; Shift changes all four', () => {
|
|
let id = seed({ paddingTop: '40px', paddingBottom: '40px', paddingLeft: '10px', paddingRight: '10px' });
|
|
down('top', 100); move(110, { altKey: true }); up();
|
|
expect(props(id).style).toMatchObject({ paddingTop: '50px', paddingBottom: '50px', paddingLeft: '10px', paddingRight: '10px' });
|
|
h.unmount();
|
|
id = seed({ paddingTop: '40px', paddingBottom: '40px', paddingLeft: '10px', paddingRight: '10px' });
|
|
down('top', 100); move(110, { shiftKey: true }); up();
|
|
expect(props(id).style).toMatchObject({ paddingTop: '50px', paddingBottom: '50px', paddingLeft: '50px', paddingRight: '50px' });
|
|
expect(undoAll()).toBe(1);
|
|
});
|
|
|
|
test('left handle uses the X axis', () => {
|
|
const id = seed({ paddingLeft: '10px' });
|
|
down('left', 0, 100); move(0, {}, 120); up();
|
|
expect(props(id).style.paddingLeft).toBe('30px');
|
|
});
|
|
|
|
test('mobile device: write lands in styleMobile, desktop style untouched', () => {
|
|
const id = seed({ paddingTop: '40px' }, 'mobile');
|
|
down('top', 100); move(124); up();
|
|
expect(props(id).styleMobile.paddingTop).toBe('64px');
|
|
expect(props(id).style.paddingTop).toBe('40px');
|
|
expect(undoAll()).toBe(1);
|
|
});
|
|
|
|
test('legacy shorthand is expanded to longhands and cleared in the same single step', () => {
|
|
const id = seed({ padding: '40px 0', paddingTop: '', paddingBottom: '', paddingLeft: '', paddingRight: '' });
|
|
down('top', 100); move(110); move(124); up();
|
|
expect(props(id).style).toMatchObject({ padding: '', paddingTop: '64px', paddingBottom: '40px', paddingLeft: '0', paddingRight: '0' });
|
|
expect(undoAll()).toBe(1);
|
|
expect(props(id).style.padding).toBe('40px 0');
|
|
});
|
|
|
|
test('Escape mid-drag restores the original and ignores further moves', () => {
|
|
const id = seed({ paddingTop: '40px', paddingBottom: '40px' });
|
|
down('top', 100); move(130, { altKey: true });
|
|
expect(props(id).style.paddingBottom).toBe('70px');
|
|
esc();
|
|
expect(props(id).style).toMatchObject({ paddingTop: '40px', paddingBottom: '40px' });
|
|
move(200); up();
|
|
expect(props(id).style.paddingTop).toBe('40px');
|
|
expect(document.querySelector('.spacing-readout')).toBeNull();
|
|
});
|
|
|
|
test('Ctrl snaps to the 8px grid', () => {
|
|
const id = seed({ paddingTop: '40px' });
|
|
down('top', 100); move(113, { ctrlKey: true }); up();
|
|
expect(props(id).style.paddingTop).toBe('56px');
|
|
});
|
|
});
|