fix: normalize structured app confirmations (#3220)

This commit is contained in:
José Almeida
2026-09-01 13:57:55 +00:00
committed by GitHub
parent 2023febe63
commit 9fa6a4d3ad
6 changed files with 110 additions and 19 deletions
@@ -0,0 +1,46 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { cleanup, fireEvent, render, screen, waitFor, within } from "@testing-library/react";
import { QuickChatDeleteDialog } from "./quick-chat-delete-dialog";
afterEach(cleanup);
function renderDeleteDialog(onConfirm = vi.fn()) {
return render(
<QuickChatDeleteDialog sessionToDelete="chat-1" onOpenChange={vi.fn()} onConfirm={onConfirm} />,
);
}
describe("QuickChatDeleteDialog structured description", () => {
// @covers AC-UI-SURFACE-TEXT-HIERARCHY-001.2
it("keeps one left-aligned description boundary with semantic paragraphs and a list", () => {
renderDeleteDialog();
const dialog = screen.getByRole("alertdialog");
const descriptions = dialog.querySelectorAll('[data-slot="alert-dialog-description"]');
expect(descriptions).toHaveLength(1);
const description = descriptions[0] as HTMLElement;
expect(description.tagName).toBe("DIV");
expect(description.className).toContain("min-w-0");
expect(description.className).toContain("text-left");
expect(description.className).toContain("space-y-2");
expect(description.querySelectorAll(":scope > p")).toHaveLength(2);
const list = description.querySelector(":scope > ul");
expect(list).not.toBeNull();
expect(list?.querySelectorAll(":scope > li")).toHaveLength(3);
});
// @covers AC-UI-SURFACE-TEXT-HIERARCHY-001.5
it("preserves the cancel and delete actions", async () => {
const onConfirm = vi.fn();
renderDeleteDialog(onConfirm);
const dialog = screen.getByRole("alertdialog");
expect(within(dialog).getByRole("button", { name: "Cancel" })).toBeTruthy();
fireEvent.click(within(dialog).getByRole("button", { name: "Delete" }));
await waitFor(() => expect(onConfirm).toHaveBeenCalledTimes(1));
});
});
@@ -29,15 +29,15 @@ export function QuickChatDeleteDialog({
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>{t("chat:deleteQuickChat")}</AlertDialogTitle>
<AlertDialogDescription asChild>
<AlertDialogDescription asChild className="min-w-0 space-y-2 text-left">
<div>
<p>{t("chat:deleteQuickChatIntro")}</p>
<ul className="list-disc list-inside mt-2 space-y-1">
<ul className="list-disc list-inside space-y-1">
<li>{t("chat:allConversationHistory")}</li>
<li>{t("chat:theTaskAndItsData")}</li>
<li>{t("chat:theAssociatedWorktree")}</li>
</ul>
<p className="mt-2">{t("chat:thisActionCannotBeUndone")}</p>
<p>{t("chat:thisActionCannotBeUndone")}</p>
</div>
</AlertDialogDescription>
</AlertDialogHeader>
@@ -260,4 +260,26 @@ describe("AgentProfileDeleteConflictDialog layout", () => {
expect(body.contains(footer)).toBe(false);
expect(within(footer).getByRole("button", { name: "Cancel" })).toBeTruthy();
});
// @covers AC-UI-SURFACE-TEXT-HIERARCHY-001.2, AC-UI-SURFACE-TEXT-HIERARCHY-001.3
it("keeps long conflict labels in one left-aligned semantic description", () => {
const longTaskTitle = `Profile task ${"x".repeat(320)}`;
renderConflictDialog({
activeSessions: [{ task_id: "long-task", task_title: longTaskTitle, is_ephemeral: false }],
watchers: [],
routingTiers: [],
automations: [],
});
const body = screen.getByTestId("agent-profile-delete-conflict-body");
expect(body.className).toContain("min-w-0");
expect(body.className).toContain("text-left");
expect(body.className).toContain("space-y-2");
const directParagraphs = body.querySelectorAll(":scope > p");
expect(directParagraphs).toHaveLength(2);
expect(directParagraphs[1]?.className).not.toContain("mt-2");
const taskItem = screen.getByText(longTaskTitle);
expect(taskItem.tagName).toBe("LI");
});
});
@@ -184,7 +184,7 @@ export function AgentProfileDeleteConflictDialog({
<AlertDialogDescription
asChild
data-testid="agent-profile-delete-conflict-body"
className="min-h-0 min-w-0 overflow-x-hidden overflow-y-auto overscroll-contain text-left"
className="min-h-0 min-w-0 space-y-2 overflow-x-hidden overflow-y-auto overscroll-contain text-left"
>
<div>
<p>{t("agents:profileInUseIntro")}</p>
@@ -210,9 +210,9 @@ export function AgentProfileDeleteConflictDialog({
/>
<UtilityAgentConflictSection utilityAgents={utilityAgents} />
{hasHardBlockers ? (
<p className="mt-2">{t("agents:changeTierMappingsFirst")}</p>
<p>{t("agents:changeTierMappingsFirst")}</p>
) : (
<p className="mt-2">{t("agents:deleteAnywayConsequences")}</p>
<p>{t("agents:deleteAnywayConsequences")}</p>
)}
</div>
</AlertDialogDescription>
@@ -242,9 +242,9 @@ function UtilityAgentConflictSection({
const { t } = useTranslation();
if (utilityAgents.length === 0) return null;
return (
<div className="mt-2" data-testid="profile-conflict-utility-agents">
<div className="space-y-1" data-testid="profile-conflict-utility-agents">
<p className="font-medium text-sm">{t("agents:conflictUtilityAgentsTitle")}</p>
<ul className="list-disc list-inside mt-1 space-y-0.5">
<ul className="list-disc list-inside space-y-0.5">
{utilityAgents.map((agent) => (
<li key={agent.id} className="text-sm">
{agent.name || agent.id}
@@ -266,9 +266,9 @@ function SessionConflictSection({
}) {
if (sessions.length === 0) return null;
return (
<div className="mt-2">
<div className="space-y-1">
<p className="font-medium text-sm">{title}</p>
<ul className="list-disc list-inside mt-1 space-y-0.5">
<ul className="list-disc list-inside space-y-0.5">
{sessions.map((t) => (
<li key={t.task_id} className="text-sm">
{t.task_title || fallback}
@@ -288,9 +288,9 @@ function WatcherConflictSection({
const entries = Object.entries(watchersByKind);
if (entries.length === 0) return null;
return (
<div className="mt-2">
<div className="space-y-1">
<p className="font-medium text-sm">{t("agents:conflictWatchersTitle")}</p>
<ul className="list-disc list-inside mt-1 space-y-0.5">
<ul className="list-disc list-inside space-y-0.5">
{entries.map(([kind, items]) => (
<li key={kind} className="text-sm">
<span className="font-medium">
@@ -317,9 +317,9 @@ function AutomationConflictSection({
const { t } = useTranslation();
if (automations.length === 0) return null;
return (
<div className="mt-2" data-testid="delete-conflict-automations">
<div className="space-y-1" data-testid="delete-conflict-automations">
<p className="font-medium text-sm">{t("agents:conflictAutomationsTitle")}</p>
<ul className="list-disc list-inside mt-1 space-y-0.5">
<ul className="list-disc list-inside space-y-0.5">
{automations.map((ref) => (
<li key={ref.id} className="text-sm">
<Trans
@@ -350,9 +350,9 @@ function RoutingTierConflictSection({
const { t } = useTranslation();
if (routingTiers.length === 0) return null;
return (
<div className="mt-2">
<div className="space-y-1">
<p className="font-medium text-sm">{t("agents:conflictTierMappingsTitle")}</p>
<ul className="list-disc list-inside mt-1 space-y-0.5">
<ul className="list-disc list-inside space-y-0.5">
{routingTiers.map((ref) => (
<li key={`${ref.workspace_id}-${ref.provider_id}-${ref.tier}`} className="text-sm">
<Trans
+1 -1
View File
@@ -104,7 +104,7 @@ the final composition.
## Work orders
- [x] [Task 01: Standardize surface typography primitives](task-01-standardize-surface-typography-primitives.md) (completed)
- [ ] [Task 02: Normalize structured app confirmations](task-02-normalize-structured-app-confirmations.md)
- [x] [Task 02: Normalize structured app confirmations](task-02-normalize-structured-app-confirmations.md) (completed)
- [ ] [Task 03: Refine task cleanup confirmations](task-03-refine-task-cleanup-confirmations.md)
- [ ] [Task 04: Normalize structured task confirmations](task-04-normalize-structured-task-confirmations.md)
- [ ] [Task 05: Add responsive text hierarchy E2E](task-05-add-responsive-text-hierarchy-e2e.md)
@@ -1,7 +1,7 @@
---
id: "02-normalize-structured-app-confirmations"
title: "Normalize structured app confirmations"
status: pending
status: completed
wave: 2
depends_on:
- "01-standardize-surface-typography-primitives"
@@ -91,4 +91,27 @@ pnpm run i18n:check
## Results
Pending implementation.
- RED: The focused component tests failed on the pre-change markup because the
Quick Chat description had no local `min-w-0` containment and the
agent-profile conflict body had no structured `space-y-2` spacing. Existing
behavior tests remained green.
- GREEN: Added local structured-description classes and semantic list-item
containment to the Quick Chat delete and agent-profile delete conflict
dialogs. Preserved all localized copy, blocker groups, action variants,
callbacks, and existing scroll containment.
- Added focused component assertions for the rendered description boundary,
direct paragraph/list structure, long dynamic task values, class-level
containment, and Quick Chat Delete callback behavior.
- Focused verification passed with 2 test files and 13 tests:
`pnpm --filter @kandev/web test --
components/quick-chat/quick-chat-delete-dialog.test.tsx
components/settings/agent-profile-delete-dialog.test.tsx`
- Final exact-head checks passed: web typecheck, focused ESLint with
`--max-warnings 0`, `pnpm run i18n:check`, Prettier checks for all four
owned source/test files, `git diff --check`, and
`python3 scripts/lint-spec-files.py --all`.
- Owned files:
`apps/web/components/quick-chat/quick-chat-delete-dialog.tsx`,
`apps/web/components/quick-chat/quick-chat-delete-dialog.test.tsx`,
`apps/web/components/settings/agent-profile-delete-dialog.tsx`, and
`apps/web/components/settings/agent-profile-delete-dialog.test.tsx`.