236 lines
9.0 KiB
JavaScript
236 lines
9.0 KiB
JavaScript
import assert from 'node:assert/strict'
|
|
import test from 'node:test'
|
|
|
|
import {
|
|
OUTPUT_MODE_PERSISTENT,
|
|
OUTPUT_MODE_QUEUE_GROUP,
|
|
applyReferenceAssignments,
|
|
calculateReferenceShelfLayout,
|
|
classifyReferenceDrag,
|
|
connectedReferenceOutputSlots,
|
|
effectiveQueueGroupSize,
|
|
loadPresetSnapshot,
|
|
moveReferenceSlot,
|
|
normalizeOutputMode,
|
|
normalizeReferenceSlots,
|
|
referencePresetDisplay,
|
|
referenceShelfHit,
|
|
referenceSlotsEqual,
|
|
relinkReferenceSlots,
|
|
snapshotReferenceOutputConnections
|
|
} from '../web/image_conveyor_math.mjs'
|
|
|
|
const reference = (name) => ({
|
|
annotated: `refs/${name}.png [input]`,
|
|
filename: `${name}.png`,
|
|
subfolder: 'refs',
|
|
type: 'input'
|
|
})
|
|
|
|
test('legacy output-mode migration preserves grouped workflows deterministically', () => {
|
|
assert.equal(normalizeOutputMode(undefined, 1, false), OUTPUT_MODE_PERSISTENT)
|
|
assert.equal(normalizeOutputMode(undefined, 2, false), OUTPUT_MODE_QUEUE_GROUP)
|
|
assert.equal(normalizeOutputMode('bad', 9, true), OUTPUT_MODE_PERSISTENT)
|
|
assert.equal(normalizeOutputMode(OUTPUT_MODE_QUEUE_GROUP, 1, true), OUTPUT_MODE_QUEUE_GROUP)
|
|
})
|
|
|
|
test('persistent mode always has an effective queue group size of one', () => {
|
|
assert.equal(effectiveQueueGroupSize(OUTPUT_MODE_PERSISTENT, 9), 1)
|
|
assert.equal(effectiveQueueGroupSize(OUTPUT_MODE_QUEUE_GROUP, 9), 9)
|
|
})
|
|
|
|
test('connected reference outputs snapshot exact sparse socket topology', () => {
|
|
const outputs = Array.from({ length: 14 }, () => ({ links: null }))
|
|
outputs[5].links = [100]
|
|
outputs[6].links = [101, 102]
|
|
outputs[8].links = []
|
|
outputs[10].links = [103]
|
|
outputs[13].links = [104]
|
|
assert.deepEqual(connectedReferenceOutputSlots(outputs), [1, 5, 8])
|
|
assert.deepEqual(connectedReferenceOutputSlots(null), [])
|
|
})
|
|
|
|
test('persistent reservations carry an explicit connection snapshot including none', () => {
|
|
const outputs = Array.from({ length: 14 }, () => ({ links: null }))
|
|
const reservation = { id: 'A', annotated: 'A.png [input]' }
|
|
assert.deepEqual(
|
|
snapshotReferenceOutputConnections(reservation, OUTPUT_MODE_PERSISTENT, outputs),
|
|
{ ...reservation, reference_output_slots: [] }
|
|
)
|
|
outputs[6].links = [1]
|
|
outputs[13].links = [2]
|
|
assert.deepEqual(
|
|
snapshotReferenceOutputConnections(reservation, OUTPUT_MODE_PERSISTENT, outputs),
|
|
{ ...reservation, reference_output_slots: [1, 8] }
|
|
)
|
|
assert.equal(snapshotReferenceOutputConnections(null, OUTPUT_MODE_PERSISTENT, outputs), null)
|
|
assert.equal(
|
|
snapshotReferenceOutputConnections(reservation, OUTPUT_MODE_QUEUE_GROUP, outputs),
|
|
reservation
|
|
)
|
|
})
|
|
|
|
test('reference slots normalize to exactly eight sparse input records', () => {
|
|
const slots = normalizeReferenceSlots([reference('a'), 'bad', null, { annotated: 'x.png [output]' }])
|
|
assert.equal(slots.length, 8)
|
|
assert.deepEqual(slots[0], reference('a'))
|
|
assert.deepEqual(slots.slice(1), Array(7).fill(null))
|
|
})
|
|
|
|
test('reference slot normalization rejects path escapes and unsupported storage', () => {
|
|
const slots = normalizeReferenceSlots([
|
|
{ annotated: '/tmp/a.png [input]' },
|
|
{ annotated: '../a.png [input]' },
|
|
{ annotated: 'C:\\tmp\\a.png [input]' },
|
|
{ annotated: 'a.svg [input]' },
|
|
{ annotated: 'a.png [output]' }
|
|
])
|
|
assert.deepEqual(slots, Array(8).fill(null))
|
|
})
|
|
|
|
test('reference slot normalization rejects dotless extension lookalikes', () => {
|
|
assert.deepEqual(
|
|
normalizeReferenceSlots([{ annotated: 'refs/png [input]' }]),
|
|
Array(8).fill(null)
|
|
)
|
|
})
|
|
|
|
test('reference-only assignment does not append, remove, or reorder Conveyor items', () => {
|
|
const items = [{ id: 'A' }, { id: 'B' }]
|
|
const state = { items, reference_slots: Array(8).fill(null) }
|
|
const next = applyReferenceAssignments(state, 2, [reference('a'), reference('b')])
|
|
assert.equal(next.items, items)
|
|
assert.deepEqual(next.items.map((item) => item.id), ['A', 'B'])
|
|
assert.deepEqual(next.reference_slots[2], reference('a'))
|
|
assert.deepEqual(next.reference_slots[3], reference('b'))
|
|
})
|
|
|
|
test('sparse assignment clamps to the fixed slot range', () => {
|
|
const next = applyReferenceAssignments(
|
|
{ items: [], reference_slots: [reference('existing')] },
|
|
7,
|
|
[reference('last'), reference('overflow')]
|
|
)
|
|
assert.equal(next.reference_slots.length, 8)
|
|
assert.deepEqual(next.reference_slots[0], reference('existing'))
|
|
assert.deepEqual(next.reference_slots[7], reference('last'))
|
|
})
|
|
|
|
test('reference shelf drag-sort moves one populated slot and preserves sparse order', () => {
|
|
const slots = [reference('a'), reference('b'), null, reference('d')]
|
|
assert.deepEqual(
|
|
moveReferenceSlot(slots, 0, 2).slice(0, 4),
|
|
[reference('b'), null, reference('a'), reference('d')]
|
|
)
|
|
assert.deepEqual(
|
|
moveReferenceSlot(slots, 3, 0).slice(0, 4),
|
|
[reference('d'), reference('a'), reference('b'), null]
|
|
)
|
|
assert.deepEqual(moveReferenceSlot(slots, 2, 0), normalizeReferenceSlots(slots))
|
|
})
|
|
|
|
test('preset snapshots are detached normalized copies and dirty comparison is exact', () => {
|
|
const preset = { id: 'preset-id', slots: [reference('a')] }
|
|
const loaded = loadPresetSnapshot(preset)
|
|
assert.equal(loaded.activePresetId, 'preset-id')
|
|
assert.equal(referenceSlotsEqual(loaded.slots, preset.slots), true)
|
|
loaded.slots[0].annotated = 'refs/changed.png [input]'
|
|
assert.equal(referenceSlotsEqual(loaded.slots, preset.slots), false)
|
|
assert.equal(preset.slots[0].annotated, 'refs/a.png [input]')
|
|
})
|
|
|
|
test('saved preset labels distinguish startup hydration from unsaved references', () => {
|
|
const slots = normalizeReferenceSlots([reference('a')])
|
|
assert.deepEqual(referencePresetDisplay([], false, 'preset-id', slots), {
|
|
active: null,
|
|
dirty: false,
|
|
label: 'Loading character…'
|
|
})
|
|
assert.equal(referencePresetDisplay([], false, '', slots).label, 'Unsaved references')
|
|
|
|
const preset = { id: 'preset-id', name: 'Mara', slots }
|
|
assert.deepEqual(referencePresetDisplay([preset], true, 'preset-id', slots), {
|
|
active: preset,
|
|
dirty: false,
|
|
label: 'Mara'
|
|
})
|
|
assert.equal(
|
|
referencePresetDisplay([preset], true, 'preset-id', [reference('changed')]).label,
|
|
'Mara *'
|
|
)
|
|
})
|
|
|
|
test('duplicate cleanup relinks sparse live reference slots without touching others', () => {
|
|
const result = relinkReferenceSlots(
|
|
[reference('legacy'), null, reference('keep')],
|
|
[{ relative_path: 'refs/legacy.png', keep_path: 'canonical/legacy.png' }]
|
|
)
|
|
assert.equal(result.changed, 1)
|
|
assert.deepEqual(result.slots[0], {
|
|
annotated: 'canonical/legacy.png [input]',
|
|
filename: 'legacy.png',
|
|
subfolder: 'canonical',
|
|
type: 'input'
|
|
})
|
|
assert.equal(result.slots[1], null)
|
|
assert.deepEqual(result.slots[2], reference('keep'))
|
|
})
|
|
|
|
test('drag classification separates reorder-capable Conveyor images from imports', () => {
|
|
assert.deepEqual(
|
|
classifyReferenceDrag({ id: 'A' }, 'conveyor', true),
|
|
{ kind: 'conveyor', requiresImport: false, canReorder: true }
|
|
)
|
|
assert.deepEqual(
|
|
classifyReferenceDrag({ id: 'A' }, 'conveyor', false),
|
|
{ kind: 'conveyor', requiresImport: false, canReorder: false }
|
|
)
|
|
assert.deepEqual(
|
|
classifyReferenceDrag({ relative_path: 'a.png' }, 'input', true),
|
|
{ kind: 'input', requiresImport: false, canReorder: false }
|
|
)
|
|
assert.deepEqual(
|
|
classifyReferenceDrag({ localFile: {} }, 'folder:id:', true),
|
|
{ kind: 'local', requiresImport: true, canReorder: false }
|
|
)
|
|
assert.equal(classifyReferenceDrag({ kind: 'folder' }, 'input'), null)
|
|
})
|
|
|
|
test('4x2 shelf layout and hit testing stay within measured pre-widget geometry', () => {
|
|
const layout = calculateReferenceShelfLayout(520, 290, 112, 30)
|
|
assert.equal(layout.usable, true)
|
|
assert.equal(layout.slots.length, 8)
|
|
assert.equal(layout.slots[0].y, layout.slots[3].y)
|
|
assert.ok(layout.slots[4].y > layout.slots[0].y)
|
|
assert.ok(layout.slots.every((slot) => slot.x >= layout.left && slot.x + slot.width <= layout.right))
|
|
const first = layout.slots[0]
|
|
assert.deepEqual(referenceShelfHit(layout, first.x + 4, first.y + 4), { type: 'slot', index: 0 })
|
|
assert.deepEqual(
|
|
referenceShelfHit(layout, first.x + first.width - 2, first.y + 2),
|
|
{ type: 'clear', index: 0 }
|
|
)
|
|
assert.equal(referenceShelfHit(layout, layout.right + 1, layout.bottom + 1), null)
|
|
})
|
|
|
|
test('shelf header hit regions map to save, menu, and preset actions', () => {
|
|
const layout = calculateReferenceShelfLayout(520, 290, 112, 30)
|
|
const menuWidth = Math.min(54, layout.width * 0.18)
|
|
const saveWidth = Math.min(52, layout.width * 0.18)
|
|
const headerY = layout.top + layout.headerHeight / 2
|
|
assert.deepEqual(referenceShelfHit(layout, layout.right - 1, headerY), { type: 'menu' })
|
|
assert.deepEqual(
|
|
referenceShelfHit(layout, layout.right - menuWidth - 1, headerY),
|
|
{ type: 'save' }
|
|
)
|
|
assert.deepEqual(
|
|
referenceShelfHit(layout, layout.right - menuWidth - saveWidth - 1, headerY),
|
|
{ type: 'preset' }
|
|
)
|
|
})
|
|
|
|
test('a narrow node produces an unusable shelf layout with no hits', () => {
|
|
const layout = calculateReferenceShelfLayout(200, 120, 112, 30)
|
|
assert.equal(layout.usable, false)
|
|
assert.equal(referenceShelfHit(layout, layout.left + 1, layout.top + 1), null)
|
|
})
|