'use strict'; const assert = require('node:assert/strict'); const fs = require('node:fs'); const { applyThumbnailTargets, orderCurrentTargetLast } = require('../js/spine-group-thumbnails'); (async () => { const targets = ['json', 'skel', 'atlas'].map((id) => ({ sourcePath: '/library/' + id })); assert.deepEqual( orderCurrentTargetLast(targets, '/library/json').map((target) => target.sourcePath), ['/library/skel', '/library/atlas', '/library/json'], 'the preview-owning item must be processed last because Eagle tears down its viewer after updating it' ); const applied = new Set(); const inputPaths = []; const result = await applyThumbnailTargets({ targets, initialSkipped: 1, settleDelayMs: 0, cleanupDelayMs: 0, rendered: { buffer: Buffer.from('thumbnail'), extension: '.webp' }, getItem: async (sourcePath) => ({ id: sourcePath.split('/').pop(), data: { customThumbnail: false } }), setThumbnail: async (id, thumbnailPath) => { assert(fs.existsSync(thumbnailPath)); assert.equal(fs.readFileSync(thumbnailPath, 'utf8'), 'thumbnail'); inputPaths.push(thumbnailPath); // Simulate Eagle consuming the supplied file. Reusing one file would make // every call after the first fail, which was the original regression. fs.unlinkSync(thumbnailPath); applied.add(id); return true; }, waitForApplied: async (sourcePath) => applied.has(sourcePath.split('/').pop()), }); assert.deepEqual(result, { success: 3, skipped: 1, failed: 0 }); assert.equal(new Set(inputPaths).size, 3); const overwritten = await applyThumbnailTargets({ targets: [{ sourcePath: '/library/custom', allowOverwrite: true }], settleDelayMs: 0, cleanupDelayMs: 0, rendered: { buffer: Buffer.from('replacement'), extension: '.webp' }, getItem: async () => ({ id: 'custom', data: { customThumbnail: true } }), setThumbnail: async (_id, thumbnailPath) => { assert(fs.existsSync(thumbnailPath)); return true; }, waitForApplied: async () => true, }); assert.deepEqual(overwritten, { success: 1, skipped: 0, failed: 0 }); const deferredReads = []; const deferred = await applyThumbnailTargets({ targets, settleDelayMs: 15, cleanupDelayMs: 25, rendered: { buffer: Buffer.from('deferred-thumbnail'), extension: '.webp' }, getItem: async (sourcePath) => ({ id: sourcePath.split('/').pop(), data: { customThumbnail: false } }), setThumbnail: async (id, thumbnailPath) => { setTimeout(() => { deferredReads.push({ id, content: fs.readFileSync(thumbnailPath, 'utf8') }); }, 10); return true; }, // Simulate the Web API exposing customThumbnail before its background file // copy has completed. waitForApplied: async () => true, }); assert.deepEqual(deferred, { success: 3, skipped: 0, failed: 0 }); assert.deepEqual(deferredReads.map((entry) => entry.id), ['json', 'skel', 'atlas']); assert(deferredReads.every((entry) => entry.content === 'deferred-thumbnail')); let rejectedCalls = 0; const retried = await applyThumbnailTargets({ targets: [{ sourcePath: '/library/retry' }], settleDelayMs: 0, cleanupDelayMs: 0, rendered: { buffer: Buffer.from('retry-thumbnail'), extension: '.png' }, getItem: async () => ({ id: 'retry', data: { customThumbnail: false } }), setThumbnail: async () => ++rejectedCalls === 1 ? false : true, waitForApplied: async () => true, }); assert.equal(rejectedCalls, 2); assert.deepEqual(retried, { success: 1, skipped: 0, failed: 0 }); console.log('PASS: first batch applies every selected item using an independent temporary file'); })().catch((error) => { console.error(error); process.exitCode = 1; });