@hoox-sh/hoox-tui 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +247 -0
- package/bin/hoox-tui.js +35 -0
- package/dist/main.js +17594 -0
- package/package.json +72 -0
- package/src/app.tsx +696 -0
- package/src/cli-bridge-test-double.ts +212 -0
- package/src/components/layout/layout.test.tsx +205 -0
- package/src/components/layout/sidebar.tsx +69 -0
- package/src/components/layout/statusbar.test.tsx +423 -0
- package/src/components/layout/statusbar.tsx +348 -0
- package/src/components/shared/animated-border.tsx +58 -0
- package/src/components/shared/chrome.test.tsx +29 -0
- package/src/components/shared/command-palette.tsx +361 -0
- package/src/components/shared/cool-brackets.tsx +75 -0
- package/src/components/shared/crash-screen.tsx +167 -0
- package/src/components/shared/error-boundary.tsx +99 -0
- package/src/components/shared/keybinding-hint.tsx +37 -0
- package/src/components/shared/panel.tsx +61 -0
- package/src/components/shared/quit-modal.tsx +65 -0
- package/src/components/shared/shared.test.tsx +538 -0
- package/src/components/shared/spinner.tsx +85 -0
- package/src/components/shared/status-dot.tsx +54 -0
- package/src/components/shared/view-header.tsx +53 -0
- package/src/components/ui/connection-toasts.test.ts +50 -0
- package/src/components/ui/connection-toasts.ts +215 -0
- package/src/components/ui/dialog.tsx +281 -0
- package/src/components/ui/select.tsx +197 -0
- package/src/components/ui/toast.tsx +185 -0
- package/src/components/views/ai-chat.test.tsx +81 -0
- package/src/components/views/ai-chat.tsx +576 -0
- package/src/components/views/config-editor/action-bar.tsx +102 -0
- package/src/components/views/config-editor/code-editor.tsx +597 -0
- package/src/components/views/config-editor/file-tree.tsx +113 -0
- package/src/components/views/config-editor/types.ts +37 -0
- package/src/components/views/config-editor.test.tsx +465 -0
- package/src/components/views/config-editor.tsx +709 -0
- package/src/components/views/dashboard/alerts-panel.tsx +154 -0
- package/src/components/views/dashboard/auto-repair-panel.tsx +229 -0
- package/src/components/views/dashboard/model-health-section.tsx +276 -0
- package/src/components/views/dashboard/pyne-health-section.tsx +112 -0
- package/src/components/views/dashboard.test.tsx +347 -0
- package/src/components/views/dashboard.tsx +647 -0
- package/src/components/views/db-query.test.tsx +115 -0
- package/src/components/views/db-query.tsx +849 -0
- package/src/components/views/edge-topology.test.tsx +198 -0
- package/src/components/views/edge-topology.tsx +572 -0
- package/src/components/views/kv-viewer.test.tsx +241 -0
- package/src/components/views/kv-viewer.tsx +717 -0
- package/src/components/views/logs-viewer.test.tsx +273 -0
- package/src/components/views/logs-viewer.tsx +616 -0
- package/src/components/views/queue-depth.test.tsx +221 -0
- package/src/components/views/queue-depth.tsx +376 -0
- package/src/components/views/secrets-viewer.test.tsx +65 -0
- package/src/components/views/secrets-viewer.tsx +548 -0
- package/src/components/views/service-manager.test.tsx +358 -0
- package/src/components/views/service-manager.tsx +946 -0
- package/src/components/views/settings.test.tsx +648 -0
- package/src/components/views/settings.tsx +1497 -0
- package/src/components/views/setup-wizard.test.tsx +297 -0
- package/src/components/views/setup-wizard.tsx +1321 -0
- package/src/components/views/trade-monitor.test.tsx +592 -0
- package/src/components/views/trade-monitor.tsx +745 -0
- package/src/components/views/worker-detail.test.tsx +289 -0
- package/src/components/views/worker-detail.tsx +676 -0
- package/src/components/views/worker-settings.test.tsx +43 -0
- package/src/components/views/worker-settings.tsx +563 -0
- package/src/components/views/workers-overview.test.tsx +260 -0
- package/src/components/views/workers-overview.tsx +631 -0
- package/src/hooks/hooks.test.ts +72 -0
- package/src/hooks/index.ts +11 -0
- package/src/hooks/renderer-ref.ts +22 -0
- package/src/hooks/use-keyboard.ts +130 -0
- package/src/hooks/use-polling.ts +73 -0
- package/src/hooks/use-service-data.ts +18 -0
- package/src/main.tsx +117 -0
- package/src/network-test-double.ts +133 -0
- package/src/opentui-augment.d.ts +45 -0
- package/src/opentui-jsx.d.ts +70 -0
- package/src/services/cli-bridge/index.ts +1662 -0
- package/src/services/cli-bridge/standalone.ts +315 -0
- package/src/services/cli-bridge/types.ts +307 -0
- package/src/services/cli-bridge.test.ts +406 -0
- package/src/services/clipboard.test.ts +57 -0
- package/src/services/clipboard.ts +180 -0
- package/src/services/dashboard-settings-loader.ts +31 -0
- package/src/services/dev-log.test.ts +168 -0
- package/src/services/dev-log.ts +194 -0
- package/src/services/hoox-path-service.test.ts +183 -0
- package/src/services/hoox-path-service.ts +185 -0
- package/src/services/tui-connection.test.ts +212 -0
- package/src/services/tui-connection.ts +236 -0
- package/src/services/tui-storage.test.ts +92 -0
- package/src/services/tui-storage.ts +86 -0
- package/src/stores/config-store.test.ts +360 -0
- package/src/stores/service-store.test.ts +1026 -0
- package/src/stores/session-restore.test.ts +134 -0
- package/src/stores/ui-store.test.ts +276 -0
- package/src/test-setup.ts +265 -0
- package/src/test-utils.ts +179 -0
- package/src/types.ts +153 -0
- package/src/utils/colors.test.ts +326 -0
- package/src/utils/formatters.test.ts +236 -0
- package/src/view-registry.test.ts +132 -0
- package/src/view-registry.tsx +368 -0
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2026 HOOX · HOOX · jango-blockchained (hoox-sh)
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Formatters Utility Tests — Time formatting, number formatting.
|
|
8
|
+
*
|
|
9
|
+
* Sources of truth (no local mirror drift):
|
|
10
|
+
* - formatRelativeTime / formatDuration (ms) → @hoox-sh/hoox-shared/format-time
|
|
11
|
+
* - formatNumber, formatUptime, formatLatency, formatRequests, formatPercent
|
|
12
|
+
* → @hoox-sh/hoox-shared formatters
|
|
13
|
+
*
|
|
14
|
+
* Uses Bun test runner.
|
|
15
|
+
*/
|
|
16
|
+
import { describe, it, expect } from "bun:test";
|
|
17
|
+
import {
|
|
18
|
+
formatRelativeTime,
|
|
19
|
+
formatDuration,
|
|
20
|
+
} from "@hoox-sh/hoox-shared/format-time";
|
|
21
|
+
import {
|
|
22
|
+
formatNumber,
|
|
23
|
+
formatLatency,
|
|
24
|
+
formatRequests,
|
|
25
|
+
formatPercent,
|
|
26
|
+
formatUptime,
|
|
27
|
+
formatDurationCompact,
|
|
28
|
+
} from "@hoox-sh/hoox-shared";
|
|
29
|
+
|
|
30
|
+
// ─── formatRelativeTime (format-time: status bar / reconnection) ─────────────
|
|
31
|
+
|
|
32
|
+
describe("formatRelativeTime", () => {
|
|
33
|
+
const now = 2000000; // fixed "now" reference for deterministic tests
|
|
34
|
+
|
|
35
|
+
it('returns "—" for timestamp 0', () => {
|
|
36
|
+
expect(formatRelativeTime(0, now)).toBe("—");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('returns "just now" for future timestamps', () => {
|
|
40
|
+
expect(formatRelativeTime(now + 5000, now)).toBe("just now");
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('returns "< 1m ago" for less than 60 seconds', () => {
|
|
44
|
+
expect(formatRelativeTime(now - 1000, now)).toBe("< 1m ago");
|
|
45
|
+
expect(formatRelativeTime(now - 59000, now)).toBe("< 1m ago");
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("returns minutes ago for 1-59 minutes", () => {
|
|
49
|
+
expect(formatRelativeTime(now - 60000, now)).toBe("1m ago");
|
|
50
|
+
expect(formatRelativeTime(now - 120000, now)).toBe("2m ago");
|
|
51
|
+
expect(formatRelativeTime(now - 3540000, now)).toBe("59m ago");
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("returns hours ago for 1-23 hours", () => {
|
|
55
|
+
expect(formatRelativeTime(now - 3600000, now)).toBe("1h ago");
|
|
56
|
+
expect(formatRelativeTime(now - 7200000, now)).toBe("2h ago");
|
|
57
|
+
expect(formatRelativeTime(now - 82800000, now)).toBe("23h ago");
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("returns days ago for 1-29 days", () => {
|
|
61
|
+
expect(formatRelativeTime(now - 86400000, now)).toBe("1d ago");
|
|
62
|
+
expect(formatRelativeTime(now - 86400000 * 7, now)).toBe("7d ago");
|
|
63
|
+
expect(formatRelativeTime(now - 86400000 * 29, now)).toBe("29d ago");
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('returns "> 30d ago" for 30+ days', () => {
|
|
67
|
+
expect(formatRelativeTime(now - 86400000 * 30, now)).toBe("> 30d ago");
|
|
68
|
+
expect(formatRelativeTime(now - 86400000 * 100, now)).toBe("> 30d ago");
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("uses Date.now() when nowMs is not provided", () => {
|
|
72
|
+
const result = formatRelativeTime(Date.now() - 60000);
|
|
73
|
+
expect(result).toMatch(/^(< 1m|\d+m) ago$/);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("handles exactly 60 seconds boundary", () => {
|
|
77
|
+
expect(formatRelativeTime(now - 60000, now)).toBe("1m ago");
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("handles exactly 60 minutes boundary", () => {
|
|
81
|
+
expect(formatRelativeTime(now - 3600000, now)).toBe("1h ago");
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("handles exactly 24 hours boundary", () => {
|
|
85
|
+
expect(formatRelativeTime(now - 86400000, now)).toBe("1d ago");
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("handles exactly 30 days boundary", () => {
|
|
89
|
+
expect(formatRelativeTime(now - 86400000 * 30, now)).toBe("> 30d ago");
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("handles negative timestamp (future) gracefully", () => {
|
|
93
|
+
expect(formatRelativeTime(now + 1, now)).toBe("just now");
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// ─── formatDuration (format-time: ms → compact downtime string) ──────────────
|
|
98
|
+
|
|
99
|
+
describe("formatDuration (ms)", () => {
|
|
100
|
+
it("formats seconds when under 1 minute", () => {
|
|
101
|
+
expect(formatDuration(0)).toBe("0s");
|
|
102
|
+
expect(formatDuration(1000)).toBe("1s");
|
|
103
|
+
expect(formatDuration(30000)).toBe("30s");
|
|
104
|
+
expect(formatDuration(59000)).toBe("59s");
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("formats minutes with seconds when seconds remain", () => {
|
|
108
|
+
expect(formatDuration(60000)).toBe("1m");
|
|
109
|
+
expect(formatDuration(61000)).toBe("1m 1s");
|
|
110
|
+
expect(formatDuration(90000)).toBe("1m 30s");
|
|
111
|
+
expect(formatDuration(3540000)).toBe("59m");
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("formats hours when 1+ hour", () => {
|
|
115
|
+
expect(formatDuration(3600000)).toBe("1h");
|
|
116
|
+
expect(formatDuration(3661000)).toBe("1h 1m");
|
|
117
|
+
expect(formatDuration(7200000)).toBe("2h");
|
|
118
|
+
expect(formatDuration(7260000)).toBe("2h 1m");
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("handles large durations", () => {
|
|
122
|
+
expect(formatDuration(90000000)).toBe("25h");
|
|
123
|
+
expect(formatDuration(91800000)).toBe("25h 30m");
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("handles milliseconds below 1 second", () => {
|
|
127
|
+
expect(formatDuration(500)).toBe("0s");
|
|
128
|
+
expect(formatDuration(999)).toBe("0s");
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// ─── Shared number / uptime / latency formatters ─────────────────────────────
|
|
133
|
+
|
|
134
|
+
describe("formatNumber (shared)", () => {
|
|
135
|
+
it("formats thousands with K suffix", () => {
|
|
136
|
+
expect(formatNumber(1500)).toBe("1.5K");
|
|
137
|
+
expect(formatNumber(2000)).toBe("2.0K");
|
|
138
|
+
expect(formatNumber(999000)).toBe("999.0K");
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("formats millions with M suffix", () => {
|
|
142
|
+
expect(formatNumber(1500000)).toBe("1.5M");
|
|
143
|
+
expect(formatNumber(2000000)).toBe("2.0M");
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("returns locale string for numbers under 1000", () => {
|
|
147
|
+
expect(formatNumber(500)).toBe((500).toLocaleString());
|
|
148
|
+
expect(formatNumber(0)).toBe((0).toLocaleString());
|
|
149
|
+
expect(formatNumber(999)).toBe((999).toLocaleString());
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("handles non-finite", () => {
|
|
153
|
+
expect(formatNumber(NaN)).toBe("—");
|
|
154
|
+
expect(formatNumber(Infinity)).toBe("—");
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
describe("formatUptime / formatDurationCompact (seconds)", () => {
|
|
159
|
+
it("formats seconds", () => {
|
|
160
|
+
expect(formatDurationCompact(0)).toBe("0s");
|
|
161
|
+
expect(formatDurationCompact(30)).toBe("30s");
|
|
162
|
+
expect(formatDurationCompact(59)).toBe("59s");
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it("formats minutes", () => {
|
|
166
|
+
expect(formatDurationCompact(60)).toBe("1m");
|
|
167
|
+
expect(formatDurationCompact(90)).toBe("1m");
|
|
168
|
+
expect(formatDurationCompact(3599)).toBe("59m");
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("formats hours with minutes", () => {
|
|
172
|
+
expect(formatDurationCompact(3600)).toBe("1h 0m");
|
|
173
|
+
expect(formatDurationCompact(3661)).toBe("1h 1m");
|
|
174
|
+
expect(formatDurationCompact(7200)).toBe("2h 0m");
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it("formats days with hours", () => {
|
|
178
|
+
expect(formatDurationCompact(86400)).toBe("1d 0h");
|
|
179
|
+
expect(formatDurationCompact(90000)).toBe("1d 1h");
|
|
180
|
+
expect(formatDurationCompact(172800)).toBe("2d 0h");
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it("formatUptime delegates to full formatDuration (seconds)", () => {
|
|
184
|
+
// formatUptime uses multi-part formatDuration (d/h/m/s)
|
|
185
|
+
expect(formatUptime(125)).toBe("2m 5s");
|
|
186
|
+
expect(formatUptime(3661)).toBe("1h 1m 1s");
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
describe("formatLatency", () => {
|
|
191
|
+
it("formats sub-millisecond latency", () => {
|
|
192
|
+
expect(formatLatency(0)).toBe("<1ms");
|
|
193
|
+
expect(formatLatency(0.5)).toBe("<1ms");
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it("formats millisecond latency", () => {
|
|
197
|
+
expect(formatLatency(1)).toBe("1ms");
|
|
198
|
+
expect(formatLatency(45)).toBe("45ms");
|
|
199
|
+
expect(formatLatency(500)).toBe("500ms");
|
|
200
|
+
expect(formatLatency(999)).toBe("999ms");
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it("formats second-level latency", () => {
|
|
204
|
+
expect(formatLatency(1000)).toBe("1.0s");
|
|
205
|
+
expect(formatLatency(2500)).toBe("2.5s");
|
|
206
|
+
expect(formatLatency(15000)).toBe("15.0s");
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it("handles non-finite", () => {
|
|
210
|
+
expect(formatLatency(NaN)).toBe("—");
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
describe("formatPercent", () => {
|
|
215
|
+
it("formats percentages with sign prefix", () => {
|
|
216
|
+
expect(formatPercent(50, 1)).toBe("+50.0%");
|
|
217
|
+
expect(formatPercent(99.9, 1)).toBe("+99.9%");
|
|
218
|
+
expect(formatPercent(0, 1)).toBe("+0.0%");
|
|
219
|
+
expect(formatPercent(-12.5, 1)).toBe("-12.5%");
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it("handles non-finite", () => {
|
|
223
|
+
expect(formatPercent(NaN)).toBe("—");
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
describe("formatRequests", () => {
|
|
228
|
+
it("formats via formatNumber", () => {
|
|
229
|
+
expect(formatRequests(0)).toBe((0).toLocaleString());
|
|
230
|
+
expect(formatRequests(42)).toBe((42).toLocaleString());
|
|
231
|
+
expect(formatRequests(999)).toBe((999).toLocaleString());
|
|
232
|
+
expect(formatRequests(1000)).toBe("1.0K");
|
|
233
|
+
expect(formatRequests(1500)).toBe("1.5K");
|
|
234
|
+
expect(formatRequests(1000000)).toBe("1.0M");
|
|
235
|
+
});
|
|
236
|
+
});
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2026 HOOX · HOOX · jango-blockchained (hoox-sh)
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, it, expect } from "bun:test";
|
|
7
|
+
import type { ViewId } from "@hoox-sh/hoox-shared";
|
|
8
|
+
import {
|
|
9
|
+
VIEW_REGISTRY,
|
|
10
|
+
REGISTERED_VIEW_IDS,
|
|
11
|
+
SIDEBAR_ITEMS,
|
|
12
|
+
getSidebarItems,
|
|
13
|
+
getViewShortcutMap,
|
|
14
|
+
getCtrlAltViewMap,
|
|
15
|
+
getViewPaletteCommands,
|
|
16
|
+
getViewFactory,
|
|
17
|
+
isRegisteredViewId,
|
|
18
|
+
ACTION_COMMANDS,
|
|
19
|
+
ALL_PALETTE_COMMANDS,
|
|
20
|
+
} from "./view-registry";
|
|
21
|
+
|
|
22
|
+
const ALL_VIEWS: ViewId[] = [
|
|
23
|
+
"dashboard",
|
|
24
|
+
"workers",
|
|
25
|
+
"worker-detail",
|
|
26
|
+
"trade-monitor",
|
|
27
|
+
"logs-viewer",
|
|
28
|
+
"service-manager",
|
|
29
|
+
"config-editor",
|
|
30
|
+
"setup-wizard",
|
|
31
|
+
"settings",
|
|
32
|
+
"queue-depth",
|
|
33
|
+
"kv-viewer",
|
|
34
|
+
"secrets-viewer",
|
|
35
|
+
"db-query",
|
|
36
|
+
"ai-chat",
|
|
37
|
+
"edge-topology",
|
|
38
|
+
"worker-settings",
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
describe("view-registry", () => {
|
|
42
|
+
it("covers every ViewId exactly once in order", () => {
|
|
43
|
+
expect(VIEW_REGISTRY.map((v) => v.id)).toEqual(ALL_VIEWS);
|
|
44
|
+
expect(REGISTERED_VIEW_IDS).toEqual(ALL_VIEWS);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("sidebar items match registry short labels", () => {
|
|
48
|
+
const items = getSidebarItems();
|
|
49
|
+
expect(items).toHaveLength(ALL_VIEWS.length);
|
|
50
|
+
expect(items).toEqual(SIDEBAR_ITEMS);
|
|
51
|
+
expect(items[0]).toEqual({
|
|
52
|
+
id: "dashboard",
|
|
53
|
+
label: "DASHBOARD",
|
|
54
|
+
shortcut: "1",
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("Ctrl digit shortcuts map 1-9 and 0", () => {
|
|
59
|
+
const map = getViewShortcutMap();
|
|
60
|
+
expect(map["1"]).toBe("dashboard");
|
|
61
|
+
expect(map["0"]).toBe("queue-depth");
|
|
62
|
+
expect(map["9"]).toBe("settings");
|
|
63
|
+
expect(Object.keys(map)).toHaveLength(10);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("Ctrl+Alt chords map k/s/c/q/e/w", () => {
|
|
67
|
+
const map = getCtrlAltViewMap();
|
|
68
|
+
expect(map.k).toBe("kv-viewer");
|
|
69
|
+
expect(map.s).toBe("secrets-viewer");
|
|
70
|
+
expect(map.c).toBe("ai-chat");
|
|
71
|
+
expect(map.q).toBe("db-query");
|
|
72
|
+
expect(map.e).toBe("edge-topology");
|
|
73
|
+
expect(map.w).toBe("worker-settings");
|
|
74
|
+
expect(Object.keys(map)).toHaveLength(6);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("ctrl and ctrl-alt key sets are unique", () => {
|
|
78
|
+
const ctrl = VIEW_REGISTRY.filter((e) => e.keyMod === "ctrl").map(
|
|
79
|
+
(e) => e.key
|
|
80
|
+
);
|
|
81
|
+
const ctrlAlt = VIEW_REGISTRY.filter((e) => e.keyMod === "ctrl-alt").map(
|
|
82
|
+
(e) => e.key
|
|
83
|
+
);
|
|
84
|
+
expect(new Set(ctrl).size).toBe(ctrl.length);
|
|
85
|
+
expect(new Set(ctrlAlt).size).toBe(ctrlAlt.length);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("palette view commands include all views", () => {
|
|
89
|
+
const cmds = getViewPaletteCommands();
|
|
90
|
+
expect(cmds.every((c) => c.category === "view")).toBe(true);
|
|
91
|
+
expect(new Set(cmds.map((c) => c.id))).toEqual(new Set(ALL_VIEWS));
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("every view has a factory", () => {
|
|
95
|
+
for (const id of ALL_VIEWS) {
|
|
96
|
+
expect(typeof getViewFactory(id)).toBe("function");
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("isRegisteredViewId accepts all registry ids and rejects junk", () => {
|
|
101
|
+
for (const id of ALL_VIEWS) {
|
|
102
|
+
expect(isRegisteredViewId(id)).toBe(true);
|
|
103
|
+
}
|
|
104
|
+
expect(isRegisteredViewId("not-a-view")).toBe(false);
|
|
105
|
+
expect(isRegisteredViewId(null)).toBe(false);
|
|
106
|
+
expect(isRegisteredViewId(42)).toBe(false);
|
|
107
|
+
expect(isRegisteredViewId("")).toBe(false);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("action commands include refresh, toggle-sidebar, quit", () => {
|
|
111
|
+
const ids = ACTION_COMMANDS.map((c) => c.id);
|
|
112
|
+
expect(ids).toContain("refresh");
|
|
113
|
+
expect(ids).toContain("toggle-sidebar");
|
|
114
|
+
expect(ids).toContain("quit");
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("ALL_PALETTE_COMMANDS merges views + actions without id collisions", () => {
|
|
118
|
+
expect(ALL_PALETTE_COMMANDS.length).toBe(
|
|
119
|
+
getViewPaletteCommands().length + ACTION_COMMANDS.length
|
|
120
|
+
);
|
|
121
|
+
const ids = ALL_PALETTE_COMMANDS.map((c) => c.id);
|
|
122
|
+
expect(new Set(ids).size).toBe(ids.length);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("action shortcuts do not collide with view Ctrl digits", () => {
|
|
126
|
+
// refresh=R, sidebar=B, quit=Q — none are digit view keys
|
|
127
|
+
const digitKeys = new Set(Object.keys(getViewShortcutMap()));
|
|
128
|
+
expect(digitKeys.has("r")).toBe(false);
|
|
129
|
+
expect(digitKeys.has("b")).toBe(false);
|
|
130
|
+
expect(digitKeys.has("q")).toBe(false);
|
|
131
|
+
});
|
|
132
|
+
});
|