import { describe, test, expect } from 'bun:test' import type { Fill, Stroke, Effect, StyleRun, GeometryPath } from '@open-pencil/core' import { copyFill, copyFills, copyStroke, copyEffect, copyStyleRun, copyGeometryPaths } from '#core/scene-graph/copy' import { expectDefined } from '#tests/helpers/assert' describe('copy helpers — mutation isolation', () => { test('copyFill: mutating copy does not affect original', () => { const original: Fill = { type: 'SOLID', color: { r: 1, g: 0, b: 0, a: 1 }, opacity: 1, visible: true } const copy = copyFill(original) copy.color.r = 0 copy.opacity = 0.5 expect(original.color.r).toBe(1) expect(original.opacity).toBe(1) }) test('copyFill: gradient stops are deep copied', () => { const original: Fill = { type: 'GRADIENT_LINEAR', color: { r: 0, g: 0, b: 0, a: 1 }, opacity: 1, visible: true, gradientStops: [ { color: { r: 1, g: 0, b: 0, a: 1 }, position: 0 }, { color: { r: 0, g: 0, b: 1, a: 1 }, position: 1 } ], gradientTransform: { m00: 1, m01: 0, m02: 0, m10: 0, m11: 1, m12: 0 } } const copy = copyFill(original) expectDefined(copy.gradientStops?.[0], 'copied gradient stop').color.r = 0 expectDefined(copy.gradientTransform, 'copied gradient transform').m00 = 99 expect(expectDefined(original.gradientStops?.[0], 'original gradient stop').color.r).toBe(1) expect(expectDefined(original.gradientTransform, 'original gradient transform').m00).toBe(1) }) test('copyStroke: dash pattern is independent', () => { const original: Stroke = { color: { r: 0, g: 0, b: 0, a: 1 }, weight: 1, opacity: 1, visible: true, align: 'CENTER', dashPattern: [5, 3] } const copy = copyStroke(original) expectDefined(copy.dashPattern, 'copied dash pattern').push(99) copy.color.g = 1 expect(original.dashPattern).toEqual([5, 3]) expect(original.color.g).toBe(0) }) test('copyEffect: offset and color are independent', () => { const original: Effect = { type: 'DROP_SHADOW', color: { r: 0, g: 0, b: 0, a: 0.5 }, offset: { x: 4, y: 4 }, radius: 8, spread: 0, visible: true } const copy = copyEffect(original) copy.offset.x = 100 copy.color.a = 1 expect(original.offset.x).toBe(4) expect(original.color.a).toBe(0.5) }) test('copyStyleRun: style object is independent', () => { const original: StyleRun = { start: 0, length: 5, style: { fontWeight: 700, fontSize: 24 } } const copy = copyStyleRun(original) copy.style.fontWeight = 400 expect(original.style.fontWeight).toBe(700) }) test('copyGeometryPaths: Uint8Array is independent', () => { const original: GeometryPath[] = [ { windingRule: 'NONZERO', commandsBlob: new Uint8Array([1, 2, 3]) } ] const copy = copyGeometryPaths(original) copy[0].commandsBlob[0] = 99 expect(original[0].commandsBlob[0]).toBe(1) }) test('copyFills: array independence', () => { const originals: Fill[] = [ { type: 'SOLID', color: { r: 1, g: 0, b: 0, a: 1 }, opacity: 1, visible: true }, { type: 'SOLID', color: { r: 0, g: 1, b: 0, a: 1 }, opacity: 0.5, visible: true } ] const copies = copyFills(originals) copies.push({ type: 'SOLID', color: { r: 0, g: 0, b: 1, a: 1 }, opacity: 1, visible: true }) copies[0].color.r = 0 expect(originals).toHaveLength(2) expect(originals[0].color.r).toBe(1) }) })