Files
fusion/packages/dashboard/app/components/settings/__tests__/section-keys.test.ts
gsxdsm f992e6aefa FN-7553: add dedicated keyboard shortcuts settings section
Adds a standalone Keyboard Shortcuts settings section with an easier-to-use capture input and support for more shortcut actions.
- Introduce a new KeyboardShortcutsSection with a dedicated ShortcutCaptureInput component for recording key combos
- Split keyboard-shortcut settings out of the general settings section into their own settings tab
- Extend the shortcut schema/types and useDashboardKeyboardShortcuts hook to support additional actions
- Update settings save-split, section-keys, and defaults/parity tests to cover the new section
- Update dashboard docs and add a changeset for the new settings section

Files changed:
 .changeset/fn-7553-keyboard-shortcuts-section.md   |   7 +
 docs/dashboard-guide.md                            |  22 ++-
 packages/core/src/__tests__/global-settings.test.ts     |   9 +-
 packages/core/src/__tests__/settings-defaults.test.ts   |   4 +
 packages/core/src/__tests__/settings-parity.test.ts     |   2 +-
 packages/core/src/settings-schema.ts               |   6 +-
 packages/core/src/types.ts                         |  12 ++
 packages/dashboard/app/App.tsx                     |  26 +++-
 packages/dashboard/app/__tests__/App.keyboard-shortcuts.test.tsx  |  47 ++++++-
 packages/dashboard/app/components/SettingsModal.css     |  56 ++++++--
 packages/dashboard/app/components/SettingsModal.tsx     |  30 +++-
 packages/dashboard/app/components/__tests__/SettingsModal.general.test.tsx |  63 ---------
 packages/dashboard/app/components/__tests__/SettingsModal.keyboardShortcuts.test.tsx |  156 +++++++++++++++++++++
 packages/dashboard/app/components/settings/__tests__/section-keys.test.ts |   1 +
 packages/dashboard/app/components/settings/save-split.ts |   6 +-
 packages/dashboard/app/components/settings/sections/GlobalGeneralSection.tsx |  38 -----
 packages/dashboard/app/components/settings/sections/KeyboardShortcutsSection.tsx |  78 +++++++++++
 packages/dashboard/app/components/settings/sections/ShortcutCaptureInput.tsx |  118 ++++++++++++++++
 packages/dashboard/app/components/settings/sections/__tests__/KeyboardShortcutsSection.test.tsx |  113 +++++++++++++++
 packages/dashboard/app/hooks/__tests__/useDashboardKeyboardShortcuts.test.tsx |  73 +++++++++-
 packages/dashboard/app/hooks/useDashboardKeyboardShortcuts.ts |  38 ++++-
 packages/dashboard/app/utils/__tests__/keyboardShortcuts.test.ts |  37 ++++-
 packages/dashboard/app/utils/keyboardShortcuts.ts  |  54 ++++++-
 23 files changed, 854 insertions(+), 142 deletions(-)

Fusion-Task-Id: FN-7553

Fusion-Task-Lineage: 51c1ad69-fbd3-45ce-8d2d-45d9962d7b76

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-04 21:28:19 -07:00

173 lines
6.1 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { GLOBAL_SETTINGS_KEYS, PROJECT_SETTINGS_KEYS } from "@fusion/core";
import {
ALL_PROJECT_RESET_KEYS,
EXCLUDED_RESET_SECTIONS,
getResetIneligibleReason,
getSectionKeyEntry,
isRegistryKeyValidForScope,
isResetEligibleSection,
} from "../section-keys";
const GLOBAL_KEY_SET = new Set<string>(GLOBAL_SETTINGS_KEYS as readonly string[]);
const PROJECT_KEY_SET = new Set<string>(PROJECT_SETTINGS_KEYS as readonly string[]);
/** Every key-owning section id we expect the registry to resolve, with its declared scope. */
const EXPECTED_KEY_OWNING_SECTIONS: Record<string, "global" | "project"> = {
// global sections (reused from GLOBAL_SECTION_KEYS in save-split.ts)
appearance: "global",
notifications: "global",
experimental: "global",
"global-general": "global",
"keyboard-shortcuts": "global",
"global-models": "global",
"node-sync": "global",
"research-global": "global",
remote: "global",
// project sections (new for FN-7506)
general: "project",
commands: "project",
worktrees: "project",
scheduling: "project",
"scheduled-evals": "project",
"node-routing": "project",
merge: "project",
"agent-permissions": "project",
backups: "project",
"research-project": "project",
"project-models": "project",
};
const EXPECTED_EXCLUDED_SECTIONS = [
"secrets",
"global-mcp",
"mcp",
"plugins",
"memory",
"authentication",
"prompts",
"cli-agents",
"hermes-runtime",
"openclaw-runtime",
"paperclip-runtime",
];
describe("settings section-keys registry", () => {
it("resolves every expected key-owning section with the correct scope", () => {
for (const [sectionId, scope] of Object.entries(EXPECTED_KEY_OWNING_SECTIONS)) {
const entry = getSectionKeyEntry(sectionId);
expect(entry, `expected ${sectionId} to be reset-eligible`).not.toBeNull();
expect(entry!.scope).toBe(scope);
expect(entry!.keys.length).toBeGreaterThan(0);
expect(isResetEligibleSection(sectionId)).toBe(true);
}
});
it("every registry key is a real member of the canonical scope key set matching its declared scope", () => {
for (const [sectionId, scope] of Object.entries(EXPECTED_KEY_OWNING_SECTIONS)) {
const entry = getSectionKeyEntry(sectionId)!;
for (const key of entry.keys) {
const validForDeclaredScope = isRegistryKeyValidForScope(key, scope);
expect(
validForDeclaredScope,
`section "${sectionId}" claims key "${key}" at scope "${scope}", but it is not a member of the matching ${scope === "global" ? "GLOBAL_SETTINGS_KEYS" : "PROJECT_SETTINGS_KEYS"} set`,
).toBe(true);
if (scope === "global") {
expect(GLOBAL_KEY_SET.has(key)).toBe(true);
} else {
expect(PROJECT_KEY_SET.has(key)).toBe(true);
}
}
}
});
it("no key is claimed by two sections at the same scope", () => {
const seenAtScope: Record<"global" | "project", Map<string, string>> = {
global: new Map(),
project: new Map(),
};
for (const [sectionId, scope] of Object.entries(EXPECTED_KEY_OWNING_SECTIONS)) {
const entry = getSectionKeyEntry(sectionId)!;
for (const key of entry.keys) {
const owner = seenAtScope[scope].get(key);
expect(
owner,
`key "${key}" at scope "${scope}" is claimed by both "${owner}" and "${sectionId}"`,
).toBeUndefined();
seenAtScope[scope].set(key, sectionId);
}
}
});
it("excludes non-key sections explicitly, with a documented reason, and marks them reset-ineligible", () => {
for (const sectionId of EXPECTED_EXCLUDED_SECTIONS) {
expect(getSectionKeyEntry(sectionId)).toBeNull();
expect(isResetEligibleSection(sectionId)).toBe(false);
expect(getResetIneligibleReason(sectionId)).toBeTruthy();
expect(EXCLUDED_RESET_SECTIONS[sectionId]).toBeTruthy();
}
});
it("treats unknown/group-header section ids as reset-ineligible without a reason", () => {
expect(getSectionKeyEntry("__project_header")).toBeNull();
expect(isResetEligibleSection("__project_header")).toBe(false);
expect(getResetIneligibleReason("__project_header")).toBeUndefined();
});
it("a representative project section (merge) maps to its expected owned keys", () => {
const entry = getSectionKeyEntry("merge")!;
expect(entry.scope).toBe("project");
expect(new Set(entry.keys)).toEqual(
new Set([
"autoMerge",
"autoResolveConflicts",
"commitAuthorEmail",
"commitAuthorEnabled",
"commitAuthorName",
"directMergeCommitStrategy",
"githubAuthMode",
"githubAuthToken",
"gitlabAuthToken",
"gitlabAuthTokenType",
"includeTaskIdInCommit",
"integrationBranch",
"maxAutoMergeRetries",
"mergeAdvanceAutoSync",
"mergeConflictStrategy",
"mergeIntegrationWorktree",
"mergeStrategy",
"mergeStrategyOverlapBehavior",
"merger",
"planApprovalMode",
"postMergeAuditMode",
"pushAfterMerge",
"pushRemote",
"smartConflictResolution",
"testMode",
]),
);
// gitlabEnabled's enable+URL fields are owned by "general" instead, not duplicated here.
expect(entry.keys).not.toContain("gitlabEnabled");
});
it("a representative global section (appearance) maps to its expected owned keys", () => {
const entry = getSectionKeyEntry("appearance")!;
expect(entry.scope).toBe("global");
expect(new Set(entry.keys)).toEqual(
new Set(["themeMode", "colorTheme", "dashboardFontScalePct", "shadcnCustomColors"]),
);
});
it("ALL_PROJECT_RESET_KEYS contains only project keys and never global-only keys", () => {
expect(ALL_PROJECT_RESET_KEYS.length).toBeGreaterThan(0);
for (const key of ALL_PROJECT_RESET_KEYS) {
expect(PROJECT_KEY_SET.has(key)).toBe(true);
}
// Sanity: a couple of known global-only keys must not sneak into the project set.
expect(ALL_PROJECT_RESET_KEYS).not.toContain("themeMode");
expect(ALL_PROJECT_RESET_KEYS).not.toContain("ntfyEnabled");
});
});