Files
SpineViewer/tests/suffix-settings.test.js
T
2026-09-17 23:38:30 +08:00

125 lines
6.4 KiB
JavaScript

'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, /<select id="suffixSelect"><\/select>/);
assert.match(viewer, /utils\.DEFAULT_SUFFIXES/);
assert.match(viewer, /initSuffixSettings\(\)\.then\(\(\) => loadAssets\(\)\)/);
assert.match(viewer, /suffixSettings\.write\(list\)/);
assert.match(viewer, /utils\.normalizeSuffixName\(value\)/);
// 下拉必须停在独立的静止值:选中“已经是当前值的项”不会触发 change,
// 否则用户点「+ 添加尾缀…」会毫无反应。
assert.match(viewer, /const SUFFIX_NONE_VALUE = '__none__';/);
assert.match(viewer, /resting\.value = SUFFIX_NONE_VALUE;/);
assert.match(viewer, /suffixSelectEl\.value = SUFFIX_NONE_VALUE;/);
assert.doesNotMatch(viewer, /suffixSelectEl\.value = SUFFIX_ADD_VALUE;/);
// 事件回调里的异步动作必须自兜底,否则失败会静默
assert.match(viewer, /runSuffixAction\(addCustomSuffix\(\)\)/);
assert.match(viewer, /runSuffixAction\(removeCustomSuffix\(/);
// 输入框上限来自 utils 常量,不能再出现第二个硬编码的数字
assert.match(viewer, /maxlength="50"/);
assert.match(viewer, /inputEl\.maxLength = utils\.SUFFIX_MAX_LENGTH;/);
// 8 种语言的说明文字都必须提到当前上限
for (const file of fs.readdirSync(path.join(__dirname, '../_locales'))) {
const table = JSON.parse(fs.readFileSync(path.join(__dirname, '../_locales', file), 'utf8'));
assert.ok(table.viewer.suffixAddNote.includes(String(utils.SUFFIX_MAX_LENGTH)), `${file}: suffixAddNote 未提到当前上限`);
}
// 尾缀脚本引用的每个 id 都必须真实存在:缺一个就会在 openSuffixDialog 里抛错,
// 而事件回调里的异常不会冒泡到界面,只会表现为“点了没反应”。
const suffixSection = viewer.slice(viewer.indexOf('// ---------- 文件名尾缀设置'), viewer.indexOf('// ---------- 启动'));
assert.ok(suffixSection.length > 0);
for (const match of suffixSection.matchAll(/getElementById\('([^']+)'\)/g)) {
assert.ok(viewer.includes(`id="${match[1]}"`), `missing element #${match[1]}`);
}
for (const id of ['suffixSelect', 'suffixDialog', 'suffixDialogTitle', 'suffixDialogNote',
'suffixDialogInput', 'suffixDialogError', 'suffixDialogConfirm', 'suffixDialogCancel']) {
assert.ok(viewer.includes(`id="${id}"`), `missing element #${id}`);
}
console.log('PASS: built-in/custom suffixes, input validation, pairing, global store, and viewer wiring');