openpencil/tests/engine/editor/opacity.test.ts
sadkodev e91ea4b061
feat(keyboard): add Figma-style opacity shortcuts 1-9/0
Add opacity keyboard shortcuts matching Figma: pressing Digit1-Digit9
sets the selected layers to 10%-90% opacity, and Digit0 sets 100%.

- Add 'selection.setOpacity' EditorCommandId with registry metadata
  (shortcut display '1-9, 0') so it appears in the command cheatsheet
- Implement the command in selection.ts using editor.undo.runBatch +
  updateNodeWithUndo, following the toggleVisibility/setBlendMode pattern
- Add setOpacityTarget closure in useEditorCommands so the external
  keyboard handler can inject the digit value before runCommand
- Add canSetOpacity capability (enabled when there is a selection)
- Add bindOpacityKeys handler in src/app/shell/keyboard/opacity.ts,
  following the nudging.ts external-handler pattern since the opacity
  value comes from event.code and cannot be expressed as a fixed
  keybinding in the registry
- Guard against text editing, number field focus, and modifier keys
- Add i18n label 'setOpacity' across all 8 locales
- Add unit tests for single/multi selection, undo batch, and no-op
2026-07-16 22:12:50 -04:00

90 lines
2.6 KiB
TypeScript

import { describe, test, expect } from 'bun:test'
import { createEditor } from '@open-pencil/core/editor'
import { getNodeOrThrow } from '#tests/helpers/assert'
describe('opacity via updateNodeWithUndo + runBatch', () => {
function setup() {
const editor = createEditor()
const pageId = editor.graph.getPages()[0].id
const rect = editor.graph.createNode('RECTANGLE', pageId, {
name: 'Rect',
x: 0,
y: 0,
width: 50,
height: 50
})
editor.select([rect.id])
return { editor, rect }
}
function setOpacity(editor: ReturnType<typeof createEditor>, opacity: number) {
const targets = editor.getSelectedNodes()
if (targets.length === 0) return
editor.undo.runBatch('Set opacity', () => {
for (const target of targets) {
if (target.opacity === opacity) continue
editor.updateNodeWithUndo(target.id, { opacity }, 'Set opacity')
}
})
}
test('sets opacity to 50%', () => {
const { editor, rect } = setup()
setOpacity(editor, 0.5)
expect(getNodeOrThrow(editor.graph, rect.id).opacity).toBe(0.5)
})
test('0 digit sets opacity to 100%', () => {
const { editor, rect } = setup()
setOpacity(editor, 0.5)
setOpacity(editor, 1)
expect(getNodeOrThrow(editor.graph, rect.id).opacity).toBe(1)
})
test('opacity change is undoable as a single batch entry', () => {
const { editor, rect } = setup()
setOpacity(editor, 0.3)
expect(editor.undo.canUndo).toBe(true)
editor.undo.undo()
expect(getNodeOrThrow(editor.graph, rect.id).opacity).toBe(1)
editor.undo.redo()
expect(getNodeOrThrow(editor.graph, rect.id).opacity).toBe(0.3)
})
test('opacity batch for multiple selections collapses to one undo entry', () => {
const { editor, rect } = setup()
const pageId = editor.graph.getPages()[0].id
const rect2 = editor.graph.createNode('RECTANGLE', pageId, {
name: 'Rect2',
x: 100,
y: 0,
width: 50,
height: 50
})
editor.select([rect.id, rect2.id])
setOpacity(editor, 0.7)
expect(getNodeOrThrow(editor.graph, rect.id).opacity).toBe(0.7)
expect(getNodeOrThrow(editor.graph, rect2.id).opacity).toBe(0.7)
editor.undo.undo()
expect(getNodeOrThrow(editor.graph, rect.id).opacity).toBe(1)
expect(getNodeOrThrow(editor.graph, rect2.id).opacity).toBe(1)
})
test('no-op when opacity already matches', () => {
const { editor, rect } = setup()
setOpacity(editor, 1)
expect(getNodeOrThrow(editor.graph, rect.id).opacity).toBe(1)
expect(editor.undo.canUndo).toBe(false)
})
})