@cueloop/client 0.1.0-alpha.76 → 0.1.0-alpha.78
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 +5 -5
- package/src/App.autoclose.test.tsx +3 -3
- package/src/App.diff.test.tsx +14 -2
- package/src/App.export.test.tsx +1 -2
- package/src/App.identity.test.tsx +78 -0
- package/src/App.plan-review.test.tsx +16 -0
- package/src/App.share-choice.test.tsx +105 -0
- package/src/App.submit-confirm.test.tsx +34 -5
- package/src/App.test.tsx +2 -2
- package/src/App.toast.test.tsx +3 -1
- package/src/App.tsx +180 -5
- package/src/App.walk.test.tsx +2 -2
- package/src/__snapshots__/view-plan.regression.test.ts.snap +1 -1
- package/src/ansi-screen.test.ts +28 -0
- package/src/ansi-screen.ts +72 -0
- package/src/app-screens.tsx +20 -3
- package/src/app-view-model.ts +4 -0
- package/src/components/AnnotatableFileView.tsx +10 -2
- package/src/components/AnnotationCards.tsx +40 -6
- package/src/components/AnsiScreen.stories.tsx +12 -0
- package/src/components/AnsiScreen.tsx +28 -0
- package/src/components/CompletionOverlay.stories.tsx +15 -1
- package/src/components/CompletionOverlay.tsx +22 -5
- package/src/components/ConfirmCard.stories.tsx +2 -0
- package/src/components/ConfirmCard.tsx +45 -18
- package/src/components/ConfirmDialog.tsx +11 -12
- package/src/components/DiffContentView.tsx +9 -5
- package/src/components/FileContentsView.tsx +1 -1
- package/src/components/GridTabContent.tsx +2 -0
- package/src/components/ManageAccessDialog.stories.tsx +36 -0
- package/src/components/ManageAccessDialog.test.tsx +94 -0
- package/src/components/ManageAccessDialog.tsx +122 -0
- package/src/components/PromptDialog.test.tsx +2 -2
- package/src/components/PromptDialog.tsx +9 -10
- package/src/components/ShareChoiceDialog.stories.tsx +34 -0
- package/src/components/ShareChoiceDialog.test.tsx +103 -0
- package/src/components/ShareChoiceDialog.tsx +80 -0
- package/src/components/SlashComposer.stories.tsx +37 -0
- package/src/components/SlashComposer.tsx +87 -0
- package/src/components/ThreadTree.tsx +11 -6
- package/src/components/ThreadView.selection.test.tsx +88 -1
- package/src/components/ThreadView.slash.test.tsx +0 -1
- package/src/components/ThreadView.tsx +71 -8
- package/src/components/Toast.tsx +0 -5
- package/src/components/WalkWizard.stories.tsx +2 -2
- package/src/components/WalkWizard.tsx +10 -12
- package/src/components/__snapshots__/stories.test.tsx.snap +277 -61
- package/src/components/primitives/Card.tsx +3 -0
- package/src/components/primitives/DialogActions.stories.tsx +20 -0
- package/src/components/primitives/DialogActions.tsx +39 -0
- package/src/components/session-tree.ts +7 -6
- package/src/components/stories-app.tsx +13 -4
- package/src/components/stories.test.tsx +8 -1
- package/src/components/story-tree.test.ts +32 -24
- package/src/components/story.ts +32 -15
- package/src/config-identity.test.ts +78 -0
- package/src/config.ts +44 -1
- package/src/diff-intraline.test.ts +75 -2
- package/src/diff-intraline.ts +95 -26
- package/src/github-identity.test.ts +58 -0
- package/src/github-identity.ts +41 -0
- package/src/intent-dispatch.test.ts +17 -2
- package/src/intent-dispatch.ts +7 -1
- package/src/keymap.test.ts +2 -2
- package/src/keymap.ts +1 -1
- package/src/pasted-image.test.ts +67 -0
- package/src/pasted-image.ts +60 -0
- package/src/run.ts +31 -19
- package/src/serve.ts +1 -1
- package/src/share.ts +11 -3
- package/src/slash-palette.test.ts +17 -0
- package/src/slash-palette.ts +11 -0
- package/src/thread-controller.curate.test.ts +1 -0
- package/src/thread-controller.derivation.test.ts +62 -0
- package/src/thread-controller.live-diff.test.ts +1 -0
- package/src/thread-controller.optimistic.test.ts +69 -0
- package/src/thread-controller.share.test.ts +36 -2
- package/src/thread-controller.switch-cache.test.ts +146 -0
- package/src/thread-controller.tree.test.ts +1 -0
- package/src/thread-controller.ts +171 -24
- package/src/use-annotation-surface.tsx +53 -29
- package/src/use-settings-dialog.tsx +24 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cueloop/client",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.78",
|
|
4
4
|
"description": "The cueloop review UI: projection renderer, keyboard grammar, review rail",
|
|
5
5
|
"homepage": "https://github.com/mmurakaru/cueloop#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -27,10 +27,10 @@
|
|
|
27
27
|
"access": "public"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@cueloop/daemon": "0.1.0-alpha.
|
|
31
|
-
"@cueloop/extension-api": "0.1.0-alpha.
|
|
32
|
-
"@cueloop/integration-obsidian": "0.1.0-alpha.
|
|
33
|
-
"@cueloop/schema": "0.1.0-alpha.
|
|
30
|
+
"@cueloop/daemon": "0.1.0-alpha.78",
|
|
31
|
+
"@cueloop/extension-api": "0.1.0-alpha.78",
|
|
32
|
+
"@cueloop/integration-obsidian": "0.1.0-alpha.78",
|
|
33
|
+
"@cueloop/schema": "0.1.0-alpha.78",
|
|
34
34
|
"@opentui/core": "^0.5.8",
|
|
35
35
|
"@opentui/keymap": "0.5.8",
|
|
36
36
|
"@opentui/react": "^0.5.8",
|
|
@@ -57,7 +57,7 @@ type Setup = Awaited<ReturnType<typeof renderApp>>;
|
|
|
57
57
|
|
|
58
58
|
async function submitApprove(setup: Setup): Promise<void> {
|
|
59
59
|
await pressKey(setup, "RETURN", { meta: true }); // open submit (approve default: no pending items)
|
|
60
|
-
await
|
|
60
|
+
await pressKey(setup, "RETURN", { meta: true }); // confirm
|
|
61
61
|
await waitForText(setup, "review approved");
|
|
62
62
|
}
|
|
63
63
|
|
|
@@ -74,8 +74,8 @@ describe("completion overlay", () => {
|
|
|
74
74
|
const frame = await waitForText(setup, "closing in 5s");
|
|
75
75
|
|
|
76
76
|
expect(frame).toContain("review approved");
|
|
77
|
-
expect(frame).toContain("close
|
|
78
|
-
expect(frame).toContain("
|
|
77
|
+
expect(frame).toContain("close");
|
|
78
|
+
expect(frame).toContain("back to plan");
|
|
79
79
|
|
|
80
80
|
// Act
|
|
81
81
|
await press(setup, "enter");
|
package/src/App.diff.test.tsx
CHANGED
|
@@ -13,7 +13,6 @@ import {
|
|
|
13
13
|
clickText,
|
|
14
14
|
dragText,
|
|
15
15
|
isolateUserConfig,
|
|
16
|
-
press,
|
|
17
16
|
pressKey,
|
|
18
17
|
renderReadyApp,
|
|
19
18
|
typeText,
|
|
@@ -95,7 +94,7 @@ describe("diff review", () => {
|
|
|
95
94
|
|
|
96
95
|
// Act - submit with the session chord (cmd+enter, no composer open), confirm request_changes
|
|
97
96
|
await pressKey(setup, "RETURN", { meta: true });
|
|
98
|
-
await
|
|
97
|
+
await pressKey(setup, "RETURN", { meta: true });
|
|
99
98
|
|
|
100
99
|
// Assert
|
|
101
100
|
await waitForText(setup, "feedback sent");
|
|
@@ -105,6 +104,19 @@ describe("diff review", () => {
|
|
|
105
104
|
expect(resolved.verdict!.feedback).toContain("Map needs an eviction story.");
|
|
106
105
|
});
|
|
107
106
|
|
|
107
|
+
test("pasting an image into a diff comment drops in an [Image #n] placeholder", async () => {
|
|
108
|
+
// Arrange - mark the added line and open the inline comment composer
|
|
109
|
+
const setup = await renderApp();
|
|
110
|
+
await dragText(setup, "new Map()", "new Map()", "new Map()".length);
|
|
111
|
+
await typeText(setup, "see ");
|
|
112
|
+
|
|
113
|
+
// Act - a burst of control bytes stands in for the binary an image paste delivers
|
|
114
|
+
await setup.mockInput.pasteBracketedText("");
|
|
115
|
+
|
|
116
|
+
// Assert - the same placeholder the summary composer shows
|
|
117
|
+
await waitForText(setup, "see [Image #1]");
|
|
118
|
+
});
|
|
119
|
+
|
|
108
120
|
test("a comment reopens with its mark painted on the code", async () => {
|
|
109
121
|
// Arrange - a stored comment anchored to the added line's words
|
|
110
122
|
server.core.sessionAnnotate(session.id, {
|
package/src/App.export.test.tsx
CHANGED
|
@@ -18,7 +18,6 @@ import type { Thread } from "@cueloop/schema";
|
|
|
18
18
|
import { App } from "./App";
|
|
19
19
|
import {
|
|
20
20
|
isolateUserConfig,
|
|
21
|
-
press,
|
|
22
21
|
waitForState,
|
|
23
22
|
waitForText,
|
|
24
23
|
pressKey,
|
|
@@ -81,7 +80,7 @@ describe("obsidian export on resolve", () => {
|
|
|
81
80
|
await waitForText(setup, "[Approve]");
|
|
82
81
|
|
|
83
82
|
// Act
|
|
84
|
-
await
|
|
83
|
+
await pressKey(setup, "RETURN", { meta: true }); // submit
|
|
85
84
|
|
|
86
85
|
// Assert
|
|
87
86
|
// the export is an async round-trip after resolve; the status line lands
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/** Reviewer identity surfaces: the Account settings tab and the author-name tooltip on a comment dot. */
|
|
2
|
+
|
|
3
|
+
import { afterEach, beforeEach, expect, 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 { DaemonServer } from "@cueloop/daemon";
|
|
9
|
+
import { App } from "./App";
|
|
10
|
+
import { isolateUserConfig, locateText, renderReadyApp, waitForText } from "./test-support";
|
|
11
|
+
|
|
12
|
+
const PLAN = "# Migration Plan\n\nThe daemon persists sessions to disk atomically.\n";
|
|
13
|
+
|
|
14
|
+
let home: string;
|
|
15
|
+
let restoreUserConfig: () => void;
|
|
16
|
+
let server: DaemonServer;
|
|
17
|
+
let sessionId: string;
|
|
18
|
+
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
home = mkdtempSync(join(tmpdir(), "cueloop-identity-"));
|
|
21
|
+
restoreUserConfig = isolateUserConfig(home);
|
|
22
|
+
server = new DaemonServer({ home, idleExitMs: 0 });
|
|
23
|
+
server.start();
|
|
24
|
+
const session = server.core.sessionCreate({
|
|
25
|
+
workspace: { repoRoot: "/repo", branch: "main" },
|
|
26
|
+
artifact: {
|
|
27
|
+
type: "plan",
|
|
28
|
+
content: PLAN,
|
|
29
|
+
meta: { title: "Migration Plan", planPath: "plan.md" },
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
sessionId = session.id;
|
|
33
|
+
});
|
|
34
|
+
afterEach(() => {
|
|
35
|
+
restoreUserConfig();
|
|
36
|
+
server.stop();
|
|
37
|
+
rmSync(home, { recursive: true, force: true });
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
async function renderApp() {
|
|
41
|
+
return renderReadyApp(<App home={home} sessionId={sessionId} />, { width: 120, height: 32 });
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
test("the Account settings tab shows the display name row and the GitHub sync action", async () => {
|
|
45
|
+
const setup = await renderApp();
|
|
46
|
+
|
|
47
|
+
await setup.mockMouse.click(1, 0);
|
|
48
|
+
await waitForText(setup, "Settings");
|
|
49
|
+
const menu = setup.captureCharFrame().split("\n");
|
|
50
|
+
const settingsRow = menu.findIndex((line) => line.includes("Settings"));
|
|
51
|
+
|
|
52
|
+
await setup.mockMouse.click(menu[settingsRow]!.indexOf("Settings") + 1, settingsRow);
|
|
53
|
+
await waitForText(setup, "Account");
|
|
54
|
+
const dialog = setup.captureCharFrame().split("\n");
|
|
55
|
+
const accountRow = dialog.findIndex((line) => line.includes("Account"));
|
|
56
|
+
|
|
57
|
+
await setup.mockMouse.click(dialog[accountRow]!.indexOf("Account") + 1, accountRow);
|
|
58
|
+
await waitForText(setup, "Display name");
|
|
59
|
+
expect(setup.captureCharFrame()).toContain("Sync from GitHub");
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("hovering an own comment's dot surfaces the author tooltip", async () => {
|
|
63
|
+
server.core.sessionAnnotate(sessionId, {
|
|
64
|
+
id: "a_own",
|
|
65
|
+
kind: "comment",
|
|
66
|
+
anchor: { quote: "atomically", prefix: "sessions to disk ", suffix: "." },
|
|
67
|
+
body: "My note.",
|
|
68
|
+
});
|
|
69
|
+
const setup = await renderApp();
|
|
70
|
+
|
|
71
|
+
await waitForText(setup, "● My note.");
|
|
72
|
+
// the tooltip is not painted until the pointer is over the dot
|
|
73
|
+
expect(setup.captureCharFrame()).not.toContain("you");
|
|
74
|
+
const dot = locateText(setup, "● My note.");
|
|
75
|
+
|
|
76
|
+
await setup.mockMouse.moveTo(dot.column, dot.row);
|
|
77
|
+
await waitForText(setup, "you");
|
|
78
|
+
});
|
|
@@ -228,6 +228,22 @@ describe("the mark stays painted while composing", () => {
|
|
|
228
228
|
await waitForState(setup, () => server.core.sessionGet(session.id).annotations.length === 1);
|
|
229
229
|
await waitForState(setup, () => backgroundsOf(setup, "The daemon").includes(THREAD_MARK));
|
|
230
230
|
}, 60_000);
|
|
231
|
+
|
|
232
|
+
test("backspace on an empty draft dismisses the composer back to the mark", async () => {
|
|
233
|
+
// Arrange
|
|
234
|
+
const setup = await renderApp();
|
|
235
|
+
|
|
236
|
+
// Act: mark, type one character, then delete it and backspace again on the now-empty draft
|
|
237
|
+
await dragText(setup, "The daemon", "daemon persists", "daemon".length);
|
|
238
|
+
await type(setup, "x");
|
|
239
|
+
await waitForText(setup, "● x");
|
|
240
|
+
await press(setup, "backspace");
|
|
241
|
+
await press(setup, "backspace");
|
|
242
|
+
|
|
243
|
+
// Assert: the composer is gone and the mark stays, ready to re-type
|
|
244
|
+
await waitForTextGone(setup, "● x");
|
|
245
|
+
expect(backgroundsOf(setup, "The daemon")).toContain(THREAD_MARK);
|
|
246
|
+
}, 60_000);
|
|
231
247
|
});
|
|
232
248
|
|
|
233
249
|
describe("compose newline convention", () => {
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/** The share shortcut opens an app-level public/private choice reachable from any view. */
|
|
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 { isolateUserConfig, press, pressKey, waitForText, waitForTextGone } from "./test-support";
|
|
14
|
+
|
|
15
|
+
const shareTransport: ShareTransport = {
|
|
16
|
+
publish: mock(async () => ({ line: "ssh p_share01@cueloop.dev", copied: true })),
|
|
17
|
+
pull: mock(async () => {
|
|
18
|
+
throw new Error("Unexpected share pull");
|
|
19
|
+
}),
|
|
20
|
+
push: mock(async () => {}),
|
|
21
|
+
watch: () => () => {},
|
|
22
|
+
parseShareId: (line) => line.match(/^ssh (\S+)@/)?.[1],
|
|
23
|
+
collaboratorAnnotations: () => [],
|
|
24
|
+
mergeFromShare: () => ({ annotations: [] }),
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const PLAN = `# Migration Plan\n\n## Context\n\nThe daemon persists sessions to disk atomically.\n`;
|
|
28
|
+
|
|
29
|
+
let home: string;
|
|
30
|
+
let restoreUserConfig: () => void;
|
|
31
|
+
let server: DaemonServer;
|
|
32
|
+
let session: Thread;
|
|
33
|
+
|
|
34
|
+
beforeEach(() => {
|
|
35
|
+
home = mkdtempSync(join(tmpdir(), "cueloop-share-choice-"));
|
|
36
|
+
restoreUserConfig = isolateUserConfig(home);
|
|
37
|
+
server = new DaemonServer({ home, idleExitMs: 0 });
|
|
38
|
+
server.start();
|
|
39
|
+
session = server.core.sessionCreate({
|
|
40
|
+
workspace: { repoRoot: "/repo", branch: "main" },
|
|
41
|
+
artifact: {
|
|
42
|
+
type: "plan",
|
|
43
|
+
content: PLAN,
|
|
44
|
+
meta: { title: "Migration Plan", planPath: "plan.md" },
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
afterEach(() => {
|
|
49
|
+
restoreUserConfig();
|
|
50
|
+
server.stop();
|
|
51
|
+
rmSync(home, { recursive: true, force: true });
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
describe("share choice", () => {
|
|
55
|
+
test("the shortcut opens the public/private choice", async () => {
|
|
56
|
+
// Arrange
|
|
57
|
+
const setup = await testRender(
|
|
58
|
+
<App home={home} sessionId={session.id} shareTransport={shareTransport} />,
|
|
59
|
+
{ width: 120, height: 32 },
|
|
60
|
+
);
|
|
61
|
+
await waitForText(setup, "cueloop");
|
|
62
|
+
|
|
63
|
+
// Act
|
|
64
|
+
await pressKey(setup, "s", { ctrl: true });
|
|
65
|
+
|
|
66
|
+
// Assert
|
|
67
|
+
await waitForText(setup, "public link");
|
|
68
|
+
await waitForText(setup, "private link");
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("the private choice opens the manage-access surface", async () => {
|
|
72
|
+
// Arrange
|
|
73
|
+
const setup = await testRender(
|
|
74
|
+
<App home={home} sessionId={session.id} shareTransport={shareTransport} />,
|
|
75
|
+
{ width: 120, height: 32 },
|
|
76
|
+
);
|
|
77
|
+
await waitForText(setup, "cueloop");
|
|
78
|
+
|
|
79
|
+
// Act: open the choice, move to private, select it
|
|
80
|
+
await pressKey(setup, "s", { ctrl: true });
|
|
81
|
+
await waitForText(setup, "private link");
|
|
82
|
+
await press(setup, "down");
|
|
83
|
+
await press(setup, "enter");
|
|
84
|
+
|
|
85
|
+
// Assert
|
|
86
|
+
await waitForText(setup, "Manage access");
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("escape cancels the choice without sharing", async () => {
|
|
90
|
+
// Arrange
|
|
91
|
+
const setup = await testRender(
|
|
92
|
+
<App home={home} sessionId={session.id} shareTransport={shareTransport} />,
|
|
93
|
+
{ width: 120, height: 32 },
|
|
94
|
+
);
|
|
95
|
+
await waitForText(setup, "cueloop");
|
|
96
|
+
|
|
97
|
+
// Act
|
|
98
|
+
await pressKey(setup, "s", { ctrl: true });
|
|
99
|
+
await waitForText(setup, "public link");
|
|
100
|
+
await press(setup, "escape");
|
|
101
|
+
|
|
102
|
+
// Assert
|
|
103
|
+
await waitForTextGone(setup, "public link");
|
|
104
|
+
});
|
|
105
|
+
});
|
|
@@ -99,8 +99,8 @@ describe("send message confirm", () => {
|
|
|
99
99
|
const frame = setup.captureCharFrame();
|
|
100
100
|
|
|
101
101
|
expect(frame).toContain("[Approve]");
|
|
102
|
-
expect(frame).toContain(" send
|
|
103
|
-
expect(frame).toContain("
|
|
102
|
+
expect(frame).toContain(" send ");
|
|
103
|
+
expect(frame).toContain(" cancel ");
|
|
104
104
|
});
|
|
105
105
|
|
|
106
106
|
test("left/right cycles the verdict selector in the overlay", async () => {
|
|
@@ -146,7 +146,7 @@ describe("send message confirm", () => {
|
|
|
146
146
|
// Assert - a bare ESC settles after the parser's escape-sequence window
|
|
147
147
|
const frame = await waitForTextGone(setup, "[Approve]");
|
|
148
148
|
|
|
149
|
-
expect(frame).not.toContain("
|
|
149
|
+
expect(frame).not.toContain(" cancel ");
|
|
150
150
|
});
|
|
151
151
|
|
|
152
152
|
test("enter in the overlay resolves the session through the controller", async () => {
|
|
@@ -162,7 +162,7 @@ describe("send message confirm", () => {
|
|
|
162
162
|
|
|
163
163
|
// Act
|
|
164
164
|
await setup.mockInput.typeText("Tighten the steps.");
|
|
165
|
-
await
|
|
165
|
+
await pressKey(setup, "RETURN", { meta: true });
|
|
166
166
|
|
|
167
167
|
// Assert - the completion flow after submit is unchanged
|
|
168
168
|
await waitForText(setup, "feedback sent");
|
|
@@ -172,6 +172,35 @@ describe("send message confirm", () => {
|
|
|
172
172
|
expect(stored.verdict!.kind).toBe("request_changes");
|
|
173
173
|
});
|
|
174
174
|
|
|
175
|
+
test("typing / in the summary opens the skills/actions palette", async () => {
|
|
176
|
+
// Arrange
|
|
177
|
+
seedAnnotations(1);
|
|
178
|
+
const setup = await renderApp();
|
|
179
|
+
await pressKey(setup, "RETURN", { meta: true });
|
|
180
|
+
await waitForText(setup, "[Changes]");
|
|
181
|
+
|
|
182
|
+
// Act
|
|
183
|
+
await setup.mockInput.typeText("/restate");
|
|
184
|
+
|
|
185
|
+
// Assert - the same slash palette the inline composer shows
|
|
186
|
+
await waitForText(setup, "restate-simplified");
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test("pasting an image into the summary drops in an [Image #n] placeholder", async () => {
|
|
190
|
+
// Arrange
|
|
191
|
+
const setup = await renderApp();
|
|
192
|
+
await pressKey(setup, "RETURN", { meta: true });
|
|
193
|
+
await waitForText(setup, "[Approve]");
|
|
194
|
+
|
|
195
|
+
// Act - a burst of control bytes stands in for the binary an image paste delivers
|
|
196
|
+
await setup.mockInput.typeText("look at this ");
|
|
197
|
+
await setup.mockInput.pasteBracketedText("");
|
|
198
|
+
|
|
199
|
+
// Assert - the raw bytes never reach the draft; a numbered placeholder does
|
|
200
|
+
await waitForText(setup, "[Image #1]");
|
|
201
|
+
expect(setup.captureCharFrame()).toContain("look at this [Image #1]");
|
|
202
|
+
});
|
|
203
|
+
|
|
175
204
|
test("read-only observers cannot open the confirm overlay", async () => {
|
|
176
205
|
// Arrange
|
|
177
206
|
seedAnnotations(1);
|
|
@@ -185,7 +214,7 @@ describe("send message confirm", () => {
|
|
|
185
214
|
const frame = setup.captureCharFrame();
|
|
186
215
|
|
|
187
216
|
expect(frame).not.toContain("[Changes]");
|
|
188
|
-
expect(frame).not.toContain("
|
|
217
|
+
expect(frame).not.toContain(" cancel ");
|
|
189
218
|
expect(server.core.sessionGet(session.id).status).toBe("pending");
|
|
190
219
|
});
|
|
191
220
|
});
|
package/src/App.test.tsx
CHANGED
|
@@ -207,7 +207,7 @@ describe("submit", () => {
|
|
|
207
207
|
|
|
208
208
|
// Act
|
|
209
209
|
await type(setup, "Expand the steps.");
|
|
210
|
-
await
|
|
210
|
+
await pressKey(setup, "RETURN", { meta: true });
|
|
211
211
|
|
|
212
212
|
// Assert
|
|
213
213
|
await waitForText(setup, "feedback sent");
|
|
@@ -231,7 +231,7 @@ describe("submit", () => {
|
|
|
231
231
|
await waitForText(setup, "[Approve]"); // no pending items → approve default
|
|
232
232
|
|
|
233
233
|
// Act
|
|
234
|
-
await
|
|
234
|
+
await pressKey(setup, "RETURN", { meta: true });
|
|
235
235
|
|
|
236
236
|
// Assert
|
|
237
237
|
await waitForState(setup, () => server.core.sessionGet(session.id).verdict !== undefined);
|
package/src/App.toast.test.tsx
CHANGED
|
@@ -73,8 +73,10 @@ describe("share toast", () => {
|
|
|
73
73
|
|
|
74
74
|
await waitForText(setup, "cueloop");
|
|
75
75
|
|
|
76
|
-
// Act: share raises the toast, then
|
|
76
|
+
// Act: share opens the choice, enter picks public and raises the toast, then a composer under it
|
|
77
77
|
await pressKey(setup, "s", { ctrl: true });
|
|
78
|
+
await waitForText(setup, "public link");
|
|
79
|
+
await press(setup, "enter");
|
|
78
80
|
await waitForText(setup, "share link copied");
|
|
79
81
|
await clickText(setup, "daemon");
|
|
80
82
|
await typeText(setup, "x");
|