import { createElement } from "react";import { renderToStaticMarkup } from "react-dom/server";import { afterEach, describe, expect, it, vi } from "vite-plus/test"; const stageArtworkState = vi.hoisted(() => ({ mode: "none" as "artwork" | "none", variant: null as "nightly" | "dev" | null,})); vi.mock("~/hooks/useSettings", () => ({ useEnvironmentIdentificationMode: () => stageArtworkState.mode,}));vi.mock("../SidebarStageBackdrop", () => ({ StageBackdropButtonArt: ({ variant }: { variant: string }) => `stage-${variant}`, useSidebarStageBackdropVariant: (enabled = true) => (enabled ? stageArtworkState.variant : null),})); import { ComposerPrimaryActions, formatPendingPrimaryActionLabel } from "./ComposerPrimaryActions"; function renderPendingActions(isRunning: boolean) { return renderToStaticMarkup( createElement(ComposerPrimaryActions, { compact: true, pendingAction: { questionIndex: 0, isLastQuestion: true, canAdvance: true, isResponding: false, isComplete: true, }, isRunning, showPlanFollowUpPrompt: false, promptHasText: false, isSendBusy: false, sendDisabledReason: null, isConnecting: false, isEnvironmentUnavailable: false, isPreparingWorktree: false, hasSendableContent: false, onPreviousPendingQuestion: () => {}, onInterrupt: () => {}, onImplementPlanInNewThread: () => {}, }), );} function renderStandaloneStop() { return renderToStaticMarkup( createElement(ComposerPrimaryActions, { compact: true, pendingAction: null, isRunning: true, showPlanFollowUpPrompt: false, promptHasText: false, isSendBusy: false, sendDisabledReason: null, isConnecting: false, isEnvironmentUnavailable: false, isPreparingWorktree: false, hasSendableContent: false, onPreviousPendingQuestion: () => {}, onInterrupt: () => {}, onImplementPlanInNewThread: () => {}, }), );} function renderRunningActions(showSendWhileRunning: boolean, hasSendableContent: boolean) { return renderToStaticMarkup( createElement(ComposerPrimaryActions, { compact: true, pendingAction: null, isRunning: true, showPlanFollowUpPrompt: false, promptHasText: hasSendableContent, isSendBusy: false, sendDisabledReason: null, isConnecting: false, isEnvironmentUnavailable: false, isPreparingWorktree: false, hasSendableContent, showSendWhileRunning, onPreviousPendingQuestion: () => {}, onInterrupt: () => {}, onImplementPlanInNewThread: () => {}, }), );} function renderSendButton() { return renderToStaticMarkup( createElement(ComposerPrimaryActions, { compact: true, pendingAction: null, isRunning: false, showPlanFollowUpPrompt: false, promptHasText: true, isSendBusy: false, sendDisabledReason: null, isConnecting: false, isEnvironmentUnavailable: false, isPreparingWorktree: false, hasSendableContent: true, onPreviousPendingQuestion: () => {}, onInterrupt: () => {}, onImplementPlanInNewThread: () => {}, }), );} afterEach(() => { stageArtworkState.mode = "none"; stageArtworkState.variant = null;}); describe("formatPendingPrimaryActionLabel", () => { it("returns 'Submitting...' while responding", () => { expect( formatPendingPrimaryActionLabel({ compact: false, isLastQuestion: false, isResponding: true, questionIndex: 0, }), ).toBe("Submitting..."); }); it("returns 'Submitting...' while responding regardless of other flags", () => { expect( formatPendingPrimaryActionLabel({ compact: true, isLastQuestion: true, isResponding: true, questionIndex: 3, }), ).toBe("Submitting..."); }); it("returns 'Submit' in compact mode on the last question", () => { expect( formatPendingPrimaryActionLabel({ compact: true, isLastQuestion: true, isResponding: false, questionIndex: 0, }), ).toBe("Submit"); }); it("returns 'Next' in compact mode when not the last question", () => { expect( formatPendingPrimaryActionLabel({ compact: true, isLastQuestion: false, isResponding: false, questionIndex: 1, }), ).toBe("Next"); }); it("returns 'Next question' when not the last question", () => { expect( formatPendingPrimaryActionLabel({ compact: false, isLastQuestion: false, isResponding: false, questionIndex: 0, }), ).toBe("Next question"); }); it("returns singular 'Submit answer' on the last question when it is the only question", () => { expect( formatPendingPrimaryActionLabel({ compact: false, isLastQuestion: true, isResponding: false, questionIndex: 0, }), ).toBe("Submit answer"); }); it("returns plural 'Submit answers' on the last question when there are multiple questions", () => { expect( formatPendingPrimaryActionLabel({ compact: false, isLastQuestion: true, isResponding: false, questionIndex: 1, }), ).toBe("Submit answers"); }); it("returns plural 'Submit answers' for higher question indices", () => { expect( formatPendingPrimaryActionLabel({ compact: false, isLastQuestion: true, isResponding: false, questionIndex: 5, }), ).toBe("Submit answers"); });}); describe("ComposerPrimaryActions", () => { it("offers Stop generation while a running turn is waiting for user input", () => { expect(renderPendingActions(true)).toContain('aria-label="Stop generation"'); }); it("does not offer Stop generation for a pending request without a running turn", () => { expect(renderPendingActions(false)).not.toContain('aria-label="Stop generation"'); }); it("matches the small pending action size without changing the standalone size", () => { expect(renderPendingActions(true)).toContain("size-8 sm:size-7"); expect(renderStandaloneStop()).toContain("size-8 sm:h-8 sm:w-8"); expect(renderStandaloneStop()).not.toContain("sm:size-7"); }); it("renders stage artwork inside the send button when artwork identification is active", () => { stageArtworkState.mode = "artwork"; stageArtworkState.variant = "nightly"; const markup = renderSendButton(); expect(markup).toContain("stage-nightly"); expect(markup).toContain("bg-transparent text-white"); expect(markup).not.toContain("bg-message-action text-message-action-foreground"); }); it("keeps the normal send-button fill when artwork identification is inactive", () => { stageArtworkState.variant = "nightly"; const markup = renderSendButton(); expect(markup).not.toContain("stage-nightly"); expect(markup).toContain("bg-message-action text-message-action-foreground"); }); it("only renders stop while running when Enter-to-send is available", () => { const markup = renderRunningActions(false, true); expect(markup).toContain('aria-label="Stop generation"'); expect(markup).not.toContain('aria-label="Send message"'); }); it("renders send alongside stop while running when Enter-to-send is unavailable", () => { const markup = renderRunningActions(true, true); expect(markup).toContain('aria-label="Stop generation"'); expect(markup).toContain('aria-label="Send message"'); expect(markup).toContain('type="submit"'); expect(markup).toContain("size-9 sm:size-8"); }); it("keeps stop as the only action while running with an empty composer", () => { const markup = renderRunningActions(true, false); expect(markup).toContain('aria-label="Stop generation"'); expect(markup).not.toContain('aria-label="Send message"'); });});
import { createElement } from "react";
import { renderToStaticMarkup } from "react-dom/server";
import { afterEach, describe, expect, it, vi } from "vite-plus/test";
const stageArtworkState = vi.hoisted(() => ({
mode: "none" as "artwork" | "none",
variant: null as "nightly" | "dev" | null,
}));
vi.mock("~/hooks/useSettings", () => ({
useEnvironmentIdentificationMode: () => stageArtworkState.mode,
}));
vi.mock("../SidebarStageBackdrop", () => ({
StageBackdropButtonArt: ({ variant }: { variant: string }) => `stage-${variant}`,
useSidebarStageBackdropVariant: (enabled = true) => (enabled ? stageArtworkState.variant : null),
}));
import { ComposerPrimaryActions, formatPendingPrimaryActionLabel } from "./ComposerPrimaryActions";
function renderPendingActions(isRunning: boolean) {
return renderToStaticMarkup(
createElement(ComposerPrimaryActions, {
compact: true,
pendingAction: {
questionIndex: 0,
isLastQuestion: true,
canAdvance: true,
isResponding: false,
isComplete: true,
},
isRunning,
showPlanFollowUpPrompt: false,
promptHasText: false,
isSendBusy: false,
sendDisabledReason: null,
isConnecting: false,
isEnvironmentUnavailable: false,
isPreparingWorktree: false,
hasSendableContent: false,
onPreviousPendingQuestion: () => {},
onInterrupt: () => {},
onImplementPlanInNewThread: () => {},
}),
);
}
function renderStandaloneStop() {
return renderToStaticMarkup(
createElement(ComposerPrimaryActions, {
compact: true,
pendingAction: null,
isRunning: true,
showPlanFollowUpPrompt: false,
promptHasText: false,
isSendBusy: false,
sendDisabledReason: null,
isConnecting: false,
isEnvironmentUnavailable: false,
isPreparingWorktree: false,
hasSendableContent: false,
onPreviousPendingQuestion: () => {},
onInterrupt: () => {},
onImplementPlanInNewThread: () => {},
}),
);
}
function renderRunningActions(showSendWhileRunning: boolean, hasSendableContent: boolean) {
return renderToStaticMarkup(
createElement(ComposerPrimaryActions, {
compact: true,
pendingAction: null,
isRunning: true,
showPlanFollowUpPrompt: false,
promptHasText: hasSendableContent,
isSendBusy: false,
sendDisabledReason: null,
isConnecting: false,
isEnvironmentUnavailable: false,
isPreparingWorktree: false,
hasSendableContent,
showSendWhileRunning,
onPreviousPendingQuestion: () => {},
onInterrupt: () => {},
onImplementPlanInNewThread: () => {},
}),
);
}
function renderSendButton() {
return renderToStaticMarkup(
createElement(ComposerPrimaryActions, {
compact: true,
pendingAction: null,
isRunning: false,
showPlanFollowUpPrompt: false,
promptHasText: true,
isSendBusy: false,
sendDisabledReason: null,
isConnecting: false,
isEnvironmentUnavailable: false,
isPreparingWorktree: false,
hasSendableContent: true,
onPreviousPendingQuestion: () => {},
onInterrupt: () => {},
onImplementPlanInNewThread: () => {},
}),
);
}
afterEach(() => {
stageArtworkState.mode = "none";
stageArtworkState.variant = null;
});
describe("formatPendingPrimaryActionLabel", () => {
it("returns 'Submitting...' while responding", () => {
expect(
formatPendingPrimaryActionLabel({
compact: false,
isLastQuestion: false,
isResponding: true,
questionIndex: 0,
}),
).toBe("Submitting...");
});
it("returns 'Submitting...' while responding regardless of other flags", () => {
expect(
formatPendingPrimaryActionLabel({
compact: true,
isLastQuestion: true,
isResponding: true,
questionIndex: 3,
}),
).toBe("Submitting...");
});
it("returns 'Submit' in compact mode on the last question", () => {
expect(
formatPendingPrimaryActionLabel({
compact: true,
isLastQuestion: true,
isResponding: false,
questionIndex: 0,
}),
).toBe("Submit");
});
it("returns 'Next' in compact mode when not the last question", () => {
expect(
formatPendingPrimaryActionLabel({
compact: true,
isLastQuestion: false,
isResponding: false,
questionIndex: 1,
}),
).toBe("Next");
});
it("returns 'Next question' when not the last question", () => {
expect(
formatPendingPrimaryActionLabel({
compact: false,
isLastQuestion: false,
isResponding: false,
questionIndex: 0,
}),
).toBe("Next question");
});
it("returns singular 'Submit answer' on the last question when it is the only question", () => {
expect(
formatPendingPrimaryActionLabel({
compact: false,
isLastQuestion: true,
isResponding: false,
questionIndex: 0,
}),
).toBe("Submit answer");
});
it("returns plural 'Submit answers' on the last question when there are multiple questions", () => {
expect(
formatPendingPrimaryActionLabel({
compact: false,
isLastQuestion: true,
isResponding: false,
questionIndex: 1,
}),
).toBe("Submit answers");
});
it("returns plural 'Submit answers' for higher question indices", () => {
expect(
formatPendingPrimaryActionLabel({
compact: false,
isLastQuestion: true,
isResponding: false,
questionIndex: 5,
}),
).toBe("Submit answers");
});
});
describe("ComposerPrimaryActions", () => {
it("offers Stop generation while a running turn is waiting for user input", () => {
expect(renderPendingActions(true)).toContain('aria-label="Stop generation"');
});
it("does not offer Stop generation for a pending request without a running turn", () => {
expect(renderPendingActions(false)).not.toContain('aria-label="Stop generation"');
});
it("matches the small pending action size without changing the standalone size", () => {
expect(renderPendingActions(true)).toContain("size-8 sm:size-7");
expect(renderStandaloneStop()).toContain("size-8 sm:h-8 sm:w-8");
expect(renderStandaloneStop()).not.toContain("sm:size-7");
});
it("renders stage artwork inside the send button when artwork identification is active", () => {
stageArtworkState.mode = "artwork";
stageArtworkState.variant = "nightly";
const markup = renderSendButton();
expect(markup).toContain("stage-nightly");
expect(markup).toContain("bg-transparent text-white");
expect(markup).not.toContain("bg-message-action text-message-action-foreground");
});
it("keeps the normal send-button fill when artwork identification is inactive", () => {
stageArtworkState.variant = "nightly";
const markup = renderSendButton();
expect(markup).not.toContain("stage-nightly");
expect(markup).toContain("bg-message-action text-message-action-foreground");
});
it("only renders stop while running when Enter-to-send is available", () => {
const markup = renderRunningActions(false, true);
expect(markup).toContain('aria-label="Stop generation"');
expect(markup).not.toContain('aria-label="Send message"');
});
it("renders send alongside stop while running when Enter-to-send is unavailable", () => {
const markup = renderRunningActions(true, true);
expect(markup).toContain('aria-label="Stop generation"');
expect(markup).toContain('aria-label="Send message"');
expect(markup).toContain('type="submit"');
expect(markup).toContain("size-9 sm:size-8");
});
it("keeps stop as the only action while running with an empty composer", () => {
const markup = renderRunningActions(true, false);
expect(markup).toContain('aria-label="Stop generation"');
expect(markup).not.toContain('aria-label="Send message"');
});
});