claudeship 0.2.11 → 0.2.14
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/apps/server/dist/app.module.js +8 -0
- package/apps/server/dist/app.module.js.map +1 -1
- package/apps/server/dist/chat/prompts/fullstack-express-prompt.d.ts +1 -1
- package/apps/server/dist/chat/prompts/fullstack-express-prompt.js +110 -1
- package/apps/server/dist/chat/prompts/fullstack-express-prompt.js.map +1 -1
- package/apps/server/dist/chat/prompts/fullstack-fastapi-prompt.d.ts +1 -1
- package/apps/server/dist/chat/prompts/fullstack-fastapi-prompt.js +110 -1
- package/apps/server/dist/chat/prompts/fullstack-fastapi-prompt.js.map +1 -1
- package/apps/server/dist/chat/prompts/web-system-prompt.d.ts +1 -1
- package/apps/server/dist/chat/prompts/web-system-prompt.js +157 -0
- package/apps/server/dist/chat/prompts/web-system-prompt.js.map +1 -1
- package/apps/server/dist/checkpoint/checkpoint.controller.d.ts +19 -0
- package/apps/server/dist/checkpoint/checkpoint.controller.js +93 -0
- package/apps/server/dist/checkpoint/checkpoint.controller.js.map +1 -0
- package/apps/server/dist/checkpoint/checkpoint.module.d.ts +2 -0
- package/apps/server/dist/checkpoint/checkpoint.module.js +25 -0
- package/apps/server/dist/checkpoint/checkpoint.module.js.map +1 -0
- package/apps/server/dist/checkpoint/checkpoint.service.d.ts +41 -0
- package/apps/server/dist/checkpoint/checkpoint.service.js +261 -0
- package/apps/server/dist/checkpoint/checkpoint.service.js.map +1 -0
- package/apps/server/dist/database/database.controller.d.ts +23 -0
- package/apps/server/dist/database/database.controller.js +109 -0
- package/apps/server/dist/database/database.controller.js.map +1 -0
- package/apps/server/dist/database/database.module.d.ts +2 -0
- package/apps/server/dist/database/database.module.js +25 -0
- package/apps/server/dist/database/database.module.js.map +1 -0
- package/apps/server/dist/database/database.service.d.ts +32 -0
- package/apps/server/dist/database/database.service.js +238 -0
- package/apps/server/dist/database/database.service.js.map +1 -0
- package/apps/server/dist/preview/preview.controller.d.ts +5 -0
- package/apps/server/dist/preview/preview.controller.js +41 -0
- package/apps/server/dist/preview/preview.controller.js.map +1 -1
- package/apps/server/dist/preview/preview.service.d.ts +20 -0
- package/apps/server/dist/preview/preview.service.js +51 -2
- package/apps/server/dist/preview/preview.service.js.map +1 -1
- package/apps/server/dist/project/project.controller.d.ts +10 -1
- package/apps/server/dist/project/project.controller.js +57 -0
- package/apps/server/dist/project/project.controller.js.map +1 -1
- package/apps/server/dist/project/project.service.d.ts +15 -0
- package/apps/server/dist/project/project.service.js +111 -0
- package/apps/server/dist/project/project.service.js.map +1 -1
- package/apps/server/dist/project-context/project-context.controller.d.ts +42 -0
- package/apps/server/dist/project-context/project-context.controller.js +127 -0
- package/apps/server/dist/project-context/project-context.controller.js.map +1 -0
- package/apps/server/dist/project-context/project-context.module.d.ts +2 -0
- package/apps/server/dist/project-context/project-context.module.js +25 -0
- package/apps/server/dist/project-context/project-context.module.js.map +1 -0
- package/apps/server/dist/project-context/project-context.service.d.ts +36 -0
- package/apps/server/dist/project-context/project-context.service.js +260 -0
- package/apps/server/dist/project-context/project-context.service.js.map +1 -0
- package/apps/server/dist/testing/testing.controller.d.ts +24 -0
- package/apps/server/dist/testing/testing.controller.js +126 -0
- package/apps/server/dist/testing/testing.controller.js.map +1 -0
- package/apps/server/dist/testing/testing.module.d.ts +2 -0
- package/apps/server/dist/testing/testing.module.js +26 -0
- package/apps/server/dist/testing/testing.module.js.map +1 -0
- package/apps/server/dist/testing/testing.service.d.ts +62 -0
- package/apps/server/dist/testing/testing.service.js +269 -0
- package/apps/server/dist/testing/testing.service.js.map +1 -0
- package/apps/server/dist/tsconfig.tsbuildinfo +1 -1
- package/apps/server/package.json +1 -1
- package/apps/web/.next/BUILD_ID +1 -1
- package/apps/web/.next/app-build-manifest.json +7 -7
- package/apps/web/.next/app-path-routes-manifest.json +2 -2
- package/apps/web/.next/build-manifest.json +2 -2
- package/apps/web/.next/cache/.previewinfo +1 -1
- package/apps/web/.next/cache/.rscinfo +1 -1
- package/apps/web/.next/cache/.tsbuildinfo +1 -1
- package/apps/web/.next/cache/config.json +3 -3
- package/apps/web/.next/cache/eslint/.cache_j3uhuz +1 -1
- package/apps/web/.next/cache/webpack/client-production/0.pack +0 -0
- package/apps/web/.next/cache/webpack/client-production/index.pack +0 -0
- package/apps/web/.next/cache/webpack/edge-server-production/index.pack +0 -0
- package/apps/web/.next/cache/webpack/server-production/0.pack +0 -0
- package/apps/web/.next/cache/webpack/server-production/index.pack +0 -0
- package/apps/web/.next/prerender-manifest.json +13 -13
- package/apps/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/apps/web/.next/server/app/_not-found.html +1 -1
- package/apps/web/.next/server/app/_not-found.rsc +2 -2
- package/apps/web/.next/server/app/index.html +1 -1
- package/apps/web/.next/server/app/index.rsc +3 -3
- package/apps/web/.next/server/app/page.js +2 -2
- package/apps/web/.next/server/app/page_client-reference-manifest.js +1 -1
- package/apps/web/.next/server/app/project/[id]/page.js +2 -2
- package/apps/web/.next/server/app/project/[id]/page_client-reference-manifest.js +1 -1
- package/apps/web/.next/server/app/settings/page_client-reference-manifest.js +1 -1
- package/apps/web/.next/server/app/settings.html +1 -1
- package/apps/web/.next/server/app/settings.rsc +3 -3
- package/apps/web/.next/server/app-paths-manifest.json +2 -2
- package/apps/web/.next/server/chunks/392.js +1 -1
- package/apps/web/.next/server/pages/404.html +1 -1
- package/apps/web/.next/server/pages/500.html +1 -1
- package/apps/web/.next/server/server-reference-manifest.json +1 -1
- package/apps/web/.next/static/chunks/712-11fca77fb30a2a06.js +1 -0
- package/apps/web/.next/static/chunks/app/page-0db1c152fbd48359.js +1 -0
- package/apps/web/.next/static/chunks/app/project/[id]/page-7c44ae18c8984726.js +1 -0
- package/apps/web/.next/static/chunks/app/settings/page-d1318c2fd58729a5.js +1 -0
- package/apps/web/.next/static/css/d0f1b036f222bc16.css +3 -0
- package/apps/web/.next/trace +18 -17
- package/apps/web/node_modules/.bin/eslint +2 -2
- package/apps/web/package.json +2 -1
- package/apps/web/src/components/checkpoint/CheckpointPanel.tsx +384 -0
- package/apps/web/src/components/database/DatabasePanel.tsx +405 -0
- package/apps/web/src/components/env/EnvPanel.tsx +356 -0
- package/apps/web/src/components/preview/ConsoleViewer.tsx +270 -0
- package/apps/web/src/components/preview/ErrorOverlay.tsx +189 -0
- package/apps/web/src/components/preview/PreviewPanel.tsx +148 -6
- package/apps/web/src/components/testing/TestRunner.tsx +481 -0
- package/apps/web/src/components/ui/tabs.tsx +55 -0
- package/apps/web/src/components/visual-editor/VisualEditor.tsx +382 -0
- package/apps/web/src/lib/api.ts +5 -2
- package/package.json +1 -1
- package/apps/web/.next/static/chunks/298-6f3d6b321c288cd3.js +0 -1
- package/apps/web/.next/static/chunks/app/page-3d093f7f480a8599.js +0 -1
- package/apps/web/.next/static/chunks/app/project/[id]/page-e5cda6f9050b0a52.js +0 -1
- package/apps/web/.next/static/chunks/app/settings/page-92d28565c3d8c755.js +0 -1
- package/apps/web/.next/static/css/8f946046a2047594.css +0 -3
- /package/apps/web/.next/static/{zw4FcukMOho6_dzgpEdNW → tV_Qc76rupeogXvWEMw6p}/_buildManifest.js +0 -0
- /package/apps/web/.next/static/{zw4FcukMOho6_dzgpEdNW → tV_Qc76rupeogXvWEMw6p}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,481 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState, useCallback } from "react";
|
|
4
|
+
import {
|
|
5
|
+
Play,
|
|
6
|
+
Square,
|
|
7
|
+
Plus,
|
|
8
|
+
Trash2,
|
|
9
|
+
CheckCircle,
|
|
10
|
+
XCircle,
|
|
11
|
+
Clock,
|
|
12
|
+
RefreshCw,
|
|
13
|
+
AlertCircle,
|
|
14
|
+
FlaskConical,
|
|
15
|
+
} from "lucide-react";
|
|
16
|
+
import { Button } from "@/components/ui/button";
|
|
17
|
+
import { Input } from "@/components/ui/input";
|
|
18
|
+
import { api } from "@/lib/api";
|
|
19
|
+
|
|
20
|
+
interface TestStep {
|
|
21
|
+
action: "navigate" | "click" | "fill" | "waitFor" | "screenshot" | "assert";
|
|
22
|
+
selector?: string;
|
|
23
|
+
url?: string;
|
|
24
|
+
value?: string;
|
|
25
|
+
name?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface TestScenario {
|
|
29
|
+
id: string;
|
|
30
|
+
name: string;
|
|
31
|
+
steps: TestStep[];
|
|
32
|
+
createdAt: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface StepResult {
|
|
36
|
+
step: TestStep;
|
|
37
|
+
status: "passed" | "failed" | "skipped";
|
|
38
|
+
duration: number;
|
|
39
|
+
error?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface TestResult {
|
|
43
|
+
scenarioId: string;
|
|
44
|
+
scenarioName: string;
|
|
45
|
+
status: "passed" | "failed" | "running";
|
|
46
|
+
startTime: number;
|
|
47
|
+
endTime?: number;
|
|
48
|
+
steps: StepResult[];
|
|
49
|
+
error?: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
interface TestRunnerProps {
|
|
53
|
+
projectId: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const ACTION_LABELS: Record<TestStep["action"], string> = {
|
|
57
|
+
navigate: "Navigate to URL",
|
|
58
|
+
click: "Click element",
|
|
59
|
+
fill: "Fill input",
|
|
60
|
+
waitFor: "Wait for element",
|
|
61
|
+
screenshot: "Take screenshot",
|
|
62
|
+
assert: "Assert visible",
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export function TestRunner({ projectId }: TestRunnerProps) {
|
|
66
|
+
const [scenarios, setScenarios] = useState<TestScenario[]>([]);
|
|
67
|
+
const [selectedScenario, setSelectedScenario] = useState<TestScenario | null>(null);
|
|
68
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
69
|
+
const [isRunning, setIsRunning] = useState(false);
|
|
70
|
+
const [error, setError] = useState<string | null>(null);
|
|
71
|
+
const [lastResult, setLastResult] = useState<TestResult | null>(null);
|
|
72
|
+
const [isEditing, setIsEditing] = useState(false);
|
|
73
|
+
const [editName, setEditName] = useState("");
|
|
74
|
+
const [editSteps, setEditSteps] = useState<TestStep[]>([]);
|
|
75
|
+
|
|
76
|
+
// Fetch scenarios
|
|
77
|
+
const fetchScenarios = useCallback(async () => {
|
|
78
|
+
setIsLoading(true);
|
|
79
|
+
setError(null);
|
|
80
|
+
try {
|
|
81
|
+
const result = await api.get<TestScenario[]>(
|
|
82
|
+
`/projects/${projectId}/testing/scenarios`
|
|
83
|
+
);
|
|
84
|
+
setScenarios(result);
|
|
85
|
+
} catch (e) {
|
|
86
|
+
setError(e instanceof Error ? e.message : "Failed to load scenarios");
|
|
87
|
+
} finally {
|
|
88
|
+
setIsLoading(false);
|
|
89
|
+
}
|
|
90
|
+
}, [projectId]);
|
|
91
|
+
|
|
92
|
+
useEffect(() => {
|
|
93
|
+
fetchScenarios();
|
|
94
|
+
}, [fetchScenarios]);
|
|
95
|
+
|
|
96
|
+
const handleSelectScenario = (scenario: TestScenario) => {
|
|
97
|
+
setSelectedScenario(scenario);
|
|
98
|
+
setLastResult(null);
|
|
99
|
+
setIsEditing(false);
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const handleNewScenario = () => {
|
|
103
|
+
setSelectedScenario(null);
|
|
104
|
+
setIsEditing(true);
|
|
105
|
+
setEditName("New Test");
|
|
106
|
+
setEditSteps([{ action: "navigate", url: "/" }]);
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const handleEditScenario = () => {
|
|
110
|
+
if (!selectedScenario) return;
|
|
111
|
+
setIsEditing(true);
|
|
112
|
+
setEditName(selectedScenario.name);
|
|
113
|
+
setEditSteps([...selectedScenario.steps]);
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const handleSaveScenario = async () => {
|
|
117
|
+
if (!editName.trim() || editSteps.length === 0) {
|
|
118
|
+
setError("Name and at least one step are required");
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
setIsLoading(true);
|
|
123
|
+
setError(null);
|
|
124
|
+
try {
|
|
125
|
+
if (selectedScenario) {
|
|
126
|
+
await api.put(
|
|
127
|
+
`/projects/${projectId}/testing/scenarios/${selectedScenario.id}`,
|
|
128
|
+
{ name: editName, steps: editSteps }
|
|
129
|
+
);
|
|
130
|
+
} else {
|
|
131
|
+
await api.post(`/projects/${projectId}/testing/scenarios`, {
|
|
132
|
+
name: editName,
|
|
133
|
+
steps: editSteps,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
await fetchScenarios();
|
|
137
|
+
setIsEditing(false);
|
|
138
|
+
} catch (e) {
|
|
139
|
+
setError(e instanceof Error ? e.message : "Failed to save scenario");
|
|
140
|
+
} finally {
|
|
141
|
+
setIsLoading(false);
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const handleDeleteScenario = async () => {
|
|
146
|
+
if (!selectedScenario) return;
|
|
147
|
+
if (!confirm("Delete this test scenario?")) return;
|
|
148
|
+
|
|
149
|
+
try {
|
|
150
|
+
await api.delete(
|
|
151
|
+
`/projects/${projectId}/testing/scenarios/${selectedScenario.id}`
|
|
152
|
+
);
|
|
153
|
+
setSelectedScenario(null);
|
|
154
|
+
await fetchScenarios();
|
|
155
|
+
} catch (e) {
|
|
156
|
+
setError(e instanceof Error ? e.message : "Failed to delete scenario");
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
const handleRunTest = async () => {
|
|
161
|
+
if (!selectedScenario) return;
|
|
162
|
+
|
|
163
|
+
setIsRunning(true);
|
|
164
|
+
setError(null);
|
|
165
|
+
setLastResult(null);
|
|
166
|
+
try {
|
|
167
|
+
const result = await api.post<TestResult>(
|
|
168
|
+
`/projects/${projectId}/testing/scenarios/${selectedScenario.id}/run`
|
|
169
|
+
);
|
|
170
|
+
setLastResult(result);
|
|
171
|
+
} catch (e) {
|
|
172
|
+
setError(e instanceof Error ? e.message : "Test execution failed");
|
|
173
|
+
} finally {
|
|
174
|
+
setIsRunning(false);
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
const handleAddStep = () => {
|
|
179
|
+
setEditSteps([...editSteps, { action: "click", selector: "" }]);
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
const handleUpdateStep = (index: number, updates: Partial<TestStep>) => {
|
|
183
|
+
const newSteps = [...editSteps];
|
|
184
|
+
newSteps[index] = { ...newSteps[index], ...updates };
|
|
185
|
+
setEditSteps(newSteps);
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
const handleRemoveStep = (index: number) => {
|
|
189
|
+
setEditSteps(editSteps.filter((_, i) => i !== index));
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
if (isLoading && scenarios.length === 0) {
|
|
193
|
+
return (
|
|
194
|
+
<div className="flex h-full items-center justify-center text-muted-foreground">
|
|
195
|
+
<RefreshCw className="h-5 w-5 animate-spin mr-2" />
|
|
196
|
+
Loading...
|
|
197
|
+
</div>
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return (
|
|
202
|
+
<div className="flex h-full flex-col">
|
|
203
|
+
{/* Header */}
|
|
204
|
+
<div className="flex items-center justify-between border-b px-4 py-3">
|
|
205
|
+
<h3 className="font-medium flex items-center gap-2">
|
|
206
|
+
<FlaskConical className="h-4 w-4" />
|
|
207
|
+
Browser Testing
|
|
208
|
+
</h3>
|
|
209
|
+
<Button variant="outline" size="sm" onClick={handleNewScenario}>
|
|
210
|
+
<Plus className="h-4 w-4 mr-1" />
|
|
211
|
+
New Test
|
|
212
|
+
</Button>
|
|
213
|
+
</div>
|
|
214
|
+
|
|
215
|
+
{/* Error */}
|
|
216
|
+
{error && (
|
|
217
|
+
<div className="flex items-center gap-2 px-4 py-2 bg-destructive/10 text-destructive text-sm">
|
|
218
|
+
<AlertCircle className="h-4 w-4" />
|
|
219
|
+
{error}
|
|
220
|
+
</div>
|
|
221
|
+
)}
|
|
222
|
+
|
|
223
|
+
<div className="flex flex-1 overflow-hidden">
|
|
224
|
+
{/* Scenario List */}
|
|
225
|
+
<div className="w-56 border-r overflow-auto">
|
|
226
|
+
<div className="p-2 space-y-1">
|
|
227
|
+
{scenarios.map((scenario) => (
|
|
228
|
+
<button
|
|
229
|
+
key={scenario.id}
|
|
230
|
+
onClick={() => handleSelectScenario(scenario)}
|
|
231
|
+
className={`w-full flex items-center gap-2 px-3 py-2 text-sm rounded-md transition-colors ${
|
|
232
|
+
selectedScenario?.id === scenario.id
|
|
233
|
+
? "bg-primary/10 text-primary"
|
|
234
|
+
: "hover:bg-muted"
|
|
235
|
+
}`}
|
|
236
|
+
>
|
|
237
|
+
<FlaskConical className="h-4 w-4 shrink-0" />
|
|
238
|
+
<span className="truncate">{scenario.name}</span>
|
|
239
|
+
</button>
|
|
240
|
+
))}
|
|
241
|
+
{scenarios.length === 0 && (
|
|
242
|
+
<p className="text-sm text-muted-foreground px-3 py-2">
|
|
243
|
+
No test scenarios yet
|
|
244
|
+
</p>
|
|
245
|
+
)}
|
|
246
|
+
</div>
|
|
247
|
+
</div>
|
|
248
|
+
|
|
249
|
+
{/* Main Content */}
|
|
250
|
+
<div className="flex-1 overflow-auto">
|
|
251
|
+
{isEditing ? (
|
|
252
|
+
// Edit Mode
|
|
253
|
+
<div className="p-4 space-y-4">
|
|
254
|
+
<Input
|
|
255
|
+
value={editName}
|
|
256
|
+
onChange={(e) => setEditName(e.target.value)}
|
|
257
|
+
placeholder="Test name"
|
|
258
|
+
className="font-medium"
|
|
259
|
+
/>
|
|
260
|
+
|
|
261
|
+
<div className="space-y-3">
|
|
262
|
+
<div className="text-sm font-medium text-muted-foreground">Steps</div>
|
|
263
|
+
{editSteps.map((step, i) => (
|
|
264
|
+
<div key={i} className="flex gap-2 items-start p-3 border rounded-lg">
|
|
265
|
+
<select
|
|
266
|
+
value={step.action}
|
|
267
|
+
onChange={(e) =>
|
|
268
|
+
handleUpdateStep(i, { action: e.target.value as TestStep["action"] })
|
|
269
|
+
}
|
|
270
|
+
className="h-9 px-2 border rounded-md bg-background text-sm"
|
|
271
|
+
>
|
|
272
|
+
{Object.entries(ACTION_LABELS).map(([action, label]) => (
|
|
273
|
+
<option key={action} value={action}>
|
|
274
|
+
{label}
|
|
275
|
+
</option>
|
|
276
|
+
))}
|
|
277
|
+
</select>
|
|
278
|
+
|
|
279
|
+
{step.action === "navigate" && (
|
|
280
|
+
<Input
|
|
281
|
+
value={step.url || ""}
|
|
282
|
+
onChange={(e) => handleUpdateStep(i, { url: e.target.value })}
|
|
283
|
+
placeholder="/path or full URL"
|
|
284
|
+
className="flex-1"
|
|
285
|
+
/>
|
|
286
|
+
)}
|
|
287
|
+
|
|
288
|
+
{(step.action === "click" ||
|
|
289
|
+
step.action === "waitFor" ||
|
|
290
|
+
step.action === "assert") && (
|
|
291
|
+
<Input
|
|
292
|
+
value={step.selector || ""}
|
|
293
|
+
onChange={(e) => handleUpdateStep(i, { selector: e.target.value })}
|
|
294
|
+
placeholder="CSS selector"
|
|
295
|
+
className="flex-1 font-mono text-sm"
|
|
296
|
+
/>
|
|
297
|
+
)}
|
|
298
|
+
|
|
299
|
+
{step.action === "fill" && (
|
|
300
|
+
<>
|
|
301
|
+
<Input
|
|
302
|
+
value={step.selector || ""}
|
|
303
|
+
onChange={(e) => handleUpdateStep(i, { selector: e.target.value })}
|
|
304
|
+
placeholder="CSS selector"
|
|
305
|
+
className="flex-1 font-mono text-sm"
|
|
306
|
+
/>
|
|
307
|
+
<Input
|
|
308
|
+
value={step.value || ""}
|
|
309
|
+
onChange={(e) => handleUpdateStep(i, { value: e.target.value })}
|
|
310
|
+
placeholder="Value"
|
|
311
|
+
className="flex-1"
|
|
312
|
+
/>
|
|
313
|
+
</>
|
|
314
|
+
)}
|
|
315
|
+
|
|
316
|
+
{step.action === "screenshot" && (
|
|
317
|
+
<Input
|
|
318
|
+
value={step.name || ""}
|
|
319
|
+
onChange={(e) => handleUpdateStep(i, { name: e.target.value })}
|
|
320
|
+
placeholder="Screenshot name"
|
|
321
|
+
className="flex-1"
|
|
322
|
+
/>
|
|
323
|
+
)}
|
|
324
|
+
|
|
325
|
+
<Button
|
|
326
|
+
variant="ghost"
|
|
327
|
+
size="sm"
|
|
328
|
+
onClick={() => handleRemoveStep(i)}
|
|
329
|
+
className="h-9 w-9 p-0 text-muted-foreground hover:text-destructive"
|
|
330
|
+
>
|
|
331
|
+
<Trash2 className="h-4 w-4" />
|
|
332
|
+
</Button>
|
|
333
|
+
</div>
|
|
334
|
+
))}
|
|
335
|
+
|
|
336
|
+
<Button variant="outline" size="sm" onClick={handleAddStep}>
|
|
337
|
+
<Plus className="h-4 w-4 mr-1" />
|
|
338
|
+
Add Step
|
|
339
|
+
</Button>
|
|
340
|
+
</div>
|
|
341
|
+
|
|
342
|
+
<div className="flex gap-2 pt-4 border-t">
|
|
343
|
+
<Button onClick={handleSaveScenario} disabled={isLoading}>
|
|
344
|
+
Save
|
|
345
|
+
</Button>
|
|
346
|
+
<Button variant="outline" onClick={() => setIsEditing(false)}>
|
|
347
|
+
Cancel
|
|
348
|
+
</Button>
|
|
349
|
+
</div>
|
|
350
|
+
</div>
|
|
351
|
+
) : selectedScenario ? (
|
|
352
|
+
// View Mode
|
|
353
|
+
<div className="p-4 space-y-4">
|
|
354
|
+
<div className="flex items-center justify-between">
|
|
355
|
+
<h4 className="text-lg font-medium">{selectedScenario.name}</h4>
|
|
356
|
+
<div className="flex gap-2">
|
|
357
|
+
<Button
|
|
358
|
+
variant="outline"
|
|
359
|
+
size="sm"
|
|
360
|
+
onClick={handleEditScenario}
|
|
361
|
+
>
|
|
362
|
+
Edit
|
|
363
|
+
</Button>
|
|
364
|
+
<Button
|
|
365
|
+
variant="outline"
|
|
366
|
+
size="sm"
|
|
367
|
+
onClick={handleDeleteScenario}
|
|
368
|
+
className="text-destructive"
|
|
369
|
+
>
|
|
370
|
+
<Trash2 className="h-4 w-4" />
|
|
371
|
+
</Button>
|
|
372
|
+
<Button
|
|
373
|
+
onClick={handleRunTest}
|
|
374
|
+
disabled={isRunning}
|
|
375
|
+
size="sm"
|
|
376
|
+
>
|
|
377
|
+
{isRunning ? (
|
|
378
|
+
<RefreshCw className="h-4 w-4 animate-spin mr-1" />
|
|
379
|
+
) : (
|
|
380
|
+
<Play className="h-4 w-4 mr-1" />
|
|
381
|
+
)}
|
|
382
|
+
Run Test
|
|
383
|
+
</Button>
|
|
384
|
+
</div>
|
|
385
|
+
</div>
|
|
386
|
+
|
|
387
|
+
{/* Steps */}
|
|
388
|
+
<div className="space-y-2">
|
|
389
|
+
<div className="text-sm font-medium text-muted-foreground">
|
|
390
|
+
{selectedScenario.steps.length} steps
|
|
391
|
+
</div>
|
|
392
|
+
{selectedScenario.steps.map((step, i) => {
|
|
393
|
+
const stepResult = lastResult?.steps[i];
|
|
394
|
+
return (
|
|
395
|
+
<div
|
|
396
|
+
key={i}
|
|
397
|
+
className={`flex items-center gap-3 p-3 border rounded-lg ${
|
|
398
|
+
stepResult?.status === "passed"
|
|
399
|
+
? "border-green-500/50 bg-green-500/5"
|
|
400
|
+
: stepResult?.status === "failed"
|
|
401
|
+
? "border-red-500/50 bg-red-500/5"
|
|
402
|
+
: ""
|
|
403
|
+
}`}
|
|
404
|
+
>
|
|
405
|
+
{stepResult ? (
|
|
406
|
+
stepResult.status === "passed" ? (
|
|
407
|
+
<CheckCircle className="h-4 w-4 text-green-500" />
|
|
408
|
+
) : stepResult.status === "failed" ? (
|
|
409
|
+
<XCircle className="h-4 w-4 text-red-500" />
|
|
410
|
+
) : (
|
|
411
|
+
<Clock className="h-4 w-4 text-muted-foreground" />
|
|
412
|
+
)
|
|
413
|
+
) : (
|
|
414
|
+
<div className="h-4 w-4 rounded-full border-2" />
|
|
415
|
+
)}
|
|
416
|
+
<div className="flex-1">
|
|
417
|
+
<span className="font-medium">{ACTION_LABELS[step.action]}</span>
|
|
418
|
+
{step.selector && (
|
|
419
|
+
<code className="ml-2 text-xs bg-muted px-1.5 py-0.5 rounded">
|
|
420
|
+
{step.selector}
|
|
421
|
+
</code>
|
|
422
|
+
)}
|
|
423
|
+
{step.url && (
|
|
424
|
+
<code className="ml-2 text-xs bg-muted px-1.5 py-0.5 rounded">
|
|
425
|
+
{step.url}
|
|
426
|
+
</code>
|
|
427
|
+
)}
|
|
428
|
+
{step.value && (
|
|
429
|
+
<span className="ml-2 text-sm text-muted-foreground">
|
|
430
|
+
= "{step.value}"
|
|
431
|
+
</span>
|
|
432
|
+
)}
|
|
433
|
+
</div>
|
|
434
|
+
</div>
|
|
435
|
+
);
|
|
436
|
+
})}
|
|
437
|
+
</div>
|
|
438
|
+
|
|
439
|
+
{/* Result */}
|
|
440
|
+
{lastResult && (
|
|
441
|
+
<div
|
|
442
|
+
className={`p-4 rounded-lg ${
|
|
443
|
+
lastResult.status === "passed"
|
|
444
|
+
? "bg-green-500/10 text-green-700 dark:text-green-400"
|
|
445
|
+
: "bg-red-500/10 text-red-700 dark:text-red-400"
|
|
446
|
+
}`}
|
|
447
|
+
>
|
|
448
|
+
<div className="flex items-center gap-2">
|
|
449
|
+
{lastResult.status === "passed" ? (
|
|
450
|
+
<CheckCircle className="h-5 w-5" />
|
|
451
|
+
) : (
|
|
452
|
+
<XCircle className="h-5 w-5" />
|
|
453
|
+
)}
|
|
454
|
+
<span className="font-medium">
|
|
455
|
+
Test {lastResult.status === "passed" ? "Passed" : "Failed"}
|
|
456
|
+
</span>
|
|
457
|
+
</div>
|
|
458
|
+
{lastResult.error && (
|
|
459
|
+
<p className="mt-2 text-sm">{lastResult.error}</p>
|
|
460
|
+
)}
|
|
461
|
+
{lastResult.endTime && lastResult.startTime && (
|
|
462
|
+
<p className="mt-1 text-sm opacity-70">
|
|
463
|
+
Duration: {lastResult.endTime - lastResult.startTime}ms
|
|
464
|
+
</p>
|
|
465
|
+
)}
|
|
466
|
+
</div>
|
|
467
|
+
)}
|
|
468
|
+
</div>
|
|
469
|
+
) : (
|
|
470
|
+
<div className="flex h-full items-center justify-center text-muted-foreground">
|
|
471
|
+
<div className="text-center">
|
|
472
|
+
<FlaskConical className="h-8 w-8 mx-auto mb-2 opacity-50" />
|
|
473
|
+
<p>Select a test or create a new one</p>
|
|
474
|
+
</div>
|
|
475
|
+
</div>
|
|
476
|
+
)}
|
|
477
|
+
</div>
|
|
478
|
+
</div>
|
|
479
|
+
</div>
|
|
480
|
+
);
|
|
481
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import * as TabsPrimitive from "@radix-ui/react-tabs"
|
|
5
|
+
|
|
6
|
+
import { cn } from "@/lib/utils"
|
|
7
|
+
|
|
8
|
+
const Tabs = TabsPrimitive.Root
|
|
9
|
+
|
|
10
|
+
const TabsList = React.forwardRef<
|
|
11
|
+
React.ElementRef<typeof TabsPrimitive.List>,
|
|
12
|
+
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
|
|
13
|
+
>(({ className, ...props }, ref) => (
|
|
14
|
+
<TabsPrimitive.List
|
|
15
|
+
ref={ref}
|
|
16
|
+
className={cn(
|
|
17
|
+
"inline-flex h-9 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground",
|
|
18
|
+
className
|
|
19
|
+
)}
|
|
20
|
+
{...props}
|
|
21
|
+
/>
|
|
22
|
+
))
|
|
23
|
+
TabsList.displayName = TabsPrimitive.List.displayName
|
|
24
|
+
|
|
25
|
+
const TabsTrigger = React.forwardRef<
|
|
26
|
+
React.ElementRef<typeof TabsPrimitive.Trigger>,
|
|
27
|
+
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
|
|
28
|
+
>(({ className, ...props }, ref) => (
|
|
29
|
+
<TabsPrimitive.Trigger
|
|
30
|
+
ref={ref}
|
|
31
|
+
className={cn(
|
|
32
|
+
"inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow",
|
|
33
|
+
className
|
|
34
|
+
)}
|
|
35
|
+
{...props}
|
|
36
|
+
/>
|
|
37
|
+
))
|
|
38
|
+
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
|
|
39
|
+
|
|
40
|
+
const TabsContent = React.forwardRef<
|
|
41
|
+
React.ElementRef<typeof TabsPrimitive.Content>,
|
|
42
|
+
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
|
|
43
|
+
>(({ className, ...props }, ref) => (
|
|
44
|
+
<TabsPrimitive.Content
|
|
45
|
+
ref={ref}
|
|
46
|
+
className={cn(
|
|
47
|
+
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
|
48
|
+
className
|
|
49
|
+
)}
|
|
50
|
+
{...props}
|
|
51
|
+
/>
|
|
52
|
+
))
|
|
53
|
+
TabsContent.displayName = TabsPrimitive.Content.displayName
|
|
54
|
+
|
|
55
|
+
export { Tabs, TabsList, TabsTrigger, TabsContent }
|