Files
ark-stronghold-protocol/test/ui/richText.test.js
T
sganggs 01a44cd288 卫戍协议:盟约 web remake — initial import
Online co-op browser remake of Arknights' Stronghold Protocol: Alliance
(client-simulated combat, official waves/bonds/items/strategies, 3D board,
facing wheel, operator loadout, official emotes, deployment scripts).
2026-09-29 16:21:31 +08:00

96 lines
4.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Rich-text formatter (public/js/ui/richText.js): official markup parsing, placeholders, bond effects —
// checked against every description in the generated data so no real text breaks the parser.
import { test, describe } from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { parseRichText, richTextPlain, rtClassName, formatPlaceholder, fillPlaceholders, formatBondEffect } from '../../public/js/ui/richText.js';
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
const load = (f) => JSON.parse(readFileSync(path.join(ROOT, 'data', f), 'utf8'));
describe('parseRichText', () => {
test('styled spans, terms, literal trigger labels, newlines', () => {
const segs = parseRichText('所有干员<@ba.vup>防御力</>提升\n<在场<@autochess.dgreen>3</>名>改为<$ba.stun>晕眩</>');
assert.deepEqual(segs, [
{ text: '所有干员', cls: [], term: false },
{ text: '防御力', cls: ['ba.vup'], term: false },
{ text: '提升', cls: [], term: false },
{ br: true },
{ text: '<在场', cls: [], term: false },
{ text: '3', cls: ['autochess.dgreen'], term: false },
{ text: '名>改为', cls: [], term: false },
{ text: '晕眩', cls: ['ba.stun'], term: true },
]);
});
test('nesting, escaped newlines, stray closers, unterminated tags', () => {
const segs = parseRichText('<@ba.vup>a<$ba.cold>b</>c</>d\\ne</>f<@ba.kw>g');
assert.deepEqual(segs.filter((s) => !s.br).map((s) => [s.text, s.cls.join('+'), s.term]), [
['a', 'ba.vup', false], ['b', 'ba.vup+ba.cold', true], ['c', 'ba.vup', false], ['d', '', false], ['e</>f', '', false], ['g', 'ba.kw', false],
]);
assert.equal(segs.filter((s) => s.br).length, 1);
});
test('non-strings and empty', () => {
assert.deepEqual(parseRichText(null), []);
assert.deepEqual(parseRichText(''), []);
assert.deepEqual(parseRichText(42), [{ text: '42', cls: [], term: false }]);
assert.equal(richTextPlain('<获得时>获得<@ba.vup>1</>资金'), '<获得时>获得1资金');
});
test('class names', () => {
assert.equal(rtClassName('ba.vup'), 'rt-vup');
assert.equal(rtClassName('ba.vdown'), 'rt-vdown');
assert.equal(rtClassName('whatever'), 'rt-hl');
assert.equal(rtClassName('ba.stun', true), 'rt-term');
});
test('every official description parses to the same plain text as `desc`', () => {
let checked = 0;
const files = ['bonds.json', 'bands.json', 'items.json', 'garrisons.json', 'effects.json'];
for (const f of files) {
for (const rec of Object.values(load(f))) {
if (typeof rec?.descRaw !== 'string' || typeof rec?.desc !== 'string') continue;
const plain = richTextPlain(rec.descRaw).replace(/\s+/g, '');
assert.equal(plain, rec.desc.replace(/\s+/g, ''), `${f}: ${rec.bondId || rec.bandId || rec.id || rec.garrisonId || rec.effectId}`);
checked++;
}
}
for (const c of Object.values(load('chess.json'))) {
if (!c?.skill?.descRaw) continue;
assert.equal(richTextPlain(c.skill.descRaw).replace(/\s+/g, ''), c.skill.desc.replace(/\s+/g, ''), c.chessId);
checked++;
}
assert.ok(checked > 500, `checked ${checked}`);
});
});
describe('placeholders', () => {
test('formatPlaceholder', () => {
assert.equal(formatPlaceholder(0.15, '0%'), '15%');
assert.equal(formatPlaceholder(0.1234, '0.0%'), '12.3%');
assert.equal(formatPlaceholder(3.6, '0'), '4');
assert.equal(formatPlaceholder(3.64, '0.0'), '3.6');
assert.equal(formatPlaceholder(-0.001, '0'), '0');
assert.equal(formatPlaceholder(NaN, '0'), '?');
assert.equal(formatPlaceholder(2), '2');
});
test('fillPlaceholders keeps unknown indexes', () => {
assert.equal(fillPlaceholders('攻击+{0:0%},持续{1}秒,{2:0}', [0.2, 5]), '攻击+20%,持续5秒,{2:0}');
assert.equal(fillPlaceholders('x{0}', ['文本']), 'x文本');
assert.equal(fillPlaceholders(null, []), '');
});
test('formatBondEffect resolves base + perStack × layers for every bond', () => {
const bonds = load('bonds.json');
const deput = bonds.deputShip;
const txt = formatBondEffect(deput, 10);
const expected = Math.round((deput.bb.base_def + deput.bb.def_per_stack * 10) * 100);
assert.ok(richTextPlain(txt).includes(`+${expected}%`), txt);
for (const b of Object.values(bonds)) {
const out = richTextPlain(formatBondEffect(b, 25));
assert.ok(!/\{\d+(:[^}]*)?\}/.test(out), `${b.bondId} leaves a placeholder: ${out}`);
}
assert.equal(formatBondEffect(null, 3), '');
assert.equal(formatBondEffect({ effectDesc: 'x{0:0}', effectDescParams: [{ index: 0, format: '0', base: 'a', perStack: 'b' }], bb: { a: 1, b: 2 } }, 'bad'), 'x1');
});
});