jettypod 4.4.118 → 4.4.121
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/.env +4 -3
- package/Cargo.lock +6450 -0
- package/Cargo.toml +35 -0
- package/README.md +5 -1
- package/TAURI-MIGRATION-PLAN.md +840 -0
- package/apps/dashboard/app/connect-claude/page.tsx +5 -6
- package/apps/dashboard/app/decision/[id]/page.tsx +63 -58
- package/apps/dashboard/app/demo/gates/page.tsx +43 -45
- package/apps/dashboard/app/design-system/page.tsx +868 -0
- package/apps/dashboard/app/globals.css +80 -4
- package/apps/dashboard/app/install-claude/page.tsx +4 -6
- package/apps/dashboard/app/login/page.tsx +72 -54
- package/apps/dashboard/app/page.tsx +101 -48
- package/apps/dashboard/app/settings/page.tsx +61 -13
- package/apps/dashboard/app/signup/page.tsx +242 -0
- package/apps/dashboard/app/subscribe/page.tsx +0 -2
- package/apps/dashboard/app/tests/page.tsx +37 -4
- package/apps/dashboard/app/welcome/page.tsx +13 -16
- package/apps/dashboard/app/work/[id]/page.tsx +117 -118
- package/apps/dashboard/app/work/[id]/proof/page.tsx +1489 -0
- package/apps/dashboard/components/AppShell.tsx +92 -85
- package/apps/dashboard/components/CardMenu.tsx +45 -12
- package/apps/dashboard/components/ClaudePanel.tsx +771 -850
- package/apps/dashboard/components/ClaudePanelInput.tsx +43 -15
- package/apps/dashboard/components/ConnectClaudeScreen.tsx +17 -34
- package/apps/dashboard/components/CopyableId.tsx +3 -4
- package/apps/dashboard/components/DetailReviewActions.tsx +100 -0
- package/apps/dashboard/components/DragContext.tsx +134 -63
- package/apps/dashboard/components/DraggableCard.tsx +3 -5
- package/apps/dashboard/components/DropZone.tsx +6 -7
- package/apps/dashboard/components/EditableDetailDescription.tsx +7 -13
- package/apps/dashboard/components/EditableDetailTitle.tsx +6 -13
- package/apps/dashboard/components/EditableTitle.tsx +26 -7
- package/apps/dashboard/components/ElapsedTimer.tsx +66 -0
- package/apps/dashboard/components/EpicGroup.tsx +359 -0
- package/apps/dashboard/components/GateCard.tsx +79 -17
- package/apps/dashboard/components/GateChoiceCard.tsx +15 -18
- package/apps/dashboard/components/InstallClaudeScreen.tsx +15 -32
- package/apps/dashboard/components/JettyLoader.tsx +37 -0
- package/apps/dashboard/components/KanbanBoard.tsx +368 -958
- package/apps/dashboard/components/KanbanCard.tsx +740 -0
- package/apps/dashboard/components/LazyCard.tsx +62 -0
- package/apps/dashboard/components/LazyMarkdown.tsx +11 -0
- package/apps/dashboard/components/MainNav.tsx +38 -73
- package/apps/dashboard/components/MessageBlock.tsx +468 -0
- package/apps/dashboard/components/ModeStartCard.tsx +15 -16
- package/apps/dashboard/components/OnboardingWelcome.tsx +213 -0
- package/apps/dashboard/components/PlaceholderCard.tsx +3 -4
- package/apps/dashboard/components/ProjectSwitcher.tsx +30 -30
- package/apps/dashboard/components/PrototypeTimeline.tsx +72 -51
- package/apps/dashboard/components/RealTimeKanbanWrapper.tsx +406 -388
- package/apps/dashboard/components/RealTimeTestsWrapper.tsx +373 -235
- package/apps/dashboard/components/ReviewFooter.tsx +139 -0
- package/apps/dashboard/components/SessionList.tsx +19 -19
- package/apps/dashboard/components/SubscribeContent.tsx +91 -47
- package/apps/dashboard/components/TestTree.tsx +16 -16
- package/apps/dashboard/components/TipCard.tsx +16 -17
- package/apps/dashboard/components/Toast.tsx +5 -6
- package/apps/dashboard/components/TypeIcon.tsx +55 -0
- package/apps/dashboard/components/ViewModeToolbar.tsx +104 -0
- package/apps/dashboard/components/WaveCompletionAnimation.tsx +52 -65
- package/apps/dashboard/components/WelcomeScreen.tsx +19 -35
- package/apps/dashboard/components/WorkItemHeader.tsx +4 -5
- package/apps/dashboard/components/WorkItemTree.tsx +11 -32
- package/apps/dashboard/components/settings/AccountSection.tsx +55 -35
- package/apps/dashboard/components/settings/AiContextSection.tsx +89 -0
- package/apps/dashboard/components/settings/ContextDocumentsSection.tsx +317 -0
- package/apps/dashboard/components/settings/EnvVarsSection.tsx +74 -152
- package/apps/dashboard/components/settings/GeneralSection.tsx +162 -56
- package/apps/dashboard/components/settings/ProjectStackSection.tsx +948 -0
- package/apps/dashboard/components/settings/SettingsLayout.tsx +4 -5
- package/apps/dashboard/components/ui/Button.tsx +104 -0
- package/apps/dashboard/components/ui/Input.tsx +78 -0
- package/apps/dashboard/components.json +1 -1
- package/apps/dashboard/contexts/ClaudeSessionContext.tsx +711 -418
- package/apps/dashboard/contexts/ConnectionStatusContext.tsx +25 -5
- package/apps/dashboard/contexts/UsageContext.tsx +87 -32
- package/apps/dashboard/dev.sh +35 -0
- package/apps/dashboard/eslint.config.mjs +9 -9
- package/apps/dashboard/hooks/useKanbanAnimation.ts +29 -0
- package/apps/dashboard/hooks/useKanbanUndo.ts +83 -0
- package/apps/dashboard/hooks/useWebSocket.ts +138 -83
- package/apps/dashboard/index.html +73 -0
- package/apps/dashboard/lib/constants.ts +43 -0
- package/apps/dashboard/lib/data-bridge.ts +722 -0
- package/apps/dashboard/lib/db.ts +69 -1265
- package/apps/dashboard/lib/environment-config.ts +173 -0
- package/apps/dashboard/lib/environment-verification.ts +119 -0
- package/apps/dashboard/lib/kanban-utils.ts +270 -0
- package/apps/dashboard/lib/proof-run.ts +495 -0
- package/apps/dashboard/lib/proof-scenario-runner.ts +346 -0
- package/apps/dashboard/lib/run-migrations.js +27 -2
- package/apps/dashboard/lib/service-recovery.ts +326 -0
- package/apps/dashboard/lib/session-state-machine.ts +1 -0
- package/apps/dashboard/lib/session-state-utils.ts +0 -164
- package/apps/dashboard/lib/session-stream-manager.ts +308 -134
- package/apps/dashboard/lib/shadows.ts +7 -0
- package/apps/dashboard/lib/stream-manager-registry.ts +46 -6
- package/apps/dashboard/lib/tauri-bridge.ts +102 -0
- package/apps/dashboard/lib/tauri.ts +106 -0
- package/apps/dashboard/lib/utils.ts +6 -0
- package/apps/dashboard/next-env.d.ts +1 -1
- package/apps/dashboard/package.json +21 -32
- package/apps/dashboard/public/bug-icon.png +0 -0
- package/apps/dashboard/public/buoy-icon.png +0 -0
- package/apps/dashboard/public/fonts/Satoshi-Variable.woff2 +0 -0
- package/apps/dashboard/public/fonts/Satoshi-VariableItalic.woff2 +0 -0
- package/apps/dashboard/public/in-flight-seagull.png +0 -0
- package/apps/dashboard/public/jetty-icon-loading-alt.svg +11 -0
- package/apps/dashboard/public/jetty-icon-loading.svg +11 -0
- package/apps/dashboard/public/jettypod_logo.png +0 -0
- package/apps/dashboard/public/pier-icon.png +0 -0
- package/apps/dashboard/public/star-icon.png +0 -0
- package/apps/dashboard/public/wrench-icon.png +0 -0
- package/apps/dashboard/scripts/tauri-build.js +228 -0
- package/apps/dashboard/scripts/upload-tauri-to-r2.js +125 -0
- package/apps/dashboard/scripts/ws-server.js +191 -0
- package/apps/dashboard/src/main.tsx +12 -0
- package/apps/dashboard/src/router.tsx +107 -0
- package/apps/dashboard/src/vite-env.d.ts +1 -0
- package/apps/dashboard/tsconfig.json +7 -12
- package/apps/dashboard/tsconfig.tsbuildinfo +1 -1
- package/apps/dashboard/vite.config.ts +33 -0
- package/apps/update-server/src/index.ts +228 -80
- package/claude-hooks/global-guardrails.js +14 -13
- package/crates/jettypod-cli/Cargo.toml +19 -0
- package/crates/jettypod-cli/src/commands.rs +1249 -0
- package/crates/jettypod-cli/src/main.rs +595 -0
- package/crates/jettypod-core/Cargo.toml +26 -0
- package/crates/jettypod-core/build.rs +98 -0
- package/crates/jettypod-core/migrations/V1__baseline.sql +197 -0
- package/crates/jettypod-core/migrations/V2__work_items_indexes.sql +6 -0
- package/crates/jettypod-core/migrations/V3__qa_steps.sql +2 -0
- package/crates/jettypod-core/src/auth.rs +294 -0
- package/crates/jettypod-core/src/config.rs +397 -0
- package/crates/jettypod-core/src/db/mod.rs +507 -0
- package/crates/jettypod-core/src/db/recovery.rs +114 -0
- package/crates/jettypod-core/src/db/startup.rs +101 -0
- package/crates/jettypod-core/src/db/validate.rs +149 -0
- package/crates/jettypod-core/src/error.rs +76 -0
- package/crates/jettypod-core/src/git.rs +458 -0
- package/crates/jettypod-core/src/lib.rs +20 -0
- package/crates/jettypod-core/src/sessions.rs +625 -0
- package/crates/jettypod-core/src/skills.rs +556 -0
- package/crates/jettypod-core/src/work.rs +1086 -0
- package/crates/jettypod-core/src/worktree.rs +628 -0
- package/crates/jettypod-core/src/ws.rs +767 -0
- package/cucumber-test.cjs +6 -0
- package/cucumber.js +9 -3
- package/docs/COMMAND_REFERENCE.md +34 -0
- package/hooks/post-checkout +32 -75
- package/hooks/post-merge +111 -10
- package/jest.setup.js +1 -0
- package/jettypod.js +145 -116
- package/lib/bdd-preflight.js +96 -0
- package/lib/chore-taxonomy.js +33 -10
- package/lib/database.js +36 -16
- package/lib/db-watcher.js +1 -1
- package/lib/git-hooks/pre-commit +1 -1
- package/lib/jettypod-backup.js +27 -4
- package/lib/merge-lock.js +111 -253
- package/lib/migrations/027-plan-at-creation-column.js +3 -1
- package/lib/migrations/029-remove-autoincrement.js +307 -0
- package/lib/migrations/029-rename-corrupted-to-cleaned.js +149 -0
- package/lib/migrations/030-rejection-round-columns.js +54 -0
- package/lib/migrations/031-session-isolation-index.js +17 -0
- package/lib/migrations/index.js +47 -4
- package/lib/schema.js +10 -5
- package/lib/seed-onboarding.js +1 -1
- package/lib/update-command/index.js +9 -175
- package/lib/work-commands/index.js +144 -19
- package/lib/work-tracking/index.js +148 -27
- package/lib/worktree-diagnostics.js +16 -16
- package/lib/worktree-facade.js +1 -1
- package/lib/worktree-manager.js +8 -8
- package/lib/worktree-reconciler.js +5 -5
- package/package.json +9 -2
- package/scripts/ndjson-to-cucumber-json.js +152 -0
- package/scripts/postinstall.js +25 -0
- package/skills-templates/bug-mode/SKILL.md +79 -20
- package/skills-templates/bug-planning/SKILL.md +25 -29
- package/skills-templates/chore-mode/SKILL.md +171 -69
- package/skills-templates/chore-mode/verification.js +51 -10
- package/skills-templates/chore-planning/SKILL.md +47 -18
- package/skills-templates/design-system-selection/SKILL.md +273 -0
- package/skills-templates/epic-planning/SKILL.md +82 -48
- package/skills-templates/external-transition/SKILL.md +47 -47
- package/skills-templates/feature-planning/SKILL.md +173 -74
- package/skills-templates/production-mode/SKILL.md +69 -49
- package/skills-templates/request-routing/SKILL.md +4 -4
- package/skills-templates/simple-improvement/SKILL.md +74 -29
- package/skills-templates/speed-mode/SKILL.md +217 -141
- package/skills-templates/stable-mode/SKILL.md +148 -89
- package/apps/dashboard/README.md +0 -36
- package/apps/dashboard/app/api/claude/[workItemId]/message/route.ts +0 -386
- package/apps/dashboard/app/api/claude/[workItemId]/pin/route.ts +0 -24
- package/apps/dashboard/app/api/claude/[workItemId]/route.ts +0 -167
- package/apps/dashboard/app/api/claude/sessions/[sessionId]/content/route.ts +0 -52
- package/apps/dashboard/app/api/claude/sessions/[sessionId]/message/route.ts +0 -378
- package/apps/dashboard/app/api/claude/sessions/[sessionId]/pin/route.ts +0 -24
- package/apps/dashboard/app/api/claude/sessions/cleanup/route.ts +0 -34
- package/apps/dashboard/app/api/claude/sessions/route.ts +0 -184
- package/apps/dashboard/app/api/decisions/[id]/route.ts +0 -25
- package/apps/dashboard/app/api/internal/set-project/route.ts +0 -17
- package/apps/dashboard/app/api/kanban/route.ts +0 -15
- package/apps/dashboard/app/api/settings/env-vars/route.ts +0 -125
- package/apps/dashboard/app/api/settings/general/route.ts +0 -21
- package/apps/dashboard/app/api/tests/route.ts +0 -9
- package/apps/dashboard/app/api/tests/run/route.ts +0 -82
- package/apps/dashboard/app/api/tests/run/stream/route.ts +0 -71
- package/apps/dashboard/app/api/tests/undefined/route.ts +0 -9
- package/apps/dashboard/app/api/usage/route.ts +0 -17
- package/apps/dashboard/app/api/work/[id]/description/route.ts +0 -21
- package/apps/dashboard/app/api/work/[id]/epic/route.ts +0 -21
- package/apps/dashboard/app/api/work/[id]/order/route.ts +0 -21
- package/apps/dashboard/app/api/work/[id]/status/route.ts +0 -21
- package/apps/dashboard/app/api/work/[id]/title/route.ts +0 -21
- package/apps/dashboard/app/layout.tsx +0 -43
- package/apps/dashboard/components/UpgradeBanner.tsx +0 -29
- package/apps/dashboard/electron/ipc-handlers.js +0 -1028
- package/apps/dashboard/electron/main.js +0 -2124
- package/apps/dashboard/electron/preload.js +0 -123
- package/apps/dashboard/electron/session-manager.js +0 -141
- package/apps/dashboard/electron-builder.config.js +0 -357
- package/apps/dashboard/hooks/useClaudeSessions.ts +0 -299
- package/apps/dashboard/lib/claude-process-manager.ts +0 -492
- package/apps/dashboard/lib/db-bridge.ts +0 -282
- package/apps/dashboard/lib/prototypes.ts +0 -202
- package/apps/dashboard/lib/test-results-db.ts +0 -307
- package/apps/dashboard/lib/tests.ts +0 -282
- package/apps/dashboard/next.config.js +0 -50
- package/apps/dashboard/postcss.config.mjs +0 -7
- package/apps/dashboard/public/file.svg +0 -1
- package/apps/dashboard/public/globe.svg +0 -1
- package/apps/dashboard/public/next.svg +0 -1
- package/apps/dashboard/public/vercel.svg +0 -1
- package/apps/dashboard/public/window.svg +0 -1
- package/apps/dashboard/scripts/download-node.js +0 -104
- package/apps/dashboard/scripts/upload-to-r2.js +0 -89
- package/docs/bdd-guidance.md +0 -390
|
@@ -1,9 +1,13 @@
|
|
|
1
|
-
'use client';
|
|
2
1
|
|
|
3
2
|
import { useState, useCallback, useMemo, useEffect, useRef } from 'react';
|
|
4
3
|
import { useWebSocket, type WebSocketMessage } from '../hooks/useWebSocket';
|
|
5
|
-
import {
|
|
6
|
-
import type { TestDashboardData, TestFeature } from '@/lib/
|
|
4
|
+
import { useSessionActions } from '../contexts/ClaudeSessionContext';
|
|
5
|
+
import type { TestDashboardData, TestFeature } from '@/lib/db';
|
|
6
|
+
import { Input } from '@/components/ui/Input';
|
|
7
|
+
import { Button } from '@/components/ui/Button';
|
|
8
|
+
import { dataBridge } from '@/lib/data-bridge';
|
|
9
|
+
import { invoke, listen } from '@/lib/tauri';
|
|
10
|
+
import { getWebSocketUrl } from '@/lib/utils';
|
|
7
11
|
|
|
8
12
|
interface RealTimeTestsWrapperProps {
|
|
9
13
|
initialData: TestDashboardData;
|
|
@@ -41,7 +45,6 @@ type StepStatus = 'pending' | 'running' | 'passed' | 'failed';
|
|
|
41
45
|
export function RealTimeTestsWrapper({ initialData }: RealTimeTestsWrapperProps) {
|
|
42
46
|
const [data, setData] = useState<TestDashboardData>(initialData);
|
|
43
47
|
const [selectedFeatureId, setSelectedFeatureId] = useState<string | null>(() => {
|
|
44
|
-
if (typeof window === 'undefined') return null;
|
|
45
48
|
return localStorage.getItem(SELECTED_FEATURE_KEY);
|
|
46
49
|
});
|
|
47
50
|
const [searchText, setSearchText] = useState('');
|
|
@@ -53,27 +56,35 @@ export function RealTimeTestsWrapper({ initialData }: RealTimeTestsWrapperProps)
|
|
|
53
56
|
const [elapsedDisplay, setElapsedDisplay] = useState<Record<string, number>>({});
|
|
54
57
|
|
|
55
58
|
const elapsedStartRef = useRef<Record<string, number>>({});
|
|
59
|
+
const activeProcessesRef = useRef<Set<number>>(new Set());
|
|
60
|
+
const unlistenersRef = useRef<(() => void)[]>([]);
|
|
56
61
|
const allFeaturesRef = useRef<TestFeature[]>([]);
|
|
57
|
-
const
|
|
62
|
+
const projectRootRef = useRef<string | null>(null);
|
|
63
|
+
const { createFixScenarioSession } = useSessionActions();
|
|
58
64
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
.
|
|
63
|
-
|
|
65
|
+
// Get project root on mount (needed for spawn_process cwd)
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
dataBridge.getProjectRoot().then(root => {
|
|
68
|
+
projectRootRef.current = root;
|
|
69
|
+
});
|
|
64
70
|
}, []);
|
|
65
71
|
|
|
66
|
-
//
|
|
72
|
+
// Kill active test processes and remove event listeners on unmount
|
|
67
73
|
useEffect(() => {
|
|
68
|
-
|
|
69
|
-
|
|
74
|
+
return () => {
|
|
75
|
+
for (const pid of activeProcessesRef.current) {
|
|
76
|
+
invoke('kill_process', { pid }).catch(() => {});
|
|
77
|
+
}
|
|
78
|
+
activeProcessesRef.current.clear();
|
|
79
|
+
for (const unlisten of unlistenersRef.current) unlisten();
|
|
80
|
+
unlistenersRef.current = [];
|
|
81
|
+
};
|
|
82
|
+
}, []);
|
|
70
83
|
|
|
71
84
|
const refreshData = useCallback(async () => {
|
|
72
|
-
const
|
|
73
|
-
const newData = await response.json();
|
|
85
|
+
const newData = await dataBridge.getTestDashboardData();
|
|
74
86
|
setData(newData);
|
|
75
|
-
|
|
76
|
-
}, [refreshUndefinedSteps]);
|
|
87
|
+
}, []);
|
|
77
88
|
|
|
78
89
|
const handleMessage = useCallback(async (message: WebSocketMessage) => {
|
|
79
90
|
if (message.type === 'test_change') {
|
|
@@ -81,8 +92,10 @@ export function RealTimeTestsWrapper({ initialData }: RealTimeTestsWrapperProps)
|
|
|
81
92
|
}
|
|
82
93
|
}, [refreshData]);
|
|
83
94
|
|
|
84
|
-
// Elapsed timer tick
|
|
95
|
+
// Elapsed timer tick — only runs when scenarios are actively running
|
|
96
|
+
const hasRunning = runningScenarios.size > 0;
|
|
85
97
|
useEffect(() => {
|
|
98
|
+
if (!hasRunning) return;
|
|
86
99
|
const interval = setInterval(() => {
|
|
87
100
|
const starts = elapsedStartRef.current;
|
|
88
101
|
if (Object.keys(starts).length === 0) return;
|
|
@@ -94,7 +107,7 @@ export function RealTimeTestsWrapper({ initialData }: RealTimeTestsWrapperProps)
|
|
|
94
107
|
);
|
|
95
108
|
}, 1000);
|
|
96
109
|
return () => clearInterval(interval);
|
|
97
|
-
}, []);
|
|
110
|
+
}, [hasRunning]);
|
|
98
111
|
|
|
99
112
|
const runScenario = useCallback(async (featureFile: string, scenarioTitle: string) => {
|
|
100
113
|
const key = `${featureFile}::${scenarioTitle}`;
|
|
@@ -114,155 +127,250 @@ export function RealTimeTestsWrapper({ initialData }: RealTimeTestsWrapperProps)
|
|
|
114
127
|
elapsedStartRef.current = { ...elapsedStartRef.current, [key]: Date.now() };
|
|
115
128
|
setElapsedDisplay(prev => ({ ...prev, [key]: 0 }));
|
|
116
129
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
130
|
+
const steps = scenario?.steps || [];
|
|
131
|
+
|
|
132
|
+
// Cucumber NDJSON message parsing state
|
|
133
|
+
const pickleSteps = new Map<string, { text: string }>();
|
|
134
|
+
const testCaseSteps = new Map<string, { id: string; pickleStepId?: string }[]>();
|
|
135
|
+
let activeTestCaseId: string | null = null;
|
|
136
|
+
let outputBuffer = '';
|
|
137
|
+
|
|
138
|
+
function resolveStepText(testStepId: string): string | null {
|
|
139
|
+
if (!activeTestCaseId) return null;
|
|
140
|
+
const tcSteps = testCaseSteps.get(activeTestCaseId);
|
|
141
|
+
if (!tcSteps) return null;
|
|
142
|
+
const testStep = tcSteps.find(s => s.id === testStepId);
|
|
143
|
+
if (!testStep?.pickleStepId) return null;
|
|
144
|
+
const ps = pickleSteps.get(testStep.pickleStepId);
|
|
145
|
+
return ps ? ps.text : null;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function processLine(line: string) {
|
|
149
|
+
const trimmed = line.trim();
|
|
150
|
+
if (!trimmed || !trimmed.startsWith('{')) return;
|
|
151
|
+
let msg: Record<string, any>;
|
|
152
|
+
try { msg = JSON.parse(trimmed); } catch { return; }
|
|
153
|
+
|
|
154
|
+
if (msg.pickle) {
|
|
155
|
+
for (const s of msg.pickle.steps) pickleSteps.set(s.id, s);
|
|
156
|
+
}
|
|
157
|
+
if (msg.testCase) {
|
|
158
|
+
testCaseSteps.set(msg.testCase.id, msg.testCase.testSteps);
|
|
159
|
+
}
|
|
160
|
+
if (msg.testCaseStarted) {
|
|
161
|
+
activeTestCaseId = msg.testCaseStarted.testCaseId;
|
|
162
|
+
}
|
|
163
|
+
if (msg.testStepStarted) {
|
|
164
|
+
const stepText = resolveStepText(msg.testStepStarted.testStepId);
|
|
165
|
+
if (stepText) {
|
|
166
|
+
const stepIndex = steps.findIndex(s => stripKeyword(s) === stepText);
|
|
167
|
+
if (stepIndex >= 0) {
|
|
168
|
+
setStepProgress(prev => {
|
|
169
|
+
const arr = [...(prev[key] || [])];
|
|
170
|
+
arr[stepIndex] = 'running';
|
|
171
|
+
return { ...prev, [key]: arr };
|
|
172
|
+
});
|
|
173
|
+
}
|
|
132
174
|
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
175
|
+
}
|
|
176
|
+
if (msg.testStepFinished) {
|
|
177
|
+
const stepText = resolveStepText(msg.testStepFinished.testStepId);
|
|
178
|
+
if (stepText) {
|
|
179
|
+
const status = msg.testStepFinished.testStepResult.status.toLowerCase();
|
|
180
|
+
const stepIndex = steps.findIndex(s => stripKeyword(s) === stepText);
|
|
181
|
+
if (stepIndex >= 0) {
|
|
182
|
+
setStepProgress(prev => {
|
|
183
|
+
const arr = [...(prev[key] || [])];
|
|
184
|
+
arr[stepIndex] = status === 'passed' ? 'passed' : 'failed';
|
|
185
|
+
return { ...prev, [key]: arr };
|
|
186
|
+
});
|
|
187
|
+
}
|
|
144
188
|
}
|
|
145
|
-
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
146
191
|
|
|
192
|
+
return new Promise<void>(async (resolve) => {
|
|
147
193
|
function cleanup() {
|
|
148
|
-
eventSource.close();
|
|
149
|
-
// Stop timer
|
|
150
194
|
const { [key]: _, ...restStarts } = elapsedStartRef.current;
|
|
151
195
|
elapsedStartRef.current = restStarts;
|
|
152
|
-
setElapsedDisplay(prev => {
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
});
|
|
156
|
-
// Clear step progress
|
|
157
|
-
setStepProgress(prev => {
|
|
158
|
-
const { [key]: _, ...rest } = prev;
|
|
159
|
-
return rest;
|
|
160
|
-
});
|
|
161
|
-
setRunningScenarios(prev => {
|
|
162
|
-
const next = new Set(prev);
|
|
163
|
-
next.delete(key);
|
|
164
|
-
return next;
|
|
165
|
-
});
|
|
196
|
+
setElapsedDisplay(prev => { const { [key]: _, ...rest } = prev; return rest; });
|
|
197
|
+
setStepProgress(prev => { const { [key]: _, ...rest } = prev; return rest; });
|
|
198
|
+
setRunningScenarios(prev => { const next = new Set(prev); next.delete(key); return next; });
|
|
166
199
|
}
|
|
167
200
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
201
|
+
try {
|
|
202
|
+
const escapedTitle = scenarioTitle.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
203
|
+
const resultsPath = projectRootRef.current
|
|
204
|
+
? `${projectRootRef.current}/cucumber-results.json`
|
|
205
|
+
: 'cucumber-results.json';
|
|
206
|
+
|
|
207
|
+
// Spawn cucumber-js first, then set up listeners
|
|
208
|
+
const pid = await invoke<number>('spawn_process', {
|
|
209
|
+
command: 'npx',
|
|
210
|
+
args: [
|
|
211
|
+
'cucumber-js', featureFile,
|
|
212
|
+
'--name', `^${escapedTitle}$`,
|
|
213
|
+
'--format', 'message',
|
|
214
|
+
'--format', `json:${resultsPath}`,
|
|
215
|
+
],
|
|
216
|
+
label: 'test-runner',
|
|
217
|
+
kind: null,
|
|
218
|
+
cwd: projectRootRef.current,
|
|
219
|
+
env: null,
|
|
220
|
+
});
|
|
221
|
+
activeProcessesRef.current.add(pid);
|
|
222
|
+
|
|
223
|
+
// Listen for stdout lines to parse NDJSON
|
|
224
|
+
const outputUnlisten = await listen<{ pid: number; lines: string[] }>(
|
|
225
|
+
'process-output-batch',
|
|
226
|
+
(event) => {
|
|
227
|
+
if (event.payload.pid !== pid) return;
|
|
228
|
+
for (const line of event.payload.lines) {
|
|
229
|
+
outputBuffer += line + '\n';
|
|
230
|
+
const bufLines = outputBuffer.split('\n');
|
|
231
|
+
outputBuffer = bufLines.pop() || '';
|
|
232
|
+
for (const l of bufLines) processLine(l);
|
|
233
|
+
}
|
|
234
|
+
},
|
|
235
|
+
);
|
|
236
|
+
unlistenersRef.current.push(outputUnlisten);
|
|
237
|
+
|
|
238
|
+
// Listen for process exit
|
|
239
|
+
const exitUnlisten = await listen<{ pid: number; code: number | null }>(
|
|
240
|
+
'process-exit',
|
|
241
|
+
async (event) => {
|
|
242
|
+
if (event.payload.pid !== pid) return;
|
|
243
|
+
activeProcessesRef.current.delete(pid);
|
|
244
|
+
// Process remaining buffer
|
|
245
|
+
if (outputBuffer.trim()) processLine(outputBuffer);
|
|
246
|
+
// Ingest results into SQLite and refresh
|
|
247
|
+
try { await dataBridge.ingestTestResults(); } catch { /* best effort */ }
|
|
248
|
+
cleanup();
|
|
249
|
+
await refreshData();
|
|
250
|
+
resolve();
|
|
251
|
+
},
|
|
252
|
+
);
|
|
253
|
+
unlistenersRef.current.push(exitUnlisten);
|
|
254
|
+
} catch {
|
|
175
255
|
cleanup();
|
|
176
256
|
await refreshData();
|
|
177
257
|
resolve();
|
|
178
|
-
}
|
|
258
|
+
}
|
|
179
259
|
});
|
|
180
260
|
}, [refreshData]);
|
|
181
261
|
|
|
182
262
|
const runAllScenarios = useCallback(async (feature: TestFeature) => {
|
|
183
263
|
setRunningAll(true);
|
|
184
264
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
265
|
+
// Cucumber NDJSON message parsing state
|
|
266
|
+
const pickleSteps = new Map<string, { text: string }>();
|
|
267
|
+
const pickleNames = new Map<string, string>();
|
|
268
|
+
const testCaseSteps = new Map<string, { id: string; pickleStepId?: string }[]>();
|
|
269
|
+
const testCasePickleIds = new Map<string, string>();
|
|
270
|
+
const testCaseStartedIds = new Map<string, string>();
|
|
271
|
+
let activeTestCaseId: string | null = null;
|
|
272
|
+
let outputBuffer = '';
|
|
273
|
+
|
|
274
|
+
function getScenarioName(testCaseId: string): string | null {
|
|
275
|
+
const pickleId = testCasePickleIds.get(testCaseId);
|
|
276
|
+
return pickleId ? pickleNames.get(pickleId) ?? null : null;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function resolveStepText(testStepId: string): string | null {
|
|
280
|
+
if (!activeTestCaseId) return null;
|
|
281
|
+
const tcSteps = testCaseSteps.get(activeTestCaseId);
|
|
282
|
+
if (!tcSteps) return null;
|
|
283
|
+
const testStep = tcSteps.find(s => s.id === testStepId);
|
|
284
|
+
if (!testStep?.pickleStepId) return null;
|
|
285
|
+
const ps = pickleSteps.get(testStep.pickleStepId);
|
|
286
|
+
return ps ? ps.text : null;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function processLine(line: string) {
|
|
290
|
+
const trimmed = line.trim();
|
|
291
|
+
if (!trimmed || !trimmed.startsWith('{')) return;
|
|
292
|
+
let msg: Record<string, any>;
|
|
293
|
+
try { msg = JSON.parse(trimmed); } catch { return; }
|
|
294
|
+
|
|
295
|
+
if (msg.pickle) {
|
|
296
|
+
pickleNames.set(msg.pickle.id, msg.pickle.name);
|
|
297
|
+
for (const s of msg.pickle.steps) pickleSteps.set(s.id, s);
|
|
298
|
+
}
|
|
299
|
+
if (msg.testCase) {
|
|
300
|
+
testCaseSteps.set(msg.testCase.id, msg.testCase.testSteps);
|
|
301
|
+
testCasePickleIds.set(msg.testCase.id, msg.testCase.pickleId);
|
|
302
|
+
}
|
|
303
|
+
if (msg.testCaseStarted) {
|
|
304
|
+
const tcs = msg.testCaseStarted;
|
|
305
|
+
activeTestCaseId = tcs.testCaseId;
|
|
306
|
+
testCaseStartedIds.set(tcs.id, tcs.testCaseId);
|
|
307
|
+
const scenario = getScenarioName(tcs.testCaseId);
|
|
308
|
+
if (scenario) {
|
|
309
|
+
const key = `${feature.featureFile}::${scenario}`;
|
|
310
|
+
const matchedScenario = feature.scenarios.find(s => s.title === scenario);
|
|
311
|
+
setRunningScenarios(prev => new Set(prev).add(key));
|
|
312
|
+
if (matchedScenario?.steps?.length) {
|
|
313
|
+
setStepProgress(prev => ({
|
|
314
|
+
...prev,
|
|
315
|
+
[key]: matchedScenario.steps.map(() => 'pending' as StepStatus),
|
|
316
|
+
}));
|
|
317
|
+
}
|
|
318
|
+
elapsedStartRef.current = { ...elapsedStartRef.current, [key]: Date.now() };
|
|
319
|
+
setElapsedDisplay(prev => ({ ...prev, [key]: 0 }));
|
|
205
320
|
}
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
});
|
|
321
|
+
}
|
|
322
|
+
if (msg.testStepStarted) {
|
|
323
|
+
const stepText = resolveStepText(msg.testStepStarted.testStepId);
|
|
324
|
+
const scenario = activeTestCaseId ? getScenarioName(activeTestCaseId) : null;
|
|
325
|
+
if (stepText && scenario) {
|
|
326
|
+
const key = `${feature.featureFile}::${scenario}`;
|
|
327
|
+
const matchedScenario = feature.scenarios.find(s => s.title === scenario);
|
|
328
|
+
const steps = matchedScenario?.steps || [];
|
|
329
|
+
const stepIndex = steps.findIndex(s => stripKeyword(s) === stepText);
|
|
330
|
+
if (stepIndex >= 0) {
|
|
331
|
+
setStepProgress(prev => {
|
|
332
|
+
const arr = [...(prev[key] || [])];
|
|
333
|
+
arr[stepIndex] = 'running';
|
|
334
|
+
return { ...prev, [key]: arr };
|
|
335
|
+
});
|
|
336
|
+
}
|
|
223
337
|
}
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
const
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
338
|
+
}
|
|
339
|
+
if (msg.testStepFinished) {
|
|
340
|
+
const stepText = resolveStepText(msg.testStepFinished.testStepId);
|
|
341
|
+
const scenario = activeTestCaseId ? getScenarioName(activeTestCaseId) : null;
|
|
342
|
+
if (stepText && scenario) {
|
|
343
|
+
const key = `${feature.featureFile}::${scenario}`;
|
|
344
|
+
const status = msg.testStepFinished.testStepResult.status.toLowerCase();
|
|
345
|
+
const matchedScenario = feature.scenarios.find(s => s.title === scenario);
|
|
346
|
+
const steps = matchedScenario?.steps || [];
|
|
347
|
+
const stepIndex = steps.findIndex(s => stripKeyword(s) === stepText);
|
|
348
|
+
if (stepIndex >= 0) {
|
|
349
|
+
setStepProgress(prev => {
|
|
350
|
+
const arr = [...(prev[key] || [])];
|
|
351
|
+
arr[stepIndex] = status === 'passed' ? 'passed' : 'failed';
|
|
352
|
+
return { ...prev, [key]: arr };
|
|
353
|
+
});
|
|
354
|
+
}
|
|
239
355
|
}
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
const
|
|
244
|
-
const
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
const { [key]: _, ...rest } = prev;
|
|
250
|
-
return rest;
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
});
|
|
256
|
-
setRunningScenarios(prev => {
|
|
257
|
-
const next = new Set(prev);
|
|
258
|
-
next.delete(key);
|
|
259
|
-
return next;
|
|
260
|
-
});
|
|
261
|
-
});
|
|
356
|
+
}
|
|
357
|
+
if (msg.testCaseFinished) {
|
|
358
|
+
const tcf = msg.testCaseFinished;
|
|
359
|
+
const testCaseId = testCaseStartedIds.get(tcf.testCaseStartedId);
|
|
360
|
+
const scenario = testCaseId ? getScenarioName(testCaseId) : null;
|
|
361
|
+
if (scenario) {
|
|
362
|
+
const key = `${feature.featureFile}::${scenario}`;
|
|
363
|
+
const { [key]: _, ...restStarts } = elapsedStartRef.current;
|
|
364
|
+
elapsedStartRef.current = restStarts;
|
|
365
|
+
setElapsedDisplay(prev => { const { [key]: _, ...rest } = prev; return rest; });
|
|
366
|
+
setStepProgress(prev => { const { [key]: _, ...rest } = prev; return rest; });
|
|
367
|
+
setRunningScenarios(prev => { const next = new Set(prev); next.delete(key); return next; });
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
262
371
|
|
|
372
|
+
return new Promise<void>(async (resolve) => {
|
|
263
373
|
function cleanup() {
|
|
264
|
-
eventSource.close();
|
|
265
|
-
// Clean up any remaining scenario state
|
|
266
374
|
for (const scenario of feature.scenarios) {
|
|
267
375
|
const key = `${feature.featureFile}::${scenario.title}`;
|
|
268
376
|
const { [key]: _, ...restStarts } = elapsedStartRef.current;
|
|
@@ -274,26 +382,63 @@ export function RealTimeTestsWrapper({ initialData }: RealTimeTestsWrapperProps)
|
|
|
274
382
|
setRunningAll(false);
|
|
275
383
|
}
|
|
276
384
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
385
|
+
try {
|
|
386
|
+
const resultsPath = projectRootRef.current
|
|
387
|
+
? `${projectRootRef.current}/cucumber-results.json`
|
|
388
|
+
: 'cucumber-results.json';
|
|
389
|
+
|
|
390
|
+
// Spawn cucumber-js first, then set up listeners
|
|
391
|
+
const pid = await invoke<number>('spawn_process', {
|
|
392
|
+
command: 'npx',
|
|
393
|
+
args: [
|
|
394
|
+
'cucumber-js', feature.featureFile,
|
|
395
|
+
'--format', 'message',
|
|
396
|
+
'--format', `json:${resultsPath}`,
|
|
397
|
+
],
|
|
398
|
+
label: 'test-runner',
|
|
399
|
+
kind: null,
|
|
400
|
+
cwd: projectRootRef.current,
|
|
401
|
+
env: null,
|
|
402
|
+
});
|
|
403
|
+
activeProcessesRef.current.add(pid);
|
|
404
|
+
|
|
405
|
+
const outputUnlisten = await listen<{ pid: number; lines: string[] }>(
|
|
406
|
+
'process-output-batch',
|
|
407
|
+
(event) => {
|
|
408
|
+
if (event.payload.pid !== pid) return;
|
|
409
|
+
for (const line of event.payload.lines) {
|
|
410
|
+
outputBuffer += line + '\n';
|
|
411
|
+
const bufLines = outputBuffer.split('\n');
|
|
412
|
+
outputBuffer = bufLines.pop() || '';
|
|
413
|
+
for (const l of bufLines) processLine(l);
|
|
414
|
+
}
|
|
415
|
+
},
|
|
416
|
+
);
|
|
417
|
+
unlistenersRef.current.push(outputUnlisten);
|
|
418
|
+
|
|
419
|
+
const exitUnlisten = await listen<{ pid: number; code: number | null }>(
|
|
420
|
+
'process-exit',
|
|
421
|
+
async (event) => {
|
|
422
|
+
if (event.payload.pid !== pid) return;
|
|
423
|
+
activeProcessesRef.current.delete(pid);
|
|
424
|
+
if (outputBuffer.trim()) processLine(outputBuffer);
|
|
425
|
+
try { await dataBridge.ingestTestResults(); } catch { /* best effort */ }
|
|
426
|
+
cleanup();
|
|
427
|
+
await refreshData();
|
|
428
|
+
resolve();
|
|
429
|
+
},
|
|
430
|
+
);
|
|
431
|
+
unlistenersRef.current.push(exitUnlisten);
|
|
432
|
+
} catch {
|
|
284
433
|
cleanup();
|
|
285
434
|
await refreshData();
|
|
286
435
|
resolve();
|
|
287
|
-
}
|
|
436
|
+
}
|
|
288
437
|
});
|
|
289
438
|
}, [refreshData]);
|
|
290
439
|
|
|
291
|
-
const wsUrl = typeof window !== 'undefined'
|
|
292
|
-
? `ws://${window.location.hostname}:8080`
|
|
293
|
-
: 'ws://localhost:8080';
|
|
294
|
-
|
|
295
440
|
useWebSocket({
|
|
296
|
-
url:
|
|
441
|
+
url: getWebSocketUrl(),
|
|
297
442
|
onMessage: handleMessage,
|
|
298
443
|
});
|
|
299
444
|
|
|
@@ -348,43 +493,43 @@ export function RealTimeTestsWrapper({ initialData }: RealTimeTestsWrapperProps)
|
|
|
348
493
|
<div className="flex-shrink-0 px-6 py-4 border-b border-zinc-200">
|
|
349
494
|
<div className="flex gap-4">
|
|
350
495
|
<div className="flex items-center gap-2 px-4 py-2 bg-zinc-100 rounded-lg">
|
|
351
|
-
<span className="text-
|
|
496
|
+
<span className="text-base text-zinc-500">Total</span>
|
|
352
497
|
<span className="font-mono font-semibold text-zinc-900">
|
|
353
498
|
{data.summary.total}
|
|
354
499
|
</span>
|
|
355
500
|
</div>
|
|
356
501
|
<button
|
|
357
502
|
onClick={() => setStatusFilter(statusFilter === 'pass' ? null : 'pass')}
|
|
358
|
-
className={`flex items-center gap-2 px-4 py-2 bg-green-50 rounded-lg cursor-pointer transition-
|
|
503
|
+
className={`flex items-center gap-2 px-4 py-2 bg-green-50 rounded-lg cursor-pointer transition-[color,background-color] duration-200 ease-out ${
|
|
359
504
|
statusFilter === 'pass' ? 'ring-2 ring-green-500' : 'hover:bg-green-100'
|
|
360
505
|
}`}
|
|
361
506
|
data-testid="status-badge-passing"
|
|
362
507
|
>
|
|
363
|
-
<span className="text-
|
|
508
|
+
<span className="text-base text-green-600">Passing</span>
|
|
364
509
|
<span className="font-mono font-semibold text-green-700">
|
|
365
510
|
{data.summary.passing}
|
|
366
511
|
</span>
|
|
367
512
|
</button>
|
|
368
513
|
<button
|
|
369
514
|
onClick={() => setStatusFilter(statusFilter === 'fail' ? null : 'fail')}
|
|
370
|
-
className={`flex items-center gap-2 px-4 py-2 bg-red-50 rounded-lg cursor-pointer transition-
|
|
515
|
+
className={`flex items-center gap-2 px-4 py-2 bg-red-50 rounded-lg cursor-pointer transition-[color,background-color] duration-200 ease-out ${
|
|
371
516
|
statusFilter === 'fail' ? 'ring-2 ring-red-500' : 'hover:bg-red-100'
|
|
372
517
|
}`}
|
|
373
518
|
data-testid="status-badge-failing"
|
|
374
519
|
>
|
|
375
|
-
<span className="text-
|
|
520
|
+
<span className="text-base text-red-600">Failing</span>
|
|
376
521
|
<span className="font-mono font-semibold text-red-700">
|
|
377
522
|
{data.summary.failing}
|
|
378
523
|
</span>
|
|
379
524
|
</button>
|
|
380
525
|
<button
|
|
381
526
|
onClick={() => setStatusFilter(statusFilter === 'pending' ? null : 'pending')}
|
|
382
|
-
className={`flex items-center gap-2 px-4 py-2 bg-amber-50 rounded-lg cursor-pointer transition-
|
|
527
|
+
className={`flex items-center gap-2 px-4 py-2 bg-amber-50 rounded-lg cursor-pointer transition-[color,background-color] duration-200 ease-out ${
|
|
383
528
|
statusFilter === 'pending' ? 'ring-2 ring-amber-500' : 'hover:bg-amber-100'
|
|
384
529
|
}`}
|
|
385
530
|
data-testid="status-badge-pending"
|
|
386
531
|
>
|
|
387
|
-
<span className="text-
|
|
532
|
+
<span className="text-base text-amber-600">Pending</span>
|
|
388
533
|
<span className="font-mono font-semibold text-amber-700">
|
|
389
534
|
{data.summary.pending}
|
|
390
535
|
</span>
|
|
@@ -392,7 +537,7 @@ export function RealTimeTestsWrapper({ initialData }: RealTimeTestsWrapperProps)
|
|
|
392
537
|
{statusFilter && (
|
|
393
538
|
<button
|
|
394
539
|
onClick={() => setStatusFilter(null)}
|
|
395
|
-
className="flex items-center gap-1 px-3 py-2 text-
|
|
540
|
+
className="flex items-center gap-1 px-3 py-2 text-base text-zinc-500 hover:text-zinc-700 rounded-lg hover:bg-zinc-100 transition-colors duration-200 ease-out"
|
|
396
541
|
data-testid="clear-status-filter"
|
|
397
542
|
>
|
|
398
543
|
<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor">
|
|
@@ -402,7 +547,7 @@ export function RealTimeTestsWrapper({ initialData }: RealTimeTestsWrapperProps)
|
|
|
402
547
|
</button>
|
|
403
548
|
)}
|
|
404
549
|
{data.summary.lastRun && (
|
|
405
|
-
<div className="flex items-center gap-2 px-4 py-2 text-
|
|
550
|
+
<div className="flex items-center gap-2 px-4 py-2 text-base text-zinc-500">
|
|
406
551
|
Last run: {parseUtcDate(data.summary.lastRun).toLocaleString()}
|
|
407
552
|
</div>
|
|
408
553
|
)}
|
|
@@ -414,13 +559,13 @@ export function RealTimeTestsWrapper({ initialData }: RealTimeTestsWrapperProps)
|
|
|
414
559
|
{/* Left Sidebar - Feature List */}
|
|
415
560
|
<div className="w-80 flex-shrink-0 border-r border-zinc-200 flex flex-col overflow-hidden">
|
|
416
561
|
{/* Search */}
|
|
417
|
-
<div className="p-
|
|
418
|
-
<
|
|
562
|
+
<div className="p-4 border-b border-zinc-200">
|
|
563
|
+
<Input
|
|
419
564
|
type="text"
|
|
420
565
|
placeholder="Search features..."
|
|
421
566
|
value={searchText}
|
|
422
567
|
onChange={(e) => setSearchText(e.target.value)}
|
|
423
|
-
|
|
568
|
+
size="sm"
|
|
424
569
|
/>
|
|
425
570
|
</div>
|
|
426
571
|
|
|
@@ -430,17 +575,17 @@ export function RealTimeTestsWrapper({ initialData }: RealTimeTestsWrapperProps)
|
|
|
430
575
|
<div className="flex flex-col items-center justify-center h-full px-6 text-center">
|
|
431
576
|
{statusFilter && searchText ? (
|
|
432
577
|
<>
|
|
433
|
-
<span className="text-zinc-400 text-
|
|
578
|
+
<span className="text-zinc-400 text-base">No {statusFilter === 'pass' ? 'passing' : statusFilter === 'fail' ? 'failing' : 'pending'} features matching “{searchText}”</span>
|
|
434
579
|
<div className="flex gap-2 mt-2">
|
|
435
580
|
<button
|
|
436
581
|
onClick={() => setStatusFilter(null)}
|
|
437
|
-
className="text-
|
|
582
|
+
className="text-base text-zinc-500 hover:text-zinc-700 underline"
|
|
438
583
|
>
|
|
439
584
|
Clear filter
|
|
440
585
|
</button>
|
|
441
586
|
<button
|
|
442
587
|
onClick={() => setSearchText('')}
|
|
443
|
-
className="text-
|
|
588
|
+
className="text-base text-zinc-500 hover:text-zinc-700 underline"
|
|
444
589
|
>
|
|
445
590
|
Clear search
|
|
446
591
|
</button>
|
|
@@ -448,28 +593,28 @@ export function RealTimeTestsWrapper({ initialData }: RealTimeTestsWrapperProps)
|
|
|
448
593
|
</>
|
|
449
594
|
) : statusFilter ? (
|
|
450
595
|
<>
|
|
451
|
-
<span className="text-zinc-400 text-
|
|
596
|
+
<span className="text-zinc-400 text-base">No {statusFilter === 'pass' ? 'passing' : statusFilter === 'fail' ? 'failing' : 'pending'} features</span>
|
|
452
597
|
<button
|
|
453
598
|
onClick={() => setStatusFilter(null)}
|
|
454
|
-
className="mt-2 text-
|
|
599
|
+
className="mt-2 text-base text-zinc-500 hover:text-zinc-700 underline"
|
|
455
600
|
>
|
|
456
601
|
Clear filter
|
|
457
602
|
</button>
|
|
458
603
|
</>
|
|
459
604
|
) : searchText ? (
|
|
460
605
|
<>
|
|
461
|
-
<span className="text-zinc-400 text-
|
|
606
|
+
<span className="text-zinc-400 text-base">No results for “{searchText}”</span>
|
|
462
607
|
<button
|
|
463
608
|
onClick={() => setSearchText('')}
|
|
464
|
-
className="mt-2 text-
|
|
609
|
+
className="mt-2 text-base text-zinc-500 hover:text-zinc-700 underline"
|
|
465
610
|
>
|
|
466
611
|
Clear search
|
|
467
612
|
</button>
|
|
468
613
|
</>
|
|
469
614
|
) : (
|
|
470
615
|
<>
|
|
471
|
-
<span className="text-zinc-400 text-
|
|
472
|
-
<span className="text-zinc-300 text-
|
|
616
|
+
<span className="text-zinc-400 text-base">No features found</span>
|
|
617
|
+
<span className="text-zinc-300 text-base mt-1">Add .feature files to get started</span>
|
|
473
618
|
</>
|
|
474
619
|
)}
|
|
475
620
|
</div>
|
|
@@ -484,21 +629,21 @@ export function RealTimeTestsWrapper({ initialData }: RealTimeTestsWrapperProps)
|
|
|
484
629
|
<button
|
|
485
630
|
key={feature.id}
|
|
486
631
|
onClick={() => setSelectedFeatureId(feature.id)}
|
|
487
|
-
className={`w-full text-left px-4 py-3 border-b border-zinc-100 hover:bg-zinc-50 transition-colors ${
|
|
632
|
+
className={`w-full text-left px-4 py-3 border-b border-zinc-100 hover:bg-zinc-50 transition-colors duration-200 ease-out ${
|
|
488
633
|
isSelected ? 'bg-zinc-100 border-l-2 border-l-zinc-900' : ''
|
|
489
634
|
}`}
|
|
490
635
|
>
|
|
491
|
-
<div className="flex items-center justify-between gap-
|
|
492
|
-
<span className={`text-
|
|
636
|
+
<div className="flex items-center justify-between gap-3">
|
|
637
|
+
<span className={`text-base font-medium truncate ${
|
|
493
638
|
status === 'fail' ? 'text-red-700' : 'text-zinc-900'
|
|
494
639
|
}`}>
|
|
495
640
|
{feature.title}
|
|
496
641
|
</span>
|
|
497
|
-
<span className={`flex-shrink-0 text-xs font-mono px-2 py-
|
|
642
|
+
<span className={`flex-shrink-0 text-xs font-mono px-2.5 py-1 rounded-full ${statusColors[status]}`}>
|
|
498
643
|
{passingCount}/{totalCount}
|
|
499
644
|
</span>
|
|
500
645
|
</div>
|
|
501
|
-
<div className="text-
|
|
646
|
+
<div className="text-base text-zinc-400 mt-0.5 truncate">
|
|
502
647
|
{feature.featureFile}
|
|
503
648
|
</div>
|
|
504
649
|
</button>
|
|
@@ -510,42 +655,33 @@ export function RealTimeTestsWrapper({ initialData }: RealTimeTestsWrapperProps)
|
|
|
510
655
|
{/* Right Panel - Detail */}
|
|
511
656
|
<div className="flex-1 overflow-y-auto">
|
|
512
657
|
{selectedFeature ? (
|
|
513
|
-
<div className="p-
|
|
514
|
-
<div className="mb-
|
|
515
|
-
<div className="flex items-start justify-between gap-
|
|
658
|
+
<div className="p-8">
|
|
659
|
+
<div className="mb-8">
|
|
660
|
+
<div className="flex items-start justify-between gap-5">
|
|
516
661
|
<div>
|
|
517
662
|
<h2 className="text-lg font-semibold text-zinc-900">{selectedFeature.title}</h2>
|
|
518
|
-
<p className="text-
|
|
663
|
+
<p className="text-base text-zinc-500 font-mono mt-1">{selectedFeature.featureFile}</p>
|
|
519
664
|
{selectedFeature.description && (
|
|
520
|
-
<p className="text-
|
|
665
|
+
<p className="text-base text-zinc-600 mt-2">{selectedFeature.description}</p>
|
|
521
666
|
)}
|
|
522
667
|
</div>
|
|
523
|
-
<
|
|
668
|
+
<Button
|
|
524
669
|
onClick={() => {
|
|
525
670
|
if (!runningAll && runningScenarios.size === 0) {
|
|
526
671
|
runAllScenarios(selectedFeature);
|
|
527
672
|
}
|
|
528
673
|
}}
|
|
529
674
|
disabled={runningAll || runningScenarios.size > 0}
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
: 'text-white bg-zinc-900 hover:bg-zinc-800'
|
|
534
|
-
}`}
|
|
675
|
+
loading={runningAll}
|
|
676
|
+
size="sm"
|
|
677
|
+
className="flex-shrink-0"
|
|
535
678
|
>
|
|
536
|
-
{runningAll ?
|
|
537
|
-
|
|
538
|
-
<span className="inline-block w-3.5 h-3.5 border-2 border-zinc-300 border-t-white rounded-full animate-spin" />
|
|
539
|
-
Running All...
|
|
540
|
-
</span>
|
|
541
|
-
) : (
|
|
542
|
-
'Run All'
|
|
543
|
-
)}
|
|
544
|
-
</button>
|
|
679
|
+
{runningAll ? 'Running All...' : 'Run All'}
|
|
680
|
+
</Button>
|
|
545
681
|
</div>
|
|
546
682
|
</div>
|
|
547
683
|
|
|
548
|
-
<div className="space-y-
|
|
684
|
+
<div className="space-y-5">
|
|
549
685
|
{selectedFeature.scenarios.map(scenario => {
|
|
550
686
|
const scenarioKey = `${selectedFeature.featureFile}::${scenario.title}`;
|
|
551
687
|
const isRunning = runningScenarios.has(scenarioKey);
|
|
@@ -554,32 +690,32 @@ export function RealTimeTestsWrapper({ initialData }: RealTimeTestsWrapperProps)
|
|
|
554
690
|
return (
|
|
555
691
|
<div
|
|
556
692
|
key={scenario.id}
|
|
557
|
-
className={`rounded-lg
|
|
693
|
+
className={`rounded-lg ${
|
|
558
694
|
isRunning
|
|
559
|
-
? 'border-
|
|
695
|
+
? 'border-2 border-[#819D9F]/30 bg-[#e8f0f0]'
|
|
560
696
|
: scenario.status === 'fail'
|
|
561
|
-
? 'border-red-200 bg-red-50'
|
|
697
|
+
? 'border-2 border-red-200 bg-red-50'
|
|
562
698
|
: scenario.status === 'pending'
|
|
563
|
-
? 'border-amber-200 bg-amber-50'
|
|
564
|
-
: '
|
|
699
|
+
? 'border-2 border-amber-200 bg-amber-50'
|
|
700
|
+
: 'bg-white'
|
|
565
701
|
}`}
|
|
566
702
|
>
|
|
567
|
-
<div className="flex items-center justify-between p-
|
|
568
|
-
<div className="flex items-center gap-
|
|
569
|
-
<span className="text-
|
|
703
|
+
<div className="flex items-center justify-between p-5">
|
|
704
|
+
<div className="flex items-center gap-3">
|
|
705
|
+
<span className="text-base">
|
|
570
706
|
{isRunning ? '🔄' : scenario.status === 'pass' ? '✅' : scenario.status === 'fail' ? '❌' : '⏳'}
|
|
571
707
|
</span>
|
|
572
|
-
<span className={`text-
|
|
573
|
-
isRunning ? 'text-
|
|
708
|
+
<span className={`text-base font-medium ${
|
|
709
|
+
isRunning ? 'text-[#5a7d7f]' : scenario.status === 'fail' ? 'text-red-700' : 'text-zinc-900'
|
|
574
710
|
}`}>
|
|
575
711
|
{scenario.title}
|
|
576
712
|
</span>
|
|
577
713
|
</div>
|
|
578
|
-
<div className="flex items-center gap-
|
|
714
|
+
<div className="flex items-center gap-3">
|
|
579
715
|
{isRunning && elapsed !== undefined ? (
|
|
580
|
-
<span className="text-xs font-mono text-
|
|
716
|
+
<span className="text-xs font-mono text-[#819D9F]">{elapsed}s</span>
|
|
581
717
|
) : (
|
|
582
|
-
<div className="flex items-center gap-
|
|
718
|
+
<div className="flex items-center gap-3">
|
|
583
719
|
<span className="text-xs font-mono text-zinc-400">{scenario.duration}</span>
|
|
584
720
|
{scenario.lastRun && (
|
|
585
721
|
<span className="text-xs text-zinc-400" title={parseUtcDate(scenario.lastRun).toLocaleString()}>
|
|
@@ -596,7 +732,7 @@ export function RealTimeTestsWrapper({ initialData }: RealTimeTestsWrapperProps)
|
|
|
596
732
|
}
|
|
597
733
|
}}
|
|
598
734
|
disabled={isRunning}
|
|
599
|
-
className={`px-
|
|
735
|
+
className={`px-3 py-1.5 text-base font-medium rounded transition-colors duration-200 ease-out ${
|
|
600
736
|
isRunning
|
|
601
737
|
? 'text-zinc-400 bg-zinc-50 cursor-not-allowed'
|
|
602
738
|
: 'text-zinc-600 bg-zinc-100 hover:bg-zinc-200'
|
|
@@ -604,7 +740,7 @@ export function RealTimeTestsWrapper({ initialData }: RealTimeTestsWrapperProps)
|
|
|
604
740
|
>
|
|
605
741
|
{isRunning ? (
|
|
606
742
|
<span className="flex items-center gap-1">
|
|
607
|
-
<span className="inline-block w-3 h-3 border-2 border-
|
|
743
|
+
<span className="inline-block w-3 h-3 border-2 border-[#819D9F]/50 border-t-[#5a7d7f] rounded-full animate-spin" />
|
|
608
744
|
Running...
|
|
609
745
|
</span>
|
|
610
746
|
) : (
|
|
@@ -616,8 +752,8 @@ export function RealTimeTestsWrapper({ initialData }: RealTimeTestsWrapperProps)
|
|
|
616
752
|
|
|
617
753
|
{/* Gherkin Steps */}
|
|
618
754
|
{scenario.steps && scenario.steps.length > 0 && (
|
|
619
|
-
<div className="px-
|
|
620
|
-
<div className="mt-
|
|
755
|
+
<div className="px-5 pb-4 border-t border-zinc-100">
|
|
756
|
+
<div className="mt-4 space-y-1.5">
|
|
621
757
|
{scenario.steps.map((step: string, i: number) => {
|
|
622
758
|
const isFailedStep = scenario.failedStep && step.includes(scenario.failedStep.replace(/^(Given |When |Then |And |But )/, ''));
|
|
623
759
|
const scenarioUndefined = undefinedStepsMap[scenario.title] || [];
|
|
@@ -626,9 +762,9 @@ export function RealTimeTestsWrapper({ initialData }: RealTimeTestsWrapperProps)
|
|
|
626
762
|
return (
|
|
627
763
|
<div
|
|
628
764
|
key={i}
|
|
629
|
-
className={`text-xs font-mono py-1 px-
|
|
765
|
+
className={`text-xs font-mono py-1.5 px-3 rounded flex items-center gap-2 ${
|
|
630
766
|
stepStatus === 'running'
|
|
631
|
-
? 'bg-
|
|
767
|
+
? 'bg-[#e8f0f0] text-zinc-900 border-l-2 border-[#819D9F]'
|
|
632
768
|
: stepStatus === 'passed'
|
|
633
769
|
? 'bg-green-50 text-green-800 border-l-2 border-green-400'
|
|
634
770
|
: stepStatus === 'failed'
|
|
@@ -644,7 +780,7 @@ export function RealTimeTestsWrapper({ initialData }: RealTimeTestsWrapperProps)
|
|
|
644
780
|
>
|
|
645
781
|
{stepStatus === 'running' && (
|
|
646
782
|
<span className="flex-shrink-0">
|
|
647
|
-
<span className="inline-block w-2.5 h-2.5 border-2 border-
|
|
783
|
+
<span className="inline-block w-2.5 h-2.5 border-2 border-[#819D9F]/50 border-t-[#5a7d7f] rounded-full animate-spin" />
|
|
648
784
|
</span>
|
|
649
785
|
)}
|
|
650
786
|
{stepStatus === 'passed' && (
|
|
@@ -673,18 +809,18 @@ export function RealTimeTestsWrapper({ initialData }: RealTimeTestsWrapperProps)
|
|
|
673
809
|
|
|
674
810
|
{/* Error Details */}
|
|
675
811
|
{scenario.status === 'fail' && scenario.error && (
|
|
676
|
-
<div className="px-
|
|
812
|
+
<div className="px-5 pb-5">
|
|
677
813
|
{scenario.failedStep && (
|
|
678
|
-
<div className="mb-
|
|
814
|
+
<div className="mb-3 text-base font-medium text-red-700">
|
|
679
815
|
Failed at: {scenario.failedStep}
|
|
680
816
|
</div>
|
|
681
817
|
)}
|
|
682
|
-
<div className="p-
|
|
818
|
+
<div className="p-4 bg-red-100 rounded border-2 border-red-200">
|
|
683
819
|
<pre className="text-xs text-red-800 whitespace-pre-wrap font-mono">
|
|
684
820
|
{scenario.error}
|
|
685
821
|
</pre>
|
|
686
822
|
</div>
|
|
687
|
-
<
|
|
823
|
+
<Button
|
|
688
824
|
onClick={(e) => {
|
|
689
825
|
e.stopPropagation();
|
|
690
826
|
createFixScenarioSession(
|
|
@@ -695,10 +831,12 @@ export function RealTimeTestsWrapper({ initialData }: RealTimeTestsWrapperProps)
|
|
|
695
831
|
scenario.steps
|
|
696
832
|
);
|
|
697
833
|
}}
|
|
698
|
-
className="mt-
|
|
834
|
+
className="mt-4"
|
|
835
|
+
variant="accent"
|
|
836
|
+
size="sm"
|
|
699
837
|
>
|
|
700
838
|
Fix with Claude
|
|
701
|
-
</
|
|
839
|
+
</Button>
|
|
702
840
|
</div>
|
|
703
841
|
)}
|
|
704
842
|
</div>
|
|
@@ -710,11 +848,11 @@ export function RealTimeTestsWrapper({ initialData }: RealTimeTestsWrapperProps)
|
|
|
710
848
|
<div className="flex flex-col items-center justify-center h-full text-center px-6">
|
|
711
849
|
{allFeatures.length === 0 ? (
|
|
712
850
|
<>
|
|
713
|
-
<span className="text-zinc-400 text-
|
|
714
|
-
<span className="text-zinc-300 text-
|
|
851
|
+
<span className="text-zinc-400 text-base">No test features available</span>
|
|
852
|
+
<span className="text-zinc-300 text-base mt-1">Create BDD feature files in the features/ directory to see test results here.</span>
|
|
715
853
|
</>
|
|
716
854
|
) : (
|
|
717
|
-
<span className="text-zinc-400 text-
|
|
855
|
+
<span className="text-zinc-400 text-base">Select a feature from the sidebar to view its scenarios</span>
|
|
718
856
|
)}
|
|
719
857
|
</div>
|
|
720
858
|
)}
|