diff --git a/test/jem-transfer.test.js b/test/jem-transfer.test.js new file mode 100644 index 0000000..4c5aa13 --- /dev/null +++ b/test/jem-transfer.test.js @@ -0,0 +1,99 @@ +const test = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const path = require('node:path'); +const vm = require('node:vm'); + +function harness() { + class Group { + constructor(name, parent) { Object.assign(this, { name, origin: [1, 2, 3], rotation: [0, 0, 0], children: [], parent }); parent?.children.push(this); } + } + const h = { Group, Cube: class {}, Plugin: { register() {} } }; + const source = fs.readFileSync(path.join(__dirname, '..', 'AnimationDirector.js'), 'utf8').replace(' Plugin.register(PLUGIN_ID, {', + ' globalThis.api = { cemTemplateCatalog, jemPresetParts, jemTransferSuggestions, validateJemChoices, jemAdditionBones, addJemEnergyVisibility, refreshJemEnergyFields };\n Plugin.register(PLUGIN_ID, {'); + vm.runInNewContext(source, h); + return h; +} + +test('JEM preset parent paths include attachment roots and nested groups', () => { + const h = harness(); + const parts = h.api.jemPresetParts({ models: [{ part: 'body', submodels: [{ id: 'fur', submodels: [{ id: 'tip' }] }] }, { part: 'head' }] }); + assert.deepEqual(Array.from(parts, (part) => [part.name, part.path]), [['body', '0'], ['fur', '0/0'], ['tip', '0/0/0'], ['head', '1']]); +}); + +test('suggests added subtrees once, strips actor prefixes and leaves vanilla hierarchy unchanged', () => { + const h = harness(), root = new h.Group('actor1_root'), body = new h.Group('actor1_body', root); + const d = new h.Group('actor1_d', body), tip = new h.Group('actor1_tip', d); + const head = new h.Group('actor1_head', root), extra = new h.Group('actor1_extra', root); + const suggestions = h.api.jemTransferSuggestions([root, body, d, tip, head, extra], [{ name: 'body', path: '0' }, { name: 'head', path: '1' }], 1); + assert.deepEqual(Array.from(suggestions, (item) => [item.name, item.target]), [['d', '0'], ['extra', '']]); + assert.equal(suggestions[0].root, d); + assert.equal(tip.parent, d); + assert.equal(body.name, 'actor1_body'); +}); + +test('selected additions require parent mapping; unchecked additions are omitted', () => { + const h = harness(), body = new h.Group('actor1_body'), d = new h.Group('actor1_d', body); + const suggestions = h.api.jemTransferSuggestions([body, d], [{ name: 'body', path: '0' }], 1); + assert.throws(() => h.api.validateJemChoices(suggestions, { include_0: true, parent_0: '' }), /Choose a target parent/); + assert.throws(() => h.api.validateJemChoices(suggestions, { include_0: false }), /at least one/); + const chosen = h.api.validateJemChoices(suggestions, { include_0: true, parent_0: '0' }); + assert.equal(chosen[0].tree.name, 'd'); + assert.equal(chosen[0].target, '0'); + assert.deepEqual(Array.from(chosen[0].parentOrigin), [1, 2, 3]); +}); + +test('CEM catalog integration handles its initial undefined return and dependency errors', async () => { + const h = harness(); + await assert.rejects(h.api.cemTemplateCatalog(), /Install and enable/); + let calls = 0; + const catalog = { models: { wolf: {} }, entities: [{ id: 'wolf' }] }; + h.window = { async loadCEMTemplateModels() { return ++calls === 1 ? undefined : catalog; } }; + assert.equal(await h.api.cemTemplateCatalog(), catalog); + assert.equal(calls, 2); +}); + +test('energy visibility is opt-in per transferred bone including nested subbones', () => { + const h = harness(), body = new h.Group('actor1_body'), d = new h.Group('actor1_d', body); + new h.Group('actor1_tip', d); + const suggestions = h.api.jemTransferSuggestions([body, d, ...d.children], [{ name: 'body', path: '0' }], 1); + assert.deepEqual(Array.from(h.api.jemAdditionBones(suggestions[0]), (bone) => [bone.name, bone.path]), [['d', 'root'], ['tip', 'root_0']]); + const selected = h.api.validateJemChoices(suggestions, { include_0: true, parent_0: '0', energy_0_root_0: true }); + assert.equal(selected[0].tree.energyVisibility, false); + assert.equal(selected[0].tree.children[0].group.energyVisibility, true); +}); + +test('energy expressions use the final unique bone name and preserve existing preset animations', () => { + const h = harness(), root = new h.Group('body'), nested = new h.Group('addition', root); + const tip = new h.Group('d3_2', nested); + root.cem_animations = [{ 'body.rx': 'existing_expression' }]; + h.api.addJemEnergyVisibility(tip); + assert.equal(root.cem_animations.length, 2); + assert.equal(root.cem_animations[0]['body.rx'], 'existing_expression'); + assert.equal(root.cem_animations[1]['d3_2.visible'], 'nbt(NeedsOfNatureEnergy,range:70-200)'); + assert.equal(tip.cem_animations, undefined); +}); + +test('parent energy visibility disables descendants and omits redundant child expressions without clearing their choices', () => { + const h = harness(), body = new h.Group('actor1_body'), d = new h.Group('actor1_d', body); + const tip = new h.Group('actor1_tip', d), end = new h.Group('actor1_end', tip); + const suggestions = h.api.jemTransferSuggestions([body, d, tip, end], [{ name: 'body', path: '0' }], 1); + const fields = Object.fromEntries(['root', 'root_0', 'root_0_0'].map((path) => { + const input = {}, bar = { style: {}, querySelectorAll() { return [input]; } }; + return [`energy_0_${path}`, { bar, input }]; + })); + const dialog = { form: { form_data: fields } }; + const result = { include_0: true, parent_0: '0', energy_0_root: true, energy_0_root_0: true, energy_0_root_0_0: true }; + h.api.refreshJemEnergyFields(dialog, suggestions, result); + assert.equal(fields.energy_0_root.input.disabled, false); + assert.equal(fields.energy_0_root_0.input.disabled, true); + assert.equal(fields.energy_0_root_0_0.input.disabled, true); + const tree = h.api.validateJemChoices(suggestions, result)[0].tree; + assert.equal(tree.energyVisibility, true); + assert.equal(tree.children[0].group.energyVisibility, false); + result.energy_0_root = false; + h.api.refreshJemEnergyFields(dialog, suggestions, result); + assert.equal(fields.energy_0_root_0.input.disabled, false); + assert.equal(fields.energy_0_root_0_0.input.disabled, true); + assert.equal(h.api.validateJemChoices(suggestions, result)[0].tree.children[0].group.energyVisibility, true); +});