- js/modes.js: per-backend capability matrix (GaCy GO / FireflyGo / RobinSR) - js/firefly.js: POST /sync payload builder + extra_data normaliser - js/enka-import.js: enka.network -> FreeSR converter with merge/replace - app.js: mode-aware UI, firefly transport, Enka import, extra_data form - index.html: enable FireflyGo, add Enka import + FireflyGo controls cards - i18n: Thai/English strings for every new label, message and error - vercel.json: /api/enka/:uid rewrite; scripts/dev-server.cjs for local dev
183 lines
6.7 KiB
JavaScript
183 lines
6.7 KiB
JavaScript
const test = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const path = require('node:path');
|
|
const { pathToFileURL } = require('node:url');
|
|
|
|
const moduleUrl = pathToFileURL(path.join(__dirname, '../js/enka-import.js'));
|
|
|
|
async function load() {
|
|
return import(moduleUrl.href);
|
|
}
|
|
|
|
function enkaPayload(overrides = {}) {
|
|
return {
|
|
detailInfo: {
|
|
avatarDetailList: [
|
|
{
|
|
avatarId: 1506,
|
|
level: 80,
|
|
promotion: 6,
|
|
rank: 2,
|
|
equipment: { tid: 23001, level: 80, promotion: 6, rank: 1 },
|
|
relicList: [
|
|
{
|
|
tid: 61011,
|
|
level: 15,
|
|
mainAffixId: 1,
|
|
subAffixList: [
|
|
{ affixId: 2, cnt: 3, step: 0 },
|
|
{ affixId: 0, cnt: 0, step: 0 }
|
|
]
|
|
}
|
|
],
|
|
skillTreeList: [
|
|
{ pointId: 1506001, level: 6 },
|
|
{ pointId: 1506002, level: 10 },
|
|
{ pointId: 1506201, level: 1 },
|
|
{ pointId: 1506202, level: 1 }
|
|
],
|
|
...overrides
|
|
}
|
|
]
|
|
}
|
|
};
|
|
}
|
|
|
|
test('normalizeEnkaUid only accepts 8-10 digit UIDs', async () => {
|
|
const { normalizeEnkaUid } = await load();
|
|
assert.equal(normalizeEnkaUid('800000000'), '800000000');
|
|
assert.equal(normalizeEnkaUid(' 12345678 '), '12345678');
|
|
assert.equal(normalizeEnkaUid('1234567'), '');
|
|
assert.equal(normalizeEnkaUid('12345678901'), '');
|
|
assert.equal(normalizeEnkaUid('80000abc'), '');
|
|
assert.equal(normalizeEnkaUid(null), '');
|
|
});
|
|
|
|
test('trace anchors and relic ids follow the Firefly/SR formulas', async () => {
|
|
const { anchorTypeForPointId, relicSetIdFromTid, relicSlotFromTid } = await load();
|
|
assert.equal(anchorTypeForPointId(1506001), '1');
|
|
assert.equal(anchorTypeForPointId(1506004), '4');
|
|
assert.equal(anchorTypeForPointId(1506007), '5');
|
|
assert.equal(anchorTypeForPointId(1506101), '6');
|
|
assert.equal(anchorTypeForPointId(1506103), '8');
|
|
assert.equal(anchorTypeForPointId(1506201), '9');
|
|
assert.equal(anchorTypeForPointId(1506999), '');
|
|
|
|
assert.equal(relicSetIdFromTid(61011), 101);
|
|
assert.equal(relicSlotFromTid(61011), 1);
|
|
assert.equal(relicSetIdFromTid('31143'), 114);
|
|
assert.equal(relicSlotFromTid('31143'), 3);
|
|
});
|
|
|
|
test('enkaToFreeSR converts avatars, light cones and relics', async () => {
|
|
const { enkaToFreeSR, countFreeSRContent } = await load();
|
|
const freesr = enkaToFreeSR(enkaPayload(), { ownerUid: 800000000 });
|
|
|
|
const avatar = freesr.avatars['1506'];
|
|
assert.equal(avatar.avatar_id, 1506);
|
|
assert.equal(avatar.owner_uid, 800000000);
|
|
assert.equal(avatar.level, 80);
|
|
assert.equal(avatar.promotion, 6);
|
|
assert.equal(avatar.data.rank, 2);
|
|
assert.equal(avatar.data.skills['1506002'], 10);
|
|
// 201/202 are stat traces, so they collapse into anchor 9.
|
|
assert.deepEqual(avatar.data.skills_by_anchor_type, { 1: 6, 2: 10, 9: 1 });
|
|
|
|
assert.equal(freesr.lightcones.length, 1);
|
|
assert.deepEqual(freesr.lightcones[0], {
|
|
level: 80,
|
|
item_id: 23001,
|
|
equip_avatar: 1506,
|
|
rank: 1,
|
|
promotion: 6,
|
|
internal_uid: 0
|
|
});
|
|
|
|
assert.equal(freesr.relics.length, 1);
|
|
assert.equal(freesr.relics[0].relic_set_id, 101);
|
|
assert.equal(freesr.relics[0].main_affix_id, 1);
|
|
assert.equal(freesr.relics[0].equip_avatar, 1506);
|
|
assert.deepEqual(freesr.relics[0].sub_affixes, [{ sub_affix_id: 2, count: 3, step: 0 }]);
|
|
|
|
assert.deepEqual(countFreeSRContent(freesr), { avatars: 1, lightcones: 1, relics: 1 });
|
|
assert.equal(freesr.battle_config.battle_type, 'DEFAULT');
|
|
assert.deepEqual(freesr.battle_config.monsters, []);
|
|
});
|
|
|
|
test('unwrapEnkaPayload accepts raw, enveloped and Firefly responses', async () => {
|
|
const { unwrapEnkaPayload } = await load();
|
|
const raw = enkaPayload();
|
|
assert.equal(unwrapEnkaPayload(raw), raw);
|
|
assert.deepEqual(unwrapEnkaPayload({ ok: true, data: raw }), raw);
|
|
assert.deepEqual(unwrapEnkaPayload({ data: raw }), raw);
|
|
assert.equal(unwrapEnkaPayload({ detailInfo: null }), null);
|
|
assert.equal(unwrapEnkaPayload(null), null);
|
|
assert.equal(unwrapEnkaPayload({ message: 'nope' }), null);
|
|
});
|
|
|
|
test('enkaToFreeSR rejects payloads without visible characters', async () => {
|
|
const { enkaToFreeSR } = await load();
|
|
assert.throws(() => enkaToFreeSR({ detailInfo: { avatarDetailList: [] } }), /ENKA_NO_AVATARS/);
|
|
assert.throws(() => enkaToFreeSR({ detailInfo: {} }), /ENKA_NO_AVATARS/);
|
|
});
|
|
|
|
test('mergeFreeSR replaces gear of re-imported characters only', async () => {
|
|
const { mergeFreeSR } = await load();
|
|
const base = {
|
|
avatars: {
|
|
1506: { avatar_id: 1506, level: 1, data: { rank: 0 } },
|
|
1001: { avatar_id: 1001, level: 80, data: { rank: 6 } }
|
|
},
|
|
lightcones: [
|
|
{ item_id: 23001, equip_avatar: 1506, internal_uid: 0 },
|
|
{ item_id: 23002, equip_avatar: 1001, internal_uid: 1 }
|
|
],
|
|
relics: [{ relic_id: 61011, equip_avatar: 1506, internal_uid: 0 }],
|
|
battle_config: { battle_type: 'MOC', stage_id: 12, monsters: [[1, 2]] }
|
|
};
|
|
const incoming = {
|
|
avatars: { 1506: { avatar_id: 1506, level: 80, data: { rank: 2 } } },
|
|
lightcones: [{ item_id: 23009, equip_avatar: 1506, internal_uid: 0 }],
|
|
relics: []
|
|
};
|
|
|
|
const merged = mergeFreeSR(base, incoming, 'merge');
|
|
assert.equal(merged.avatars[1506].level, 80);
|
|
assert.equal(merged.avatars[1001].level, 80);
|
|
assert.deepEqual(merged.lightcones.map(lc => lc.item_id), [23002, 23009]);
|
|
assert.equal(merged.relics.length, 0);
|
|
// The active challenge must survive an import.
|
|
assert.equal(merged.battle_config.stage_id, 12);
|
|
// The source document is not mutated.
|
|
assert.equal(base.avatars[1506].level, 1);
|
|
});
|
|
|
|
test('mergeFreeSR replace mode clears the roster but keeps battle config', async () => {
|
|
const { mergeFreeSR } = await load();
|
|
const base = {
|
|
avatars: { 1001: { avatar_id: 1001 } },
|
|
lightcones: [{ item_id: 23002, equip_avatar: 1001 }],
|
|
relics: [{ relic_id: 61011, equip_avatar: 1001 }],
|
|
battle_config: { battle_type: 'MOC', stage_id: 12 }
|
|
};
|
|
const incoming = { avatars: { 1506: { avatar_id: 1506 } }, lightcones: [], relics: [] };
|
|
const replaced = mergeFreeSR(base, incoming, 'replace');
|
|
|
|
assert.deepEqual(Object.keys(replaced.avatars), ['1506']);
|
|
assert.deepEqual(replaced.lightcones, []);
|
|
assert.deepEqual(replaced.relics, []);
|
|
assert.equal(replaced.battle_config.stage_id, 12);
|
|
});
|
|
|
|
test('mergeFreeSR works without an existing document', async () => {
|
|
const { mergeFreeSR, FREE_SR_DEFAULT_BATTLE_CONFIG } = await load();
|
|
const merged = mergeFreeSR(null, { avatars: { 1506: { avatar_id: 1506 } }, lightcones: [], relics: [] });
|
|
assert.deepEqual(Object.keys(merged.avatars), ['1506']);
|
|
assert.deepEqual(merged.battle_config, {
|
|
...FREE_SR_DEFAULT_BATTLE_CONFIG,
|
|
blessings: [],
|
|
custom_stats: [],
|
|
monsters: []
|
|
});
|
|
});
|