fix(ui): dismiss skipped confirmations when sending a message (#19496)

## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Tasks and Agent Chat show saved requests above the message composer.
> - A person can send a separate message while a confirmation is open.
> - The old code closed only question forms, after the message save
finished.
> - A confirmation could stay open or open again after a refresh.
> - This change closes the form when sending starts and keeps the
request in the feed.
> - People can open the request later and make the original decision.

## Linked Issues or Issue Description

**What happened?**

A confirmation stayed open when a person sent a separate message.

**Expected behavior**

The form closes immediately. The request stays in the feed. A person can
open it later. Sending a message does not record an approval.

**Steps to reproduce**

1. Open a task with a pending confirmation.
2. Send a separate message below the form.
3. Observe the form while the message save is pending.
4. Reload the task and open the request from the feed.

**Paperclip version or commit**

Reproduced at a434edf991.

**Deployment mode**

Source checkout and isolated Storybook fixtures.

## What Changed

- Close the composer request before the message save finishes.
- Keep earlier saved requests in history after a newer human message.
- Save browser dismissal IDs in one batch after a successful send.
- Restore the previous form after a failed send, including a reopened
request after an optimistic comment changes automatic selection.
- Track deliberate selection changes separately from automatic
filtering. Keep a newer request open when an earlier send fails.
- Allow a new request to open while an earlier message is being saved.
- Add tests for confirmation and checkbox forms in task and Agent Chat
modes.
- Add Send Instead Of Confirming and Reopen Skipped Confirmation
stories.
- Update the existing answer-later story to check the current answer
receipt.
- Update the behavior and Storybook documentation.

## Verification

- TDD: Six new cases failed before the main fix. Four more cases
reproduced the review finding before the recovery fix. They cover failed
sends with an optimistic comment and its rollback in both task and Agent
Chat modes.
- Component checks: 518 passed, one existing test skipped. Run `pnpm
--filter @paperclipai/ui exec vitest run
src/components/TaskChatThread.test.tsx
src/components/task-chat/TaskChatComposer.test.tsx`.
- UI typecheck passed after preparing the local plugin SDK dependency.
- Token gates passed.
- Catalog structure checks passed. The three catalog test files passed
all 30 tests.
- Six Playwright cases passed in an owned Storybook server. They cover
the initial form, sending, manual reopening, light theme, mobile, and
answering later. Play assertions passed. Accessibility reports had zero
violations.
- The stories use the production thread with simulated saves and
decisions. Existing optional fixture endpoints return 404. These do not
prevent the selected journeys.
- No visual baseline comparison was possible. The baseline manifest has
no published snapshots.
- `pnpm verify --explain` requested broader CI coverage. [Required
CI](https://github.com/paperclipai/paperclip/actions/runs/42253585800)
passed on fda44f2648.
- Greptile and Paperclip Review both report 5/5 with no findings on the
final head.
- The remote commit tree matches the tested local tree:
7e22201513.

## Risks

- Older pending requests now stay in history after a newer human
message. People must select the feed entry to open them.
- Dismissal changes presentation. Server approval checks and request
decisions keep their existing behavior.
- Browser fixtures do not prove live provider continuation or staging
behavior.

## Model Used

OpenAI GPT-6 Codex. The exact backend model identifier and context
window were not exposed to this run. Used reasoning, repository
inspection, edits, shell tools, component tests, and browser tools.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <[email protected]>
This commit is contained in:
DottaandPaperclip authored and GitHub committed 2026-11-07 08:34:14 -06:00
1 parent a4050187ec
commit c740cb9ded
7 files changed
+198 -25

No files matched your search

+4
View File
@@ -648,6 +648,10 @@ behavior for review cards. Its dismissal save and decisions are simulated;
server persistence and permission boundaries have separate regression tests.
The demo uses real thread/form components with fixture response callbacks.
The Agent Chat stories share the same component fixture.
The **Send instead of confirming** and **Reopen skipped confirmation** stories
exercise sending a separate message while a completion review is open, then
opening that unchanged request from the feed. These use the production thread
with simulated message saves and decision callbacks.
Use **Composer → New task** to review task creation through the production
`TaskChatComposer`. The editor, file menu, work modes, assignee/model picker,
+6
View File
@@ -2023,6 +2023,12 @@ Dismissal never creates an answer, decision, execution, or continuation wake.
Raw runtime requests retain browser-local dismissal; reopening restores the
original request and draft. A failed save leaves the request open with an error.
Sending a separate message closes the current composer request immediately,
before the send finishes. After a successful send, earlier requests stay in the
feed and can be reopened manually; a newer request can still open normally.
If sending fails, restore the previous form unless the person has opened a
different request. This presentation change does not answer or approve a request.
A saved ordinary question becomes historical after a newer human task message
or explicit human dismissal. It remains answerable after completion without
reopening work or waking the agent. Dismissed ordinary requests, including native
+117
View File
@@ -4036,6 +4036,123 @@ describe.each([true, false])("Unanswered question history (conversationMode=%s)"
expect(container.querySelector('[data-testid="task-chat-unanswered-question"]')).not.toBeNull();
});
it.each(["request_confirmation", "request_checkbox_confirmation"] as const)("immediately closes %s while a new message is sending and keeps it reopenable", async kind => {
const fixtures = await import("@/fixtures/issueThreadInteractionFixtures");
const request = kind === "request_confirmation"
? fixtures.genericPendingRequestConfirmationInteraction
: fixtures.pendingRequestCheckboxConfirmationInteraction;
let finishSend!: () => void;
const onAdd = vi.fn(() => new Promise<void>(resolve => { finishSend = resolve; }));
const onAcceptInteraction = vi.fn();
const onRejectInteraction = vi.fn();
const show = () => render(<TaskChatThread {...props} comments={[]} interactions={[request]}
onAdd={onAdd} onAcceptInteraction={onAcceptInteraction} onRejectInteraction={onRejectInteraction} />);
show();
expect(takeover()).not.toBeNull();
await act(async () => editorCallbacks.change("Let's discuss something else first."));
await act(async () => container.querySelector<HTMLButtonElement>('[data-testid="task-chat-composer-send"]')!.click());
expect(onAdd).toHaveBeenCalledTimes(1);
expect(takeover()).toBeNull();
await act(async () => finishSend());
expect(pendingIndicator()).toBeNull();
flushSync(() => root!.unmount());
root = createRoot(container);
show();
expect(takeover()).toBeNull();
await act(async () => container.querySelector<HTMLButtonElement>(`[aria-label="Open request: ${request.title}"]`)!.click());
expect(takeover()).not.toBeNull();
expect(request.status).toBe("pending");
expect(onAcceptInteraction).not.toHaveBeenCalled();
expect(onRejectInteraction).not.toHaveBeenCalled();
await act(async () => editorCallbacks.change("Keep the review for later."));
await act(async () => container.querySelector<HTMLButtonElement>('[data-testid="task-chat-composer-send"]')!.click());
expect(takeover()).toBeNull();
await act(async () => finishSend());
expect(takeover()).toBeNull();
});
it("leaves an earlier confirmation in history after a newer user message, including on reload", async () => {
const request = { ...genericPendingRequestConfirmationInteraction, createdAt: old.createdAt };
const show = (comments = movedOn.slice(0, 0)) => render(<TaskChatThread {...props} comments={comments} interactions={[request]} />);
show();
expect(takeover()).not.toBeNull();
await act(async () => show(movedOn));
expect(takeover()).toBeNull();
expect(pendingIndicator()).toBeNull();
flushSync(() => root!.unmount());
root = createRoot(container);
show(movedOn);
expect(takeover()).toBeNull();
await act(async () => container.querySelector<HTMLButtonElement>(`[aria-label="Open request: ${request.title}"]`)!.click());
expect(takeover()).not.toBeNull();
});
it.each([true, false])("keeps a new confirmation open when an earlier message finishes sending (success=%s)", async success => {
const request = genericPendingRequestConfirmationInteraction;
const next = { ...request, id: "new-review", title: "New review", createdAt: new Date("2026-11-06T12:00:00Z") };
let finishSend!: () => void;
let rejectSend!: (error: Error) => void;
const onAdd = vi.fn(() => new Promise<void>((resolve, reject) => { finishSend = resolve; rejectSend = reject; }));
const show = (interactions = [request]) => render(<TaskChatThread {...props} comments={[]} interactions={interactions} onAdd={onAdd} />);
show();
await act(async () => editorCallbacks.change("Let's discuss something else first."));
await act(async () => container.querySelector<HTMLButtonElement>('[data-testid="task-chat-composer-send"]')!.click());
expect(takeover()).toBeNull();
await act(async () => show([request, next]));
expect(takeover()?.textContent).toContain("New review");
await act(async () => {
if (success) finishSend();
else rejectSend(new Error("Message not sent"));
});
expect(takeover()?.textContent).toContain("New review");
});
it("restores the confirmation when sending a separate message fails", async () => {
render(<TaskChatThread {...props} comments={[]} interactions={[genericPendingRequestConfirmationInteraction]}
onAdd={vi.fn().mockRejectedValue(new Error("Message not sent"))} />);
await act(async () => editorCallbacks.change("Let's discuss something else first."));
await act(async () => container.querySelector<HTMLButtonElement>('[data-testid="task-chat-composer-send"]')!.click());
expect(takeover()).not.toBeNull();
expect(container.querySelector('[data-testid="mock-editor"]')?.textContent).toContain("Let's discuss something else first.");
});
it.each([false, true])("restores a reopened historical confirmation after a failed optimistic send (rollback=%s)", async rollback => {
const request = { ...genericPendingRequestConfirmationInteraction, createdAt: old.createdAt };
let rejectSend!: (error: Error) => void;
const onAdd = vi.fn(() => new Promise<void>((_resolve, reject) => { rejectSend = reject; }));
const show = (comments = movedOn) => render(<TaskChatThread {...props} comments={comments} interactions={[request]} onAdd={onAdd} />);
show();
await act(async () => container.querySelector<HTMLButtonElement>(`[aria-label="Open request: ${request.title}"]`)!.click());
expect(takeover()).not.toBeNull();
await act(async () => editorCallbacks.change("Let's discuss something else first."));
await act(async () => container.querySelector<HTMLButtonElement>('[data-testid="task-chat-composer-send"]')!.click());
const optimistic = { ...movedOn[0]!, id: "optimistic-send", createdAt: new Date("2026-08-15T12:10:00Z") };
await act(async () => show([...movedOn, optimistic]));
expect(takeover()).toBeNull();
await act(async () => {
if (rollback) show();
rejectSend(new Error("Message not sent"));
});
expect(takeover()).not.toBeNull();
expect(takeover()?.textContent).toContain(request.title);
expect(container.querySelector('[data-testid="mock-editor"]')?.textContent).toContain("Let's discuss something else first.");
});
it("does not restore an earlier confirmation after the person opens and dismisses another request during a failed send", async () => {
const first = { ...genericPendingRequestConfirmationInteraction, id: "earlier-review", title: "Earlier review", createdAt: old.createdAt };
const second = { ...first, id: "later-review", title: "Later review" };
let rejectSend!: (error: Error) => void;
const onAdd = vi.fn(() => new Promise<void>((_resolve, reject) => { rejectSend = reject; }));
render(<TaskChatThread {...props} comments={movedOn} interactions={[first, second]} onAdd={onAdd} />);
await act(async () => container.querySelector<HTMLButtonElement>('[aria-label="Open request: Earlier review"]')!.click());
await act(async () => editorCallbacks.change("Let's discuss something else first."));
await act(async () => container.querySelector<HTMLButtonElement>('[data-testid="task-chat-composer-send"]')!.click());
await act(async () => container.querySelector<HTMLButtonElement>('[aria-label="Open request: Later review"]')!.click());
await dismiss();
await act(async () => rejectSend(new Error("Message not sent")));
expect(takeover()).toBeNull();
});
it.each(["Cancel", "dismiss"])("leaves only the history card after %s on a fresh question", async action => {
render(<TaskChatThread {...props} comments={[]} interactions={[old]} />);
await click("Yes");
+34 -21
View File
@@ -2786,29 +2786,27 @@ export function TaskChatThread(props: TaskChatThreadProps) {
input.kind === "durable"
? Boolean(input.interaction.dismissedAt) || dismissedInputIds.has(input.interaction.id) || dismissedInputIds.has(input.key)
: dismissedInputIds.has(input.key), [dismissedInputIds]);
const pendingReminderInputs = useMemo(() => pendingComposerInputs.filter(input =>
!isInputDismissed(input) && !(input.kind === "durable" && input.interaction.kind === "ask_user_questions")),
[pendingComposerInputs, isInputDismissed]);
const [dismissError, setDismissError] = useState<string | null>(null);
const latestUserComment = useMemo(() => comments
.filter(comment => !comment.deletedAt && comment.authorUserId && !comment.authorAgentId && !comment.createdByRunId)
.reduce<(typeof comments)[number] | null>((latest, comment) =>
!latest || toMs(comment.createdAt) >= toMs(latest.createdAt) ? comment : latest, null), [comments]);
const latestUserCommentId = latestUserComment?.id ?? null;
// A later message leaves a question answerable in history, without reopening
// its form on every render or reload. Explicit selection can still reopen it.
const currentPendingInputs = useMemo(() => pendingComposerInputs.filter(input =>
!isInputDismissed(input) && !(input.kind === "durable" && input.interaction.kind === "ask_user_questions"
!isInputDismissed(input) && !(input.kind === "durable"
&& latestUserComment && toMs(input.interaction.createdAt) < toMs(latestUserComment.createdAt))),
[pendingComposerInputs, isInputDismissed, latestUserComment]);
const pendingReminderInputs = useMemo(() => currentPendingInputs.filter(input =>
!(input.kind === "durable" && input.interaction.kind === "ask_user_questions")),
[currentPendingInputs]);
const currentPendingKeys = useMemo(() => new Set(currentPendingInputs.map(input => input.key)), [currentPendingInputs]);
const [takeoverMode, setTakeoverMode] = useState<"open" | "normal">("open");
const [selectedPendingKey, setSelectedPendingKey] = useState<string | null>(
null,
);
const explicitlyOpenedInputRef = useRef<string | null>(null);
const inputSelectionVersionRef = useRef(0);
const knownPendingKeysRef = useRef<Set<string>>(new Set());
const lastInputUserCommentRef = useRef(latestUserCommentId);
const lastInputUserCommentRef = useRef(latestUserComment);
useEffect(() => {
const currentKeys = new Set(
pendingComposerInputs.map((input) => input.key),
@@ -2816,21 +2814,27 @@ export function TaskChatThread(props: TaskChatThreadProps) {
const hasNewInput = pendingComposerInputs.some(
(input) => currentPendingKeys.has(input.key) && !knownPendingKeysRef.current.has(input.key),
);
const movedOn = lastInputUserCommentRef.current !== latestUserCommentId;
lastInputUserCommentRef.current = latestUserCommentId;
const previousUserComment = lastInputUserCommentRef.current;
const movedOn = latestUserComment && previousUserComment?.id !== latestUserComment.id
&& (!previousUserComment || toMs(latestUserComment.createdAt) >= toMs(previousUserComment.createdAt));
lastInputUserCommentRef.current = latestUserComment;
knownPendingKeysRef.current = currentKeys;
setSelectedPendingKey((current) => {
if (hasNewInput) return currentPendingInputs[0]?.key ?? null;
const currentInput = pendingComposerInputs.find(input => input.key === current);
const questionMovedOn = movedOn && currentInput?.kind === "durable"
&& currentInput.interaction.kind === "ask_user_questions" && !currentPendingKeys.has(currentInput.key);
const requestMovedOn = movedOn && currentInput?.kind === "durable"
&& !isInputDismissed(currentInput) && !currentPendingKeys.has(currentInput.key);
return current && currentKeys.has(current)
&& (currentPendingKeys.has(current) || explicitlyOpenedInputRef.current === current)
&& !questionMovedOn
&& !requestMovedOn
? current
: (currentPendingInputs[0]?.key ?? null);
});
if (hasNewInput) setTakeoverMode("open");
}, [pendingComposerInputs, latestUserCommentId, currentPendingInputs, currentPendingKeys]);
if (hasNewInput) {
inputSelectionVersionRef.current += 1;
setTakeoverMode("open");
}
}, [pendingComposerInputs, latestUserComment, currentPendingInputs, currentPendingKeys, isInputDismissed]);
const selectedPendingInput =
pendingComposerInputs.find((input) => input.key === selectedPendingKey) ??
currentPendingInputs[0] ??
@@ -2843,6 +2847,7 @@ export function TaskChatThread(props: TaskChatThreadProps) {
? `paperclip:task-input:${issueId ?? "unknown"}:${selectedPendingInput.key}`
: undefined;
const openPendingTakeover = useCallback(() => {
inputSelectionVersionRef.current += 1;
if (!pendingReminderInputs.some(input => input.key === selectedPendingInput?.key)) {
setSelectedPendingKey(pendingReminderInputs[0]?.key ?? null);
}
@@ -2850,6 +2855,7 @@ export function TaskChatThread(props: TaskChatThreadProps) {
}, [pendingReminderInputs, selectedPendingInput]);
const showNextPendingInput = useCallback(() => {
if (pendingReminderInputs.length < 2) return;
inputSelectionVersionRef.current += 1;
const currentIndex = pendingReminderInputs.findIndex(
(input) => input.key === selectedPendingInput?.key,
);
@@ -2860,6 +2866,7 @@ export function TaskChatThread(props: TaskChatThreadProps) {
}, [pendingReminderInputs, selectedPendingInput?.key]);
const skipPendingInput = useCallback(
async (input: PendingComposerInput) => {
inputSelectionVersionRef.current += 1;
if (input.kind === "runtime") {
await handleRuntimeRequestDecision(input.item, { action: "cancel" });
} else {
@@ -2880,19 +2887,23 @@ export function TaskChatThread(props: TaskChatThreadProps) {
agentId: args[2].assigneeAgentId,
agent: agentMap?.get(args[2].assigneeAgentId ?? ""),
} : undefined);
const selectionVersion = ++inputSelectionVersionRef.current;
explicitlyOpenedInputRef.current = null;
setTakeoverMode("normal");
try {
await onAdd(...args);
if (selectedPendingInput?.kind === "durable" && selectedPendingInput.interaction.kind === "ask_user_questions") {
dismissInput(selectedPendingInput.interaction.id);
setSelectedPendingKey(null);
setTakeoverMode("normal");
}
dismissInput(pendingComposerInputs.map(input => input.kind === "durable" ? input.interaction.id : input.key));
} catch (error) {
if (inputSelectionVersionRef.current === selectionVersion) {
explicitlyOpenedInputRef.current = selectedPendingInput?.key ?? null;
setSelectedPendingKey(selectedPendingInput?.key ?? null);
setTakeoverMode(takeoverMode);
}
rejectSubmission(attempt, error);
throw error;
}
},
[beginSubmission, rejectSubmission, onAdd, selectedPendingInput, dismissInput, agentMap],
[beginSubmission, rejectSubmission, onAdd, pendingComposerInputs, selectedPendingInput, dismissInput, agentMap, takeoverMode],
);
const optimisticRunnerStartup =
hasAgentAssignee &&
@@ -2972,6 +2983,7 @@ export function TaskChatThread(props: TaskChatThreadProps) {
);
const reopenToolReview = useCallback((interactionId: string) => {
inputSelectionVersionRef.current += 1;
const key = pendingComposerInputs.find(input => input.kind === "durable" && input.interaction.id === interactionId)?.key ?? `interaction:${interactionId}`;
explicitlyOpenedInputRef.current = key;
setSelectedPendingKey(key);
@@ -3090,6 +3102,7 @@ export function TaskChatThread(props: TaskChatThreadProps) {
pendingCount: Math.max(1, pendingReminderInputs.length),
content: takeoverContent,
onDismiss: () => {
inputSelectionVersionRef.current += 1;
void (async () => {
setDismissError(null);
try {
+2 -2
View File
@@ -16,10 +16,10 @@ export function useDismissedTaskQuestions(issueId?: string | null, userId?: stri
const [state, setState] = useState(() => ({ storageKey, ids: loadDismissedQuestions(storageKey) }));
const dismissedQuestionIds = useMemo(() => state.storageKey === storageKey
? state.ids : loadDismissedQuestions(storageKey), [state, storageKey]);
const dismissQuestion = useCallback((interactionId: string) => {
const dismissQuestion = useCallback((interactionId: string | readonly string[]) => {
// Another tab may have saved more dismissals since this thread rendered.
const ids = new Set([...loadDismissedQuestions(storageKey), ...dismissedQuestionIds]);
ids.add(interactionId);
for (const id of typeof interactionId === "string" ? [interactionId] : interactionId) ids.add(id);
try {
if (storageKey) localStorage.setItem(storageKey, JSON.stringify([...ids]));
} catch {
@@ -33,7 +33,7 @@ export function QuestionChat({ movedOn = false, multiple = false, answered = fal
return client;
});
const [interactions, setInteractions] = useState<IssueThreadInteraction[]>([
...(review ? [{ ...genericPendingRequestConfirmationInteraction, id: "completion-review", title: "Review requested", summary: "Provide app configuration to finish integration.", payload: { version: 1 as const, prompt: "Provide app configuration to finish integration.", acceptLabel: "Approve completion", rejectLabel: "Continue work", target: { type: "custom" as const, key: "native_completion_review", label: "Completion decision" } } }] : []),
...(review ? [{ ...genericPendingRequestConfirmationInteraction, id: "completion-review", createdAt: question.createdAt, title: "Review requested", summary: "Provide app configuration to finish integration.", payload: { version: 1 as const, prompt: "Provide app configuration to finish integration.", acceptLabel: "Approve completion", rejectLabel: "Continue work", target: { type: "custom" as const, key: "native_completion_review", label: "Completion decision" } } }] : []),
...(review ? [] : [answered ? { ...question, status: "answered" as const, resolvedAt: new Date("2026-04-01T12:06:00Z"), result: { version: 1 as const, answers: [{ questionId: "color", optionIds: ["blue"] }] } } : question]),
...(multiple ? [otherQuestion] : []),
]);
@@ -22,9 +22,42 @@ const meta = {
export default meta;
type Story = StoryObj<typeof meta>;
function resetDismissals() {
localStorage.removeItem("paperclip:task-question-dismissals:user-board:task-unanswered-questions-demo");
}
async function sendInsteadOfConfirming(canvasElement: HTMLElement) {
const canvas = within(canvasElement);
await waitFor(() => expect(canvas.getByTestId("task-chat-composer-takeover")).toBeVisible());
await userEvent.type(canvas.getByRole("textbox", { name: "editable markdown" }), "Leave that review for later. Show me the integration status.");
await userEvent.click(canvas.getByRole("button", { name: /^Send$/ }));
await expect(canvas.queryByTestId("task-chat-composer-takeover")).not.toBeInTheDocument();
await expect(canvas.queryByTestId("task-chat-pending-input-indicator")).not.toBeInTheDocument();
await expect(canvas.getByRole("button", { name: "Open request: Review requested" })).toBeVisible();
}
// Keep the test-drive story's dismissal preference across actual page reloads.
export const TestDrive: Story = {};
export const CompletionReview: Story = { args: { review: true } };
export const SendInsteadOfConfirming: Story = {
args: { review: true },
beforeEach: resetDismissals,
play: async ({ canvasElement }) => {
await sendInsteadOfConfirming(canvasElement);
},
};
export const ReopenSkippedConfirmation: Story = {
args: { review: true },
beforeEach: resetDismissals,
play: async ({ canvasElement }) => {
await sendInsteadOfConfirming(canvasElement);
const canvas = within(canvasElement);
await userEvent.click(canvas.getByRole("button", { name: "Open request: Review requested" }));
await expect(canvas.getByTestId("task-chat-composer-takeover")).toBeVisible();
await expect(canvas.getByRole("button", { name: "Approve completion" })).toBeVisible();
await expect(canvas.getByRole("button", { name: "Continue work" })).toBeVisible();
},
};
export const MovedOn: Story = { args: { movedOn: true } };
export const MultipleUnanswered: Story = { args: { multiple: true } };
export const AnsweredHistory: Story = { args: { answered: true } };
@@ -44,6 +77,6 @@ export const DismissAndAnswerLater: Story = {
await userEvent.click(canvas.getByTestId("task-chat-unanswered-question"));
await expect(canvas.getByRole("radio", { name: "Green" })).toBeChecked();
await userEvent.click(canvas.getByRole("button", { name: /^(Send|Submit) answers$/ }));
await waitFor(() => expect(canvas.getByTestId("task-chat-answered-questions-receipt")).toBeVisible());
await waitFor(() => expect(canvas.getByText("Answers recorded", { exact: true })).toBeVisible());
},
};