@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/App.test.tsx
CHANGED
|
@@ -1,18 +1,27 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Virtual-terminal component tests (tier 2): the real App over a real
|
|
3
|
-
* in-process daemon in a temp home. Char-frame assertions + mock keys -
|
|
4
|
-
* the whole review loop drivable without a terminal.
|
|
5
|
-
*/
|
|
1
|
+
/** Virtual-terminal component tests (tier 2): the real App over a real in-process daemon in a temp home. Char-frame assertions + mock keys - the whole review loop drivable without a terminal. */
|
|
6
2
|
|
|
7
3
|
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
8
4
|
import { mkdtempSync, rmSync } from "node:fs";
|
|
9
5
|
import { tmpdir } from "node:os";
|
|
10
6
|
import { join } from "node:path";
|
|
11
7
|
import React from "react";
|
|
12
|
-
import { testRender } from "@opentui/react/test-utils";
|
|
13
8
|
import { DaemonServer } from "@cueloop/daemon";
|
|
14
|
-
import type {
|
|
9
|
+
import type { Thread } from "@cueloop/schema";
|
|
15
10
|
import { App } from "./App";
|
|
11
|
+
import {
|
|
12
|
+
clickText,
|
|
13
|
+
dragText,
|
|
14
|
+
frameRow,
|
|
15
|
+
isolateUserConfig,
|
|
16
|
+
locateText,
|
|
17
|
+
press,
|
|
18
|
+
navCommand,
|
|
19
|
+
pressKey,
|
|
20
|
+
renderReadyApp,
|
|
21
|
+
typeText as type,
|
|
22
|
+
waitForState,
|
|
23
|
+
waitForText,
|
|
24
|
+
} from "./test-support";
|
|
16
25
|
|
|
17
26
|
const PLAN = `# Migration Plan
|
|
18
27
|
|
|
@@ -27,192 +36,385 @@ The daemon persists sessions to disk atomically.
|
|
|
27
36
|
`;
|
|
28
37
|
|
|
29
38
|
let home: string;
|
|
39
|
+
let restoreUserConfig: () => void;
|
|
30
40
|
let server: DaemonServer;
|
|
31
|
-
let session:
|
|
41
|
+
let session: Thread;
|
|
32
42
|
|
|
33
43
|
beforeEach(() => {
|
|
34
44
|
home = mkdtempSync(join(tmpdir(), "cueloop-app-"));
|
|
45
|
+
restoreUserConfig = isolateUserConfig(home);
|
|
35
46
|
server = new DaemonServer({ home, idleExitMs: 0 });
|
|
36
47
|
server.start();
|
|
37
48
|
session = server.core.sessionCreate({
|
|
38
49
|
workspace: { repoRoot: "/repo", branch: "main" },
|
|
39
|
-
artifact: {
|
|
50
|
+
artifact: {
|
|
51
|
+
type: "plan",
|
|
52
|
+
content: PLAN,
|
|
53
|
+
meta: { title: "Migration Plan", planPath: "plan.md" },
|
|
54
|
+
},
|
|
40
55
|
});
|
|
41
56
|
});
|
|
42
57
|
afterEach(() => {
|
|
58
|
+
restoreUserConfig();
|
|
43
59
|
server.stop();
|
|
44
60
|
rmSync(home, { recursive: true, force: true });
|
|
45
61
|
});
|
|
46
62
|
|
|
47
63
|
async function renderApp(sessionId?: string) {
|
|
48
|
-
|
|
64
|
+
return renderReadyApp(<App home={home} sessionId={sessionId ?? session.id} />, {
|
|
49
65
|
width: 120,
|
|
50
66
|
height: 32,
|
|
51
67
|
});
|
|
52
|
-
// let the async daemon connect + first fetch land
|
|
53
|
-
for (let i = 0; i < 40 && !setup.captureCharFrame().includes("cueloop"); i++) {
|
|
54
|
-
await Bun.sleep(25);
|
|
55
|
-
await setup.renderOnce();
|
|
56
|
-
}
|
|
57
|
-
await setup.renderOnce();
|
|
58
|
-
return setup;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
type Setup = Awaited<ReturnType<typeof renderApp>>;
|
|
62
|
-
|
|
63
|
-
/** Drive one key press: letters type as text; named keys use KeyCodes ids. */
|
|
64
|
-
async function press(setup: Setup, k: string): Promise<void> {
|
|
65
|
-
if (k === "enter") setup.mockInput.pressKey("RETURN");
|
|
66
|
-
else if (k === "escape") setup.mockInput.pressKey("ESCAPE");
|
|
67
|
-
else if (k === "backspace") setup.mockInput.pressKey("BACKSPACE");
|
|
68
|
-
else if (k === "left") setup.mockInput.pressKey("ARROW_LEFT");
|
|
69
|
-
else if (k === "right") setup.mockInput.pressKey("ARROW_RIGHT");
|
|
70
|
-
else await type(setup, k);
|
|
71
|
-
await Bun.sleep(15);
|
|
72
|
-
await setup.renderOnce();
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
async function type(setup: Setup, text: string): Promise<void> {
|
|
76
|
-
await setup.mockInput.typeText(text);
|
|
77
|
-
await Bun.sleep(15);
|
|
78
|
-
await setup.renderOnce();
|
|
79
68
|
}
|
|
80
69
|
|
|
81
70
|
describe("plan rendering", () => {
|
|
82
|
-
test("renders the plan with headings, list markers, and the
|
|
71
|
+
test("renders the plan with headings, list markers, and the footer", async () => {
|
|
72
|
+
// Arrange
|
|
83
73
|
const setup = await renderApp();
|
|
74
|
+
|
|
75
|
+
// Assert
|
|
84
76
|
const frame = setup.captureCharFrame();
|
|
77
|
+
|
|
85
78
|
expect(frame).toContain("Migration Plan");
|
|
86
79
|
expect(frame).toContain("Context");
|
|
87
80
|
expect(frame).toContain("persists sessions to disk atomically");
|
|
88
|
-
expect(frame).toContain("
|
|
89
|
-
expect(frame).toContain("
|
|
90
|
-
|
|
81
|
+
expect(frame).toContain("· move the store");
|
|
82
|
+
expect(frame).toContain("Send message (");
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("a direct open still populates the Threads sidebar with other pending reviews", async () => {
|
|
86
|
+
// Arrange - a second pending review exists alongside the one opened directly
|
|
87
|
+
server.core.sessionCreate({
|
|
88
|
+
workspace: { repoRoot: "/repo", branch: "main" },
|
|
89
|
+
artifact: { type: "plan", content: "# Other Plan\n", meta: { title: "Other Plan" } },
|
|
90
|
+
});
|
|
91
|
+
const setup = await renderApp();
|
|
92
|
+
|
|
93
|
+
// Act - open the collapsed Threads sidebar (the mirrored panel toggle by the gear)
|
|
94
|
+
await setup.mockMouse.click(4, 0);
|
|
95
|
+
|
|
96
|
+
// Assert - the sidebar lists the other pending review, so you can jump to it
|
|
97
|
+
await waitForText(setup, "Other Plan");
|
|
91
98
|
});
|
|
92
99
|
});
|
|
93
100
|
|
|
94
|
-
describe("
|
|
95
|
-
test("
|
|
101
|
+
describe("thread view grammar", () => {
|
|
102
|
+
test("↓ moves the caret between blocks and typing anchors to the block under it", async () => {
|
|
103
|
+
// Arrange
|
|
96
104
|
const setup = await renderApp();
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
await setup
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
105
|
+
|
|
106
|
+
// Act: two blocks down lands on the paragraph; a printable opens a draft there
|
|
107
|
+
await pressKey(setup, "ARROW_DOWN");
|
|
108
|
+
await pressKey(setup, "ARROW_DOWN");
|
|
109
|
+
await type(setup, "x");
|
|
110
|
+
|
|
111
|
+
// Assert: the draft card sits right under the paragraph's row
|
|
112
|
+
await waitForText(setup, "● x");
|
|
113
|
+
expect(frameRow(setup, "● x")).toBeGreaterThan(frameRow(setup, "persists sessions"));
|
|
114
|
+
expect(frameRow(setup, "● x")).toBeLessThan(frameRow(setup, "Steps"));
|
|
103
115
|
});
|
|
104
116
|
|
|
105
|
-
test("comment flow:
|
|
117
|
+
test("comment flow: click a word, type a body, cmd+⏎ saves to the daemon", async () => {
|
|
118
|
+
// Arrange
|
|
106
119
|
const setup = await renderApp();
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
await
|
|
110
|
-
await setup.renderOnce();
|
|
111
|
-
expect(setup.captureCharFrame()).toContain("COMMENT ON");
|
|
120
|
+
|
|
121
|
+
// Act
|
|
122
|
+
await clickText(setup, "daemon");
|
|
112
123
|
await type(setup, "Define atomically.");
|
|
113
|
-
await
|
|
114
|
-
|
|
115
|
-
|
|
124
|
+
await pressKey(setup, "RETURN", { meta: true });
|
|
125
|
+
|
|
126
|
+
// Assert
|
|
127
|
+
await waitForState(setup, () => server.core.sessionGet(session.id).annotations.length === 1);
|
|
116
128
|
const stored = server.core.sessionGet(session.id);
|
|
129
|
+
|
|
117
130
|
expect(stored.annotations.length).toBe(1);
|
|
118
131
|
expect(stored.annotations[0]!.body).toBe("Define atomically.");
|
|
119
|
-
expect(
|
|
132
|
+
expect(stored.annotations[0]!.anchor.quote).toBe("daemon");
|
|
120
133
|
});
|
|
121
134
|
|
|
122
|
-
test("
|
|
135
|
+
test("a drag marks the exact span the comment anchors to", async () => {
|
|
136
|
+
// Arrange
|
|
123
137
|
const setup = await renderApp();
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
await
|
|
127
|
-
await press(setup, "l"); // "The daemon"
|
|
128
|
-
await press(setup, "c");
|
|
138
|
+
|
|
139
|
+
// Act: from the start of "The" to the end of "daemon"
|
|
140
|
+
await dragText(setup, "The daemon", "daemon persists", "daemon".length);
|
|
129
141
|
await type(setup, "Which daemon?");
|
|
130
|
-
await
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
142
|
+
await pressKey(setup, "RETURN", { meta: true });
|
|
143
|
+
|
|
144
|
+
// Assert
|
|
145
|
+
await waitForState(setup, () => server.core.sessionGet(session.id).annotations.length === 1);
|
|
146
|
+
expect(server.core.sessionGet(session.id).annotations[0]!.anchor.quote).toBe("The daemon");
|
|
134
147
|
});
|
|
135
148
|
|
|
136
|
-
test("x cuts
|
|
149
|
+
test("option+x cuts the caret's block into the working copy; option+x restores it", async () => {
|
|
150
|
+
// Arrange
|
|
137
151
|
const setup = await renderApp();
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
await setup
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
await
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
152
|
+
|
|
153
|
+
await clickText(setup, "move the store");
|
|
154
|
+
|
|
155
|
+
// Act
|
|
156
|
+
await navCommand(setup, "x");
|
|
157
|
+
|
|
158
|
+
// Assert - the cut lands in the working copy
|
|
159
|
+
await waitForState(
|
|
160
|
+
setup,
|
|
161
|
+
() => !(server.core.sessionGet(session.id).workingCopy ?? "").includes("move the store"),
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
// Act
|
|
165
|
+
await navCommand(setup, "x");
|
|
166
|
+
|
|
167
|
+
// Assert
|
|
168
|
+
await waitForState(setup, () => server.core.sessionGet(session.id).workingCopy === undefined);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test("ctrl+e opens the inline editor; the header toggles to normal and leaving tracks the edit", async () => {
|
|
172
|
+
// Arrange
|
|
173
|
+
const setup = await renderApp();
|
|
174
|
+
|
|
175
|
+
// Act - open the inline markdown editor over the thread body
|
|
176
|
+
await pressKey(setup, "e", { ctrl: true });
|
|
177
|
+
|
|
178
|
+
// Assert - the editor owns the pane (its save hint shows) and the header offers the way back
|
|
179
|
+
await waitForText(setup, "save & close");
|
|
180
|
+
expect(setup.captureCharFrame()).toContain("normal");
|
|
181
|
+
|
|
182
|
+
// Act - type into the body, then leave through the header toggle
|
|
183
|
+
await type(setup, "MORE");
|
|
184
|
+
await clickText(setup, "normal");
|
|
185
|
+
|
|
186
|
+
// Assert - the edit is tracked into the working copy, and the read-only view is back
|
|
187
|
+
await waitForState(setup, () =>
|
|
188
|
+
(server.core.sessionGet(session.id).workingCopy ?? "").includes("MORE"),
|
|
189
|
+
);
|
|
166
190
|
});
|
|
167
191
|
});
|
|
168
192
|
|
|
169
193
|
describe("submit", () => {
|
|
170
|
-
test("
|
|
194
|
+
test("cmd+⏎ opens the rail confirm card; message + summary resolve the session", async () => {
|
|
195
|
+
// Arrange
|
|
171
196
|
const setup = await renderApp();
|
|
172
|
-
|
|
173
|
-
await
|
|
174
|
-
await press(setup, "c");
|
|
197
|
+
|
|
198
|
+
await clickText(setup, "daemon");
|
|
175
199
|
await type(setup, "Needs a phase list.");
|
|
176
|
-
await
|
|
177
|
-
await
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
200
|
+
await pressKey(setup, "RETURN", { meta: true });
|
|
201
|
+
await waitForText(setup, "Needs a phase list");
|
|
202
|
+
|
|
203
|
+
// Act: with no composer open the same chord opens submit
|
|
204
|
+
await pressKey(setup, "RETURN", { meta: true });
|
|
205
|
+
|
|
206
|
+
// Assert - the card opens on the default message, approve
|
|
207
|
+
await waitForText(setup, "[Approve]");
|
|
208
|
+
|
|
209
|
+
// Act - cycle to request changes, then send with a summary
|
|
210
|
+
await press(setup, "right");
|
|
211
|
+
await waitForText(setup, "[Changes]");
|
|
181
212
|
await type(setup, "Expand the steps.");
|
|
182
|
-
await
|
|
183
|
-
|
|
184
|
-
|
|
213
|
+
await pressKey(setup, "RETURN", { meta: true });
|
|
214
|
+
|
|
215
|
+
// Assert
|
|
216
|
+
await waitForText(setup, "feedback sent");
|
|
185
217
|
const stored = server.core.sessionGet(session.id);
|
|
218
|
+
|
|
186
219
|
expect(stored.status).toBe("resolved");
|
|
187
|
-
expect(stored.
|
|
188
|
-
expect(stored.
|
|
220
|
+
expect(stored.message!.outcome).toBe("changes_requested");
|
|
221
|
+
expect(stored.message!.body).toContain("Needs a phase list.");
|
|
189
222
|
// submit hands the reviewer back to the agent via the completion overlay
|
|
190
|
-
expect(setup.captureCharFrame()).toContain("
|
|
223
|
+
expect(setup.captureCharFrame()).toContain("feedback sent");
|
|
191
224
|
});
|
|
192
225
|
|
|
193
|
-
test("approve via ←/→
|
|
226
|
+
test("approve via ←/→ message cycling", async () => {
|
|
227
|
+
// Arrange
|
|
194
228
|
const setup = await renderApp();
|
|
229
|
+
|
|
230
|
+
// Act
|
|
231
|
+
await pressKey(setup, "RETURN", { meta: true });
|
|
232
|
+
|
|
233
|
+
// Assert
|
|
234
|
+
await waitForText(setup, "[Approve]"); // no pending items → approve default
|
|
235
|
+
|
|
236
|
+
// Act
|
|
237
|
+
await pressKey(setup, "RETURN", { meta: true });
|
|
238
|
+
|
|
239
|
+
// Assert
|
|
240
|
+
await waitForState(setup, () => server.core.sessionGet(session.id).message !== undefined);
|
|
241
|
+
expect(server.core.sessionGet(session.id).message!.outcome).toBe("approved");
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
describe("no-thread shell", () => {
|
|
246
|
+
test("opening with nothing selected lands in the shell with a Welcome tab and the Threads sidebar", async () => {
|
|
247
|
+
// Arrange
|
|
248
|
+
const setup = await renderReadyApp(<App home={home} />, { width: 120, height: 32 });
|
|
249
|
+
|
|
250
|
+
// the Welcome playground measures its width before it paints, so wait for its copy, not the header
|
|
251
|
+
await waitForText(setup, "Getting started");
|
|
252
|
+
|
|
253
|
+
// Assert - the same shell header, the disposable Welcome tab, and the pending thread
|
|
254
|
+
const frame = setup.captureCharFrame();
|
|
255
|
+
|
|
256
|
+
expect(frame).toContain("Getting started");
|
|
257
|
+
expect(frame).toContain("Migration Plan");
|
|
258
|
+
expect(frame).not.toContain("· resume"); // the bespoke inbox header is retired
|
|
259
|
+
|
|
260
|
+
// Act
|
|
195
261
|
await press(setup, "enter");
|
|
196
|
-
|
|
197
|
-
|
|
262
|
+
|
|
263
|
+
// Assert
|
|
264
|
+
await waitForText(setup, "Send message (");
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
test("picking a thread from the no-thread shell keeps the Threads sidebar open", async () => {
|
|
268
|
+
// Arrange - a second pending thread so the sidebar has a row that never shows
|
|
269
|
+
// in the opened thread's own view
|
|
270
|
+
server.core.sessionCreate({
|
|
271
|
+
workspace: { repoRoot: "/repo", branch: "main" },
|
|
272
|
+
artifact: { type: "plan", content: "# Other Plan\n", meta: { title: "Other Plan" } },
|
|
273
|
+
});
|
|
274
|
+
const setup = await renderReadyApp(<App home={home} />, { width: 120, height: 32 });
|
|
275
|
+
|
|
276
|
+
await waitForText(setup, "Getting started");
|
|
277
|
+
await waitForText(setup, "Other Plan"); // the sidebar opens by default here
|
|
278
|
+
|
|
279
|
+
// Act - open the thread under the cursor
|
|
198
280
|
await press(setup, "enter");
|
|
199
|
-
await
|
|
200
|
-
|
|
281
|
+
await waitForText(setup, "Send message (");
|
|
282
|
+
|
|
283
|
+
// Assert - the sidebar stayed open across the swap: both titles are on screen,
|
|
284
|
+
// and the non-opened one can only come from the still-open sidebar
|
|
285
|
+
const frame = setup.captureCharFrame();
|
|
286
|
+
|
|
287
|
+
expect(frame).toContain("Migration Plan");
|
|
288
|
+
expect(frame).toContain("Other Plan");
|
|
201
289
|
});
|
|
202
|
-
});
|
|
203
290
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
const setup = await
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
291
|
+
test("the row kebab menu stars a thread into a Starred section", async () => {
|
|
292
|
+
// Arrange
|
|
293
|
+
const setup = await renderReadyApp(<App home={home} />, { width: 120, height: 32 });
|
|
294
|
+
|
|
295
|
+
await waitForText(setup, "Migration Plan");
|
|
296
|
+
|
|
297
|
+
// Act - hover the row to reveal its kebab, open the menu, and pick Star
|
|
298
|
+
const row = locateText(setup, "Migration Plan");
|
|
299
|
+
|
|
300
|
+
await setup.mockMouse.moveTo(row.column, row.row);
|
|
301
|
+
await waitForText(setup, "⋮");
|
|
302
|
+
await clickText(setup, "⋮");
|
|
303
|
+
// "star" alone collides with "Getting started" in the welcome pane, so target the menu via its
|
|
304
|
+
// unique "rename" row - the star action sits directly above it
|
|
305
|
+
await waitForText(setup, "rename");
|
|
306
|
+
const rename = locateText(setup, "rename");
|
|
307
|
+
await setup.mockMouse.click(rename.column, rename.row - 1);
|
|
308
|
+
|
|
309
|
+
// Assert - a Starred section now holds the thread
|
|
310
|
+
await waitForText(setup, "Starred");
|
|
311
|
+
expect(frameRow(setup, "Migration Plan")).toBeGreaterThan(frameRow(setup, "Starred"));
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
test("a bare launch shows the Welcome surface with the right region collapsed", async () => {
|
|
315
|
+
// Arrange
|
|
316
|
+
const setup = await renderReadyApp(<App home={home} />, { width: 120, height: 32 });
|
|
317
|
+
|
|
318
|
+
await waitForText(setup, "Getting started");
|
|
319
|
+
|
|
320
|
+
// Assert - Welcome fills the thread pane (not a Changes tab); the right region stays collapsed
|
|
321
|
+
const frame = setup.captureCharFrame();
|
|
322
|
+
|
|
323
|
+
expect(frame).toContain("Getting started");
|
|
324
|
+
expect(frame).not.toContain("Changes"); // the Changes editor is closed on a bare launch
|
|
325
|
+
expect(frame).toContain("Migration Plan"); // the sidebar lists the pending thread
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
test("the menu opens from the shell gear and escape is not a trap", async () => {
|
|
329
|
+
// Arrange
|
|
330
|
+
const setup = await renderReadyApp(<App home={home} />, { width: 120, height: 32 });
|
|
331
|
+
|
|
332
|
+
await waitForText(setup, "Getting started");
|
|
333
|
+
|
|
334
|
+
// Act - open the settings dialog from the top-left gear (the Threads panel header, row 0)
|
|
335
|
+
await setup.mockMouse.click(1, 0);
|
|
336
|
+
|
|
337
|
+
// Assert - the settings dialog with its Keybinds leaf appears
|
|
338
|
+
await waitForText(setup, "Keybinds");
|
|
339
|
+
expect(setup.captureCharFrame()).toContain("settings");
|
|
340
|
+
|
|
341
|
+
// Act - escape closes the menu (not a trap), and the thread nav still works
|
|
342
|
+
await press(setup, "escape");
|
|
343
|
+
await waitForState(setup, () => !setup.captureCharFrame().includes("Keybinds"));
|
|
213
344
|
await press(setup, "enter");
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
345
|
+
|
|
346
|
+
// Assert
|
|
347
|
+
await waitForText(setup, "Send message (");
|
|
348
|
+
});
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
describe("the thread view and the menu", () => {
|
|
352
|
+
test("the keybinds dialog lists the thread view grammar, keeps keys away from the text, and closes on escape", async () => {
|
|
353
|
+
// Arrange
|
|
354
|
+
const setup = await renderApp();
|
|
355
|
+
|
|
356
|
+
await waitForText(setup, "The daemon persists");
|
|
357
|
+
|
|
358
|
+
// Act - open the menu from the top-left settings gear, then the keybinds dialog
|
|
359
|
+
await setup.mockMouse.click(1, 0);
|
|
360
|
+
await waitForText(setup, "Keybinds");
|
|
361
|
+
const dropUp = setup.captureCharFrame().split("\n");
|
|
362
|
+
const keybindsRow = dropUp.findIndex((line) => line.includes("Keybinds"));
|
|
363
|
+
|
|
364
|
+
await setup.mockMouse.click(dropUp[keybindsRow]!.indexOf("Keybinds") + 1, keybindsRow);
|
|
365
|
+
await waitForText(setup, "mark text, across blocks");
|
|
366
|
+
|
|
367
|
+
// Assert - the thread grammar, not the plan sheet's
|
|
368
|
+
const dialog = setup.captureCharFrame();
|
|
369
|
+
|
|
370
|
+
expect(dialog).toContain("nav mode");
|
|
371
|
+
expect(dialog).toContain("place the caret");
|
|
372
|
+
expect(dialog).not.toContain("grow/shrink");
|
|
373
|
+
|
|
374
|
+
// Act - a printable behind the dialog must not open a comment; escape closes it
|
|
375
|
+
await press(setup, "x");
|
|
376
|
+
expect(setup.captureCharFrame()).not.toContain("● x");
|
|
377
|
+
await press(setup, "escape");
|
|
378
|
+
|
|
379
|
+
// Assert
|
|
380
|
+
await waitForState(setup, () => !setup.captureCharFrame().includes("mark text, across blocks"));
|
|
381
|
+
expect(setup.captureCharFrame()).not.toContain("● x");
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
test("clicking the Settings nav group is inert and never corrupts the active category", async () => {
|
|
385
|
+
// Arrange - open the settings dialog from the gear
|
|
386
|
+
const setup = await renderApp();
|
|
387
|
+
|
|
388
|
+
await setup.mockMouse.click(1, 0);
|
|
389
|
+
await waitForText(setup, "Keybinds");
|
|
390
|
+
|
|
391
|
+
// the nav folder is the last "settings" on screen (the first is the dialog title)
|
|
392
|
+
const lines = setup.captureCharFrame().split("\n");
|
|
393
|
+
let folderRow = -1;
|
|
394
|
+
let folderColumn = -1;
|
|
395
|
+
|
|
396
|
+
lines.forEach((line, row) => {
|
|
397
|
+
const column = line.indexOf("settings");
|
|
398
|
+
|
|
399
|
+
if (column !== -1) {
|
|
400
|
+
folderColumn = column;
|
|
401
|
+
folderRow = row;
|
|
402
|
+
}
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
// Act - click the synthetic Settings group folder, then press a nav key. The
|
|
406
|
+
// key handler dereferences the active category, so a corrupt id would throw
|
|
407
|
+
// (the global key handler swallows the throw into console.error).
|
|
408
|
+
const keyHandlerErrors: string[] = [];
|
|
409
|
+
const originalConsoleError = console.error;
|
|
410
|
+
|
|
411
|
+
console.error = (...args: unknown[]) => keyHandlerErrors.push(args.map(String).join(" "));
|
|
412
|
+
await setup.mockMouse.click(folderColumn + 1, folderRow);
|
|
413
|
+
await pressKey(setup, "ARROW_DOWN");
|
|
414
|
+
console.error = originalConsoleError;
|
|
415
|
+
|
|
416
|
+
// Assert - no key-handler crash, and the dialog is still up
|
|
417
|
+
expect(keyHandlerErrors.filter((line) => line.includes("keypress handler"))).toEqual([]);
|
|
418
|
+
expect(setup.captureCharFrame()).toContain("Keybinds");
|
|
217
419
|
});
|
|
218
420
|
});
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/** The share toast is non-modal: while it is up, escape still cancels an open overlay. */
|
|
2
|
+
|
|
3
|
+
import { afterEach, beforeEach, describe, mock, test } from "bun:test";
|
|
4
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import React from "react";
|
|
8
|
+
import { testRender } from "@opentui/react/test-utils";
|
|
9
|
+
import { DaemonServer } from "@cueloop/daemon";
|
|
10
|
+
import type { Thread } from "@cueloop/schema";
|
|
11
|
+
import { App } from "./App";
|
|
12
|
+
import type { ShareTransport } from "./thread-controller";
|
|
13
|
+
import {
|
|
14
|
+
clickText,
|
|
15
|
+
isolateUserConfig,
|
|
16
|
+
press,
|
|
17
|
+
pressKey,
|
|
18
|
+
typeText,
|
|
19
|
+
waitForText,
|
|
20
|
+
waitForTextGone,
|
|
21
|
+
} from "./test-support";
|
|
22
|
+
|
|
23
|
+
const publishShare = mock(async () => ({ line: "ssh p_share01@cueloop.dev", copied: true }));
|
|
24
|
+
const shareTransport: ShareTransport = {
|
|
25
|
+
publish: publishShare,
|
|
26
|
+
pull: mock(async () => {
|
|
27
|
+
throw new Error("Unexpected share pull");
|
|
28
|
+
}),
|
|
29
|
+
push: mock(async () => {}),
|
|
30
|
+
watch: () => () => {},
|
|
31
|
+
revoke: async () => {},
|
|
32
|
+
parseShareId: (line) => line.match(/^ssh (\S+)@/)?.[1],
|
|
33
|
+
formatShareLine: (id: string) => "ssh " + id + "@cueloop.dev",
|
|
34
|
+
collaboratorAnnotations: () => [],
|
|
35
|
+
mergeFromShare: () => ({ annotations: [] }),
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const PLAN = `# Migration Plan\n\n## Context\n\nThe daemon persists sessions to disk atomically.\n`;
|
|
39
|
+
|
|
40
|
+
let home: string;
|
|
41
|
+
let restoreUserConfig: () => void;
|
|
42
|
+
let server: DaemonServer;
|
|
43
|
+
let session: Thread;
|
|
44
|
+
|
|
45
|
+
beforeEach(() => {
|
|
46
|
+
home = mkdtempSync(join(tmpdir(), "cueloop-toast-"));
|
|
47
|
+
restoreUserConfig = isolateUserConfig(home);
|
|
48
|
+
server = new DaemonServer({ home, idleExitMs: 0 });
|
|
49
|
+
server.start();
|
|
50
|
+
session = server.core.sessionCreate({
|
|
51
|
+
workspace: { repoRoot: "/repo", branch: "main" },
|
|
52
|
+
artifact: {
|
|
53
|
+
type: "plan",
|
|
54
|
+
content: PLAN,
|
|
55
|
+
meta: { title: "Migration Plan", planPath: "plan.md" },
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
afterEach(() => {
|
|
60
|
+
restoreUserConfig();
|
|
61
|
+
server.stop();
|
|
62
|
+
rmSync(home, { recursive: true, force: true });
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
describe("share toast", () => {
|
|
66
|
+
test("escape cancels an open composer even while the toast is up", async () => {
|
|
67
|
+
// Arrange
|
|
68
|
+
const setup = await testRender(
|
|
69
|
+
<App home={home} sessionId={session.id} shareTransport={shareTransport} />,
|
|
70
|
+
{
|
|
71
|
+
width: 120,
|
|
72
|
+
height: 32,
|
|
73
|
+
},
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
await waitForText(setup, "cueloop");
|
|
77
|
+
|
|
78
|
+
// Act: share opens the dialog, the new-link wizard publishes and raises the toast, then a composer under it
|
|
79
|
+
await pressKey(setup, "s", { ctrl: true });
|
|
80
|
+
await waitForText(setup, "+ new link");
|
|
81
|
+
await press(setup, "enter"); // step into the links body
|
|
82
|
+
await press(setup, "enter"); // activate "+ new link" - the wizard opens
|
|
83
|
+
await waitForText(setup, "link name");
|
|
84
|
+
await press(setup, "enter"); // the name field submits and creates the link
|
|
85
|
+
await waitForText(setup, "link copied");
|
|
86
|
+
await press(setup, "escape"); // close the dialog; the non-modal toast stays up
|
|
87
|
+
await waitForTextGone(setup, "share externally");
|
|
88
|
+
await clickText(setup, "daemon");
|
|
89
|
+
await typeText(setup, "x");
|
|
90
|
+
await waitForText(setup, "● x");
|
|
91
|
+
|
|
92
|
+
// Act: escape must reach the composer, not get eaten by the toast
|
|
93
|
+
await press(setup, "escape");
|
|
94
|
+
|
|
95
|
+
// Assert: the composer closed on the first escape
|
|
96
|
+
await waitForTextGone(setup, "● x");
|
|
97
|
+
});
|
|
98
|
+
});
|