235 lines
7.8 KiB
JavaScript
235 lines
7.8 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/firefly.js'));
|
|
|
|
async function load() {
|
|
return import(moduleUrl.href);
|
|
}
|
|
|
|
function sampleFreesr() {
|
|
return {
|
|
// Unknown top-level keys must not leak into the /sync payload.
|
|
lineups: { 0: 1506 },
|
|
scene: { plane_id: 1 },
|
|
avatars: {
|
|
1506: {
|
|
avatar_id: 1506,
|
|
owner_uid: 800000000,
|
|
level: 80,
|
|
promotion: 6,
|
|
sp_value: 70,
|
|
sp_max: 140,
|
|
techniques: [1, 2],
|
|
enhanced_id: 0,
|
|
data: {
|
|
rank: 2,
|
|
skills: { 1506001: 6, 1506002: 10, 1506201: 1 },
|
|
skills_by_anchor_type: { 1: 6, 2: 10 },
|
|
ex_rank_id_list: [5501, 0]
|
|
}
|
|
}
|
|
},
|
|
lightcones: [
|
|
{ item_id: 23001, level: 80, promotion: 6, rank: 1, equip_avatar: 1506, internal_uid: 0 },
|
|
{ item_id: 0, equip_avatar: 0 }
|
|
],
|
|
relics: [
|
|
{
|
|
relic_id: 61011,
|
|
relic_set_id: 101,
|
|
level: 15,
|
|
main_affix_id: 1,
|
|
equip_avatar: 1506,
|
|
internal_uid: 0,
|
|
sub_affixes: [
|
|
{ sub_affix_id: 2, count: 3, step: 0 },
|
|
{ sub_affix_id: 0, count: 1, step: 0 }
|
|
]
|
|
}
|
|
],
|
|
battle_config: {
|
|
battle_type: 'MOC',
|
|
blessings: [1, 2],
|
|
custom_stats: [],
|
|
cycle_count: 3,
|
|
stage_id: 12,
|
|
path_resonance_id: 0,
|
|
monsters: [[1002010, 1]],
|
|
custom_battle_lineup: { 0: 1506 },
|
|
unknown_key: 'drop me'
|
|
},
|
|
loadout: [{ avatar_id: 1506 }]
|
|
};
|
|
}
|
|
|
|
test('fireflySyncUrl always resolves to a /sync endpoint', async () => {
|
|
const { fireflySyncUrl, FIREFLY_DEFAULT_BASE_URL } = await load();
|
|
assert.equal(fireflySyncUrl('http://localhost:21000'), 'http://localhost:21000/sync');
|
|
assert.equal(fireflySyncUrl('http://localhost:21000/'), 'http://localhost:21000/sync');
|
|
assert.equal(fireflySyncUrl('http://localhost:21000/sync'), 'http://localhost:21000/sync');
|
|
assert.equal(fireflySyncUrl('http://localhost:21000/SYNC'), 'http://localhost:21000/SYNC');
|
|
assert.equal(fireflySyncUrl(''), `${FIREFLY_DEFAULT_BASE_URL}/sync`);
|
|
assert.equal(fireflySyncUrl(null), `${FIREFLY_DEFAULT_BASE_URL}/sync`);
|
|
assert.equal(fireflySyncUrl(' '), `${FIREFLY_DEFAULT_BASE_URL}/sync`);
|
|
});
|
|
|
|
test('normalizeExtraData coerces the server payload into the FireflyGo shape', async () => {
|
|
const { normalizeExtraData } = await load();
|
|
const extra = normalizeExtraData({
|
|
multi_path: { main: '3', march_7: 1, multi_path_main: '1, 2 ,3', multi_path_march_7: [4] },
|
|
setting: { censorship: 1, cm: 'true', first_person: 0, hide_ui: false, invasion_percent: '25' },
|
|
challenge: { skip_node: '2', challenge_peak_group_id: 7, challenge_peak_group_id_list: '8,9' },
|
|
theory_craft: { hp: { 1001: [12000] }, cycle_count: '2', mode: 'true', stage_id: '5', custom_lineup: [1001, 1002] },
|
|
lua: 42
|
|
});
|
|
|
|
assert.deepEqual(extra.multi_path, {
|
|
main: 3,
|
|
march_7: 1,
|
|
multi_path_main: [1, 2, 3],
|
|
multi_path_march_7: [4]
|
|
});
|
|
assert.deepEqual(extra.setting, {
|
|
censorship: true,
|
|
cm: true,
|
|
first_person: false,
|
|
hide_ui: false,
|
|
invasion_percent: 25
|
|
});
|
|
assert.deepEqual(extra.challenge, {
|
|
skip_node: 2,
|
|
challenge_peak_group_id: 7,
|
|
challenge_peak_group_id_list: [8, 9]
|
|
});
|
|
assert.deepEqual(extra.theory_craft, {
|
|
hp: { 1001: [12000] },
|
|
cycle_count: 2,
|
|
mode: true,
|
|
stage_id: 5,
|
|
custom_lineup: [1001, 1002]
|
|
});
|
|
assert.equal(extra.lua, '42');
|
|
});
|
|
|
|
test('normalizeExtraData fills missing blocks and rejects empty input', async () => {
|
|
const { normalizeExtraData } = await load();
|
|
assert.equal(normalizeExtraData(null), null);
|
|
assert.equal(normalizeExtraData('nope'), null);
|
|
assert.equal(normalizeExtraData([]), null);
|
|
|
|
const extra = normalizeExtraData({});
|
|
assert.deepEqual(extra.multi_path, { main: 0, march_7: 0, multi_path_main: [], multi_path_march_7: [] });
|
|
assert.deepEqual(extra.challenge, { skip_node: 0, challenge_peak_group_id: 0, challenge_peak_group_id_list: [] });
|
|
assert.equal(extra.lua, null);
|
|
assert.equal(extra.theory_craft, undefined);
|
|
});
|
|
|
|
test('buildSyncPayload emits the FreeSR shape FireflyGo expects', async () => {
|
|
const { buildSyncPayload } = await load();
|
|
const payload = buildSyncPayload(sampleFreesr());
|
|
|
|
assert.deepEqual(Object.keys(payload).sort(), ['avatars', 'battle_config', 'lightcones', 'loadout', 'relics']);
|
|
assert.equal(payload.extra_data, undefined);
|
|
|
|
const avatar = payload.avatars['1506'];
|
|
assert.equal(avatar.avatar_id, 1506);
|
|
assert.equal(avatar.level, 80);
|
|
assert.equal(avatar.sp_max, 140);
|
|
assert.deepEqual(avatar.techniques, [1, 2]);
|
|
// enhanced_id 0 must not be sent.
|
|
assert.equal('enhanced_id' in avatar, false);
|
|
// Zero ex-ranks are filtered out.
|
|
assert.deepEqual(avatar.data.ex_rank_id_list, [5501]);
|
|
assert.deepEqual(avatar.data.skills_by_anchor_type, { 1: 6, 2: 10 });
|
|
|
|
// Gear without an item id is dropped.
|
|
assert.equal(payload.lightcones.length, 1);
|
|
assert.equal(payload.lightcones[0].item_id, 23001);
|
|
|
|
assert.equal(payload.relics.length, 1);
|
|
assert.deepEqual(payload.relics[0].sub_affixes, [{ sub_affix_id: 2, count: 3, step: 0 }]);
|
|
|
|
// Unknown battle_config keys such as custom_battle_lineup are stripped and
|
|
// the blessing/monster arrays are coerced into FireflyGo's zod shape.
|
|
assert.deepEqual(payload.battle_config, {
|
|
battle_type: 'MOC',
|
|
blessings: [{ id: 1, level: 1 }, { id: 2, level: 1 }],
|
|
custom_stats: [],
|
|
cycle_count: 3,
|
|
stage_id: 12,
|
|
path_resonance_id: 0,
|
|
monsters: [[{ monster_id: 1002010, level: 1, amount: 1 }, { monster_id: 1, level: 1, amount: 1 }]]
|
|
});
|
|
});
|
|
|
|
test('buildSyncPayload normalises detailed blessings and monster waves', async () => {
|
|
const { buildSyncPayload } = await load();
|
|
const payload = buildSyncPayload({
|
|
battle_config: {
|
|
battle_type: 'PEAK',
|
|
blessings: [
|
|
{ id: 3034002, level: '2', dynamic_key: { key: 'abc', value: '4' } },
|
|
{ id: 0, level: 1 },
|
|
{ id: '3001' }
|
|
],
|
|
cycle_count: '6',
|
|
stage_id: '30127123',
|
|
monsters: [
|
|
[
|
|
{ monster_id: 4033010, level: 92, amount: 2 },
|
|
{ monster_id: 0, level: 1 },
|
|
{ monster_id: '4033020', level: '95' }
|
|
],
|
|
[],
|
|
[4044010]
|
|
]
|
|
}
|
|
});
|
|
|
|
assert.deepEqual(payload.battle_config.blessings, [
|
|
{ id: 3034002, level: 2, dynamic_key: { key: 'abc', value: 4 } },
|
|
{ id: 3001, level: 1 }
|
|
]);
|
|
assert.deepEqual(payload.battle_config.monsters, [
|
|
[
|
|
{ monster_id: 4033010, level: 92, amount: 2 },
|
|
{ monster_id: 4033020, level: 95, amount: 1 }
|
|
],
|
|
[{ monster_id: 4044010, level: 1, amount: 1 }]
|
|
]);
|
|
assert.equal(payload.battle_config.cycle_count, 6);
|
|
assert.equal(payload.battle_config.stage_id, 30127123);
|
|
});
|
|
|
|
test('buildSyncPayload derives the anchor map when the save has none', async () => {
|
|
const { buildSyncPayload } = await load();
|
|
const payload = buildSyncPayload({
|
|
avatars: {
|
|
1001: {
|
|
avatar_id: 1001,
|
|
data: { rank: 6, skills: { 1001001: 6, 1001002: 10, 1001103: 1, 1001201: 1, 1001202: 1 } }
|
|
}
|
|
},
|
|
lightcones: [],
|
|
relics: []
|
|
});
|
|
assert.deepEqual(payload.avatars['1001'].data.skills_by_anchor_type, {
|
|
1: 6, 2: 10, 9: 1, 8: 1
|
|
});
|
|
});
|
|
|
|
test('buildSyncPayload survives empty or malformed documents', async () => {
|
|
const { buildSyncPayload } = await load();
|
|
const empty = buildSyncPayload(null);
|
|
assert.deepEqual(empty.avatars, {});
|
|
assert.deepEqual(empty.lightcones, []);
|
|
assert.deepEqual(empty.relics, []);
|
|
assert.equal(empty.battle_config.battle_type, 'DEFAULT');
|
|
|
|
const malformed = buildSyncPayload({ avatars: { 1: null, 2: { avatar_id: 0 } } });
|
|
assert.deepEqual(malformed.avatars, {});
|
|
});
|