2026-11-03 16:42:02 +07:00
|
|
|
const test = require('node:test');
|
|
|
|
|
const assert = require('node:assert/strict');
|
|
|
|
|
const fs = require('node:fs');
|
|
|
|
|
const path = require('node:path');
|
|
|
|
|
const { pathToFileURL } = require('node:url');
|
|
|
|
|
|
|
|
|
|
const modesUrl = pathToFileURL(path.join(__dirname, '../js/modes.js'));
|
|
|
|
|
|
|
|
|
|
async function load() {
|
|
|
|
|
return import(modesUrl.href);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
test('GaCy GO keeps every tool feature', async () => {
|
|
|
|
|
const { modeCapabilities } = await load();
|
|
|
|
|
const caps = modeCapabilities('gac');
|
|
|
|
|
for (const feature of [
|
|
|
|
|
'saveManager', 'exranks', 'inventory', 'monsterSettings', 'damageAnalyst',
|
|
|
|
|
'sceneCard', 'gmConsole', 'battleSummary', 'toolSession'
|
|
|
|
|
]) {
|
|
|
|
|
assert.equal(caps[feature], true, `${feature} must be available on GaCy GO`);
|
|
|
|
|
}
|
2026-11-04 17:14:50 +07:00
|
|
|
assert.equal(caps.fireflyEndgame, false);
|
2026-11-03 16:42:02 +07:00
|
|
|
assert.equal(caps.extraData, false);
|
|
|
|
|
assert.equal(caps.ownedAvatarsOnly, false);
|
|
|
|
|
assert.deepEqual(caps.jsonModes, ['freesr', 'persistent']);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('FireflyGo hides every GaCy-only feature and exposes extra_data', async () => {
|
|
|
|
|
const { modeCapabilities, modeSupports } = await load();
|
|
|
|
|
const caps = modeCapabilities('firefly');
|
|
|
|
|
for (const feature of [
|
|
|
|
|
'saveManager', 'exranks', 'inventory', 'monsterSettings', 'damageAnalyst',
|
|
|
|
|
'sceneCard', 'gmConsole', 'battleSummary', 'toolSession'
|
|
|
|
|
]) {
|
|
|
|
|
assert.equal(caps[feature], false, `${feature} must be hidden on FireflyGo`);
|
|
|
|
|
}
|
|
|
|
|
assert.equal(caps.extraData, true);
|
2026-11-04 17:14:50 +07:00
|
|
|
assert.equal(caps.fireflyEndgame, true);
|
2026-11-03 16:42:02 +07:00
|
|
|
assert.equal(caps.ownedAvatarsOnly, true);
|
|
|
|
|
assert.deepEqual(caps.jsonModes, ['freesr']);
|
|
|
|
|
assert.equal(modeSupports('extraData', 'firefly'), true);
|
2026-11-04 17:14:50 +07:00
|
|
|
assert.equal(modeSupports('fireflyEndgame', 'firefly'), true);
|
|
|
|
|
assert.equal(modeSupports('monsterSettings', 'firefly'), false);
|
|
|
|
|
assert.equal(modeSupports('fireflyEndgame', 'gac'), false);
|
|
|
|
|
assert.equal(modeSupports('fireflyEndgame', 'robin'), false);
|
2026-11-03 16:42:02 +07:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('unknown modes fall back to GaCy GO', async () => {
|
|
|
|
|
const { normalizeMode, modeCapabilities, modeSupports, MODE_LABELS } = await load();
|
|
|
|
|
assert.equal(normalizeMode('nope'), 'gac');
|
|
|
|
|
assert.equal(normalizeMode(undefined), 'gac');
|
|
|
|
|
assert.equal(normalizeMode('firefly'), 'firefly');
|
|
|
|
|
assert.deepEqual(modeCapabilities('legacy'), modeCapabilities('gac'));
|
|
|
|
|
assert.equal(modeSupports('inventory', 'legacy'), true);
|
|
|
|
|
assert.equal(MODE_LABELS.firefly, 'FireflyGo');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('mode UI strings exist in Thai and English', async () => {
|
|
|
|
|
const { TRANSLATIONS } = await import('../js/i18n.js');
|
|
|
|
|
const keys = [
|
|
|
|
|
'serverModeFirefly',
|
|
|
|
|
'apiUrlHintGaC', 'apiUrlHintFirefly', 'apiUrlHintRobin',
|
|
|
|
|
'enkaImportTitle', 'enkaImportSubtitle', 'enkaUidLabel', 'enkaUidPlaceholder',
|
|
|
|
|
'enkaUidInvalid', 'enkaModeMerge', 'enkaModeReplace', 'btnEnkaFetch',
|
|
|
|
|
'enkaPasteLabel', 'enkaPastePlaceholder', 'btnEnkaParsePaste',
|
|
|
|
|
'enkaFetching', 'enkaImportDone', 'enkaUidNotFound', 'enkaRateLimited',
|
|
|
|
|
'enkaFetchFailed', 'enkaFetchFailedDetail', 'enkaNoAvatars', 'enkaPasteEmpty',
|
|
|
|
|
'enkaPasteInvalid', 'enkaProxyUnavailable', 'enkaSyncFailed',
|
|
|
|
|
'fireflyExtrasTitle', 'fireflyExtrasBadge', 'fireflyExtrasSubtitle',
|
|
|
|
|
'fireflyMultiPathTitle', 'fireflyMultiPathMain', 'fireflyMultiPathMarch7',
|
|
|
|
|
'fireflyMultiPathMainList', 'fireflyMultiPathMarch7List',
|
|
|
|
|
'fireflySettingTitle', 'fireflySettingCensorship', 'fireflySettingCm',
|
|
|
|
|
'fireflySettingFirstPerson', 'fireflySettingHideUi', 'fireflySettingInvasion',
|
|
|
|
|
'fireflyChallengeTitle', 'fireflyChallengeSkipNode', 'fireflyChallengeGroupId',
|
|
|
|
|
'fireflyChallengeGroupList', 'fireflyTheoryTitle', 'fireflyTheoryMode',
|
|
|
|
|
'fireflyTheoryCycleCount', 'fireflyTheoryStageId', 'fireflyTheoryLineup',
|
|
|
|
|
'fireflyTheoryHp', 'fireflyLuaLabel', 'fireflyLuaPlaceholder', 'fireflyListPlaceholder',
|
|
|
|
|
'btnFireflyProbe', 'btnFireflySaveExtra', 'btnFireflySync',
|
|
|
|
|
'fireflyProbing', 'fireflySyncing', 'fireflyProbeSuccess', 'fireflySyncSuccess',
|
|
|
|
|
'fireflyRequestFailed', 'fireflyRequestFailedDetail', 'fireflyTimeout',
|
2026-11-04 17:14:50 +07:00
|
|
|
'fireflyNoData', 'fireflyExtraSaved', 'fireflyHpInvalid',
|
|
|
|
|
'ffEndgameTitle', 'ffEndgameTablistLabel', 'ffEndgameDescription', 'ffEndgameRule', 'ffEndgameModePeak',
|
|
|
|
|
'ffEndgameLoading', 'ffEndgameReady', 'ffEndgameCatalogError', 'ffEndgameSeason',
|
|
|
|
|
'ffEndgameFloor', 'ffEndgameFloorValue', 'ffEndgameSide', 'ffEndgameSideFirst',
|
|
|
|
|
'ffEndgameSideSecond', 'ffEndgameSideThird', 'ffEndgameStage', 'ffEndgameStageValue',
|
|
|
|
|
'ffEndgameStageKindPre', 'ffEndgameStageKindBoss', 'ffEndgameStageKindHard',
|
|
|
|
|
'ffEndgameStageId', 'ffEndgamePreview', 'ffEndgameNoStages', 'ffEndgameUseCycle',
|
|
|
|
|
'ffEndgameUseBuff', 'ffEndgameCycles', 'ffEndgameBlessings', 'ffEndgameMonsterCount',
|
|
|
|
|
'ffEndgameApply', 'ffEndgameApplySync', 'ffEndgameReset', 'ffEndgameResetDone',
|
|
|
|
|
'ffEndgameSaving', 'ffEndgameSaved', 'ffEndgameSynced', 'ffEndgameApplyError',
|
|
|
|
|
'ffEndgameNoData', 'ffEndgameSyncError', 'ffEndgameCustomDescription',
|
|
|
|
|
'ffEndgameAddWave', 'ffEndgameRemoveWave', 'ffEndgameAddMonster',
|
|
|
|
|
'ffEndgameRemoveMonster', 'ffEndgameMonsterId', 'ffEndgameMonsterLevel',
|
|
|
|
|
'ffEndgameMonsterAmount', 'ffEndgameCycleCount'
|
2026-11-03 16:42:02 +07:00
|
|
|
];
|
|
|
|
|
for (const lang of ['th', 'en']) {
|
|
|
|
|
for (const key of keys) {
|
|
|
|
|
const value = TRANSLATIONS[lang]?.[key];
|
|
|
|
|
assert.equal(typeof value, 'string', `${lang}.${key} must be a string`);
|
|
|
|
|
assert.ok(value.trim().length > 0, `${lang}.${key} must not be empty`);
|
|
|
|
|
assert.notEqual(value, key, `${lang}.${key} must be translated`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('the removed paused notice is no longer referenced', () => {
|
|
|
|
|
const app = fs.readFileSync(path.join(__dirname, '../js/app.js'), 'utf8');
|
|
|
|
|
assert.equal(app.includes('serverModesPausedNotice'), false);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('mode-gated DOM hooks exist in index.html', () => {
|
|
|
|
|
const html = fs.readFileSync(path.join(__dirname, '../index.html'), 'utf8');
|
|
|
|
|
for (const id of [
|
|
|
|
|
'tab-btn-inventory', 'tab-btn-monster-settings', 'tab-btn-persistent', 'tab-btn-exrank',
|
|
|
|
|
'card-lineup', 'card-server-status', 'card-firefly-extras', 'api-url-mode-hint',
|
|
|
|
|
'enka-uid-input', 'enka-import-mode', 'btn-enka-fetch', 'enka-import-status',
|
|
|
|
|
'enka-paste-input', 'btn-enka-paste',
|
|
|
|
|
'ff-multi-path-main', 'ff-multi-path-march7', 'ff-multi-path-main-list',
|
|
|
|
|
'ff-multi-path-march7-list', 'ff-setting-censorship', 'ff-setting-cm',
|
|
|
|
|
'ff-setting-first-person', 'ff-setting-hide-ui', 'ff-setting-invasion-percent',
|
|
|
|
|
'ff-challenge-skip-node', 'ff-challenge-peak-group', 'ff-challenge-peak-list',
|
|
|
|
|
'ff-theory-mode', 'ff-theory-cycle-count', 'ff-theory-stage-id', 'ff-theory-lineup',
|
|
|
|
|
'ff-theory-hp', 'ff-lua', 'btn-firefly-probe', 'btn-firefly-save-extra',
|
2026-11-04 17:14:50 +07:00
|
|
|
'btn-firefly-sync', 'firefly-extra-status',
|
|
|
|
|
'monster-settings-gac', 'monster-settings-firefly', 'ff-endgame-tabs',
|
|
|
|
|
'ff-endgame-status', 'ff-endgame-view', 'ff-endgame-reset',
|
|
|
|
|
'ff-endgame-apply', 'ff-endgame-apply-sync'
|
2026-11-03 16:42:02 +07:00
|
|
|
]) {
|
|
|
|
|
assert.ok(html.includes(`id="${id}"`), `index.html must contain #${id}`);
|
|
|
|
|
}
|
|
|
|
|
});
|