@cueloop/client 0.1.0-alpha.32 → 0.1.0-alpha.34
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/package.json +27 -26
- package/src/App.autoclose.test.tsx +7 -4
- package/src/App.diff.test.tsx +131 -5
- package/src/App.export.test.tsx +23 -5
- package/src/App.plan-review.test.tsx +133 -32
- package/src/App.readonly.test.tsx +35 -13
- package/src/App.rendering.test.tsx +14 -4
- package/src/App.review-panel.test.tsx +27 -15
- package/src/App.submit-confirm.test.tsx +14 -13
- package/src/App.test.tsx +50 -13
- package/src/App.toast.test.tsx +75 -0
- package/src/App.tsx +505 -101
- package/src/App.walk.test.tsx +31 -11
- package/src/attribution.ts +8 -2
- package/src/clipboard.ts +5 -1
- package/src/components/AnnotationCard.stories.tsx +29 -10
- package/src/components/AnnotationCard.tsx +57 -37
- package/src/components/Breadcrumb.tsx +7 -1
- package/src/components/CodeBlock.stories.tsx +19 -2
- package/src/components/CodeBlock.tsx +24 -9
- package/src/components/CompletionOverlay.tsx +13 -3
- package/src/components/ConfirmCard.stories.tsx +22 -3
- package/src/components/ConfirmCard.tsx +23 -5
- package/src/components/ConfirmDialog.tsx +10 -2
- package/src/components/DiffSheet.scroll.test.tsx +123 -0
- package/src/components/DiffSheet.stories.tsx +35 -1
- package/src/components/DiffSheet.syntax.test.tsx +62 -0
- package/src/components/DiffSheet.tsx +213 -101
- package/src/components/InboxList.stories.tsx +16 -3
- package/src/components/InboxList.tsx +17 -10
- package/src/components/KeybindsDialog.stories.tsx +31 -0
- package/src/components/KeybindsDialog.tsx +58 -0
- package/src/components/MarkerPopover.stories.tsx +58 -0
- package/src/components/MarkerPopover.tsx +102 -0
- package/src/components/MenuBar.stories.tsx +37 -0
- package/src/components/MenuBar.tsx +74 -0
- package/src/components/PlanSheet.stories.tsx +35 -3
- package/src/components/PlanSheet.tsx +160 -100
- package/src/components/PromptDialog.tsx +19 -3
- package/src/components/ReviewPanel.stories.tsx +17 -19
- package/src/components/ReviewPanel.tsx +17 -32
- package/src/components/ReviewRail.stories.tsx +159 -47
- package/src/components/ReviewRail.tsx +206 -62
- package/src/components/SettingsDialog.stories.tsx +51 -3
- package/src/components/SettingsDialog.tsx +11 -5
- package/src/components/SettingsRows.stories.tsx +1 -1
- package/src/components/SettingsRows.tsx +19 -3
- package/src/components/Toast.stories.tsx +12 -0
- package/src/components/Toast.tsx +44 -0
- package/src/components/WalkWizard.stories.tsx +13 -2
- package/src/components/WalkWizard.tsx +9 -3
- package/src/components/__snapshots__/stories.test.tsx.snap +853 -623
- package/src/components/diff-sheet-layout.test.ts +127 -0
- package/src/components/diff-sheet-layout.ts +167 -0
- package/src/components/primitives/Button.stories.tsx +2 -2
- package/src/components/primitives/Button.tsx +5 -1
- package/src/components/primitives/Dialog.tsx +12 -1
- package/src/components/primitives/Tabs.tsx +40 -39
- package/src/components/stories-app.tsx +34 -6
- package/src/components/story-fixtures.ts +11 -3
- package/src/components/story.ts +3 -1
- package/src/components/syntax-highlight.ts +47 -12
- package/src/components/theme-context.stories.tsx +6 -1
- package/src/components/theme-context.tsx +7 -1
- package/src/config.test.ts +178 -2
- package/src/config.ts +121 -23
- package/src/diff-hunk-curate.test.ts +315 -0
- package/src/diff-hunk-curate.ts +233 -0
- package/src/diff-intraline.test.ts +164 -0
- package/src/diff-intraline.ts +132 -0
- package/src/diff-syntax.test.ts +96 -0
- package/src/diff-syntax.ts +154 -0
- package/src/editor.test.ts +13 -4
- package/src/editor.ts +31 -5
- package/src/herdr.ts +8 -2
- package/src/index.ts +9 -1
- package/src/integrations.ts +3 -1
- package/src/intent-dispatch.test.ts +193 -9
- package/src/intent-dispatch.ts +151 -18
- package/src/key-bindings.test.ts +19 -9
- package/src/key-bindings.ts +77 -16
- package/src/keymap.test.ts +171 -26
- package/src/keymap.ts +83 -16
- package/src/review-panel.test.ts +3 -1
- package/src/review-panel.ts +9 -2
- package/src/serve.test.ts +13 -5
- package/src/session-controller.curate.test.ts +253 -0
- package/src/session-controller.share.test.ts +77 -16
- package/src/session-controller.ts +319 -26
- package/src/share.ts +41 -8
- package/src/test-support.ts +97 -23
- package/src/theme-presets.ts +168 -0
- package/src/theme.ts +1 -3
- package/src/version.ts +5 -0
- package/src/view-diff.ts +15 -4
- package/src/view-plan.test.ts +40 -6
- package/src/view-plan.ts +71 -28
- package/src/walk.test.ts +7 -2
- package/src/walk.ts +7 -2
- package/src/components/ComposeBar.stories.tsx +0 -18
- package/src/components/ComposeBar.tsx +0 -54
|
@@ -9,7 +9,8 @@ import { testRender } from "@opentui/react/test-utils";
|
|
|
9
9
|
import { DaemonServer } from "@cueloop/daemon";
|
|
10
10
|
import { makeAnchor, parseBlocks, type ReviewSession } from "@cueloop/schema";
|
|
11
11
|
import { App } from "./App";
|
|
12
|
-
import {
|
|
12
|
+
import { DARK } from "./theme";
|
|
13
|
+
import { isolateUserConfig, press, waitForState, waitForText } from "./test-support";
|
|
13
14
|
|
|
14
15
|
const PLAN = `# Migration Plan
|
|
15
16
|
|
|
@@ -35,7 +36,11 @@ beforeEach(() => {
|
|
|
35
36
|
server.start();
|
|
36
37
|
session = server.core.sessionCreate({
|
|
37
38
|
workspace: { repoRoot: "/repo", branch: "main" },
|
|
38
|
-
artifact: {
|
|
39
|
+
artifact: {
|
|
40
|
+
type: "plan",
|
|
41
|
+
content: PLAN,
|
|
42
|
+
meta: { title: "Migration Plan", planPath: "plan.md" },
|
|
43
|
+
},
|
|
39
44
|
});
|
|
40
45
|
});
|
|
41
46
|
afterEach(() => {
|
|
@@ -56,26 +61,28 @@ async function renderObserver() {
|
|
|
56
61
|
/** Session state that any mutating verb would change. */
|
|
57
62
|
function snapshot() {
|
|
58
63
|
const stored = server.core.sessionGet(session.id);
|
|
59
|
-
return {
|
|
64
|
+
return {
|
|
65
|
+
annotations: stored.annotations.length,
|
|
66
|
+
workingCopy: stored.workingCopy,
|
|
67
|
+
status: stored.status,
|
|
68
|
+
};
|
|
60
69
|
}
|
|
61
70
|
|
|
62
71
|
describe("observer rendering", () => {
|
|
63
|
-
test("shows the observer badge and
|
|
72
|
+
test("shows the observer badge and the bottom menu", async () => {
|
|
64
73
|
// Arrange
|
|
65
74
|
const setup = await renderObserver();
|
|
66
75
|
|
|
67
76
|
// Assert
|
|
68
77
|
const frame = setup.captureCharFrame();
|
|
69
78
|
expect(frame).toContain("· observer");
|
|
70
|
-
expect(frame).toContain("
|
|
71
|
-
expect(frame).not.toContain("⏎ submit");
|
|
79
|
+
expect(frame).toContain("menu");
|
|
72
80
|
});
|
|
73
81
|
});
|
|
74
82
|
|
|
75
83
|
describe("observer verbs are blocked", () => {
|
|
76
84
|
for (const [key, verb] of [
|
|
77
85
|
["c", "comment"],
|
|
78
|
-
["s", "suggest"],
|
|
79
86
|
["x", "cut"],
|
|
80
87
|
["e", "edit"],
|
|
81
88
|
["enter", "submit"],
|
|
@@ -88,14 +95,13 @@ describe("observer verbs are blocked", () => {
|
|
|
88
95
|
await press(setup, key);
|
|
89
96
|
const frame = await waitForText(setup, "observer - read-only");
|
|
90
97
|
// no overlay opened: compose/submit bars never appear
|
|
91
|
-
expect(frame).not.toContain(
|
|
92
|
-
expect(frame).not.toContain("SUGGEST REPLACEMENT");
|
|
98
|
+
expect(frame).not.toContain('comment on "');
|
|
93
99
|
expect(frame).not.toContain("verdict ←/→");
|
|
94
100
|
expect(snapshot()).toEqual(before);
|
|
95
101
|
});
|
|
96
102
|
}
|
|
97
103
|
|
|
98
|
-
test("span mode c
|
|
104
|
+
test("span mode c is blocked too", async () => {
|
|
99
105
|
// Arrange
|
|
100
106
|
const setup = await renderObserver();
|
|
101
107
|
|
|
@@ -109,7 +115,7 @@ describe("observer verbs are blocked", () => {
|
|
|
109
115
|
// Assert
|
|
110
116
|
const frame = setup.captureCharFrame();
|
|
111
117
|
expect(frame).toContain("observer - read-only");
|
|
112
|
-
expect(frame).not.toContain(
|
|
118
|
+
expect(frame).not.toContain('comment on "');
|
|
113
119
|
expect(snapshot().annotations).toBe(0);
|
|
114
120
|
});
|
|
115
121
|
});
|
|
@@ -144,7 +150,23 @@ describe("observer navigation still works", () => {
|
|
|
144
150
|
await press(setup, "n");
|
|
145
151
|
await setup.renderOnce();
|
|
146
152
|
|
|
147
|
-
// Assert
|
|
148
|
-
|
|
153
|
+
// Assert - focus shows as the card's bright kind-colored border (transparent
|
|
154
|
+
// fill); a selected card is the only accent-colored border in the frame
|
|
155
|
+
await waitForText(setup, "COMMENT · me");
|
|
156
|
+
await waitForState(setup, () => hasBorderColor(setup, DARK.accent));
|
|
149
157
|
});
|
|
150
158
|
});
|
|
159
|
+
|
|
160
|
+
/** Whether any border character (box-drawing) is painted in the given foreground hex. */
|
|
161
|
+
function hasBorderColor(setup: Awaited<ReturnType<typeof renderObserver>>, hex: string): boolean {
|
|
162
|
+
for (const line of setup.captureSpans().lines) {
|
|
163
|
+
for (const span of line.spans) {
|
|
164
|
+
if (!/[╭╮╰╯│─┌┐└┘]/.test(span.text)) continue;
|
|
165
|
+
const [red, green, blue] = span.fg.toInts();
|
|
166
|
+
const rendered =
|
|
167
|
+
"#" + [red, green, blue].map((part) => part.toString(16).padStart(2, "0")).join("");
|
|
168
|
+
if (rendered === hex) return true;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
@@ -51,7 +51,10 @@ afterEach(() => {
|
|
|
51
51
|
});
|
|
52
52
|
|
|
53
53
|
async function frame(): Promise<string> {
|
|
54
|
-
const setup = await testRender(<App home={home} sessionId={session.id} />, {
|
|
54
|
+
const setup = await testRender(<App home={home} sessionId={session.id} />, {
|
|
55
|
+
width: 120,
|
|
56
|
+
height: 36,
|
|
57
|
+
});
|
|
55
58
|
await waitForText(setup, "Render Plan");
|
|
56
59
|
// async highlights settle within the visual-idle wait
|
|
57
60
|
await setup.waitForVisualIdle();
|
|
@@ -80,7 +83,8 @@ describe("code blocks", () => {
|
|
|
80
83
|
describe("block spacing", () => {
|
|
81
84
|
// the review panel's full-height divider paints a `│` on every row, so read
|
|
82
85
|
// the plan column (everything left of the divider) before asserting spacing
|
|
83
|
-
|
|
86
|
+
// the plan body is framed: content sits between the left border and the next │
|
|
87
|
+
const planColumn = (line: string): string => line.split("│")[1]?.trimEnd() ?? "";
|
|
84
88
|
|
|
85
89
|
test("a code block never glues to the list item above it", async () => {
|
|
86
90
|
// Arrange
|
|
@@ -111,11 +115,17 @@ describe("block spacing", () => {
|
|
|
111
115
|
workspace: { repoRoot: "/repo", branch: "main" },
|
|
112
116
|
artifact: { type: "plan", content: listPlan, meta: {} },
|
|
113
117
|
});
|
|
114
|
-
const setup = await testRender(<App home={home} sessionId={s2.id} />, {
|
|
118
|
+
const setup = await testRender(<App home={home} sessionId={s2.id} />, {
|
|
119
|
+
width: 120,
|
|
120
|
+
height: 30,
|
|
121
|
+
});
|
|
115
122
|
await waitForText(setup, "one");
|
|
116
123
|
|
|
117
124
|
// Assert
|
|
118
|
-
const lines = setup
|
|
125
|
+
const lines = setup
|
|
126
|
+
.captureCharFrame()
|
|
127
|
+
.split("\n")
|
|
128
|
+
.map((line) => line.trimEnd());
|
|
119
129
|
const one = lines.findIndex((line) => line.includes("- one"));
|
|
120
130
|
expect(lines[one + 1]).toContain("- two");
|
|
121
131
|
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/** The review panel end to end: the b key cycles expanded -> compact -> hidden -> expanded on the live App, and the collapse mode persists to the user config so it survives a restart. Rendering details are locked by the ReviewPanel stories; this suite proves the App wiring and the persistence. */
|
|
2
2
|
|
|
3
3
|
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
4
|
-
import { mkdtempSync,
|
|
4
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
5
5
|
import { tmpdir } from "node:os";
|
|
6
6
|
import { join } from "node:path";
|
|
7
7
|
import React from "react";
|
|
@@ -10,7 +10,7 @@ import { DaemonServer } from "@cueloop/daemon";
|
|
|
10
10
|
import type { ReviewSession } from "@cueloop/schema";
|
|
11
11
|
import { App } from "./App";
|
|
12
12
|
import { loadConfig } from "./config";
|
|
13
|
-
import { isolateUserConfig, press, waitForText, waitForTextGone } from "./test-support";
|
|
13
|
+
import { isolateUserConfig, press, settle, waitForText, waitForTextGone } from "./test-support";
|
|
14
14
|
|
|
15
15
|
const PLAN = "# Plan\n\nShip the thing.\n";
|
|
16
16
|
|
|
@@ -38,7 +38,10 @@ afterEach(() => {
|
|
|
38
38
|
});
|
|
39
39
|
|
|
40
40
|
async function renderApp() {
|
|
41
|
-
const setup = await testRender(<App home={home} sessionId={session.id} />, {
|
|
41
|
+
const setup = await testRender(<App home={home} sessionId={session.id} />, {
|
|
42
|
+
width: 120,
|
|
43
|
+
height: 30,
|
|
44
|
+
});
|
|
42
45
|
await waitForText(setup, "Review");
|
|
43
46
|
return setup;
|
|
44
47
|
}
|
|
@@ -47,26 +50,35 @@ describe("review panel", () => {
|
|
|
47
50
|
test("b cycles expanded -> compact -> hidden -> expanded", async () => {
|
|
48
51
|
// Arrange
|
|
49
52
|
const setup = await renderApp();
|
|
50
|
-
|
|
51
|
-
//
|
|
52
|
-
|
|
53
|
+
// the plan frame widens as the rail shrinks: expanded < compact < hidden.
|
|
54
|
+
// its width is the column of its top-right corner (the first ╮ on the row).
|
|
55
|
+
const planWidth = () =>
|
|
56
|
+
setup
|
|
57
|
+
.captureCharFrame()
|
|
58
|
+
.split("\n")
|
|
59
|
+
.find((line) => line.includes("╮"))
|
|
60
|
+
?.indexOf("╮") ?? 0;
|
|
61
|
+
|
|
62
|
+
// Assert: expanded shows the submit button and leaves room for the rail
|
|
63
|
+
expect(setup.captureCharFrame()).toContain("Submit review");
|
|
64
|
+
const expandedWidth = planWidth();
|
|
53
65
|
|
|
54
66
|
// Act
|
|
55
67
|
await press(setup, "b");
|
|
56
68
|
|
|
57
|
-
// Assert
|
|
58
|
-
await
|
|
59
|
-
|
|
60
|
-
expect(
|
|
61
|
-
expect(frame).not.toContain("Submit review");
|
|
69
|
+
// Assert: compact drops the submit button and widens the plan (rail is a strip)
|
|
70
|
+
await waitForTextGone(setup, "Submit review");
|
|
71
|
+
const compactWidth = planWidth();
|
|
72
|
+
expect(compactWidth).toBeGreaterThan(expandedWidth);
|
|
62
73
|
|
|
63
74
|
// Act
|
|
64
75
|
await press(setup, "b");
|
|
76
|
+
await settle(setup);
|
|
65
77
|
|
|
66
|
-
// Assert
|
|
67
|
-
|
|
68
|
-
expect(
|
|
69
|
-
expect(
|
|
78
|
+
// Assert: hidden spans the plan full width, no rail column
|
|
79
|
+
const hiddenWidth = planWidth();
|
|
80
|
+
expect(hiddenWidth).toBeGreaterThan(compactWidth);
|
|
81
|
+
expect(hiddenWidth).toBeGreaterThan(115);
|
|
70
82
|
|
|
71
83
|
// Act
|
|
72
84
|
await press(setup, "b");
|
|
@@ -35,7 +35,11 @@ beforeEach(() => {
|
|
|
35
35
|
server.start();
|
|
36
36
|
session = server.core.sessionCreate({
|
|
37
37
|
workspace: { repoRoot: "/repo", branch: "main" },
|
|
38
|
-
artifact: {
|
|
38
|
+
artifact: {
|
|
39
|
+
type: "plan",
|
|
40
|
+
content: PLAN,
|
|
41
|
+
meta: { title: "Migration Plan", planPath: "plan.md" },
|
|
42
|
+
},
|
|
39
43
|
});
|
|
40
44
|
});
|
|
41
45
|
afterEach(() => {
|
|
@@ -45,16 +49,17 @@ afterEach(() => {
|
|
|
45
49
|
});
|
|
46
50
|
|
|
47
51
|
async function renderApp(options: { readOnly?: boolean } = {}) {
|
|
48
|
-
const setup = await testRender(
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
+
const setup = await testRender(
|
|
53
|
+
<App home={home} sessionId={session.id} readOnly={options.readOnly ?? false} />,
|
|
54
|
+
{
|
|
55
|
+
width: 120,
|
|
56
|
+
height: 32,
|
|
57
|
+
},
|
|
58
|
+
);
|
|
52
59
|
await waitForText(setup, "cueloop");
|
|
53
60
|
return setup;
|
|
54
61
|
}
|
|
55
62
|
|
|
56
|
-
type Setup = Awaited<ReturnType<typeof renderApp>>;
|
|
57
|
-
|
|
58
63
|
/** Seed annotations directly through the daemon core (all on one block). */
|
|
59
64
|
function seedAnnotations(count: number): void {
|
|
60
65
|
const blocks = parseBlocks(PLAN);
|
|
@@ -74,7 +79,7 @@ describe("rail submit confirm", () => {
|
|
|
74
79
|
const setup = await renderApp();
|
|
75
80
|
|
|
76
81
|
// Assert
|
|
77
|
-
expect(setup.captureCharFrame()).toContain("Submit review
|
|
82
|
+
expect(setup.captureCharFrame()).toContain("Submit review");
|
|
78
83
|
|
|
79
84
|
// Act
|
|
80
85
|
await press(setup, "enter");
|
|
@@ -88,10 +93,6 @@ describe("rail submit confirm", () => {
|
|
|
88
93
|
expect(frame).toContain("[Approve]"); // nothing pending: approve default
|
|
89
94
|
expect(frame).toContain(" Submit ");
|
|
90
95
|
expect(frame).toContain(" Cancel ");
|
|
91
|
-
// key hints live in the status line, not on the buttons
|
|
92
|
-
expect(frame).toContain("verdict ←/→ · ⏎ submit · esc cancel");
|
|
93
|
-
// the bottom bar stayed a one-line hint: no detached verdict bar
|
|
94
|
-
expect(frame).not.toContain("Submit review (0) on ⏎");
|
|
95
96
|
});
|
|
96
97
|
|
|
97
98
|
test("left/right cycles the verdict selector in the card", async () => {
|
|
@@ -136,7 +137,7 @@ describe("rail submit confirm", () => {
|
|
|
136
137
|
// a bare ESC settles after the parser's escape-sequence window
|
|
137
138
|
const frame = await waitForTextGone(setup, "[Approve]");
|
|
138
139
|
expect(frame).not.toContain("0 annotations");
|
|
139
|
-
expect(frame).toContain("Submit review
|
|
140
|
+
expect(frame).toContain("Submit review");
|
|
140
141
|
});
|
|
141
142
|
|
|
142
143
|
test("enter in the card resolves the session through the controller", async () => {
|
package/src/App.test.tsx
CHANGED
|
@@ -9,7 +9,13 @@ import { testRender } from "@opentui/react/test-utils";
|
|
|
9
9
|
import { DaemonServer } from "@cueloop/daemon";
|
|
10
10
|
import type { ReviewSession } from "@cueloop/schema";
|
|
11
11
|
import { App } from "./App";
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
isolateUserConfig,
|
|
14
|
+
press,
|
|
15
|
+
typeText as type,
|
|
16
|
+
waitForState,
|
|
17
|
+
waitForText,
|
|
18
|
+
} from "./test-support";
|
|
13
19
|
|
|
14
20
|
const PLAN = `# Migration Plan
|
|
15
21
|
|
|
@@ -35,7 +41,11 @@ beforeEach(() => {
|
|
|
35
41
|
server.start();
|
|
36
42
|
session = server.core.sessionCreate({
|
|
37
43
|
workspace: { repoRoot: "/repo", branch: "main" },
|
|
38
|
-
artifact: {
|
|
44
|
+
artifact: {
|
|
45
|
+
type: "plan",
|
|
46
|
+
content: PLAN,
|
|
47
|
+
meta: { title: "Migration Plan", planPath: "plan.md" },
|
|
48
|
+
},
|
|
39
49
|
});
|
|
40
50
|
});
|
|
41
51
|
afterEach(() => {
|
|
@@ -65,8 +75,8 @@ describe("plan rendering", () => {
|
|
|
65
75
|
expect(frame).toContain("Context");
|
|
66
76
|
expect(frame).toContain("persists sessions to disk atomically");
|
|
67
77
|
expect(frame).toContain("- move the store");
|
|
68
|
-
expect(frame).toContain("Review
|
|
69
|
-
expect(frame).toContain("Submit review
|
|
78
|
+
expect(frame).toContain("Review");
|
|
79
|
+
expect(frame).toContain("Submit review");
|
|
70
80
|
});
|
|
71
81
|
});
|
|
72
82
|
|
|
@@ -138,8 +148,8 @@ describe("keyboard grammar", () => {
|
|
|
138
148
|
// Act
|
|
139
149
|
await press(setup, "x");
|
|
140
150
|
|
|
141
|
-
// Assert
|
|
142
|
-
await waitForText(setup, "
|
|
151
|
+
// Assert - the rail shows the cut as a removal card titled CUT · me
|
|
152
|
+
await waitForText(setup, "CUT · me");
|
|
143
153
|
expect(server.core.sessionGet(session.id).workingCopy).not.toContain("move the store");
|
|
144
154
|
|
|
145
155
|
// Act
|
|
@@ -179,7 +189,7 @@ describe("submit", () => {
|
|
|
179
189
|
await press(setup, "c");
|
|
180
190
|
await type(setup, "Needs a phase list.");
|
|
181
191
|
await press(setup, "enter");
|
|
182
|
-
await waitForText(setup, "
|
|
192
|
+
await waitForText(setup, "Needs a phase list");
|
|
183
193
|
|
|
184
194
|
// Act
|
|
185
195
|
await press(setup, "enter"); // open submit
|
|
@@ -221,19 +231,46 @@ describe("submit", () => {
|
|
|
221
231
|
});
|
|
222
232
|
|
|
223
233
|
describe("inbox", () => {
|
|
224
|
-
test("inbox mode
|
|
234
|
+
test("inbox mode wears the review chrome (header + menu bar) and opens a session", async () => {
|
|
225
235
|
// Arrange
|
|
226
236
|
const setup = await testRender(<App home={home} />, { width: 120, height: 32 });
|
|
227
|
-
await waitForText(setup, "
|
|
237
|
+
await waitForText(setup, "cueloop");
|
|
228
238
|
|
|
229
|
-
// Assert
|
|
230
|
-
|
|
231
|
-
expect(
|
|
239
|
+
// Assert - the same header/menu chrome as review, plus the session row
|
|
240
|
+
const frame = setup.captureCharFrame();
|
|
241
|
+
expect(frame).toContain("cueloop · resume");
|
|
242
|
+
expect(frame).toContain("Migration Plan");
|
|
243
|
+
expect(frame).toContain("menu"); // the shared MenuBar sits at the bottom
|
|
244
|
+
expect(frame).not.toContain("inbox ("); // the old inline header is gone
|
|
232
245
|
|
|
233
246
|
// Act
|
|
234
247
|
await press(setup, "enter");
|
|
235
248
|
|
|
236
249
|
// Assert
|
|
237
|
-
await waitForText(setup, "Submit review
|
|
250
|
+
await waitForText(setup, "Submit review");
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
test("the menu opens from the inbox and escape is not a trap", async () => {
|
|
254
|
+
// Arrange
|
|
255
|
+
const setup = await testRender(<App home={home} />, { width: 120, height: 32 });
|
|
256
|
+
await waitForText(setup, "cueloop · resume");
|
|
257
|
+
const lines = setup.captureCharFrame().split("\n");
|
|
258
|
+
const menuRow = lines.findIndex((line) => line.includes("menu"));
|
|
259
|
+
const menuColumn = lines[menuRow]!.indexOf("menu");
|
|
260
|
+
|
|
261
|
+
// Act - open the drop-up from the shared menu bar
|
|
262
|
+
await setup.mockMouse.click(menuColumn + 1, menuRow);
|
|
263
|
+
|
|
264
|
+
// Assert - the drop-up options appear
|
|
265
|
+
await waitForText(setup, "Keybinds");
|
|
266
|
+
expect(setup.captureCharFrame()).toContain("Settings");
|
|
267
|
+
|
|
268
|
+
// Act - escape closes the menu (not a trap), and the inbox nav still works
|
|
269
|
+
await press(setup, "escape");
|
|
270
|
+
await waitForState(setup, () => !setup.captureCharFrame().includes("Keybinds"));
|
|
271
|
+
await press(setup, "enter");
|
|
272
|
+
|
|
273
|
+
// Assert
|
|
274
|
+
await waitForText(setup, "Submit review");
|
|
238
275
|
});
|
|
239
276
|
});
|
|
@@ -0,0 +1,75 @@
|
|
|
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 { ReviewSession } from "@cueloop/schema";
|
|
11
|
+
|
|
12
|
+
// the controller reaches the gateway through "./share"; stub it so `S` yields a toast
|
|
13
|
+
const publishShare = mock(async () => ({ line: "ssh p_share01@cueloop.dev", copied: true }));
|
|
14
|
+
mock.module("./share", () => ({
|
|
15
|
+
publishShare,
|
|
16
|
+
pullShare: mock(async () => null),
|
|
17
|
+
pushShare: mock(async () => {}),
|
|
18
|
+
shareIdFromLine: (line: string) => line.match(/^ssh (\S+)@/)?.[1],
|
|
19
|
+
collaboratorAnnotations: () => [],
|
|
20
|
+
}));
|
|
21
|
+
|
|
22
|
+
const { App } = await import("./App");
|
|
23
|
+
const { isolateUserConfig, press, waitForText, waitForTextGone } = await import("./test-support");
|
|
24
|
+
|
|
25
|
+
const PLAN = `# Migration Plan\n\n## Context\n\nThe daemon persists sessions to disk atomically.\n`;
|
|
26
|
+
|
|
27
|
+
let home: string;
|
|
28
|
+
let restoreUserConfig: () => void;
|
|
29
|
+
let server: DaemonServer;
|
|
30
|
+
let session: ReviewSession;
|
|
31
|
+
|
|
32
|
+
beforeEach(() => {
|
|
33
|
+
home = mkdtempSync(join(tmpdir(), "cueloop-toast-"));
|
|
34
|
+
restoreUserConfig = isolateUserConfig(home);
|
|
35
|
+
server = new DaemonServer({ home, idleExitMs: 0 });
|
|
36
|
+
server.start();
|
|
37
|
+
session = server.core.sessionCreate({
|
|
38
|
+
workspace: { repoRoot: "/repo", branch: "main" },
|
|
39
|
+
artifact: {
|
|
40
|
+
type: "plan",
|
|
41
|
+
content: PLAN,
|
|
42
|
+
meta: { title: "Migration Plan", planPath: "plan.md" },
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
afterEach(() => {
|
|
47
|
+
restoreUserConfig();
|
|
48
|
+
server.stop();
|
|
49
|
+
rmSync(home, { recursive: true, force: true });
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe("share toast", () => {
|
|
53
|
+
test("escape cancels an open composer even while the toast is up", async () => {
|
|
54
|
+
// Arrange
|
|
55
|
+
const setup = await testRender(<App home={home} sessionId={session.id} />, {
|
|
56
|
+
width: 120,
|
|
57
|
+
height: 32,
|
|
58
|
+
});
|
|
59
|
+
await waitForText(setup, "cueloop");
|
|
60
|
+
|
|
61
|
+
// Act: share raises the toast, then open a composer under it
|
|
62
|
+
await press(setup, "S");
|
|
63
|
+
await waitForText(setup, "share link copied");
|
|
64
|
+
await press(setup, "j");
|
|
65
|
+
await press(setup, "j");
|
|
66
|
+
await press(setup, "c");
|
|
67
|
+
await waitForText(setup, 'comment on "');
|
|
68
|
+
|
|
69
|
+
// Act: escape must reach the composer, not get eaten by the toast
|
|
70
|
+
await press(setup, "escape");
|
|
71
|
+
|
|
72
|
+
// Assert: the composer closed on the first escape
|
|
73
|
+
await waitForTextGone(setup, 'comment on "');
|
|
74
|
+
});
|
|
75
|
+
});
|