Group settings with explicit modes and one save action

This commit is contained in:
2026-10-07 19:06:56 -04:00
parent e4f37f6964
commit bab78038dc
7 changed files with 425 additions and 78 deletions
+51 -1
View File
@@ -2,7 +2,8 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { newFixture, type Fixture } from '../helpers/fixture.js';
let fixture: Fixture;
import { defaultFleetSettings as defaults } from '../../src/lib/fleet-settings.js';
import { defaultFleetSettings as defaults, additionalConcurrencyKeys, fleetSettingLabels, settingDisplayValue, settingHelp,
settingMinimum, settingsGroups, settingUnit, settingZeroMode, validateFleetSettings, type FleetSettings } from '../../src/lib/fleet-settings.js';
beforeEach(async () => {
vi.resetModules();
fixture = await newFixture([]);
@@ -139,3 +140,52 @@ it('saves a section without overwriting changes in the other section', async ()
expect(response.status).toBe(200);
expect(await GET().json()).toMatchObject({ deployKeepDays: 42, rebuildConcurrency: 2 });
});
describe('settings presentation semantics', () => {
it('places every saved setting in exactly one searchable group', () => {
const grouped = settingsGroups.flatMap(group => group.keys);
expect(grouped.length).toBe(Object.keys(defaults).length);
expect(new Set(grouped).size).toBe(grouped.length);
expect([...grouped].sort()).toEqual(Object.keys(defaults).sort());
expect(settingsGroups.map(group => group.title)).toEqual([
'Concurrency', 'Operation history', 'Matches & players', 'Telemetry & metrics', 'Files & versions', 'Accounts & audit'
]);
});
it('distinguishes an unlimited cap from permanent retention, age expiry, and minimum counts', () => {
for (const key of [...additionalConcurrencyKeys, 'telemetryMaxPerServer', 'telemetryMaxTotal'] as (keyof FleetSettings)[]) {
expect(settingZeroMode(key)).toBe('No limit');
}
for (const key of Object.keys(defaults) as (keyof FleetSettings)[]) {
if (key.endsWith('Days') && !['versionKeepDays', 'gameFileGraceDays'].includes(key)) expect(settingZeroMode(key)).toBe('Forever');
if (settingMinimum(key) === 1) expect(settingZeroMode(key)).toBeNull();
}
expect(settingZeroMode('versionKeepDays')).toBe('No age expiry');
expect(settingZeroMode('gameFileGraceDays')).toBe('No delay');
expect(settingZeroMode('versionKeepCount')).toBe('No minimum');
expect(settingZeroMode('historyKeepCount')).toBe('No minimum');
expect(settingHelp.versionKeepDays).toContain('version count policy may still remove');
expect(settingHelp.versionKeepCount).toContain('a positive count removes older unused versions');
});
it('formats numeric values and defaults without changing their stored meaning', () => {
const original = structuredClone(defaults);
for (const key of Object.keys(defaults) as (keyof FleetSettings)[]) {
const zero = settingZeroMode(key);
if (zero) {
expect(settingDisplayValue(key, 0)).toBe(zero);
expect(validateFleetSettings({ ...defaults, [key]: 0 })[key]).toBe(0);
}
expect(settingDisplayValue(key, 1)).toBe(`1 ${settingUnit(key).slice(0, -1)}`);
expect(settingDisplayValue(key, 14)).toBe(`14 ${settingUnit(key)}`);
expect(validateFleetSettings({ ...defaults, [key]: 14 })[key]).toBe(14);
}
expect(settingDisplayValue('telemetryMaxTotal', 10_000)).toBe('10,000 records');
expect(settingDisplayValue('preparationConcurrency', 2)).toBe('2 hosts');
expect(settingDisplayValue('versionKeepCount', 1)).toBe('1 version');
expect(settingDisplayValue('matchKeepDays', NaN)).toBe('—');
expect(defaults).toEqual(original);
expect(fleetSettingLabels.destroyConcurrency).toBe('Concurrent server removals');
expect(fleetSettingLabels.destroyConcurrencyPerHost).toBe('Concurrent server removals per host');
});
});
+42
View File
@@ -0,0 +1,42 @@
import { describe, expect, it, vi } from 'vitest';
import { render } from 'svelte/server';
import SettingsEditor from '../../src/lib/ui/components/SettingsEditor.svelte';
import { defaultFleetSettings } from '../../src/lib/fleet-settings.js';
vi.mock('$app/navigation', () => ({ beforeNavigate: vi.fn(), goto: vi.fn() }));
const html = (initial = defaultFleetSettings) => render(SettingsEditor, { props: { initial } }).body.replaceAll(/<!--.*?-->/g, '');
describe('grouped Settings editor', () => {
it('shows one section with meaningful defaults, and keeps every category reachable from the rail and mobile selector', () => {
const body = html();
expect(body).toContain('aria-label="Settings sections"');
expect(body).toContain('aria-label="Settings section"');
expect(body).toContain('href="#settings-group-matches-players"');
expect(body).toContain('href="#settings-group-files-versions"');
expect(body).toContain('id="setting-restartConcurrency"');
expect(body).not.toContain('id="setting-matchKeepDays"');
expect(body).toContain('1 operation');
expect(body).toContain('No limit');
expect(body).not.toMatch(/<input[^>]*value="0"/);
});
it('compares with the saved baseline rather than factory defaults and disables Save and Discard when unchanged', () => {
const body = html({ ...defaultFleetSettings, preparationConcurrency: 7 });
expect(body).toContain('All changes saved');
expect(body).not.toContain('unsaved changes');
expect(body).not.toContain('class="changed-marker"');
expect(body).toMatch(/<input[^>]*id="setting-preparationConcurrency"[^>]*value="7"/);
expect(body).toMatch(/<button[^>]*disabled[^>]*>[\s\S]*?<span[^>]*>Discard<\/span>/);
expect(body).toMatch(/<button[^>]*type="submit"[^>]*form="[^"]+"[^>]*disabled[^>]*>Save changes<\/button>/);
});
it('keeps zero-mode controls labelled and cleanup tools outside the one settings form', () => {
const body = html();
expect(body).toMatch(/<label[^>]*for="setting-restartConcurrencyPerHost-zero"/);
expect(body).toMatch(/<input[^>]*id="setting-restartConcurrencyPerHost-zero"[^>]*checked/);
expect(body.match(/<form\b/g)).toHaveLength(1);
expect(body.indexOf('</form>')).toBeLessThan(body.indexOf('Cleanup preview and protected records'));
expect(body).toContain('Expired data cannot be restored by increasing retention.');
expect(body).toContain('Player safety checks still apply.');
});
});