@cueloop/client 0.1.0-alpha.63 → 0.1.0-alpha.65
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 +7 -6
- package/src/App.autoclose.test.tsx +2 -2
- package/src/App.export.test.tsx +2 -2
- package/src/App.plan-review.test.tsx +76 -247
- package/src/App.readonly.test.tsx +80 -35
- package/src/App.rendering.test.tsx +5 -6
- package/src/App.review-panel.test.tsx +29 -32
- package/src/App.submit-confirm.test.tsx +27 -12
- package/src/App.test.tsx +73 -49
- package/src/App.toast.test.tsx +34 -22
- package/src/App.tree.test.tsx +224 -0
- package/src/App.tsx +134 -87
- package/src/app-screens.tsx +27 -1
- package/src/app-view-model.ts +7 -109
- package/src/components/AnnotationCard.tsx +2 -2
- package/src/components/DiffSheet.scroll.test.tsx +5 -2
- package/src/components/DiffSheet.tsx +1 -6
- package/src/components/KeybindsDialog.tsx +61 -17
- package/src/components/PrototypeSheet.tsx +5 -9
- package/src/components/ReviewPanel.stories.tsx +14 -0
- package/src/components/ReviewRail.stories.tsx +14 -0
- package/src/components/ReviewRail.tsx +15 -6
- package/src/components/ThreadView.selection.test.tsx +812 -0
- package/src/components/ThreadView.stories.tsx +26 -0
- package/src/components/ThreadView.tsx +1620 -0
- package/src/components/TreePane.stories.tsx +106 -0
- package/src/components/TreePane.tsx +156 -0
- package/src/components/__snapshots__/stories.test.tsx.snap +120 -172
- package/src/components/primitives/Tabs.tsx +1 -1
- package/src/components/story-fixtures.ts +6 -2
- package/src/components/story.ts +16 -4
- package/src/components/syntax-highlight.ts +10 -2
- package/src/components/terminal-pane.test.tsx +1 -1
- package/src/components/terminal-pane.ts +7 -1
- package/src/config.ts +112 -99
- package/src/diff-hunk-curate.ts +4 -104
- package/src/editor.ts +5 -1
- package/src/ghostty-terminal.test.ts +1 -1
- package/src/herdr.ts +10 -2
- package/src/index.ts +1 -0
- package/src/intent-dispatch.test.ts +235 -55
- package/src/intent-dispatch.ts +104 -34
- package/src/key-bindings.test.ts +1 -1
- package/src/key-bindings.ts +16 -7
- package/src/keymap.test.ts +9 -11
- package/src/keymap.ts +14 -7
- package/src/kitty-image.test.ts +3 -3
- package/src/prototype-browser.ts +7 -6
- package/src/serve.ts +1 -1
- package/src/session-controller.curate.test.ts +106 -4
- package/src/session-controller.share.test.ts +220 -70
- package/src/session-controller.tree.test.ts +228 -0
- package/src/session-controller.ts +370 -94
- package/src/share.ts +117 -17
- package/src/test-support.test.ts +193 -1
- package/src/test-support.ts +69 -1
- package/src/theme-presets.ts +9 -2
- package/src/thread-chords.test.ts +154 -0
- package/src/thread-chords.ts +144 -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/view-diff.ts +1 -4
- package/src/view-plan.ts +35 -19
- package/src/components/PlanSheet.stories.tsx +0 -113
- package/src/components/PlanSheet.tsx +0 -422
- package/src/components/plan-sheet-run-style.ts +0 -90
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.65",
|
|
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": {
|
|
@@ -26,10 +26,10 @@
|
|
|
26
26
|
"access": "public"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@cueloop/daemon": "0.1.0-alpha.
|
|
30
|
-
"@cueloop/extension-api": "0.1.0-alpha.
|
|
31
|
-
"@cueloop/integration-obsidian": "0.1.0-alpha.
|
|
32
|
-
"@cueloop/schema": "0.1.0-alpha.
|
|
29
|
+
"@cueloop/daemon": "0.1.0-alpha.65",
|
|
30
|
+
"@cueloop/extension-api": "0.1.0-alpha.65",
|
|
31
|
+
"@cueloop/integration-obsidian": "0.1.0-alpha.65",
|
|
32
|
+
"@cueloop/schema": "0.1.0-alpha.65",
|
|
33
33
|
"@opentui/core": "^0.5.8",
|
|
34
34
|
"@opentui/keymap": "0.5.8",
|
|
35
35
|
"@opentui/react": "^0.5.8",
|
|
@@ -37,7 +37,8 @@
|
|
|
37
37
|
"@pierre/diffs": "^1.3.6",
|
|
38
38
|
"diff": "^9",
|
|
39
39
|
"puppeteer-core": "^25.8.0",
|
|
40
|
-
"react": "^19.2.0"
|
|
40
|
+
"react": "^19.2.0",
|
|
41
|
+
"valibot": "^1.4.2"
|
|
41
42
|
},
|
|
42
43
|
"devDependencies": {
|
|
43
44
|
"@types/react": "^19.2.0",
|
|
@@ -10,7 +10,7 @@ import { ManualClock } from "@opentui/core/testing";
|
|
|
10
10
|
import { DaemonServer } from "@cueloop/daemon";
|
|
11
11
|
import type { ReviewSession } from "@cueloop/schema";
|
|
12
12
|
import { App } from "./App";
|
|
13
|
-
import { isolateUserConfig, press, settle, waitForText } from "./test-support";
|
|
13
|
+
import { isolateUserConfig, press, settle, waitForText, pressKey } from "./test-support";
|
|
14
14
|
|
|
15
15
|
const PLAN = "# Plan\n\nShip the thing.\n";
|
|
16
16
|
|
|
@@ -56,7 +56,7 @@ async function renderApp(onExit?: (code: number) => void) {
|
|
|
56
56
|
type Setup = Awaited<ReturnType<typeof renderApp>>;
|
|
57
57
|
|
|
58
58
|
async function submitApprove(setup: Setup): Promise<void> {
|
|
59
|
-
await
|
|
59
|
+
await pressKey(setup, "RETURN", { meta: true }); // open submit (approve default: no pending items)
|
|
60
60
|
await press(setup, "enter"); // confirm
|
|
61
61
|
await waitForText(setup, "review approved");
|
|
62
62
|
}
|
package/src/App.export.test.tsx
CHANGED
|
@@ -17,7 +17,7 @@ import { testRender } from "@opentui/react/test-utils";
|
|
|
17
17
|
import { DaemonServer } from "@cueloop/daemon";
|
|
18
18
|
import type { ReviewSession } from "@cueloop/schema";
|
|
19
19
|
import { App } from "./App";
|
|
20
|
-
import { isolateUserConfig, press, waitForState, waitForText } from "./test-support";
|
|
20
|
+
import { isolateUserConfig, press, waitForState, waitForText, pressKey } from "./test-support";
|
|
21
21
|
|
|
22
22
|
const PLAN = `# Migration Plan
|
|
23
23
|
|
|
@@ -69,7 +69,7 @@ describe("obsidian export on resolve", () => {
|
|
|
69
69
|
await waitForText(setup, "cueloop");
|
|
70
70
|
|
|
71
71
|
// Act
|
|
72
|
-
await
|
|
72
|
+
await pressKey(setup, "RETURN", { meta: true }); // open the submit overlay, keep the default verdict (approve)
|
|
73
73
|
|
|
74
74
|
// Assert
|
|
75
75
|
await waitForText(setup, "[Approve]");
|
|
@@ -12,9 +12,11 @@ import { App } from "./App";
|
|
|
12
12
|
import { DEFAULT_QUICK_ACTIONS, quickActionBody } from "./config";
|
|
13
13
|
import { DARK } from "./theme";
|
|
14
14
|
import {
|
|
15
|
+
clickText,
|
|
16
|
+
dragText,
|
|
15
17
|
isolateUserConfig,
|
|
16
18
|
press,
|
|
17
|
-
|
|
19
|
+
pressKey,
|
|
18
20
|
typeText as type,
|
|
19
21
|
waitForState,
|
|
20
22
|
waitForText,
|
|
@@ -92,38 +94,6 @@ function backgroundsOf(setup: Setup, needle: string): string[] {
|
|
|
92
94
|
return backgrounds;
|
|
93
95
|
}
|
|
94
96
|
|
|
95
|
-
function foregroundsOf(setup: Setup, needle: string): string[] {
|
|
96
|
-
const foregrounds: string[] = [];
|
|
97
|
-
|
|
98
|
-
for (const line of setup.captureSpans().lines) {
|
|
99
|
-
for (const span of line.spans) {
|
|
100
|
-
if (!span.text.includes(needle)) continue;
|
|
101
|
-
const [red, green, blue] = span.fg.toInts();
|
|
102
|
-
|
|
103
|
-
foregrounds.push(
|
|
104
|
-
"#" + [red, green, blue].map((part) => part.toString(16).padStart(2, "0")).join(""),
|
|
105
|
-
);
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
return foregrounds;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/** Move the cursor to "The daemon persists sessions to disk atomically." */
|
|
113
|
-
async function toContextParagraph(setup: Setup): Promise<void> {
|
|
114
|
-
await press(setup, "j");
|
|
115
|
-
await press(setup, "j");
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/** Click the first on-screen occurrence of a string, by its char-frame position. */
|
|
119
|
-
async function clickText(setup: Setup, needle: string): Promise<void> {
|
|
120
|
-
const lines = setup.captureCharFrame().split("\n");
|
|
121
|
-
const row = lines.findIndex((line) => line.includes(needle));
|
|
122
|
-
|
|
123
|
-
await setup.mockMouse.click(lines[row]!.indexOf(needle) + 1, row);
|
|
124
|
-
await setup.renderOnce();
|
|
125
|
-
}
|
|
126
|
-
|
|
127
97
|
describe("share button", () => {
|
|
128
98
|
test("the owner's plan header shows the Share button next to Edit", async () => {
|
|
129
99
|
// Arrange / Act
|
|
@@ -221,259 +191,127 @@ describe("attribution", () => {
|
|
|
221
191
|
});
|
|
222
192
|
});
|
|
223
193
|
|
|
224
|
-
describe("
|
|
225
|
-
test("
|
|
194
|
+
describe("marks feed the comment anchor", () => {
|
|
195
|
+
test("a drag marks a character-precise span and typing anchors the comment to it", async () => {
|
|
226
196
|
// Arrange
|
|
227
197
|
const setup = await renderApp();
|
|
228
198
|
|
|
229
|
-
await toContextParagraph(setup);
|
|
230
|
-
|
|
231
|
-
// Act
|
|
232
|
-
await press(setup, "v");
|
|
233
|
-
await press(setup, "l"); // "The daemon"
|
|
234
|
-
await press(setup, "c");
|
|
235
|
-
|
|
236
|
-
// Assert
|
|
237
|
-
await waitForText(setup, 'comment on "The daemon"');
|
|
238
|
-
|
|
239
199
|
// Act
|
|
200
|
+
await dragText(setup, "The daemon", "daemon persists", "daemon".length);
|
|
240
201
|
await type(setup, "Which daemon?");
|
|
241
|
-
await
|
|
202
|
+
await pressKey(setup, "RETURN", { meta: true });
|
|
242
203
|
|
|
243
204
|
// Assert
|
|
244
205
|
await waitForState(setup, () => server.core.sessionGet(session.id).annotations.length === 1);
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
expect(stored.annotations[0]!.anchor.quote).toBe("The daemon");
|
|
206
|
+
expect(server.core.sessionGet(session.id).annotations[0]!.anchor.quote).toBe("The daemon");
|
|
248
207
|
});
|
|
249
208
|
|
|
250
|
-
test("a
|
|
209
|
+
test("a drag that ends mid-sentence keeps exactly the dragged words", async () => {
|
|
251
210
|
// Arrange
|
|
252
211
|
const setup = await renderApp();
|
|
253
|
-
const lines = setup.captureCharFrame().split("\n");
|
|
254
|
-
const row = lines.findIndex((line) => line.includes("persists sessions"));
|
|
255
|
-
const startColumn = lines[row]!.indexOf("persists");
|
|
256
|
-
|
|
257
|
-
expect(row).toBeGreaterThan(-1);
|
|
258
|
-
|
|
259
|
-
// Act
|
|
260
|
-
// the drag's end cell is exclusive: release one past the last character
|
|
261
|
-
await setup.mockMouse.drag(startColumn, row, startColumn + "persists sessions".length, row);
|
|
262
|
-
await settle(setup);
|
|
263
|
-
await press(setup, "c");
|
|
264
|
-
|
|
265
|
-
// Assert
|
|
266
|
-
await waitForText(setup, 'comment on "persists sessions"');
|
|
267
212
|
|
|
268
|
-
// Act
|
|
213
|
+
// Act: release one past the last character of "sessions"
|
|
214
|
+
await dragText(setup, "persists", "sessions", "sessions".length);
|
|
269
215
|
await type(setup, "sessions plural?");
|
|
270
|
-
await
|
|
216
|
+
await pressKey(setup, "RETURN", { meta: true });
|
|
271
217
|
|
|
272
218
|
// Assert
|
|
273
219
|
await waitForState(setup, () => server.core.sessionGet(session.id).annotations.length === 1);
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
220
|
+
expect(server.core.sessionGet(session.id).annotations[0]!.anchor.quote).toBe(
|
|
221
|
+
"persists sessions",
|
|
222
|
+
);
|
|
277
223
|
});
|
|
278
224
|
});
|
|
279
225
|
|
|
280
|
-
|
|
281
|
-
|
|
226
|
+
/** The thread view's mark backdrop in the dark palette. */
|
|
227
|
+
const THREAD_MARK = "#463852";
|
|
228
|
+
|
|
229
|
+
describe("the mark stays painted while composing", () => {
|
|
230
|
+
test("the mark paints while composing, escape un-paints, save keeps it as the comment's mark", async () => {
|
|
282
231
|
// Arrange
|
|
283
232
|
const setup = await renderApp();
|
|
284
233
|
|
|
285
|
-
await toContextParagraph(setup);
|
|
286
|
-
|
|
287
234
|
// Act
|
|
288
|
-
await
|
|
289
|
-
await
|
|
290
|
-
await press(setup, "c");
|
|
235
|
+
await dragText(setup, "The daemon", "daemon persists", "daemon".length);
|
|
236
|
+
await type(setup, "x");
|
|
291
237
|
|
|
292
|
-
// Assert
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
await waitForText(setup, "Save");
|
|
296
|
-
expect(setup.captureCharFrame()).toContain("Cancel");
|
|
297
|
-
await waitForState(setup, () => backgroundsOf(setup, "The daemon").includes(DARK.accent));
|
|
298
|
-
expect(backgroundsOf(setup, "The daemon")).toContain(DARK.accent);
|
|
238
|
+
// Assert: composing, the marked words carry the mark backdrop
|
|
239
|
+
await waitForText(setup, "● x");
|
|
240
|
+
expect(backgroundsOf(setup, "The daemon")).toContain(THREAD_MARK);
|
|
299
241
|
|
|
300
|
-
// Act
|
|
301
|
-
|
|
242
|
+
// Act: escape discards the draft; the mark stays for re-typing, a second escape drops it
|
|
243
|
+
await press(setup, "escape");
|
|
244
|
+
await waitForTextGone(setup, "● x");
|
|
245
|
+
expect(backgroundsOf(setup, "The daemon")).toContain(THREAD_MARK);
|
|
302
246
|
await press(setup, "escape");
|
|
303
247
|
|
|
304
248
|
// Assert
|
|
305
|
-
await
|
|
306
|
-
expect(backgroundsOf(setup, "The daemon")).not.toContain(DARK.accent);
|
|
249
|
+
await waitForState(setup, () => !backgroundsOf(setup, "The daemon").includes(THREAD_MARK));
|
|
307
250
|
|
|
308
|
-
// Act
|
|
309
|
-
|
|
310
|
-
await press(setup, "c");
|
|
251
|
+
// Act: mark again, save
|
|
252
|
+
await dragText(setup, "The daemon", "daemon persists", "daemon".length);
|
|
311
253
|
await type(setup, "Which daemon?");
|
|
312
|
-
await
|
|
254
|
+
await pressKey(setup, "RETURN", { meta: true });
|
|
313
255
|
|
|
314
|
-
// Assert
|
|
256
|
+
// Assert: the saved comment keeps the passage marked
|
|
315
257
|
await waitForState(setup, () => server.core.sessionGet(session.id).annotations.length === 1);
|
|
316
|
-
await
|
|
317
|
-
// the whole cursor block is the anchor here; check the highlight landed
|
|
318
|
-
const stored = server.core.sessionGet(session.id);
|
|
319
|
-
|
|
320
|
-
expect(stored.annotations.length).toBe(1);
|
|
321
|
-
expect(backgroundsOf(setup, stored.annotations[0]!.anchor.quote.slice(0, 20))).toContain(
|
|
322
|
-
DARK.markCommentBackground,
|
|
323
|
-
);
|
|
324
|
-
// renderApp plus this many frame-waits grazes the 5s default on a loaded CI
|
|
325
|
-
// runner, and the whole-suite publish lane has timed even 15s out; give the
|
|
326
|
-
// heaviest frame-wait chain generous headroom so runner load cannot flake it.
|
|
258
|
+
await waitForState(setup, () => backgroundsOf(setup, "The daemon").includes(THREAD_MARK));
|
|
327
259
|
}, 60_000);
|
|
328
|
-
|
|
329
|
-
test("clicking the Cancel word-button closes the compose box", async () => {
|
|
330
|
-
// Arrange
|
|
331
|
-
const setup = await renderApp();
|
|
332
|
-
|
|
333
|
-
await toContextParagraph(setup);
|
|
334
|
-
await press(setup, "v");
|
|
335
|
-
await press(setup, "l");
|
|
336
|
-
await press(setup, "c");
|
|
337
|
-
await waitForText(setup, "Cancel");
|
|
338
|
-
|
|
339
|
-
// Act
|
|
340
|
-
await clickText(setup, "Cancel");
|
|
341
|
-
|
|
342
|
-
// Assert
|
|
343
|
-
await waitForTextGone(setup, 'comment on "');
|
|
344
|
-
});
|
|
345
|
-
|
|
346
|
-
test("clicking Cancel in a saved card's edit composer closes it", async () => {
|
|
347
|
-
// Arrange
|
|
348
|
-
const setup = await renderApp();
|
|
349
|
-
|
|
350
|
-
await toContextParagraph(setup);
|
|
351
|
-
await press(setup, "v");
|
|
352
|
-
await press(setup, "l");
|
|
353
|
-
await press(setup, "c");
|
|
354
|
-
await waitForText(setup, "Cancel");
|
|
355
|
-
await type(setup, "note body");
|
|
356
|
-
await press(setup, "enter");
|
|
357
|
-
await waitForState(setup, () => server.core.sessionGet(session.id).annotations.length === 1);
|
|
358
|
-
// select the saved card, then activate it to open the in-place editor
|
|
359
|
-
await clickText(setup, "note body");
|
|
360
|
-
await clickText(setup, "note body");
|
|
361
|
-
await waitForText(setup, "Cancel");
|
|
362
|
-
|
|
363
|
-
// Act
|
|
364
|
-
await clickText(setup, "Cancel");
|
|
365
|
-
|
|
366
|
-
// Assert - the editor closes back to the plain saved card
|
|
367
|
-
await waitForTextGone(setup, "Cancel");
|
|
368
|
-
});
|
|
369
260
|
});
|
|
370
261
|
|
|
371
262
|
describe("compose newline convention", () => {
|
|
372
|
-
|
|
373
|
-
// modifyOtherKeys encoding preserves those modifiers so the grammar and the
|
|
374
|
-
// focused textarea can tell a bare submit from a newline.
|
|
375
|
-
async function renderModifierAwareApp() {
|
|
376
|
-
const setup = await testRender(<App home={home} sessionId={session.id} />, {
|
|
377
|
-
width: 120,
|
|
378
|
-
height: 32,
|
|
379
|
-
otherModifiersMode: true,
|
|
380
|
-
});
|
|
381
|
-
|
|
382
|
-
await waitForText(setup, "cueloop");
|
|
383
|
-
|
|
384
|
-
return setup;
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
async function pressReturnWith(
|
|
388
|
-
setup: Setup,
|
|
389
|
-
modifiers: { shift?: boolean; meta?: boolean },
|
|
390
|
-
): Promise<void> {
|
|
391
|
-
setup.mockInput.pressKey("RETURN", modifiers);
|
|
392
|
-
await settle(setup);
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
test("option+return inserts a newline; a bare return submits the multiline body", async () => {
|
|
263
|
+
test("enter breaks the line; cmd+enter saves the multiline body verbatim", async () => {
|
|
396
264
|
// Arrange
|
|
397
|
-
const setup = await
|
|
265
|
+
const setup = await renderApp();
|
|
398
266
|
|
|
399
|
-
await
|
|
400
|
-
await press(setup, "v");
|
|
401
|
-
await press(setup, "l");
|
|
402
|
-
await press(setup, "c");
|
|
403
|
-
await waitForText(setup, 'comment on "The daemon"');
|
|
267
|
+
await clickText(setup, "daemon");
|
|
404
268
|
|
|
405
269
|
// Act
|
|
406
270
|
await type(setup, "first line");
|
|
407
|
-
await
|
|
271
|
+
await press(setup, "enter");
|
|
408
272
|
await type(setup, "second line");
|
|
409
273
|
|
|
410
|
-
// Assert
|
|
411
|
-
// still composing: the newline did not submit
|
|
274
|
+
// Assert: still composing, the newline did not save
|
|
412
275
|
expect(server.core.sessionGet(session.id).annotations.length).toBe(0);
|
|
413
276
|
|
|
414
277
|
// Act
|
|
415
|
-
await
|
|
278
|
+
await pressKey(setup, "RETURN", { meta: true });
|
|
416
279
|
|
|
417
280
|
// Assert
|
|
418
281
|
await waitForState(setup, () => server.core.sessionGet(session.id).annotations.length === 1);
|
|
419
282
|
expect(server.core.sessionGet(session.id).annotations[0]!.body).toBe("first line\nsecond line");
|
|
420
283
|
});
|
|
421
284
|
|
|
422
|
-
test("shift+return still inserts a newline (existing muscle memory)", async () => {
|
|
423
|
-
// Arrange
|
|
424
|
-
const setup = await renderModifierAwareApp();
|
|
425
|
-
|
|
426
|
-
await toContextParagraph(setup);
|
|
427
|
-
await press(setup, "c");
|
|
428
|
-
|
|
429
|
-
// Act
|
|
430
|
-
await type(setup, "alpha");
|
|
431
|
-
await pressReturnWith(setup, { shift: true }); // shift+return -> newline
|
|
432
|
-
await type(setup, "beta");
|
|
433
|
-
|
|
434
|
-
// Assert
|
|
435
|
-
expect(server.core.sessionGet(session.id).annotations.length).toBe(0);
|
|
436
|
-
|
|
437
|
-
// Act
|
|
438
|
-
await press(setup, "enter");
|
|
439
|
-
|
|
440
|
-
// Assert
|
|
441
|
-
await waitForState(setup, () => server.core.sessionGet(session.id).annotations.length === 1);
|
|
442
|
-
expect(server.core.sessionGet(session.id).annotations[0]!.body).toBe("alpha\nbeta");
|
|
443
|
-
});
|
|
444
|
-
|
|
445
285
|
test("escape cancels the composer without saving", async () => {
|
|
446
286
|
// Arrange
|
|
447
|
-
const setup = await
|
|
287
|
+
const setup = await renderApp();
|
|
448
288
|
|
|
449
|
-
await
|
|
450
|
-
await press(setup, "c");
|
|
451
|
-
await waitForText(setup, 'comment on "');
|
|
289
|
+
await clickText(setup, "daemon");
|
|
452
290
|
await type(setup, "never saved");
|
|
291
|
+
await waitForText(setup, "● never saved");
|
|
453
292
|
|
|
454
293
|
// Act
|
|
455
294
|
await press(setup, "escape");
|
|
456
295
|
|
|
457
296
|
// Assert
|
|
458
|
-
await waitForTextGone(setup,
|
|
297
|
+
await waitForTextGone(setup, "● never saved");
|
|
459
298
|
expect(server.core.sessionGet(session.id).annotations.length).toBe(0);
|
|
460
299
|
});
|
|
461
300
|
});
|
|
462
301
|
|
|
463
302
|
describe("the document selects, the rail edits", () => {
|
|
464
|
-
test("e edits the
|
|
303
|
+
test("option+e edits the focused card in place; enter saves through the controller", async () => {
|
|
465
304
|
// Arrange
|
|
466
305
|
const setup = await renderApp();
|
|
467
306
|
|
|
468
|
-
await
|
|
469
|
-
await press(setup, "c");
|
|
307
|
+
await clickText(setup, "daemon");
|
|
470
308
|
await type(setup, "Needs a citation.");
|
|
471
|
-
await
|
|
309
|
+
await pressKey(setup, "RETURN", { meta: true });
|
|
472
310
|
await waitForText(setup, "COMMENT · me");
|
|
473
311
|
|
|
474
|
-
// Act
|
|
475
|
-
|
|
476
|
-
await
|
|
312
|
+
// Act: focus the card, then turn its body into an input in place
|
|
313
|
+
await pressKey(setup, "n", { meta: true });
|
|
314
|
+
await pressKey(setup, "e", { meta: true });
|
|
477
315
|
await type(setup, " And a link.");
|
|
478
316
|
await press(setup, "enter");
|
|
479
317
|
|
|
@@ -505,51 +343,51 @@ describe("the document selects, the rail edits", () => {
|
|
|
505
343
|
await waitForText(setup, "1a2b3c4d");
|
|
506
344
|
|
|
507
345
|
// Act - focus the collaborator card, then activate it
|
|
508
|
-
await
|
|
509
|
-
await
|
|
346
|
+
await pressKey(setup, "n", { meta: true });
|
|
347
|
+
await pressKey(setup, "e", { meta: true });
|
|
510
348
|
|
|
511
349
|
// Assert - the rename prompt opens; the body never becomes an editor
|
|
512
350
|
await waitForText(setup, "Rename author");
|
|
513
351
|
});
|
|
514
352
|
|
|
515
|
-
test("
|
|
353
|
+
test("option+backspace deletes the focused card and un-paints the document mark", async () => {
|
|
516
354
|
// Arrange
|
|
517
355
|
const setup = await renderApp();
|
|
518
356
|
|
|
519
|
-
await
|
|
520
|
-
await press(setup, "c");
|
|
357
|
+
await dragText(setup, "daemon", "daemon persists", "daemon".length);
|
|
521
358
|
await type(setup, "Delete me.");
|
|
522
|
-
await
|
|
359
|
+
await pressKey(setup, "RETURN", { meta: true });
|
|
523
360
|
await waitForText(setup, "COMMENT · me");
|
|
524
361
|
expect(server.core.sessionGet(session.id).annotations.length).toBe(1);
|
|
525
|
-
|
|
362
|
+
await waitForState(setup, () => backgroundsOf(setup, "daemon").includes(THREAD_MARK));
|
|
526
363
|
|
|
527
364
|
// Act
|
|
528
|
-
await
|
|
365
|
+
await pressKey(setup, "n", { meta: true });
|
|
366
|
+
await pressKey(setup, "BACKSPACE", { meta: true });
|
|
529
367
|
|
|
530
368
|
// Assert
|
|
531
369
|
await waitForTextGone(setup, "COMMENT · me");
|
|
532
370
|
expect(server.core.sessionGet(session.id).annotations.length).toBe(0);
|
|
533
|
-
expect(backgroundsOf(setup, "
|
|
371
|
+
expect(backgroundsOf(setup, "daemon")).not.toContain(THREAD_MARK);
|
|
534
372
|
});
|
|
535
373
|
});
|
|
536
374
|
|
|
537
375
|
describe("plan cut removals", () => {
|
|
538
|
-
test("
|
|
376
|
+
test("option+x cuts the caret's block into a rail removal card and option+u restores it", async () => {
|
|
539
377
|
// Arrange
|
|
540
378
|
const setup = await renderApp();
|
|
541
379
|
|
|
542
|
-
await
|
|
380
|
+
await clickText(setup, "daemon");
|
|
543
381
|
|
|
544
|
-
// Act - no card is
|
|
545
|
-
await
|
|
382
|
+
// Act - no card is focused, so the cut takes the block under the caret
|
|
383
|
+
await pressKey(setup, "x", { meta: true });
|
|
546
384
|
|
|
547
385
|
// Assert - the rail shows a removal card titled CUT · me for the cut block
|
|
548
386
|
await waitForText(setup, "CUT · me");
|
|
549
387
|
await waitForState(setup, () => server.core.sessionGet(session.id).workingCopy !== undefined);
|
|
550
388
|
|
|
551
389
|
// Act - undo restores it (no selection, so the last removal)
|
|
552
|
-
await
|
|
390
|
+
await pressKey(setup, "u", { meta: true });
|
|
553
391
|
|
|
554
392
|
// Assert - the block returns and the working copy is pristine again
|
|
555
393
|
await waitForText(setup, "removal restored");
|
|
@@ -661,18 +499,13 @@ describe("edit-exit reconciliation", () => {
|
|
|
661
499
|
// Arrange
|
|
662
500
|
const setup = await renderApp();
|
|
663
501
|
|
|
664
|
-
await
|
|
665
|
-
await press(setup, "c");
|
|
502
|
+
await clickText(setup, "daemon");
|
|
666
503
|
await type(setup, "Anchor me to the doomed passage.");
|
|
667
|
-
await
|
|
504
|
+
await pressKey(setup, "RETURN", { meta: true });
|
|
668
505
|
await waitForText(setup, "COMMENT · me");
|
|
669
506
|
|
|
670
507
|
// Act
|
|
671
|
-
|
|
672
|
-
// shows as the card's quote line dropping the selection tone (blue)
|
|
673
|
-
await press(setup, "escape");
|
|
674
|
-
await waitForState(setup, () => !foregroundsOf(setup, "The daemon").includes(DARK.blue));
|
|
675
|
-
await press(setup, "e");
|
|
508
|
+
await pressKey(setup, "e", { ctrl: true });
|
|
676
509
|
|
|
677
510
|
// Assert
|
|
678
511
|
await waitForText(setup, "1 annotation no longer match - the passage was removed.");
|
|
@@ -686,23 +519,19 @@ describe("edit-exit reconciliation", () => {
|
|
|
686
519
|
});
|
|
687
520
|
});
|
|
688
521
|
|
|
689
|
-
describe("
|
|
690
|
-
test("
|
|
522
|
+
describe("the quick-action palette", () => {
|
|
523
|
+
test("/ lists the quick actions and a pick seeds the comment with the preset body", async () => {
|
|
691
524
|
// Arrange
|
|
692
525
|
const setup = await renderApp();
|
|
693
526
|
|
|
694
|
-
await
|
|
695
|
-
|
|
696
|
-
// Act - marking a span reveals the inline toolbar
|
|
697
|
-
await press(setup, "v");
|
|
698
|
-
await waitForText(setup, "actions");
|
|
527
|
+
await dragText(setup, "The daemon", "daemon persists", "daemon".length);
|
|
699
528
|
|
|
700
|
-
//
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
await
|
|
704
|
-
await press(setup, "j");
|
|
529
|
+
// Act - a leading slash opens the palette; step to the second default and pick it
|
|
530
|
+
await type(setup, "/");
|
|
531
|
+
await waitForText(setup, "zoom-out-research-in-depth");
|
|
532
|
+
await pressKey(setup, "ARROW_DOWN");
|
|
705
533
|
await press(setup, "enter");
|
|
534
|
+
await pressKey(setup, "RETURN", { meta: true });
|
|
706
535
|
|
|
707
536
|
// Assert - a comment annotation was created with the second default's body
|
|
708
537
|
await waitForState(setup, () => server.core.sessionGet(session.id).annotations.length === 1);
|