fix: normalize structured app confirmations (#3220)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
+25
-2
@@ -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`.
|
||||
|
||||
Reference in New Issue
Block a user