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() { const h = { Plugin: { register() {} }, Animation: { selected: null, all: [] }, Format: { id: 'geckolib_model' }, Animator: { preview() { h.rendered = h.Animation.selected; } }, Timeline: { playing: true, custom_range: [0, 0], loop() { this.playing = false; }, setTime(time) { this.time = time; }, start() { this.playing = true; this.loop(); } } }; const source = fs.readFileSync(path.join(__dirname, '..', 'AnimationDirector.js'), 'utf8').replace(' Plugin.register(PLUGIN_ID, {', ' globalThis.api = { installStageAutoplay, removeStageAutoplay, nextAutoplayStage, setEnabled(value) { autoplayStagesAction = { value }; } };\n Plugin.register(PLUGIN_ID, {'); vm.runInNewContext(source, h); h.clip = (name, loop = 'once') => ({ name, length: 2, loop, select() { h.Animation.selected = this; } }); h.api.setEnabled(true); h.api.installStageAutoplay(); return h; } test('autoplay advances completed non-looping stages by number within the same ID', () => { const h = harness(), first = h.clip('example.p1'), next = h.clip('example.p2'); h.Animation.selected = first; h.Animation.all = [h.clip('other.p2'), h.clip('example.p10'), next, first]; h.Timeline.loop(); assert.equal(h.Animation.selected, next); assert.equal(h.Timeline.time, 0); assert.equal(h.Timeline.playing, true); assert.equal(h.rendered, next); }); test('autoplay leaves looping stages, custom ranges, disabled toggle and final stage alone', () => { for (const mode of ['loop', 'range', 'disabled', 'final']) { const h = harness(), first = h.clip('example.p1', mode === 'loop' ? 'loop' : 'hold'); h.Animation.selected = first; h.Animation.all = mode === 'final' ? [first] : [first, h.clip('example.p2')]; if (mode === 'range') h.Timeline.custom_range = [0, 1]; if (mode === 'disabled') h.api.setEnabled(false); h.Timeline.loop(); assert.equal(h.pending, undefined); assert.equal(h.Animation.selected, first); } }); test('manual playback stops do not advance and unload restores playback', () => { const h = harness(), first = h.clip('example.p1'), next = h.clip('example.p2'); h.Animation.selected = first; h.Animation.all = [first, next]; h.Timeline.playing = false; h.Timeline.loop(); assert.equal(h.pending, undefined); assert.equal(h.Animation.selected, first); assert.equal(h.Timeline.playing, false); const wrapper = h.Timeline.loop; h.api.removeStageAutoplay(); assert.notEqual(h.Timeline.loop, wrapper); }); test('autoplay overrides global looping for once stages and restores it even on failure', () => { const h = harness(); h.api.removeStageAutoplay(); h.BarItems = { looped_animation_playback: { value: true } }; h.Timeline.loop = function() { if (!h.BarItems.looped_animation_playback.value) this.playing = false; }; const first = h.clip('example.p1'), next = h.clip('example.p2'); h.Animation.selected = first; h.Animation.all = [first, next]; h.api.installStageAutoplay(); h.Timeline.loop(); assert.equal(h.Animation.selected, next); assert.equal(h.BarItems.looped_animation_playback.value, true); h.api.removeStageAutoplay(); h.Timeline.loop = () => { throw new Error('preview failed'); }; h.api.installStageAutoplay(); assert.throws(() => h.Timeline.loop(), /preview failed/); assert.equal(h.BarItems.looped_animation_playback.value, true); });