@echothink-ui/task 0.1.0

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.
Files changed (64) hide show
  1. package/README.md +5 -0
  2. package/dist/components/BackendThinkingChain.d.ts +2 -0
  3. package/dist/components/BlockingReasonPanel.d.ts +2 -0
  4. package/dist/components/DAGEdge.d.ts +2 -0
  5. package/dist/components/DAGLegend.d.ts +2 -0
  6. package/dist/components/DAGNode.d.ts +4 -0
  7. package/dist/components/DecisionRequiredPanel.d.ts +2 -0
  8. package/dist/components/HumanInterventionPanel.d.ts +2 -0
  9. package/dist/components/MobileTaskShell.d.ts +12 -0
  10. package/dist/components/TaskApprovalPanel.d.ts +2 -0
  11. package/dist/components/TaskCard.d.ts +2 -0
  12. package/dist/components/TaskDependencyList.d.ts +2 -0
  13. package/dist/components/TaskDetailPanel.d.ts +2 -0
  14. package/dist/components/TaskHandoffPanel.d.ts +2 -0
  15. package/dist/components/TaskProgressIndicator.d.ts +2 -0
  16. package/dist/components/TaskRetryPanel.d.ts +2 -0
  17. package/dist/components/TaskRunLog.d.ts +2 -0
  18. package/dist/components/TaskStatusBadge.d.ts +2 -0
  19. package/dist/components/TaskTable.d.ts +5 -0
  20. package/dist/components/TaskTimeline.d.ts +2 -0
  21. package/dist/components/TaskWaveDAG.d.ts +2 -0
  22. package/dist/components/TaskWaveHeader.d.ts +2 -0
  23. package/dist/components/TaskWaveTable.d.ts +2 -0
  24. package/dist/components/utils.d.ts +13 -0
  25. package/dist/index.cjs +2434 -0
  26. package/dist/index.cjs.map +1 -0
  27. package/dist/index.css +2402 -0
  28. package/dist/index.css.map +1 -0
  29. package/dist/index.d.ts +27 -0
  30. package/dist/index.js +2388 -0
  31. package/dist/index.js.map +1 -0
  32. package/dist/types.d.ts +249 -0
  33. package/package.json +45 -0
  34. package/src/components/BackendThinkingChain.tsx +129 -0
  35. package/src/components/BlockingReasonPanel.tsx +67 -0
  36. package/src/components/DAGEdge.tsx +97 -0
  37. package/src/components/DAGLegend.tsx +86 -0
  38. package/src/components/DAGNode.tsx +103 -0
  39. package/src/components/DecisionRequiredPanel.tsx +166 -0
  40. package/src/components/HumanInterventionPanel.tsx +82 -0
  41. package/src/components/MobileTaskShell.tsx +52 -0
  42. package/src/components/TaskApprovalPanel.tsx +159 -0
  43. package/src/components/TaskCard.tsx +71 -0
  44. package/src/components/TaskDependencyList.test.tsx +54 -0
  45. package/src/components/TaskDependencyList.tsx +105 -0
  46. package/src/components/TaskDetailPanel.test.tsx +49 -0
  47. package/src/components/TaskDetailPanel.tsx +139 -0
  48. package/src/components/TaskHandoffPanel.tsx +125 -0
  49. package/src/components/TaskProgressIndicator.tsx +70 -0
  50. package/src/components/TaskRetryPanel.test.tsx +29 -0
  51. package/src/components/TaskRetryPanel.tsx +103 -0
  52. package/src/components/TaskRunLog.tsx +156 -0
  53. package/src/components/TaskStatusBadge.tsx +29 -0
  54. package/src/components/TaskTable.tsx +294 -0
  55. package/src/components/TaskTimeline.tsx +98 -0
  56. package/src/components/TaskWaveDAG.tsx +202 -0
  57. package/src/components/TaskWaveHeader.tsx +82 -0
  58. package/src/components/TaskWaveTable.tsx +151 -0
  59. package/src/components/css.d.ts +1 -0
  60. package/src/components/utils.ts +116 -0
  61. package/src/index.test.tsx +316 -0
  62. package/src/index.tsx +90 -0
  63. package/src/styles.css +2889 -0
  64. package/src/types.ts +289 -0
@@ -0,0 +1,82 @@
1
+ import * as React from "react";
2
+ import clsx from "clsx";
3
+ import { ActionGroup, Badge, InlineNotification, Surface } from "@echothink-ui/core";
4
+ import type { EthSeverity } from "@echothink-ui/core";
5
+ import type { HumanInterventionPanelProps } from "../types";
6
+
7
+ function urgencySeverity(urgency: HumanInterventionPanelProps["urgency"]): EthSeverity {
8
+ if (urgency === "high") return "danger";
9
+ if (urgency === "medium") return "warning";
10
+ return "info";
11
+ }
12
+
13
+ function urgencyLabel(urgency: HumanInterventionPanelProps["urgency"]) {
14
+ if (urgency === "high") return "High urgency";
15
+ if (urgency === "medium") return "Medium urgency";
16
+ return "Low urgency";
17
+ }
18
+
19
+ export function HumanInterventionPanel({
20
+ reason,
21
+ affectedTask,
22
+ actions,
23
+ urgency = "medium",
24
+ className,
25
+ title = "Human intervention required",
26
+ status = "approval-required",
27
+ severity,
28
+ role = "region",
29
+ "aria-labelledby": ariaLabelledBy,
30
+ ...props
31
+ }: HumanInterventionPanelProps) {
32
+ const titleId = React.useId();
33
+ const panelSeverity = severity ?? urgencySeverity(urgency);
34
+ const labelledBy = ariaLabelledBy ?? (title ? titleId : undefined);
35
+
36
+ return (
37
+ <Surface
38
+ {...props}
39
+ role={role}
40
+ aria-labelledby={labelledBy}
41
+ className={clsx("eth-task-human-intervention-panel", className)}
42
+ title={title ? <span id={titleId}>{title}</span> : undefined}
43
+ status={status}
44
+ severity={panelSeverity}
45
+ data-eth-component="HumanInterventionPanel"
46
+ >
47
+ <InlineNotification severity={panelSeverity} title="Intervention requested">
48
+ {reason ?? "Review this task and choose the next action before the agent can continue."}
49
+ </InlineNotification>
50
+
51
+ <dl className="eth-task-human-intervention-panel__details" aria-label="Intervention details">
52
+ {affectedTask ? (
53
+ <div>
54
+ <dt>Affected task</dt>
55
+ <dd>
56
+ <span className="eth-task-human-intervention-panel__task-title">
57
+ {affectedTask.title}
58
+ </span>
59
+ <span className="eth-task-human-intervention-panel__task-id">{affectedTask.id}</span>
60
+ </dd>
61
+ </div>
62
+ ) : null}
63
+ <div>
64
+ <dt>Urgency</dt>
65
+ <dd>
66
+ <Badge severity={panelSeverity}>{urgencyLabel(urgency)}</Badge>
67
+ </dd>
68
+ </div>
69
+ </dl>
70
+
71
+ {actions?.length ? (
72
+ <div
73
+ className="eth-task-human-intervention-panel__actions"
74
+ role="group"
75
+ aria-label="Intervention actions"
76
+ >
77
+ <ActionGroup actions={actions} />
78
+ </div>
79
+ ) : null}
80
+ </Surface>
81
+ );
82
+ }
@@ -0,0 +1,52 @@
1
+ import * as React from "react";
2
+ import clsx from "clsx";
3
+
4
+ export interface MobileTaskShellProps {
5
+ title?: React.ReactNode;
6
+ subtitle?: React.ReactNode;
7
+ progress?: React.ReactNode;
8
+ main?: React.ReactNode;
9
+ footer?: React.ReactNode;
10
+ identity?: React.ReactNode;
11
+ children?: React.ReactNode;
12
+ className?: string;
13
+ }
14
+
15
+ export function MobileTaskShell({
16
+ title,
17
+ subtitle,
18
+ progress,
19
+ main,
20
+ footer,
21
+ identity,
22
+ children,
23
+ className
24
+ }: MobileTaskShellProps) {
25
+ const content = main ?? children;
26
+ const hasHeader = title || subtitle || progress;
27
+ const hasFooter = footer || identity;
28
+
29
+ return (
30
+ <section
31
+ className={clsx("eth-mobile-task-shell", className)}
32
+ data-eth-component="MobileTaskShell"
33
+ >
34
+ {hasHeader ? (
35
+ <header className="eth-mobile-task-shell__header">
36
+ <div className="eth-mobile-task-shell__title-block">
37
+ {title ? <h1 className="eth-mobile-task-shell__title">{title}</h1> : null}
38
+ {subtitle ? <p className="eth-mobile-task-shell__subtitle">{subtitle}</p> : null}
39
+ </div>
40
+ {progress ? <div className="eth-mobile-task-shell__progress">{progress}</div> : null}
41
+ </header>
42
+ ) : null}
43
+ <main className="eth-mobile-task-shell__main">{content}</main>
44
+ {hasFooter ? (
45
+ <footer className="eth-mobile-task-shell__footer">
46
+ {footer ? <div className="eth-mobile-task-shell__footer-main">{footer}</div> : null}
47
+ {identity ? <div className="eth-mobile-task-shell__identity">{identity}</div> : null}
48
+ </footer>
49
+ ) : null}
50
+ </section>
51
+ );
52
+ }
@@ -0,0 +1,159 @@
1
+ import * as React from "react";
2
+ import { Button, FormField, InlineNotification, Textarea } from "@echothink-ui/core";
3
+ import type { DecisionEvidence, TaskApprovalPanelProps } from "../types";
4
+ import { DecisionRequiredPanel } from "./DecisionRequiredPanel";
5
+
6
+ function refEvidence(
7
+ taskRef: React.ReactNode,
8
+ diffRef: React.ReactNode,
9
+ policyRef: React.ReactNode,
10
+ evidence: DecisionEvidence[] = []
11
+ ): DecisionEvidence[] {
12
+ const refs: DecisionEvidence[] = [];
13
+ if (taskRef) refs.push({ id: "task-ref", label: "Task reference", preview: taskRef });
14
+ if (diffRef) refs.push({ id: "diff-ref", label: "Diff reference", preview: diffRef });
15
+ if (policyRef) refs.push({ id: "policy-ref", label: "Policy reference", preview: policyRef });
16
+ return [...refs, ...evidence];
17
+ }
18
+
19
+ function stateNotification(state: NonNullable<TaskApprovalPanelProps["state"]>) {
20
+ if (state === "approved") {
21
+ return {
22
+ severity: "success" as const,
23
+ title: "Output approved",
24
+ description: "The approval has been recorded with the current audit metadata."
25
+ };
26
+ }
27
+
28
+ if (state === "rejected") {
29
+ return {
30
+ severity: "danger" as const,
31
+ title: "Output rejected",
32
+ description: "The rejection reason and evidence references are retained for review."
33
+ };
34
+ }
35
+
36
+ if (state === "pending") {
37
+ return {
38
+ severity: "info" as const,
39
+ title: "Decision pending",
40
+ description: "The approval decision is being prepared for audit capture."
41
+ };
42
+ }
43
+
44
+ return null;
45
+ }
46
+
47
+ export function TaskApprovalPanel({
48
+ taskRef,
49
+ diffRef,
50
+ policyRef,
51
+ summary,
52
+ evidence,
53
+ onApprove,
54
+ onReject,
55
+ onRequestChanges,
56
+ state = "idle",
57
+ title = "Task approval required",
58
+ description,
59
+ footer,
60
+ ...props
61
+ }: TaskApprovalPanelProps) {
62
+ const [reason, setReason] = React.useState("");
63
+ const reasonId = React.useId();
64
+ const notification = stateNotification(state);
65
+ const policy = policyRef ?? "Default task policy";
66
+ const isPending = state === "pending";
67
+
68
+ return (
69
+ <section className="eth-task-approval-panel" data-eth-component="TaskApprovalPanel">
70
+ <DecisionRequiredPanel
71
+ {...props}
72
+ title={title}
73
+ summary={
74
+ <div className="eth-task-approval-panel__body">
75
+ {summary ? <div className="eth-task-approval-panel__summary">{summary}</div> : null}
76
+ {notification ? (
77
+ <InlineNotification
78
+ className="eth-task-approval-panel__state"
79
+ severity={notification.severity}
80
+ title={notification.title}
81
+ >
82
+ {notification.description}
83
+ </InlineNotification>
84
+ ) : null}
85
+ <FormField
86
+ id={reasonId}
87
+ label="Decision reason"
88
+ helperText="Stored with approval audit metadata."
89
+ className="eth-task-approval-panel__reason"
90
+ >
91
+ <Textarea
92
+ value={reason}
93
+ rows={4}
94
+ placeholder="Summarize the approval rationale, rejection issue, or requested change."
95
+ onChange={(event) => setReason(event.currentTarget.value)}
96
+ />
97
+ </FormField>
98
+ <dl className="eth-task-approval-panel__audit">
99
+ <div>
100
+ <dt>Task</dt>
101
+ <dd>{taskRef ?? "-"}</dd>
102
+ </div>
103
+ {diffRef ? (
104
+ <div>
105
+ <dt>Diff</dt>
106
+ <dd>{diffRef}</dd>
107
+ </div>
108
+ ) : null}
109
+ <div>
110
+ <dt>Policy</dt>
111
+ <dd>{policy}</dd>
112
+ </div>
113
+ </dl>
114
+ </div>
115
+ }
116
+ description={description ?? "Review the task output and choose an approval outcome."}
117
+ riskLevel="high"
118
+ evidence={refEvidence(taskRef, diffRef, policyRef, evidence)}
119
+ options={[]}
120
+ footer={
121
+ <div className="eth-task-approval-panel__footer">
122
+ <div
123
+ className="eth-task-approval-panel__actions"
124
+ role="group"
125
+ aria-label="Approval actions"
126
+ >
127
+ <Button
128
+ intent="success"
129
+ disabled={isPending || !onApprove}
130
+ aria-pressed={state === "approved"}
131
+ onClick={() => onApprove?.(reason)}
132
+ >
133
+ Approve
134
+ </Button>
135
+ <Button
136
+ intent="danger"
137
+ disabled={isPending || !onReject}
138
+ aria-pressed={state === "rejected"}
139
+ onClick={() => onReject?.(reason)}
140
+ >
141
+ Reject
142
+ </Button>
143
+ <Button
144
+ intent="secondary"
145
+ disabled={isPending || !onRequestChanges}
146
+ onClick={() => onRequestChanges?.(reason)}
147
+ >
148
+ Request changes
149
+ </Button>
150
+ </div>
151
+ {footer ? (
152
+ <div className="eth-task-approval-panel__custom-footer">{footer}</div>
153
+ ) : null}
154
+ </div>
155
+ }
156
+ />
157
+ </section>
158
+ );
159
+ }
@@ -0,0 +1,71 @@
1
+ import * as React from "react";
2
+ import clsx from "clsx";
3
+ import { Badge, Surface, Tag } from "@echothink-ui/core";
4
+ import type { TaskCardProps } from "../types";
5
+ import { formatDateTime, severityForPriority } from "./utils";
6
+
7
+ export function TaskCard({ task, actions, onSelect, className, ...props }: TaskCardProps) {
8
+ const cardActions = actions?.map((action) => ({
9
+ ...action,
10
+ intent: action.intent ?? "tertiary"
11
+ }));
12
+
13
+ const interactiveProps = onSelect
14
+ ? {
15
+ role: "button",
16
+ tabIndex: 0,
17
+ onClick: () => onSelect(task),
18
+ onKeyDown: (event: React.KeyboardEvent<HTMLElement>) => {
19
+ if (event.key === "Enter" || event.key === " ") {
20
+ event.preventDefault();
21
+ onSelect(task);
22
+ }
23
+ }
24
+ }
25
+ : {};
26
+
27
+ return (
28
+ <Surface
29
+ {...props}
30
+ {...interactiveProps}
31
+ className={clsx("eth-task-card", onSelect && "eth-task-card--selectable", className)}
32
+ density="compact"
33
+ title={task.title}
34
+ subtitle={task.id}
35
+ description={task.description}
36
+ status={task.status}
37
+ actions={cardActions}
38
+ metadata={[
39
+ {
40
+ label: "Assignee",
41
+ value: task.assignee ? (
42
+ <span className="eth-task-card__assignee">{task.assignee}</span>
43
+ ) : (
44
+ "-"
45
+ )
46
+ },
47
+ {
48
+ label: "Due",
49
+ value: formatDateTime(task.dueAt)
50
+ },
51
+ {
52
+ label: "Priority",
53
+ value: task.priority ? (
54
+ <Badge severity={severityForPriority(task.priority)}>{task.priority}</Badge>
55
+ ) : (
56
+ "-"
57
+ )
58
+ }
59
+ ]}
60
+ data-eth-component="TaskCard"
61
+ >
62
+ {task.tags?.length ? (
63
+ <div className="eth-task-card__tags" aria-label="Task tags">
64
+ {task.tags.map((tag) => (
65
+ <Tag key={tag}>{tag}</Tag>
66
+ ))}
67
+ </div>
68
+ ) : null}
69
+ </Surface>
70
+ );
71
+ }
@@ -0,0 +1,54 @@
1
+ import { render, screen, within } from "@testing-library/react";
2
+ import { describe, expect, it } from "vitest";
3
+ import { TaskDependencyList } from "./TaskDependencyList";
4
+
5
+ describe("@echothink-ui/task TaskDependencyList", () => {
6
+ it("renders explicit prerequisites and dependents with counts and status labels", () => {
7
+ render(
8
+ <TaskDependencyList
9
+ blockedBy={[{ id: "task-118", title: "Legal approval", status: "approval-required" }]}
10
+ blocks={[{ id: "task-173", title: "Publish launch landing page", status: "queued" }]}
11
+ />
12
+ );
13
+
14
+ const blockedBy = screen.getByRole("region", { name: "Blocked by" });
15
+ const blocks = screen.getByRole("region", { name: "Blocks" });
16
+
17
+ expect(within(blockedBy).getByText("1 task")).toBeTruthy();
18
+ expect(within(blockedBy).getByText("Prerequisite")).toBeTruthy();
19
+ expect(within(blockedBy).getByText("Legal approval")).toBeTruthy();
20
+ expect(within(blockedBy).getByText("Approval Required")).toBeTruthy();
21
+ expect(within(blocks).getByText("1 task")).toBeTruthy();
22
+ expect(within(blocks).getByText("Dependent")).toBeTruthy();
23
+ expect(within(blocks).getByText("Publish launch landing page")).toBeTruthy();
24
+ expect(within(blocks).getByText("Queued")).toBeTruthy();
25
+ });
26
+
27
+ it("derives dependency groups from relationship metadata", () => {
28
+ render(
29
+ <TaskDependencyList
30
+ dependencies={[
31
+ {
32
+ id: "task-201",
33
+ title: "Security sign-off",
34
+ status: "pending-approval",
35
+ relationship: "blocked-by"
36
+ },
37
+ {
38
+ id: "task-202",
39
+ title: "Notify launch team",
40
+ status: "not-started",
41
+ relationship: "blocks"
42
+ }
43
+ ]}
44
+ />
45
+ );
46
+
47
+ expect(
48
+ within(screen.getByRole("region", { name: "Blocked by" })).getByText("task-201")
49
+ ).toBeTruthy();
50
+ expect(
51
+ within(screen.getByRole("region", { name: "Blocks" })).getByText("task-202")
52
+ ).toBeTruthy();
53
+ });
54
+ });
@@ -0,0 +1,105 @@
1
+ import * as React from "react";
2
+ import clsx from "clsx";
3
+ import { Badge, EmptyState } from "@echothink-ui/core";
4
+ import type { TaskDependency, TaskDependencyListProps } from "../types";
5
+ import { TaskStatusBadge } from "./TaskStatusBadge";
6
+
7
+ type DependencyRelationship = "blocks" | "blocked-by";
8
+
9
+ const groupDescriptions: Record<DependencyRelationship, string> = {
10
+ "blocked-by": "Prerequisite work that must clear before this task can move forward.",
11
+ blocks: "Downstream work waiting on this task."
12
+ };
13
+
14
+ function taskCountLabel(count: number) {
15
+ return `${count} task${count === 1 ? "" : "s"}`;
16
+ }
17
+
18
+ function relationshipLabel(relationship: DependencyRelationship) {
19
+ return relationship === "blocks" ? "Dependent" : "Prerequisite";
20
+ }
21
+
22
+ function DependencyGroup({
23
+ title,
24
+ relationship,
25
+ dependencies
26
+ }: {
27
+ title: string;
28
+ relationship: DependencyRelationship;
29
+ dependencies: TaskDependency[];
30
+ }) {
31
+ const headingId = React.useId();
32
+
33
+ if (!dependencies.length) return null;
34
+
35
+ return (
36
+ <section className="eth-task-dependencies__group" aria-labelledby={headingId}>
37
+ <div className="eth-task-dependencies__group-header">
38
+ <div className="eth-task-dependencies__group-heading">
39
+ <h4 id={headingId}>{title}</h4>
40
+ <p>{groupDescriptions[relationship]}</p>
41
+ </div>
42
+ <Badge>{taskCountLabel(dependencies.length)}</Badge>
43
+ </div>
44
+ <ul className="eth-task-dependencies__list">
45
+ {dependencies.map((dependency) => (
46
+ <li
47
+ key={dependency.id}
48
+ className={clsx(
49
+ "eth-task-dependencies__item",
50
+ `eth-task-dependencies__item--${dependency.status}`
51
+ )}
52
+ data-status={dependency.status}
53
+ >
54
+ <div className="eth-task-dependencies__item-main">
55
+ <span className="eth-task-dependencies__relationship">
56
+ {relationshipLabel(relationship)}
57
+ </span>
58
+ <span className="eth-task-dependencies__title">{dependency.title}</span>
59
+ <span className="eth-task-dependencies__id">{dependency.id}</span>
60
+ </div>
61
+ <TaskStatusBadge status={dependency.status} />
62
+ </li>
63
+ ))}
64
+ </ul>
65
+ </section>
66
+ );
67
+ }
68
+
69
+ export function TaskDependencyList({
70
+ dependencies = [],
71
+ blocks = [],
72
+ blockedBy = [],
73
+ emptyTitle = "No dependencies",
74
+ className,
75
+ ...props
76
+ }: TaskDependencyListProps) {
77
+ const derivedBlocks = dependencies.filter((dependency) => dependency.relationship === "blocks");
78
+ const derivedBlockedBy = dependencies.filter(
79
+ (dependency) => dependency.relationship !== "blocks"
80
+ );
81
+ const blockList = blocks.length ? blocks : derivedBlocks;
82
+ const blockedByList = blockedBy.length ? blockedBy : derivedBlockedBy;
83
+ const hasDependencies = blockList.length || blockedByList.length;
84
+
85
+ return (
86
+ <div
87
+ {...props}
88
+ className={clsx("eth-task-dependencies", className)}
89
+ data-eth-component="TaskDependencyList"
90
+ >
91
+ {hasDependencies ? (
92
+ <>
93
+ <DependencyGroup
94
+ title="Blocked by"
95
+ relationship="blocked-by"
96
+ dependencies={blockedByList}
97
+ />
98
+ <DependencyGroup title="Blocks" relationship="blocks" dependencies={blockList} />
99
+ </>
100
+ ) : (
101
+ <EmptyState title={emptyTitle} />
102
+ )}
103
+ </div>
104
+ );
105
+ }
@@ -0,0 +1,49 @@
1
+ import { render, screen } from "@testing-library/react";
2
+ import { describe, expect, it } from "vitest";
3
+ import { TaskDetailPanel } from "./TaskDetailPanel";
4
+
5
+ const task = {
6
+ id: "t1",
7
+ title: "Create landing page hero",
8
+ assignee: "JD",
9
+ dueAt: "2026-05-30T13:00:00Z",
10
+ status: "in-progress" as const,
11
+ priority: "high" as const,
12
+ description: "Design hero with social-proof modules and animated stat strip."
13
+ };
14
+
15
+ describe("TaskDetailPanel", () => {
16
+ it("expands populated dependencies and history without duplicating actions", () => {
17
+ render(
18
+ <TaskDetailPanel
19
+ task={task}
20
+ dependencies={[
21
+ {
22
+ id: "d1",
23
+ title: "Approve brief",
24
+ status: "approval-required",
25
+ relationship: "blocked-by"
26
+ }
27
+ ]}
28
+ events={[
29
+ {
30
+ id: "h1",
31
+ timestamp: "2026-05-25T10:00:00Z",
32
+ actor: "JD",
33
+ kind: "status_change",
34
+ summary: "Moved to in progress"
35
+ }
36
+ ]}
37
+ actions={[{ id: "complete", label: "Complete", intent: "primary" }]}
38
+ />
39
+ );
40
+
41
+ expect(screen.getByText("Dependencies").closest("details")?.hasAttribute("open")).toBe(
42
+ true
43
+ );
44
+ expect(screen.getByText("History").closest("details")?.hasAttribute("open")).toBe(
45
+ true
46
+ );
47
+ expect(screen.getAllByRole("button", { name: "Complete" })).toHaveLength(1);
48
+ });
49
+ });