Files
kandev/apps/web/components/task/recent-task-switcher-model.test.ts
João Salavisa 5257dfbaab fix(web): close the i18n coverage gaps in the non-JSX scanner and locale catalogs (#2727)
Co-authored-by: Kandev Agent <agent@kandev.dev>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-16 23:03:29 +00:00

253 lines
8.1 KiB
TypeScript

import { afterEach, describe, expect, it } from "vitest";
import { i18n, t } from "@/lib/i18n";
import type { RecentTaskEntry } from "@/lib/recent-tasks";
import {
repositoryId as toRepositoryId,
sessionId as toSessionId,
taskId as toTaskId,
workspaceId as toWorkspaceId,
type Repository,
type TaskSession,
} from "@/lib/types/http";
import {
buildRecentTaskDisplayItems,
buildRecentTaskEntry,
getInitialReverseSelectionIndex,
getInitialSelectionIndex,
getNextSelectionIndex,
getPreviousSelectionIndex,
getTaskStatusBadge,
type RecentTaskBuildContext,
} from "./recent-task-switcher-model";
const visitedAt = "2026-05-02T10:00:00Z";
const WORKSPACE_ID = "workspace-1";
const REPOSITORY_ID = "repo-1";
const CURRENT_TASK_ID = "task-current";
const PREVIOUS_TASK_ID = "task-previous";
const WF_MAIN = "wf-1";
const WF_REVIEW = "wf-2";
function recent(taskId: string, title: string): RecentTaskEntry {
return {
taskId,
title,
visitedAt,
taskState: "TODO",
sessionState: null,
repositoryPath: null,
workflowId: WF_MAIN,
workflowName: "Stored Workflow",
workflowStepTitle: "Stored Step",
workspaceId: WORKSPACE_ID,
};
}
function session(taskIdStr: string, state: TaskSession["state"]): TaskSession {
return {
id: toSessionId(`${taskIdStr}-session`),
task_id: toTaskId(taskIdStr),
state,
started_at: visitedAt,
updated_at: visitedAt,
is_primary: true,
};
}
function buildContext(): RecentTaskBuildContext {
const repository: Repository = {
id: toRepositoryId(REPOSITORY_ID),
workspace_id: toWorkspaceId(WORKSPACE_ID),
name: "Local Repo",
source_type: "local",
local_path: "/tmp/local-repo",
provider: "github",
provider_repo_id: "repo-gh-id",
provider_owner: "kdlbs",
provider_name: "kandev",
default_branch: "main",
worktree_branch_prefix: "task",
pull_before_worktree: false,
setup_script: "",
cleanup_script: "",
dev_script: "",
copy_files: "",
created_at: visitedAt,
updated_at: visitedAt,
};
return {
activeTaskId: CURRENT_TASK_ID,
kanbanWorkflowId: WF_MAIN,
kanbanTasks: [
{
id: CURRENT_TASK_ID,
workflowId: WF_MAIN,
workflowStepId: "step-1",
title: "Current live title",
position: 0,
state: "IN_PROGRESS",
repositoryId: REPOSITORY_ID,
primarySessionId: `${CURRENT_TASK_ID}-session`,
createdAt: visitedAt,
updatedAt: visitedAt,
},
],
kanbanSteps: [{ id: "step-1", title: "Working", color: "bg-blue-500", position: 0 }],
snapshots: {
[WF_REVIEW]: {
workflowId: WF_REVIEW,
workflowName: "Review Flow",
steps: [{ id: "step-2", title: "Review", color: "bg-green-500", position: 0 }],
tasks: [
{
id: PREVIOUS_TASK_ID,
workflowId: WF_REVIEW,
workflowStepId: "step-2",
title: "Previous live title",
position: 0,
state: "REVIEW",
repositoryId: REPOSITORY_ID,
createdAt: visitedAt,
updatedAt: visitedAt,
},
],
},
},
workflows: [
{ id: WF_MAIN, workspaceId: WORKSPACE_ID, name: "Main Flow" },
{ id: WF_REVIEW, workspaceId: WORKSPACE_ID, name: "Review Flow" },
],
repositoriesByWorkspace: { [WORKSPACE_ID]: [repository] },
sessionsByTaskId: { [CURRENT_TASK_ID]: [session(CURRENT_TASK_ID, "RUNNING")] },
gitStatusByEnvId: {},
environmentIdBySessionId: {},
};
}
afterEach(async () => {
await i18n.changeLanguage("en");
});
describe("recent task switcher model", () => {
it("starts selection on the first non-current task when possible", () => {
const items = [
{ taskId: CURRENT_TASK_ID },
{ taskId: PREVIOUS_TASK_ID },
{ taskId: "task-older" },
];
expect(getInitialSelectionIndex(items, CURRENT_TASK_ID)).toBe(1);
expect(getInitialSelectionIndex(items, "task-missing")).toBe(0);
expect(getInitialSelectionIndex([], CURRENT_TASK_ID)).toBe(-1);
});
it("starts reverse selection on the last non-current task", () => {
const items = [
{ taskId: CURRENT_TASK_ID },
{ taskId: PREVIOUS_TASK_ID },
{ taskId: "task-older" },
];
expect(getInitialReverseSelectionIndex(items, CURRENT_TASK_ID)).toBe(2);
// When the current task is the last item, skip back to the previous one.
expect(
getInitialReverseSelectionIndex(
[{ taskId: PREVIOUS_TASK_ID }, { taskId: CURRENT_TASK_ID }],
CURRENT_TASK_ID,
),
).toBe(0);
// All items are current -> fall back to the last index.
expect(getInitialReverseSelectionIndex([{ taskId: CURRENT_TASK_ID }], CURRENT_TASK_ID)).toBe(0);
expect(getInitialReverseSelectionIndex([], CURRENT_TASK_ID)).toBe(-1);
});
it("cycles through items and handles empty lists", () => {
expect(getNextSelectionIndex(0, 3)).toBe(1);
expect(getNextSelectionIndex(2, 3)).toBe(0);
expect(getNextSelectionIndex(-1, 3)).toBe(0);
expect(getNextSelectionIndex(0, 0)).toBe(-1);
});
it("cycles backward through items and handles empty lists", () => {
expect(getPreviousSelectionIndex(1, 3)).toBe(0);
expect(getPreviousSelectionIndex(0, 3)).toBe(2);
expect(getPreviousSelectionIndex(-1, 3)).toBe(2);
expect(getPreviousSelectionIndex(0, 0)).toBe(-1);
});
it("maps task and session states to compact status badges", () => {
expect(getTaskStatusBadge("IN_PROGRESS", "RUNNING")).toMatchObject({
labelKey: "task:sessionStatusRunning",
});
expect(getTaskStatusBadge("REVIEW", "WAITING_FOR_INPUT")).toMatchObject({
labelKey: "task:sessionStatusTurnFinished",
});
expect(getTaskStatusBadge("TODO", undefined)).toMatchObject({ labelKey: "task:statusTodo" });
expect(getTaskStatusBadge("FAILED", "FAILED")).toMatchObject({
labelKey: "task:sessionStatusFailed",
variant: "destructive",
});
});
it("builds display items from live task metadata with stored fallback", () => {
const display = buildRecentTaskDisplayItems(
[recent(CURRENT_TASK_ID, "Stored current"), recent(PREVIOUS_TASK_ID, "Stored previous")],
buildContext(),
);
expect(display[0]).toMatchObject({
taskId: CURRENT_TASK_ID,
title: "Current live title",
isCurrent: true,
repositoryPath: "kdlbs/kandev",
workflowName: "Main Flow",
workflowStepTitle: "Working",
});
expect(display[0]?.statusBadge.labelKey).toBe("task:sessionStatusRunning");
expect(display[1]).toMatchObject({
taskId: PREVIOUS_TASK_ID,
title: "Previous live title",
isCurrent: false,
repositoryPath: "kdlbs/kandev",
workflowName: "Review Flow",
workflowStepTitle: "Review",
});
});
it("builds a recent entry for the active task while preserving prior visit time", () => {
const previous = recent(CURRENT_TASK_ID, "Old title");
const entry = buildRecentTaskEntry(CURRENT_TASK_ID, buildContext(), previous);
expect(entry).toMatchObject({
taskId: CURRENT_TASK_ID,
title: "Current live title",
visitedAt,
taskState: "IN_PROGRESS",
sessionState: "RUNNING",
repositoryPath: "kdlbs/kandev",
workflowName: "Main Flow",
workflowStepTitle: "Working",
workspaceId: WORKSPACE_ID,
});
});
});
describe("status badge localization", () => {
it("emits catalog keys that resolve to the badge copy", () => {
expect(t(getTaskStatusBadge("IN_PROGRESS", "RUNNING").labelKey)).toBe("Running");
expect(t(getTaskStatusBadge("REVIEW", "WAITING_FOR_INPUT").labelKey)).toBe("Turn Finished");
expect(t(getTaskStatusBadge("TODO", undefined).labelKey)).toBe("Todo");
expect(t(getTaskStatusBadge(undefined, undefined).labelKey)).toBe("Backlog");
});
it("keeps badge copy out of module scope, so a locale switch reaches it", async () => {
await i18n.changeLanguage("pseudo");
// The table is built at import time. Resolving `t()` there would have
// frozen these at "en" and this would still read "Running".
expect(t(getTaskStatusBadge("IN_PROGRESS", "RUNNING").labelKey)).toMatch(/[^\p{ASCII}]/u);
});
});