mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-05-28 12:55:17 -07:00
feat(macos): configuration window + curl-backed macOS updater (#71)
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
|
||||
import { ResolvedConfig } from '../../types/config';
|
||||
import {
|
||||
CONFIG_OPTION_REGISTRY,
|
||||
CONFIG_TEMPLATE_SECTIONS,
|
||||
@@ -13,6 +14,77 @@ import { buildImmersionConfigOptionRegistry } from './options-immersion';
|
||||
import { buildIntegrationConfigOptionRegistry } from './options-integrations';
|
||||
import { buildSubtitleConfigOptionRegistry } from './options-subtitle';
|
||||
|
||||
function collectConfigLeafPaths(config: ResolvedConfig): string[] {
|
||||
const leaves: string[] = [];
|
||||
const visit = (value: unknown, prefix: string): void => {
|
||||
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
||||
leaves.push(prefix);
|
||||
return;
|
||||
}
|
||||
const entries = Object.entries(value as Record<string, unknown>);
|
||||
if (entries.length === 0) {
|
||||
leaves.push(prefix);
|
||||
return;
|
||||
}
|
||||
for (const [key, child] of entries) {
|
||||
visit(child, prefix ? `${prefix}.${key}` : key);
|
||||
}
|
||||
};
|
||||
visit(config, '');
|
||||
return leaves;
|
||||
}
|
||||
|
||||
// DEFAULT_CONFIG leaves that intentionally do not have a curated
|
||||
// CONFIG_OPTION_REGISTRY entry. The generated config.example.jsonc still
|
||||
// includes these paths, but their inline comments fall back to an auto-
|
||||
// humanized key name instead of a written description.
|
||||
//
|
||||
// Current intentional gaps:
|
||||
// - subtitleStyle.*: thin wrappers around standard CSS properties; the
|
||||
// CSS reference is the canonical documentation surface.
|
||||
// - keybindings: an array of {key, command} objects, documented at the
|
||||
// section level via CONFIG_TEMPLATE_SECTIONS rather than per-leaf.
|
||||
//
|
||||
// New leaves added to DEFAULT_CONFIG should prefer a registry entry over
|
||||
// an allowlist entry. Only allowlist a path when the registry is genuinely
|
||||
// the wrong surface for it.
|
||||
const UNDOCUMENTED_LEAVES: ReadonlySet<string> = new Set([
|
||||
'keybindings',
|
||||
'subtitleStyle.backdropFilter',
|
||||
'subtitleStyle.backgroundColor',
|
||||
'subtitleStyle.fontColor',
|
||||
'subtitleStyle.fontFamily',
|
||||
'subtitleStyle.fontKerning',
|
||||
'subtitleStyle.fontSize',
|
||||
'subtitleStyle.fontStyle',
|
||||
'subtitleStyle.fontWeight',
|
||||
'subtitleStyle.jlptColors.N1',
|
||||
'subtitleStyle.jlptColors.N2',
|
||||
'subtitleStyle.jlptColors.N3',
|
||||
'subtitleStyle.jlptColors.N4',
|
||||
'subtitleStyle.jlptColors.N5',
|
||||
'subtitleStyle.knownWordColor',
|
||||
'subtitleStyle.letterSpacing',
|
||||
'subtitleStyle.lineHeight',
|
||||
'subtitleStyle.nPlusOneColor',
|
||||
'subtitleStyle.secondary.backdropFilter',
|
||||
'subtitleStyle.secondary.backgroundColor',
|
||||
'subtitleStyle.secondary.fontColor',
|
||||
'subtitleStyle.secondary.fontFamily',
|
||||
'subtitleStyle.secondary.fontKerning',
|
||||
'subtitleStyle.secondary.fontSize',
|
||||
'subtitleStyle.secondary.fontStyle',
|
||||
'subtitleStyle.secondary.fontWeight',
|
||||
'subtitleStyle.secondary.letterSpacing',
|
||||
'subtitleStyle.secondary.lineHeight',
|
||||
'subtitleStyle.secondary.textRendering',
|
||||
'subtitleStyle.secondary.textShadow',
|
||||
'subtitleStyle.secondary.wordSpacing',
|
||||
'subtitleStyle.textRendering',
|
||||
'subtitleStyle.textShadow',
|
||||
'subtitleStyle.wordSpacing',
|
||||
]);
|
||||
|
||||
test('config option registry includes critical paths and has unique entries', () => {
|
||||
const paths = CONFIG_OPTION_REGISTRY.map((entry) => entry.path);
|
||||
|
||||
@@ -40,6 +112,35 @@ test('config option registry includes critical paths and has unique entries', ()
|
||||
assert.equal(new Set(paths).size, paths.length);
|
||||
});
|
||||
|
||||
test('every DEFAULT_CONFIG leaf is in CONFIG_OPTION_REGISTRY or UNDOCUMENTED_LEAVES', () => {
|
||||
const registryPaths = new Set(CONFIG_OPTION_REGISTRY.map((entry) => entry.path));
|
||||
const leaves = collectConfigLeafPaths(DEFAULT_CONFIG);
|
||||
|
||||
const missing = leaves
|
||||
.filter((path) => !registryPaths.has(path) && !UNDOCUMENTED_LEAVES.has(path))
|
||||
.sort();
|
||||
assert.deepEqual(
|
||||
missing,
|
||||
[],
|
||||
`Add CONFIG_OPTION_REGISTRY entries (preferred) or add to UNDOCUMENTED_LEAVES allowlist: ${missing.join(', ')}`,
|
||||
);
|
||||
|
||||
const stale = [...UNDOCUMENTED_LEAVES].filter((path) => registryPaths.has(path)).sort();
|
||||
assert.deepEqual(
|
||||
stale,
|
||||
[],
|
||||
`Remove from UNDOCUMENTED_LEAVES (now covered by CONFIG_OPTION_REGISTRY): ${stale.join(', ')}`,
|
||||
);
|
||||
|
||||
const leafSet = new Set(leaves);
|
||||
const orphaned = [...UNDOCUMENTED_LEAVES].filter((path) => !leafSet.has(path)).sort();
|
||||
assert.deepEqual(
|
||||
orphaned,
|
||||
[],
|
||||
`Remove from UNDOCUMENTED_LEAVES (no longer a DEFAULT_CONFIG leaf): ${orphaned.join(', ')}`,
|
||||
);
|
||||
});
|
||||
|
||||
test('config template sections include expected domains and unique keys', () => {
|
||||
const keys = CONFIG_TEMPLATE_SECTIONS.map((section) => section.key);
|
||||
const requiredKeys: (typeof keys)[number][] = [
|
||||
|
||||
Reference in New Issue
Block a user