import { describe, test, expect } from 'bun:test' import { parseColor, normalizeColor, colorToHex, colorToHex8, colorToHexRaw, colorToRgba255, colorToCSS, colorToCSSCompact, rgba255ToColor, colorToFill, colorDistance } from '@open-pencil/core' describe('parseColor', () => { test('hex 6-digit', () => { const c = parseColor('#FF0000') expect(c.r).toBeCloseTo(1) expect(c.g).toBeCloseTo(0) expect(c.b).toBeCloseTo(0) expect(c.a).toBe(1) }) test('hex 3-digit', () => { const c = parseColor('#f00') expect(c.r).toBeCloseTo(1) expect(c.g).toBeCloseTo(0) expect(c.b).toBeCloseTo(0) }) test('hex 8-digit with alpha', () => { const c = parseColor('#FF000080') expect(c.r).toBeCloseTo(1) expect(c.a).toBeCloseTo(0.5, 1) }) test('rgb()', () => { const c = parseColor('rgb(255, 0, 0)') expect(c.r).toBeCloseTo(1) expect(c.g).toBeCloseTo(0) expect(c.b).toBeCloseTo(0) expect(c.a).toBe(1) }) test('rgba()', () => { const c = parseColor('rgba(255, 0, 0, 0.5)') expect(c.r).toBeCloseTo(1) expect(c.a).toBeCloseTo(0.5) }) test('named color', () => { const c = parseColor('blue') expect(c.b).toBeCloseTo(1) expect(c.r).toBeCloseTo(0) expect(c.a).toBe(1) }) test('white', () => { const c = parseColor('#FFFFFF') expect(c.r).toBeCloseTo(1) expect(c.g).toBeCloseTo(1) expect(c.b).toBeCloseTo(1) }) test('invalid returns black fallback', () => { const c = parseColor('invalid') expect(c.r).toBe(0) expect(c.g).toBe(0) expect(c.b).toBe(0) expect(c.a).toBe(1) }) test('empty string returns black fallback', () => { const c = parseColor('') expect(c.r).toBe(0) expect(c.g).toBe(0) expect(c.b).toBe(0) }) }) describe('normalizeColor', () => { test('undefined returns black', () => { const c = normalizeColor() expect(c).toEqual({ r: 0, g: 0, b: 0, a: 1 }) }) test('partial fills defaults', () => { const c = normalizeColor({ r: 0.5 }) expect(c).toEqual({ r: 0.5, g: 0, b: 0, a: 1 }) }) test('full color passes through', () => { const c = normalizeColor({ r: 0.1, g: 0.2, b: 0.3, a: 0.4 }) expect(c).toEqual({ r: 0.1, g: 0.2, b: 0.3, a: 0.4 }) }) }) describe('colorToHex', () => { test('red', () => { expect(colorToHex({ r: 1, g: 0, b: 0, a: 1 })).toBe('#FF0000') }) test('black', () => { expect(colorToHex({ r: 0, g: 0, b: 0, a: 1 })).toBe('#000000') }) test('white', () => { expect(colorToHex({ r: 1, g: 1, b: 1, a: 1 })).toBe('#FFFFFF') }) }) describe('colorToHex8', () => { test('opaque returns 6-char hex', () => { expect(colorToHex8({ r: 1, g: 0, b: 0, a: 1 })).toBe('#FF0000') }) test('semi-transparent returns 8-char hex', () => { const hex = colorToHex8({ r: 1, g: 0, b: 0, a: 0.5 }) expect(hex).toMatch(/^#[0-9A-F]{8}$/) expect(hex.slice(0, 7)).toBe('#FF0000') }) test('alpha override', () => { const hex = colorToHex8({ r: 1, g: 0, b: 0, a: 1 }, 0.5) expect(hex).toMatch(/^#FF0000[0-9A-F]{2}$/) }) test('alpha=0 returns 8-char hex', () => { expect(colorToHex8({ r: 1, g: 0, b: 0, a: 0 })).toBe('#FF000000') }) test('uppercase', () => { const hex = colorToHex8({ r: 0.8, g: 0.6, b: 0.1, a: 0.5 }) expect(hex).toBe(hex.toUpperCase()) }) }) describe('colorToHexRaw', () => { test('strips # prefix', () => { expect(colorToHexRaw({ r: 1, g: 0, b: 0, a: 1 })).toBe('FF0000') }) }) describe('colorToRgba255', () => { test('converts 0-1 to 0-255', () => { const result = colorToRgba255({ r: 1, g: 0.5, b: 0, a: 0.8 }) expect(result.r).toBe(255) expect(result.g).toBe(128) expect(result.b).toBe(0) expect(result.a).toBe(0.8) }) }) describe('colorToCSS', () => { test('opaque uses rgb()', () => { const result = colorToCSS({ r: 1, g: 0, b: 0, a: 1 }) expect(result).toBe('rgb(255, 0, 0)') }) test('transparent uses rgba()', () => { const result = colorToCSS({ r: 1, g: 0, b: 0, a: 0.5 }) expect(result).toBe('rgba(255, 0, 0, 0.5)') }) }) describe('colorToCSSCompact', () => { test('no spaces between values', () => { const result = colorToCSSCompact({ r: 1, g: 0, b: 0, a: 0.5 }) expect(result).toBe('rgba(255,0,0,0.5)') }) test('opaque omits alpha', () => { const result = colorToCSSCompact({ r: 1, g: 0, b: 0, a: 1 }) expect(result).toBe('rgb(255,0,0)') }) }) describe('rgba255ToColor', () => { test('converts 255-scale to 0-1', () => { const c = rgba255ToColor(255, 128, 0, 0.5) expect(c.r).toBeCloseTo(1) expect(c.g).toBeCloseTo(0.502, 2) expect(c.b).toBe(0) expect(c.a).toBe(0.5) }) test('alpha defaults to 1', () => { const c = rgba255ToColor(0, 0, 0) expect(c.a).toBe(1) }) }) describe('colorToFill', () => { test('from Color object', () => { const fill = colorToFill({ r: 1, g: 0, b: 0, a: 0.5 }) expect(fill.type).toBe('SOLID') expect(fill.color.r).toBe(1) expect(fill.opacity).toBe(0.5) expect(fill.visible).toBe(true) }) test('from hex string', () => { const fill = colorToFill('#ff0000') expect(fill.type).toBe('SOLID') expect(fill.color.r).toBeCloseTo(1) expect(fill.color.g).toBeCloseTo(0) }) }) describe('colorDistance', () => { test('identical colors have zero distance', () => { const c = { r: 0.5, g: 0.3, b: 0.1, a: 1 } expect(colorDistance(c, c)).toBe(0) }) test('black to white is ~441', () => { const d = colorDistance({ r: 0, g: 0, b: 0, a: 1 }, { r: 1, g: 1, b: 1, a: 1 }) expect(d).toBeCloseTo(441.67, 0) }) test('similar reds are close', () => { const d = colorDistance({ r: 1, g: 0, b: 0, a: 1 }, { r: 0.95, g: 0.02, b: 0.02, a: 1 }) expect(d).toBeLessThan(15) }) test('red to blue is far', () => { const d = colorDistance({ r: 1, g: 0, b: 0, a: 1 }, { r: 0, g: 0, b: 1, a: 1 }) expect(d).toBeGreaterThan(300) }) })