| 1 | import { describe, expect, it } from "vite-plus/test"; |
| 2 | |
| 3 | import { |
| 4 | REMOVE_QUEUED_MESSAGE_ACCESSIBILITY_LABEL, |
| 5 | buildCancelQueuedRunCommand, |
| 6 | resolveQueueDragBeforeRunId, |
| 7 | resolveThreadQueueRowControls, |
| 8 | resolveQueueDropBeforeRunId, |
| 9 | } from "./threadQueueControlPresentation"; |
| 10 | import { threadDragGapOffset } from "./threadDragGap"; |
| 11 | |
| 12 | describe("threadQueueControlPresentation", () => { |
| 13 | it("preserves queue reorder and steer controls with removal", () => { |
| 14 | const controls = resolveThreadQueueRowControls({ |
| 15 | busy: false, |
| 16 | canPromoteToSteer: true, |
| 17 | canReorder: true, |
| 18 | index: 1, |
| 19 | queuedCount: 3, |
| 20 | text: "Please review the follow-up change.", |
| 21 | }); |
| 22 | |
| 23 | expect(controls.displayText).toBe("Please review the follow-up change."); |
| 24 | expect(controls.canMoveUp).toBe(true); |
| 25 | expect(controls.canMoveDown).toBe(true); |
| 26 | expect(controls.canSteer).toBe(true); |
| 27 | expect(controls.canDismiss).toBe(true); |
| 28 | expect(controls.dismissAccessibilityLabel).toBe(REMOVE_QUEUED_MESSAGE_ACCESSIBILITY_LABEL); |
| 29 | }); |
| 30 | |
| 31 | it("disables edge reorder controls and busy dismissal", () => { |
| 32 | const first = resolveThreadQueueRowControls({ |
| 33 | busy: false, |
| 34 | canPromoteToSteer: false, |
| 35 | canReorder: true, |
| 36 | index: 0, |
| 37 | queuedCount: 2, |
| 38 | text: "First", |
| 39 | }); |
| 40 | const busy = resolveThreadQueueRowControls({ |
| 41 | busy: true, |
| 42 | canPromoteToSteer: true, |
| 43 | canReorder: true, |
| 44 | index: 0, |
| 45 | queuedCount: 1, |
| 46 | text: "Queued message", |
| 47 | }); |
| 48 | |
| 49 | expect(first.canMoveUp).toBe(false); |
| 50 | expect(first.canMoveDown).toBe(true); |
| 51 | expect(first.canSteer).toBe(false); |
| 52 | expect(busy.canDismiss).toBe(false); |
| 53 | expect(busy.canMoveUp).toBe(false); |
| 54 | expect(busy.canSteer).toBe(false); |
| 55 | }); |
| 56 | |
| 57 | it("keeps the row already open in the composer from being reopened or steered", () => { |
| 58 | const editing = resolveThreadQueueRowControls({ |
| 59 | busy: false, |
| 60 | canPromoteToSteer: true, |
| 61 | canReorder: true, |
| 62 | index: 1, |
| 63 | isEditing: true, |
| 64 | queuedCount: 3, |
| 65 | text: "Being edited", |
| 66 | }); |
| 67 | |
| 68 | expect(editing.isEditing).toBe(true); |
| 69 | expect(editing.canEdit).toBe(false); |
| 70 | expect(editing.canSteer).toBe(false); |
| 71 | // Reordering and removing a message stay available while it is edited. |
| 72 | expect(editing.canMoveUp).toBe(true); |
| 73 | expect(editing.canDismiss).toBe(true); |
| 74 | }); |
| 75 | |
| 76 | it("builds cancelQueuedRun command arguments for removal", () => { |
| 77 | expect( |
| 78 | buildCancelQueuedRunCommand({ |
| 79 | environmentId: "environment:test" as never, |
| 80 | runId: "run:queued" as never, |
| 81 | threadId: "thread:test" as never, |
| 82 | }), |
| 83 | ).toEqual({ |
| 84 | environmentId: "environment:test", |
| 85 | input: { |
| 86 | runId: "run:queued", |
| 87 | threadId: "thread:test", |
| 88 | }, |
| 89 | }); |
| 90 | }); |
| 91 | }); |
| 92 | |
| 93 | describe("queue drag insertion", () => { |
| 94 | const rows = [ |
| 95 | { id: "first" as never, y: 0, height: 80 }, |
| 96 | { id: "second" as never, y: 80, height: 140 }, |
| 97 | { id: "third" as never, y: 220, height: 80 }, |
| 98 | ]; |
| 99 | |
| 100 | it("moves between variable-height rows and to either end", () => { |
| 101 | expect(resolveQueueDropBeforeRunId(rows, rows[0]!.id, 140)).toBe("third"); |
| 102 | expect(resolveQueueDropBeforeRunId(rows, rows[0]!.id, 300)).toBeNull(); |
| 103 | expect(resolveQueueDropBeforeRunId(rows, rows[2]!.id, -300)).toBe("first"); |
| 104 | }); |
| 105 | |
| 106 | it("opens the destination gap while the dragged row crosses other rows", () => { |
| 107 | const offsets = (runId: (typeof rows)[number]["id"], translation: number) => { |
| 108 | const before = resolveQueueDragBeforeRunId(rows, runId, translation); |
| 109 | if (before === undefined) return; |
| 110 | const source = rows.find((row) => row.id === runId)!; |
| 111 | const last = rows.at(-1)!; |
| 112 | const insertion = |
| 113 | before === null ? last.y + last.height : rows.find((row) => row.id === before)!.y; |
| 114 | return rows.map((row) => threadDragGapOffset(row.y, source.y, source.height, insertion)); |
| 115 | }; |
| 116 | |
| 117 | expect(resolveQueueDragBeforeRunId(rows, rows[0]!.id, 0)).toBe(rows[1]!.id); |
| 118 | expect(offsets(rows[0]!.id, 140)).toEqual([0, -80, 0]); |
| 119 | expect(offsets(rows[0]!.id, 300)).toEqual([0, -80, -80]); |
| 120 | expect(offsets(rows[2]!.id, -300)).toEqual([80, 80, 0]); |
| 121 | expect(offsets(rows[1]!.id, 0)).toEqual([0, 0, 0]); |
| 122 | }); |
| 123 | |
| 124 | it("does not send a reorder for an unchanged or unmeasured drop", () => { |
| 125 | expect(resolveQueueDropBeforeRunId(rows, rows[1]!.id, 0)).toBeUndefined(); |
| 126 | expect(resolveQueueDropBeforeRunId(rows, rows[2]!.id, 20)).toBeUndefined(); |
| 127 | expect(resolveQueueDropBeforeRunId([{ id: rows[0]!.id }], rows[0]!.id, 10)).toBeUndefined(); |
| 128 | expect(resolveQueueDropBeforeRunId(rows, "missing" as never, 100)).toBeUndefined(); |
| 129 | }); |
| 130 | }); |