test(editor): cover auto-layout hover targets

This commit is contained in:
Danila Poyarkov 2026-05-14 18:28:04 +03:00
parent 00110c6c65
commit d2c3b1bce8
3 changed files with 134 additions and 4 deletions

View file

@ -15,7 +15,7 @@
"lint": "bun run lint:structure && oxlint -c oxlint.json --type-aware --type-check src/ packages/core/src/ packages/vue/src/ packages/cli/src/ packages/mcp/src/",
"lint:structure": "oxlint -c oxlint.json vite.config.ts vite/ src/ packages/core/src/ packages/vue/src/ packages/cli/src/ packages/mcp/src/ tests/ scripts/",
"format": "oxfmt --write .oxfmtrc.json vite.config.ts vite/ src/ packages/core/src/ packages/cli/src/ packages/mcp/src/ packages/vue/src/ tests scripts/",
"check": "bun run lint && tsgo --noEmit && bun run check:vue && bun run check:i18n",
"check": "bun run lint && tsgo --noEmit && bun run check:vue && bun run check:i18n && bun run check:arch",
"check:i18n": "bun scripts/check-locales.ts",
"check:arch": "steiger .",
"check:vue": "vue-tsc --noEmit -p tsconfig.json && vue-tsc --noEmit -p packages/vue/tsconfig.json",

View file

@ -1,5 +1,5 @@
<script setup lang="ts">
import { computed, nextTick, ref, watch } from 'vue'
import { computed, nextTick, ref, watch, type Component } from 'vue'
import {
AUTO_LAYOUT_PADDING_EDITOR_OFFSET_X,
AUTO_LAYOUT_PADDING_EDITOR_OFFSET_Y
@ -26,6 +26,10 @@ import { useEditorStore } from '@/app/editor/active-store'
import { useCanvasCollaborationAwareness } from '@/app/editor/canvas/collaboration-awareness'
import { createCanvasContextSelection } from '@/app/editor/canvas/context-selection'
import { fadeOutGlobalLoader } from '@/app/editor/canvas/loader-overlay'
import IconLucidePanelBottom from '~icons/lucide/panel-bottom'
import IconLucidePanelLeft from '~icons/lucide/panel-left'
import IconLucidePanelRight from '~icons/lucide/panel-right'
import IconLucidePanelTop from '~icons/lucide/panel-top'
import CanvasMenu from './CanvasMenu.vue'
import ScrubInput from './ScrubInput.vue'
@ -67,6 +71,13 @@ const {
useTextEdit(canvasRef, store)
const { isDraggingOver } = useCanvasDrop(canvasRef, store)
const paddingSideIcons = {
top: IconLucidePanelTop,
right: IconLucidePanelRight,
bottom: IconLucidePanelBottom,
left: IconLucidePanelLeft
} satisfies Record<'top' | 'right' | 'bottom' | 'left', Component>
const paddingEditorRef = ref<HTMLElement | null>(null)
const paddingEditorAnchor = computed(() => {
@ -83,6 +94,10 @@ const paddingEditorAnchor = computed(() => {
return { x: abs.x + node.width - node.paddingRight / 2, y: abs.y + node.height / 2 }
})
const paddingEditorReference = useCanvasVirtualReference(canvasRef, store, paddingEditorAnchor)
const paddingEditorIcon = computed(() => {
const edit = autoLayoutPaddingEdit.value
return edit ? paddingSideIcons[edit.side] : IconLucidePanelTop
})
watch(autoLayoutPaddingEdit, async (edit) => {
if (!edit) return
@ -146,11 +161,14 @@ const cursor = computed(() => toolCursor(store.state.activeTool, cursorOverride.
:model-value="autoLayoutPaddingEdit.value"
:min="0"
:step="1"
icon="↔"
data-test-id="auto-layout-padding-input"
@update:model-value="updateAutoLayoutPaddingEdit"
@commit="(value: number) => commitAutoLayoutPaddingEdit(value)"
/>
>
<template #icon>
<component :is="paddingEditorIcon" class="size-3.5" />
</template>
</ScrubInput>
</PopoverContent>
</PopoverPortal>
</PopoverRoot>

View file

@ -0,0 +1,112 @@
import { describe, expect, test } from 'bun:test'
import { resolveAutoLayoutHover } from '#vue/shared/input/auto-layout-hover'
import type { Editor } from '@open-pencil/core/editor'
import type { SceneNode } from '@open-pencil/core/scene-graph'
function frame(overrides: Partial<SceneNode> = {}): SceneNode {
return {
id: 'frame',
type: 'FRAME',
name: 'Frame',
parentId: 'page',
childIds: ['title', 'body', 'bar'],
visible: true,
locked: false,
x: 100,
y: 100,
width: 280,
height: 160,
rotation: 0,
layoutMode: 'VERTICAL',
layoutPositioning: 'AUTO',
itemSpacing: 8,
paddingTop: 20,
paddingRight: 20,
paddingBottom: 20,
paddingLeft: 20,
...overrides
} as SceneNode
}
function child(id: string, y: number, height: number): SceneNode {
return {
id,
type: id === 'bar' ? 'RECTANGLE' : 'TEXT',
name: id,
parentId: 'frame',
childIds: [],
visible: true,
locked: false,
x: 20,
y,
width: id === 'bar' ? 180 : 240,
height,
rotation: 0,
layoutMode: 'NONE',
layoutPositioning: 'AUTO'
} as SceneNode
}
function editor() {
const nodes = new Map<string, SceneNode>([
['frame', frame()],
['title', child('title', 20, 32)],
['body', child('body', 60, 48)],
['bar', child('bar', 116, 12)]
])
return {
state: {
selectedIds: new Set(['frame'])
},
graph: {
getNode: (id: string) => nodes.get(id) ?? null,
getAbsolutePosition: (id: string) => {
const node = nodes.get(id)
if (!node) return { x: 0, y: 0 }
if (node.parentId === 'frame') return { x: 100 + node.x, y: 100 + node.y }
return { x: node.x, y: node.y }
}
}
} as Editor
}
describe('auto-layout hover resolver', () => {
test('returns null outside the selected auto-layout frame', () => {
expect(resolveAutoLayoutHover(90, 90, editor())).toBeNull()
})
test('detects padding regions and padding value handles', () => {
expect(resolveAutoLayoutHover(240, 105, editor())).toMatchObject({
kind: 'padding-value',
side: 'top'
})
expect(resolveAutoLayoutHover(375, 130, editor())).toMatchObject({
kind: 'padding',
side: 'right'
})
})
test('detects shared spacing regions and value handles', () => {
expect(resolveAutoLayoutHover(240, 156, editor())).toMatchObject({
kind: 'spacing-value',
index: 0
})
expect(resolveAutoLayoutHover(130, 212, editor())).toMatchObject({
kind: 'spacing',
index: 1
})
})
test('detects child hover inside selected auto-layout frame', () => {
expect(resolveAutoLayoutHover(130, 180, editor())).toMatchObject({
kind: 'children',
index: 1
})
})
test('falls back to frame hover inside empty selected areas', () => {
expect(resolveAutoLayoutHover(240, 235, editor())).toMatchObject({ kind: 'frame' })
})
})