Files
kandev/apps/web/components/settings/workflow-card-dialogs.test.tsx
T
João Salavisa 03d5f98cb6 feat(i18n): localize Settings → Workflows (#2201)
Co-authored-by: Kandev Agent <agent@kandev.dev>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 15:30:27 +02:00

158 lines
4.9 KiB
TypeScript

import { afterEach, describe, expect, it, vi } from "vitest";
import { cleanup, render, screen } from "@testing-library/react";
import type { Workflow, WorkflowStep } from "@/lib/types/http";
import { StepDeleteDialog, WorkflowDeleteDialog } from "./workflow-card-dialogs";
/**
* These dialogs used to build their descriptions by concatenation — an English
* `s` for the plural and a leading-space suffix for the unsaved-changes hint.
* Both are now catalog messages (`count` + `_one`/`_other`, and a joined
* sentence), so the shapes that would silently regress are pinned here:
*
* - the singular / plural task counts in each of the three step variants,
* - the space between the base sentence and the unsaved-changes hint,
* - the step name arriving as an interpolated value rather than baked into
* the message (steps are renamed freely, so it is user data).
*/
const STEP_NAME = "In Review";
const NO_WORKFLOWS: Workflow[] = [];
const NO_STEPS: WorkflowStep[] = [];
// Only `id` and `name` are read (the migration <Select>), but the fixture
// satisfies the whole contract so a field added to WorkflowStep fails here
// rather than being silently absent behind an `as` cast.
function step(id: string, name: string): WorkflowStep {
return {
id,
workflow_id: "workflow-1" as WorkflowStep["workflow_id"],
name,
position: 0,
color: "bg-slate-500",
created_at: "",
updated_at: "",
};
}
function renderWorkflowDelete(taskCount: number | null, hasUnsavedChanges = false) {
render(
<WorkflowDeleteDialog
open
onOpenChange={vi.fn()}
workflowTaskCount={taskCount}
otherWorkflows={NO_WORKFLOWS}
targetWorkflowId=""
setTargetWorkflowId={vi.fn()}
targetWorkflowSteps={NO_STEPS}
targetStepId=""
setTargetStepId={vi.fn()}
migrateLoading={false}
deleteLoading={false}
onDelete={vi.fn()}
onMigrateAndDelete={vi.fn()}
hasUnsavedChanges={hasUnsavedChanges}
/>,
);
}
function renderStepDelete(
stepTaskCount: number | null,
stepsForMigration: WorkflowStep[] = [],
hasUnsavedChanges = false,
) {
render(
<StepDeleteDialog
open
onOpenChange={vi.fn()}
stepName={STEP_NAME}
stepTaskCount={stepTaskCount}
stepsForMigration={stepsForMigration}
targetStep=""
setTargetStep={vi.fn()}
loading={false}
pending={false}
onMigrateAndDelete={vi.fn()}
onDeleteAndTasks={vi.fn()}
hasUnsavedChanges={hasUnsavedChanges}
/>,
);
}
const migrationTarget = [step("step-2", "Done")];
describe("WorkflowDeleteDialog description", () => {
afterEach(cleanup);
it("uses the singular task form for exactly one task", () => {
renderWorkflowDelete(1);
expect(
screen.getByText(
"This workflow has 1 task. Choose where to migrate them, or delete the workflow and archive the tasks.",
),
).toBeTruthy();
});
it("uses the plural task form for more than one task", () => {
renderWorkflowDelete(3);
expect(
screen.getByText(
"This workflow has 3 tasks. Choose where to migrate them, or delete the workflow and archive the tasks.",
),
).toBeTruthy();
});
it("falls back to the no-tasks sentence and appends the unsaved-changes hint with a space", () => {
renderWorkflowDelete(0, true);
expect(
screen.getByText(
"This will permanently delete the workflow and all its steps. Unsaved workflow changes will be discarded.",
),
).toBeTruthy();
});
});
describe("StepDeleteDialog description", () => {
afterEach(cleanup);
it("names the step without tasks", () => {
renderStepDelete(0);
expect(
screen.getByText(`This will permanently delete the ${STEP_NAME} workflow step.`),
).toBeTruthy();
});
it("inflects the migration sentence on the task count", () => {
renderStepDelete(1, migrationTarget);
expect(
screen.getByText(
`${STEP_NAME} has 1 task. Choose where to migrate them, or delete the step and its tasks.`,
),
).toBeTruthy();
cleanup();
renderStepDelete(2, migrationTarget);
expect(
screen.getByText(
`${STEP_NAME} has 2 tasks. Choose where to migrate them, or delete the step and its tasks.`,
),
).toBeTruthy();
});
it("inflects the no-migration-target sentence on the task count", () => {
renderStepDelete(1);
expect(screen.getByText(`Deleting ${STEP_NAME} will also affect its 1 task.`)).toBeTruthy();
cleanup();
renderStepDelete(4);
expect(screen.getByText(`Deleting ${STEP_NAME} will also affect its 4 tasks.`)).toBeTruthy();
});
it("appends the unsaved-step hint as a separate sentence", () => {
renderStepDelete(2, migrationTarget, true);
expect(
screen.getByText(
`${STEP_NAME} has 2 tasks. Choose where to migrate them, or delete the step and its tasks. Unsaved step changes will be discarded.`,
),
).toBeTruthy();
});
});