fix: suppress title previews during Kanban multi-select (#3919)

This commit is contained in:
Carlos Florêncio
2026-09-25 10:31:14 +01:00
committed by GitHub
parent 04b84aa9ef
commit 567b965be0
10 changed files with 346 additions and 6 deletions
+1 -1
View File
@@ -275,7 +275,7 @@ export function KanbanCardShell({
<KanbanCardBody
task={task}
repositoryChips={repositoryChips ?? []}
enableTitleHover
enableTitleHover={!isMultiSelectMode}
actions={
<KanbanCardActionSlot
isMultiSelectMode={isMultiSelectMode}
@@ -208,6 +208,21 @@ describe("KanbanCard — title-preview disclosure widening (AC-UI-PIPELINE-ROW-0
});
});
describe("KanbanCard — title preview in multi-select mode", () => {
// @covers AC-TASKS-RICH-TASK-TITLE-PREVIEWS-001.2
it.each([
["unselected", false],
["selected", true],
] as const)("does not mount a title preview for a %s card", (_label, isSelected) => {
renderCard(baseTask({ description: "Some description content" }), [], {
isMultiSelectMode: true,
isSelected,
});
expect(screen.queryByTestId("task-title-preview-trigger")).toBeNull();
});
});
// @covers AC-TASKS-MOBILE-KANBAN-SCROLL-001.1, AC-TASKS-MOBILE-KANBAN-SCROLL-001.4
it.each(["Enter", " "])(
"phone cards open with %j without exposing or invoking drag pickup",
@@ -84,9 +84,11 @@ function RowMenuTrigger({
function RowInfoColumn({
task,
repositoryChips,
isMultiSelectMode,
}: {
task: Task;
repositoryChips: ReturnType<typeof resolveTaskRepositoryChips>;
isMultiSelectMode?: boolean;
}) {
const { t } = useTranslation();
const sessionCount = task.sessionCount ?? 0;
@@ -94,7 +96,7 @@ function RowInfoColumn({
return (
<div className="w-[200px] min-w-0 shrink-0" data-testid="pipeline-row-info">
<div data-testid="pipeline-row-title">
<CardTitle task={task} enableTitleHover />
<CardTitle task={task} enableTitleHover={!isMultiSelectMode} />
</div>
{/* Height is reserved whether or not the task has a repository, so a
row with no repository is the same height as one with, and the
@@ -259,7 +261,11 @@ export function PipelineRow({
/>
</div>
)}
<RowInfoColumn task={task} repositoryChips={repositoryChips} />
<RowInfoColumn
task={task}
repositoryChips={repositoryChips}
isMultiSelectMode={isMultiSelectMode}
/>
<div
ref={overflowStage.outerRef}
data-testid="pipeline-row-overflow-region"
@@ -135,13 +135,13 @@ function renderKanbanCard() {
);
}
function renderPipelineRow() {
function renderPipelineRow(task: Task = TASK, isMultiSelectMode = false) {
return render(
<ToastProvider>
<StateProvider initialState={SHARED_INITIAL_STATE}>
<TooltipProvider delayDuration={0}>
<Graph2TaskPipeline
task={TASK}
task={task}
steps={STEPS}
moveTargetSteps={STEPS}
workspaceId={WORKSPACE_ID}
@@ -151,6 +151,7 @@ function renderPipelineRow() {
onPreviewTask={() => undefined}
onOpenTask={() => undefined}
onDeleteTask={() => undefined}
isMultiSelectMode={isMultiSelectMode}
/>
</TooltipProvider>
</StateProvider>
@@ -158,6 +159,22 @@ function renderPipelineRow() {
);
}
describe("Pipeline title preview in multi-select mode", () => {
// @covers AC-TASKS-RICH-TASK-TITLE-PREVIEWS-001.2
it.each([
["outside", false, true],
["inside", true, false],
] as const)("is %s multi-select mode", (_mode, isMultiSelectMode, previewEnabled) => {
renderPipelineRow({ ...TASK, parentTaskId: "parent-1" }, isMultiSelectMode);
if (previewEnabled) {
expect(screen.getByTestId("task-title-preview-trigger")).not.toBeNull();
} else {
expect(screen.queryByTestId("task-title-preview-trigger")).toBeNull();
}
});
});
async function openMenuAndSnapshot() {
const trigger = screen.getByRole("button", { name: t("kanban:moreOptions") });
fireEvent.pointerDown(trigger, { button: 0, pointerId: 1 });
@@ -141,4 +141,61 @@ test.describe("Task title hover card on the Kanban card", () => {
await title.click();
await expect(testPage).toHaveURL(new RegExp(`/t/${parent.id}`));
});
// @covers AC-TASKS-RICH-TASK-TITLE-PREVIEWS-001.2, AC-TASKS-RICH-TASK-TITLE-PREVIEWS-001.3, AC-TASKS-RICH-TASK-TITLE-PREVIEWS-001.4
test("multi-select closes and suppresses title previews, then restores them when it ends", async ({
testPage,
apiClient,
seedData,
}) => {
test.setTimeout(120_000);
const { parent } = await seedParentWithSubtasks(
apiClient,
seedData,
"Preview selection parent",
);
const sibling = await apiClient.createTask(seedData.workspaceId, "Preview selection sibling", {
description: "Second preview eligible task",
workflow_id: seedData.workflowId,
workflow_step_id: seedData.startStepId,
repository_ids: [seedData.repositoryId],
});
const kanban = new KanbanPage(testPage);
await kanban.goto();
const parentCard = kanban.taskCard(parent.id);
const siblingCard = kanban.taskCard(sibling.id);
await expect(parentCard).toBeVisible({ timeout: 45_000 });
await expect(siblingCard).toBeVisible({ timeout: 45_000 });
const parentTitle = parentCard.getByTestId("task-card-title");
await parentTitle.hover();
const hoverCard = testPage.getByTestId("task-title-hover-card");
await expect(hoverCard).toBeVisible();
// Modifier-click enters selection mode while the pointer remains on the
// open title preview, so this proves the mode transition itself closes it.
await parentTitle.click({ modifiers: ["ControlOrMeta"] });
await expect(kanban.multiSelectToolbar).toContainText("1 selected");
await expect(hoverCard).toHaveCount(0);
const siblingTitle = siblingCard.getByTestId("task-card-title");
await siblingTitle.hover();
await dwell(
testPage,
300,
"negative-assertion",
"multi-select must suppress a preview on an unselected title",
);
await expect(hoverCard).toHaveCount(0);
await siblingTitle.click();
await expect(kanban.multiSelectToolbar).toContainText("2 selected");
await expect(kanban.board).toBeVisible();
await expect(testPage).not.toHaveURL(new RegExp(`/t/${sibling.id}`));
await kanban.multiSelectToggle.first().click();
await expect(kanban.multiSelectToolbar).not.toBeVisible();
await parentTitle.hover();
await expect(hoverCard).toBeVisible();
});
});