@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,174 @@
|
|
|
1
|
+
import { render, screen } from '@/test/test-utils';
|
|
2
|
+
import { vi } from 'vitest';
|
|
3
|
+
import { ProjectListView } from '../project-list-view';
|
|
4
|
+
import { createMockProjectSummary } from '@/test/fixtures';
|
|
5
|
+
import type { ProjectSummary } from '@/types';
|
|
6
|
+
|
|
7
|
+
// Mock ProjectHealthCard to keep tests focused on ProjectListView logic
|
|
8
|
+
vi.mock('../project-health-card', () => ({
|
|
9
|
+
ProjectHealthCard: ({ project }: { project: ProjectSummary }) => (
|
|
10
|
+
<div data-testid={`project-card-${project.projectName}`}>{project.projectName}</div>
|
|
11
|
+
),
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
describe('ProjectListView', () => {
|
|
15
|
+
const defaultProps = {
|
|
16
|
+
loading: false,
|
|
17
|
+
error: undefined,
|
|
18
|
+
filteredProjects: [] as ProjectSummary[],
|
|
19
|
+
activeProjects: [] as ProjectSummary[],
|
|
20
|
+
historyProjects: [] as ProjectSummary[],
|
|
21
|
+
statusFilter: 'all' as const,
|
|
22
|
+
sortMode: 'status' as const,
|
|
23
|
+
cardStatusFilter: 'all' as const,
|
|
24
|
+
historyCollapsed: false,
|
|
25
|
+
onHistoryCollapsedChange: vi.fn(),
|
|
26
|
+
onHideProject: vi.fn(),
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
beforeEach(() => {
|
|
30
|
+
vi.clearAllMocks();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('renders loading skeleton when loading', () => {
|
|
34
|
+
const { container } = render(
|
|
35
|
+
<ProjectListView {...defaultProps} loading={true} />
|
|
36
|
+
);
|
|
37
|
+
const pulsingElements = container.querySelectorAll('.animate-pulse');
|
|
38
|
+
expect(pulsingElements.length).toBeGreaterThan(0);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('renders error banner when error is set', () => {
|
|
42
|
+
render(
|
|
43
|
+
<ProjectListView {...defaultProps} error="Server error" />
|
|
44
|
+
);
|
|
45
|
+
expect(screen.getByTestId('error-banner')).toHaveTextContent('Failed to load projects: Server error');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('renders empty state when no projects match filter', () => {
|
|
49
|
+
render(
|
|
50
|
+
<ProjectListView {...defaultProps} filteredProjects={[]} />
|
|
51
|
+
);
|
|
52
|
+
expect(screen.getByTestId('empty-state')).toBeInTheDocument();
|
|
53
|
+
expect(screen.getByText('No projects found')).toBeInTheDocument();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('renders idle empty state when no active and no history', () => {
|
|
57
|
+
const projects = [createMockProjectSummary()];
|
|
58
|
+
render(
|
|
59
|
+
<ProjectListView
|
|
60
|
+
{...defaultProps}
|
|
61
|
+
filteredProjects={projects}
|
|
62
|
+
activeProjects={[]}
|
|
63
|
+
historyProjects={[]}
|
|
64
|
+
/>
|
|
65
|
+
);
|
|
66
|
+
expect(screen.getByTestId('idle-empty-state')).toBeInTheDocument();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('renders idle-with-history banner when no active but has history', () => {
|
|
70
|
+
const projects = [createMockProjectSummary()];
|
|
71
|
+
render(
|
|
72
|
+
<ProjectListView
|
|
73
|
+
{...defaultProps}
|
|
74
|
+
filteredProjects={projects}
|
|
75
|
+
activeProjects={[]}
|
|
76
|
+
historyProjects={projects}
|
|
77
|
+
statusFilter="all"
|
|
78
|
+
/>
|
|
79
|
+
);
|
|
80
|
+
expect(screen.getByTestId('idle-with-history-banner')).toBeInTheDocument();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('renders active runs section with project cards', () => {
|
|
84
|
+
const projects = [
|
|
85
|
+
createMockProjectSummary({ projectName: 'alpha' }),
|
|
86
|
+
createMockProjectSummary({ projectName: 'beta' }),
|
|
87
|
+
];
|
|
88
|
+
render(
|
|
89
|
+
<ProjectListView
|
|
90
|
+
{...defaultProps}
|
|
91
|
+
filteredProjects={projects}
|
|
92
|
+
activeProjects={projects}
|
|
93
|
+
statusFilter="all"
|
|
94
|
+
/>
|
|
95
|
+
);
|
|
96
|
+
expect(screen.getByTestId('active-runs-section')).toBeInTheDocument();
|
|
97
|
+
expect(screen.getByTestId('project-card-alpha')).toBeInTheDocument();
|
|
98
|
+
expect(screen.getByTestId('project-card-beta')).toBeInTheDocument();
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('renders filtered grid for completed filter', () => {
|
|
102
|
+
const projects = [createMockProjectSummary({ projectName: 'gamma' })];
|
|
103
|
+
render(
|
|
104
|
+
<ProjectListView
|
|
105
|
+
{...defaultProps}
|
|
106
|
+
filteredProjects={projects}
|
|
107
|
+
statusFilter="completed"
|
|
108
|
+
/>
|
|
109
|
+
);
|
|
110
|
+
expect(screen.getByTestId('project-grid-filtered')).toBeInTheDocument();
|
|
111
|
+
expect(screen.getByTestId('project-card-gamma')).toBeInTheDocument();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('renders history section when historyProjects exist', () => {
|
|
115
|
+
const activeP = [createMockProjectSummary({ projectName: 'active1' })];
|
|
116
|
+
const historyP = [createMockProjectSummary({ projectName: 'history1' })];
|
|
117
|
+
render(
|
|
118
|
+
<ProjectListView
|
|
119
|
+
{...defaultProps}
|
|
120
|
+
filteredProjects={[...activeP, ...historyP]}
|
|
121
|
+
activeProjects={activeP}
|
|
122
|
+
historyProjects={historyP}
|
|
123
|
+
statusFilter="all"
|
|
124
|
+
historyCollapsed={false}
|
|
125
|
+
/>
|
|
126
|
+
);
|
|
127
|
+
expect(screen.getByTestId('recent-history-section')).toBeInTheDocument();
|
|
128
|
+
expect(screen.getByTestId('project-grid-history')).toBeInTheDocument();
|
|
129
|
+
expect(screen.getByTestId('project-card-history1')).toBeInTheDocument();
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('hides history grid when historyCollapsed is true', () => {
|
|
133
|
+
const historyP = [createMockProjectSummary({ projectName: 'history1' })];
|
|
134
|
+
render(
|
|
135
|
+
<ProjectListView
|
|
136
|
+
{...defaultProps}
|
|
137
|
+
filteredProjects={historyP}
|
|
138
|
+
historyProjects={historyP}
|
|
139
|
+
statusFilter="all"
|
|
140
|
+
historyCollapsed={true}
|
|
141
|
+
/>
|
|
142
|
+
);
|
|
143
|
+
expect(screen.getByTestId('recent-history-section')).toBeInTheDocument();
|
|
144
|
+
expect(screen.queryByTestId('project-grid-history')).not.toBeInTheDocument();
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('shows "In Progress" header in status sort mode', () => {
|
|
148
|
+
const projects = [createMockProjectSummary({ projectName: 'proj1' })];
|
|
149
|
+
render(
|
|
150
|
+
<ProjectListView
|
|
151
|
+
{...defaultProps}
|
|
152
|
+
filteredProjects={projects}
|
|
153
|
+
activeProjects={projects}
|
|
154
|
+
sortMode="status"
|
|
155
|
+
statusFilter="all"
|
|
156
|
+
/>
|
|
157
|
+
);
|
|
158
|
+
expect(screen.getByText('In Progress')).toBeInTheDocument();
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('shows "Recent Activity" header in activity sort mode', () => {
|
|
162
|
+
const projects = [createMockProjectSummary({ projectName: 'proj1' })];
|
|
163
|
+
render(
|
|
164
|
+
<ProjectListView
|
|
165
|
+
{...defaultProps}
|
|
166
|
+
filteredProjects={projects}
|
|
167
|
+
activeProjects={projects}
|
|
168
|
+
sortMode="activity"
|
|
169
|
+
statusFilter="all"
|
|
170
|
+
/>
|
|
171
|
+
);
|
|
172
|
+
expect(screen.getByText('Recent Activity')).toBeInTheDocument();
|
|
173
|
+
});
|
|
174
|
+
});
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { render, screen, act } from '@/test/test-utils';
|
|
2
|
+
import { vi } from 'vitest';
|
|
3
|
+
import { ProjectSearchInput } from '../project-search-input';
|
|
4
|
+
import userEvent from '@testing-library/user-event';
|
|
5
|
+
|
|
6
|
+
describe('ProjectSearchInput', () => {
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
vi.useFakeTimers({ shouldAdvanceTime: true });
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
afterEach(() => {
|
|
12
|
+
vi.useRealTimers();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('renders with default placeholder', () => {
|
|
16
|
+
render(<ProjectSearchInput onSearch={vi.fn()} />);
|
|
17
|
+
expect(screen.getByPlaceholderText('Filter runs...')).toBeInTheDocument();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('renders with a custom placeholder', () => {
|
|
21
|
+
render(<ProjectSearchInput onSearch={vi.fn()} placeholder="Search here..." />);
|
|
22
|
+
expect(screen.getByPlaceholderText('Search here...')).toBeInTheDocument();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('calls onSearch after debounce delay when typing', async () => {
|
|
26
|
+
const onSearch = vi.fn();
|
|
27
|
+
const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime });
|
|
28
|
+
render(<ProjectSearchInput onSearch={onSearch} debounceMs={300} />);
|
|
29
|
+
|
|
30
|
+
const input = screen.getByPlaceholderText('Filter runs...');
|
|
31
|
+
await user.type(input, 'hello');
|
|
32
|
+
|
|
33
|
+
// Should not have called yet with "hello" (debounced)
|
|
34
|
+
// But will have been called with intermediate values as timers advance
|
|
35
|
+
// After all timers settle, it should have the final value
|
|
36
|
+
act(() => {
|
|
37
|
+
vi.advanceTimersByTime(300);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
expect(onSearch).toHaveBeenLastCalledWith('hello');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('debounces rapid typing', async () => {
|
|
44
|
+
const onSearch = vi.fn();
|
|
45
|
+
const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime });
|
|
46
|
+
render(<ProjectSearchInput onSearch={onSearch} debounceMs={500} />);
|
|
47
|
+
|
|
48
|
+
const input = screen.getByPlaceholderText('Filter runs...');
|
|
49
|
+
await user.type(input, 'abc');
|
|
50
|
+
|
|
51
|
+
// Clear all calls so far
|
|
52
|
+
onSearch.mockClear();
|
|
53
|
+
|
|
54
|
+
// Advance to trigger the final debounced call
|
|
55
|
+
act(() => {
|
|
56
|
+
vi.advanceTimersByTime(500);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// Should have called with the final value
|
|
60
|
+
expect(onSearch).toHaveBeenCalledWith('abc');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('calls onSearch with empty string initially after debounce', () => {
|
|
64
|
+
const onSearch = vi.fn();
|
|
65
|
+
render(<ProjectSearchInput onSearch={onSearch} debounceMs={300} />);
|
|
66
|
+
|
|
67
|
+
// Initial render triggers useEffect with empty value
|
|
68
|
+
act(() => {
|
|
69
|
+
vi.advanceTimersByTime(300);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
expect(onSearch).toHaveBeenCalledWith('');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('updates the input value as the user types', async () => {
|
|
76
|
+
const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime });
|
|
77
|
+
render(<ProjectSearchInput onSearch={vi.fn()} />);
|
|
78
|
+
|
|
79
|
+
const input = screen.getByPlaceholderText('Filter runs...');
|
|
80
|
+
await user.type(input, 'test');
|
|
81
|
+
|
|
82
|
+
expect(input).toHaveValue('test');
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('uses custom debounceMs', async () => {
|
|
86
|
+
const onSearch = vi.fn();
|
|
87
|
+
const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime });
|
|
88
|
+
render(<ProjectSearchInput onSearch={onSearch} debounceMs={100} />);
|
|
89
|
+
|
|
90
|
+
const input = screen.getByPlaceholderText('Filter runs...');
|
|
91
|
+
await user.type(input, 'x');
|
|
92
|
+
|
|
93
|
+
onSearch.mockClear();
|
|
94
|
+
|
|
95
|
+
// Advance less than debounce
|
|
96
|
+
act(() => {
|
|
97
|
+
vi.advanceTimersByTime(50);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// Not called yet with the last character value change
|
|
101
|
+
const _callsAfter50 = onSearch.mock.calls.length;
|
|
102
|
+
|
|
103
|
+
// Advance past debounce
|
|
104
|
+
act(() => {
|
|
105
|
+
vi.advanceTimersByTime(100);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
expect(onSearch).toHaveBeenCalledWith('x');
|
|
109
|
+
});
|
|
110
|
+
});
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { render, screen } from '@/test/test-utils';
|
|
2
|
+
import { ProjectSectionHeader } from '../project-section-header';
|
|
3
|
+
|
|
4
|
+
describe('ProjectSectionHeader', () => {
|
|
5
|
+
const defaultProps = {
|
|
6
|
+
projectName: 'my-project',
|
|
7
|
+
activeRuns: 2,
|
|
8
|
+
completedRuns: 5,
|
|
9
|
+
failedRuns: 1,
|
|
10
|
+
totalRuns: 8,
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
it('renders the project name', () => {
|
|
14
|
+
render(<ProjectSectionHeader {...defaultProps} />);
|
|
15
|
+
expect(screen.getByText('my-project')).toBeInTheDocument();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('shows active runs badge when activeRuns > 0', () => {
|
|
19
|
+
render(<ProjectSectionHeader {...defaultProps} activeRuns={3} />);
|
|
20
|
+
expect(screen.getByText('3 active')).toBeInTheDocument();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('does not show active runs badge when activeRuns is 0', () => {
|
|
24
|
+
render(<ProjectSectionHeader {...defaultProps} activeRuns={0} />);
|
|
25
|
+
expect(screen.queryByText(/active/)).not.toBeInTheDocument();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('shows completed runs badge when completedRuns > 0', () => {
|
|
29
|
+
render(<ProjectSectionHeader {...defaultProps} completedRuns={7} />);
|
|
30
|
+
expect(screen.getByText('7 completed')).toBeInTheDocument();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('does not show completed runs badge when completedRuns is 0', () => {
|
|
34
|
+
render(<ProjectSectionHeader {...defaultProps} completedRuns={0} />);
|
|
35
|
+
expect(screen.queryByText(/completed/)).not.toBeInTheDocument();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('shows failed runs badge when failedRuns > 0', () => {
|
|
39
|
+
render(<ProjectSectionHeader {...defaultProps} failedRuns={3} />);
|
|
40
|
+
expect(screen.getByText('3 failed')).toBeInTheDocument();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('does not show failed runs badge when failedRuns is 0', () => {
|
|
44
|
+
render(<ProjectSectionHeader {...defaultProps} failedRuns={0} />);
|
|
45
|
+
expect(screen.queryByText(/failed/)).not.toBeInTheDocument();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('shows total runs count with plural "runs"', () => {
|
|
49
|
+
render(<ProjectSectionHeader {...defaultProps} totalRuns={10} />);
|
|
50
|
+
expect(screen.getByText(/10 runs/)).toBeInTheDocument();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('shows singular "run" for totalRuns === 1', () => {
|
|
54
|
+
render(<ProjectSectionHeader {...defaultProps} totalRuns={1} />);
|
|
55
|
+
expect(screen.getByText(/1 run(?!s)/)).toBeInTheDocument();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('renders without latestUpdate', () => {
|
|
59
|
+
render(<ProjectSectionHeader {...defaultProps} />);
|
|
60
|
+
// Should render without errors; the total runs text should be there
|
|
61
|
+
expect(screen.getByText(/8 runs/)).toBeInTheDocument();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('displays relative time when latestUpdate is provided', () => {
|
|
65
|
+
// Use a recent timestamp
|
|
66
|
+
const recent = new Date(Date.now() - 30000).toISOString(); // 30 seconds ago
|
|
67
|
+
render(<ProjectSectionHeader {...defaultProps} latestUpdate={recent} />);
|
|
68
|
+
// Should show something like "30s ago"
|
|
69
|
+
expect(screen.getByText(/ago/)).toBeInTheDocument();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('shows "just now" for future timestamps', () => {
|
|
73
|
+
const future = new Date(Date.now() + 60000).toISOString();
|
|
74
|
+
render(<ProjectSectionHeader {...defaultProps} latestUpdate={future} />);
|
|
75
|
+
expect(screen.getByText(/just now/)).toBeInTheDocument();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('hides all badges when counts are 0', () => {
|
|
79
|
+
render(
|
|
80
|
+
<ProjectSectionHeader
|
|
81
|
+
{...defaultProps}
|
|
82
|
+
activeRuns={0}
|
|
83
|
+
completedRuns={0}
|
|
84
|
+
failedRuns={0}
|
|
85
|
+
/>
|
|
86
|
+
);
|
|
87
|
+
expect(screen.queryByText(/active/)).not.toBeInTheDocument();
|
|
88
|
+
expect(screen.queryByText(/completed/)).not.toBeInTheDocument();
|
|
89
|
+
expect(screen.queryByText(/failed/)).not.toBeInTheDocument();
|
|
90
|
+
});
|
|
91
|
+
});
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { render, screen } from '@/test/test-utils';
|
|
2
|
+
import { vi } from 'vitest';
|
|
3
|
+
import { ProjectSection } from '../project-section';
|
|
4
|
+
import { createMockRun, resetIdCounter } from '@/test/fixtures';
|
|
5
|
+
import type { Run } from '@/types';
|
|
6
|
+
|
|
7
|
+
// Mock useProjectRuns hook
|
|
8
|
+
const mockUseProjectRuns = vi.fn();
|
|
9
|
+
vi.mock('@/hooks/use-project-runs', () => ({
|
|
10
|
+
useProjectRuns: (...args: any[]) => mockUseProjectRuns(...args),
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
// Mock next/link
|
|
14
|
+
vi.mock('next/link', () => ({
|
|
15
|
+
__esModule: true,
|
|
16
|
+
default: ({ href, children }: { href: string; children: React.ReactNode }) => (
|
|
17
|
+
<a href={href} data-testid="next-link">
|
|
18
|
+
{children}
|
|
19
|
+
</a>
|
|
20
|
+
),
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
beforeEach(() => {
|
|
24
|
+
resetIdCounter();
|
|
25
|
+
vi.clearAllMocks();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
function setupMockHook(runs: Run[], totalCount: number, loading = false) {
|
|
29
|
+
mockUseProjectRuns.mockReturnValue({
|
|
30
|
+
runs,
|
|
31
|
+
totalCount,
|
|
32
|
+
loading,
|
|
33
|
+
error: null,
|
|
34
|
+
refresh: vi.fn(),
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
describe('ProjectSection', () => {
|
|
39
|
+
it('renders RunCards for fetched runs', () => {
|
|
40
|
+
const runs = [
|
|
41
|
+
createMockRun({ runId: 'run-1', processId: 'alpha-process' }),
|
|
42
|
+
createMockRun({ runId: 'run-2', processId: 'beta-process' }),
|
|
43
|
+
];
|
|
44
|
+
setupMockHook(runs, 2);
|
|
45
|
+
|
|
46
|
+
render(<ProjectSection projectName="test-project" runs={[]} />);
|
|
47
|
+
expect(screen.getByText('Alpha Process')).toBeInTheDocument();
|
|
48
|
+
expect(screen.getByText('Beta Process')).toBeInTheDocument();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('shows loading spinner when loading with no runs', () => {
|
|
52
|
+
setupMockHook([], 0, true);
|
|
53
|
+
|
|
54
|
+
const { container } = render(
|
|
55
|
+
<ProjectSection projectName="test-project" runs={[]} />
|
|
56
|
+
);
|
|
57
|
+
// The loading spinner uses animate-spin class
|
|
58
|
+
const spinner = container.querySelector('.animate-spin');
|
|
59
|
+
expect(spinner).toBeInTheDocument();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('shows "No runs found" when not loading and no runs', () => {
|
|
63
|
+
setupMockHook([], 0, false);
|
|
64
|
+
|
|
65
|
+
render(<ProjectSection projectName="test-project" runs={[]} />);
|
|
66
|
+
expect(screen.getByText('No runs found')).toBeInTheDocument();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('renders the search input', () => {
|
|
70
|
+
setupMockHook([], 0, false);
|
|
71
|
+
|
|
72
|
+
render(<ProjectSection projectName="test-project" runs={[]} />);
|
|
73
|
+
expect(
|
|
74
|
+
screen.getByPlaceholderText('Search by run ID, process, task, or error...')
|
|
75
|
+
).toBeInTheDocument();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('renders pagination controls when there are items', () => {
|
|
79
|
+
const runs = [createMockRun({ runId: 'run-1' })];
|
|
80
|
+
setupMockHook(runs, 15);
|
|
81
|
+
|
|
82
|
+
render(<ProjectSection projectName="test-project" runs={[]} />);
|
|
83
|
+
// PaginationControls renders range text
|
|
84
|
+
expect(screen.getByText(/of 15/)).toBeInTheDocument();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('passes enabled=true by default to useProjectRuns', () => {
|
|
88
|
+
setupMockHook([], 0, false);
|
|
89
|
+
|
|
90
|
+
render(<ProjectSection projectName="test-project" runs={[]} />);
|
|
91
|
+
expect(mockUseProjectRuns).toHaveBeenCalledWith(
|
|
92
|
+
'test-project',
|
|
93
|
+
expect.objectContaining({ enabled: true })
|
|
94
|
+
);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('passes enabled=false when prop is false', () => {
|
|
98
|
+
setupMockHook([], 0, false);
|
|
99
|
+
|
|
100
|
+
render(
|
|
101
|
+
<ProjectSection projectName="test-project" runs={[]} enabled={false} />
|
|
102
|
+
);
|
|
103
|
+
expect(mockUseProjectRuns).toHaveBeenCalledWith(
|
|
104
|
+
'test-project',
|
|
105
|
+
expect.objectContaining({ enabled: false })
|
|
106
|
+
);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('passes statusFilter to useProjectRuns', () => {
|
|
110
|
+
setupMockHook([], 0, false);
|
|
111
|
+
|
|
112
|
+
render(
|
|
113
|
+
<ProjectSection
|
|
114
|
+
projectName="test-project"
|
|
115
|
+
runs={[]}
|
|
116
|
+
statusFilter="completed"
|
|
117
|
+
/>
|
|
118
|
+
);
|
|
119
|
+
expect(mockUseProjectRuns).toHaveBeenCalledWith(
|
|
120
|
+
'test-project',
|
|
121
|
+
expect.objectContaining({ status: 'completed' })
|
|
122
|
+
);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('does not pass status when statusFilter is "all"', () => {
|
|
126
|
+
setupMockHook([], 0, false);
|
|
127
|
+
|
|
128
|
+
render(
|
|
129
|
+
<ProjectSection
|
|
130
|
+
projectName="test-project"
|
|
131
|
+
runs={[]}
|
|
132
|
+
statusFilter="all"
|
|
133
|
+
/>
|
|
134
|
+
);
|
|
135
|
+
expect(mockUseProjectRuns).toHaveBeenCalledWith(
|
|
136
|
+
'test-project',
|
|
137
|
+
expect.objectContaining({ status: undefined })
|
|
138
|
+
);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('does not render pagination when totalCount is 0', () => {
|
|
142
|
+
setupMockHook([], 0, false);
|
|
143
|
+
|
|
144
|
+
const { container: _container } = render(
|
|
145
|
+
<ProjectSection projectName="test-project" runs={[]} />
|
|
146
|
+
);
|
|
147
|
+
// PaginationControls returns null when totalItems is 0
|
|
148
|
+
expect(screen.queryByLabelText('Previous page')).not.toBeInTheDocument();
|
|
149
|
+
expect(screen.queryByLabelText('Next page')).not.toBeInTheDocument();
|
|
150
|
+
});
|
|
151
|
+
});
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { render, screen } from '@/test/test-utils';
|
|
2
|
+
import { vi } from 'vitest';
|
|
3
|
+
import { RunCard } from '../run-card';
|
|
4
|
+
import {
|
|
5
|
+
createMockRun,
|
|
6
|
+
createMockTaskEffect,
|
|
7
|
+
resetIdCounter,
|
|
8
|
+
} from '@/test/fixtures';
|
|
9
|
+
|
|
10
|
+
// Mock next/link to render a plain anchor
|
|
11
|
+
vi.mock('next/link', () => ({
|
|
12
|
+
__esModule: true,
|
|
13
|
+
default: ({ href, children }: { href: string; children: React.ReactNode }) => (
|
|
14
|
+
<a href={href} data-testid="next-link">
|
|
15
|
+
{children}
|
|
16
|
+
</a>
|
|
17
|
+
),
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
resetIdCounter();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
describe('RunCard', () => {
|
|
25
|
+
it('renders the friendly process name', () => {
|
|
26
|
+
const run = createMockRun({ processId: 'data-pipeline/ingest' });
|
|
27
|
+
render(<RunCard run={run} />);
|
|
28
|
+
expect(screen.getByText('Data Pipeline Ingest')).toBeInTheDocument();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('renders a status badge for the run status', () => {
|
|
32
|
+
const run = createMockRun({ status: 'completed' });
|
|
33
|
+
render(<RunCard run={run} />);
|
|
34
|
+
expect(screen.getByText('Completed')).toBeInTheDocument();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('renders the task count', () => {
|
|
38
|
+
const run = createMockRun({ completedTasks: 2, totalTasks: 5 });
|
|
39
|
+
render(<RunCard run={run} />);
|
|
40
|
+
expect(screen.getByText('2/5 tasks')).toBeInTheDocument();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('renders formatted duration', () => {
|
|
44
|
+
const run = createMockRun({ duration: 59000 });
|
|
45
|
+
render(<RunCard run={run} />);
|
|
46
|
+
expect(screen.getByText('59s')).toBeInTheDocument();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('links to the run detail page', () => {
|
|
50
|
+
const run = createMockRun({ runId: 'run-abc-123' });
|
|
51
|
+
render(<RunCard run={run} />);
|
|
52
|
+
const link = screen.getByTestId('next-link');
|
|
53
|
+
expect(link).toHaveAttribute('href', '/runs/run-abc-123');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('shows the project name tag when present', () => {
|
|
57
|
+
const run = createMockRun({ projectName: 'my-project' });
|
|
58
|
+
render(<RunCard run={run} />);
|
|
59
|
+
expect(screen.getByText('my-project')).toBeInTheDocument();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('does not show source label', () => {
|
|
63
|
+
const run = createMockRun({ projectName: 'my-project', sourceLabel: 'cli' });
|
|
64
|
+
render(<RunCard run={run} />);
|
|
65
|
+
expect(screen.queryByText('cli')).not.toBeInTheDocument();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('shows a progress bar when there are tasks', () => {
|
|
69
|
+
const run = createMockRun({ totalTasks: 5, completedTasks: 3 });
|
|
70
|
+
render(<RunCard run={run} />);
|
|
71
|
+
// The progress bar renders a div with style width
|
|
72
|
+
const progressBar = document.querySelector('[style*="width"]');
|
|
73
|
+
expect(progressBar).toBeInTheDocument();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('does not show a progress bar when totalTasks is 0', () => {
|
|
77
|
+
const run = createMockRun({ totalTasks: 0, tasks: [] });
|
|
78
|
+
render(<RunCard run={run} />);
|
|
79
|
+
const progressBar = document.querySelector('[style*="width"]');
|
|
80
|
+
expect(progressBar).toBeNull();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('shows 100% progress for completed runs', () => {
|
|
84
|
+
const run = createMockRun({ status: 'completed', totalTasks: 5, completedTasks: 3 });
|
|
85
|
+
render(<RunCard run={run} />);
|
|
86
|
+
const progressBar = document.querySelector('[style*="width"]') as HTMLElement;
|
|
87
|
+
expect(progressBar?.style.width).toBe('100%');
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('displays the failed step text for failed runs', () => {
|
|
91
|
+
const run = createMockRun({
|
|
92
|
+
status: 'failed',
|
|
93
|
+
failedStep: 'process-data step',
|
|
94
|
+
});
|
|
95
|
+
render(<RunCard run={run} />);
|
|
96
|
+
expect(screen.getByText(/Failed at: process-data step/)).toBeInTheDocument();
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('truncates long failed step text to 80 chars', () => {
|
|
100
|
+
const longStep = 'a'.repeat(100);
|
|
101
|
+
const run = createMockRun({
|
|
102
|
+
status: 'failed',
|
|
103
|
+
failedStep: longStep,
|
|
104
|
+
});
|
|
105
|
+
render(<RunCard run={run} />);
|
|
106
|
+
expect(screen.getByText(/Failed at:/)).toHaveTextContent(
|
|
107
|
+
`Failed at: ${'a'.repeat(80)}...`
|
|
108
|
+
);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('shows breakpoint question when a pending breakpoint exists', () => {
|
|
112
|
+
const breakpointTask = createMockTaskEffect({
|
|
113
|
+
kind: 'breakpoint',
|
|
114
|
+
status: 'requested',
|
|
115
|
+
breakpointQuestion: 'Approve deployment?',
|
|
116
|
+
});
|
|
117
|
+
const run = createMockRun({
|
|
118
|
+
status: 'waiting',
|
|
119
|
+
tasks: [breakpointTask],
|
|
120
|
+
});
|
|
121
|
+
render(<RunCard run={run} />);
|
|
122
|
+
expect(screen.getByText('Approve deployment?')).toBeInTheDocument();
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('prefers run-level breakpointQuestion over task-level', () => {
|
|
126
|
+
const breakpointTask = createMockTaskEffect({
|
|
127
|
+
kind: 'breakpoint',
|
|
128
|
+
status: 'requested',
|
|
129
|
+
breakpointQuestion: 'Task-level question?',
|
|
130
|
+
});
|
|
131
|
+
const run = createMockRun({
|
|
132
|
+
status: 'waiting',
|
|
133
|
+
tasks: [breakpointTask],
|
|
134
|
+
breakpointQuestion: 'Run-level question?',
|
|
135
|
+
});
|
|
136
|
+
render(<RunCard run={run} />);
|
|
137
|
+
expect(screen.getByText('Run-level question?')).toBeInTheDocument();
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('applies selected styling when selected prop is true', () => {
|
|
141
|
+
const run = createMockRun();
|
|
142
|
+
const { container } = render(<RunCard run={run} selected />);
|
|
143
|
+
const card = container.querySelector('.ring-1');
|
|
144
|
+
expect(card).toBeInTheDocument();
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('renders with waiting status indicator', () => {
|
|
148
|
+
const run = createMockRun({ status: 'waiting' });
|
|
149
|
+
render(<RunCard run={run} />);
|
|
150
|
+
expect(screen.getByText('Waiting')).toBeInTheDocument();
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('renders with failed status indicator', () => {
|
|
154
|
+
const run = createMockRun({ status: 'failed' });
|
|
155
|
+
render(<RunCard run={run} />);
|
|
156
|
+
expect(screen.getByText('Failed')).toBeInTheDocument();
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('renders with pending status indicator', () => {
|
|
160
|
+
const run = createMockRun({ status: 'pending' });
|
|
161
|
+
render(<RunCard run={run} />);
|
|
162
|
+
expect(screen.getByText('Pending')).toBeInTheDocument();
|
|
163
|
+
});
|
|
164
|
+
});
|