@cueloop/client 0.1.0-alpha.8 → 0.1.0-alpha.81
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/native/build-pty.sh +51 -0
- package/native/build.sh +82 -0
- package/native/darwin-arm64/libcuelooppty.dylib +0 -0
- package/native/darwin-arm64/libcueloopvt.dylib +0 -0
- package/native/src/pty.zig +193 -0
- package/native/src/shim.zig +114 -0
- package/package.json +40 -32
- package/src/App.appearance.test.tsx +53 -0
- package/src/App.autoclose.test.tsx +77 -51
- package/src/App.diff.test.tsx +176 -45
- package/src/App.export.test.tsx +58 -41
- package/src/App.focus.test.ts +89 -0
- package/src/App.identity.test.tsx +78 -0
- package/src/App.plan-render.test.tsx +209 -0
- package/src/App.plan-review.test.tsx +428 -0
- package/src/App.prototype-markdown.test.tsx +92 -0
- package/src/App.prototype.test.tsx +176 -0
- package/src/App.readonly.test.tsx +129 -67
- package/src/App.regression.test.tsx +94 -0
- package/src/App.rendering.test.tsx +61 -35
- package/src/App.share-dialog.test.tsx +172 -0
- package/src/App.submit-confirm.test.tsx +222 -0
- package/src/App.test.tsx +333 -131
- package/src/App.toast.test.tsx +98 -0
- package/src/App.tsx +1338 -608
- package/src/App.visual.test.tsx +57 -0
- package/src/App.walk.test.tsx +299 -0
- package/src/App.workbench.test.tsx +701 -0
- package/src/__snapshots__/view-plan.regression.test.ts.snap +100 -0
- package/src/annotation-palette.ts +62 -0
- package/src/ansi-screen.test.ts +28 -0
- package/src/ansi-screen.ts +72 -0
- package/src/app-key-state.ts +61 -0
- package/src/app-screens.tsx +574 -0
- package/src/app-view-model.ts +162 -0
- package/src/attribution.test.ts +31 -0
- package/src/attribution.ts +42 -0
- package/src/clipboard.test.ts +23 -0
- package/src/clipboard.ts +33 -0
- package/src/components/AnnotatableFileView.stories.tsx +42 -0
- package/src/components/AnnotatableFileView.test.tsx +102 -0
- package/src/components/AnnotatableFileView.tsx +134 -0
- package/src/components/AnnotationCards.stories.tsx +86 -0
- package/src/components/AnnotationCards.tsx +341 -0
- package/src/components/AnsiScreen.stories.tsx +12 -0
- package/src/components/AnsiScreen.tsx +28 -0
- package/src/components/AppHeader.stories.tsx +50 -0
- package/src/components/AppHeader.tsx +83 -0
- package/src/components/AppShell.brand.test.tsx +58 -0
- package/src/components/AppShell.stories.tsx +89 -0
- package/src/components/AppShell.tsx +329 -0
- package/src/components/AppShell.zoom.test.tsx +135 -0
- package/src/components/BareWorkbenchFileView.stories.tsx +27 -0
- package/src/components/BareWorkbenchFileView.tsx +64 -0
- package/src/components/Breadcrumb.stories.tsx +19 -0
- package/src/components/Breadcrumb.tsx +45 -0
- package/src/components/ChangesColumn.stories.tsx +44 -0
- package/src/components/ChangesColumn.tsx +199 -0
- package/src/components/ChangesFileTree.test.tsx +61 -0
- package/src/components/CodeBlock.stories.tsx +42 -0
- package/src/components/CodeBlock.tsx +84 -0
- package/src/components/CompletionOverlay.stories.tsx +37 -0
- package/src/components/CompletionOverlay.tsx +77 -0
- package/src/components/ConfirmCard.stories.tsx +37 -0
- package/src/components/ConfirmCard.tsx +153 -0
- package/src/components/ConfirmDialog.stories.tsx +21 -0
- package/src/components/ConfirmDialog.tsx +72 -0
- package/src/components/DiffContentView.scroll.test.tsx +121 -0
- package/src/components/DiffContentView.split.test.tsx +140 -0
- package/src/components/DiffContentView.stories.tsx +80 -0
- package/src/components/DiffContentView.syntax.test.tsx +82 -0
- package/src/components/DiffContentView.tsx +876 -0
- package/src/components/DiscussionMarkerRail.stories.tsx +54 -0
- package/src/components/DiscussionMarkerRail.tsx +186 -0
- package/src/components/EditorGrid.stories.tsx +59 -0
- package/src/components/EditorGrid.tabs.test.tsx +118 -0
- package/src/components/EditorGrid.tsx +426 -0
- package/src/components/EditorGrid.zoom.test.tsx +68 -0
- package/src/components/FileContentsView.stories.tsx +16 -0
- package/src/components/FileContentsView.test.tsx +29 -0
- package/src/components/FileContentsView.tsx +98 -0
- package/src/components/GridTabContent.stories.tsx +59 -0
- package/src/components/GridTabContent.tsx +182 -0
- package/src/components/HandleChipsInput.stories.tsx +24 -0
- package/src/components/HandleChipsInput.test.tsx +35 -0
- package/src/components/HandleChipsInput.tsx +145 -0
- package/src/components/KeybindsDialog.stories.tsx +31 -0
- package/src/components/KeybindsDialog.tsx +103 -0
- package/src/components/MarkdownGridBlock.stories.tsx +33 -0
- package/src/components/MarkdownGridBlock.tsx +72 -0
- package/src/components/MarkdownThreadEditor.stories.tsx +15 -0
- package/src/components/MarkdownThreadEditor.test.tsx +98 -0
- package/src/components/MarkdownThreadEditor.tsx +162 -0
- package/src/components/MenuBar.stories.tsx +28 -0
- package/src/components/MenuBar.tsx +44 -0
- package/src/components/NavModeHint.stories.tsx +40 -0
- package/src/components/NavModeHint.tsx +31 -0
- package/src/components/OverlayScrollbar.stories.tsx +33 -0
- package/src/components/OverlayScrollbar.test.tsx +139 -0
- package/src/components/OverlayScrollbar.tsx +129 -0
- package/src/components/PaneGrid.stories.tsx +84 -0
- package/src/components/PaneGrid.tsx +128 -0
- package/src/components/PanelColumn.stories.tsx +25 -0
- package/src/components/PanelColumn.tsx +98 -0
- package/src/components/ProjectTreeView.stories.tsx +25 -0
- package/src/components/ProjectTreeView.test.tsx +59 -0
- package/src/components/ProjectTreeView.tsx +111 -0
- package/src/components/PromptDialog.stories.tsx +36 -0
- package/src/components/PromptDialog.test.tsx +61 -0
- package/src/components/PromptDialog.tsx +99 -0
- package/src/components/PrototypeContentView.stories.tsx +19 -0
- package/src/components/PrototypeContentView.tsx +446 -0
- package/src/components/RootOverlay.stories.tsx +45 -0
- package/src/components/RootOverlay.tsx +48 -0
- package/src/components/ScrollArea.stories.tsx +20 -0
- package/src/components/ScrollArea.tsx +44 -0
- package/src/components/SettingsDialog.stories.tsx +117 -0
- package/src/components/SettingsDialog.tsx +186 -0
- package/src/components/SettingsRows.stories.tsx +18 -0
- package/src/components/SettingsRows.tsx +112 -0
- package/src/components/ShareDialog.stories.tsx +121 -0
- package/src/components/ShareDialog.tsx +552 -0
- package/src/components/ShellHeader.stories.tsx +50 -0
- package/src/components/ShellHeader.tsx +137 -0
- package/src/components/SlashComposer.stories.tsx +37 -0
- package/src/components/SlashComposer.tsx +87 -0
- package/src/components/ThreadFooter.stories.tsx +30 -0
- package/src/components/ThreadFooter.test.tsx +47 -0
- package/src/components/ThreadFooter.tsx +76 -0
- package/src/components/ThreadTree.stories.tsx +46 -0
- package/src/components/ThreadTree.tsx +255 -0
- package/src/components/ThreadView.selection.test.tsx +899 -0
- package/src/components/ThreadView.slash.test.tsx +146 -0
- package/src/components/ThreadView.stories.tsx +26 -0
- package/src/components/ThreadView.tsx +472 -0
- package/src/components/Toast.stories.tsx +19 -0
- package/src/components/Toast.tsx +52 -0
- package/src/components/Tooltip.stories.tsx +26 -0
- package/src/components/Tooltip.tsx +72 -0
- package/src/components/TreePane.stories.tsx +106 -0
- package/src/components/TreePane.tsx +157 -0
- package/src/components/WalkWizard.stories.tsx +100 -0
- package/src/components/WalkWizard.tsx +150 -0
- package/src/components/WelcomePlayground.stories.tsx +12 -0
- package/src/components/WelcomePlayground.test.tsx +47 -0
- package/src/components/WelcomePlayground.tsx +142 -0
- package/src/components/__snapshots__/stories.test.tsx.snap +2238 -0
- package/src/components/diff-content-view-layout.test.ts +198 -0
- package/src/components/diff-content-view-layout.ts +84 -0
- package/src/components/editor-grid.test.ts +86 -0
- package/src/components/editor-grid.ts +138 -0
- package/src/components/file-tree.test.ts +60 -0
- package/src/components/file-tree.ts +112 -0
- package/src/components/menu-control.stories.tsx +35 -0
- package/src/components/menu-control.tsx +43 -0
- package/src/components/pages/Workspace.stories.tsx +181 -0
- package/src/components/primitives/Button.stories.tsx +32 -0
- package/src/components/primitives/Button.tsx +56 -0
- package/src/components/primitives/Card.stories.tsx +25 -0
- package/src/components/primitives/Card.tsx +63 -0
- package/src/components/primitives/Dialog.stories.tsx +24 -0
- package/src/components/primitives/Dialog.test.tsx +37 -0
- package/src/components/primitives/Dialog.tsx +71 -0
- package/src/components/primitives/DialogActions.stories.tsx +20 -0
- package/src/components/primitives/DialogActions.tsx +39 -0
- package/src/components/primitives/IconButton.stories.tsx +36 -0
- package/src/components/primitives/IconButton.tsx +62 -0
- package/src/components/primitives/StatusBar.stories.tsx +11 -0
- package/src/components/primitives/StatusBar.tsx +23 -0
- package/src/components/primitives/Switch.stories.tsx +17 -0
- package/src/components/primitives/Switch.tsx +32 -0
- package/src/components/primitives/Tabs.stories.tsx +29 -0
- package/src/components/primitives/Tabs.tsx +102 -0
- package/src/components/primitives/Toolbar.stories.tsx +17 -0
- package/src/components/primitives/Toolbar.tsx +14 -0
- package/src/components/primitives/Tree.stories.tsx +124 -0
- package/src/components/primitives/Tree.tsx +100 -0
- package/src/components/primitives/frame.test.ts +12 -0
- package/src/components/primitives/frame.ts +17 -0
- package/src/components/primitives/icons.ts +61 -0
- package/src/components/primitives/tree-model.test.ts +99 -0
- package/src/components/primitives/tree-model.ts +137 -0
- package/src/components/quick-actions-editor.stories.tsx +58 -0
- package/src/components/quick-actions-editor.test.tsx +117 -0
- package/src/components/quick-actions-editor.tsx +226 -0
- package/src/components/share-dialog-store.ts +58 -0
- package/src/components/stories-app.tsx +193 -0
- package/src/components/stories.test.tsx +74 -0
- package/src/components/story-fixtures.ts +102 -0
- package/src/components/story-tree.test.ts +108 -0
- package/src/components/story.ts +159 -0
- package/src/components/syntax-highlight.ts +133 -0
- package/src/components/terminal-pane.test.tsx +40 -0
- package/src/components/terminal-pane.ts +199 -0
- package/src/components/theme-context.stories.tsx +46 -0
- package/src/components/theme-context.tsx +37 -0
- package/src/components/thread-tree.test.ts +134 -0
- package/src/components/thread-tree.ts +109 -0
- package/src/components/truncate-text.ts +7 -0
- package/src/components/truncate-title.test.ts +31 -0
- package/src/components/truncate-title.ts +15 -0
- package/src/config-identity.test.ts +78 -0
- package/src/config.test.ts +517 -33
- package/src/config.ts +467 -57
- package/src/diff-fold.test.ts +78 -0
- package/src/diff-fold.ts +126 -0
- package/src/diff-hunk-curate.test.ts +315 -0
- package/src/diff-hunk-curate.ts +153 -0
- package/src/diff-intraline.test.ts +238 -0
- package/src/diff-intraline.ts +236 -0
- package/src/diff-syntax.test.ts +112 -0
- package/src/diff-syntax.ts +179 -0
- package/src/discussions.ts +79 -0
- package/src/editor.test.ts +149 -0
- package/src/editor.ts +139 -9
- package/src/ghostty-terminal.test.ts +85 -0
- package/src/ghostty-terminal.ts +199 -0
- package/src/github-identity.test.ts +58 -0
- package/src/github-identity.ts +41 -0
- package/src/herdr.test.ts +9 -34
- package/src/herdr.ts +25 -21
- package/src/index.ts +13 -0
- package/src/integrations.test.ts +41 -0
- package/src/integrations.ts +32 -0
- package/src/intent-dispatch.test.ts +723 -0
- package/src/intent-dispatch.ts +692 -0
- package/src/key-bindings.test.ts +156 -0
- package/src/key-bindings.ts +459 -0
- package/src/keymap.test.ts +461 -113
- package/src/keymap.ts +321 -79
- package/src/kitty-image.test.ts +66 -0
- package/src/kitty-image.ts +138 -0
- package/src/launch-layout.test.ts +52 -0
- package/src/launch-layout.ts +42 -0
- package/src/mark-runs.ts +121 -0
- package/src/markdown-editor-syntax-style.ts +50 -0
- package/src/markdown-grid.test.ts +101 -0
- package/src/markdown-grid.ts +203 -0
- package/src/markdown-highlight.test.ts +61 -0
- package/src/markdown-highlight.ts +122 -0
- package/src/pasted-image.test.ts +67 -0
- package/src/pasted-image.ts +60 -0
- package/src/perf/otlp-trace.test.ts +116 -0
- package/src/perf/otlp-trace.ts +130 -0
- package/src/perf/perf-report.ts +17 -0
- package/src/perf/perf-timings.test.ts +18 -0
- package/src/perf/perf-timings.ts +38 -0
- package/src/prototype-browser.test.ts +64 -0
- package/src/prototype-browser.ts +329 -0
- package/src/prototype-pixels.tsx +35 -0
- package/src/pty.test.ts +81 -0
- package/src/pty.ts +245 -0
- package/src/ready-signal.test.ts +85 -0
- package/src/ready-signal.ts +52 -0
- package/src/run.ts +78 -13
- package/src/serve.test.ts +24 -12
- package/src/serve.ts +52 -10
- package/src/share.ts +237 -0
- package/src/skills.test.ts +74 -0
- package/src/skills.ts +75 -0
- package/src/slash-palette.test.ts +103 -0
- package/src/slash-palette.ts +129 -0
- package/src/split-diff.test.ts +110 -0
- package/src/split-diff.ts +149 -0
- package/src/test-support.test.ts +264 -0
- package/src/test-support.ts +369 -0
- package/src/theme-presets.test.ts +51 -0
- package/src/theme-presets.ts +214 -0
- package/src/theme.ts +84 -19
- package/src/thread-chords.test.ts +79 -0
- package/src/thread-chords.ts +163 -0
- package/src/thread-controller.curate.test.ts +450 -0
- package/src/thread-controller.derivation.test.ts +62 -0
- package/src/thread-controller.live-diff.test.ts +322 -0
- package/src/thread-controller.optimistic.test.ts +69 -0
- package/src/thread-controller.share.test.ts +561 -0
- package/src/thread-controller.switch-cache.test.ts +176 -0
- package/src/thread-controller.tree.test.ts +261 -0
- package/src/thread-controller.ts +1893 -0
- package/src/thread-selection.test.ts +210 -0
- package/src/thread-selection.ts +243 -0
- package/src/tree-view.test.ts +128 -0
- package/src/tree-view.ts +148 -0
- package/src/use-annotation-surface.tsx +915 -0
- package/src/use-changes-workbench.ts +190 -0
- package/src/use-frame-measure.ts +41 -0
- package/src/use-remember-layout.ts +31 -0
- package/src/use-settings-dialog.tsx +320 -0
- package/src/use-terminal-virtualizer.ts +233 -0
- package/src/use-thread-body-editing.tsx +58 -0
- package/src/version.ts +5 -0
- package/src/view-diff.test.ts +137 -9
- package/src/view-diff.ts +269 -23
- package/src/view-plan.regression.test.ts +60 -0
- package/src/view-plan.test.ts +456 -0
- package/src/view-plan.ts +719 -0
- package/src/visual/contrast.test.ts +35 -0
- package/src/visual/contrast.ts +33 -0
- package/src/walk.test.ts +121 -0
- package/src/walk.ts +74 -0
- package/src/workbench-snapshot.test.ts +81 -0
- package/src/workbench-snapshot.ts +29 -0
- package/src/session-controller.ts +0 -345
- package/src/syntax.test.ts +0 -41
- package/src/syntax.ts +0 -88
- package/src/view.test.ts +0 -157
- package/src/view.ts +0 -312
package/src/keymap.test.ts
CHANGED
|
@@ -1,8 +1,4 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The keyboard grammar as data: every mode x key produces a known intent
|
|
3
|
-
* list, the read-only rule is one gate, and plan and diff reviews share one
|
|
4
|
-
* path for annotation navigation, deletion, and submit.
|
|
5
|
-
*/
|
|
1
|
+
/** The keyboard grammar as data: every mode x key maps to a known intent list, gated once by the read-only rule. */
|
|
6
2
|
|
|
7
3
|
import { describe, expect, test } from "bun:test";
|
|
8
4
|
import { DEFAULT_KEYS } from "./config";
|
|
@@ -20,217 +16,512 @@ function state(patch: Partial<KeyState> = {}): KeyState {
|
|
|
20
16
|
annotationCount: 1,
|
|
21
17
|
hasFocusedAnnotation: true,
|
|
22
18
|
cursorAnnotatable: true,
|
|
19
|
+
walkAtEnd: false,
|
|
23
20
|
...patch,
|
|
24
21
|
};
|
|
25
22
|
}
|
|
26
23
|
|
|
27
|
-
function key(name: string, shift = false
|
|
28
|
-
return { name, shift };
|
|
24
|
+
function key(name: string, shift = false, meta = false) {
|
|
25
|
+
return { name, shift, meta };
|
|
29
26
|
}
|
|
30
27
|
|
|
31
28
|
describe("compose overlay", () => {
|
|
32
|
-
|
|
29
|
+
// Arrange
|
|
30
|
+
const keyState = state({ overlay: "compose" });
|
|
33
31
|
const table: [string, Intent[]][] = [
|
|
34
|
-
["escape", [{
|
|
35
|
-
["return", [{
|
|
36
|
-
["enter", [{
|
|
32
|
+
["escape", [{ type: "closeOverlay" }]],
|
|
33
|
+
["return", [{ type: "saveCompose" }]],
|
|
34
|
+
["enter", [{ type: "saveCompose" }]],
|
|
37
35
|
["left", []],
|
|
38
36
|
["right", []],
|
|
39
37
|
["j", []],
|
|
40
38
|
["q", []],
|
|
41
39
|
];
|
|
40
|
+
|
|
42
41
|
for (const [name, expected] of table) {
|
|
43
42
|
test(`${name} -> ${JSON.stringify(expected)}`, () => {
|
|
44
|
-
expect(reduceKey(
|
|
43
|
+
expect(reduceKey(keyState, key(name))).toEqual(expected);
|
|
45
44
|
});
|
|
46
45
|
}
|
|
46
|
+
|
|
47
|
+
// ⌥/Alt+⏎ (meta) and shift+⏎ insert a newline in the composer, so the
|
|
48
|
+
// grammar must not save on them; only a bare ⏎ saves.
|
|
49
|
+
test("option/alt+return (meta) does not save - the textarea inserts a newline", () => {
|
|
50
|
+
expect(reduceKey(keyState, key("return", false, true))).toEqual([]);
|
|
51
|
+
});
|
|
52
|
+
test("shift+return does not save - the textarea inserts a newline", () => {
|
|
53
|
+
expect(reduceKey(keyState, key("return", true, false))).toEqual([]);
|
|
54
|
+
});
|
|
47
55
|
});
|
|
48
56
|
|
|
49
57
|
describe("submit overlay", () => {
|
|
50
|
-
|
|
58
|
+
// Arrange
|
|
59
|
+
const keyState = state({ overlay: "submit" });
|
|
51
60
|
const table: [string, Intent[]][] = [
|
|
52
|
-
["escape", [{
|
|
53
|
-
["return", [
|
|
54
|
-
["enter", [
|
|
55
|
-
["left", [{
|
|
56
|
-
["right", [{
|
|
61
|
+
["escape", [{ type: "closeOverlay" }]],
|
|
62
|
+
["return", []],
|
|
63
|
+
["enter", []],
|
|
64
|
+
["left", [{ type: "cycleMessage", direction: -1 }]],
|
|
65
|
+
["right", [{ type: "cycleMessage", direction: 1 }]],
|
|
57
66
|
["j", []],
|
|
58
67
|
["q", []],
|
|
59
68
|
];
|
|
69
|
+
|
|
60
70
|
for (const [name, expected] of table) {
|
|
61
71
|
test(`${name} -> ${JSON.stringify(expected)}`, () => {
|
|
62
|
-
expect(reduceKey(
|
|
72
|
+
expect(reduceKey(keyState, key(name))).toEqual(expected);
|
|
63
73
|
});
|
|
64
74
|
}
|
|
65
75
|
});
|
|
66
76
|
|
|
67
77
|
describe("completion overlay", () => {
|
|
78
|
+
// Arrange
|
|
68
79
|
const prompt = state({ overlay: "completion-prompt" });
|
|
69
80
|
const counting = state({ overlay: "completion-counting" });
|
|
70
81
|
const table: [KeyState, string, Intent[]][] = [
|
|
71
|
-
[prompt, "return", [{
|
|
72
|
-
[prompt, "enter", [{
|
|
73
|
-
[prompt, "q", [{
|
|
74
|
-
[prompt, "a", [{
|
|
75
|
-
[prompt, "escape", [{
|
|
82
|
+
[prompt, "return", [{ type: "finishReview" }]],
|
|
83
|
+
[prompt, "enter", [{ type: "finishReview" }]],
|
|
84
|
+
[prompt, "q", [{ type: "finishReview" }]],
|
|
85
|
+
[prompt, "a", [{ type: "optInAutoClose" }]],
|
|
86
|
+
[prompt, "escape", [{ type: "dismissCompletion" }]],
|
|
76
87
|
[prompt, "j", []],
|
|
77
|
-
[counting, "return", [{
|
|
78
|
-
[counting, "q", [{
|
|
79
|
-
[counting, "a", []],
|
|
80
|
-
[counting, "escape", [{
|
|
88
|
+
[counting, "return", [{ type: "finishReview" }]],
|
|
89
|
+
[counting, "q", [{ type: "finishReview" }]],
|
|
90
|
+
[counting, "a", [{ type: "optInAutoClose" }]],
|
|
91
|
+
[counting, "escape", [{ type: "dismissCompletion" }]],
|
|
81
92
|
];
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
93
|
+
|
|
94
|
+
for (const [overlayState, name, expected] of table) {
|
|
95
|
+
test(`${overlayState.overlay} ${name} -> ${JSON.stringify(expected)}`, () => {
|
|
96
|
+
expect(reduceKey(overlayState, key(name))).toEqual(expected);
|
|
85
97
|
});
|
|
86
98
|
}
|
|
87
99
|
});
|
|
88
100
|
|
|
89
101
|
describe("inbox mode", () => {
|
|
90
|
-
|
|
102
|
+
// Arrange
|
|
103
|
+
const keyState = state({ view: "inbox" });
|
|
91
104
|
const table: [string, Intent[]][] = [
|
|
92
|
-
["j", [{
|
|
93
|
-
["down", [{
|
|
94
|
-
["k", [{
|
|
95
|
-
["up", [{
|
|
96
|
-
["return", [{
|
|
97
|
-
["enter", [{
|
|
98
|
-
["q", [{
|
|
105
|
+
["j", [{ type: "inboxMove", to: "down" }]],
|
|
106
|
+
["down", [{ type: "inboxMove", to: "down" }]],
|
|
107
|
+
["k", [{ type: "inboxMove", to: "up" }]],
|
|
108
|
+
["up", [{ type: "inboxMove", to: "up" }]],
|
|
109
|
+
["return", [{ type: "openSession" }]],
|
|
110
|
+
["enter", [{ type: "openSession" }]],
|
|
111
|
+
["q", [{ type: "exit" }]],
|
|
99
112
|
["c", []],
|
|
100
113
|
["escape", []],
|
|
101
114
|
];
|
|
115
|
+
|
|
102
116
|
for (const [name, expected] of table) {
|
|
103
117
|
test(`${name} -> ${JSON.stringify(expected)}`, () => {
|
|
104
|
-
expect(reduceKey(
|
|
118
|
+
expect(reduceKey(keyState, key(name))).toEqual(expected);
|
|
105
119
|
});
|
|
106
120
|
}
|
|
107
121
|
|
|
108
122
|
test("an empty inbox swallows every non-quit key", () => {
|
|
123
|
+
// Arrange
|
|
109
124
|
const empty = state({ view: "inbox", hasInboxItems: false });
|
|
125
|
+
|
|
126
|
+
// Assert
|
|
110
127
|
for (const name of ["j", "k", "down", "up", "return", "enter"]) {
|
|
111
128
|
expect(reduceKey(empty, key(name))).toEqual([]);
|
|
112
129
|
}
|
|
113
|
-
expect(reduceKey(empty, key("q"))).toEqual([{
|
|
130
|
+
expect(reduceKey(empty, key("q"))).toEqual([{ type: "exit" }]);
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
describe("collaborator (share) capabilities", () => {
|
|
135
|
+
test("can annotate: comment opens the composer", () => {
|
|
136
|
+
// Arrange
|
|
137
|
+
const collab = state({ canEditPlan: false, canSubmitMessage: false });
|
|
138
|
+
|
|
139
|
+
// Act / Assert
|
|
140
|
+
expect(reduceKey(collab, key("c"), "comment")).toEqual([
|
|
141
|
+
{ type: "openCompose", kind: "comment", from: "cursor" },
|
|
142
|
+
]);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test("can edit their own card: edit with a focused annotation rewrites it", () => {
|
|
146
|
+
// Arrange
|
|
147
|
+
const collab = state({ canEditPlan: false, hasFocusedAnnotation: true });
|
|
148
|
+
|
|
149
|
+
// Act / Assert
|
|
150
|
+
expect(reduceKey(collab, key("e"), "edit")).toEqual([{ type: "editCard" }]);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test("cannot edit the plan: cut/edit with no card is silent (no nag, no button)", () => {
|
|
154
|
+
// Arrange
|
|
155
|
+
const collab = state({ canEditPlan: false, hasFocusedAnnotation: false });
|
|
156
|
+
|
|
157
|
+
// Act / Assert
|
|
158
|
+
expect(reduceKey(collab, key("e"), "edit")).toEqual([]);
|
|
159
|
+
expect(reduceKey(collab, key("c"), "cut")).toEqual([]);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test("cannot submit a message: there is no agent on a share", () => {
|
|
163
|
+
// Arrange
|
|
164
|
+
const collab = state({ canSubmitMessage: false });
|
|
165
|
+
|
|
166
|
+
// Act / Assert
|
|
167
|
+
expect(reduceKey(collab, key("x"), "submit")).toEqual([
|
|
168
|
+
{ type: "status", message: "shared view - your notes save as you go; q to leave" },
|
|
169
|
+
]);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test("cannot re-share: only the owner shares", () => {
|
|
173
|
+
// Arrange
|
|
174
|
+
const collab = state({ canShare: false });
|
|
175
|
+
|
|
176
|
+
// Act / Assert
|
|
177
|
+
expect(reduceKey(collab, key("S", true), "share")).toEqual([
|
|
178
|
+
{ type: "status", message: "only the plan owner can share" },
|
|
179
|
+
]);
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
describe("share", () => {
|
|
184
|
+
test("the owner's share key publishes", () => {
|
|
185
|
+
// Act / Assert
|
|
186
|
+
expect(reduceKey(state(), key("S", true), "share")).toEqual([{ type: "share" }]);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test("an observer cannot share", () => {
|
|
190
|
+
// Act / Assert
|
|
191
|
+
expect(reduceKey(state({ readOnly: true }), key("S", true), "share")).toEqual([
|
|
192
|
+
{ type: "status", message: "observer - read-only" },
|
|
193
|
+
]);
|
|
114
194
|
});
|
|
115
195
|
});
|
|
116
196
|
|
|
117
197
|
describe("plan normal mode", () => {
|
|
118
|
-
|
|
198
|
+
// Arrange
|
|
199
|
+
const keyState = state();
|
|
119
200
|
const table: [string, boolean, Intent[]][] = [
|
|
120
|
-
["j", false, [{
|
|
121
|
-
["down", false, [{
|
|
122
|
-
["k", false, [{
|
|
123
|
-
["up", false, [{
|
|
124
|
-
["g", false, [{
|
|
125
|
-
["g", true, [{
|
|
126
|
-
["v", false, [{
|
|
127
|
-
["c", false, [{
|
|
128
|
-
|
|
129
|
-
["x", false, [{
|
|
130
|
-
["e", false, [{
|
|
131
|
-
["n", false, [{
|
|
132
|
-
["p", false, [{
|
|
133
|
-
["backspace", false, [{
|
|
134
|
-
["return", false, [{
|
|
135
|
-
["q", false, [{
|
|
136
|
-
["escape", false, []],
|
|
201
|
+
["j", false, [{ type: "move", to: "down" }]],
|
|
202
|
+
["down", false, [{ type: "move", to: "down" }]],
|
|
203
|
+
["k", false, [{ type: "move", to: "up" }]],
|
|
204
|
+
["up", false, [{ type: "move", to: "up" }]],
|
|
205
|
+
["g", false, [{ type: "move", to: "top" }]],
|
|
206
|
+
["g", true, [{ type: "move", to: "bottom" }]],
|
|
207
|
+
["v", false, [{ type: "startSpan" }]],
|
|
208
|
+
["c", false, [{ type: "openCompose", kind: "comment", from: "cursor" }]],
|
|
209
|
+
// the default state has a selected card, so Cut and edit act on the card
|
|
210
|
+
["x", false, [{ type: "removeAnnotation" }]],
|
|
211
|
+
["e", false, [{ type: "editCard" }]],
|
|
212
|
+
["n", false, [{ type: "nextAnnotation" }]],
|
|
213
|
+
["p", false, [{ type: "prevAnnotation" }]],
|
|
214
|
+
["backspace", false, [{ type: "removeAnnotation" }]],
|
|
215
|
+
["return", false, [{ type: "openSubmit" }]],
|
|
216
|
+
["q", false, [{ type: "exit" }]],
|
|
217
|
+
["escape", false, [{ type: "deselect" }]],
|
|
137
218
|
["z", false, []],
|
|
138
219
|
];
|
|
220
|
+
|
|
139
221
|
for (const [name, shift, expected] of table) {
|
|
140
222
|
test(`${shift ? "shift+" : ""}${name} -> ${JSON.stringify(expected)}`, () => {
|
|
141
|
-
expect(reduceKey(
|
|
223
|
+
expect(reduceKey(keyState, key(name, shift))).toEqual(expected);
|
|
142
224
|
});
|
|
143
225
|
}
|
|
144
226
|
|
|
145
|
-
test("resolved sessions guard the mutating
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
227
|
+
test("resolved sessions guard the mutating primitives", () => {
|
|
228
|
+
// Arrange
|
|
229
|
+
const resolvedState = state({ resolved: true });
|
|
230
|
+
|
|
231
|
+
// Assert
|
|
232
|
+
for (const name of ["c", "x", "e"]) {
|
|
233
|
+
expect(reduceKey(resolvedState, key(name))).toEqual([
|
|
234
|
+
{ type: "status", message: "review submitted - read-only" },
|
|
235
|
+
]);
|
|
149
236
|
}
|
|
150
|
-
expect(reduceKey(
|
|
151
|
-
expect(reduceKey(
|
|
237
|
+
expect(reduceKey(resolvedState, key("return"))).toEqual([]);
|
|
238
|
+
expect(reduceKey(resolvedState, key("backspace"))).toEqual([]);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
test("u restores a rail removal (a cut block); a share viewer's is silent", () => {
|
|
242
|
+
// Assert - the undo intent leaves the target to the dispatcher
|
|
243
|
+
expect(reduceKey(state(), key("u"))).toEqual([{ type: "restoreCuration" }]);
|
|
244
|
+
expect(reduceKey(state({ resolved: true }), key("u"))).toEqual([
|
|
245
|
+
{ type: "status", message: "review submitted - read-only" },
|
|
246
|
+
]);
|
|
247
|
+
expect(reduceKey(state({ canEditPlan: false }), key("u"))).toEqual([]);
|
|
248
|
+
expect(reduceKey(state({ readOnly: true }), key("u"))).toEqual([
|
|
249
|
+
{ type: "status", message: "observer - read-only" },
|
|
250
|
+
]);
|
|
152
251
|
});
|
|
153
252
|
|
|
154
253
|
test("cut text cannot host a comment or a span", () => {
|
|
254
|
+
// Arrange
|
|
155
255
|
const cut = state({ cursorAnnotatable: false });
|
|
156
|
-
|
|
157
|
-
|
|
256
|
+
|
|
257
|
+
// Assert
|
|
258
|
+
expect(reduceKey(cut, key("c"))).toEqual([
|
|
259
|
+
{ type: "status", message: "text is cut - restore it first" },
|
|
260
|
+
]);
|
|
158
261
|
expect(reduceKey(cut, key("v"))).toEqual([]);
|
|
159
262
|
});
|
|
160
263
|
|
|
161
264
|
test("annotation navigation without annotations reports, delete without focus is silent", () => {
|
|
265
|
+
// Arrange
|
|
162
266
|
const none = state({ annotationCount: 0, hasFocusedAnnotation: false });
|
|
163
|
-
|
|
164
|
-
|
|
267
|
+
|
|
268
|
+
// Assert
|
|
269
|
+
expect(reduceKey(none, key("n"))).toEqual([{ type: "status", message: "no annotations" }]);
|
|
270
|
+
expect(reduceKey(none, key("p"))).toEqual([{ type: "status", message: "no annotations" }]);
|
|
165
271
|
expect(reduceKey(none, key("backspace"))).toEqual([]);
|
|
166
272
|
});
|
|
273
|
+
|
|
274
|
+
test("without a selected card x cuts the block and e opens the editor", () => {
|
|
275
|
+
// Arrange
|
|
276
|
+
const unfocused = state({ hasFocusedAnnotation: false });
|
|
277
|
+
|
|
278
|
+
// Assert
|
|
279
|
+
expect(reduceKey(unfocused, key("x"))).toEqual([{ type: "cut" }]);
|
|
280
|
+
expect(reduceKey(unfocused, key("e"))).toEqual([{ type: "edit" }]);
|
|
281
|
+
});
|
|
167
282
|
});
|
|
168
283
|
|
|
169
284
|
describe("span mode", () => {
|
|
170
|
-
|
|
285
|
+
// Arrange
|
|
286
|
+
const keyState = state({ spanMode: true });
|
|
171
287
|
const table: [string, Intent[]][] = [
|
|
172
|
-
["l", [{
|
|
173
|
-
["h", [{
|
|
174
|
-
["w", [{
|
|
175
|
-
["b", [{
|
|
176
|
-
["$", [{
|
|
177
|
-
["0", [{
|
|
178
|
-
["c", [{
|
|
179
|
-
["
|
|
180
|
-
["
|
|
181
|
-
["
|
|
288
|
+
["l", [{ type: "spanKey", name: "l" }]],
|
|
289
|
+
["h", [{ type: "spanKey", name: "h" }]],
|
|
290
|
+
["w", [{ type: "spanKey", name: "w" }]],
|
|
291
|
+
["b", [{ type: "spanKey", name: "b" }]],
|
|
292
|
+
["$", [{ type: "spanKey", name: "$" }]],
|
|
293
|
+
["0", [{ type: "spanKey", name: "0" }]],
|
|
294
|
+
["c", [{ type: "openCompose", kind: "comment", from: "span" }]],
|
|
295
|
+
["x", [{ type: "spanCut" }]],
|
|
296
|
+
["a", [{ type: "openSpanActions" }]],
|
|
297
|
+
["escape", [{ type: "closeOverlay" }]],
|
|
298
|
+
["q", [{ type: "exit" }]],
|
|
182
299
|
["j", []],
|
|
183
300
|
];
|
|
301
|
+
|
|
302
|
+
for (const [name, expected] of table) {
|
|
303
|
+
test(`${name} -> ${JSON.stringify(expected)}`, () => {
|
|
304
|
+
expect(reduceKey(keyState, key(name))).toEqual(expected);
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
test("an observer's span-mode mutations answer read-only", () => {
|
|
309
|
+
// Arrange
|
|
310
|
+
const observer = state({ spanMode: true, readOnly: true });
|
|
311
|
+
|
|
312
|
+
// Assert
|
|
313
|
+
expect(reduceKey(observer, key("x"))).toEqual([
|
|
314
|
+
{ type: "status", message: "observer - read-only" },
|
|
315
|
+
]);
|
|
316
|
+
expect(reduceKey(observer, key("a"))).toEqual([
|
|
317
|
+
{ type: "status", message: "observer - read-only" },
|
|
318
|
+
]);
|
|
319
|
+
expect(reduceKey(observer, key("c"))).toEqual([
|
|
320
|
+
{ type: "status", message: "observer - read-only" },
|
|
321
|
+
]);
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
test("span-mode cut is owner-only, like the normal plan cut", () => {
|
|
325
|
+
// Arrange
|
|
326
|
+
const collaborator = state({ spanMode: true, canEditPlan: false });
|
|
327
|
+
|
|
328
|
+
// Assert
|
|
329
|
+
expect(reduceKey(collaborator, key("x"))).toEqual([]);
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
test("all span-mode mutations are blocked once the review is resolved", () => {
|
|
333
|
+
// Arrange
|
|
334
|
+
const resolved = state({ spanMode: true, resolved: true });
|
|
335
|
+
const readOnlyStatus: Intent[] = [{ type: "status", message: "review submitted - read-only" }];
|
|
336
|
+
|
|
337
|
+
// Assert - comment, cut, and quick-actions all answer read-only
|
|
338
|
+
expect(reduceKey(resolved, key("x"))).toEqual(readOnlyStatus);
|
|
339
|
+
expect(reduceKey(resolved, key("c"))).toEqual(readOnlyStatus);
|
|
340
|
+
expect(reduceKey(resolved, key("a"))).toEqual(readOnlyStatus);
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
test("span-mode cut gates for an observer even when cut is rebound off x", () => {
|
|
344
|
+
// Arrange - the span sub-grammar hardwires x, so the read-only gate must catch it by name
|
|
345
|
+
const rebound = { ...DEFAULT_KEYS, cut: ["d"] };
|
|
346
|
+
const observer = state({ spanMode: true, readOnly: true, keys: rebound });
|
|
347
|
+
|
|
348
|
+
// Assert
|
|
349
|
+
expect(reduceKey(observer, key("x"))).toEqual([
|
|
350
|
+
{ type: "status", message: "observer - read-only" },
|
|
351
|
+
]);
|
|
352
|
+
});
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
describe("span actions overlay", () => {
|
|
356
|
+
// Arrange
|
|
357
|
+
const keyState = state({ overlay: "spanActions" });
|
|
358
|
+
const table: [string, Intent[]][] = [
|
|
359
|
+
["j", [{ type: "moveSpanAction", direction: 1 }]],
|
|
360
|
+
["down", [{ type: "moveSpanAction", direction: 1 }]],
|
|
361
|
+
["k", [{ type: "moveSpanAction", direction: -1 }]],
|
|
362
|
+
["up", [{ type: "moveSpanAction", direction: -1 }]],
|
|
363
|
+
["return", [{ type: "pickSpanAction" }]],
|
|
364
|
+
["enter", [{ type: "pickSpanAction" }]],
|
|
365
|
+
["escape", [{ type: "closeSpanActions" }]],
|
|
366
|
+
["c", []],
|
|
367
|
+
["q", []],
|
|
368
|
+
];
|
|
369
|
+
|
|
184
370
|
for (const [name, expected] of table) {
|
|
185
371
|
test(`${name} -> ${JSON.stringify(expected)}`, () => {
|
|
186
|
-
expect(reduceKey(
|
|
372
|
+
expect(reduceKey(keyState, key(name))).toEqual(expected);
|
|
187
373
|
});
|
|
188
374
|
}
|
|
189
375
|
});
|
|
190
376
|
|
|
191
377
|
describe("diff mode", () => {
|
|
192
|
-
|
|
378
|
+
// Arrange
|
|
379
|
+
const keyState = state({ view: "diff" });
|
|
193
380
|
const table: [string, Intent[]][] = [
|
|
194
|
-
["j", [{
|
|
195
|
-
["k", [{
|
|
196
|
-
["g", [{
|
|
197
|
-
["c", [{
|
|
198
|
-
["n", [{
|
|
199
|
-
["p", [{
|
|
200
|
-
["backspace", [{
|
|
201
|
-
["return", [{
|
|
202
|
-
["v", [{
|
|
203
|
-
["x", [{
|
|
204
|
-
["e", [{
|
|
205
|
-
["s", [{
|
|
206
|
-
["q", [{
|
|
381
|
+
["j", [{ type: "move", to: "down" }]],
|
|
382
|
+
["k", [{ type: "move", to: "up" }]],
|
|
383
|
+
["g", [{ type: "move", to: "top" }]],
|
|
384
|
+
["c", [{ type: "openCompose", kind: "comment", from: "cursor" }]],
|
|
385
|
+
["n", [{ type: "nextAnnotation" }]],
|
|
386
|
+
["p", [{ type: "prevAnnotation" }]],
|
|
387
|
+
["backspace", [{ type: "removeAnnotation" }]],
|
|
388
|
+
["return", [{ type: "openSubmit" }]],
|
|
389
|
+
["v", [{ type: "status", message: "plan-only primitive - diff review uses c on a line" }]],
|
|
390
|
+
["x", [{ type: "rejectChange" }]],
|
|
391
|
+
["e", [{ type: "status", message: "plan-only primitive - diff review uses c on a line" }]],
|
|
392
|
+
["s", [{ type: "toggleDiffView" }]],
|
|
393
|
+
["q", [{ type: "exit" }]],
|
|
207
394
|
];
|
|
395
|
+
|
|
208
396
|
for (const [name, expected] of table) {
|
|
209
397
|
test(`${name} -> ${JSON.stringify(expected)}`, () => {
|
|
210
|
-
expect(reduceKey(
|
|
398
|
+
expect(reduceKey(keyState, key(name))).toEqual(expected);
|
|
211
399
|
});
|
|
212
400
|
}
|
|
213
401
|
|
|
402
|
+
test("shift+x rejects the whole hunk under the cursor", () => {
|
|
403
|
+
// Assert
|
|
404
|
+
expect(reduceKey(keyState, key("x", true))).toEqual([{ type: "rejectHunk" }]);
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
test("curation is owner-only and locked once resolved", () => {
|
|
408
|
+
// Assert - a resolved diff answers before curating
|
|
409
|
+
expect(reduceKey(state({ view: "diff", resolved: true }), key("x"))).toEqual([
|
|
410
|
+
{ type: "status", message: "review submitted - read-only" },
|
|
411
|
+
]);
|
|
412
|
+
// a collaborator (canEditPlan false) cannot curate hunks
|
|
413
|
+
expect(reduceKey(state({ view: "diff", canEditPlan: false }), key("x"))).toEqual([
|
|
414
|
+
{ type: "status", message: "only the diff owner can curate hunks" },
|
|
415
|
+
]);
|
|
416
|
+
// an observer's mutating attempt is stopped by the read-only gate
|
|
417
|
+
expect(reduceKey(state({ view: "diff", readOnly: true }), key("x"))).toEqual([
|
|
418
|
+
{ type: "status", message: "observer - read-only" },
|
|
419
|
+
]);
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
test("u restores a curated-out item, owner-only and locked once resolved", () => {
|
|
423
|
+
// Assert - the undo intent leaves the target to the dispatcher
|
|
424
|
+
expect(reduceKey(keyState, key("u"))).toEqual([{ type: "restoreCuration" }]);
|
|
425
|
+
expect(reduceKey(state({ view: "diff", resolved: true }), key("u"))).toEqual([
|
|
426
|
+
{ type: "status", message: "review submitted - read-only" },
|
|
427
|
+
]);
|
|
428
|
+
expect(reduceKey(state({ view: "diff", canEditPlan: false }), key("u"))).toEqual([
|
|
429
|
+
{ type: "status", message: "only the diff owner can curate hunks" },
|
|
430
|
+
]);
|
|
431
|
+
expect(reduceKey(state({ view: "diff", readOnly: true }), key("u"))).toEqual([
|
|
432
|
+
{ type: "status", message: "observer - read-only" },
|
|
433
|
+
]);
|
|
434
|
+
});
|
|
435
|
+
|
|
214
436
|
test("comment guards: resolved first, then non-code rows", () => {
|
|
437
|
+
// Assert
|
|
215
438
|
expect(reduceKey(state({ view: "diff", resolved: true }), key("c"))).toEqual([
|
|
216
|
-
{
|
|
439
|
+
{ type: "status", message: "review submitted - read-only" },
|
|
217
440
|
]);
|
|
218
441
|
expect(reduceKey(state({ view: "diff", cursorAnnotatable: false }), key("c"))).toEqual([
|
|
219
|
-
{
|
|
442
|
+
{ type: "status", message: "move to a code line to comment" },
|
|
443
|
+
]);
|
|
444
|
+
});
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
describe("guided walk", () => {
|
|
448
|
+
test("w enters the walk in diff view; resolved and read-only sessions answer", () => {
|
|
449
|
+
// Assert
|
|
450
|
+
expect(reduceKey(state({ view: "diff" }), key("w"))).toEqual([{ type: "walkStart" }]);
|
|
451
|
+
expect(reduceKey(state({ view: "diff", resolved: true }), key("w"))).toEqual([
|
|
452
|
+
{ type: "status", message: "review submitted - read-only" },
|
|
453
|
+
]);
|
|
454
|
+
expect(reduceKey(state({ view: "diff", readOnly: true }), key("w"))).toEqual([
|
|
455
|
+
{ type: "status", message: "observer - read-only" },
|
|
456
|
+
]);
|
|
457
|
+
expect(reduceKey(state(), key("w"))).toEqual([
|
|
458
|
+
{ type: "status", message: "the guided walk is a diff-review mode" },
|
|
459
|
+
]);
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
// Arrange
|
|
463
|
+
const walking = state({ view: "diff", overlay: "walk" });
|
|
464
|
+
const table: [string, Intent[]][] = [
|
|
465
|
+
["]", [{ type: "walkForward" }]],
|
|
466
|
+
["[", [{ type: "walkBack" }]],
|
|
467
|
+
["escape", [{ type: "walkLeave" }]],
|
|
468
|
+
["q", [{ type: "exit" }]],
|
|
469
|
+
// mid-walk return does nothing: submit lives on the end card only
|
|
470
|
+
["return", []],
|
|
471
|
+
["j", []],
|
|
472
|
+
["c", []],
|
|
473
|
+
];
|
|
474
|
+
|
|
475
|
+
for (const [name, expected] of table) {
|
|
476
|
+
test(`walking ${name} -> ${JSON.stringify(expected)}`, () => {
|
|
477
|
+
expect(reduceKey(walking, key(name))).toEqual(expected);
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
test("the end card's return leaves the walk and opens the submit confirm", () => {
|
|
482
|
+
// Arrange
|
|
483
|
+
const atEnd = state({ view: "diff", overlay: "walk", walkAtEnd: true });
|
|
484
|
+
|
|
485
|
+
// Assert
|
|
486
|
+
expect(reduceKey(atEnd, key("return"))).toEqual([
|
|
487
|
+
{ type: "walkLeave" },
|
|
488
|
+
{ type: "openSubmit" },
|
|
220
489
|
]);
|
|
490
|
+
expect(reduceKey(atEnd, key("enter"))).toEqual([{ type: "walkLeave" }, { type: "openSubmit" }]);
|
|
491
|
+
});
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
describe("span and walk own their keys", () => {
|
|
495
|
+
test("span mode owns b as a span primitive", () => {
|
|
496
|
+
// Assert
|
|
497
|
+
expect(reduceKey(state({ spanMode: true }), key("b"))).toEqual([
|
|
498
|
+
{ type: "spanKey", name: "b" },
|
|
499
|
+
]);
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
test("the walk overlay keeps the brackets for stepping", () => {
|
|
503
|
+
// Arrange
|
|
504
|
+
const walking = state({ view: "diff", overlay: "walk" });
|
|
505
|
+
|
|
506
|
+
// Assert
|
|
507
|
+
expect(reduceKey(walking, key("]"))).toEqual([{ type: "walkForward" }]);
|
|
508
|
+
expect(reduceKey(walking, key("["))).toEqual([{ type: "walkBack" }]);
|
|
221
509
|
});
|
|
222
510
|
});
|
|
223
511
|
|
|
224
512
|
describe("duplicated-branch collapse", () => {
|
|
225
513
|
test("plan and diff produce the same intents for annotation nav, delete, and submit", () => {
|
|
226
|
-
|
|
514
|
+
const patches: Partial<KeyState>[] = [
|
|
227
515
|
{},
|
|
228
516
|
{ annotationCount: 0, hasFocusedAnnotation: false },
|
|
229
517
|
{ resolved: true },
|
|
230
|
-
]
|
|
518
|
+
];
|
|
519
|
+
|
|
520
|
+
for (const patch of patches) {
|
|
231
521
|
for (const name of ["n", "p", "backspace", "return"]) {
|
|
232
522
|
const plan = reduceKey(state(patch), key(name));
|
|
233
523
|
const diff = reduceKey(state({ ...patch, view: "diff" }), key(name));
|
|
524
|
+
|
|
234
525
|
expect(diff).toEqual(plan);
|
|
235
526
|
}
|
|
236
527
|
}
|
|
@@ -240,24 +531,81 @@ describe("duplicated-branch collapse", () => {
|
|
|
240
531
|
describe("read-only filter", () => {
|
|
241
532
|
test("every mutating key answers observer - read-only, in plan and diff", () => {
|
|
242
533
|
for (const view of ["plan", "diff"] as const) {
|
|
243
|
-
const
|
|
244
|
-
|
|
245
|
-
|
|
534
|
+
const keyState = state({ view, readOnly: true });
|
|
535
|
+
|
|
536
|
+
for (const name of ["c", "x", "e", "backspace", "return", "enter"]) {
|
|
537
|
+
expect(reduceKey(keyState, key(name))).toEqual([
|
|
538
|
+
{ type: "status", message: "observer - read-only" },
|
|
539
|
+
]);
|
|
246
540
|
}
|
|
247
541
|
}
|
|
248
542
|
});
|
|
249
543
|
|
|
250
544
|
test("navigation and annotation focus still work for observers", () => {
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
545
|
+
// Arrange
|
|
546
|
+
const keyState = state({ readOnly: true });
|
|
547
|
+
|
|
548
|
+
// Assert
|
|
549
|
+
expect(reduceKey(keyState, key("j"))).toEqual([{ type: "move", to: "down" }]);
|
|
550
|
+
expect(reduceKey(keyState, key("n"))).toEqual([{ type: "nextAnnotation" }]);
|
|
551
|
+
expect(reduceKey(keyState, key("q"))).toEqual([{ type: "exit" }]);
|
|
255
552
|
});
|
|
256
553
|
|
|
257
|
-
test("span-mode c
|
|
258
|
-
|
|
259
|
-
const
|
|
260
|
-
|
|
261
|
-
|
|
554
|
+
test("span-mode c is gated by key name even under a rebound keymap", () => {
|
|
555
|
+
// Arrange
|
|
556
|
+
const rebound = { ...DEFAULT_KEYS, comment: ["m"] };
|
|
557
|
+
const keyState = state({ spanMode: true, readOnly: true, keys: rebound });
|
|
558
|
+
|
|
559
|
+
// Assert
|
|
560
|
+
expect(reduceKey(keyState, key("c"))).toEqual([
|
|
561
|
+
{ type: "status", message: "observer - read-only" },
|
|
562
|
+
]);
|
|
563
|
+
});
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
describe("inbox delete", () => {
|
|
567
|
+
test("d on a session requests delete", () => {
|
|
568
|
+
// Arrange / Act / Assert
|
|
569
|
+
expect(reduceKey(state({ view: "inbox" }), key("d"))).toEqual([
|
|
570
|
+
{ type: "requestDeleteSession" },
|
|
571
|
+
]);
|
|
572
|
+
});
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
describe("confirm overlay", () => {
|
|
576
|
+
// Arrange
|
|
577
|
+
const keyState = state({ overlay: "confirm" });
|
|
578
|
+
const table: [string, Intent[]][] = [
|
|
579
|
+
["return", [{ type: "confirmDialog" }]],
|
|
580
|
+
["enter", [{ type: "confirmDialog" }]],
|
|
581
|
+
["escape", [{ type: "closeOverlay" }]],
|
|
582
|
+
["j", []],
|
|
583
|
+
["d", []],
|
|
584
|
+
];
|
|
585
|
+
|
|
586
|
+
for (const [name, expected] of table) {
|
|
587
|
+
test(`${name} -> ${JSON.stringify(expected)}`, () => {
|
|
588
|
+
expect(reduceKey(keyState, key(name))).toEqual(expected);
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
describe("rename grammar", () => {
|
|
594
|
+
test("r on a focused note opens rename", () => {
|
|
595
|
+
// Arrange / Act / Assert
|
|
596
|
+
expect(reduceKey(state({ hasFocusedAnnotation: true }), key("r"))).toEqual([
|
|
597
|
+
{ type: "openRename" },
|
|
598
|
+
]);
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
test("prompt overlay routes ⏎ to confirm and esc to cancel", () => {
|
|
602
|
+
// Arrange / Act / Assert
|
|
603
|
+
expect(reduceKey(state({ overlay: "prompt" }), key("return"))).toEqual([
|
|
604
|
+
{ type: "confirmDialog" },
|
|
605
|
+
]);
|
|
606
|
+
expect(reduceKey(state({ overlay: "prompt" }), key("escape"))).toEqual([
|
|
607
|
+
{ type: "closeOverlay" },
|
|
608
|
+
]);
|
|
609
|
+
expect(reduceKey(state({ overlay: "prompt" }), key("a"))).toEqual([]);
|
|
262
610
|
});
|
|
263
611
|
});
|