'use strict'; const assert = require('node:assert/strict'); const fs = require('node:fs'); const os = require('node:os'); const path = require('node:path'); const utils = require('../js/spine-utils.js'); const settings = require('../js/spine-preview-settings.js'); // --- 内置尾缀(默认值,用户不可删除)--- assert.deepEqual(utils.getSuffixes(), utils.DEFAULT_SUFFIXES); assert.equal(utils.normalizeStem('hero-mesh.skel'), 'hero'); assert.equal(utils.normalizeStem('hero-data.json'), 'hero'); // --- 自定义尾缀生效后才参与归一化 --- assert.equal(utils.normalizeStem('hero-hover.skel'), 'hero-hover'); utils.setSuffixes(['hover']); assert.equal(utils.normalizeStem('hero-hover.skel'), 'hero'); assert.equal(utils.normalizeStem('hero_hover.atlas.txt'), 'hero'); assert.deepEqual(utils.getCustomSuffixes(), ['hover']); assert.deepEqual(utils.getSuffixes(), utils.DEFAULT_SUFFIXES.concat('hover')); // --- 长度上限:常量与正则必须一致(改上限时只动一个地方)--- assert.equal(utils.SUFFIX_MAX_LENGTH, 50); assert.equal(utils.normalizeSuffixName('a'.repeat(utils.SUFFIX_MAX_LENGTH)), 'a'.repeat(utils.SUFFIX_MAX_LENGTH)); assert.equal(utils.normalizeSuffixName('a'.repeat(utils.SUFFIX_MAX_LENGTH + 1)), null); // --- 非法、重复、内置重名的尾缀一律丢弃(磁盘内容不可信)--- assert.deepEqual( utils.setSuffixes(['hover', 'HOVER', 'mesh', '', ' ', '-', '_x', 'bad name', 'a'.repeat(utils.SUFFIX_MAX_LENGTH + 1), 'a'.repeat(utils.SUFFIX_MAX_LENGTH)]), utils.DEFAULT_SUFFIXES.concat(['hover', 'x', 'a'.repeat(utils.SUFFIX_MAX_LENGTH)]) ); assert.equal(utils.normalizeSuffixName(' Hover '), 'hover'); assert.equal(utils.normalizeSuffixName('_outline'), 'outline'); assert.equal(utils.normalizeSuffixName(''), null); assert.equal(utils.normalizeSuffixName('a.b'), null); assert.equal(utils.normalizeSuffixName('!'), null); assert.equal(utils.normalizeSuffixName(null), null); // --- 尾缀决定配套资源能否配对(strictStem 与导入缩略图共用同一条路径)--- const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'spineviewer-suffix-')); try { fs.writeFileSync(path.join(dir, 'hero-hover.json'), JSON.stringify({ skeleton: { spine: '4.2.05' }, bones: [{ name: 'root' }], slots: [], skins: [], animations: {}, })); fs.writeFileSync(path.join(dir, 'hero.atlas'), 'hero.png\nsize: 16,16\nformat: RGBA8888\nfilter: Linear,Linear\nrepeat: none\nbounds: 0,0,16,16\n'); fs.writeFileSync(path.join(dir, 'hero.png'), Buffer.from([0x89, 0x50, 0x4e, 0x47])); const source = path.join(dir, 'hero-hover.json'); const options = { searchRoots: [dir], strictStem: true }; utils.setSuffixes([]); assert.equal(utils.discoverAssets(source, options).atlasPath, null); utils.setSuffixes(['hover']); const assets = utils.discoverAssets(source, options); assert.equal(assets.skeletonPath, source); assert.equal(assets.atlasPath, path.join(dir, 'hero.atlas')); assert.equal(assets.texturePaths['hero.png'], path.join(dir, 'hero.png')); assert.equal(assets.base, 'hero'); } finally { utils.setSuffixes([]); fs.rmSync(dir, { recursive: true, force: true }); } // --- 全局尾缀存储:读取、原子写入、损坏文件不拖垮查看器 --- const userData = fs.mkdtempSync(path.join(os.tmpdir(), 'spineviewer-userdata-')); try { const store = settings.createSuffixStore(userData); assert.deepEqual(store.read(), []); store.write(['hover', 'outline']); assert.deepEqual(store.read(), ['hover', 'outline']); assert.equal(path.dirname(store.file), path.join(userData, 'SpineViewer')); assert.deepEqual(fs.readdirSync(path.dirname(store.file)).filter((name) => name.endsWith('.tmp')), []); fs.writeFileSync(store.file, '{ not json'); assert.throws(() => store.read()); store.write([]); // 损坏文件仍可被下一次写入覆盖 assert.deepEqual(store.read(), []); } finally { fs.rmSync(userData, { recursive: true, force: true }); } // --- 查看器接线:启动时先读尾缀配置,再加载资源 --- const viewer = fs.readFileSync(path.join(__dirname, '../viewer/spine.html'), 'utf8'); assert.match(viewer, /