1import { createElement } from "react";
2import { renderToStaticMarkup } from "react-dom/server";
3import { afterEach, describe, expect, it, vi } from "vite-plus/test";
4
5const stageArtworkState = vi.hoisted(() => ({
6 mode: "none" as "artwork" | "none",
7 variant: null as "nightly" | "dev" | null,
8}));
9
10vi.mock("~/hooks/useSettings", () => ({
11 useEnvironmentIdentificationMode: () => stageArtworkState.mode,
12}));
13vi.mock("../SidebarStageBackdrop", () => ({
14 StageBackdropButtonArt: ({ variant }: { variant: string }) => `stage-${variant}`,
15 useSidebarStageBackdropVariant: (enabled = true) => (enabled ? stageArtworkState.variant : null),
16}));
17
18import { ComposerPrimaryActions } from "./ComposerPrimaryActions";
19
20function renderPendingActions(isRunning: boolean) {
21 return renderToStaticMarkup(
22 createElement(ComposerPrimaryActions, {
23 compact: true,
24 canOperateThread: true,
25 pendingAction: {
26 questionIndex: 0,
27 isLastQuestion: true,
28 canAdvance: true,
29 isResponding: false,
30 isComplete: true,
31 },
32 isRunning,
33 canInterrupt: isRunning,
34 showPlanFollowUpPrompt: false,
35 promptHasText: false,
36 isSendBusy: false,
37 sendDisabledReason: null,
38 isConnecting: false,
39 isEnvironmentUnavailable: false,
40 isPreparingWorktree: false,
41 hasSendableContent: false,
42 onPreviousPendingQuestion: () => {},
43 onInterrupt: () => {},
44 onImplementPlanInNewThread: () => {},
45 }),
46 );
47}
48
49function renderSendButton(sendDisabledReason: string | null = null) {
50 return renderToStaticMarkup(
51 createElement(ComposerPrimaryActions, {
52 compact: true,
53 canOperateThread: true,
54 pendingAction: null,
55 isRunning: false,
56 canInterrupt: false,
57 showPlanFollowUpPrompt: false,
58 promptHasText: true,
59 isSendBusy: false,
60 sendDisabledReason,
61 isConnecting: false,
62 isEnvironmentUnavailable: false,
63 isPreparingWorktree: false,
64 hasSendableContent: true,
65 onPreviousPendingQuestion: () => {},
66 onInterrupt: () => {},
67 onImplementPlanInNewThread: () => {},
68 }),
69 );
70}
71
72afterEach(() => {
73 stageArtworkState.mode = "none";
74 stageArtworkState.variant = null;
75});
76
77describe("ComposerPrimaryActions", () => {
78 it("disables and labels the send button while feedback is uploading", () => {
79 const markup = renderSendButton("Sending feedback");
80
81 expect(markup).toContain("disabled");
82 expect(markup).toContain('aria-label="Sending feedback"');
83 });
84
85 it("offers Stop generation while a running turn is waiting for user input", () => {
86 expect(renderPendingActions(true)).toContain('aria-label="Stop generation"');
87 });
88
89 it("does not offer Stop generation for a pending request without a running turn", () => {
90 expect(renderPendingActions(false)).not.toContain('aria-label="Stop generation"');
91 });
92
93 it("renders stage artwork inside the send button when artwork identification is active", () => {
94 stageArtworkState.mode = "artwork";
95 stageArtworkState.variant = "nightly";
96
97 const markup = renderSendButton();
98
99 expect(markup).toContain("stage-nightly");
100 });
101
102 it("hides stage artwork when artwork identification is inactive", () => {
103 stageArtworkState.variant = "nightly";
104
105 const markup = renderSendButton();
106
107 expect(markup).not.toContain("stage-nightly");
108 });
109});