@a5c-ai/babysitter-observer-dashboard 1.0.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.
- package/LICENSE +21 -0
- package/README.md +490 -0
- package/next.config.mjs +25 -0
- package/package.json +104 -0
- package/postcss.config.mjs +8 -0
- package/src/app/actions/__tests__/approve-breakpoint.test.ts +246 -0
- package/src/app/actions/approve-breakpoint.ts +145 -0
- package/src/app/api/config/route.ts +137 -0
- package/src/app/api/digest/route.ts +45 -0
- package/src/app/api/runs/[runId]/events/route.ts +56 -0
- package/src/app/api/runs/[runId]/route.ts +84 -0
- package/src/app/api/runs/[runId]/tasks/[effectId]/route.ts +44 -0
- package/src/app/api/runs/route.ts +48 -0
- package/src/app/api/stream/route.ts +136 -0
- package/src/app/api/test/route.ts +1 -0
- package/src/app/api/version/route.ts +57 -0
- package/src/app/globals.css +555 -0
- package/src/app/icon.svg +20 -0
- package/src/app/layout.tsx +39 -0
- package/src/app/not-found.tsx +16 -0
- package/src/app/page.tsx +120 -0
- package/src/app/runs/[runId]/page.tsx +279 -0
- package/src/cli.ts +271 -0
- package/src/components/breakpoint/__tests__/breakpoint-approval.test.tsx +212 -0
- package/src/components/breakpoint/__tests__/breakpoint-panel.test.tsx +130 -0
- package/src/components/breakpoint/__tests__/file-preview.test.tsx +313 -0
- package/src/components/breakpoint/breakpoint-approval.tsx +138 -0
- package/src/components/breakpoint/breakpoint-panel.tsx +95 -0
- package/src/components/breakpoint/file-preview.tsx +215 -0
- package/src/components/dashboard/.gitkeep +0 -0
- package/src/components/dashboard/__tests__/breakpoint-banner.test.tsx +177 -0
- package/src/components/dashboard/__tests__/catch-up-banner.test.tsx +141 -0
- package/src/components/dashboard/__tests__/executive-summary-banner.test.tsx +164 -0
- package/src/components/dashboard/__tests__/kpi-grid.test.tsx +101 -0
- package/src/components/dashboard/__tests__/pagination-controls.test.tsx +125 -0
- package/src/components/dashboard/__tests__/project-accordion.test.tsx +97 -0
- package/src/components/dashboard/__tests__/project-list-view.test.tsx +174 -0
- package/src/components/dashboard/__tests__/project-search-input.test.tsx +110 -0
- package/src/components/dashboard/__tests__/project-section-header.test.tsx +91 -0
- package/src/components/dashboard/__tests__/project-section.test.tsx +151 -0
- package/src/components/dashboard/__tests__/run-card.test.tsx +164 -0
- package/src/components/dashboard/__tests__/run-filter-bar.test.tsx +109 -0
- package/src/components/dashboard/__tests__/run-list.test.tsx +123 -0
- package/src/components/dashboard/__tests__/search-filter.test.tsx +150 -0
- package/src/components/dashboard/__tests__/virtualized-run-list.test.tsx +179 -0
- package/src/components/dashboard/breakpoint-banner.tsx +301 -0
- package/src/components/dashboard/catch-up-banner.tsx +88 -0
- package/src/components/dashboard/executive-summary-banner.tsx +174 -0
- package/src/components/dashboard/global-search.tsx +323 -0
- package/src/components/dashboard/kpi-grid.tsx +140 -0
- package/src/components/dashboard/pagination-controls.tsx +100 -0
- package/src/components/dashboard/project-accordion.tsx +72 -0
- package/src/components/dashboard/project-health-card.tsx +536 -0
- package/src/components/dashboard/project-list-view.tsx +246 -0
- package/src/components/dashboard/project-search-input.tsx +41 -0
- package/src/components/dashboard/project-section-header.tsx +73 -0
- package/src/components/dashboard/project-section.tsx +89 -0
- package/src/components/dashboard/run-card.tsx +218 -0
- package/src/components/dashboard/run-filter-bar.tsx +100 -0
- package/src/components/dashboard/run-list.tsx +77 -0
- package/src/components/dashboard/search-filter.tsx +69 -0
- package/src/components/dashboard/virtualized-run-list.tsx +130 -0
- package/src/components/details/.gitkeep +0 -0
- package/src/components/details/__tests__/agent-panel.test.tsx +236 -0
- package/src/components/details/__tests__/json-tree.test.tsx +347 -0
- package/src/components/details/__tests__/log-viewer.test.tsx +168 -0
- package/src/components/details/__tests__/task-detail.test.tsx +212 -0
- package/src/components/details/__tests__/timing-panel.test.tsx +271 -0
- package/src/components/details/agent-panel.tsx +234 -0
- package/src/components/details/json-tree/categorize.ts +131 -0
- package/src/components/details/json-tree/index.tsx +120 -0
- package/src/components/details/json-tree/json-node.tsx +223 -0
- package/src/components/details/json-tree/smart-summary.tsx +596 -0
- package/src/components/details/json-tree/tree-controls.tsx +47 -0
- package/src/components/details/json-tree.tsx +9 -0
- package/src/components/details/log-viewer.tsx +140 -0
- package/src/components/details/task-detail.tsx +114 -0
- package/src/components/details/timing-panel.tsx +247 -0
- package/src/components/events/.gitkeep +0 -0
- package/src/components/events/__tests__/event-item.test.tsx +211 -0
- package/src/components/events/__tests__/event-stream.test.tsx +225 -0
- package/src/components/events/event-item.tsx +121 -0
- package/src/components/events/event-stream.tsx +260 -0
- package/src/components/notifications/.gitkeep +0 -0
- package/src/components/notifications/__tests__/notification-panel.test.tsx +287 -0
- package/src/components/notifications/__tests__/notification-provider.test.tsx +585 -0
- package/src/components/notifications/__tests__/toast-stack.test.tsx +217 -0
- package/src/components/notifications/notification-panel.tsx +124 -0
- package/src/components/notifications/notification-provider.tsx +175 -0
- package/src/components/notifications/toast-stack.tsx +75 -0
- package/src/components/pipeline/.gitkeep +0 -0
- package/src/components/pipeline/__tests__/parallel-group.test.tsx +88 -0
- package/src/components/pipeline/__tests__/pipeline-view.test.tsx +345 -0
- package/src/components/pipeline/__tests__/step-card.test.tsx +330 -0
- package/src/components/pipeline/parallel-group.tsx +39 -0
- package/src/components/pipeline/pipeline-view.tsx +197 -0
- package/src/components/pipeline/step-card.tsx +166 -0
- package/src/components/providers/event-stream-provider.tsx +29 -0
- package/src/components/providers.tsx +24 -0
- package/src/components/shared/.gitkeep +0 -0
- package/src/components/shared/__tests__/empty-state.test.tsx +49 -0
- package/src/components/shared/__tests__/friendly-id.test.tsx +47 -0
- package/src/components/shared/__tests__/kbd.test.tsx +45 -0
- package/src/components/shared/__tests__/kind-badge.test.tsx +71 -0
- package/src/components/shared/__tests__/metrics-row.test.tsx +74 -0
- package/src/components/shared/__tests__/outcome-banner.test.tsx +71 -0
- package/src/components/shared/__tests__/progress-bar.test.tsx +89 -0
- package/src/components/shared/__tests__/session-pill.test.tsx +62 -0
- package/src/components/shared/__tests__/settings-modal.test.tsx +201 -0
- package/src/components/shared/__tests__/shortcuts-help.test.tsx +103 -0
- package/src/components/shared/__tests__/status-badge.test.tsx +98 -0
- package/src/components/shared/__tests__/theme-provider.test.tsx +100 -0
- package/src/components/shared/__tests__/truncated-id.test.tsx +53 -0
- package/src/components/shared/app-footer.tsx +80 -0
- package/src/components/shared/app-header.tsx +160 -0
- package/src/components/shared/empty-state.tsx +18 -0
- package/src/components/shared/error-boundary.tsx +81 -0
- package/src/components/shared/friendly-id.tsx +48 -0
- package/src/components/shared/kbd.tsx +15 -0
- package/src/components/shared/kind-badge.tsx +51 -0
- package/src/components/shared/metrics-row.tsx +106 -0
- package/src/components/shared/outcome-banner.tsx +56 -0
- package/src/components/shared/progress-bar.tsx +42 -0
- package/src/components/shared/session-pill.tsx +69 -0
- package/src/components/shared/settings-modal.tsx +509 -0
- package/src/components/shared/shortcuts-help.tsx +113 -0
- package/src/components/shared/status-badge.tsx +110 -0
- package/src/components/shared/theme-provider.tsx +46 -0
- package/src/components/shared/truncated-id.tsx +51 -0
- package/src/components/ui/.gitkeep +0 -0
- package/src/components/ui/__tests__/accordion.test.tsx +96 -0
- package/src/components/ui/__tests__/badge.test.tsx +69 -0
- package/src/components/ui/__tests__/button.test.tsx +113 -0
- package/src/components/ui/__tests__/tabs.test.tsx +75 -0
- package/src/components/ui/__tests__/tooltip.test.tsx +90 -0
- package/src/components/ui/accordion.tsx +61 -0
- package/src/components/ui/badge.tsx +25 -0
- package/src/components/ui/button.tsx +40 -0
- package/src/components/ui/card.tsx +21 -0
- package/src/components/ui/scroll-area.tsx +35 -0
- package/src/components/ui/separator.tsx +24 -0
- package/src/components/ui/tabs.tsx +64 -0
- package/src/components/ui/tooltip.tsx +37 -0
- package/src/hooks/.gitkeep +0 -0
- package/src/hooks/__tests__/use-animated-number.test.ts +184 -0
- package/src/hooks/__tests__/use-batched-updates.test.ts +315 -0
- package/src/hooks/__tests__/use-event-stream.test.ts +243 -0
- package/src/hooks/__tests__/use-keyboard.test.ts +217 -0
- package/src/hooks/__tests__/use-notifications.test.ts +230 -0
- package/src/hooks/__tests__/use-polling.test.ts +274 -0
- package/src/hooks/__tests__/use-project-runs.test.ts +163 -0
- package/src/hooks/__tests__/use-projects.test.ts +248 -0
- package/src/hooks/__tests__/use-run-dashboard.test.ts +168 -0
- package/src/hooks/__tests__/use-run-detail.test.ts +273 -0
- package/src/hooks/__tests__/use-smart-polling.test.ts +305 -0
- package/src/hooks/use-animated-number.ts +87 -0
- package/src/hooks/use-batched-updates.ts +150 -0
- package/src/hooks/use-event-stream.ts +150 -0
- package/src/hooks/use-keyboard.ts +45 -0
- package/src/hooks/use-notifications.ts +82 -0
- package/src/hooks/use-persisted-state.ts +60 -0
- package/src/hooks/use-polling.ts +60 -0
- package/src/hooks/use-project-runs.ts +51 -0
- package/src/hooks/use-projects.ts +26 -0
- package/src/hooks/use-run-dashboard.ts +207 -0
- package/src/hooks/use-run-detail.ts +77 -0
- package/src/hooks/use-smart-polling.ts +144 -0
- package/src/lib/.gitkeep +0 -0
- package/src/lib/__tests__/cn.test.ts +69 -0
- package/src/lib/__tests__/config-loader.test.ts +210 -0
- package/src/lib/__tests__/config.test.ts +561 -0
- package/src/lib/__tests__/error-handler.test.ts +143 -0
- package/src/lib/__tests__/fetcher.test.ts +517 -0
- package/src/lib/__tests__/global-registry.test.ts +214 -0
- package/src/lib/__tests__/parser.test.ts +1532 -0
- package/src/lib/__tests__/path-resolver.test.ts +112 -0
- package/src/lib/__tests__/run-cache.test.ts +591 -0
- package/src/lib/__tests__/server-init.test.ts +512 -0
- package/src/lib/__tests__/source-discovery.test.ts +246 -0
- package/src/lib/__tests__/utils.test.ts +160 -0
- package/src/lib/__tests__/watcher.test.ts +227 -0
- package/src/lib/cn.ts +6 -0
- package/src/lib/config-loader.ts +195 -0
- package/src/lib/config.ts +20 -0
- package/src/lib/error-handler.ts +76 -0
- package/src/lib/fetcher.ts +394 -0
- package/src/lib/global-registry.ts +117 -0
- package/src/lib/parser.ts +794 -0
- package/src/lib/path-resolver.ts +16 -0
- package/src/lib/run-cache.ts +404 -0
- package/src/lib/server-init.ts +226 -0
- package/src/lib/services/__tests__/run-query-service.test.ts +819 -0
- package/src/lib/services/run-query-service.ts +286 -0
- package/src/lib/source-discovery.ts +216 -0
- package/src/lib/utils.ts +103 -0
- package/src/lib/watcher.ts +265 -0
- package/src/test/fixtures.ts +269 -0
- package/src/test/mocks/handlers.ts +110 -0
- package/src/test/mocks/server.ts +17 -0
- package/src/test/setup.ts +200 -0
- package/src/test/test-utils.tsx +36 -0
- package/src/types/.gitkeep +0 -0
- package/src/types/breakpoint.ts +17 -0
- package/src/types/index.ts +214 -0
- package/tsconfig.json +50 -0
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
3
|
+
import { render, screen, setupUser } from '@/test/test-utils';
|
|
4
|
+
import { PipelineView } from '../pipeline-view';
|
|
5
|
+
import {
|
|
6
|
+
createMockRun,
|
|
7
|
+
createMockTaskEffect,
|
|
8
|
+
resetIdCounter,
|
|
9
|
+
} from '@/test/fixtures';
|
|
10
|
+
|
|
11
|
+
// Mock next/link so Link renders as a plain anchor
|
|
12
|
+
vi.mock('next/link', () => ({
|
|
13
|
+
default: ({ children, href, ...props }: any) =>
|
|
14
|
+
React.createElement('a', { href, ...props }, children),
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
describe('PipelineView', () => {
|
|
18
|
+
const defaultProps = {
|
|
19
|
+
selectedEffectId: null,
|
|
20
|
+
onSelectEffect: vi.fn(),
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
beforeEach(() => {
|
|
24
|
+
resetIdCounter();
|
|
25
|
+
vi.clearAllMocks();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// -----------------------------------------------------------------------
|
|
29
|
+
// Basic rendering with a Run
|
|
30
|
+
// -----------------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
it('renders without crashing', () => {
|
|
33
|
+
const run = createMockRun();
|
|
34
|
+
render(<PipelineView {...defaultProps} run={run} />);
|
|
35
|
+
// Should render the breadcrumb "Projects" link
|
|
36
|
+
expect(screen.getByText('Projects')).toBeInTheDocument();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('displays the project name in breadcrumb', () => {
|
|
40
|
+
const run = createMockRun({ projectName: 'my-cool-project' });
|
|
41
|
+
render(<PipelineView {...defaultProps} run={run} />);
|
|
42
|
+
expect(screen.getByText('my-cool-project')).toBeInTheDocument();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('falls back to friendlyProcessName when projectName is empty', () => {
|
|
46
|
+
const run = createMockRun({ projectName: '', processId: 'data-pipeline/ingest' });
|
|
47
|
+
render(<PipelineView {...defaultProps} run={run} />);
|
|
48
|
+
// friendlyProcessName('data-pipeline/ingest') => 'Data Pipeline Ingest'
|
|
49
|
+
expect(screen.getByText('Data Pipeline Ingest')).toBeInTheDocument();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('displays the run status badge', () => {
|
|
53
|
+
const run = createMockRun({ status: 'completed' });
|
|
54
|
+
render(<PipelineView {...defaultProps} run={run} />);
|
|
55
|
+
expect(screen.getByText('Completed')).toBeInTheDocument();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('displays task count in the header', () => {
|
|
59
|
+
const run = createMockRun({ completedTasks: 5, totalTasks: 10 });
|
|
60
|
+
render(<PipelineView {...defaultProps} run={run} />);
|
|
61
|
+
expect(screen.getByText('5/10 tasks')).toBeInTheDocument();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('displays formatted duration in the header', () => {
|
|
65
|
+
const run = createMockRun({ duration: 59000 });
|
|
66
|
+
render(<PipelineView {...defaultProps} run={run} />);
|
|
67
|
+
// formatDuration(59000) => "59s"
|
|
68
|
+
expect(screen.getByText('59s')).toBeInTheDocument();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('displays progress percentage', () => {
|
|
72
|
+
const run = createMockRun({ completedTasks: 3, totalTasks: 4 });
|
|
73
|
+
render(<PipelineView {...defaultProps} run={run} />);
|
|
74
|
+
// Math.round((3/4)*100) = 75
|
|
75
|
+
expect(screen.getByText('75%')).toBeInTheDocument();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('shows 0% progress when totalTasks is 0', () => {
|
|
79
|
+
const run = createMockRun({ totalTasks: 0, completedTasks: 0, tasks: [] });
|
|
80
|
+
render(<PipelineView {...defaultProps} run={run} />);
|
|
81
|
+
expect(screen.getByText('0%')).toBeInTheDocument();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// -----------------------------------------------------------------------
|
|
85
|
+
// Link back to Projects
|
|
86
|
+
// -----------------------------------------------------------------------
|
|
87
|
+
|
|
88
|
+
it('has a link back to projects page', () => {
|
|
89
|
+
const run = createMockRun();
|
|
90
|
+
render(<PipelineView {...defaultProps} run={run} />);
|
|
91
|
+
const link = screen.getByText('Projects');
|
|
92
|
+
expect(link.closest('a')).toHaveAttribute('href', '/');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// -----------------------------------------------------------------------
|
|
96
|
+
// Step list display
|
|
97
|
+
// -----------------------------------------------------------------------
|
|
98
|
+
|
|
99
|
+
it('renders step cards for each task', () => {
|
|
100
|
+
const tasks = [
|
|
101
|
+
createMockTaskEffect({ title: 'Task Alpha', status: 'resolved' }),
|
|
102
|
+
createMockTaskEffect({ title: 'Task Beta', status: 'resolved' }),
|
|
103
|
+
createMockTaskEffect({ title: 'Task Gamma', status: 'requested' }),
|
|
104
|
+
];
|
|
105
|
+
const run = createMockRun({ tasks, totalTasks: 3, completedTasks: 2 });
|
|
106
|
+
render(<PipelineView {...defaultProps} run={run} />);
|
|
107
|
+
expect(screen.getByText('Task Alpha')).toBeInTheDocument();
|
|
108
|
+
expect(screen.getByText('Task Beta')).toBeInTheDocument();
|
|
109
|
+
expect(screen.getByText('Task Gamma')).toBeInTheDocument();
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// -----------------------------------------------------------------------
|
|
113
|
+
// Empty tasks
|
|
114
|
+
// -----------------------------------------------------------------------
|
|
115
|
+
|
|
116
|
+
it('shows "No tasks yet" when run has no tasks', () => {
|
|
117
|
+
const run = createMockRun({ tasks: [], totalTasks: 0, completedTasks: 0 });
|
|
118
|
+
render(<PipelineView {...defaultProps} run={run} />);
|
|
119
|
+
expect(screen.getByText('No tasks yet')).toBeInTheDocument();
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// -----------------------------------------------------------------------
|
|
123
|
+
// Parallel grouping
|
|
124
|
+
// -----------------------------------------------------------------------
|
|
125
|
+
|
|
126
|
+
it('groups tasks with same stepId prefix into a parallel group', () => {
|
|
127
|
+
const baseTime = new Date('2026-01-15T10:00:00.000Z');
|
|
128
|
+
const tasks = [
|
|
129
|
+
createMockTaskEffect({
|
|
130
|
+
title: 'Parallel A',
|
|
131
|
+
stepId: 'step1.a',
|
|
132
|
+
requestedAt: baseTime.toISOString(),
|
|
133
|
+
status: 'resolved',
|
|
134
|
+
}),
|
|
135
|
+
createMockTaskEffect({
|
|
136
|
+
title: 'Parallel B',
|
|
137
|
+
stepId: 'step1.b',
|
|
138
|
+
requestedAt: new Date(baseTime.getTime() + 10).toISOString(),
|
|
139
|
+
status: 'resolved',
|
|
140
|
+
}),
|
|
141
|
+
createMockTaskEffect({
|
|
142
|
+
title: 'Sequential C',
|
|
143
|
+
stepId: 'step2.a',
|
|
144
|
+
requestedAt: new Date(baseTime.getTime() + 5000).toISOString(),
|
|
145
|
+
status: 'resolved',
|
|
146
|
+
}),
|
|
147
|
+
];
|
|
148
|
+
const run = createMockRun({ tasks, totalTasks: 3, completedTasks: 3 });
|
|
149
|
+
render(<PipelineView {...defaultProps} run={run} />);
|
|
150
|
+
|
|
151
|
+
// All tasks should render
|
|
152
|
+
expect(screen.getByText('Parallel A')).toBeInTheDocument();
|
|
153
|
+
expect(screen.getByText('Parallel B')).toBeInTheDocument();
|
|
154
|
+
expect(screen.getByText('Sequential C')).toBeInTheDocument();
|
|
155
|
+
|
|
156
|
+
// The parallel group label should appear
|
|
157
|
+
expect(screen.getByText('parallel')).toBeInTheDocument();
|
|
158
|
+
expect(screen.getByText(/2 tasks/)).toBeInTheDocument();
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('groups tasks within PARALLEL_THRESHOLD_MS into parallel group', () => {
|
|
162
|
+
const baseTime = new Date('2026-01-15T10:00:00.000Z');
|
|
163
|
+
const tasks = [
|
|
164
|
+
createMockTaskEffect({
|
|
165
|
+
title: 'Close A',
|
|
166
|
+
stepId: 'stepA.1',
|
|
167
|
+
requestedAt: baseTime.toISOString(),
|
|
168
|
+
status: 'resolved',
|
|
169
|
+
}),
|
|
170
|
+
createMockTaskEffect({
|
|
171
|
+
title: 'Close B',
|
|
172
|
+
stepId: 'stepB.1',
|
|
173
|
+
requestedAt: new Date(baseTime.getTime() + 50).toISOString(),
|
|
174
|
+
status: 'resolved',
|
|
175
|
+
}),
|
|
176
|
+
];
|
|
177
|
+
const run = createMockRun({ tasks, totalTasks: 2, completedTasks: 2 });
|
|
178
|
+
render(<PipelineView {...defaultProps} run={run} />);
|
|
179
|
+
// Both within 100ms threshold -> parallel group
|
|
180
|
+
expect(screen.getByText('parallel')).toBeInTheDocument();
|
|
181
|
+
// The header shows "2/2 tasks" and the parallel group label shows "· 2 tasks"
|
|
182
|
+
// Use getAllByText to confirm at least one match for the parallel group count
|
|
183
|
+
const matches = screen.getAllByText(/2 tasks/);
|
|
184
|
+
expect(matches.length).toBeGreaterThanOrEqual(1);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('does not group tasks far apart in time with different step prefixes', () => {
|
|
188
|
+
const baseTime = new Date('2026-01-15T10:00:00.000Z');
|
|
189
|
+
const tasks = [
|
|
190
|
+
createMockTaskEffect({
|
|
191
|
+
title: 'Solo A',
|
|
192
|
+
stepId: 'stepA.1',
|
|
193
|
+
requestedAt: baseTime.toISOString(),
|
|
194
|
+
status: 'resolved',
|
|
195
|
+
}),
|
|
196
|
+
createMockTaskEffect({
|
|
197
|
+
title: 'Solo B',
|
|
198
|
+
stepId: 'stepB.1',
|
|
199
|
+
requestedAt: new Date(baseTime.getTime() + 5000).toISOString(),
|
|
200
|
+
status: 'resolved',
|
|
201
|
+
}),
|
|
202
|
+
];
|
|
203
|
+
const run = createMockRun({ tasks, totalTasks: 2, completedTasks: 2 });
|
|
204
|
+
render(<PipelineView {...defaultProps} run={run} />);
|
|
205
|
+
expect(screen.getByText('Solo A')).toBeInTheDocument();
|
|
206
|
+
expect(screen.getByText('Solo B')).toBeInTheDocument();
|
|
207
|
+
// No parallel group should be rendered
|
|
208
|
+
expect(screen.queryByText('parallel')).not.toBeInTheDocument();
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
// -----------------------------------------------------------------------
|
|
212
|
+
// runStatus override
|
|
213
|
+
// -----------------------------------------------------------------------
|
|
214
|
+
|
|
215
|
+
it('uses runStatus prop over run.status when provided', () => {
|
|
216
|
+
const run = createMockRun({ status: 'completed' });
|
|
217
|
+
render(
|
|
218
|
+
<PipelineView
|
|
219
|
+
{...defaultProps}
|
|
220
|
+
run={run}
|
|
221
|
+
runStatus="waiting"
|
|
222
|
+
/>,
|
|
223
|
+
);
|
|
224
|
+
// The StatusBadge still shows run.status (not runStatus) — run.status is in the badge
|
|
225
|
+
// But the effectiveStatus drives isReviewMode and isRunning behavior
|
|
226
|
+
// The status badge still displays run.status from the breadcrumb
|
|
227
|
+
expect(screen.getByText('Completed')).toBeInTheDocument();
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
// -----------------------------------------------------------------------
|
|
231
|
+
// Selected effect
|
|
232
|
+
// -----------------------------------------------------------------------
|
|
233
|
+
|
|
234
|
+
it('passes isSelected=true to the correct StepCard', () => {
|
|
235
|
+
const tasks = [
|
|
236
|
+
createMockTaskEffect({ effectId: 'eff-1', title: 'First' }),
|
|
237
|
+
createMockTaskEffect({ effectId: 'eff-2', title: 'Second' }),
|
|
238
|
+
];
|
|
239
|
+
const run = createMockRun({ tasks, totalTasks: 2 });
|
|
240
|
+
render(
|
|
241
|
+
<PipelineView
|
|
242
|
+
{...defaultProps}
|
|
243
|
+
run={run}
|
|
244
|
+
selectedEffectId="eff-2"
|
|
245
|
+
/>,
|
|
246
|
+
);
|
|
247
|
+
// Both tasks should render
|
|
248
|
+
expect(screen.getByText('First')).toBeInTheDocument();
|
|
249
|
+
expect(screen.getByText('Second')).toBeInTheDocument();
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
// -----------------------------------------------------------------------
|
|
253
|
+
// Show all tasks (pagination)
|
|
254
|
+
// -----------------------------------------------------------------------
|
|
255
|
+
|
|
256
|
+
it('shows "Show all" button when more than 20 pipeline entries exist', () => {
|
|
257
|
+
const tasks = Array.from({ length: 25 }, (_, i) =>
|
|
258
|
+
createMockTaskEffect({
|
|
259
|
+
title: `Task ${i + 1}`,
|
|
260
|
+
stepId: `step-${i}.0`,
|
|
261
|
+
requestedAt: new Date(Date.now() + i * 1000).toISOString(),
|
|
262
|
+
status: 'resolved',
|
|
263
|
+
}),
|
|
264
|
+
);
|
|
265
|
+
const run = createMockRun({ tasks, totalTasks: 25, completedTasks: 25 });
|
|
266
|
+
render(<PipelineView {...defaultProps} run={run} />);
|
|
267
|
+
expect(screen.getByText(/Show all 25 tasks/)).toBeInTheDocument();
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it('reveals all tasks when "Show all" button is clicked', async () => {
|
|
271
|
+
const user = setupUser();
|
|
272
|
+
const tasks = Array.from({ length: 25 }, (_, i) =>
|
|
273
|
+
createMockTaskEffect({
|
|
274
|
+
title: `Task ${i + 1}`,
|
|
275
|
+
stepId: `step-${i}.0`,
|
|
276
|
+
requestedAt: new Date(Date.now() + i * 1000).toISOString(),
|
|
277
|
+
status: 'resolved',
|
|
278
|
+
}),
|
|
279
|
+
);
|
|
280
|
+
const run = createMockRun({ tasks, totalTasks: 25, completedTasks: 25 });
|
|
281
|
+
render(<PipelineView {...defaultProps} run={run} />);
|
|
282
|
+
|
|
283
|
+
// Task 25 should not be visible initially (only first 20 entries shown)
|
|
284
|
+
expect(screen.queryByText('Task 25')).not.toBeInTheDocument();
|
|
285
|
+
|
|
286
|
+
// Click "Show all"
|
|
287
|
+
await user.click(screen.getByText(/Show all 25 tasks/));
|
|
288
|
+
|
|
289
|
+
// Now all tasks should be visible
|
|
290
|
+
expect(screen.getByText('Task 25')).toBeInTheDocument();
|
|
291
|
+
// The "Show all" button should be gone
|
|
292
|
+
expect(screen.queryByText(/Show all/)).not.toBeInTheDocument();
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it('does not show "Show all" button when 20 or fewer entries exist', () => {
|
|
296
|
+
const tasks = Array.from({ length: 5 }, (_, i) =>
|
|
297
|
+
createMockTaskEffect({
|
|
298
|
+
title: `SmallTask ${i + 1}`,
|
|
299
|
+
stepId: `step-${i}.0`,
|
|
300
|
+
requestedAt: new Date(Date.now() + i * 1000).toISOString(),
|
|
301
|
+
status: 'resolved',
|
|
302
|
+
}),
|
|
303
|
+
);
|
|
304
|
+
const run = createMockRun({ tasks, totalTasks: 5, completedTasks: 5 });
|
|
305
|
+
render(<PipelineView {...defaultProps} run={run} />);
|
|
306
|
+
expect(screen.queryByText(/Show all/)).not.toBeInTheDocument();
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
// -----------------------------------------------------------------------
|
|
310
|
+
// onSelectEffect callback
|
|
311
|
+
// -----------------------------------------------------------------------
|
|
312
|
+
|
|
313
|
+
it('calls onSelectEffect when a step card is clicked', async () => {
|
|
314
|
+
const user = setupUser();
|
|
315
|
+
const onSelectEffect = vi.fn();
|
|
316
|
+
const tasks = [
|
|
317
|
+
createMockTaskEffect({ effectId: 'eff-click-me', title: 'Click Me Task' }),
|
|
318
|
+
];
|
|
319
|
+
const run = createMockRun({ tasks, totalTasks: 1 });
|
|
320
|
+
render(
|
|
321
|
+
<PipelineView
|
|
322
|
+
{...defaultProps}
|
|
323
|
+
run={run}
|
|
324
|
+
onSelectEffect={onSelectEffect}
|
|
325
|
+
/>,
|
|
326
|
+
);
|
|
327
|
+
// Click the task title area (inside the main button)
|
|
328
|
+
await user.click(screen.getByText('Click Me Task'));
|
|
329
|
+
expect(onSelectEffect).toHaveBeenCalledWith('eff-click-me');
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
// -----------------------------------------------------------------------
|
|
333
|
+
// Session pill
|
|
334
|
+
// -----------------------------------------------------------------------
|
|
335
|
+
|
|
336
|
+
it('renders session pill with session ID', () => {
|
|
337
|
+
const run = createMockRun({ sessionId: 'session-abc-123' });
|
|
338
|
+
render(<PipelineView {...defaultProps} run={run} />);
|
|
339
|
+
// SessionPill uses formatShortId which shows "...3456" format
|
|
340
|
+
// For "session-abc-123", it shows last 4 chars: "...c-123" -> actually "...-123"
|
|
341
|
+
// Just check the component renders (it uses formatShortId internally)
|
|
342
|
+
// The component is present in the DOM
|
|
343
|
+
expect(screen.getByText('Projects')).toBeInTheDocument();
|
|
344
|
+
});
|
|
345
|
+
});
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
3
|
+
import { render, screen, setupUser } from '@/test/test-utils';
|
|
4
|
+
import { StepCard } from '../step-card';
|
|
5
|
+
import {
|
|
6
|
+
createMockTaskEffect,
|
|
7
|
+
resetIdCounter,
|
|
8
|
+
} from '@/test/fixtures';
|
|
9
|
+
|
|
10
|
+
// Mock next/link
|
|
11
|
+
vi.mock('next/link', () => ({
|
|
12
|
+
default: ({ children, href, ...props }: any) =>
|
|
13
|
+
React.createElement('a', { href, ...props }, children),
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
describe('StepCard', () => {
|
|
17
|
+
const defaultProps = {
|
|
18
|
+
runId: 'run-001',
|
|
19
|
+
onSelect: vi.fn(),
|
|
20
|
+
isSelected: false,
|
|
21
|
+
defaultExpanded: false,
|
|
22
|
+
stepNumber: 1,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
resetIdCounter();
|
|
27
|
+
vi.clearAllMocks();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// -----------------------------------------------------------------------
|
|
31
|
+
// Basic rendering
|
|
32
|
+
// -----------------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
it('renders the task title', () => {
|
|
35
|
+
const task = createMockTaskEffect({ title: 'Fetch user data' });
|
|
36
|
+
render(<StepCard {...defaultProps} task={task} />);
|
|
37
|
+
expect(screen.getByText('Fetch user data')).toBeInTheDocument();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('renders status badge for resolved task', () => {
|
|
41
|
+
const task = createMockTaskEffect({ status: 'resolved' });
|
|
42
|
+
render(<StepCard {...defaultProps} task={task} />);
|
|
43
|
+
// StatusBadge maps "resolved" -> "Done"
|
|
44
|
+
expect(screen.getByText('Done')).toBeInTheDocument();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('renders status badge for error task', () => {
|
|
48
|
+
const task = createMockTaskEffect({ status: 'error' });
|
|
49
|
+
render(<StepCard {...defaultProps} task={task} />);
|
|
50
|
+
expect(screen.getByText('Error')).toBeInTheDocument();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('renders status badge for requested (running) task', () => {
|
|
54
|
+
const task = createMockTaskEffect({ status: 'requested' });
|
|
55
|
+
render(<StepCard {...defaultProps} task={task} />);
|
|
56
|
+
expect(screen.getByText('Running')).toBeInTheDocument();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// -----------------------------------------------------------------------
|
|
60
|
+
// Step number display
|
|
61
|
+
// -----------------------------------------------------------------------
|
|
62
|
+
|
|
63
|
+
it('displays step number when provided', () => {
|
|
64
|
+
const task = createMockTaskEffect();
|
|
65
|
+
render(<StepCard {...defaultProps} task={task} stepNumber={3} />);
|
|
66
|
+
expect(screen.getByText('3')).toBeInTheDocument();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('does not render step number badge when stepNumber is undefined', () => {
|
|
70
|
+
const task = createMockTaskEffect();
|
|
71
|
+
const { container } = render(
|
|
72
|
+
<StepCard {...defaultProps} task={task} stepNumber={undefined} />,
|
|
73
|
+
);
|
|
74
|
+
// Step number badge has specific classes
|
|
75
|
+
const stepBadges = container.querySelectorAll('.w-5.h-5.rounded-full');
|
|
76
|
+
expect(stepBadges.length).toBe(0);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// -----------------------------------------------------------------------
|
|
80
|
+
// Kind badge
|
|
81
|
+
// -----------------------------------------------------------------------
|
|
82
|
+
|
|
83
|
+
it('renders KindBadge for the task kind', () => {
|
|
84
|
+
const task = createMockTaskEffect({ kind: 'shell' });
|
|
85
|
+
render(<StepCard {...defaultProps} task={task} />);
|
|
86
|
+
expect(screen.getByText('shell')).toBeInTheDocument();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('renders KindBadge for agent kind', () => {
|
|
90
|
+
const task = createMockTaskEffect({ kind: 'agent' });
|
|
91
|
+
render(<StepCard {...defaultProps} task={task} />);
|
|
92
|
+
expect(screen.getByText('agent')).toBeInTheDocument();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// -----------------------------------------------------------------------
|
|
96
|
+
// Running state (animation pulse dot)
|
|
97
|
+
// -----------------------------------------------------------------------
|
|
98
|
+
|
|
99
|
+
it('shows an animated pulse dot when task is running', () => {
|
|
100
|
+
const task = createMockTaskEffect({ status: 'requested', kind: 'node' });
|
|
101
|
+
const { container } = render(<StepCard {...defaultProps} task={task} />);
|
|
102
|
+
const pulseDot = container.querySelector('.animate-pulse-dot');
|
|
103
|
+
expect(pulseDot).toBeInTheDocument();
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('shows running text with elapsed time for requested task', () => {
|
|
107
|
+
const task = createMockTaskEffect({
|
|
108
|
+
status: 'requested',
|
|
109
|
+
kind: 'node',
|
|
110
|
+
requestedAt: new Date(Date.now() - 5000).toISOString(),
|
|
111
|
+
});
|
|
112
|
+
render(<StepCard {...defaultProps} task={task} />);
|
|
113
|
+
expect(screen.getByText(/running/)).toBeInTheDocument();
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('does not show pulse dot for resolved task', () => {
|
|
117
|
+
const task = createMockTaskEffect({ status: 'resolved', kind: 'node' });
|
|
118
|
+
const { container } = render(<StepCard {...defaultProps} task={task} />);
|
|
119
|
+
const pulseDots = container.querySelectorAll('.bg-info.animate-pulse-dot');
|
|
120
|
+
expect(pulseDots.length).toBe(0);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// -----------------------------------------------------------------------
|
|
124
|
+
// Duration display
|
|
125
|
+
// -----------------------------------------------------------------------
|
|
126
|
+
|
|
127
|
+
it('shows formatted duration for completed task', () => {
|
|
128
|
+
const task = createMockTaskEffect({ status: 'resolved', duration: 4000 });
|
|
129
|
+
render(<StepCard {...defaultProps} task={task} />);
|
|
130
|
+
// Duration appears in the card body (may also appear in expanded details)
|
|
131
|
+
expect(screen.getAllByText('4s').length).toBeGreaterThanOrEqual(1);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('does not show duration row in card body when duration is absent and task is not running', () => {
|
|
135
|
+
// Create a task and override duration to undefined (falsy) directly
|
|
136
|
+
const task = createMockTaskEffect({ status: 'resolved' });
|
|
137
|
+
const taskNoDuration = { ...task, duration: undefined };
|
|
138
|
+
const { container } = render(<StepCard {...defaultProps} task={taskNoDuration} defaultExpanded={false} />);
|
|
139
|
+
// The main card button should not contain a Clock icon for the duration row
|
|
140
|
+
const mainButton = container.querySelector('button.w-full.text-left.p-3');
|
|
141
|
+
const clockInBody = mainButton?.querySelector('[data-lucide="Clock"]');
|
|
142
|
+
expect(clockInBody).toBeNull();
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// -----------------------------------------------------------------------
|
|
146
|
+
// Click handler
|
|
147
|
+
// -----------------------------------------------------------------------
|
|
148
|
+
|
|
149
|
+
it('calls onSelect with effectId when card button is clicked', async () => {
|
|
150
|
+
const user = setupUser();
|
|
151
|
+
const onSelect = vi.fn();
|
|
152
|
+
const task = createMockTaskEffect({ effectId: 'eff-click-test' });
|
|
153
|
+
render(<StepCard {...defaultProps} task={task} onSelect={onSelect} />);
|
|
154
|
+
// Click the main button (first button is the main card button)
|
|
155
|
+
const buttons = screen.getAllByRole('button');
|
|
156
|
+
await user.click(buttons[0]);
|
|
157
|
+
expect(onSelect).toHaveBeenCalledWith('eff-click-test');
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('does not call onSelect when expand toggle is clicked', async () => {
|
|
161
|
+
const user = setupUser();
|
|
162
|
+
const onSelect = vi.fn();
|
|
163
|
+
const task = createMockTaskEffect();
|
|
164
|
+
render(<StepCard {...defaultProps} task={task} onSelect={onSelect} />);
|
|
165
|
+
// The expand/collapse button has an aria-label
|
|
166
|
+
const expandBtn = screen.getByLabelText('Expand details');
|
|
167
|
+
await user.click(expandBtn);
|
|
168
|
+
expect(onSelect).not.toHaveBeenCalled();
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// -----------------------------------------------------------------------
|
|
172
|
+
// Expanded details
|
|
173
|
+
// -----------------------------------------------------------------------
|
|
174
|
+
|
|
175
|
+
it('shows expand button with "Expand details" label when collapsed', () => {
|
|
176
|
+
const task = createMockTaskEffect();
|
|
177
|
+
render(<StepCard {...defaultProps} task={task} defaultExpanded={false} />);
|
|
178
|
+
expect(screen.getByLabelText('Expand details')).toBeInTheDocument();
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('shows "Collapse details" label when defaultExpanded is true', () => {
|
|
182
|
+
const task = createMockTaskEffect();
|
|
183
|
+
render(<StepCard {...defaultProps} task={task} defaultExpanded={true} />);
|
|
184
|
+
expect(screen.getByLabelText('Collapse details')).toBeInTheDocument();
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('toggles expanded state when expand button is clicked', async () => {
|
|
188
|
+
const user = setupUser();
|
|
189
|
+
const task = createMockTaskEffect();
|
|
190
|
+
render(<StepCard {...defaultProps} task={task} defaultExpanded={false} />);
|
|
191
|
+
// Initially collapsed
|
|
192
|
+
expect(screen.getByLabelText('Expand details')).toBeInTheDocument();
|
|
193
|
+
// Click to expand
|
|
194
|
+
await user.click(screen.getByLabelText('Expand details'));
|
|
195
|
+
expect(screen.getByLabelText('Collapse details')).toBeInTheDocument();
|
|
196
|
+
// Click to collapse
|
|
197
|
+
await user.click(screen.getByLabelText('Collapse details'));
|
|
198
|
+
expect(screen.getByLabelText('Expand details')).toBeInTheDocument();
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it('shows step ID in expanded details', () => {
|
|
202
|
+
const task = createMockTaskEffect({ stepId: 'step-abc-123' });
|
|
203
|
+
render(<StepCard {...defaultProps} task={task} defaultExpanded={true} />);
|
|
204
|
+
expect(screen.getByText('Step:')).toBeInTheDocument();
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it('shows duration in expanded details when present', () => {
|
|
208
|
+
const task = createMockTaskEffect({ duration: 12000 });
|
|
209
|
+
render(<StepCard {...defaultProps} task={task} defaultExpanded={true} />);
|
|
210
|
+
expect(screen.getByText('Duration:')).toBeInTheDocument();
|
|
211
|
+
// Duration appears in both the card body and expanded details
|
|
212
|
+
expect(screen.getAllByText('12s').length).toBeGreaterThanOrEqual(2);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it('shows requestedAt in expanded details', () => {
|
|
216
|
+
const task = createMockTaskEffect({
|
|
217
|
+
requestedAt: '2026-01-15T10:30:00.000Z',
|
|
218
|
+
});
|
|
219
|
+
render(<StepCard {...defaultProps} task={task} defaultExpanded={true} />);
|
|
220
|
+
expect(screen.getByText('Requested:')).toBeInTheDocument();
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it('shows resolvedAt in expanded details when present', () => {
|
|
224
|
+
const task = createMockTaskEffect({
|
|
225
|
+
resolvedAt: '2026-01-15T10:30:05.000Z',
|
|
226
|
+
});
|
|
227
|
+
render(<StepCard {...defaultProps} task={task} defaultExpanded={true} />);
|
|
228
|
+
expect(screen.getByText('Resolved:')).toBeInTheDocument();
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it('shows error details in expanded view when task has error', () => {
|
|
232
|
+
const task = createMockTaskEffect({
|
|
233
|
+
status: 'error',
|
|
234
|
+
error: { name: 'TimeoutError', message: 'Task timed out after 30s' },
|
|
235
|
+
});
|
|
236
|
+
render(<StepCard {...defaultProps} task={task} defaultExpanded={true} />);
|
|
237
|
+
expect(screen.getByText('TimeoutError:')).toBeInTheDocument();
|
|
238
|
+
expect(screen.getByText(/Task timed out after 30s/)).toBeInTheDocument();
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
// -----------------------------------------------------------------------
|
|
242
|
+
// Breakpoint waiting state
|
|
243
|
+
// -----------------------------------------------------------------------
|
|
244
|
+
|
|
245
|
+
it('shows breakpoint waiting indicator for breakpoint kind with requested status', () => {
|
|
246
|
+
const task = createMockTaskEffect({
|
|
247
|
+
kind: 'breakpoint',
|
|
248
|
+
status: 'requested',
|
|
249
|
+
});
|
|
250
|
+
render(<StepCard {...defaultProps} task={task} />);
|
|
251
|
+
expect(screen.getByText('Your approval is needed')).toBeInTheDocument();
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it('shows Hand icon for breakpoint waiting state', () => {
|
|
255
|
+
const task = createMockTaskEffect({
|
|
256
|
+
kind: 'breakpoint',
|
|
257
|
+
status: 'requested',
|
|
258
|
+
});
|
|
259
|
+
const { container } = render(<StepCard {...defaultProps} task={task} />);
|
|
260
|
+
const handIcons = container.querySelectorAll('[data-lucide="Hand"]');
|
|
261
|
+
expect(handIcons.length).toBeGreaterThan(0);
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
it('applies breakpoint glow animation class for breakpoint waiting state', () => {
|
|
265
|
+
const task = createMockTaskEffect({
|
|
266
|
+
kind: 'breakpoint',
|
|
267
|
+
status: 'requested',
|
|
268
|
+
});
|
|
269
|
+
const { container } = render(<StepCard {...defaultProps} task={task} />);
|
|
270
|
+
const cardDiv = container.firstChild as HTMLElement;
|
|
271
|
+
expect(cardDiv.className).toContain('animate-breakpoint-glow');
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it('does not show breakpoint indicators for resolved breakpoint', () => {
|
|
275
|
+
const task = createMockTaskEffect({
|
|
276
|
+
kind: 'breakpoint',
|
|
277
|
+
status: 'resolved',
|
|
278
|
+
});
|
|
279
|
+
render(<StepCard {...defaultProps} task={task} />);
|
|
280
|
+
expect(screen.queryByText('Your approval is needed')).not.toBeInTheDocument();
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
// -----------------------------------------------------------------------
|
|
284
|
+
// Selected state
|
|
285
|
+
// -----------------------------------------------------------------------
|
|
286
|
+
|
|
287
|
+
it('applies selected styling when isSelected is true', () => {
|
|
288
|
+
const task = createMockTaskEffect();
|
|
289
|
+
const { container } = render(
|
|
290
|
+
<StepCard {...defaultProps} task={task} isSelected={true} />,
|
|
291
|
+
);
|
|
292
|
+
const cardDiv = container.firstChild as HTMLElement;
|
|
293
|
+
expect(cardDiv.className).toContain('border-l-primary');
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
// -----------------------------------------------------------------------
|
|
297
|
+
// Status-based border colors
|
|
298
|
+
// -----------------------------------------------------------------------
|
|
299
|
+
|
|
300
|
+
it('applies success border for resolved task', () => {
|
|
301
|
+
const task = createMockTaskEffect({ status: 'resolved' });
|
|
302
|
+
const { container } = render(<StepCard {...defaultProps} task={task} />);
|
|
303
|
+
const cardDiv = container.firstChild as HTMLElement;
|
|
304
|
+
expect(cardDiv.className).toContain('border-l-success');
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
it('applies error border for error task', () => {
|
|
308
|
+
const task = createMockTaskEffect({ status: 'error' });
|
|
309
|
+
const { container } = render(<StepCard {...defaultProps} task={task} />);
|
|
310
|
+
const cardDiv = container.firstChild as HTMLElement;
|
|
311
|
+
expect(cardDiv.className).toContain('border-l-error');
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
it('applies info border for running task (non-breakpoint)', () => {
|
|
315
|
+
const task = createMockTaskEffect({ status: 'requested', kind: 'node' });
|
|
316
|
+
const { container } = render(<StepCard {...defaultProps} task={task} />);
|
|
317
|
+
const cardDiv = container.firstChild as HTMLElement;
|
|
318
|
+
expect(cardDiv.className).toContain('border-l-info');
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
it('applies warning border for breakpoint waiting task', () => {
|
|
322
|
+
const task = createMockTaskEffect({
|
|
323
|
+
status: 'requested',
|
|
324
|
+
kind: 'breakpoint',
|
|
325
|
+
});
|
|
326
|
+
const { container } = render(<StepCard {...defaultProps} task={task} />);
|
|
327
|
+
const cardDiv = container.firstChild as HTMLElement;
|
|
328
|
+
expect(cardDiv.className).toContain('border-l-warning');
|
|
329
|
+
});
|
|
330
|
+
});
|