openpencil/tests/engine/editor/opacity.test.ts
Danila Poyarkov 798a3c2b2c fix(editor): harden opacity shortcuts
- Match numeric keypad input by generated key so NumLock-off navigation remains available
- Coalesce buffered opacity updates into one undo interaction
- Add end-to-end coverage for digit composition, modifiers, and undo
2026-07-18 04:22:38 +03:00

188 lines
5.3 KiB
TypeScript

import { describe, test, expect } from 'bun:test'
import { createEditor, opacityFromBuffer } from '@open-pencil/core/editor'
import { getNodeOrThrow } from '#tests/helpers/assert'
describe('editor.setOpacity', () => {
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 }
}
test('sets opacity to 50%', () => {
const { editor, rect } = setup()
editor.setOpacity(0.5)
expect(getNodeOrThrow(editor.graph, rect.id).opacity).toBe(0.5)
})
test('sets opacity to 100% (digit 0)', () => {
const { editor, rect } = setup()
editor.setOpacity(0.5)
editor.setOpacity(1)
expect(getNodeOrThrow(editor.graph, rect.id).opacity).toBe(1)
})
test('clamps opacity above 100%', () => {
const { editor, rect } = setup()
editor.setOpacity(1.5)
expect(getNodeOrThrow(editor.graph, rect.id).opacity).toBe(1)
})
test('clamps opacity below 0%', () => {
const { editor, rect } = setup()
editor.setOpacity(-0.3)
expect(getNodeOrThrow(editor.graph, rect.id).opacity).toBe(0)
})
test('opacity change is undoable as a single batch entry', () => {
const { editor, rect } = setup()
editor.setOpacity(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('coalesces buffered shortcut updates into one undo entry', () => {
const { editor, rect } = setup()
editor.setOpacity(0.2, 'shortcut-session')
editor.setOpacity(0.28, 'shortcut-session')
editor.undo.undo()
expect(getNodeOrThrow(editor.graph, rect.id).opacity).toBe(1)
editor.undo.redo()
expect(getNodeOrThrow(editor.graph, rect.id).opacity).toBe(0.28)
})
test('keeps separate shortcut sessions as separate undo entries', () => {
const { editor, rect } = setup()
editor.setOpacity(0.2, 'shortcut-session-1')
editor.setOpacity(0.8, 'shortcut-session-2')
editor.undo.undo()
expect(getNodeOrThrow(editor.graph, rect.id).opacity).toBe(0.2)
editor.undo.undo()
expect(getNodeOrThrow(editor.graph, rect.id).opacity).toBe(1)
})
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])
editor.setOpacity(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()
editor.setOpacity(1)
expect(getNodeOrThrow(editor.graph, rect.id).opacity).toBe(1)
expect(editor.undo.canUndo).toBe(false)
})
test('no-op with no selection', () => {
const { editor } = setup()
editor.clearSelection()
editor.setOpacity(0.5)
expect(editor.undo.canUndo).toBe(false)
})
test('rejects NaN', () => {
const { editor, rect } = setup()
editor.setOpacity(Number.NaN)
expect(getNodeOrThrow(editor.graph, rect.id).opacity).toBe(1)
expect(editor.undo.canUndo).toBe(false)
})
test('rejects Infinity', () => {
const { editor, rect } = setup()
editor.setOpacity(Number.POSITIVE_INFINITY)
expect(getNodeOrThrow(editor.graph, rect.id).opacity).toBe(1)
expect(editor.undo.canUndo).toBe(false)
})
test('rejects -Infinity', () => {
const { editor, rect } = setup()
editor.setOpacity(Number.NEGATIVE_INFINITY)
expect(getNodeOrThrow(editor.graph, rect.id).opacity).toBe(1)
expect(editor.undo.canUndo).toBe(false)
})
})
describe('opacityFromBuffer', () => {
test('single 0 returns 100%', () => {
expect(opacityFromBuffer('0')).toBe(1)
})
test('single digit returns decena', () => {
expect(opacityFromBuffer('5')).toBe(0.5)
expect(opacityFromBuffer('2')).toBe(0.2)
expect(opacityFromBuffer('9')).toBe(0.9)
})
test('two digits returns literal percent', () => {
expect(opacityFromBuffer('28')).toBe(0.28)
expect(opacityFromBuffer('35')).toBe(0.35)
expect(opacityFromBuffer('00')).toBe(0)
expect(opacityFromBuffer('05')).toBe(0.05)
})
test('three digits clamps to 100%', () => {
expect(opacityFromBuffer('100')).toBe(1)
expect(opacityFromBuffer('150')).toBe(1)
})
test('invalid buffer returns 100%', () => {
expect(opacityFromBuffer('')).toBe(1)
expect(opacityFromBuffer('abc')).toBe(1)
})
test('rejects partially numeric buffers', () => {
expect(opacityFromBuffer('12x')).toBe(1)
expect(opacityFromBuffer('5abc')).toBe(1)
expect(opacityFromBuffer('3.5')).toBe(1)
expect(opacityFromBuffer('1e2')).toBe(1)
expect(opacityFromBuffer('-5')).toBe(1)
expect(opacityFromBuffer(' 5')).toBe(1)
})
})