@taskforcehq/taskforce 0.3.303 → 0.3.305
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +140 -247
- package/dist/Taskforce.module.css +102 -5
- package/dist/TaskforceCore.js +635 -238
- package/dist/TaskforceCore.test.js +2243 -495
- package/dist/annotatedAttachments/service.d.ts +1 -1
- package/dist/annotatedAttachments/types.d.ts +1 -1
- package/dist/compat/workspaceSyncCompat.js +6 -0
- package/dist/components/features/AgentsModule.js +1 -1
- package/dist/components/features/AgentsModule.test.js +1 -1
- package/dist/components/features/DocumentIndex.d.ts +1 -1
- package/dist/components/features/DocumentIndex.js +1 -1
- package/dist/components/features/DocumentViewer.d.ts +1 -1
- package/dist/components/features/DocumentViewer.js +32 -9
- package/dist/components/features/DocumentWorkspace.d.ts +5 -22
- package/dist/components/features/DocumentWorkspace.js +12 -155
- package/dist/components/features/DocumentWorkspace.test.js +193 -6
- package/dist/components/features/TaskSettings.js +87 -41
- package/dist/components/features/TaskSettings.test.js +180 -40
- package/dist/components/features/documentWorkspaceModel.d.ts +24 -0
- package/dist/components/features/documentWorkspaceModel.js +57 -0
- package/dist/components/features/useDocumentWorkspaceController.d.ts +34 -0
- package/dist/components/features/useDocumentWorkspaceController.js +113 -0
- package/dist/components/task/TaskActionHeader.d.ts +2 -1
- package/dist/components/task/TaskActionHeader.js +3 -3
- package/dist/components/task/TaskActionHeader.test.js +8 -0
- package/dist/components/task/TaskCard.js +16 -1
- package/dist/components/task/TaskCard.test.js +31 -0
- package/dist/components/task/TaskForm.d.ts +9 -8
- package/dist/components/task/TaskForm.js +374 -219
- package/dist/components/task/TaskForm.test.js +482 -361
- package/dist/components/task/TaskKanban.d.ts +1 -1
- package/dist/components/task/TaskKanban.js +33 -161
- package/dist/components/task/TaskStatusActions.d.ts +6 -1
- package/dist/components/task/TaskStatusActions.js +8 -3
- package/dist/components/task/TaskStatusActions.test.js +9 -0
- package/dist/components/task/taskKanbanDropRules.d.ts +58 -0
- package/dist/components/task/taskKanbanDropRules.js +187 -0
- package/dist/components/task/taskKanbanDropRules.test.d.ts +1 -0
- package/dist/components/task/taskKanbanDropRules.test.js +114 -0
- package/dist/components/ui/Modal.d.ts +1 -1
- package/dist/components/ui/Modal.js +1 -0
- package/dist/components/views/AppShellHeader.js +5 -1
- package/dist/components/views/AppShellHeader.test.js +4 -0
- package/dist/components/views/PlanComparisonPage.d.ts +3 -2
- package/dist/components/views/PlanComparisonPage.js +9 -10
- package/dist/components/views/PlanComparisonPage.test.js +55 -41
- package/dist/components/views/PlansPage.d.ts +5 -1
- package/dist/components/views/PlansPage.js +150 -81
- package/dist/components/views/PlansPage.test.js +367 -41
- package/dist/components/views/StandaloneLayout.js +135 -54
- package/dist/components/views/WidgetView.js +11 -2
- package/dist/components/views/panels/FilterToolbar.test.js +6 -4
- package/dist/components/views/standalone/modals/AccountHubModal.d.ts +2 -14
- package/dist/components/views/standalone/modals/AccountHubModal.js +12 -14
- package/dist/components/views/standalone/modals/AccountHubModal.test.js +24 -1
- package/dist/components/views/standalone/modals/SyncStatusModal.js +1 -1
- package/dist/config/envSchema.js +1 -1
- package/dist/core/AiProfileService.d.ts +6 -1
- package/dist/core/AiProfileService.js +161 -16
- package/dist/core/AiProfileService.test.js +3 -1
- package/dist/core/AiProfiles.test.js +200 -0
- package/dist/core/AuthTokenService.test.d.ts +1 -0
- package/dist/core/AuthTokenService.test.js +78 -0
- package/dist/core/DeletedTaskLifecycleService.d.ts +2 -1
- package/dist/core/EntitlementsPolicy.test.js +75 -13
- package/dist/core/PlanEntitlementService.d.ts +9 -1
- package/dist/core/PlanEntitlementService.js +144 -19
- package/dist/core/PlanVersionPolicy.test.js +19 -26
- package/dist/core/SignupMonetizationSettings.test.js +75 -15
- package/dist/core/SystemAdmin.test.js +212 -56
- package/dist/core/TaskActivityService.d.ts +4 -1
- package/dist/core/TaskActivityService.js +45 -0
- package/dist/core/TaskAuditTimeline.test.js +52 -1
- package/dist/core/TaskTaxonomyValidation.test.js +53 -0
- package/dist/core/Taskforce.d.ts +18 -11
- package/dist/core/Taskforce.js +93 -12
- package/dist/core/WorkspacePlanMode.test.js +73 -8
- package/dist/core/shared.d.ts +2 -0
- package/dist/core/shared.js +11 -0
- package/dist/documentReviews/service.d.ts +1 -1
- package/dist/documentReviews/types.d.ts +1 -1
- package/dist/hooks/auth/useTaskforceAuthBootstrap.d.ts +48 -0
- package/dist/hooks/auth/useTaskforceAuthBootstrap.js +219 -0
- package/dist/hooks/sync/auth.d.ts +37 -0
- package/dist/hooks/sync/auth.js +63 -0
- package/dist/hooks/sync/bootstrap.d.ts +34 -0
- package/dist/hooks/sync/bootstrap.js +52 -0
- package/dist/hooks/sync/orchestratorShared.d.ts +36 -0
- package/dist/hooks/sync/orchestratorShared.js +209 -0
- package/dist/hooks/sync/orchestratorShared.test.d.ts +1 -0
- package/dist/hooks/sync/orchestratorShared.test.js +49 -0
- package/dist/hooks/sync/realtime.d.ts +25 -0
- package/dist/hooks/sync/realtime.js +60 -0
- package/dist/hooks/sync/recovery.d.ts +69 -0
- package/dist/hooks/sync/recovery.js +118 -0
- package/dist/hooks/sync/transfers.d.ts +123 -0
- package/dist/hooks/sync/transfers.js +435 -0
- package/dist/hooks/sync/useDataVersionRefresh.d.ts +16 -0
- package/dist/hooks/sync/useDataVersionRefresh.js +73 -0
- package/dist/hooks/ui/useResourceExport.d.ts +3 -1
- package/dist/hooks/ui/useResourceExport.js +5 -2
- package/dist/hooks/ui/useWorkflowTemplates.js +11 -6
- package/dist/hooks/useRealtimeSync.js +2 -1
- package/dist/hooks/useSyncOrchestrator.aiProfiles.test.js +8 -0
- package/dist/hooks/useSyncOrchestrator.context-assets.test.d.ts +1 -0
- package/dist/hooks/useSyncOrchestrator.context-assets.test.js +228 -0
- package/dist/hooks/useSyncOrchestrator.d.ts +12 -8
- package/dist/hooks/useSyncOrchestrator.js +388 -879
- package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +641 -6
- package/dist/hooks/useTaskMutations.d.ts +2 -1
- package/dist/hooks/useTaskMutations.js +57 -7
- package/dist/hooks/useTaskMutations.test.js +176 -0
- package/dist/hooks/useTaskforce.d.ts +13 -4
- package/dist/hooks/useTaskforce.js +302 -817
- package/dist/hooks/useTaskforce.sync-behavior.test.js +78 -0
- package/dist/hooks/useWorkspaceSyncController.d.ts +4 -1
- package/dist/hooks/useWorkspaceSyncController.js +33 -11
- package/dist/hooks/useWorkspaceSyncController.test.js +1 -0
- package/dist/hooks/workspace/useTaskforceApiRouting.test.js +17 -0
- package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.d.ts +166 -0
- package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.js +434 -0
- package/dist/hooks/workspace/workspaceLocalState.d.ts +19 -0
- package/dist/hooks/workspace/workspaceLocalState.js +100 -0
- package/dist/hooks/workspace/workspaceLocalState.test.d.ts +1 -0
- package/dist/hooks/workspace/workspaceLocalState.test.js +59 -0
- package/dist/localization/locales/en-US.js +2 -2
- package/dist/mcp/adminWorkspaceRegistrar.d.ts +2 -0
- package/dist/mcp/adminWorkspaceRegistrar.js +154 -0
- package/dist/mcp/canonicalAssetHelpers.d.ts +70 -0
- package/dist/mcp/canonicalAssetHelpers.js +259 -0
- package/dist/mcp/clientIntegrations.d.ts +12 -1
- package/dist/mcp/clientIntegrations.js +161 -6
- package/dist/mcp/clientIntegrations.test.js +76 -4
- package/dist/mcp/collaborationRegistrar.d.ts +2 -0
- package/dist/mcp/collaborationRegistrar.js +71 -0
- package/dist/mcp/documentAssetRegistrar.d.ts +2 -0
- package/dist/mcp/documentAssetRegistrar.js +346 -0
- package/dist/mcp/documentHelpers.d.ts +100 -0
- package/dist/mcp/documentHelpers.js +161 -0
- package/dist/mcp/runtime.js +2455 -3925
- package/dist/mcp/runtime.test.js +240 -8
- package/dist/mcp/taskAttachmentHelpers.d.ts +41 -0
- package/dist/mcp/taskAttachmentHelpers.js +60 -0
- package/dist/mcp/taskAttachmentTypes.d.ts +37 -0
- package/dist/mcp/taskAttachmentTypes.js +1 -0
- package/dist/mcp/taskPlanningRegistrar.d.ts +2 -0
- package/dist/mcp/taskPlanningRegistrar.js +418 -0
- package/dist/mcp/toolCatalog.d.ts +35 -0
- package/dist/mcp/toolCatalog.js +3 -0
- package/dist/plugins/vite.js +1 -1
- package/dist/runtime/appGateDefinitions.d.ts +1 -1
- package/dist/runtime/appGateDefinitions.js +1 -1
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.js +46 -3
- package/dist/server/index.test.js +12 -1
- package/dist/server/routes/admin.d.ts +1 -5
- package/dist/server/routes/admin.js +151 -80
- package/dist/server/routes/annotatedAttachments.d.ts +1 -1
- package/dist/server/routes/annotatedAttachments.js +1 -1
- package/dist/server/routes/auth.d.ts +1 -4
- package/dist/server/routes/auth.js +59 -64
- package/dist/server/routes/authSupport.d.ts +30 -0
- package/dist/server/routes/authSupport.js +81 -0
- package/dist/server/routes/billing.d.ts +1 -1
- package/dist/server/routes/billing.js +122 -255
- package/dist/server/routes/billing.test.js +257 -86
- package/dist/server/routes/documentReviews.d.ts +1 -1
- package/dist/server/routes/documentReviews.js +1 -1
- package/dist/server/routes/documents.d.ts +4 -2
- package/dist/server/routes/documents.js +73 -7
- package/dist/server/routes/primitives.d.ts +11 -0
- package/dist/server/routes/primitives.js +73 -0
- package/dist/server/routes/resources.d.ts +1 -1
- package/dist/server/routes/resources.js +1 -1
- package/dist/server/routes/shared.d.ts +31 -1
- package/dist/server/routes/shared.js +26 -0
- package/dist/server/routes/sync.d.ts +1 -1
- package/dist/server/routes/sync.integration.test.js +20 -0
- package/dist/server/routes/sync.js +111 -238
- package/dist/server/routes/tasks.d.ts +1 -1
- package/dist/server/routes/tasks.js +50 -8
- package/dist/server/routes/workspaces.d.ts +1 -1
- package/dist/server/routes/workspaces.js +1 -1
- package/dist/server/routes.d.ts +2 -9
- package/dist/server/routes.js +84 -181
- package/dist/server/routes.test.js +912 -32
- package/dist/shared/taskActor.d.ts +10 -0
- package/dist/shared/taskActor.js +1 -0
- package/dist/sync/collaborationSyncPayload.js +4 -0
- package/dist/sync/collaborationSyncPayload.test.js +8 -0
- package/dist/sync/collaborationSyncState.d.ts +3 -1
- package/dist/sync/collaborationSyncState.js +19 -0
- package/dist/sync/contentSyncState.js +28 -17
- package/dist/sync/syncApplyHandlers.d.ts +87 -1
- package/dist/sync/syncApplyHandlers.js +370 -8
- package/dist/sync/syncApplyHandlers.test.d.ts +1 -0
- package/dist/sync/syncApplyHandlers.test.js +379 -0
- package/dist/sync/syncService.d.ts +9 -1
- package/dist/sync/syncService.js +44 -2
- package/dist/sync/syncService.test.js +62 -0
- package/dist/sync/workspacePullFeed.d.ts +1 -0
- package/dist/sync/workspacePullFeed.js +66 -59
- package/dist/sync/workspacePullFeed.test.js +72 -4
- package/dist/sync/workspaceSyncModel.d.ts +25 -1
- package/dist/sync/workspaceSyncModel.js +77 -3
- package/dist/sync/workspaceSyncModel.test.js +153 -0
- package/dist/sync/workspaceSyncState.d.ts +2 -0
- package/dist/sync/workspaceSyncState.js +1 -0
- package/dist/types.d.ts +2 -10
- package/dist/ui/assets/AgentsModule-N2MnQBZk.js +1 -0
- package/dist/ui/assets/{AnnotatedAttachmentWorkspace-C_TmXZwA.js → AnnotatedAttachmentWorkspace-BG6P_GVW.js} +1 -1
- package/dist/ui/assets/DocumentWorkspace-B3nV_Gc5.css +1 -0
- package/dist/ui/assets/DocumentWorkspace-DRbDMwh8.js +252 -0
- package/dist/ui/assets/{InitiativesModule-4-Rh2K2n.js → InitiativesModule-Dhm7M8e7.js} +1 -1
- package/dist/ui/assets/{PlansPage-BlVC_lRq.css → PlansPage-C2dd2n1X.css} +1 -1
- package/dist/ui/assets/PlansPage-Cq0zXj3I.js +1 -0
- package/dist/ui/assets/TaskSettings-ln0aF7xc.js +9 -0
- package/dist/ui/assets/{WorkflowsModule-CGk9s3NJ.js → WorkflowsModule-BcS4afRn.js} +1 -1
- package/dist/ui/assets/index-C2-OXZV7.css +1 -0
- package/dist/ui/assets/index-DcSI5F86.js +6 -0
- package/dist/ui/assets/{vendor-icons-pWocEipT.js → vendor-icons-BSUaRwF8.js} +1 -1
- package/dist/ui/index.html +4 -4
- package/dist/utils/commercialLifecyclePresentation.d.ts +19 -0
- package/dist/utils/commercialLifecyclePresentation.js +196 -0
- package/dist/utils/taskActivity.js +4 -0
- package/dist/utils/taskActivity.test.js +25 -1
- package/package.json +10 -1
- package/scripts/check-cloud-mcp.mjs +83 -0
- package/scripts/playwright-auth.sh +9 -1
- package/scripts/run-billing-performance-smoke.sh +24 -0
- package/dist/ui/assets/AgentsModule-CpsTmrIW.js +0 -1
- package/dist/ui/assets/DocumentWorkspace-C3GyzrWm.js +0 -250
- package/dist/ui/assets/DocumentWorkspace-C_8T8oz-.css +0 -1
- package/dist/ui/assets/PlansPage-BP7AYOqr.js +0 -1
- package/dist/ui/assets/TaskSettings-BOC5F6Ag.js +0 -8
- package/dist/ui/assets/index-CLIMgR6g.js +0 -6
- package/dist/ui/assets/index-DneETxcu.css +0 -1
|
@@ -1,26 +1,26 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { render, screen, fireEvent } from
|
|
3
|
-
import userEvent from
|
|
4
|
-
import { describe, it, expect, vi } from
|
|
5
|
-
import { TaskForm } from
|
|
6
|
-
describe(
|
|
2
|
+
import { render, screen, fireEvent } from "@testing-library/react";
|
|
3
|
+
import userEvent from "@testing-library/user-event";
|
|
4
|
+
import { describe, it, expect, vi } from "vitest";
|
|
5
|
+
import { TaskForm } from "./TaskForm";
|
|
6
|
+
describe("TaskForm Component", () => {
|
|
7
7
|
const defaultProps = {
|
|
8
8
|
editingTaskId: null,
|
|
9
|
-
error:
|
|
10
|
-
title:
|
|
11
|
-
description:
|
|
12
|
-
category:
|
|
13
|
-
type:
|
|
9
|
+
error: "",
|
|
10
|
+
title: "",
|
|
11
|
+
description: "",
|
|
12
|
+
category: "general",
|
|
13
|
+
type: "feature",
|
|
14
14
|
priority: 2,
|
|
15
15
|
complexity: 3,
|
|
16
|
-
approach:
|
|
17
|
-
assignee:
|
|
18
|
-
scheduledDate:
|
|
19
|
-
dueDate:
|
|
20
|
-
workstreamInput:
|
|
16
|
+
approach: "",
|
|
17
|
+
assignee: "unassigned",
|
|
18
|
+
scheduledDate: "",
|
|
19
|
+
dueDate: "",
|
|
20
|
+
workstreamInput: "",
|
|
21
21
|
checklistItems: [],
|
|
22
22
|
comments: [],
|
|
23
|
-
newCommentText:
|
|
23
|
+
newCommentText: "",
|
|
24
24
|
contextFiles: [],
|
|
25
25
|
descriptionFocused: false,
|
|
26
26
|
showMarkdownHelp: false,
|
|
@@ -32,31 +32,59 @@ describe('TaskForm Component', () => {
|
|
|
32
32
|
taxonomies: [],
|
|
33
33
|
approaches: [],
|
|
34
34
|
assigneeOptions: [
|
|
35
|
-
{
|
|
36
|
-
|
|
35
|
+
{
|
|
36
|
+
value: "unassigned",
|
|
37
|
+
label: "Unassigned",
|
|
38
|
+
icon: "HelpCircle",
|
|
39
|
+
color: "var(--text-secondary)",
|
|
40
|
+
kind: "unassigned",
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
value: "member-1",
|
|
44
|
+
label: "Alex Example",
|
|
45
|
+
icon: "User",
|
|
46
|
+
color: "#22c55e",
|
|
47
|
+
kind: "member",
|
|
48
|
+
},
|
|
37
49
|
],
|
|
38
50
|
categories: [
|
|
39
|
-
{ value:
|
|
40
|
-
{ value:
|
|
51
|
+
{ value: "general", label: "General", disabled: false },
|
|
52
|
+
{ value: "backend", label: "Backend", disabled: false },
|
|
53
|
+
],
|
|
54
|
+
types: [
|
|
55
|
+
{ value: "feature", label: "Feature" },
|
|
56
|
+
{ value: "bug", label: "Bug" },
|
|
57
|
+
],
|
|
58
|
+
priorities: [
|
|
59
|
+
{ value: 3, label: "High" },
|
|
60
|
+
{ value: 2, label: "Medium" },
|
|
41
61
|
],
|
|
42
|
-
types: [{ value: 'feature', label: 'Feature' }, { value: 'bug', label: 'Bug' }],
|
|
43
|
-
priorities: [{ value: 3, label: 'High' }, { value: 2, label: 'Medium' }],
|
|
44
62
|
workstreams: [
|
|
45
63
|
{
|
|
46
|
-
id:
|
|
64
|
+
id: "workstream-1",
|
|
47
65
|
referenceNumber: 12,
|
|
48
|
-
referenceLabel:
|
|
49
|
-
title:
|
|
66
|
+
referenceLabel: "WS-12",
|
|
67
|
+
title: "Platform Refinement",
|
|
50
68
|
description: null,
|
|
51
|
-
createdAt:
|
|
69
|
+
createdAt: "2026-04-03T00:00:00.000Z",
|
|
52
70
|
},
|
|
53
71
|
{
|
|
54
|
-
id:
|
|
72
|
+
id: "workstream-2",
|
|
55
73
|
referenceNumber: 44,
|
|
56
|
-
referenceLabel:
|
|
57
|
-
title:
|
|
74
|
+
referenceLabel: "WS-44",
|
|
75
|
+
title: "Growth Experiments",
|
|
58
76
|
description: null,
|
|
59
|
-
createdAt:
|
|
77
|
+
createdAt: "2026-04-03T00:00:00.000Z",
|
|
78
|
+
},
|
|
79
|
+
],
|
|
80
|
+
initiatives: [
|
|
81
|
+
{
|
|
82
|
+
id: "initiative-1",
|
|
83
|
+
referenceNumber: 3,
|
|
84
|
+
referenceLabel: "IN-3",
|
|
85
|
+
title: "Execution model",
|
|
86
|
+
description: null,
|
|
87
|
+
createdAt: "2026-04-03T00:00:00.000Z",
|
|
60
88
|
},
|
|
61
89
|
],
|
|
62
90
|
copiedId: null,
|
|
@@ -80,7 +108,7 @@ describe('TaskForm Component', () => {
|
|
|
80
108
|
onShowCategoryManagerChange: vi.fn(),
|
|
81
109
|
onShowTypeManagerChange: vi.fn(),
|
|
82
110
|
onConfiguringCategoryChange: vi.fn(),
|
|
83
|
-
onSubmit: vi.fn(e => e.preventDefault()),
|
|
111
|
+
onSubmit: vi.fn((e) => e.preventDefault()),
|
|
84
112
|
onAddComment: vi.fn(),
|
|
85
113
|
onAddContextFile: vi.fn(),
|
|
86
114
|
onRemoveContextFile: vi.fn(),
|
|
@@ -100,295 +128,325 @@ describe('TaskForm Component', () => {
|
|
|
100
128
|
return render(_jsx(TaskForm, { ...defaultProps, ...props }));
|
|
101
129
|
};
|
|
102
130
|
// --- Create Task Tests ---
|
|
103
|
-
it(
|
|
131
|
+
it("3.1 Open add form - Renders correctly", () => {
|
|
104
132
|
renderForm();
|
|
105
|
-
expect(screen.getByPlaceholderText(
|
|
106
|
-
expect(screen.getByTitle(
|
|
133
|
+
expect(screen.getByPlaceholderText("What needs to be done?")).toBeInTheDocument();
|
|
134
|
+
expect(screen.getByTitle("Markdown Help")).toBeInTheDocument();
|
|
107
135
|
});
|
|
108
|
-
it(
|
|
109
|
-
renderForm({ title:
|
|
136
|
+
it("3.2 Title required - Submit disabled if empty", () => {
|
|
137
|
+
renderForm({ title: "" });
|
|
110
138
|
// In the implementation, the form submit handles validation, but let's check input attribute
|
|
111
|
-
const input = screen.getByPlaceholderText(
|
|
139
|
+
const input = screen.getByPlaceholderText("What needs to be done?");
|
|
112
140
|
expect(input).toBeRequired();
|
|
113
141
|
});
|
|
114
|
-
it(
|
|
142
|
+
it("3.3 Create minimal - Title input updates", async () => {
|
|
115
143
|
const user = userEvent.setup();
|
|
116
144
|
renderForm();
|
|
117
|
-
const input = screen.getByPlaceholderText(
|
|
118
|
-
await user.type(input,
|
|
145
|
+
const input = screen.getByPlaceholderText("What needs to be done?");
|
|
146
|
+
await user.type(input, "New Task");
|
|
119
147
|
expect(defaultProps.onTitleChange).toHaveBeenCalled();
|
|
120
148
|
});
|
|
121
|
-
it(
|
|
149
|
+
it("3.4 Create with all fields - Description updates", async () => {
|
|
122
150
|
const user = userEvent.setup();
|
|
123
151
|
renderForm({ descriptionFocused: true }); // Ensure textarea is rendered
|
|
124
|
-
const textarea = screen.getByPlaceholderText(
|
|
125
|
-
await user.type(textarea,
|
|
152
|
+
const textarea = screen.getByPlaceholderText("What needs to be done? (Markdown supported)");
|
|
153
|
+
await user.type(textarea, "Details");
|
|
126
154
|
expect(defaultProps.onDescriptionChange).toHaveBeenCalled();
|
|
127
155
|
});
|
|
128
|
-
it(
|
|
156
|
+
it("renders actual newline paragraphs in description preview without escaped text", () => {
|
|
129
157
|
const { container } = renderForm({
|
|
130
|
-
description:
|
|
131
|
-
descriptionFocused: false
|
|
158
|
+
description: "Phase 1\n\nPhase 2",
|
|
159
|
+
descriptionFocused: false,
|
|
132
160
|
});
|
|
133
|
-
expect(screen.getByText(
|
|
134
|
-
expect(screen.getByText(
|
|
135
|
-
expect(container.textContent).not.toContain(
|
|
161
|
+
expect(screen.getByText("Phase 1")).toBeInTheDocument();
|
|
162
|
+
expect(screen.getByText("Phase 2")).toBeInTheDocument();
|
|
163
|
+
expect(container.textContent).not.toContain("\\n\\n");
|
|
136
164
|
});
|
|
137
|
-
it(
|
|
165
|
+
it("3.5 Category selection", async () => {
|
|
138
166
|
const user = userEvent.setup();
|
|
139
167
|
renderForm();
|
|
140
|
-
const dropdown = screen.getByRole(
|
|
168
|
+
const dropdown = screen.getByRole("combobox", { name: /category/i });
|
|
141
169
|
await user.click(dropdown);
|
|
142
|
-
const option = screen.getByText(
|
|
170
|
+
const option = screen.getByText("Backend");
|
|
143
171
|
await user.click(option);
|
|
144
|
-
expect(defaultProps.onCategoryChange).toHaveBeenCalledWith(
|
|
172
|
+
expect(defaultProps.onCategoryChange).toHaveBeenCalledWith("backend");
|
|
145
173
|
});
|
|
146
|
-
it(
|
|
174
|
+
it("keeps a legacy category selectable when the current task already uses it", async () => {
|
|
147
175
|
const user = userEvent.setup();
|
|
148
176
|
renderForm({
|
|
149
|
-
category:
|
|
177
|
+
category: "backend",
|
|
150
178
|
categories: [
|
|
151
|
-
{ value:
|
|
152
|
-
{ value:
|
|
153
|
-
]
|
|
179
|
+
{ value: "default", label: "General", disabled: false },
|
|
180
|
+
{ value: "backend", label: "Backend", disabled: true },
|
|
181
|
+
],
|
|
154
182
|
});
|
|
155
|
-
const dropdown = screen.getByRole(
|
|
183
|
+
const dropdown = screen.getByRole("combobox", { name: /category/i });
|
|
156
184
|
await user.click(dropdown);
|
|
157
|
-
expect(screen.getAllByText(
|
|
185
|
+
expect(screen.getAllByText("Backend (Legacy)").length).toBeGreaterThan(0);
|
|
158
186
|
});
|
|
159
|
-
it(
|
|
187
|
+
it("uses the configured default category label in the category picker", async () => {
|
|
160
188
|
const user = userEvent.setup();
|
|
161
189
|
renderForm({
|
|
162
190
|
categories: [
|
|
163
|
-
{
|
|
164
|
-
|
|
165
|
-
|
|
191
|
+
{
|
|
192
|
+
value: "default",
|
|
193
|
+
label: "Inbox Work",
|
|
194
|
+
icon: "Star",
|
|
195
|
+
color: "teal-500",
|
|
196
|
+
disabled: false,
|
|
197
|
+
},
|
|
198
|
+
{ value: "backend", label: "Backend", disabled: false },
|
|
199
|
+
],
|
|
166
200
|
});
|
|
167
|
-
const dropdown = screen.getByRole(
|
|
201
|
+
const dropdown = screen.getByRole("combobox", { name: /category/i });
|
|
168
202
|
await user.click(dropdown);
|
|
169
|
-
expect(screen.getByText(
|
|
170
|
-
expect(screen.queryByText(
|
|
203
|
+
expect(screen.getByText("Inbox Work")).toBeInTheDocument();
|
|
204
|
+
expect(screen.queryByText("General")).not.toBeInTheDocument();
|
|
171
205
|
});
|
|
172
|
-
it(
|
|
206
|
+
it("3.6 Type selection", async () => {
|
|
173
207
|
const user = userEvent.setup();
|
|
174
208
|
renderForm();
|
|
175
|
-
const dropdown = screen.getByRole(
|
|
209
|
+
const dropdown = screen.getByRole("combobox", { name: /type/i });
|
|
176
210
|
await user.click(dropdown);
|
|
177
|
-
const option = screen.getByText(
|
|
211
|
+
const option = screen.getByText("Bug");
|
|
178
212
|
await user.click(option);
|
|
179
|
-
expect(defaultProps.onTypeChange).toHaveBeenCalledWith(
|
|
213
|
+
expect(defaultProps.onTypeChange).toHaveBeenCalledWith("bug");
|
|
180
214
|
});
|
|
181
|
-
it(
|
|
215
|
+
it("keeps a retired type selectable when the current task already uses it", async () => {
|
|
182
216
|
const user = userEvent.setup();
|
|
183
217
|
renderForm({
|
|
184
|
-
type:
|
|
218
|
+
type: "issue",
|
|
185
219
|
types: [
|
|
186
|
-
{ value:
|
|
187
|
-
{ value:
|
|
188
|
-
]
|
|
220
|
+
{ value: "task", label: "Task", status: "active" },
|
|
221
|
+
{ value: "issue", label: "Issue", status: "retired" },
|
|
222
|
+
],
|
|
189
223
|
});
|
|
190
|
-
const dropdown = screen.getByRole(
|
|
224
|
+
const dropdown = screen.getByRole("combobox", { name: /type/i });
|
|
191
225
|
await user.click(dropdown);
|
|
192
|
-
expect(screen.getAllByText(
|
|
226
|
+
expect(screen.getAllByText("Issue (Retired)").length).toBeGreaterThan(0);
|
|
193
227
|
});
|
|
194
|
-
it(
|
|
228
|
+
it("3.7 Priority selection", () => {
|
|
195
229
|
renderForm();
|
|
196
230
|
// LevelSelector uses divs/buttons. Looking at LevelSelector implementation (implied):
|
|
197
231
|
// It renders options. Let's click one.
|
|
198
232
|
// Priority and Complexity both have 'High' options.
|
|
199
|
-
const priorityOption = screen.getAllByTitle(
|
|
233
|
+
const priorityOption = screen.getAllByTitle("High")[0];
|
|
200
234
|
fireEvent.click(priorityOption);
|
|
201
235
|
expect(defaultProps.onPriorityChange).toHaveBeenCalledWith(3);
|
|
202
236
|
});
|
|
203
|
-
it(
|
|
237
|
+
it("renders workspace member assignee options", async () => {
|
|
204
238
|
const user = userEvent.setup();
|
|
205
239
|
renderForm();
|
|
206
|
-
const dropdown = screen.getByRole(
|
|
240
|
+
const dropdown = screen.getByRole("combobox", { name: /assigned to/i });
|
|
207
241
|
await user.click(dropdown);
|
|
208
|
-
expect(screen.getByText(
|
|
242
|
+
expect(screen.getByText("Alex Example")).toBeInTheDocument();
|
|
209
243
|
});
|
|
210
|
-
it(
|
|
244
|
+
it("shows attach to workstream only on the new task form", () => {
|
|
211
245
|
renderForm();
|
|
212
|
-
expect(screen.getByText(
|
|
213
|
-
expect(screen.getByRole(
|
|
246
|
+
expect(screen.getByText("Attach to workstream")).toBeInTheDocument();
|
|
247
|
+
expect(screen.getByRole("combobox", { name: /attach to workstream/i })).toBeInTheDocument();
|
|
214
248
|
});
|
|
215
|
-
it(
|
|
249
|
+
it("does not show attach to workstream on the edit task form", () => {
|
|
216
250
|
renderForm({
|
|
217
|
-
editingTaskId:
|
|
251
|
+
editingTaskId: "task-1",
|
|
218
252
|
currentTask: {
|
|
219
|
-
id:
|
|
220
|
-
title:
|
|
221
|
-
description:
|
|
222
|
-
category:
|
|
223
|
-
type:
|
|
253
|
+
id: "task-1",
|
|
254
|
+
title: "Existing task",
|
|
255
|
+
description: "",
|
|
256
|
+
category: "general",
|
|
257
|
+
type: "feature",
|
|
224
258
|
priority: 2,
|
|
225
|
-
status:
|
|
226
|
-
createdAt:
|
|
227
|
-
}
|
|
259
|
+
status: "task",
|
|
260
|
+
createdAt: "2026-04-03T00:00:00.000Z",
|
|
261
|
+
},
|
|
228
262
|
});
|
|
229
|
-
expect(screen.queryByText(
|
|
263
|
+
expect(screen.queryByText("Attach to workstream")).not.toBeInTheDocument();
|
|
230
264
|
});
|
|
231
|
-
it(
|
|
265
|
+
it("switches the create-workstream control into manual entry when plus is clicked", async () => {
|
|
232
266
|
const user = userEvent.setup();
|
|
233
267
|
renderForm();
|
|
234
|
-
await user.click(screen.getByRole(
|
|
235
|
-
const manualInput = screen.getByRole(
|
|
268
|
+
await user.click(screen.getByRole("button", { name: /enter workstream by name/i }));
|
|
269
|
+
const manualInput = screen.getByRole("textbox", {
|
|
270
|
+
name: /attach to workstream/i,
|
|
271
|
+
});
|
|
236
272
|
expect(manualInput).toBeInTheDocument();
|
|
237
|
-
await user.type(manualInput,
|
|
273
|
+
await user.type(manualInput, "Platform Refinement");
|
|
238
274
|
expect(defaultProps.onWorkstreamInputChange).toHaveBeenCalled();
|
|
239
275
|
});
|
|
240
|
-
it(
|
|
276
|
+
it("renders custom taxonomy descriptions as helper text", () => {
|
|
241
277
|
renderForm({
|
|
242
|
-
taxonomies: [
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
278
|
+
taxonomies: [
|
|
279
|
+
{
|
|
280
|
+
id: "review-status",
|
|
281
|
+
label: "Review Status",
|
|
282
|
+
description: "Use this to track review routing.",
|
|
283
|
+
widgetType: "select",
|
|
247
284
|
isSystem: false,
|
|
248
285
|
multiSelect: false,
|
|
249
|
-
options: [{ value:
|
|
250
|
-
}
|
|
286
|
+
options: [{ value: "pending", label: "Pending", status: "active" }],
|
|
287
|
+
},
|
|
288
|
+
],
|
|
251
289
|
});
|
|
252
|
-
expect(screen.getByText(
|
|
290
|
+
expect(screen.getByText("Use this to track review routing.")).toBeInTheDocument();
|
|
253
291
|
});
|
|
254
|
-
it(
|
|
292
|
+
it("hides custom taxonomies from the task form when form visibility is disabled", () => {
|
|
255
293
|
renderForm({
|
|
256
|
-
taxonomies: [
|
|
257
|
-
|
|
258
|
-
|
|
294
|
+
taxonomies: [
|
|
295
|
+
{
|
|
296
|
+
id: "review-status",
|
|
297
|
+
label: "Review Status",
|
|
259
298
|
formEnabled: false,
|
|
260
|
-
widgetType:
|
|
299
|
+
widgetType: "select",
|
|
261
300
|
isSystem: false,
|
|
262
301
|
multiSelect: false,
|
|
263
|
-
options: [{ value:
|
|
264
|
-
}
|
|
302
|
+
options: [{ value: "pending", label: "Pending", status: "active" }],
|
|
303
|
+
},
|
|
304
|
+
],
|
|
265
305
|
});
|
|
266
|
-
expect(screen.queryByText(
|
|
306
|
+
expect(screen.queryByText("Review Status")).not.toBeInTheDocument();
|
|
267
307
|
});
|
|
268
|
-
it(
|
|
308
|
+
it("keeps a hidden custom taxonomy visible when the current task already has a value", () => {
|
|
269
309
|
renderForm({
|
|
270
|
-
formTaxonomies: {
|
|
271
|
-
taxonomies: [
|
|
272
|
-
|
|
273
|
-
|
|
310
|
+
formTaxonomies: { "review-status": "pending" },
|
|
311
|
+
taxonomies: [
|
|
312
|
+
{
|
|
313
|
+
id: "review-status",
|
|
314
|
+
label: "Review Status",
|
|
274
315
|
formEnabled: false,
|
|
275
|
-
widgetType:
|
|
316
|
+
widgetType: "select",
|
|
276
317
|
isSystem: false,
|
|
277
318
|
multiSelect: false,
|
|
278
|
-
options: [{ value:
|
|
279
|
-
}
|
|
319
|
+
options: [{ value: "pending", label: "Pending", status: "active" }],
|
|
320
|
+
},
|
|
321
|
+
],
|
|
280
322
|
});
|
|
281
|
-
expect(screen.getByText(
|
|
323
|
+
expect(screen.getByText("Review Status")).toBeInTheDocument();
|
|
282
324
|
});
|
|
283
|
-
it(
|
|
325
|
+
it("renders multi-select custom taxonomies as selectable chips", async () => {
|
|
284
326
|
const user = userEvent.setup();
|
|
285
327
|
renderForm({
|
|
286
|
-
taxonomies: [
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
328
|
+
taxonomies: [
|
|
329
|
+
{
|
|
330
|
+
id: "channels",
|
|
331
|
+
label: "Channels",
|
|
332
|
+
widgetType: "select",
|
|
290
333
|
isSystem: false,
|
|
291
334
|
multiSelect: true,
|
|
292
335
|
options: [
|
|
293
|
-
{ value:
|
|
294
|
-
{ value:
|
|
295
|
-
]
|
|
296
|
-
}
|
|
336
|
+
{ value: "email", label: "Email", status: "active" },
|
|
337
|
+
{ value: "social", label: "Social", status: "active" },
|
|
338
|
+
],
|
|
339
|
+
},
|
|
340
|
+
],
|
|
297
341
|
});
|
|
298
|
-
await user.click(screen.getByRole(
|
|
299
|
-
expect(defaultProps.onTaxonomyChange).toHaveBeenCalledWith(
|
|
342
|
+
await user.click(screen.getByRole("button", { name: "Email" }));
|
|
343
|
+
expect(defaultProps.onTaxonomyChange).toHaveBeenCalledWith("channels", [
|
|
344
|
+
"email",
|
|
345
|
+
]);
|
|
300
346
|
});
|
|
301
|
-
it(
|
|
347
|
+
it("keeps selected retired multi-select custom taxonomy options available", () => {
|
|
302
348
|
renderForm({
|
|
303
|
-
formTaxonomies: { channels: [
|
|
304
|
-
taxonomies: [
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
349
|
+
formTaxonomies: { channels: ["email"] },
|
|
350
|
+
taxonomies: [
|
|
351
|
+
{
|
|
352
|
+
id: "channels",
|
|
353
|
+
label: "Channels",
|
|
354
|
+
widgetType: "select",
|
|
308
355
|
isSystem: false,
|
|
309
356
|
multiSelect: true,
|
|
310
357
|
options: [
|
|
311
|
-
{ value:
|
|
312
|
-
{ value:
|
|
313
|
-
]
|
|
314
|
-
}
|
|
358
|
+
{ value: "email", label: "Email", status: "retired" },
|
|
359
|
+
{ value: "social", label: "Social", status: "active" },
|
|
360
|
+
],
|
|
361
|
+
},
|
|
362
|
+
],
|
|
315
363
|
});
|
|
316
|
-
expect(screen.getByRole(
|
|
317
|
-
expect(screen.getByRole(
|
|
364
|
+
expect(screen.getByRole("button", { name: /Email \(Retired\)/i })).toBeInTheDocument();
|
|
365
|
+
expect(screen.getByRole("button", { name: "Social" })).toBeInTheDocument();
|
|
318
366
|
});
|
|
319
|
-
it(
|
|
367
|
+
it("hides retired custom taxonomies from new-task entry when there is no selected value", () => {
|
|
320
368
|
renderForm({
|
|
321
|
-
taxonomies: [
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
369
|
+
taxonomies: [
|
|
370
|
+
{
|
|
371
|
+
id: "channels",
|
|
372
|
+
label: "Channels",
|
|
373
|
+
status: "retired",
|
|
374
|
+
widgetType: "select",
|
|
326
375
|
isSystem: false,
|
|
327
376
|
multiSelect: false,
|
|
328
|
-
options: [{ value:
|
|
329
|
-
}
|
|
377
|
+
options: [{ value: "email", label: "Email", status: "active" }],
|
|
378
|
+
},
|
|
379
|
+
],
|
|
330
380
|
});
|
|
331
|
-
expect(screen.queryByText(
|
|
381
|
+
expect(screen.queryByText("Channels")).not.toBeInTheDocument();
|
|
332
382
|
});
|
|
333
|
-
it(
|
|
383
|
+
it("keeps retired custom taxonomies visible when the current task already has a value", () => {
|
|
334
384
|
renderForm({
|
|
335
|
-
formTaxonomies: { channels:
|
|
336
|
-
taxonomies: [
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
385
|
+
formTaxonomies: { channels: "email" },
|
|
386
|
+
taxonomies: [
|
|
387
|
+
{
|
|
388
|
+
id: "channels",
|
|
389
|
+
label: "Channels",
|
|
390
|
+
status: "retired",
|
|
391
|
+
widgetType: "select",
|
|
341
392
|
isSystem: false,
|
|
342
393
|
multiSelect: false,
|
|
343
|
-
options: [{ value:
|
|
344
|
-
}
|
|
394
|
+
options: [{ value: "email", label: "Email", status: "active" }],
|
|
395
|
+
},
|
|
396
|
+
],
|
|
345
397
|
});
|
|
346
|
-
expect(screen.getByText(
|
|
398
|
+
expect(screen.getByText("Channels")).toBeInTheDocument();
|
|
347
399
|
});
|
|
348
400
|
// --- Edit Task Tests ---
|
|
349
|
-
it(
|
|
350
|
-
renderForm({ editingTaskId:
|
|
351
|
-
expect(screen.getByDisplayValue(
|
|
401
|
+
it("3.10 Load existing task", () => {
|
|
402
|
+
renderForm({ editingTaskId: "123", title: "Existing Task" });
|
|
403
|
+
expect(screen.getByDisplayValue("Existing Task")).toBeInTheDocument();
|
|
352
404
|
});
|
|
353
|
-
it(
|
|
405
|
+
it("3.11 ID badge visible in edit mode", () => {
|
|
354
406
|
// Form-level edit mode validation.
|
|
355
|
-
renderForm({ editingTaskId:
|
|
407
|
+
renderForm({ editingTaskId: "123" });
|
|
356
408
|
expect(screen.getByText(/Activity & Comments/)).toBeInTheDocument();
|
|
357
409
|
});
|
|
358
|
-
it(
|
|
410
|
+
it("3.12 Copy ID", async () => {
|
|
359
411
|
// Header copy action is outside TaskForm; keep form render sanity check.
|
|
360
412
|
renderForm();
|
|
361
|
-
expect(screen.getByPlaceholderText(
|
|
413
|
+
expect(screen.getByPlaceholderText("What needs to be done?")).toBeInTheDocument();
|
|
362
414
|
});
|
|
363
|
-
it(
|
|
415
|
+
it("renders assignee audit activity with readable labels instead of raw ids", () => {
|
|
364
416
|
renderForm({
|
|
365
|
-
editingTaskId:
|
|
417
|
+
editingTaskId: "123",
|
|
366
418
|
showComments: true,
|
|
367
419
|
assigneeOptions: [
|
|
368
420
|
...defaultProps.assigneeOptions,
|
|
369
|
-
{
|
|
421
|
+
{
|
|
422
|
+
value: "ai-profile-1",
|
|
423
|
+
label: "Planner Bot",
|
|
424
|
+
icon: "Bot",
|
|
425
|
+
color: "#8b5cf6",
|
|
426
|
+
kind: "agent",
|
|
427
|
+
},
|
|
370
428
|
],
|
|
371
429
|
currentTask: {
|
|
372
|
-
id:
|
|
373
|
-
title:
|
|
430
|
+
id: "123",
|
|
431
|
+
title: "Existing Task",
|
|
374
432
|
activity: [
|
|
375
433
|
{
|
|
376
|
-
id:
|
|
377
|
-
type:
|
|
378
|
-
timestamp:
|
|
434
|
+
id: "event:task-event-1",
|
|
435
|
+
type: "event",
|
|
436
|
+
timestamp: "2026-03-23T12:00:00.000Z",
|
|
379
437
|
event: {
|
|
380
|
-
id:
|
|
381
|
-
taskId:
|
|
382
|
-
workspaceId:
|
|
383
|
-
action:
|
|
384
|
-
actor:
|
|
385
|
-
actorType:
|
|
386
|
-
createdAt:
|
|
438
|
+
id: "task-event-1",
|
|
439
|
+
taskId: "123",
|
|
440
|
+
workspaceId: "workspace-1",
|
|
441
|
+
action: "task-updated",
|
|
442
|
+
actor: "member-1",
|
|
443
|
+
actorType: "human",
|
|
444
|
+
createdAt: "2026-03-23T12:00:00.000Z",
|
|
387
445
|
details: {
|
|
388
446
|
changes: {
|
|
389
447
|
assignee: {
|
|
390
|
-
from:
|
|
391
|
-
to:
|
|
448
|
+
from: "unassigned",
|
|
449
|
+
to: "ai-profile-1",
|
|
392
450
|
},
|
|
393
451
|
},
|
|
394
452
|
},
|
|
@@ -397,52 +455,88 @@ describe('TaskForm Component', () => {
|
|
|
397
455
|
],
|
|
398
456
|
},
|
|
399
457
|
});
|
|
400
|
-
expect(screen.getByText(
|
|
458
|
+
expect(screen.getByText("ASSIGNED TO: Unassigned -> Planner Bot")).toBeInTheDocument();
|
|
401
459
|
expect(screen.queryByText(/ai-profile-1/i)).not.toBeInTheDocument();
|
|
402
460
|
});
|
|
403
|
-
it(
|
|
461
|
+
it("renders planning relationship audit activity with reference labels instead of raw ids", () => {
|
|
462
|
+
renderForm({
|
|
463
|
+
editingTaskId: "123",
|
|
464
|
+
showComments: true,
|
|
465
|
+
currentTask: {
|
|
466
|
+
id: "123",
|
|
467
|
+
title: "Existing Task",
|
|
468
|
+
activity: [
|
|
469
|
+
{
|
|
470
|
+
id: "event:task-event-relationship",
|
|
471
|
+
type: "event",
|
|
472
|
+
timestamp: "2026-03-23T12:00:00.000Z",
|
|
473
|
+
event: {
|
|
474
|
+
id: "task-event-relationship",
|
|
475
|
+
taskId: "123",
|
|
476
|
+
workspaceId: "workspace-1",
|
|
477
|
+
action: "task-relationship-changed",
|
|
478
|
+
actor: "member-1",
|
|
479
|
+
actorType: "human",
|
|
480
|
+
createdAt: "2026-03-23T12:00:00.000Z",
|
|
481
|
+
details: {
|
|
482
|
+
changes: {
|
|
483
|
+
workstreamId: {
|
|
484
|
+
from: null,
|
|
485
|
+
to: "workstream-1",
|
|
486
|
+
},
|
|
487
|
+
},
|
|
488
|
+
},
|
|
489
|
+
},
|
|
490
|
+
},
|
|
491
|
+
],
|
|
492
|
+
},
|
|
493
|
+
});
|
|
494
|
+
expect(screen.getByText("Workstream: none -> WS-12")).toBeInTheDocument();
|
|
495
|
+
expect(screen.queryByText(/workstream-1/i)).not.toBeInTheDocument();
|
|
496
|
+
});
|
|
497
|
+
it("3.13 Update title", async () => {
|
|
404
498
|
const user = userEvent.setup();
|
|
405
|
-
renderForm({ editingTaskId:
|
|
406
|
-
const input = screen.getByDisplayValue(
|
|
499
|
+
renderForm({ editingTaskId: "123", title: "Old Title" });
|
|
500
|
+
const input = screen.getByDisplayValue("Old Title");
|
|
407
501
|
await user.clear(input);
|
|
408
|
-
await user.type(input,
|
|
502
|
+
await user.type(input, "New Title");
|
|
409
503
|
expect(defaultProps.onTitleChange).toHaveBeenCalled();
|
|
410
504
|
});
|
|
411
|
-
it(
|
|
505
|
+
it("keeps scheduled date editable in the task form", () => {
|
|
412
506
|
renderForm({
|
|
413
|
-
editingTaskId:
|
|
414
|
-
currentTask: { id:
|
|
507
|
+
editingTaskId: "parent-1",
|
|
508
|
+
currentTask: { id: "parent-1", title: "Parent Task" },
|
|
415
509
|
});
|
|
416
|
-
const scheduledLabel = screen.getByText(
|
|
510
|
+
const scheduledLabel = screen.getByText("Scheduled:");
|
|
417
511
|
const scheduledInput = scheduledLabel.nextElementSibling;
|
|
418
512
|
expect(scheduledInput).toBeTruthy();
|
|
419
513
|
if (!scheduledInput)
|
|
420
|
-
throw new Error(
|
|
514
|
+
throw new Error("Scheduled date input missing");
|
|
421
515
|
expect(scheduledInput.disabled).toBe(false);
|
|
422
|
-
expect(screen.queryByText(
|
|
516
|
+
expect(screen.queryByText("Parent tasks cannot be scheduled. Schedule leaf tasks instead.")).not.toBeInTheDocument();
|
|
423
517
|
});
|
|
424
|
-
it(
|
|
518
|
+
it("3.14 Context Documents section renders", async () => {
|
|
425
519
|
const user = userEvent.setup();
|
|
426
520
|
renderForm();
|
|
427
|
-
expect(screen.getByText(
|
|
428
|
-
await user.click(screen.getByTitle(
|
|
429
|
-
expect(screen.getByRole(
|
|
521
|
+
expect(screen.getByText("Context Documents")).toBeInTheDocument();
|
|
522
|
+
await user.click(screen.getByTitle("Show context documents"));
|
|
523
|
+
expect(screen.getByRole("button", { name: /upload/i })).toBeInTheDocument();
|
|
430
524
|
});
|
|
431
|
-
it(
|
|
525
|
+
it("3.15 Markdown Help Toggle", async () => {
|
|
432
526
|
const user = userEvent.setup();
|
|
433
527
|
renderForm();
|
|
434
|
-
const helpBtn = screen.getByTitle(
|
|
528
|
+
const helpBtn = screen.getByTitle("Markdown Help");
|
|
435
529
|
await user.click(helpBtn);
|
|
436
530
|
expect(defaultProps.onShowMarkdownHelpChange).toHaveBeenCalledWith(true);
|
|
437
531
|
});
|
|
438
|
-
it(
|
|
532
|
+
it("3.16 Description Focus", () => {
|
|
439
533
|
renderForm();
|
|
440
|
-
renderForm({ description:
|
|
441
|
-
const previewDiv = screen.getByText(
|
|
534
|
+
renderForm({ description: "Test Desc", descriptionFocused: false });
|
|
535
|
+
const previewDiv = screen.getByText("Test Desc");
|
|
442
536
|
fireEvent.click(previewDiv);
|
|
443
537
|
expect(defaultProps.onDescriptionFocusedChange).toHaveBeenCalledWith(true);
|
|
444
538
|
});
|
|
445
|
-
it(
|
|
539
|
+
it("3.18 Category Manager Toggle", async () => {
|
|
446
540
|
const user = userEvent.setup();
|
|
447
541
|
renderForm();
|
|
448
542
|
// The + button next to Category label
|
|
@@ -471,252 +565,279 @@ describe('TaskForm Component', () => {
|
|
|
471
565
|
const { container } = renderForm();
|
|
472
566
|
// Select the first button with class manageLink (assuming style imports work or we use classname)
|
|
473
567
|
// Styles are imported as object `styles`. In test, classes are mocked or stringified?
|
|
474
|
-
// Vitest CSS handling usually preserves class names if using `identity-obj-proxy` or similar,
|
|
568
|
+
// Vitest CSS handling usually preserves class names if using `identity-obj-proxy` or similar,
|
|
475
569
|
// but here we have `css: true` in vitest config.
|
|
476
570
|
// It might be hashed.
|
|
477
571
|
// Fallback: The button contains an SVG.
|
|
478
572
|
// Let's try to find the button by its visual role.
|
|
479
573
|
// It is `tabIndex={-1}`.
|
|
480
|
-
const buttons = screen.getAllByRole(
|
|
574
|
+
const buttons = screen.getAllByRole("button", { hidden: true }); // tabIndex -1 might hide it from accessibility tree? No.
|
|
481
575
|
// Let's skip this specific test for now or try to match by association.
|
|
482
576
|
expect(true).toBe(true);
|
|
483
577
|
});
|
|
484
578
|
// --- Unsaved Changes Tests ---
|
|
485
579
|
// These logic reside in the Parent (Taskforce), checking state vs original.
|
|
486
|
-
// TaskForm just calls `onChange`.
|
|
580
|
+
// TaskForm just calls `onChange`.
|
|
487
581
|
// So "Test 3.19 Trigger warning" is an INTEGRATION test for Taskforce, not Unit test for TaskForm.
|
|
488
582
|
// But we CAN verify that TaskForm calls the change handlers correctly, which we did in 3.3, 3.4.
|
|
489
583
|
// --- Comments ---
|
|
490
|
-
it(
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
584
|
+
it("3.23 Shows the activity sidebar in edit mode", () => {
|
|
585
|
+
renderForm({ editingTaskId: "123" });
|
|
586
|
+
expect(screen.getByText("Activity & Comments")).toBeInTheDocument();
|
|
587
|
+
expect(screen.getByPlaceholderText("Type a comment...")).toBeInTheDocument();
|
|
588
|
+
});
|
|
589
|
+
it("scrolls the activity thread to the latest item on open", () => {
|
|
590
|
+
const originalScrollIntoView = HTMLElement.prototype.scrollIntoView;
|
|
591
|
+
const scrollIntoView = vi.fn();
|
|
592
|
+
Object.defineProperty(HTMLElement.prototype, "scrollIntoView", {
|
|
593
|
+
configurable: true,
|
|
594
|
+
value: scrollIntoView,
|
|
595
|
+
});
|
|
596
|
+
try {
|
|
597
|
+
renderForm({
|
|
598
|
+
editingTaskId: "123",
|
|
599
|
+
comments: [
|
|
600
|
+
{
|
|
601
|
+
id: "comment-scroll-1",
|
|
602
|
+
author: "member-1",
|
|
603
|
+
text: "Latest comment",
|
|
604
|
+
timestamp: "2026-03-22T10:10:00.000Z",
|
|
605
|
+
},
|
|
606
|
+
],
|
|
607
|
+
});
|
|
608
|
+
expect(scrollIntoView).toHaveBeenCalled();
|
|
609
|
+
}
|
|
610
|
+
finally {
|
|
611
|
+
if (originalScrollIntoView) {
|
|
612
|
+
Object.defineProperty(HTMLElement.prototype, "scrollIntoView", {
|
|
613
|
+
configurable: true,
|
|
614
|
+
value: originalScrollIntoView,
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
else {
|
|
618
|
+
delete HTMLElement.prototype.scrollIntoView;
|
|
619
|
+
}
|
|
620
|
+
}
|
|
496
621
|
});
|
|
497
|
-
it(
|
|
622
|
+
it("3.24 Add comment", async () => {
|
|
498
623
|
const user = userEvent.setup();
|
|
499
|
-
renderForm({ editingTaskId:
|
|
500
|
-
|
|
501
|
-
// The button has <Send /> icon.
|
|
502
|
-
// We can find by class or hierarchy.
|
|
503
|
-
// Or find the textarea and the button next to it.
|
|
504
|
-
const textarea = screen.getByPlaceholderText('Type a comment...');
|
|
505
|
-
const button = textarea.nextSibling; // Assuming button is next sibling
|
|
506
|
-
await user.click(button);
|
|
624
|
+
renderForm({ editingTaskId: "123", newCommentText: "Nice work" });
|
|
625
|
+
await user.click(screen.getByRole("button", { name: /send comment/i }));
|
|
507
626
|
expect(defaultProps.onAddComment).toHaveBeenCalled();
|
|
508
627
|
});
|
|
509
|
-
it(
|
|
628
|
+
it("renders named AI comment authors from resolved actor data", () => {
|
|
510
629
|
renderForm({
|
|
511
|
-
editingTaskId:
|
|
630
|
+
editingTaskId: "123",
|
|
512
631
|
showComments: true,
|
|
513
632
|
comments: [
|
|
514
633
|
{
|
|
515
|
-
id:
|
|
516
|
-
author:
|
|
517
|
-
text:
|
|
518
|
-
timestamp:
|
|
634
|
+
id: "comment-1",
|
|
635
|
+
author: "ai-profile-123",
|
|
636
|
+
text: "I reviewed the task details.",
|
|
637
|
+
timestamp: "2026-03-22T10:00:00.000Z",
|
|
519
638
|
actor: {
|
|
520
|
-
id:
|
|
521
|
-
kind:
|
|
522
|
-
label:
|
|
523
|
-
username:
|
|
524
|
-
icon:
|
|
525
|
-
color:
|
|
639
|
+
id: "ai-profile-123",
|
|
640
|
+
kind: "ai",
|
|
641
|
+
label: "Codex",
|
|
642
|
+
username: "codex",
|
|
643
|
+
icon: "Bot",
|
|
644
|
+
color: "#0ea5e9",
|
|
526
645
|
isNamed: true,
|
|
527
646
|
},
|
|
528
647
|
},
|
|
529
648
|
],
|
|
530
649
|
});
|
|
531
|
-
expect(screen.getByText(
|
|
532
|
-
expect(screen.queryByText(
|
|
650
|
+
expect(screen.getByText("Codex")).toBeInTheDocument();
|
|
651
|
+
expect(screen.queryByText("AI Agent")).not.toBeInTheDocument();
|
|
533
652
|
});
|
|
534
|
-
it(
|
|
653
|
+
it("renders task audit events in the activity feed", () => {
|
|
535
654
|
renderForm({
|
|
536
|
-
editingTaskId:
|
|
655
|
+
editingTaskId: "123",
|
|
537
656
|
showComments: true,
|
|
538
657
|
currentTask: {
|
|
539
|
-
id:
|
|
540
|
-
title:
|
|
541
|
-
status:
|
|
542
|
-
createdAt:
|
|
658
|
+
id: "task-activity-1",
|
|
659
|
+
title: "Activity Task",
|
|
660
|
+
status: "in-progress",
|
|
661
|
+
createdAt: "2026-03-22T09:00:00.000Z",
|
|
543
662
|
activity: [
|
|
544
663
|
{
|
|
545
|
-
id:
|
|
546
|
-
type:
|
|
547
|
-
timestamp:
|
|
664
|
+
id: "event:1",
|
|
665
|
+
type: "event",
|
|
666
|
+
timestamp: "2026-03-22T10:00:00.000Z",
|
|
548
667
|
event: {
|
|
549
|
-
id:
|
|
550
|
-
taskId:
|
|
551
|
-
workspaceId:
|
|
552
|
-
action:
|
|
553
|
-
actor:
|
|
554
|
-
actorType:
|
|
555
|
-
createdAt:
|
|
668
|
+
id: "event-1",
|
|
669
|
+
taskId: "task-activity-1",
|
|
670
|
+
workspaceId: "default",
|
|
671
|
+
action: "task-status-changed",
|
|
672
|
+
actor: "user",
|
|
673
|
+
actorType: "human",
|
|
674
|
+
createdAt: "2026-03-22T10:00:00.000Z",
|
|
556
675
|
details: {
|
|
557
676
|
changes: {
|
|
558
|
-
status: { from:
|
|
559
|
-
}
|
|
560
|
-
}
|
|
561
|
-
}
|
|
562
|
-
}
|
|
563
|
-
]
|
|
677
|
+
status: { from: "task", to: "in-progress" },
|
|
678
|
+
},
|
|
679
|
+
},
|
|
680
|
+
},
|
|
681
|
+
},
|
|
682
|
+
],
|
|
564
683
|
},
|
|
565
684
|
});
|
|
566
|
-
expect(screen.getByText(
|
|
567
|
-
expect(screen.queryByText(
|
|
685
|
+
expect(screen.getByText("Status: task -> in-progress")).toBeInTheDocument();
|
|
686
|
+
expect(screen.queryByText("System")).not.toBeInTheDocument();
|
|
568
687
|
});
|
|
569
|
-
it(
|
|
688
|
+
it("summarizes description audit events without rendering the full description body", () => {
|
|
570
689
|
renderForm({
|
|
571
|
-
editingTaskId:
|
|
690
|
+
editingTaskId: "123",
|
|
572
691
|
showComments: true,
|
|
573
692
|
currentTask: {
|
|
574
|
-
id:
|
|
575
|
-
title:
|
|
576
|
-
status:
|
|
577
|
-
createdAt:
|
|
693
|
+
id: "task-activity-2",
|
|
694
|
+
title: "Description Audit Task",
|
|
695
|
+
status: "task",
|
|
696
|
+
createdAt: "2026-03-22T09:00:00.000Z",
|
|
578
697
|
activity: [
|
|
579
698
|
{
|
|
580
|
-
id:
|
|
581
|
-
type:
|
|
582
|
-
timestamp:
|
|
699
|
+
id: "event:2",
|
|
700
|
+
type: "event",
|
|
701
|
+
timestamp: "2026-03-22T10:00:00.000Z",
|
|
583
702
|
event: {
|
|
584
|
-
id:
|
|
585
|
-
taskId:
|
|
586
|
-
workspaceId:
|
|
587
|
-
action:
|
|
588
|
-
actor:
|
|
589
|
-
actorType:
|
|
590
|
-
createdAt:
|
|
703
|
+
id: "event-2",
|
|
704
|
+
taskId: "task-activity-2",
|
|
705
|
+
workspaceId: "default",
|
|
706
|
+
action: "task-updated",
|
|
707
|
+
actor: "user",
|
|
708
|
+
actorType: "human",
|
|
709
|
+
createdAt: "2026-03-22T10:00:00.000Z",
|
|
591
710
|
details: {
|
|
592
711
|
changes: {
|
|
593
712
|
description: {
|
|
594
|
-
from:
|
|
595
|
-
to:
|
|
596
|
-
}
|
|
597
|
-
}
|
|
598
|
-
}
|
|
599
|
-
}
|
|
600
|
-
}
|
|
601
|
-
]
|
|
713
|
+
from: "",
|
|
714
|
+
to: "Line one\n\nLine two\n\nLine three",
|
|
715
|
+
},
|
|
716
|
+
},
|
|
717
|
+
},
|
|
718
|
+
},
|
|
719
|
+
},
|
|
720
|
+
],
|
|
602
721
|
},
|
|
603
722
|
});
|
|
604
|
-
expect(screen.getByText(
|
|
723
|
+
expect(screen.getByText("DESCRIPTION: added")).toBeInTheDocument();
|
|
605
724
|
expect(screen.queryByText(/Line one/)).not.toBeInTheDocument();
|
|
606
725
|
expect(screen.queryByText(/Line two/)).not.toBeInTheDocument();
|
|
607
726
|
expect(screen.queryByText(/Line three/)).not.toBeInTheDocument();
|
|
608
727
|
});
|
|
609
|
-
it(
|
|
728
|
+
it("summarizes title audit events without rendering before and after title text", () => {
|
|
610
729
|
renderForm({
|
|
611
|
-
editingTaskId:
|
|
730
|
+
editingTaskId: "123",
|
|
612
731
|
showComments: true,
|
|
613
732
|
currentTask: {
|
|
614
|
-
id:
|
|
615
|
-
title:
|
|
616
|
-
status:
|
|
617
|
-
createdAt:
|
|
733
|
+
id: "task-activity-3",
|
|
734
|
+
title: "Renamed task",
|
|
735
|
+
status: "task",
|
|
736
|
+
createdAt: "2026-03-22T09:00:00.000Z",
|
|
618
737
|
activity: [
|
|
619
738
|
{
|
|
620
|
-
id:
|
|
621
|
-
type:
|
|
622
|
-
timestamp:
|
|
739
|
+
id: "event:3",
|
|
740
|
+
type: "event",
|
|
741
|
+
timestamp: "2026-03-22T10:00:00.000Z",
|
|
623
742
|
event: {
|
|
624
|
-
id:
|
|
625
|
-
taskId:
|
|
626
|
-
workspaceId:
|
|
627
|
-
action:
|
|
628
|
-
actor:
|
|
629
|
-
actorType:
|
|
630
|
-
createdAt:
|
|
743
|
+
id: "event-3",
|
|
744
|
+
taskId: "task-activity-3",
|
|
745
|
+
workspaceId: "default",
|
|
746
|
+
action: "task-updated",
|
|
747
|
+
actor: "user",
|
|
748
|
+
actorType: "human",
|
|
749
|
+
createdAt: "2026-03-22T10:00:00.000Z",
|
|
631
750
|
details: {
|
|
632
751
|
changes: {
|
|
633
752
|
title: {
|
|
634
|
-
from:
|
|
635
|
-
to:
|
|
636
|
-
}
|
|
637
|
-
}
|
|
638
|
-
}
|
|
639
|
-
}
|
|
640
|
-
}
|
|
641
|
-
]
|
|
753
|
+
from: "Old title",
|
|
754
|
+
to: "New title",
|
|
755
|
+
},
|
|
756
|
+
},
|
|
757
|
+
},
|
|
758
|
+
},
|
|
759
|
+
},
|
|
760
|
+
],
|
|
642
761
|
},
|
|
643
762
|
});
|
|
644
|
-
expect(screen.getByText(
|
|
645
|
-
expect(screen.queryByText(
|
|
646
|
-
expect(screen.queryByText(
|
|
763
|
+
expect(screen.getByText("TITLE: updated")).toBeInTheDocument();
|
|
764
|
+
expect(screen.queryByText("Old title")).not.toBeInTheDocument();
|
|
765
|
+
expect(screen.queryByText("New title")).not.toBeInTheDocument();
|
|
647
766
|
});
|
|
648
|
-
it(
|
|
767
|
+
it("falls back to the current task actor label for AI comments when only the id is present", () => {
|
|
649
768
|
renderForm({
|
|
650
|
-
editingTaskId:
|
|
769
|
+
editingTaskId: "123",
|
|
651
770
|
showComments: true,
|
|
652
771
|
currentTask: {
|
|
653
|
-
id:
|
|
654
|
-
title:
|
|
655
|
-
assignee:
|
|
772
|
+
id: "123",
|
|
773
|
+
title: "Codex-owned task",
|
|
774
|
+
assignee: "ai-profile-123",
|
|
656
775
|
assigneeActor: {
|
|
657
|
-
id:
|
|
658
|
-
kind:
|
|
659
|
-
label:
|
|
660
|
-
username:
|
|
661
|
-
icon:
|
|
662
|
-
color:
|
|
776
|
+
id: "ai-profile-123",
|
|
777
|
+
kind: "ai",
|
|
778
|
+
label: "Codex",
|
|
779
|
+
username: "codex",
|
|
780
|
+
icon: "Bot",
|
|
781
|
+
color: "#0ea5e9",
|
|
663
782
|
isNamed: true,
|
|
664
783
|
},
|
|
665
784
|
},
|
|
666
785
|
comments: [
|
|
667
786
|
{
|
|
668
|
-
id:
|
|
669
|
-
author:
|
|
670
|
-
text:
|
|
671
|
-
timestamp:
|
|
787
|
+
id: "comment-2",
|
|
788
|
+
author: "ai-profile-123",
|
|
789
|
+
text: "Rendered from task actor fallback.",
|
|
790
|
+
timestamp: "2026-03-22T10:05:00.000Z",
|
|
672
791
|
},
|
|
673
792
|
],
|
|
674
793
|
});
|
|
675
|
-
expect(screen.getByText(
|
|
794
|
+
expect(screen.getByText("Codex")).toBeInTheDocument();
|
|
676
795
|
expect(screen.queryByText(/019d|ai-profile/i)).not.toBeInTheDocument();
|
|
677
796
|
});
|
|
678
|
-
it(
|
|
797
|
+
it("uses the resolved actor icon and color tint for named AI comments", () => {
|
|
679
798
|
const { container } = renderForm({
|
|
680
|
-
editingTaskId:
|
|
799
|
+
editingTaskId: "123",
|
|
681
800
|
showComments: true,
|
|
682
801
|
comments: [
|
|
683
802
|
{
|
|
684
|
-
id:
|
|
685
|
-
author:
|
|
686
|
-
text:
|
|
687
|
-
timestamp:
|
|
803
|
+
id: "comment-3",
|
|
804
|
+
author: "claude",
|
|
805
|
+
text: "Resolved actor presentation.",
|
|
806
|
+
timestamp: "2026-03-22T10:10:00.000Z",
|
|
688
807
|
actor: {
|
|
689
|
-
id:
|
|
690
|
-
kind:
|
|
691
|
-
label:
|
|
692
|
-
username:
|
|
693
|
-
icon:
|
|
694
|
-
color:
|
|
808
|
+
id: "ai-profile-claude",
|
|
809
|
+
kind: "ai",
|
|
810
|
+
label: "Claude",
|
|
811
|
+
username: "claude",
|
|
812
|
+
icon: "Bot",
|
|
813
|
+
color: "#f59e0b",
|
|
695
814
|
isNamed: true,
|
|
696
815
|
},
|
|
697
816
|
},
|
|
698
817
|
],
|
|
699
818
|
});
|
|
700
|
-
expect(container.querySelector(
|
|
701
|
-
const bubble = screen
|
|
819
|
+
expect(container.querySelector(".lucide-bot")).toBeTruthy();
|
|
820
|
+
const bubble = screen
|
|
821
|
+
.getByText("Resolved actor presentation.")
|
|
822
|
+
.closest('[class*="commentText"]');
|
|
702
823
|
expect(bubble).toHaveStyle({
|
|
703
|
-
borderColor:
|
|
824
|
+
borderColor: "rgba(245, 158, 11, 0.35)",
|
|
704
825
|
});
|
|
705
826
|
});
|
|
706
827
|
// --- Context Upload ---
|
|
707
|
-
it(
|
|
828
|
+
it("3.25 Render context image", async () => {
|
|
708
829
|
const user = userEvent.setup();
|
|
709
|
-
renderForm({ contextFiles: [{ path:
|
|
710
|
-
await user.click(screen.getByTitle(
|
|
830
|
+
renderForm({ contextFiles: [{ path: "img.png", timestamp: "" }] });
|
|
831
|
+
await user.click(screen.getByTitle("Show context documents"));
|
|
711
832
|
const img = screen.getByAltText(/Context file 0/i);
|
|
712
833
|
expect(img).toBeInTheDocument();
|
|
713
|
-
expect(img).toHaveAttribute(
|
|
834
|
+
expect(img).toHaveAttribute("src", "img.png");
|
|
714
835
|
});
|
|
715
|
-
it(
|
|
836
|
+
it("3.26 Remove context file", async () => {
|
|
716
837
|
const user = userEvent.setup();
|
|
717
|
-
renderForm({ contextFiles: [{ path:
|
|
718
|
-
await user.click(screen.getByTitle(
|
|
719
|
-
const deleteBtn = screen.getByTitle(
|
|
838
|
+
renderForm({ contextFiles: [{ path: "img.png", timestamp: "" }] });
|
|
839
|
+
await user.click(screen.getByTitle("Show context documents"));
|
|
840
|
+
const deleteBtn = screen.getByTitle("Delete");
|
|
720
841
|
await user.click(deleteBtn);
|
|
721
842
|
expect(defaultProps.onRemoveContextFile).toHaveBeenCalledWith(0);
|
|
722
843
|
});
|