import { describe, expect, test } from 'bun:test' import { computed, ref } from 'vue' import { createEditor } from '@open-pencil/core/editor' import type { SceneNode } from '@open-pencil/scene-graph' import { MIXED, type MixedValue } from '#vue/controls/node-props/use' import { DEFAULT_STROKE, createStrokeGeometryActions, createStrokeGeometryState } from '#vue/controls/stroke/helpers' import { firstPageId, makeSceneGraph } from '#tests/helpers/scene' function strokedRect(overrides: Partial = {}) { const graph = makeSceneGraph() const node = graph.createNode('RECTANGLE', firstPageId(graph), { strokes: [{ ...DEFAULT_STROKE }], ...overrides }) return { graph, node } } function merged(nodes: SceneNode[]) { return (key: K): MixedValue => { const first = nodes[0]?.[key] if (first === undefined || nodes.some((node) => node[key] !== first)) return MIXED return first } } describe('stroke geometry controls', () => { test('is active only when every selected node has a stroke and reports mixed values', () => { const { node } = strokedRect() const other = structuredClone(node) other.id = 'other' other.strokeJoin = 'BEVEL' const nodes = ref([node, other]) const state = createStrokeGeometryState({ nodes: computed(() => nodes.value), merged: (key) => merged(nodes.value)(key) }) expect(state.advancedActive.value).toBe(true) expect(state.cap.value).toBe('NONE') expect(state.join.value).toBe(MIXED) nodes.value[1].strokes = [] expect(state.advancedActive.value).toBe(false) }) test('synchronizes node and paint cap and join with one multi-selection undo', () => { const graph = makeSceneGraph() const pageId = firstPageId(graph) const first = graph.createNode('RECTANGLE', pageId, { strokes: [{ ...DEFAULT_STROKE }] }) const second = graph.createNode('RECTANGLE', pageId, { strokes: [{ ...DEFAULT_STROKE }] }) const editor = createEditor({ graph }) const nodes = computed(() => [graph.getNode(first.id), graph.getNode(second.id)].filter(Boolean) ) const actions = createStrokeGeometryActions(editor, nodes) actions.setCap('ROUND') actions.setJoin('BEVEL') expect(graph.getNode(first.id)).toMatchObject({ strokeCap: 'ROUND', strokeJoin: 'BEVEL', strokes: [{ cap: 'ROUND', join: 'BEVEL' }] }) expect(graph.getNode(second.id)?.strokes[0]).toMatchObject({ cap: 'ROUND', join: 'BEVEL' }) editor.undo.undo() expect(graph.getNode(first.id)?.strokeJoin).toBe('MITER') expect(graph.getNode(second.id)?.strokeJoin).toBe('MITER') expect(graph.getNode(first.id)?.strokeCap).toBe('ROUND') }) test('previews and commits miter limit as one multi-selection undo step', () => { const graph = makeSceneGraph() const pageId = firstPageId(graph) const first = graph.createNode('RECTANGLE', pageId, { strokes: [{ ...DEFAULT_STROKE }] }) const second = graph.createNode('RECTANGLE', pageId, { strokes: [{ ...DEFAULT_STROKE }], strokeMiterLimit: 8 }) const editor = createEditor({ graph }) const nodes = computed(() => [graph.getNode(first.id), graph.getNode(second.id)].filter(Boolean) ) const actions = createStrokeGeometryActions(editor, nodes) actions.updateMiterLimit(12) expect(graph.getNode(first.id)?.strokeMiterLimit).toBe(12) expect(graph.getNode(second.id)?.strokeMiterLimit).toBe(12) actions.commitMiterLimit(12) editor.undo.undo() expect(graph.getNode(first.id)?.strokeMiterLimit).toBe(4) expect(graph.getNode(second.id)?.strokeMiterLimit).toBe(8) }) })