125 lines
6.4 KiB
JavaScript
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');
|