mirror of
https://github.com/olehomelchenko/astrolabe.git
synced 2026-08-08 02:02:33 +00:00
Editor: composition wireframe — pull-out, stacking, and simplify
This commit is contained in:
@@ -199,24 +199,26 @@ describe('CompositionWireframe — drag to restructure', () => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
const drag = (fromKey: string, to: { x: number; y: number }) => {
|
||||
const drag = (fromKey: string, to: { x: number; y: number }, shift = false) => {
|
||||
act(() => {
|
||||
item(fromKey).dispatchEvent(
|
||||
new MouseEvent('pointerdown', { bubbles: true, clientX: 100, clientY: 150 }),
|
||||
);
|
||||
});
|
||||
act(() => {
|
||||
window.dispatchEvent(new MouseEvent('pointermove', { clientX: to.x, clientY: to.y }));
|
||||
window.dispatchEvent(
|
||||
new MouseEvent('pointermove', { clientX: to.x, clientY: to.y, shiftKey: shift }),
|
||||
);
|
||||
});
|
||||
act(() => {
|
||||
window.dispatchEvent(new MouseEvent('pointerup', {}));
|
||||
});
|
||||
};
|
||||
|
||||
test('dropping onto a perpendicular edge wraps the two views into a row', async () => {
|
||||
test('dropping onto a view’s far cross edge pairs the two into a row', async () => {
|
||||
setEditableSpec(COMPOSED);
|
||||
await renderOpen();
|
||||
drag('vconcat|1', { x: 190, y: 50 }); // right edge of the top view
|
||||
drag('vconcat|1', { x: 190, y: 50 }); // right edge of the top view (cross axis)
|
||||
expect(useAppStore.getState().composeRequest).toMatchObject({
|
||||
kind: 'wrap',
|
||||
targetPath: ['vconcat', 0],
|
||||
@@ -239,6 +241,123 @@ describe('CompositionWireframe — drag to restructure', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('CompositionWireframe — pull a view out via the frame margin', () => {
|
||||
const HCON = JSON.stringify({ hconcat: [{ mark: 'point' }, { mark: 'bar' }, { mark: 'line' }] });
|
||||
// A row of three boxes inset inside the root frame, leaving a margin (the pull-out
|
||||
// zone) all around. happy-dom has no layout, so the hit-test rects are stubbed.
|
||||
const RECTS: Record<string, { x: number; y: number; w: number; h: number }> = {
|
||||
root: { x: 0, y: 0, w: 300, h: 120 },
|
||||
'hconcat|0': { x: 24, y: 24, w: 80, h: 72 },
|
||||
'hconcat|1': { x: 110, y: 24, w: 80, h: 72 },
|
||||
'hconcat|2': { x: 196, y: 24, w: 80, h: 72 },
|
||||
};
|
||||
beforeEach(() => {
|
||||
vi.spyOn(HTMLElement.prototype, 'getBoundingClientRect').mockImplementation(function (
|
||||
this: HTMLElement,
|
||||
): DOMRect {
|
||||
const r = (this.dataset?.key && RECTS[this.dataset.key]) || { x: 0, y: 0, w: 0, h: 0 };
|
||||
const box = {
|
||||
left: r.x,
|
||||
top: r.y,
|
||||
right: r.x + r.w,
|
||||
bottom: r.y + r.h,
|
||||
width: r.w,
|
||||
height: r.h,
|
||||
x: r.x,
|
||||
y: r.y,
|
||||
};
|
||||
return { ...box, toJSON: () => ({}) };
|
||||
});
|
||||
});
|
||||
afterEach(() => vi.restoreAllMocks());
|
||||
|
||||
const drag = (fromKey: string, to: { x: number; y: number }, shift = false) => {
|
||||
act(() => {
|
||||
item(fromKey).dispatchEvent(
|
||||
new MouseEvent('pointerdown', { bubbles: true, clientX: 60, clientY: 60 }),
|
||||
);
|
||||
});
|
||||
act(() => {
|
||||
window.dispatchEvent(
|
||||
new MouseEvent('pointermove', { clientX: to.x, clientY: to.y, shiftKey: shift }),
|
||||
);
|
||||
});
|
||||
act(() => {
|
||||
window.dispatchEvent(new MouseEvent('pointerup', {}));
|
||||
});
|
||||
};
|
||||
|
||||
test('dropping in the cross-axis margin pulls the view into a new full-span row', async () => {
|
||||
setEditableSpec(HCON);
|
||||
await renderOpen();
|
||||
drag('hconcat|0', { x: 150, y: 8 }); // top margin of the row — across its axis
|
||||
expect(useAppStore.getState().composeRequest).toMatchObject({
|
||||
kind: 'wrap-container',
|
||||
containerPath: [],
|
||||
sourcePath: ['hconcat', 0],
|
||||
axis: 'vertical',
|
||||
side: 'before',
|
||||
});
|
||||
});
|
||||
|
||||
test('a with-axis margin reorders to that end of the row, not a pull', async () => {
|
||||
setEditableSpec(HCON);
|
||||
await renderOpen();
|
||||
drag('hconcat|1', { x: 8, y: 60 }); // left margin (along the axis), before every box
|
||||
expect(useAppStore.getState().composeRequest).toMatchObject({
|
||||
kind: 'move',
|
||||
arrayPath: ['hconcat'],
|
||||
from: 1,
|
||||
to: 0,
|
||||
});
|
||||
});
|
||||
|
||||
test('dropping over a sibling’s central band reorders past it', async () => {
|
||||
setEditableSpec(HCON);
|
||||
await renderOpen();
|
||||
drag('hconcat|2', { x: 50, y: 70 }); // central band of the first box → before it
|
||||
expect(useAppStore.getState().composeRequest).toMatchObject({
|
||||
kind: 'move',
|
||||
arrayPath: ['hconcat'],
|
||||
from: 2,
|
||||
to: 0,
|
||||
});
|
||||
});
|
||||
|
||||
test('dropping on a sibling’s top edge stacks the two into a column', async () => {
|
||||
setEditableSpec(HCON);
|
||||
await renderOpen();
|
||||
drag('hconcat|2', { x: 130, y: 30 }); // top edge of the middle box (cross axis)
|
||||
expect(useAppStore.getState().composeRequest).toMatchObject({
|
||||
kind: 'wrap',
|
||||
targetPath: ['hconcat', 1],
|
||||
sourcePath: ['hconcat', 2],
|
||||
axis: 'vertical',
|
||||
side: 'before',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('CompositionWireframe — simplify redundant wrappers', () => {
|
||||
const simplifyButton = () =>
|
||||
Array.from(document.body.querySelectorAll('button')).find((b) => b.textContent === 'Simplify');
|
||||
|
||||
test('offers Simplify for a single-child composition and dispatches it', async () => {
|
||||
setEditableSpec(JSON.stringify({ hconcat: [{ mark: 'point' }] }));
|
||||
await renderOpen();
|
||||
const btn = simplifyButton();
|
||||
expect(btn).toBeTruthy();
|
||||
act(() => btn!.click());
|
||||
expect(useAppStore.getState().composeRequest).toMatchObject({ kind: 'simplify' });
|
||||
});
|
||||
|
||||
test('no Simplify prompt when every composition has multiple views', async () => {
|
||||
setEditableSpec(COMPOSED); // a vconcat of two
|
||||
await renderOpen();
|
||||
expect(simplifyButton()).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('markIconName', () => {
|
||||
test('maps marks to glyphs, collapsing synonyms', () => {
|
||||
expect(markIconName('bar')).toBe('mark-bar');
|
||||
|
||||
Reference in New Issue
Block a user