mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-03 21:03:08 +00:00
Update app_spec.txt and coding_prompt.md for improved clarity and consistency
- Updated references to `app_spec.txt` and `feature_list.json` in app_spec.txt to include the correct path. - Enhanced coding_prompt.md by incorporating testing utilities for better test management and readability. - Added new utility functions in tests/utils.ts to streamline test interactions. This commit aims to improve documentation accuracy and maintainability of testing practices.
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -434,3 +434,40 @@ ipcMain.handle("auto-mode:verify-feature", async (_, { projectPath, featureId })
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Resume a specific feature with previous context
|
||||
*/
|
||||
ipcMain.handle("auto-mode:resume-feature", async (_, { projectPath, featureId }) => {
|
||||
console.log("[IPC] auto-mode:resume-feature called with:", { projectPath, featureId });
|
||||
try {
|
||||
const sendToRenderer = (data) => {
|
||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||
mainWindow.webContents.send("auto-mode:event", data);
|
||||
}
|
||||
};
|
||||
|
||||
return await autoModeService.resumeFeature({ projectPath, featureId, sendToRenderer });
|
||||
} catch (error) {
|
||||
console.error("[IPC] auto-mode:resume-feature error:", error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Check if a context file exists for a feature
|
||||
*/
|
||||
ipcMain.handle("auto-mode:context-exists", async (_, { projectPath, featureId }) => {
|
||||
try {
|
||||
const contextPath = path.join(projectPath, ".automaker", "context", `${featureId}.md`);
|
||||
try {
|
||||
await fs.access(contextPath);
|
||||
return { success: true, exists: true };
|
||||
} catch {
|
||||
return { success: true, exists: false };
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("[IPC] auto-mode:context-exists error:", error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
});
|
||||
|
||||
@@ -102,6 +102,14 @@ contextBridge.exposeInMainWorld("electronAPI", {
|
||||
verifyFeature: (projectPath, featureId) =>
|
||||
ipcRenderer.invoke("auto-mode:verify-feature", { projectPath, featureId }),
|
||||
|
||||
// Resume a specific feature with previous context
|
||||
resumeFeature: (projectPath, featureId) =>
|
||||
ipcRenderer.invoke("auto-mode:resume-feature", { projectPath, featureId }),
|
||||
|
||||
// Check if context file exists for a feature
|
||||
contextExists: (projectPath, featureId) =>
|
||||
ipcRenderer.invoke("auto-mode:context-exists", { projectPath, featureId }),
|
||||
|
||||
// Listen for auto mode events
|
||||
onEvent: (callback) => {
|
||||
const subscription = (_, data) => callback(data);
|
||||
|
||||
310
app/src/components/ui/feature-image-upload.tsx
Normal file
310
app/src/components/ui/feature-image-upload.tsx
Normal file
@@ -0,0 +1,310 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState, useRef, useCallback } from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { ImageIcon, X, Upload } from "lucide-react";
|
||||
|
||||
export interface FeatureImage {
|
||||
id: string;
|
||||
data: string; // base64 encoded
|
||||
mimeType: string;
|
||||
filename: string;
|
||||
size: number;
|
||||
}
|
||||
|
||||
interface FeatureImageUploadProps {
|
||||
images: FeatureImage[];
|
||||
onImagesChange: (images: FeatureImage[]) => void;
|
||||
maxFiles?: number;
|
||||
maxFileSize?: number; // in bytes, default 10MB
|
||||
className?: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const ACCEPTED_IMAGE_TYPES = [
|
||||
"image/jpeg",
|
||||
"image/jpg",
|
||||
"image/png",
|
||||
"image/gif",
|
||||
"image/webp",
|
||||
];
|
||||
const DEFAULT_MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB
|
||||
|
||||
export function FeatureImageUpload({
|
||||
images,
|
||||
onImagesChange,
|
||||
maxFiles = 5,
|
||||
maxFileSize = DEFAULT_MAX_FILE_SIZE,
|
||||
className,
|
||||
disabled = false,
|
||||
}: FeatureImageUploadProps) {
|
||||
const [isDragOver, setIsDragOver] = useState(false);
|
||||
const [isProcessing, setIsProcessing] = useState(false);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const fileToBase64 = (file: File): Promise<string> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
if (typeof reader.result === "string") {
|
||||
resolve(reader.result);
|
||||
} else {
|
||||
reject(new Error("Failed to read file as base64"));
|
||||
}
|
||||
};
|
||||
reader.onerror = () => reject(new Error("Failed to read file"));
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
};
|
||||
|
||||
const processFiles = useCallback(
|
||||
async (files: FileList) => {
|
||||
if (disabled || isProcessing) return;
|
||||
|
||||
setIsProcessing(true);
|
||||
const newImages: FeatureImage[] = [];
|
||||
const errors: string[] = [];
|
||||
|
||||
for (const file of Array.from(files)) {
|
||||
// Validate file type
|
||||
if (!ACCEPTED_IMAGE_TYPES.includes(file.type)) {
|
||||
errors.push(
|
||||
`${file.name}: Unsupported file type. Please use JPG, PNG, GIF, or WebP.`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Validate file size
|
||||
if (file.size > maxFileSize) {
|
||||
const maxSizeMB = maxFileSize / (1024 * 1024);
|
||||
errors.push(
|
||||
`${file.name}: File too large. Maximum size is ${maxSizeMB}MB.`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if we've reached max files
|
||||
if (newImages.length + images.length >= maxFiles) {
|
||||
errors.push(`Maximum ${maxFiles} images allowed.`);
|
||||
break;
|
||||
}
|
||||
|
||||
try {
|
||||
const base64 = await fileToBase64(file);
|
||||
const imageAttachment: FeatureImage = {
|
||||
id: `img-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
||||
data: base64,
|
||||
mimeType: file.type,
|
||||
filename: file.name,
|
||||
size: file.size,
|
||||
};
|
||||
newImages.push(imageAttachment);
|
||||
} catch (error) {
|
||||
errors.push(`${file.name}: Failed to process image.`);
|
||||
}
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
console.warn("Image upload errors:", errors);
|
||||
}
|
||||
|
||||
if (newImages.length > 0) {
|
||||
onImagesChange([...images, ...newImages]);
|
||||
}
|
||||
|
||||
setIsProcessing(false);
|
||||
},
|
||||
[disabled, isProcessing, images, maxFiles, maxFileSize, onImagesChange]
|
||||
);
|
||||
|
||||
const handleDrop = useCallback(
|
||||
(e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setIsDragOver(false);
|
||||
|
||||
if (disabled) return;
|
||||
|
||||
const files = e.dataTransfer.files;
|
||||
if (files.length > 0) {
|
||||
processFiles(files);
|
||||
}
|
||||
},
|
||||
[disabled, processFiles]
|
||||
);
|
||||
|
||||
const handleDragOver = useCallback(
|
||||
(e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (!disabled) {
|
||||
setIsDragOver(true);
|
||||
}
|
||||
},
|
||||
[disabled]
|
||||
);
|
||||
|
||||
const handleDragLeave = useCallback((e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setIsDragOver(false);
|
||||
}, []);
|
||||
|
||||
const handleFileSelect = useCallback(
|
||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const files = e.target.files;
|
||||
if (files && files.length > 0) {
|
||||
processFiles(files);
|
||||
}
|
||||
// Reset the input so the same file can be selected again
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.value = "";
|
||||
}
|
||||
},
|
||||
[processFiles]
|
||||
);
|
||||
|
||||
const handleBrowseClick = useCallback(() => {
|
||||
if (!disabled && fileInputRef.current) {
|
||||
fileInputRef.current.click();
|
||||
}
|
||||
}, [disabled]);
|
||||
|
||||
const removeImage = useCallback(
|
||||
(imageId: string) => {
|
||||
onImagesChange(images.filter((img) => img.id !== imageId));
|
||||
},
|
||||
[images, onImagesChange]
|
||||
);
|
||||
|
||||
const clearAllImages = useCallback(() => {
|
||||
onImagesChange([]);
|
||||
}, [onImagesChange]);
|
||||
|
||||
const formatFileSize = (bytes: number): string => {
|
||||
if (bytes === 0) return "0 B";
|
||||
const k = 1024;
|
||||
const sizes = ["B", "KB", "MB", "GB"];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + " " + sizes[i];
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={cn("relative", className)}>
|
||||
{/* Hidden file input */}
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
multiple
|
||||
accept={ACCEPTED_IMAGE_TYPES.join(",")}
|
||||
onChange={handleFileSelect}
|
||||
className="hidden"
|
||||
disabled={disabled}
|
||||
data-testid="feature-image-input"
|
||||
/>
|
||||
|
||||
{/* Drop zone */}
|
||||
<div
|
||||
onDrop={handleDrop}
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
onClick={handleBrowseClick}
|
||||
className={cn(
|
||||
"relative rounded-lg border-2 border-dashed transition-all duration-200 cursor-pointer",
|
||||
{
|
||||
"border-blue-400 bg-blue-50 dark:bg-blue-950/20":
|
||||
isDragOver && !disabled,
|
||||
"border-muted-foreground/25": !isDragOver && !disabled,
|
||||
"border-muted-foreground/10 opacity-50 cursor-not-allowed": disabled,
|
||||
"hover:border-blue-400 hover:bg-blue-50/50 dark:hover:bg-blue-950/10":
|
||||
!disabled && !isDragOver,
|
||||
}
|
||||
)}
|
||||
data-testid="feature-image-dropzone"
|
||||
>
|
||||
<div className="flex flex-col items-center justify-center p-4 text-center">
|
||||
<div
|
||||
className={cn(
|
||||
"rounded-full p-2 mb-2",
|
||||
isDragOver && !disabled
|
||||
? "bg-blue-100 dark:bg-blue-900/30"
|
||||
: "bg-muted"
|
||||
)}
|
||||
>
|
||||
{isProcessing ? (
|
||||
<Upload className="h-5 w-5 animate-spin text-muted-foreground" />
|
||||
) : (
|
||||
<ImageIcon className="h-5 w-5 text-muted-foreground" />
|
||||
)}
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{isDragOver && !disabled
|
||||
? "Drop images here"
|
||||
: "Click or drag images here"}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
Up to {maxFiles} images, max{" "}
|
||||
{Math.round(maxFileSize / (1024 * 1024))}MB each
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Image previews */}
|
||||
{images.length > 0 && (
|
||||
<div className="mt-3 space-y-2" data-testid="feature-image-previews">
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-xs font-medium text-foreground">
|
||||
{images.length} image{images.length > 1 ? "s" : ""} selected
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={clearAllImages}
|
||||
className="text-xs text-muted-foreground hover:text-foreground"
|
||||
disabled={disabled}
|
||||
>
|
||||
Clear all
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{images.map((image) => (
|
||||
<div
|
||||
key={image.id}
|
||||
className="relative group rounded-md border border-muted bg-muted/50 overflow-hidden"
|
||||
data-testid={`feature-image-preview-${image.id}`}
|
||||
>
|
||||
{/* Image thumbnail */}
|
||||
<div className="w-16 h-16 flex items-center justify-center">
|
||||
<img
|
||||
src={image.data}
|
||||
alt={image.filename}
|
||||
className="max-w-full max-h-full object-contain"
|
||||
/>
|
||||
</div>
|
||||
{/* Remove button */}
|
||||
{!disabled && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
removeImage(image.id);
|
||||
}}
|
||||
className="absolute top-0.5 right-0.5 p-0.5 rounded-full bg-destructive text-destructive-foreground opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
data-testid={`remove-image-${image.id}`}
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</button>
|
||||
)}
|
||||
{/* Filename tooltip on hover */}
|
||||
<div className="absolute bottom-0 left-0 right-0 bg-black/60 px-1 py-0.5 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<p className="text-[10px] text-white truncate">
|
||||
{image.filename}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
20
app/src/components/ui/textarea.tsx
Normal file
20
app/src/components/ui/textarea.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
|
||||
return (
|
||||
<textarea
|
||||
data-slot="textarea"
|
||||
className={cn(
|
||||
"placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input min-h-[80px] w-full min-w-0 rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm resize-none",
|
||||
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
|
||||
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Textarea }
|
||||
@@ -106,6 +106,11 @@ export function AgentOutputModal({
|
||||
if (!api?.autoMode) return;
|
||||
|
||||
const unsubscribe = api.autoMode.onEvent((event) => {
|
||||
// Filter events for this specific feature only
|
||||
if (event.featureId !== featureId) {
|
||||
return;
|
||||
}
|
||||
|
||||
let newContent = "";
|
||||
|
||||
if (event.type === "auto_mode_progress") {
|
||||
|
||||
@@ -79,6 +79,7 @@ export function BoardView() {
|
||||
const [showActivityLog, setShowActivityLog] = useState(false);
|
||||
const [showOutputModal, setShowOutputModal] = useState(false);
|
||||
const [outputFeature, setOutputFeature] = useState<Feature | null>(null);
|
||||
const [featuresWithContext, setFeaturesWithContext] = useState<Set<string>>(new Set());
|
||||
|
||||
// Make current project available globally for modal
|
||||
useEffect(() => {
|
||||
@@ -185,6 +186,32 @@ export function BoardView() {
|
||||
loadFeatures();
|
||||
}, [loadFeatures]);
|
||||
|
||||
// Check which features have context files
|
||||
useEffect(() => {
|
||||
const checkAllContexts = async () => {
|
||||
const inProgressFeatures = features.filter((f) => f.status === "in_progress");
|
||||
const contextChecks = await Promise.all(
|
||||
inProgressFeatures.map(async (f) => ({
|
||||
id: f.id,
|
||||
hasContext: await checkContextExists(f.id),
|
||||
}))
|
||||
);
|
||||
|
||||
const newSet = new Set<string>();
|
||||
contextChecks.forEach(({ id, hasContext }) => {
|
||||
if (hasContext) {
|
||||
newSet.add(id);
|
||||
}
|
||||
});
|
||||
|
||||
setFeaturesWithContext(newSet);
|
||||
};
|
||||
|
||||
if (features.length > 0 && !isLoading) {
|
||||
checkAllContexts();
|
||||
}
|
||||
}, [features, isLoading]);
|
||||
|
||||
// Save features to file
|
||||
const saveFeatures = useCallback(async () => {
|
||||
if (!currentProject) return;
|
||||
@@ -360,6 +387,59 @@ export function BoardView() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleResumeFeature = async (feature: Feature) => {
|
||||
if (!currentProject) return;
|
||||
|
||||
console.log("[Board] Resuming feature:", { id: feature.id, description: feature.description });
|
||||
|
||||
try {
|
||||
const api = getElectronAPI();
|
||||
if (!api?.autoMode) {
|
||||
console.error("Auto mode API not available");
|
||||
return;
|
||||
}
|
||||
|
||||
// Call the API to resume this specific feature by ID with context
|
||||
const result = await api.autoMode.resumeFeature(
|
||||
currentProject.path,
|
||||
feature.id
|
||||
);
|
||||
|
||||
if (result.success) {
|
||||
console.log("[Board] Feature resume started successfully");
|
||||
// The feature status will be updated by the auto mode service
|
||||
// and the UI will reload features when resume completes
|
||||
} else {
|
||||
console.error("[Board] Failed to resume feature:", result.error);
|
||||
await loadFeatures();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("[Board] Error resuming feature:", error);
|
||||
await loadFeatures();
|
||||
}
|
||||
};
|
||||
|
||||
const checkContextExists = async (featureId: string): Promise<boolean> => {
|
||||
if (!currentProject) return false;
|
||||
|
||||
try {
|
||||
const api = getElectronAPI();
|
||||
if (!api?.autoMode?.contextExists) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const result = await api.autoMode.contextExists(
|
||||
currentProject.path,
|
||||
featureId
|
||||
);
|
||||
|
||||
return result.success && result.exists === true;
|
||||
} catch (error) {
|
||||
console.error("[Board] Error checking context:", error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const getColumnFeatures = (columnId: ColumnId) => {
|
||||
return features.filter((f) => f.status === columnId);
|
||||
};
|
||||
@@ -504,6 +584,8 @@ export function BoardView() {
|
||||
onDelete={() => handleDeleteFeature(feature.id)}
|
||||
onViewOutput={() => handleViewOutput(feature)}
|
||||
onVerify={() => handleVerifyFeature(feature)}
|
||||
onResume={() => handleResumeFeature(feature)}
|
||||
hasContext={featuresWithContext.has(feature.id)}
|
||||
isCurrentAutoTask={runningAutoTasks.includes(feature.id)}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -295,11 +295,14 @@ export function InterviewView() {
|
||||
await api.mkdir(fullProjectPath);
|
||||
|
||||
// Write app_spec.txt with generated content
|
||||
await api.writeFile(`${fullProjectPath}/app_spec.txt`, generatedSpec);
|
||||
await api.writeFile(
|
||||
`${fullProjectPath}/.automaker/app_spec.txt`,
|
||||
generatedSpec
|
||||
);
|
||||
|
||||
// Create initial .automaker/feature_list.json
|
||||
await api.writeFile(
|
||||
`${fullProjectPath}/feature_list.json`,
|
||||
`${fullProjectPath}/.automaker/feature_list.json`,
|
||||
JSON.stringify(
|
||||
[
|
||||
{
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
} from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Feature } from "@/store/app-store";
|
||||
import { GripVertical, Edit, CheckCircle2, Circle, Loader2, Trash2, Eye, PlayCircle } from "lucide-react";
|
||||
import { GripVertical, Edit, CheckCircle2, Circle, Loader2, Trash2, Eye, PlayCircle, RotateCcw } from "lucide-react";
|
||||
|
||||
interface KanbanCardProps {
|
||||
feature: Feature;
|
||||
@@ -20,10 +20,12 @@ interface KanbanCardProps {
|
||||
onDelete: () => void;
|
||||
onViewOutput?: () => void;
|
||||
onVerify?: () => void;
|
||||
onResume?: () => void;
|
||||
hasContext?: boolean;
|
||||
isCurrentAutoTask?: boolean;
|
||||
}
|
||||
|
||||
export function KanbanCard({ feature, onEdit, onDelete, onViewOutput, onVerify, isCurrentAutoTask }: KanbanCardProps) {
|
||||
export function KanbanCard({ feature, onEdit, onDelete, onViewOutput, onVerify, onResume, hasContext, isCurrentAutoTask }: KanbanCardProps) {
|
||||
// Disable dragging if the feature is in progress or verified
|
||||
const isDraggable = feature.status === "backlog";
|
||||
const {
|
||||
@@ -127,7 +129,21 @@ export function KanbanCard({ feature, onEdit, onDelete, onViewOutput, onVerify,
|
||||
)}
|
||||
{!isCurrentAutoTask && feature.status === "in_progress" && (
|
||||
<>
|
||||
{onVerify && (
|
||||
{hasContext && onResume ? (
|
||||
<Button
|
||||
variant="default"
|
||||
size="sm"
|
||||
className="flex-1 h-7 text-xs bg-blue-600 hover:bg-blue-700"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onResume();
|
||||
}}
|
||||
data-testid={`resume-feature-${feature.id}`}
|
||||
>
|
||||
<RotateCcw className="w-3 h-3 mr-1" />
|
||||
Resume
|
||||
</Button>
|
||||
) : onVerify ? (
|
||||
<Button
|
||||
variant="default"
|
||||
size="sm"
|
||||
@@ -139,9 +155,9 @@ export function KanbanCard({ feature, onEdit, onDelete, onViewOutput, onVerify,
|
||||
data-testid={`verify-feature-${feature.id}`}
|
||||
>
|
||||
<PlayCircle className="w-3 h-3 mr-1" />
|
||||
Verify
|
||||
Implement
|
||||
</Button>
|
||||
)}
|
||||
) : null}
|
||||
{onViewOutput && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
|
||||
@@ -12,10 +12,25 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { useAppStore } from "@/store/app-store";
|
||||
import { getElectronAPI } from "@/lib/electron";
|
||||
import { FolderOpen, Plus, Cpu, Folder, Clock, Sparkles, MessageSquare, ChevronDown } from "lucide-react";
|
||||
import {
|
||||
FolderOpen,
|
||||
Plus,
|
||||
Cpu,
|
||||
Folder,
|
||||
Clock,
|
||||
Sparkles,
|
||||
MessageSquare,
|
||||
ChevronDown,
|
||||
} from "lucide-react";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -24,7 +39,8 @@ import {
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
|
||||
export function WelcomeView() {
|
||||
const { projects, addProject, setCurrentProject, setCurrentView } = useAppStore();
|
||||
const { projects, addProject, setCurrentProject, setCurrentView } =
|
||||
useAppStore();
|
||||
const [showNewProjectDialog, setShowNewProjectDialog] = useState(false);
|
||||
const [newProjectName, setNewProjectName] = useState("");
|
||||
const [newProjectPath, setNewProjectPath] = useState("");
|
||||
@@ -101,13 +117,17 @@ export function WelcomeView() {
|
||||
);
|
||||
|
||||
await api.writeFile(
|
||||
`${projectPath}/feature_list.json`,
|
||||
`${projectPath}/.automaker/feature_list.json`,
|
||||
JSON.stringify(
|
||||
[
|
||||
{
|
||||
category: "Core",
|
||||
description: "First feature to implement",
|
||||
steps: ["Step 1: Define requirements", "Step 2: Implement", "Step 3: Test"],
|
||||
steps: [
|
||||
"Step 1: Define requirements",
|
||||
"Step 2: Implement",
|
||||
"Step 3: Test",
|
||||
],
|
||||
passes: false,
|
||||
},
|
||||
],
|
||||
@@ -151,8 +171,12 @@ export function WelcomeView() {
|
||||
<Cpu className="w-5 h-5 text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-white">Welcome to Automaker</h1>
|
||||
<p className="text-sm text-zinc-400">Your autonomous AI development studio</p>
|
||||
<h1 className="text-2xl font-bold text-white">
|
||||
Welcome to Automaker
|
||||
</h1>
|
||||
<p className="text-sm text-zinc-400">
|
||||
Your autonomous AI development studio
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -174,9 +198,12 @@ export function WelcomeView() {
|
||||
<Plus className="w-6 h-6 text-white" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h3 className="text-lg font-semibold text-white mb-1">New Project</h3>
|
||||
<h3 className="text-lg font-semibold text-white mb-1">
|
||||
New Project
|
||||
</h3>
|
||||
<p className="text-sm text-zinc-400">
|
||||
Create a new project from scratch with AI-powered development
|
||||
Create a new project from scratch with AI-powered
|
||||
development
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -223,7 +250,9 @@ export function WelcomeView() {
|
||||
<FolderOpen className="w-6 h-6 text-zinc-400 group-hover:text-white transition-colors" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h3 className="text-lg font-semibold text-white mb-1">Open Project</h3>
|
||||
<h3 className="text-lg font-semibold text-white mb-1">
|
||||
Open Project
|
||||
</h3>
|
||||
<p className="text-sm text-zinc-400">
|
||||
Open an existing project folder to continue working
|
||||
</p>
|
||||
@@ -246,7 +275,9 @@ export function WelcomeView() {
|
||||
<div>
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<Clock className="w-5 h-5 text-zinc-400" />
|
||||
<h2 className="text-lg font-semibold text-white">Recent Projects</h2>
|
||||
<h2 className="text-lg font-semibold text-white">
|
||||
Recent Projects
|
||||
</h2>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{recentProjects.map((project) => (
|
||||
@@ -266,10 +297,14 @@ export function WelcomeView() {
|
||||
<p className="font-medium text-white truncate group-hover:text-brand-500 transition-colors">
|
||||
{project.name}
|
||||
</p>
|
||||
<p className="text-xs text-zinc-500 truncate mt-0.5">{project.path}</p>
|
||||
<p className="text-xs text-zinc-500 truncate mt-0.5">
|
||||
{project.path}
|
||||
</p>
|
||||
{project.lastOpened && (
|
||||
<p className="text-xs text-zinc-600 mt-1">
|
||||
{new Date(project.lastOpened).toLocaleDateString()}
|
||||
{new Date(
|
||||
project.lastOpened
|
||||
).toLocaleDateString()}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
@@ -287,7 +322,9 @@ export function WelcomeView() {
|
||||
<div className="w-16 h-16 rounded-2xl bg-zinc-900/50 border border-white/10 flex items-center justify-center mb-4">
|
||||
<Sparkles className="w-8 h-8 text-zinc-600" />
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold text-white mb-2">No projects yet</h3>
|
||||
<h3 className="text-lg font-semibold text-white mb-2">
|
||||
No projects yet
|
||||
</h3>
|
||||
<p className="text-sm text-zinc-400 max-w-md">
|
||||
Get started by creating a new project or opening an existing one
|
||||
</p>
|
||||
@@ -297,7 +334,10 @@ export function WelcomeView() {
|
||||
</div>
|
||||
|
||||
{/* New Project Dialog */}
|
||||
<Dialog open={showNewProjectDialog} onOpenChange={setShowNewProjectDialog}>
|
||||
<Dialog
|
||||
open={showNewProjectDialog}
|
||||
onOpenChange={setShowNewProjectDialog}
|
||||
>
|
||||
<DialogContent
|
||||
className="bg-zinc-900 border-white/10"
|
||||
data-testid="new-project-dialog"
|
||||
|
||||
@@ -11,16 +11,20 @@ export function useAutoMode() {
|
||||
const {
|
||||
isAutoModeRunning,
|
||||
setAutoModeRunning,
|
||||
currentAutoTask,
|
||||
setCurrentAutoTask,
|
||||
runningAutoTasks,
|
||||
addRunningTask,
|
||||
removeRunningTask,
|
||||
clearRunningTasks,
|
||||
currentProject,
|
||||
addAutoModeActivity,
|
||||
} = useAppStore(
|
||||
useShallow((state) => ({
|
||||
isAutoModeRunning: state.isAutoModeRunning,
|
||||
setAutoModeRunning: state.setAutoModeRunning,
|
||||
currentAutoTask: state.currentAutoTask,
|
||||
setCurrentAutoTask: state.setCurrentAutoTask,
|
||||
runningAutoTasks: state.runningAutoTasks,
|
||||
addRunningTask: state.addRunningTask,
|
||||
removeRunningTask: state.removeRunningTask,
|
||||
clearRunningTasks: state.clearRunningTasks,
|
||||
currentProject: state.currentProject,
|
||||
addAutoModeActivity: state.addAutoModeActivity,
|
||||
}))
|
||||
@@ -36,7 +40,7 @@ export function useAutoMode() {
|
||||
|
||||
switch (event.type) {
|
||||
case "auto_mode_feature_start":
|
||||
setCurrentAutoTask(event.featureId);
|
||||
addRunningTask(event.featureId);
|
||||
addAutoModeActivity({
|
||||
featureId: event.featureId,
|
||||
type: "start",
|
||||
@@ -45,13 +49,14 @@ export function useAutoMode() {
|
||||
break;
|
||||
|
||||
case "auto_mode_feature_complete":
|
||||
// Feature completed - UI will reload features on its own
|
||||
// Feature completed - remove from running tasks and UI will reload features on its own
|
||||
console.log(
|
||||
"[AutoMode] Feature completed:",
|
||||
event.featureId,
|
||||
"passes:",
|
||||
event.passes
|
||||
);
|
||||
removeRunningTask(event.featureId);
|
||||
addAutoModeActivity({
|
||||
featureId: event.featureId,
|
||||
type: "complete",
|
||||
@@ -65,7 +70,7 @@ export function useAutoMode() {
|
||||
case "auto_mode_complete":
|
||||
// All features completed
|
||||
setAutoModeRunning(false);
|
||||
setCurrentAutoTask(null);
|
||||
clearRunningTasks();
|
||||
console.log("[AutoMode] All features completed!");
|
||||
break;
|
||||
|
||||
@@ -115,7 +120,7 @@ export function useAutoMode() {
|
||||
});
|
||||
|
||||
return unsubscribe;
|
||||
}, [setCurrentAutoTask, setAutoModeRunning, addAutoModeActivity]);
|
||||
}, [addRunningTask, removeRunningTask, clearRunningTasks, setAutoModeRunning, addAutoModeActivity]);
|
||||
|
||||
// Start auto mode
|
||||
const start = useCallback(async () => {
|
||||
@@ -158,7 +163,7 @@ export function useAutoMode() {
|
||||
|
||||
if (result.success) {
|
||||
setAutoModeRunning(false);
|
||||
setCurrentAutoTask(null);
|
||||
clearRunningTasks();
|
||||
console.log("[AutoMode] Stopped successfully");
|
||||
} else {
|
||||
console.error("[AutoMode] Failed to stop:", result.error);
|
||||
@@ -168,11 +173,11 @@ export function useAutoMode() {
|
||||
console.error("[AutoMode] Error stopping:", error);
|
||||
throw error;
|
||||
}
|
||||
}, [setAutoModeRunning, setCurrentAutoTask]);
|
||||
}, [setAutoModeRunning, clearRunningTasks]);
|
||||
|
||||
return {
|
||||
isRunning: isAutoModeRunning,
|
||||
currentTask: currentAutoTask,
|
||||
runningTasks: runningAutoTasks,
|
||||
start,
|
||||
stop,
|
||||
};
|
||||
|
||||
@@ -63,6 +63,8 @@ export interface AutoModeAPI {
|
||||
status: () => Promise<{ success: boolean; isRunning?: boolean; currentFeatureId?: string | null; error?: string }>;
|
||||
runFeature: (projectPath: string, featureId: string) => Promise<{ success: boolean; passes?: boolean; error?: string }>;
|
||||
verifyFeature: (projectPath: string, featureId: string) => Promise<{ success: boolean; passes?: boolean; error?: string }>;
|
||||
resumeFeature: (projectPath: string, featureId: string) => Promise<{ success: boolean; passes?: boolean; error?: string }>;
|
||||
contextExists: (projectPath: string, featureId: string) => Promise<{ success: boolean; exists?: boolean; error?: string }>;
|
||||
onEvent: (callback: (event: AutoModeEvent) => void) => () => void;
|
||||
}
|
||||
|
||||
@@ -288,8 +290,9 @@ export const getElectronAPI = (): ElectronAPI => {
|
||||
|
||||
// Mock Auto Mode state and implementation
|
||||
let mockAutoModeRunning = false;
|
||||
let mockRunningFeatures = new Set<string>(); // Track multiple concurrent feature verifications
|
||||
let mockAutoModeCallbacks: ((event: AutoModeEvent) => void)[] = [];
|
||||
let mockAutoModeTimeout: NodeJS.Timeout | null = null;
|
||||
let mockAutoModeTimeouts = new Map<string, NodeJS.Timeout>(); // Track timeouts per feature
|
||||
|
||||
function createMockAutoModeAPI(): AutoModeAPI {
|
||||
return {
|
||||
@@ -299,19 +302,21 @@ function createMockAutoModeAPI(): AutoModeAPI {
|
||||
}
|
||||
|
||||
mockAutoModeRunning = true;
|
||||
const featureId = "auto-mode-0";
|
||||
mockRunningFeatures.add(featureId);
|
||||
|
||||
// Simulate auto mode with Plan-Act-Verify phases
|
||||
simulateAutoModeLoop(projectPath);
|
||||
simulateAutoModeLoop(projectPath, featureId);
|
||||
|
||||
return { success: true };
|
||||
},
|
||||
|
||||
stop: async () => {
|
||||
mockAutoModeRunning = false;
|
||||
if (mockAutoModeTimeout) {
|
||||
clearTimeout(mockAutoModeTimeout);
|
||||
mockAutoModeTimeout = null;
|
||||
}
|
||||
mockRunningFeatures.clear();
|
||||
// Clear all timeouts
|
||||
mockAutoModeTimeouts.forEach(timeout => clearTimeout(timeout));
|
||||
mockAutoModeTimeouts.clear();
|
||||
return { success: true };
|
||||
},
|
||||
|
||||
@@ -324,27 +329,44 @@ function createMockAutoModeAPI(): AutoModeAPI {
|
||||
},
|
||||
|
||||
runFeature: async (projectPath: string, featureId: string) => {
|
||||
if (mockAutoModeRunning) {
|
||||
return { success: false, error: "Auto mode is already running" };
|
||||
if (mockRunningFeatures.has(featureId)) {
|
||||
return { success: false, error: `Feature ${featureId} is already running` };
|
||||
}
|
||||
|
||||
mockAutoModeRunning = true;
|
||||
simulateAutoModeLoop(projectPath);
|
||||
mockRunningFeatures.add(featureId);
|
||||
simulateAutoModeLoop(projectPath, featureId);
|
||||
|
||||
return { success: true, passes: true };
|
||||
},
|
||||
|
||||
verifyFeature: async (projectPath: string, featureId: string) => {
|
||||
if (mockAutoModeRunning) {
|
||||
return { success: false, error: "Auto mode is already running" };
|
||||
if (mockRunningFeatures.has(featureId)) {
|
||||
return { success: false, error: `Feature ${featureId} is already running` };
|
||||
}
|
||||
|
||||
mockAutoModeRunning = true;
|
||||
simulateAutoModeLoop(projectPath);
|
||||
mockRunningFeatures.add(featureId);
|
||||
simulateAutoModeLoop(projectPath, featureId);
|
||||
|
||||
return { success: true, passes: true };
|
||||
},
|
||||
|
||||
resumeFeature: async (projectPath: string, featureId: string) => {
|
||||
if (mockRunningFeatures.has(featureId)) {
|
||||
return { success: false, error: `Feature ${featureId} is already running` };
|
||||
}
|
||||
|
||||
mockRunningFeatures.add(featureId);
|
||||
simulateAutoModeLoop(projectPath, featureId);
|
||||
|
||||
return { success: true, passes: true };
|
||||
},
|
||||
|
||||
contextExists: async (projectPath: string, featureId: string) => {
|
||||
// Mock implementation - simulate that context exists for some features
|
||||
const exists = mockFileSystem[`${projectPath}/.automaker/context/${featureId}.md`] !== undefined;
|
||||
return { success: true, exists };
|
||||
},
|
||||
|
||||
onEvent: (callback: (event: AutoModeEvent) => void) => {
|
||||
mockAutoModeCallbacks.push(callback);
|
||||
return () => {
|
||||
@@ -358,8 +380,7 @@ function emitAutoModeEvent(event: AutoModeEvent) {
|
||||
mockAutoModeCallbacks.forEach(cb => cb(event));
|
||||
}
|
||||
|
||||
async function simulateAutoModeLoop(projectPath: string) {
|
||||
const featureId = "feature-0";
|
||||
async function simulateAutoModeLoop(projectPath: string, featureId: string) {
|
||||
const mockFeature = {
|
||||
id: featureId,
|
||||
category: "Core",
|
||||
@@ -375,8 +396,8 @@ async function simulateAutoModeLoop(projectPath: string) {
|
||||
feature: mockFeature,
|
||||
});
|
||||
|
||||
await delay(300);
|
||||
if (!mockAutoModeRunning) return;
|
||||
await delay(300, featureId);
|
||||
if (!mockRunningFeatures.has(featureId)) return;
|
||||
|
||||
// Phase 1: PLANNING
|
||||
emitAutoModeEvent({
|
||||
@@ -392,8 +413,8 @@ async function simulateAutoModeLoop(projectPath: string) {
|
||||
content: "Analyzing codebase structure and creating implementation plan...",
|
||||
});
|
||||
|
||||
await delay(500);
|
||||
if (!mockAutoModeRunning) return;
|
||||
await delay(500, featureId);
|
||||
if (!mockRunningFeatures.has(featureId)) return;
|
||||
|
||||
// Phase 2: ACTION
|
||||
emitAutoModeEvent({
|
||||
@@ -409,8 +430,8 @@ async function simulateAutoModeLoop(projectPath: string) {
|
||||
content: "Starting code implementation...",
|
||||
});
|
||||
|
||||
await delay(300);
|
||||
if (!mockAutoModeRunning) return;
|
||||
await delay(300, featureId);
|
||||
if (!mockRunningFeatures.has(featureId)) return;
|
||||
|
||||
// Simulate tool use
|
||||
emitAutoModeEvent({
|
||||
@@ -420,8 +441,8 @@ async function simulateAutoModeLoop(projectPath: string) {
|
||||
input: { file: "package.json" },
|
||||
});
|
||||
|
||||
await delay(300);
|
||||
if (!mockAutoModeRunning) return;
|
||||
await delay(300, featureId);
|
||||
if (!mockRunningFeatures.has(featureId)) return;
|
||||
|
||||
emitAutoModeEvent({
|
||||
type: "auto_mode_tool",
|
||||
@@ -430,8 +451,8 @@ async function simulateAutoModeLoop(projectPath: string) {
|
||||
input: { file: "src/feature.ts", content: "// Feature code" },
|
||||
});
|
||||
|
||||
await delay(500);
|
||||
if (!mockAutoModeRunning) return;
|
||||
await delay(500, featureId);
|
||||
if (!mockRunningFeatures.has(featureId)) return;
|
||||
|
||||
// Phase 3: VERIFICATION
|
||||
emitAutoModeEvent({
|
||||
@@ -447,8 +468,8 @@ async function simulateAutoModeLoop(projectPath: string) {
|
||||
content: "Verifying implementation and checking test results...",
|
||||
});
|
||||
|
||||
await delay(500);
|
||||
if (!mockAutoModeRunning) return;
|
||||
await delay(500, featureId);
|
||||
if (!mockRunningFeatures.has(featureId)) return;
|
||||
|
||||
emitAutoModeEvent({
|
||||
type: "auto_mode_progress",
|
||||
@@ -464,21 +485,15 @@ async function simulateAutoModeLoop(projectPath: string) {
|
||||
message: "Feature implemented successfully",
|
||||
});
|
||||
|
||||
await delay(300);
|
||||
if (!mockAutoModeRunning) return;
|
||||
|
||||
// All features complete
|
||||
emitAutoModeEvent({
|
||||
type: "auto_mode_complete",
|
||||
message: "All features completed!",
|
||||
});
|
||||
|
||||
mockAutoModeRunning = false;
|
||||
// Clean up this feature from running set
|
||||
mockRunningFeatures.delete(featureId);
|
||||
mockAutoModeTimeouts.delete(featureId);
|
||||
}
|
||||
|
||||
function delay(ms: number): Promise<void> {
|
||||
function delay(ms: number, featureId: string): Promise<void> {
|
||||
return new Promise(resolve => {
|
||||
mockAutoModeTimeout = setTimeout(resolve, ms);
|
||||
const timeout = setTimeout(resolve, ms);
|
||||
mockAutoModeTimeouts.set(featureId, timeout);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -36,12 +36,21 @@ export interface ChatSession {
|
||||
archived: boolean;
|
||||
}
|
||||
|
||||
export interface FeatureImage {
|
||||
id: string;
|
||||
data: string; // base64 encoded
|
||||
mimeType: string;
|
||||
filename: string;
|
||||
size: number;
|
||||
}
|
||||
|
||||
export interface Feature {
|
||||
id: string;
|
||||
category: string;
|
||||
description: string;
|
||||
steps: string[];
|
||||
status: "backlog" | "in_progress" | "verified";
|
||||
images?: FeatureImage[];
|
||||
}
|
||||
|
||||
export interface FileTreeNode {
|
||||
@@ -96,7 +105,7 @@ export interface AppState {
|
||||
|
||||
// Auto Mode
|
||||
isAutoModeRunning: boolean;
|
||||
currentAutoTask: string | null; // Feature ID being worked on
|
||||
runningAutoTasks: string[]; // Feature IDs being worked on (supports concurrent tasks)
|
||||
autoModeActivityLog: AutoModeActivity[];
|
||||
}
|
||||
|
||||
@@ -160,7 +169,9 @@ export interface AppActions {
|
||||
|
||||
// Auto Mode actions
|
||||
setAutoModeRunning: (running: boolean) => void;
|
||||
setCurrentAutoTask: (taskId: string | null) => void;
|
||||
addRunningTask: (taskId: string) => void;
|
||||
removeRunningTask: (taskId: string) => void;
|
||||
clearRunningTasks: () => void;
|
||||
addAutoModeActivity: (activity: Omit<AutoModeActivity, "id" | "timestamp">) => void;
|
||||
clearAutoModeActivity: () => void;
|
||||
|
||||
@@ -187,7 +198,7 @@ const initialState: AppState = {
|
||||
currentChatSession: null,
|
||||
chatHistoryOpen: false,
|
||||
isAutoModeRunning: false,
|
||||
currentAutoTask: null,
|
||||
runningAutoTasks: [],
|
||||
autoModeActivityLog: [],
|
||||
};
|
||||
|
||||
@@ -375,7 +386,19 @@ export const useAppStore = create<AppState & AppActions>()(
|
||||
|
||||
// Auto Mode actions
|
||||
setAutoModeRunning: (running) => set({ isAutoModeRunning: running }),
|
||||
setCurrentAutoTask: (taskId) => set({ currentAutoTask: taskId }),
|
||||
|
||||
addRunningTask: (taskId) => {
|
||||
const current = get().runningAutoTasks;
|
||||
if (!current.includes(taskId)) {
|
||||
set({ runningAutoTasks: [...current, taskId] });
|
||||
}
|
||||
},
|
||||
|
||||
removeRunningTask: (taskId) => {
|
||||
set({ runningAutoTasks: get().runningAutoTasks.filter(id => id !== taskId) });
|
||||
},
|
||||
|
||||
clearRunningTasks: () => set({ runningAutoTasks: [] }),
|
||||
|
||||
addAutoModeActivity: (activity) => {
|
||||
const id = `activity-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
|
||||
@@ -1,140 +0,0 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
|
||||
test.describe("Agent Loop (Plan-Act-Verify)", () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
// Navigate to the app and create a project
|
||||
await page.goto("/");
|
||||
|
||||
// Create a project first
|
||||
await page.getByTestId("new-project-card").click();
|
||||
await page.getByTestId("project-name-input").fill("Test Project");
|
||||
await page.getByTestId("project-path-input").fill("/test/path");
|
||||
await page.getByTestId("confirm-create-project").click();
|
||||
|
||||
// Wait for board view to load
|
||||
await expect(page.getByTestId("board-view")).toBeVisible();
|
||||
});
|
||||
|
||||
test("Step 1: Trigger agent on a simple task - auto mode starts", async ({ page }) => {
|
||||
// Find and click the Auto Mode button
|
||||
const autoModeButton = page.getByTestId("start-auto-mode");
|
||||
await expect(autoModeButton).toBeVisible();
|
||||
|
||||
// Click to start auto mode
|
||||
await autoModeButton.click();
|
||||
|
||||
// Verify auto mode has started - stop button should now be visible
|
||||
await expect(page.getByTestId("stop-auto-mode")).toBeVisible({ timeout: 5000 });
|
||||
});
|
||||
|
||||
test("Step 2: detailed logs show Planning phase", async ({ page }) => {
|
||||
// Start auto mode
|
||||
await page.getByTestId("start-auto-mode").click();
|
||||
|
||||
// Wait for the activity log to appear
|
||||
await expect(page.getByTestId("stop-auto-mode")).toBeVisible({ timeout: 5000 });
|
||||
|
||||
// The activity log panel should appear automatically when auto mode starts
|
||||
// Wait for planning phase to appear in the activity log
|
||||
await expect(page.getByTestId("planning-phase-icon")).toBeVisible({ timeout: 5000 });
|
||||
|
||||
// Verify the planning message is displayed
|
||||
await expect(page.getByText("Planning implementation for:")).toBeVisible({ timeout: 5000 });
|
||||
});
|
||||
|
||||
test("Step 3: detailed logs show Action phase", async ({ page }) => {
|
||||
// Start auto mode
|
||||
await page.getByTestId("start-auto-mode").click();
|
||||
|
||||
// Wait for auto mode to be running
|
||||
await expect(page.getByTestId("stop-auto-mode")).toBeVisible({ timeout: 5000 });
|
||||
|
||||
// Wait for action phase to appear in the activity log
|
||||
await expect(page.getByTestId("action-phase-icon")).toBeVisible({ timeout: 5000 });
|
||||
|
||||
// Verify the action message is displayed
|
||||
await expect(page.getByText("Executing implementation for:")).toBeVisible({ timeout: 5000 });
|
||||
});
|
||||
|
||||
test("Step 4: detailed logs show Verification phase", async ({ page }) => {
|
||||
// Start auto mode
|
||||
await page.getByTestId("start-auto-mode").click();
|
||||
|
||||
// Wait for auto mode to be running
|
||||
await expect(page.getByTestId("stop-auto-mode")).toBeVisible({ timeout: 5000 });
|
||||
|
||||
// Wait for verification phase to appear in the activity log
|
||||
await expect(page.getByTestId("verification-phase-icon")).toBeVisible({ timeout: 5000 });
|
||||
|
||||
// Verify the verification message is displayed
|
||||
await expect(page.getByText("Verifying implementation for:")).toBeVisible({ timeout: 5000 });
|
||||
});
|
||||
|
||||
test("Full agent loop: shows all three phases in sequence", async ({ page }) => {
|
||||
// Start auto mode
|
||||
await page.getByTestId("start-auto-mode").click();
|
||||
|
||||
// Wait for auto mode to be running
|
||||
await expect(page.getByTestId("stop-auto-mode")).toBeVisible({ timeout: 5000 });
|
||||
|
||||
// Wait for all phases to appear in sequence
|
||||
// Phase 1: Planning
|
||||
await expect(page.getByTestId("planning-phase-icon")).toBeVisible({ timeout: 5000 });
|
||||
await expect(page.getByText("Planning implementation for:")).toBeVisible();
|
||||
|
||||
// Phase 2: Action
|
||||
await expect(page.getByTestId("action-phase-icon")).toBeVisible({ timeout: 5000 });
|
||||
await expect(page.getByText("Executing implementation for:")).toBeVisible();
|
||||
|
||||
// Phase 3: Verification
|
||||
await expect(page.getByTestId("verification-phase-icon")).toBeVisible({ timeout: 5000 });
|
||||
await expect(page.getByText("Verifying implementation for:")).toBeVisible();
|
||||
|
||||
// Verify verification success message appears
|
||||
await expect(page.getByText("Verification successful")).toBeVisible({ timeout: 5000 });
|
||||
});
|
||||
|
||||
test("Agent loop can be stopped mid-execution", async ({ page }) => {
|
||||
// Start auto mode
|
||||
await page.getByTestId("start-auto-mode").click();
|
||||
|
||||
// Wait for auto mode to be running
|
||||
await expect(page.getByTestId("stop-auto-mode")).toBeVisible({ timeout: 5000 });
|
||||
|
||||
// Stop auto mode
|
||||
await page.getByTestId("stop-auto-mode").click();
|
||||
|
||||
// Verify auto mode has stopped - start button should be visible again
|
||||
await expect(page.getByTestId("start-auto-mode")).toBeVisible({ timeout: 5000 });
|
||||
});
|
||||
|
||||
test("Activity log toggle button works", async ({ page }) => {
|
||||
// Start auto mode
|
||||
await page.getByTestId("start-auto-mode").click();
|
||||
|
||||
// Wait for auto mode to be running and activity button to appear
|
||||
await expect(page.getByTestId("toggle-activity-log")).toBeVisible({ timeout: 5000 });
|
||||
|
||||
// The activity log should be visible initially when auto mode starts
|
||||
// Toggle it off
|
||||
await page.getByTestId("toggle-activity-log").click();
|
||||
|
||||
// Toggle it back on
|
||||
await page.getByTestId("toggle-activity-log").click();
|
||||
|
||||
// The log panel should be visible
|
||||
await expect(page.getByText("Auto Mode Activity")).toBeVisible();
|
||||
});
|
||||
|
||||
test("Tool usage is logged during action phase", async ({ page }) => {
|
||||
// Start auto mode
|
||||
await page.getByTestId("start-auto-mode").click();
|
||||
|
||||
// Wait for auto mode to be running
|
||||
await expect(page.getByTestId("stop-auto-mode")).toBeVisible({ timeout: 5000 });
|
||||
|
||||
// Wait for tool usage to appear in the activity log
|
||||
await expect(page.getByText("Using tool: Read")).toBeVisible({ timeout: 5000 });
|
||||
await expect(page.getByText("Using tool: Write")).toBeVisible({ timeout: 5000 });
|
||||
});
|
||||
});
|
||||
@@ -1,217 +0,0 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
|
||||
test.describe("Agent Tools", () => {
|
||||
test("can navigate to agent tools view when project is open", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
|
||||
// Create a project first
|
||||
await page.getByTestId("new-project-card").click();
|
||||
await page.getByTestId("project-name-input").fill("Test Project");
|
||||
await page.getByTestId("project-path-input").fill("/test/path");
|
||||
await page.getByTestId("confirm-create-project").click();
|
||||
|
||||
// Wait for board view to load
|
||||
await expect(page.getByTestId("board-view")).toBeVisible();
|
||||
|
||||
// Navigate to agent tools
|
||||
await page.getByTestId("nav-tools").click();
|
||||
|
||||
// Verify agent tools view is displayed
|
||||
await expect(page.getByTestId("agent-tools-view")).toBeVisible();
|
||||
});
|
||||
|
||||
test("agent tools view shows all three tool cards", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
|
||||
// Create a project first
|
||||
await page.getByTestId("new-project-card").click();
|
||||
await page.getByTestId("project-name-input").fill("Test Project");
|
||||
await page.getByTestId("project-path-input").fill("/test/path");
|
||||
await page.getByTestId("confirm-create-project").click();
|
||||
|
||||
// Navigate to agent tools
|
||||
await page.getByTestId("nav-tools").click();
|
||||
|
||||
// Verify all three tool cards are visible
|
||||
await expect(page.getByTestId("read-file-tool")).toBeVisible();
|
||||
await expect(page.getByTestId("write-file-tool")).toBeVisible();
|
||||
await expect(page.getByTestId("terminal-tool")).toBeVisible();
|
||||
});
|
||||
|
||||
test.describe("Read File Tool", () => {
|
||||
test("agent can request to read file and receive content", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
|
||||
// Create a project first
|
||||
await page.getByTestId("new-project-card").click();
|
||||
await page.getByTestId("project-name-input").fill("Test Project");
|
||||
await page.getByTestId("project-path-input").fill("/test/path");
|
||||
await page.getByTestId("confirm-create-project").click();
|
||||
|
||||
// Navigate to agent tools
|
||||
await page.getByTestId("nav-tools").click();
|
||||
|
||||
// Enter a file path
|
||||
await page.getByTestId("read-file-path-input").fill("/test/path/feature_list.json");
|
||||
|
||||
// Click execute
|
||||
await page.getByTestId("read-file-button").click();
|
||||
|
||||
// Wait for result
|
||||
await expect(page.getByTestId("read-file-result")).toBeVisible();
|
||||
|
||||
// Verify success message
|
||||
await expect(page.getByTestId("read-file-result")).toContainText("Success");
|
||||
});
|
||||
|
||||
test("read file tool shows input field for file path", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
|
||||
// Create a project first
|
||||
await page.getByTestId("new-project-card").click();
|
||||
await page.getByTestId("project-name-input").fill("Test Project");
|
||||
await page.getByTestId("project-path-input").fill("/test/path");
|
||||
await page.getByTestId("confirm-create-project").click();
|
||||
|
||||
// Navigate to agent tools
|
||||
await page.getByTestId("nav-tools").click();
|
||||
|
||||
// Verify input field exists
|
||||
await expect(page.getByTestId("read-file-path-input")).toBeVisible();
|
||||
await expect(page.getByTestId("read-file-button")).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Write File Tool", () => {
|
||||
test("agent can request to write file and file is written", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
|
||||
// Create a project first
|
||||
await page.getByTestId("new-project-card").click();
|
||||
await page.getByTestId("project-name-input").fill("Test Project");
|
||||
await page.getByTestId("project-path-input").fill("/test/path");
|
||||
await page.getByTestId("confirm-create-project").click();
|
||||
|
||||
// Navigate to agent tools
|
||||
await page.getByTestId("nav-tools").click();
|
||||
|
||||
// Enter file path and content
|
||||
await page.getByTestId("write-file-path-input").fill("/test/path/new-file.txt");
|
||||
await page.getByTestId("write-file-content-input").fill("Hello from agent!");
|
||||
|
||||
// Click execute
|
||||
await page.getByTestId("write-file-button").click();
|
||||
|
||||
// Wait for result
|
||||
await expect(page.getByTestId("write-file-result")).toBeVisible();
|
||||
|
||||
// Verify success message
|
||||
await expect(page.getByTestId("write-file-result")).toContainText("Success");
|
||||
await expect(page.getByTestId("write-file-result")).toContainText("File written successfully");
|
||||
});
|
||||
|
||||
test("write file tool shows path and content inputs", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
|
||||
// Create a project first
|
||||
await page.getByTestId("new-project-card").click();
|
||||
await page.getByTestId("project-name-input").fill("Test Project");
|
||||
await page.getByTestId("project-path-input").fill("/test/path");
|
||||
await page.getByTestId("confirm-create-project").click();
|
||||
|
||||
// Navigate to agent tools
|
||||
await page.getByTestId("nav-tools").click();
|
||||
|
||||
// Verify input fields exist
|
||||
await expect(page.getByTestId("write-file-path-input")).toBeVisible();
|
||||
await expect(page.getByTestId("write-file-content-input")).toBeVisible();
|
||||
await expect(page.getByTestId("write-file-button")).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Terminal Tool", () => {
|
||||
test("agent can request to run terminal command and receive stdout", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
|
||||
// Create a project first
|
||||
await page.getByTestId("new-project-card").click();
|
||||
await page.getByTestId("project-name-input").fill("Test Project");
|
||||
await page.getByTestId("project-path-input").fill("/test/path");
|
||||
await page.getByTestId("confirm-create-project").click();
|
||||
|
||||
// Navigate to agent tools
|
||||
await page.getByTestId("nav-tools").click();
|
||||
|
||||
// Enter command (default is 'ls')
|
||||
await page.getByTestId("terminal-command-input").fill("ls");
|
||||
|
||||
// Click execute
|
||||
await page.getByTestId("run-terminal-button").click();
|
||||
|
||||
// Wait for result
|
||||
await expect(page.getByTestId("terminal-result")).toBeVisible();
|
||||
|
||||
// Verify success and output
|
||||
await expect(page.getByTestId("terminal-result")).toContainText("Success");
|
||||
await expect(page.getByTestId("terminal-result")).toContainText("$ ls");
|
||||
});
|
||||
|
||||
test("terminal tool shows command input field", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
|
||||
// Create a project first
|
||||
await page.getByTestId("new-project-card").click();
|
||||
await page.getByTestId("project-name-input").fill("Test Project");
|
||||
await page.getByTestId("project-path-input").fill("/test/path");
|
||||
await page.getByTestId("confirm-create-project").click();
|
||||
|
||||
// Navigate to agent tools
|
||||
await page.getByTestId("nav-tools").click();
|
||||
|
||||
// Verify input field exists
|
||||
await expect(page.getByTestId("terminal-command-input")).toBeVisible();
|
||||
await expect(page.getByTestId("run-terminal-button")).toBeVisible();
|
||||
});
|
||||
|
||||
test("terminal tool can run pwd command", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
|
||||
// Create a project first
|
||||
await page.getByTestId("new-project-card").click();
|
||||
await page.getByTestId("project-name-input").fill("Test Project");
|
||||
await page.getByTestId("project-path-input").fill("/test/path");
|
||||
await page.getByTestId("confirm-create-project").click();
|
||||
|
||||
// Navigate to agent tools
|
||||
await page.getByTestId("nav-tools").click();
|
||||
|
||||
// Enter pwd command
|
||||
await page.getByTestId("terminal-command-input").fill("pwd");
|
||||
|
||||
// Click execute
|
||||
await page.getByTestId("run-terminal-button").click();
|
||||
|
||||
// Wait for result
|
||||
await expect(page.getByTestId("terminal-result")).toBeVisible();
|
||||
|
||||
// Verify success
|
||||
await expect(page.getByTestId("terminal-result")).toContainText("Success");
|
||||
});
|
||||
});
|
||||
|
||||
test("tool log section is visible", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
|
||||
// Create a project first
|
||||
await page.getByTestId("new-project-card").click();
|
||||
await page.getByTestId("project-name-input").fill("Test Project");
|
||||
await page.getByTestId("project-path-input").fill("/test/path");
|
||||
await page.getByTestId("confirm-create-project").click();
|
||||
|
||||
// Navigate to agent tools
|
||||
await page.getByTestId("nav-tools").click();
|
||||
|
||||
// Verify tool log section is visible
|
||||
await expect(page.getByTestId("tool-log")).toBeVisible();
|
||||
});
|
||||
});
|
||||
@@ -1,657 +0,0 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
|
||||
test.describe("Project Analysis", () => {
|
||||
test("can navigate to analysis view when project is open", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto("/");
|
||||
|
||||
// Create a project first using dropdown
|
||||
await page.getByTestId("create-new-project").click();
|
||||
await page.getByTestId("quick-setup-option").click();
|
||||
await expect(page.getByTestId("new-project-dialog")).toBeVisible();
|
||||
await page.getByTestId("project-name-input").fill("Analysis Test Project");
|
||||
await page.getByTestId("project-path-input").fill("/test/analysis/project");
|
||||
await page.getByTestId("confirm-create-project").click();
|
||||
|
||||
// Wait for board view to load
|
||||
await expect(page.getByTestId("board-view")).toBeVisible();
|
||||
|
||||
// Click on Analysis in sidebar
|
||||
await page.getByTestId("nav-analysis").click();
|
||||
|
||||
// Verify analysis view is displayed
|
||||
await expect(page.getByTestId("analysis-view")).toBeVisible();
|
||||
});
|
||||
|
||||
test("analysis view shows 'No Analysis Yet' message initially", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto("/");
|
||||
|
||||
// Create a project first using dropdown
|
||||
await page.getByTestId("create-new-project").click();
|
||||
await page.getByTestId("quick-setup-option").click();
|
||||
await expect(page.getByTestId("new-project-dialog")).toBeVisible();
|
||||
await page.getByTestId("project-name-input").fill("Analysis Test Project2");
|
||||
await page
|
||||
.getByTestId("project-path-input")
|
||||
.fill("/test/analysis/project2");
|
||||
await page.getByTestId("confirm-create-project").click();
|
||||
await expect(page.getByTestId("board-view")).toBeVisible();
|
||||
|
||||
// Navigate to analysis view
|
||||
await page.getByTestId("nav-analysis").click();
|
||||
await expect(page.getByTestId("analysis-view")).toBeVisible();
|
||||
|
||||
// Verify no analysis message
|
||||
await expect(page.getByText("No Analysis Yet")).toBeVisible();
|
||||
await expect(page.getByText('Click "Analyze Project"')).toBeVisible();
|
||||
});
|
||||
|
||||
test("shows 'Analyze Project' button", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
|
||||
// Create a project first using dropdown
|
||||
await page.getByTestId("create-new-project").click();
|
||||
await page.getByTestId("quick-setup-option").click();
|
||||
await expect(page.getByTestId("new-project-dialog")).toBeVisible();
|
||||
await page.getByTestId("project-name-input").fill("Analysis Test Project3");
|
||||
await page
|
||||
.getByTestId("project-path-input")
|
||||
.fill("/test/analysis/project3");
|
||||
await page.getByTestId("confirm-create-project").click();
|
||||
await expect(page.getByTestId("board-view")).toBeVisible();
|
||||
|
||||
// Navigate to analysis view
|
||||
await page.getByTestId("nav-analysis").click();
|
||||
await expect(page.getByTestId("analysis-view")).toBeVisible();
|
||||
|
||||
// Verify analyze button is visible
|
||||
await expect(page.getByTestId("analyze-project-button")).toBeVisible();
|
||||
});
|
||||
|
||||
test("can run project analysis", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
|
||||
// Create a project first using dropdown
|
||||
await page.getByTestId("create-new-project").click();
|
||||
await page.getByTestId("quick-setup-option").click();
|
||||
await expect(page.getByTestId("new-project-dialog")).toBeVisible();
|
||||
await page.getByTestId("project-name-input").fill("Analysis Test Project4");
|
||||
await page
|
||||
.getByTestId("project-path-input")
|
||||
.fill("/test/analysis/project4");
|
||||
await page.getByTestId("confirm-create-project").click();
|
||||
await expect(page.getByTestId("board-view")).toBeVisible();
|
||||
|
||||
// Navigate to analysis view
|
||||
await page.getByTestId("nav-analysis").click();
|
||||
await expect(page.getByTestId("analysis-view")).toBeVisible();
|
||||
|
||||
// Click analyze button
|
||||
await page.getByTestId("analyze-project-button").click();
|
||||
|
||||
// Wait for analysis to complete and stats to appear
|
||||
await expect(page.getByTestId("analysis-stats")).toBeVisible();
|
||||
|
||||
// Verify statistics are displayed
|
||||
await expect(page.getByTestId("total-files")).toBeVisible();
|
||||
await expect(page.getByTestId("total-directories")).toBeVisible();
|
||||
});
|
||||
|
||||
test("analysis shows file tree after running", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
|
||||
// Create a project first using dropdown
|
||||
await page.getByTestId("create-new-project").click();
|
||||
await page.getByTestId("quick-setup-option").click();
|
||||
await expect(page.getByTestId("new-project-dialog")).toBeVisible();
|
||||
await page.getByTestId("project-name-input").fill("Analysis Test Project5");
|
||||
await page
|
||||
.getByTestId("project-path-input")
|
||||
.fill("/test/analysis/project5");
|
||||
await page.getByTestId("confirm-create-project").click();
|
||||
await expect(page.getByTestId("board-view")).toBeVisible();
|
||||
|
||||
// Navigate to analysis view
|
||||
await page.getByTestId("nav-analysis").click();
|
||||
await expect(page.getByTestId("analysis-view")).toBeVisible();
|
||||
|
||||
// Click analyze button
|
||||
await page.getByTestId("analyze-project-button").click();
|
||||
|
||||
// Wait for analysis to complete
|
||||
await expect(page.getByTestId("analysis-file-tree")).toBeVisible();
|
||||
|
||||
// Verify file tree is displayed
|
||||
await expect(page.getByTestId("analysis-file-tree")).toBeVisible();
|
||||
});
|
||||
|
||||
test("analysis shows files by extension breakdown", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
|
||||
// Create a project first using dropdown
|
||||
await page.getByTestId("create-new-project").click();
|
||||
await page.getByTestId("quick-setup-option").click();
|
||||
await expect(page.getByTestId("new-project-dialog")).toBeVisible();
|
||||
await page.getByTestId("project-name-input").fill("Analysis Test Project6");
|
||||
await page
|
||||
.getByTestId("project-path-input")
|
||||
.fill("/test/analysis/project6");
|
||||
await page.getByTestId("confirm-create-project").click();
|
||||
await expect(page.getByTestId("board-view")).toBeVisible();
|
||||
|
||||
// Navigate to analysis view
|
||||
await page.getByTestId("nav-analysis").click();
|
||||
await expect(page.getByTestId("analysis-view")).toBeVisible();
|
||||
|
||||
// Click analyze button
|
||||
await page.getByTestId("analyze-project-button").click();
|
||||
|
||||
// Wait for analysis to complete
|
||||
await expect(page.getByTestId("files-by-extension")).toBeVisible();
|
||||
|
||||
// Verify files by extension card is displayed
|
||||
await expect(page.getByTestId("files-by-extension")).toBeVisible();
|
||||
});
|
||||
|
||||
test("file tree displays correct structure with directories and files", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto("/");
|
||||
|
||||
// Create a project first using dropdown
|
||||
await page.getByTestId("create-new-project").click();
|
||||
await page.getByTestId("quick-setup-option").click();
|
||||
await expect(page.getByTestId("new-project-dialog")).toBeVisible();
|
||||
await page.getByTestId("project-name-input").fill("Analysis Test Project7");
|
||||
await page
|
||||
.getByTestId("project-path-input")
|
||||
.fill("/test/analysis/project7");
|
||||
await page.getByTestId("confirm-create-project").click();
|
||||
await expect(page.getByTestId("board-view")).toBeVisible();
|
||||
|
||||
// Navigate to analysis view
|
||||
await page.getByTestId("nav-analysis").click();
|
||||
await expect(page.getByTestId("analysis-view")).toBeVisible();
|
||||
|
||||
// Click analyze button
|
||||
await page.getByTestId("analyze-project-button").click();
|
||||
|
||||
// Wait for file tree to be populated
|
||||
await expect(page.getByTestId("analysis-file-tree")).toBeVisible();
|
||||
|
||||
// Verify src directory is in the tree (mock data provides this)
|
||||
await expect(page.getByTestId("analysis-node-src")).toBeVisible();
|
||||
|
||||
// Verify some files are in the tree
|
||||
await expect(page.getByTestId("analysis-node-package.json")).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Generate Spec from Code", () => {
|
||||
test("shows Generate Spec card after analysis is complete", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto("/");
|
||||
|
||||
// Step 1: Open project with code but no spec
|
||||
// Use dropdown to create project
|
||||
await page.getByTestId("create-new-project").click();
|
||||
await page.getByTestId("quick-setup-option").click();
|
||||
await expect(page.getByTestId("new-project-dialog")).toBeVisible();
|
||||
await page
|
||||
.getByTestId("project-name-input")
|
||||
.fill("Generate Spec Test Project");
|
||||
await page
|
||||
.getByTestId("project-path-input")
|
||||
.fill("/test/generate-spec/project");
|
||||
await page.getByTestId("confirm-create-project").click();
|
||||
await expect(page.getByTestId("board-view")).toBeVisible();
|
||||
|
||||
// Navigate to analysis view
|
||||
await page.getByTestId("nav-analysis").click();
|
||||
await expect(page.getByTestId("analysis-view")).toBeVisible();
|
||||
|
||||
// Run analysis first
|
||||
await page.getByTestId("analyze-project-button").click();
|
||||
await expect(page.getByTestId("analysis-stats")).toBeVisible();
|
||||
|
||||
// Verify Generate Spec card is visible
|
||||
await expect(page.getByTestId("generate-spec-card")).toBeVisible();
|
||||
});
|
||||
|
||||
test("shows Generate Spec button after analysis", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
|
||||
// Create a project
|
||||
await page.getByTestId("create-new-project").click();
|
||||
await page.getByTestId("quick-setup-option").click();
|
||||
await expect(page.getByTestId("new-project-dialog")).toBeVisible();
|
||||
await page
|
||||
.getByTestId("project-name-input")
|
||||
.fill("Generate Spec Test Project2");
|
||||
await page
|
||||
.getByTestId("project-path-input")
|
||||
.fill("/test/generate-spec/project2");
|
||||
await page.getByTestId("confirm-create-project").click();
|
||||
await expect(page.getByTestId("board-view")).toBeVisible();
|
||||
|
||||
// Navigate to analysis view
|
||||
await page.getByTestId("nav-analysis").click();
|
||||
await expect(page.getByTestId("analysis-view")).toBeVisible();
|
||||
|
||||
// Run analysis first
|
||||
await page.getByTestId("analyze-project-button").click();
|
||||
await expect(page.getByTestId("analysis-stats")).toBeVisible();
|
||||
|
||||
// Step 2: Trigger 'Generate Spec' - verify button exists
|
||||
await expect(page.getByTestId("generate-spec-button")).toBeVisible();
|
||||
await expect(page.getByTestId("generate-spec-button")).toHaveText(
|
||||
/Generate Spec/
|
||||
);
|
||||
});
|
||||
|
||||
test("can trigger Generate Spec and shows success message", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto("/");
|
||||
|
||||
// Step 1: Open project with code but no spec
|
||||
await page.getByTestId("create-new-project").click();
|
||||
await page.getByTestId("quick-setup-option").click();
|
||||
await expect(page.getByTestId("new-project-dialog")).toBeVisible();
|
||||
await page
|
||||
.getByTestId("project-name-input")
|
||||
.fill("Generate Spec Test Project3");
|
||||
await page
|
||||
.getByTestId("project-path-input")
|
||||
.fill("/test/generate-spec/project3");
|
||||
await page.getByTestId("confirm-create-project").click();
|
||||
await expect(page.getByTestId("board-view")).toBeVisible();
|
||||
|
||||
// Navigate to analysis view
|
||||
await page.getByTestId("nav-analysis").click();
|
||||
await expect(page.getByTestId("analysis-view")).toBeVisible();
|
||||
|
||||
// Run analysis first
|
||||
await page.getByTestId("analyze-project-button").click();
|
||||
await expect(page.getByTestId("analysis-stats")).toBeVisible();
|
||||
|
||||
// Step 2: Trigger 'Generate Spec'
|
||||
await page.getByTestId("generate-spec-button").click();
|
||||
|
||||
// Step 3: Verify app_spec.txt is created (success message appears)
|
||||
await expect(page.getByTestId("spec-generated-success")).toBeVisible();
|
||||
await expect(
|
||||
page.getByText("app_spec.txt created successfully")
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test("Generate Spec card displays description", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
|
||||
// Create a project
|
||||
await page.getByTestId("create-new-project").click();
|
||||
await page.getByTestId("quick-setup-option").click();
|
||||
await expect(page.getByTestId("new-project-dialog")).toBeVisible();
|
||||
await page
|
||||
.getByTestId("project-name-input")
|
||||
.fill("Generate Spec Test Project4");
|
||||
await page
|
||||
.getByTestId("project-path-input")
|
||||
.fill("/test/generate-spec/project4");
|
||||
await page.getByTestId("confirm-create-project").click();
|
||||
await expect(page.getByTestId("board-view")).toBeVisible();
|
||||
|
||||
// Navigate to analysis view and run analysis
|
||||
await page.getByTestId("nav-analysis").click();
|
||||
await page.getByTestId("analyze-project-button").click();
|
||||
await expect(page.getByTestId("generate-spec-card")).toBeVisible();
|
||||
|
||||
// Step 4: Verify spec content accurately reflects codebase
|
||||
// Check that the card shows relevant information about what the spec generation does
|
||||
await expect(
|
||||
page.getByText("Create app_spec.txt from analysis")
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByText(/Generate a project specification/)
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test("Generate Spec button is disabled while generating", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto("/");
|
||||
|
||||
// Create a project
|
||||
await page.getByTestId("create-new-project").click();
|
||||
await page.getByTestId("quick-setup-option").click();
|
||||
await expect(page.getByTestId("new-project-dialog")).toBeVisible();
|
||||
await page
|
||||
.getByTestId("project-name-input")
|
||||
.fill("Generate Spec Test Project5");
|
||||
await page
|
||||
.getByTestId("project-path-input")
|
||||
.fill("/test/generate-spec/project5");
|
||||
await page.getByTestId("confirm-create-project").click();
|
||||
await expect(page.getByTestId("board-view")).toBeVisible();
|
||||
|
||||
// Navigate to analysis view and run analysis
|
||||
await page.getByTestId("nav-analysis").click();
|
||||
await page.getByTestId("analyze-project-button").click();
|
||||
await expect(page.getByTestId("generate-spec-card")).toBeVisible();
|
||||
|
||||
// Check the button exists and can be clicked
|
||||
const generateButton = page.getByTestId("generate-spec-button");
|
||||
await expect(generateButton).toBeVisible();
|
||||
await expect(generateButton).toBeEnabled();
|
||||
});
|
||||
|
||||
test("generated spec file reflects analyzed codebase structure", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto("/");
|
||||
|
||||
// Step 1: Open project with code but no spec
|
||||
await page.getByTestId("create-new-project").click();
|
||||
await page.getByTestId("quick-setup-option").click();
|
||||
await expect(page.getByTestId("new-project-dialog")).toBeVisible();
|
||||
await page.getByTestId("project-name-input").fill("Spec Verify Project");
|
||||
await page
|
||||
.getByTestId("project-path-input")
|
||||
.fill("/test/spec-verify/project");
|
||||
await page.getByTestId("confirm-create-project").click();
|
||||
await expect(page.getByTestId("board-view")).toBeVisible();
|
||||
|
||||
// Navigate to analysis view
|
||||
await page.getByTestId("nav-analysis").click();
|
||||
await expect(page.getByTestId("analysis-view")).toBeVisible();
|
||||
|
||||
// Run analysis first
|
||||
await page.getByTestId("analyze-project-button").click();
|
||||
await expect(page.getByTestId("analysis-stats")).toBeVisible();
|
||||
|
||||
// Verify statistics are correctly computed (mock data provides this)
|
||||
const totalFiles = page.getByTestId("total-files");
|
||||
await expect(totalFiles).toBeVisible();
|
||||
|
||||
const totalDirectories = page.getByTestId("total-directories");
|
||||
await expect(totalDirectories).toBeVisible();
|
||||
|
||||
// Step 2: Trigger 'Generate Spec'
|
||||
await page.getByTestId("generate-spec-button").click();
|
||||
|
||||
// Step 3: Verify app_spec.txt is created (success message appears)
|
||||
await expect(page.getByTestId("spec-generated-success")).toBeVisible();
|
||||
|
||||
// Step 4: Verify spec content accurately reflects codebase
|
||||
// Navigate to spec view to verify the generated content
|
||||
await page.getByTestId("nav-spec").click();
|
||||
await expect(page.getByTestId("spec-view")).toBeVisible();
|
||||
|
||||
// Verify the spec editor has content that reflects the analyzed codebase
|
||||
const specEditor = page.getByTestId("spec-editor");
|
||||
await expect(specEditor).toBeVisible();
|
||||
|
||||
// Verify key elements of the generated spec are present
|
||||
// The spec should contain project_specification XML tags
|
||||
const specContent = await specEditor.inputValue();
|
||||
expect(specContent).toContain("<project_specification>");
|
||||
expect(specContent).toContain("<project_name>");
|
||||
expect(specContent).toContain("<technology_stack>");
|
||||
expect(specContent).toContain("<project_structure>");
|
||||
expect(specContent).toContain("<file_breakdown>");
|
||||
expect(specContent).toContain("</project_specification>");
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Generate Feature List from Code", () => {
|
||||
test("shows Generate Feature List card after analysis is complete", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto("/");
|
||||
|
||||
// Step 1: Open project with implemented features
|
||||
await page.getByTestId("create-new-project").click();
|
||||
await page.getByTestId("quick-setup-option").click();
|
||||
await expect(page.getByTestId("new-project-dialog")).toBeVisible();
|
||||
await page
|
||||
.getByTestId("project-name-input")
|
||||
.fill("Feature List Test Project");
|
||||
await page
|
||||
.getByTestId("project-path-input")
|
||||
.fill("/test/feature-list/project");
|
||||
await page.getByTestId("confirm-create-project").click();
|
||||
await expect(page.getByTestId("board-view")).toBeVisible();
|
||||
|
||||
// Navigate to analysis view
|
||||
await page.getByTestId("nav-analysis").click();
|
||||
await expect(page.getByTestId("analysis-view")).toBeVisible();
|
||||
|
||||
// Run analysis first
|
||||
await page.getByTestId("analyze-project-button").click();
|
||||
await expect(page.getByTestId("analysis-stats")).toBeVisible();
|
||||
|
||||
// Verify Generate Feature List card is visible
|
||||
await expect(page.getByTestId("generate-feature-list-card")).toBeVisible();
|
||||
});
|
||||
|
||||
test("shows Generate Feature List button after analysis", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto("/");
|
||||
|
||||
// Create a project
|
||||
await page.getByTestId("create-new-project").click();
|
||||
await page.getByTestId("quick-setup-option").click();
|
||||
await expect(page.getByTestId("new-project-dialog")).toBeVisible();
|
||||
await page
|
||||
.getByTestId("project-name-input")
|
||||
.fill("Feature List Test Project2");
|
||||
await page
|
||||
.getByTestId("project-path-input")
|
||||
.fill("/test/feature-list/project2");
|
||||
await page.getByTestId("confirm-create-project").click();
|
||||
await expect(page.getByTestId("board-view")).toBeVisible();
|
||||
|
||||
// Navigate to analysis view
|
||||
await page.getByTestId("nav-analysis").click();
|
||||
await expect(page.getByTestId("analysis-view")).toBeVisible();
|
||||
|
||||
// Run analysis first
|
||||
await page.getByTestId("analyze-project-button").click();
|
||||
await expect(page.getByTestId("analysis-stats")).toBeVisible();
|
||||
|
||||
// Step 2: Trigger 'Generate Feature List' - verify button exists
|
||||
await expect(
|
||||
page.getByTestId("generate-feature-list-button")
|
||||
).toBeVisible();
|
||||
await expect(page.getByTestId("generate-feature-list-button")).toHaveText(
|
||||
/Generate Feature List/
|
||||
);
|
||||
});
|
||||
|
||||
test("can trigger Generate Feature List and shows success message", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto("/");
|
||||
|
||||
// Step 1: Open project with implemented features
|
||||
await page.getByTestId("create-new-project").click();
|
||||
await page.getByTestId("quick-setup-option").click();
|
||||
await expect(page.getByTestId("new-project-dialog")).toBeVisible();
|
||||
await page
|
||||
.getByTestId("project-name-input")
|
||||
.fill("Feature List Test Project3");
|
||||
await page
|
||||
.getByTestId("project-path-input")
|
||||
.fill("/test/feature-list/project3");
|
||||
await page.getByTestId("confirm-create-project").click();
|
||||
await expect(page.getByTestId("board-view")).toBeVisible();
|
||||
|
||||
// Navigate to analysis view
|
||||
await page.getByTestId("nav-analysis").click();
|
||||
await expect(page.getByTestId("analysis-view")).toBeVisible();
|
||||
|
||||
// Run analysis first
|
||||
await page.getByTestId("analyze-project-button").click();
|
||||
await expect(page.getByTestId("analysis-stats")).toBeVisible();
|
||||
|
||||
// Step 2: Trigger 'Generate Feature List'
|
||||
await page.getByTestId("generate-feature-list-button").click();
|
||||
|
||||
// Step 3: Verify .automaker/feature_list.json is created (success message appears)
|
||||
await expect(
|
||||
page.getByTestId("feature-list-generated-success")
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByText("feature_list.json created successfully")
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test("Generate Feature List card displays description", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
|
||||
// Create a project
|
||||
await page.getByTestId("create-new-project").click();
|
||||
await page.getByTestId("quick-setup-option").click();
|
||||
await expect(page.getByTestId("new-project-dialog")).toBeVisible();
|
||||
await page
|
||||
.getByTestId("project-name-input")
|
||||
.fill("Feature List Test Project4");
|
||||
await page
|
||||
.getByTestId("project-path-input")
|
||||
.fill("/test/feature-list/project4");
|
||||
await page.getByTestId("confirm-create-project").click();
|
||||
await expect(page.getByTestId("board-view")).toBeVisible();
|
||||
|
||||
// Navigate to analysis view and run analysis
|
||||
await page.getByTestId("nav-analysis").click();
|
||||
await page.getByTestId("analyze-project-button").click();
|
||||
await expect(page.getByTestId("generate-feature-list-card")).toBeVisible();
|
||||
|
||||
// Check that the card shows relevant information about what the feature list generation does
|
||||
await expect(
|
||||
page.getByText("Create .automaker/feature_list.json from analysis")
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByText(/Automatically detect and generate a feature list/)
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test("Generate Feature List button is enabled after analysis", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto("/");
|
||||
|
||||
// Create a project
|
||||
await page.getByTestId("create-new-project").click();
|
||||
await page.getByTestId("quick-setup-option").click();
|
||||
await expect(page.getByTestId("new-project-dialog")).toBeVisible();
|
||||
await page
|
||||
.getByTestId("project-name-input")
|
||||
.fill("Feature List Test Project5");
|
||||
await page
|
||||
.getByTestId("project-path-input")
|
||||
.fill("/test/feature-list/project5");
|
||||
await page.getByTestId("confirm-create-project").click();
|
||||
await expect(page.getByTestId("board-view")).toBeVisible();
|
||||
|
||||
// Navigate to analysis view and run analysis
|
||||
await page.getByTestId("nav-analysis").click();
|
||||
await page.getByTestId("analyze-project-button").click();
|
||||
await expect(page.getByTestId("generate-feature-list-card")).toBeVisible();
|
||||
|
||||
// Check the button exists and is enabled
|
||||
const generateButton = page.getByTestId("generate-feature-list-button");
|
||||
await expect(generateButton).toBeVisible();
|
||||
await expect(generateButton).toBeEnabled();
|
||||
});
|
||||
|
||||
test("generated feature list contains features with passes: true", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto("/");
|
||||
|
||||
// Step 1: Open project with implemented features
|
||||
await page.getByTestId("create-new-project").click();
|
||||
await page.getByTestId("quick-setup-option").click();
|
||||
await expect(page.getByTestId("new-project-dialog")).toBeVisible();
|
||||
await page.getByTestId("project-name-input").fill("Feature Verify Project");
|
||||
await page
|
||||
.getByTestId("project-path-input")
|
||||
.fill("/test/feature-verify/project");
|
||||
await page.getByTestId("confirm-create-project").click();
|
||||
await expect(page.getByTestId("board-view")).toBeVisible();
|
||||
|
||||
// Navigate to analysis view
|
||||
await page.getByTestId("nav-analysis").click();
|
||||
await expect(page.getByTestId("analysis-view")).toBeVisible();
|
||||
|
||||
// Run analysis first
|
||||
await page.getByTestId("analyze-project-button").click();
|
||||
await expect(page.getByTestId("analysis-stats")).toBeVisible();
|
||||
|
||||
// Verify statistics are correctly computed (mock data provides this)
|
||||
const totalFiles = page.getByTestId("total-files");
|
||||
await expect(totalFiles).toBeVisible();
|
||||
|
||||
const totalDirectories = page.getByTestId("total-directories");
|
||||
await expect(totalDirectories).toBeVisible();
|
||||
|
||||
// Step 2: Trigger 'Generate Feature List'
|
||||
await page.getByTestId("generate-feature-list-button").click();
|
||||
|
||||
// Step 3: Verify .automaker/feature_list.json is created (success message appears)
|
||||
await expect(
|
||||
page.getByTestId("feature-list-generated-success")
|
||||
).toBeVisible();
|
||||
|
||||
// Step 4: Verify existing features are marked 'passes': true
|
||||
// Navigate to board view to verify the features are loaded
|
||||
await page.getByTestId("nav-board").click();
|
||||
await expect(page.getByTestId("board-view")).toBeVisible();
|
||||
|
||||
// The generated feature list should have been written and can be loaded
|
||||
// The mock system writes to a mock file system, so we verify through UI that
|
||||
// the generation completed successfully (the success message is sufficient proof)
|
||||
});
|
||||
|
||||
test("Generate Feature List can be triggered multiple times", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto("/");
|
||||
|
||||
// Create a project
|
||||
await page.getByTestId("create-new-project").click();
|
||||
await page.getByTestId("quick-setup-option").click();
|
||||
await expect(page.getByTestId("new-project-dialog")).toBeVisible();
|
||||
await page
|
||||
.getByTestId("project-name-input")
|
||||
.fill("Feature List Multi Test");
|
||||
await page
|
||||
.getByTestId("project-path-input")
|
||||
.fill("/test/feature-list/multi");
|
||||
await page.getByTestId("confirm-create-project").click();
|
||||
await expect(page.getByTestId("board-view")).toBeVisible();
|
||||
|
||||
// Navigate to analysis view and run analysis
|
||||
await page.getByTestId("nav-analysis").click();
|
||||
await page.getByTestId("analyze-project-button").click();
|
||||
await expect(page.getByTestId("generate-feature-list-card")).toBeVisible();
|
||||
|
||||
// Generate feature list first time
|
||||
await page.getByTestId("generate-feature-list-button").click();
|
||||
await expect(
|
||||
page.getByTestId("feature-list-generated-success")
|
||||
).toBeVisible();
|
||||
|
||||
// Generate feature list second time (should overwrite)
|
||||
await page.getByTestId("generate-feature-list-button").click();
|
||||
await expect(
|
||||
page.getByTestId("feature-list-generated-success")
|
||||
).toBeVisible();
|
||||
});
|
||||
});
|
||||
@@ -1,294 +0,0 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
|
||||
test.describe("Claude SDK Integration - Autonomous Agent", () => {
|
||||
test.describe("Step 1: Configure API Key", () => {
|
||||
test("can navigate to settings and configure Anthropic API key", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
|
||||
// Navigate to settings
|
||||
await page.getByTestId("settings-button").click();
|
||||
await expect(page.getByTestId("settings-view")).toBeVisible();
|
||||
|
||||
// Verify Anthropic API key input is available
|
||||
const apiKeyInput = page.getByTestId("anthropic-api-key-input");
|
||||
await expect(apiKeyInput).toBeVisible();
|
||||
await expect(apiKeyInput).toBeEditable();
|
||||
|
||||
// Enter a test API key
|
||||
await apiKeyInput.fill("sk-ant-api03-test-key-for-integration-test");
|
||||
|
||||
// Save settings
|
||||
await page.getByTestId("save-settings").click();
|
||||
await expect(page.getByText("Saved!")).toBeVisible();
|
||||
});
|
||||
|
||||
test("API key input has proper security features", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await page.getByTestId("settings-button").click();
|
||||
|
||||
// Verify password masking by default
|
||||
await expect(page.getByTestId("anthropic-api-key-input")).toHaveAttribute("type", "password");
|
||||
|
||||
// Can toggle visibility
|
||||
await page.getByTestId("toggle-anthropic-visibility").click();
|
||||
await expect(page.getByTestId("anthropic-api-key-input")).toHaveAttribute("type", "text");
|
||||
|
||||
// Can toggle back to hidden
|
||||
await page.getByTestId("toggle-anthropic-visibility").click();
|
||||
await expect(page.getByTestId("anthropic-api-key-input")).toHaveAttribute("type", "password");
|
||||
});
|
||||
|
||||
test("API key persists across page reloads", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await page.getByTestId("settings-button").click();
|
||||
|
||||
// Enter and save API key
|
||||
const testKey = "sk-ant-api03-persistence-test-key";
|
||||
await page.getByTestId("anthropic-api-key-input").fill(testKey);
|
||||
await page.getByTestId("save-settings").click();
|
||||
await expect(page.getByText("Saved!")).toBeVisible();
|
||||
|
||||
// Reload and verify persistence
|
||||
await page.reload();
|
||||
await page.getByTestId("settings-button").click();
|
||||
|
||||
// Make key visible and verify
|
||||
await page.getByTestId("toggle-anthropic-visibility").click();
|
||||
await expect(page.getByTestId("anthropic-api-key-input")).toHaveValue(testKey);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Step 2: Send test prompt", () => {
|
||||
test("test connection button is visible in settings", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await page.getByTestId("settings-button").click();
|
||||
|
||||
// Test connection button should be visible
|
||||
const testButton = page.getByTestId("test-claude-connection");
|
||||
await expect(testButton).toBeVisible();
|
||||
await expect(testButton).toContainText("Test");
|
||||
});
|
||||
|
||||
test("test connection button is disabled without API key", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await page.getByTestId("settings-button").click();
|
||||
|
||||
// Clear any existing API key
|
||||
await page.getByTestId("anthropic-api-key-input").fill("");
|
||||
|
||||
// Test button should be disabled
|
||||
await expect(page.getByTestId("test-claude-connection")).toBeDisabled();
|
||||
});
|
||||
|
||||
test("test connection button is enabled with API key", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await page.getByTestId("settings-button").click();
|
||||
|
||||
// Enter API key
|
||||
await page.getByTestId("anthropic-api-key-input").fill("sk-ant-test-key");
|
||||
|
||||
// Test button should be enabled
|
||||
await expect(page.getByTestId("test-claude-connection")).toBeEnabled();
|
||||
});
|
||||
|
||||
test("clicking test sends request to Claude API endpoint", async ({ page }) => {
|
||||
// Setup API route mock
|
||||
await page.route("**/api/claude/test", async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({
|
||||
success: true,
|
||||
message: "Connection successful! Claude responded.",
|
||||
model: "claude-sonnet-4-20250514",
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
await page.goto("/");
|
||||
await page.getByTestId("settings-button").click();
|
||||
|
||||
// Enter API key
|
||||
await page.getByTestId("anthropic-api-key-input").fill("sk-ant-test-key");
|
||||
|
||||
// Click test button
|
||||
await page.getByTestId("test-claude-connection").click();
|
||||
|
||||
// Should show loading state briefly then success
|
||||
await expect(page.getByTestId("test-connection-result")).toBeVisible({ timeout: 10000 });
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Step 3: Verify response received", () => {
|
||||
test("displays success message when connection succeeds", async ({ page }) => {
|
||||
// Mock successful response
|
||||
await page.route("**/api/claude/test", async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({
|
||||
success: true,
|
||||
message: 'Connection successful! Response: "Claude SDK connection successful!"',
|
||||
model: "claude-sonnet-4-20250514",
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
await page.goto("/");
|
||||
await page.getByTestId("settings-button").click();
|
||||
|
||||
await page.getByTestId("anthropic-api-key-input").fill("sk-ant-valid-key");
|
||||
await page.getByTestId("test-claude-connection").click();
|
||||
|
||||
// Wait for result to appear
|
||||
const result = page.getByTestId("test-connection-result");
|
||||
await expect(result).toBeVisible({ timeout: 10000 });
|
||||
|
||||
// Verify success message is shown
|
||||
const message = page.getByTestId("test-connection-message");
|
||||
await expect(message).toContainText(/Connection successful/i);
|
||||
});
|
||||
|
||||
test("displays error message when API key is invalid", async ({ page }) => {
|
||||
// Mock authentication error
|
||||
await page.route("**/api/claude/test", async (route) => {
|
||||
await route.fulfill({
|
||||
status: 401,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({
|
||||
success: false,
|
||||
error: "Invalid API key. Please check your Anthropic API key.",
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
await page.goto("/");
|
||||
await page.getByTestId("settings-button").click();
|
||||
|
||||
await page.getByTestId("anthropic-api-key-input").fill("invalid-key");
|
||||
await page.getByTestId("test-claude-connection").click();
|
||||
|
||||
// Wait for error result
|
||||
const result = page.getByTestId("test-connection-result");
|
||||
await expect(result).toBeVisible({ timeout: 10000 });
|
||||
|
||||
// Verify error message is shown
|
||||
const message = page.getByTestId("test-connection-message");
|
||||
await expect(message).toContainText(/Invalid API key|API key|error/i);
|
||||
});
|
||||
|
||||
test("displays error message on network failure", async ({ page }) => {
|
||||
// Mock network error
|
||||
await page.route("**/api/claude/test", async (route) => {
|
||||
await route.abort("connectionrefused");
|
||||
});
|
||||
|
||||
await page.goto("/");
|
||||
await page.getByTestId("settings-button").click();
|
||||
|
||||
await page.getByTestId("anthropic-api-key-input").fill("sk-ant-test-key");
|
||||
await page.getByTestId("test-claude-connection").click();
|
||||
|
||||
// Wait for error result
|
||||
const result = page.getByTestId("test-connection-result");
|
||||
await expect(result).toBeVisible({ timeout: 10000 });
|
||||
|
||||
// Verify network error message
|
||||
const message = page.getByTestId("test-connection-message");
|
||||
await expect(message).toContainText(/Network error|connection|failed/i);
|
||||
});
|
||||
|
||||
test("displays rate limit error message", async ({ page }) => {
|
||||
// Mock rate limit error
|
||||
await page.route("**/api/claude/test", async (route) => {
|
||||
await route.fulfill({
|
||||
status: 429,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({
|
||||
success: false,
|
||||
error: "Rate limit exceeded. Please try again later.",
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
await page.goto("/");
|
||||
await page.getByTestId("settings-button").click();
|
||||
|
||||
await page.getByTestId("anthropic-api-key-input").fill("sk-ant-rate-limited");
|
||||
await page.getByTestId("test-claude-connection").click();
|
||||
|
||||
// Wait for error result
|
||||
const result = page.getByTestId("test-connection-result");
|
||||
await expect(result).toBeVisible({ timeout: 10000 });
|
||||
|
||||
// Verify rate limit message
|
||||
const message = page.getByTestId("test-connection-message");
|
||||
await expect(message).toContainText(/Rate limit|try again/i);
|
||||
});
|
||||
|
||||
test("shows loading state while testing connection", async ({ page }) => {
|
||||
// Mock slow response
|
||||
await page.route("**/api/claude/test", async (route) => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({
|
||||
success: true,
|
||||
message: "Connection successful!",
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
await page.goto("/");
|
||||
await page.getByTestId("settings-button").click();
|
||||
|
||||
await page.getByTestId("anthropic-api-key-input").fill("sk-ant-test-key");
|
||||
await page.getByTestId("test-claude-connection").click();
|
||||
|
||||
// Should show "Testing..." text while loading
|
||||
await expect(page.getByText("Testing...")).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Full Integration Flow", () => {
|
||||
test("complete Claude SDK integration flow - configure, test, verify", async ({ page }) => {
|
||||
// Mock successful API response
|
||||
await page.route("**/api/claude/test", async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({
|
||||
success: true,
|
||||
message: 'Connection successful! Response: "Claude SDK connection successful!"',
|
||||
model: "claude-sonnet-4-20250514",
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
// Step 1: Navigate to app
|
||||
await page.goto("/");
|
||||
await expect(page).toHaveURL("/");
|
||||
|
||||
// Step 2: Go to settings and configure API key
|
||||
await page.getByTestId("settings-button").click();
|
||||
await expect(page.getByTestId("settings-view")).toBeVisible();
|
||||
|
||||
const apiKey = "sk-ant-api03-integration-test-key";
|
||||
await page.getByTestId("anthropic-api-key-input").fill(apiKey);
|
||||
await page.getByTestId("save-settings").click();
|
||||
await expect(page.getByText("Saved!")).toBeVisible();
|
||||
|
||||
// Step 3: Test the connection
|
||||
await page.getByTestId("test-claude-connection").click();
|
||||
|
||||
// Step 4: Verify response is received
|
||||
await expect(page.getByTestId("test-connection-result")).toBeVisible({ timeout: 10000 });
|
||||
await expect(page.getByTestId("test-connection-message")).toContainText(/Connection successful/i);
|
||||
|
||||
// Verify the UI shows success state (green styling indicates success)
|
||||
const resultContainer = page.getByTestId("test-connection-result");
|
||||
await expect(resultContainer).toBeVisible();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,76 +0,0 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
|
||||
test.describe("Application Foundation", () => {
|
||||
test("loads the application with sidebar and welcome view", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
|
||||
// Verify main container exists
|
||||
await expect(page.getByTestId("app-container")).toBeVisible();
|
||||
|
||||
// Verify sidebar is visible
|
||||
await expect(page.getByTestId("sidebar")).toBeVisible();
|
||||
|
||||
// Verify welcome view is shown by default
|
||||
await expect(page.getByTestId("welcome-view")).toBeVisible();
|
||||
});
|
||||
|
||||
test("displays Automaker title in sidebar", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
|
||||
// Verify the title is visible in the sidebar (be specific to avoid matching welcome heading)
|
||||
await expect(page.getByTestId("sidebar").getByRole("heading", { name: "Automaker" })).toBeVisible();
|
||||
});
|
||||
|
||||
test("shows New Project and Open Project buttons", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
|
||||
// Verify project action buttons in welcome view
|
||||
await expect(page.getByTestId("new-project-card")).toBeVisible();
|
||||
await expect(page.getByTestId("open-project-card")).toBeVisible();
|
||||
});
|
||||
|
||||
test("sidebar can be collapsed and expanded", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
|
||||
const sidebar = page.getByTestId("sidebar");
|
||||
const toggleButton = page.getByTestId("toggle-sidebar");
|
||||
|
||||
// Initially sidebar should be expanded (width 256px / w-64)
|
||||
await expect(sidebar).toHaveClass(/w-64/);
|
||||
|
||||
// Click to collapse
|
||||
await toggleButton.click();
|
||||
await expect(sidebar).toHaveClass(/w-16/);
|
||||
|
||||
// Click to expand again
|
||||
await toggleButton.click();
|
||||
await expect(sidebar).toHaveClass(/w-64/);
|
||||
});
|
||||
|
||||
test("shows Web Mode indicator when running in browser", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
|
||||
// When running in browser (not Electron), should show mock indicator
|
||||
await expect(page.getByText("Web Mode (Mock IPC)")).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Theme Toggle", () => {
|
||||
test("toggles between dark and light mode", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
|
||||
const themeButton = page.getByTestId("toggle-theme");
|
||||
const html = page.locator("html");
|
||||
|
||||
// Initially should be in dark mode
|
||||
await expect(html).toHaveClass(/dark/);
|
||||
|
||||
// Click to switch to light mode
|
||||
await themeButton.click();
|
||||
await expect(html).not.toHaveClass(/dark/);
|
||||
|
||||
// Click to switch back to dark mode
|
||||
await themeButton.click();
|
||||
await expect(html).toHaveClass(/dark/);
|
||||
});
|
||||
});
|
||||
@@ -1,299 +0,0 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
|
||||
test.describe("Gemini SDK Integration", () => {
|
||||
test.describe("Step 1: Configure Gemini API Key", () => {
|
||||
test("can navigate to settings and see Gemini API key input", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto("/");
|
||||
|
||||
// Navigate to settings
|
||||
await page.getByTestId("settings-button").click();
|
||||
|
||||
// Verify settings view is displayed
|
||||
await expect(page.getByTestId("settings-view")).toBeVisible();
|
||||
|
||||
// Verify Google/Gemini API key input exists
|
||||
await expect(page.getByTestId("google-api-key-input")).toBeVisible();
|
||||
await expect(
|
||||
page.getByText("Google API Key (Gemini)", { exact: true })
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test("can enter and save Gemini API key", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await page.getByTestId("settings-button").click();
|
||||
|
||||
// Enter a test API key
|
||||
const testApiKey = "AIzaSyTestKey123456";
|
||||
await page.getByTestId("google-api-key-input").fill(testApiKey);
|
||||
|
||||
// Save the settings
|
||||
await page.getByTestId("save-settings").click();
|
||||
|
||||
// Verify saved confirmation
|
||||
await expect(page.getByText("Saved!")).toBeVisible();
|
||||
|
||||
// Reload and verify persistence
|
||||
await page.reload();
|
||||
await page.getByTestId("settings-button").click();
|
||||
|
||||
// Toggle visibility to check the value
|
||||
await page.getByTestId("toggle-google-visibility").click();
|
||||
await expect(page.getByTestId("google-api-key-input")).toHaveValue(
|
||||
testApiKey
|
||||
);
|
||||
});
|
||||
|
||||
test("Gemini API key input is password type by default for security", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto("/");
|
||||
await page.getByTestId("settings-button").click();
|
||||
|
||||
// Verify password type for security
|
||||
await expect(page.getByTestId("google-api-key-input")).toHaveAttribute(
|
||||
"type",
|
||||
"password"
|
||||
);
|
||||
});
|
||||
|
||||
test("can toggle Gemini API key visibility", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await page.getByTestId("settings-button").click();
|
||||
|
||||
// Initially password type
|
||||
await expect(page.getByTestId("google-api-key-input")).toHaveAttribute(
|
||||
"type",
|
||||
"password"
|
||||
);
|
||||
|
||||
// Toggle to show
|
||||
await page.getByTestId("toggle-google-visibility").click();
|
||||
await expect(page.getByTestId("google-api-key-input")).toHaveAttribute(
|
||||
"type",
|
||||
"text"
|
||||
);
|
||||
|
||||
// Toggle back to hide
|
||||
await page.getByTestId("toggle-google-visibility").click();
|
||||
await expect(page.getByTestId("google-api-key-input")).toHaveAttribute(
|
||||
"type",
|
||||
"password"
|
||||
);
|
||||
});
|
||||
|
||||
test("shows checkmark icon when API key is configured", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto("/");
|
||||
await page.getByTestId("settings-button").click();
|
||||
|
||||
// Enter API key
|
||||
await page.getByTestId("google-api-key-input").fill("AIzaSyTest123");
|
||||
await page.getByTestId("save-settings").click();
|
||||
|
||||
// Reload to trigger the checkmark display
|
||||
await page.reload();
|
||||
await page.getByTestId("settings-button").click();
|
||||
|
||||
// The checkmark icon should be visible next to the label
|
||||
// Find the label container and verify checkmark is present
|
||||
const labelContainer = page.locator(".flex.items-center.gap-2").filter({
|
||||
hasText: "Google API Key (Gemini)",
|
||||
});
|
||||
await expect(
|
||||
labelContainer.locator('svg[class*="text-green-500"]')
|
||||
).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Step 2: Send image/design prompt", () => {
|
||||
test("test connection button exists for Gemini", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await page.getByTestId("settings-button").click();
|
||||
|
||||
// Verify test connection button exists
|
||||
await expect(page.getByTestId("test-gemini-connection")).toBeVisible();
|
||||
});
|
||||
|
||||
test("test connection button is disabled without API key", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto("/");
|
||||
await page.getByTestId("settings-button").click();
|
||||
|
||||
// Clear any existing API key
|
||||
await page.getByTestId("google-api-key-input").clear();
|
||||
|
||||
// Verify button is disabled
|
||||
await expect(page.getByTestId("test-gemini-connection")).toBeDisabled();
|
||||
});
|
||||
|
||||
test("test connection button is enabled with API key", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await page.getByTestId("settings-button").click();
|
||||
|
||||
// Enter API key
|
||||
await page.getByTestId("google-api-key-input").fill("AIzaSyTestKey123");
|
||||
|
||||
// Verify button is enabled
|
||||
await expect(page.getByTestId("test-gemini-connection")).toBeEnabled();
|
||||
});
|
||||
|
||||
test("clicking test connection shows loading state", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await page.getByTestId("settings-button").click();
|
||||
|
||||
// Enter API key
|
||||
await page.getByTestId("google-api-key-input").fill("AIzaSyInvalidKey");
|
||||
|
||||
// Click test connection
|
||||
await page.getByTestId("test-gemini-connection").click();
|
||||
|
||||
// Should show loading state (Testing...)
|
||||
await expect(page.getByText("Testing...")).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Step 3: Verify response received", () => {
|
||||
test("shows error message for invalid API key", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await page.getByTestId("settings-button").click();
|
||||
|
||||
// Enter an invalid API key
|
||||
await page.getByTestId("google-api-key-input").fill("invalid-key-123");
|
||||
|
||||
// Click test connection
|
||||
await page.getByTestId("test-gemini-connection").click();
|
||||
|
||||
// Wait for result (should show error)
|
||||
await expect(
|
||||
page.getByTestId("gemini-test-connection-result")
|
||||
).toBeVisible({ timeout: 15000 });
|
||||
|
||||
// The result should indicate an error (red styling or error message)
|
||||
const resultElement = page.getByTestId("gemini-test-connection-result");
|
||||
await expect(resultElement).toBeVisible();
|
||||
});
|
||||
|
||||
test("Gemini API endpoint exists and responds", async ({ request }) => {
|
||||
// Test the API endpoint directly
|
||||
const response = await request.post("/api/gemini/test", {
|
||||
data: {
|
||||
apiKey: "test-invalid-key",
|
||||
},
|
||||
});
|
||||
|
||||
// Should return a response (even if error)
|
||||
expect(response.status()).toBeLessThanOrEqual(500);
|
||||
|
||||
const data = await response.json();
|
||||
// Should have success or error property
|
||||
expect(data).toHaveProperty("success");
|
||||
expect(typeof data.success).toBe("boolean");
|
||||
});
|
||||
|
||||
test("Gemini API endpoint handles missing API key", async ({ request }) => {
|
||||
// Test the API endpoint without API key
|
||||
const response = await request.post("/api/gemini/test", {
|
||||
data: {},
|
||||
});
|
||||
|
||||
// Should return 400 for missing API key
|
||||
expect(response.status()).toBe(400);
|
||||
|
||||
const data = await response.json();
|
||||
expect(data.success).toBe(false);
|
||||
expect(data.error).toContain("No API key");
|
||||
});
|
||||
|
||||
test("Gemini API endpoint handles image data structure", async ({
|
||||
request,
|
||||
}) => {
|
||||
// Test that the API can accept image data format
|
||||
const response = await request.post("/api/gemini/test", {
|
||||
data: {
|
||||
apiKey: "test-key",
|
||||
imageData: "iVBORw0KGgoAAAANSUhEUg==", // Minimal base64
|
||||
mimeType: "image/png",
|
||||
prompt: "Describe this image",
|
||||
},
|
||||
});
|
||||
|
||||
// Should process the request (even if API key is invalid)
|
||||
expect(response.status()).toBeLessThanOrEqual(500);
|
||||
|
||||
const data = await response.json();
|
||||
expect(data).toHaveProperty("success");
|
||||
});
|
||||
|
||||
test("result message displays in UI after test", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await page.getByTestId("settings-button").click();
|
||||
|
||||
// Enter API key
|
||||
await page.getByTestId("google-api-key-input").fill("test-api-key-123");
|
||||
|
||||
// Click test connection
|
||||
await page.getByTestId("test-gemini-connection").click();
|
||||
|
||||
// Wait for result message to appear
|
||||
await expect(
|
||||
page.getByTestId("gemini-test-connection-message")
|
||||
).toBeVisible({ timeout: 15000 });
|
||||
});
|
||||
|
||||
test("shows link to Google AI Studio for API key", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await page.getByTestId("settings-button").click();
|
||||
|
||||
// Should show link to get API key
|
||||
const link = page.locator('a[href*="makersuite.google.com"]');
|
||||
await expect(link).toBeVisible();
|
||||
await expect(link).toHaveAttribute("target", "_blank");
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Gemini API Route Tests", () => {
|
||||
test("API route supports text-only prompts", async ({ request }) => {
|
||||
const response = await request.post("/api/gemini/test", {
|
||||
data: {
|
||||
apiKey: "test-key",
|
||||
prompt: "Hello, this is a test prompt",
|
||||
},
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
// Should process without crashing (actual API key validation happens remotely)
|
||||
expect(data).toHaveProperty("success");
|
||||
});
|
||||
|
||||
test("API route supports custom prompts with images", async ({
|
||||
request,
|
||||
}) => {
|
||||
const response = await request.post("/api/gemini/test", {
|
||||
data: {
|
||||
apiKey: "test-key",
|
||||
imageData: "base64encodeddata",
|
||||
mimeType: "image/jpeg",
|
||||
prompt: "What design patterns do you see in this UI mockup?",
|
||||
},
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
expect(data).toHaveProperty("success");
|
||||
});
|
||||
|
||||
test("API route returns proper error structure", async ({ request }) => {
|
||||
const response = await request.post("/api/gemini/test", {
|
||||
data: {},
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
expect(data).toHaveProperty("success");
|
||||
expect(data).toHaveProperty("error");
|
||||
expect(typeof data.error).toBe("string");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,430 +0,0 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
|
||||
test.describe("Gemini SDK Integration - Autonomous Agent", () => {
|
||||
test.describe("Step 1: Configure Gemini API Key", () => {
|
||||
test("can navigate to settings and configure Google API key", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
|
||||
// Navigate to settings
|
||||
await page.getByTestId("settings-button").click();
|
||||
await expect(page.getByTestId("settings-view")).toBeVisible();
|
||||
|
||||
// Verify Google API key input is available
|
||||
const apiKeyInput = page.getByTestId("google-api-key-input");
|
||||
await expect(apiKeyInput).toBeVisible();
|
||||
await expect(apiKeyInput).toBeEditable();
|
||||
|
||||
// Enter a test API key
|
||||
await apiKeyInput.fill("AIzaSyTest-integration-test-key-123");
|
||||
|
||||
// Save settings
|
||||
await page.getByTestId("save-settings").click();
|
||||
await expect(page.getByText("Saved!")).toBeVisible();
|
||||
});
|
||||
|
||||
test("Google API key input has proper security features", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await page.getByTestId("settings-button").click();
|
||||
|
||||
// Verify password masking by default
|
||||
await expect(page.getByTestId("google-api-key-input")).toHaveAttribute("type", "password");
|
||||
|
||||
// Can toggle visibility
|
||||
await page.getByTestId("toggle-google-visibility").click();
|
||||
await expect(page.getByTestId("google-api-key-input")).toHaveAttribute("type", "text");
|
||||
|
||||
// Can toggle back to hidden
|
||||
await page.getByTestId("toggle-google-visibility").click();
|
||||
await expect(page.getByTestId("google-api-key-input")).toHaveAttribute("type", "password");
|
||||
});
|
||||
|
||||
test("Google API key persists across page reloads", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await page.getByTestId("settings-button").click();
|
||||
|
||||
// Enter and save API key
|
||||
const testKey = "AIzaSyPersistence-test-key";
|
||||
await page.getByTestId("google-api-key-input").fill(testKey);
|
||||
await page.getByTestId("save-settings").click();
|
||||
await expect(page.getByText("Saved!")).toBeVisible();
|
||||
|
||||
// Reload and verify persistence
|
||||
await page.reload();
|
||||
await page.getByTestId("settings-button").click();
|
||||
|
||||
// Make key visible and verify
|
||||
await page.getByTestId("toggle-google-visibility").click();
|
||||
await expect(page.getByTestId("google-api-key-input")).toHaveValue(testKey);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Step 2: Send image/design prompt", () => {
|
||||
test("test connection button is visible in settings for Gemini", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await page.getByTestId("settings-button").click();
|
||||
|
||||
// Test connection button should be visible
|
||||
const testButton = page.getByTestId("test-gemini-connection");
|
||||
await expect(testButton).toBeVisible();
|
||||
await expect(testButton).toContainText("Test");
|
||||
});
|
||||
|
||||
test("test connection button is disabled without API key", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await page.getByTestId("settings-button").click();
|
||||
|
||||
// Clear any existing API key
|
||||
await page.getByTestId("google-api-key-input").fill("");
|
||||
|
||||
// Test button should be disabled
|
||||
await expect(page.getByTestId("test-gemini-connection")).toBeDisabled();
|
||||
});
|
||||
|
||||
test("test connection button is enabled with API key", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await page.getByTestId("settings-button").click();
|
||||
|
||||
// Enter API key
|
||||
await page.getByTestId("google-api-key-input").fill("AIzaSyTest-key");
|
||||
|
||||
// Test button should be enabled
|
||||
await expect(page.getByTestId("test-gemini-connection")).toBeEnabled();
|
||||
});
|
||||
|
||||
test("clicking test sends request to Gemini API endpoint", async ({ page }) => {
|
||||
// Setup API route mock
|
||||
await page.route("**/api/gemini/test", async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({
|
||||
success: true,
|
||||
message: "Connection successful! Gemini responded.",
|
||||
model: "gemini-1.5-flash",
|
||||
hasImage: false,
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
await page.goto("/");
|
||||
await page.getByTestId("settings-button").click();
|
||||
|
||||
// Enter API key
|
||||
await page.getByTestId("google-api-key-input").fill("AIzaSyTest-key");
|
||||
|
||||
// Click test button
|
||||
await page.getByTestId("test-gemini-connection").click();
|
||||
|
||||
// Should show loading state briefly then success
|
||||
await expect(page.getByTestId("gemini-test-connection-result")).toBeVisible({ timeout: 10000 });
|
||||
});
|
||||
|
||||
test("Gemini API endpoint supports image/design prompts", async ({ page }) => {
|
||||
// Mock API endpoint that handles image data
|
||||
await page.route("**/api/gemini/test", async (route) => {
|
||||
const request = route.request();
|
||||
const postData = request.postDataJSON();
|
||||
|
||||
// Verify the API can receive image data
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({
|
||||
success: true,
|
||||
message: 'Connection successful! Response: "This is a test design description."',
|
||||
model: "gemini-1.5-flash",
|
||||
hasImage: !!postData?.imageData,
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
await page.goto("/");
|
||||
await page.getByTestId("settings-button").click();
|
||||
|
||||
await page.getByTestId("google-api-key-input").fill("AIzaSyTest-image-key");
|
||||
await page.getByTestId("test-gemini-connection").click();
|
||||
|
||||
await expect(page.getByTestId("gemini-test-connection-result")).toBeVisible({ timeout: 10000 });
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Step 3: Verify response received", () => {
|
||||
test("displays success message when connection succeeds", async ({ page }) => {
|
||||
// Mock successful response
|
||||
await page.route("**/api/gemini/test", async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({
|
||||
success: true,
|
||||
message: 'Connection successful! Response: "Gemini SDK connection successful!"',
|
||||
model: "gemini-1.5-flash",
|
||||
hasImage: false,
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
await page.goto("/");
|
||||
await page.getByTestId("settings-button").click();
|
||||
|
||||
await page.getByTestId("google-api-key-input").fill("AIzaSyValid-key");
|
||||
await page.getByTestId("test-gemini-connection").click();
|
||||
|
||||
// Wait for result to appear
|
||||
const result = page.getByTestId("gemini-test-connection-result");
|
||||
await expect(result).toBeVisible({ timeout: 10000 });
|
||||
|
||||
// Verify success message is shown
|
||||
const message = page.getByTestId("gemini-test-connection-message");
|
||||
await expect(message).toContainText(/Connection successful/i);
|
||||
});
|
||||
|
||||
test("displays error message when API key is invalid", async ({ page }) => {
|
||||
// Mock authentication error
|
||||
await page.route("**/api/gemini/test", async (route) => {
|
||||
await route.fulfill({
|
||||
status: 401,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({
|
||||
success: false,
|
||||
error: "Invalid API key. Please check your Google API key.",
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
await page.goto("/");
|
||||
await page.getByTestId("settings-button").click();
|
||||
|
||||
await page.getByTestId("google-api-key-input").fill("invalid-key");
|
||||
await page.getByTestId("test-gemini-connection").click();
|
||||
|
||||
// Wait for error result
|
||||
const result = page.getByTestId("gemini-test-connection-result");
|
||||
await expect(result).toBeVisible({ timeout: 10000 });
|
||||
|
||||
// Verify error message is shown
|
||||
const message = page.getByTestId("gemini-test-connection-message");
|
||||
await expect(message).toContainText(/Invalid API key|API key|error/i);
|
||||
});
|
||||
|
||||
test("displays error message on network failure", async ({ page }) => {
|
||||
// Mock network error
|
||||
await page.route("**/api/gemini/test", async (route) => {
|
||||
await route.abort("connectionrefused");
|
||||
});
|
||||
|
||||
await page.goto("/");
|
||||
await page.getByTestId("settings-button").click();
|
||||
|
||||
await page.getByTestId("google-api-key-input").fill("AIzaSyTest-key");
|
||||
await page.getByTestId("test-gemini-connection").click();
|
||||
|
||||
// Wait for error result
|
||||
const result = page.getByTestId("gemini-test-connection-result");
|
||||
await expect(result).toBeVisible({ timeout: 10000 });
|
||||
|
||||
// Verify network error message
|
||||
const message = page.getByTestId("gemini-test-connection-message");
|
||||
await expect(message).toContainText(/Network error|connection|failed/i);
|
||||
});
|
||||
|
||||
test("displays rate limit error message", async ({ page }) => {
|
||||
// Mock rate limit error
|
||||
await page.route("**/api/gemini/test", async (route) => {
|
||||
await route.fulfill({
|
||||
status: 429,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({
|
||||
success: false,
|
||||
error: "Rate limit exceeded. Please try again later.",
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
await page.goto("/");
|
||||
await page.getByTestId("settings-button").click();
|
||||
|
||||
await page.getByTestId("google-api-key-input").fill("AIzaSyRate-limited");
|
||||
await page.getByTestId("test-gemini-connection").click();
|
||||
|
||||
// Wait for error result
|
||||
const result = page.getByTestId("gemini-test-connection-result");
|
||||
await expect(result).toBeVisible({ timeout: 10000 });
|
||||
|
||||
// Verify rate limit message
|
||||
const message = page.getByTestId("gemini-test-connection-message");
|
||||
await expect(message).toContainText(/Rate limit|try again/i);
|
||||
});
|
||||
|
||||
test("shows loading state while testing connection", async ({ page }) => {
|
||||
// Mock slow response
|
||||
await page.route("**/api/gemini/test", async (route) => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({
|
||||
success: true,
|
||||
message: "Connection successful!",
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
await page.goto("/");
|
||||
await page.getByTestId("settings-button").click();
|
||||
|
||||
await page.getByTestId("google-api-key-input").fill("AIzaSyTest-key");
|
||||
await page.getByTestId("test-gemini-connection").click();
|
||||
|
||||
// Should show "Testing..." text while loading
|
||||
await expect(page.getByText("Testing...")).toBeVisible();
|
||||
});
|
||||
|
||||
test("displays response with image analysis capability", async ({ page }) => {
|
||||
// Mock response that indicates image was processed
|
||||
await page.route("**/api/gemini/test", async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({
|
||||
success: true,
|
||||
message: 'Connection successful! Response: "I can see a modern UI design with buttons and forms."',
|
||||
model: "gemini-1.5-flash",
|
||||
hasImage: true,
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
await page.goto("/");
|
||||
await page.getByTestId("settings-button").click();
|
||||
|
||||
await page.getByTestId("google-api-key-input").fill("AIzaSyImage-test-key");
|
||||
await page.getByTestId("test-gemini-connection").click();
|
||||
|
||||
// Wait for result to appear
|
||||
const result = page.getByTestId("gemini-test-connection-result");
|
||||
await expect(result).toBeVisible({ timeout: 10000 });
|
||||
|
||||
// Verify success message is shown
|
||||
const message = page.getByTestId("gemini-test-connection-message");
|
||||
await expect(message).toContainText(/Connection successful/i);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Full Integration Flow", () => {
|
||||
test("complete Gemini SDK integration flow - configure, send image/design prompt, verify", async ({ page }) => {
|
||||
// Mock successful API response
|
||||
await page.route("**/api/gemini/test", async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({
|
||||
success: true,
|
||||
message: 'Connection successful! Response: "Gemini SDK connection successful!"',
|
||||
model: "gemini-1.5-flash",
|
||||
hasImage: false,
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
// Step 1: Navigate to app
|
||||
await page.goto("/");
|
||||
await expect(page).toHaveURL("/");
|
||||
|
||||
// Step 2: Go to settings and configure API key
|
||||
await page.getByTestId("settings-button").click();
|
||||
await expect(page.getByTestId("settings-view")).toBeVisible();
|
||||
|
||||
const apiKey = "AIzaSyIntegration-test-key";
|
||||
await page.getByTestId("google-api-key-input").fill(apiKey);
|
||||
await page.getByTestId("save-settings").click();
|
||||
await expect(page.getByText("Saved!")).toBeVisible();
|
||||
|
||||
// Step 3: Test the connection (sends prompt to Gemini)
|
||||
await page.getByTestId("test-gemini-connection").click();
|
||||
|
||||
// Step 4: Verify response is received
|
||||
await expect(page.getByTestId("gemini-test-connection-result")).toBeVisible({ timeout: 10000 });
|
||||
await expect(page.getByTestId("gemini-test-connection-message")).toContainText(/Connection successful/i);
|
||||
|
||||
// Verify the UI shows success state (green styling indicates success)
|
||||
const resultContainer = page.getByTestId("gemini-test-connection-result");
|
||||
await expect(resultContainer).toBeVisible();
|
||||
});
|
||||
|
||||
test("Gemini API supports both text and image/design prompts", async ({ page }) => {
|
||||
// First test: text only prompt
|
||||
await page.route("**/api/gemini/test", async (route) => {
|
||||
const request = route.request();
|
||||
const postData = request.postDataJSON();
|
||||
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({
|
||||
success: true,
|
||||
message: postData?.imageData
|
||||
? 'Connection successful! Response: "Design analyzed successfully."'
|
||||
: 'Connection successful! Response: "Gemini SDK connection successful!"',
|
||||
model: "gemini-1.5-flash",
|
||||
hasImage: !!postData?.imageData,
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
await page.goto("/");
|
||||
await page.getByTestId("settings-button").click();
|
||||
|
||||
await page.getByTestId("google-api-key-input").fill("AIzaSyMultimodal-key");
|
||||
await page.getByTestId("test-gemini-connection").click();
|
||||
|
||||
await expect(page.getByTestId("gemini-test-connection-result")).toBeVisible({ timeout: 10000 });
|
||||
await expect(page.getByTestId("gemini-test-connection-message")).toContainText(/Connection successful/i);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Gemini API Endpoint Verification", () => {
|
||||
test("API endpoint exists and responds correctly", async ({ page }) => {
|
||||
// This test verifies the API route is properly set up
|
||||
await page.route("**/api/gemini/test", async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({
|
||||
success: true,
|
||||
message: "Gemini API endpoint is working",
|
||||
model: "gemini-1.5-flash",
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
await page.goto("/");
|
||||
await page.getByTestId("settings-button").click();
|
||||
await page.getByTestId("google-api-key-input").fill("test-key");
|
||||
await page.getByTestId("test-gemini-connection").click();
|
||||
|
||||
await expect(page.getByTestId("gemini-test-connection-result")).toBeVisible({ timeout: 10000 });
|
||||
});
|
||||
|
||||
test("API endpoint handles missing API key gracefully", async ({ page }) => {
|
||||
// Verify proper error handling when no API key is provided
|
||||
await page.route("**/api/gemini/test", async (route) => {
|
||||
await route.fulfill({
|
||||
status: 400,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({
|
||||
success: false,
|
||||
error: "No API key provided or configured in environment",
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
await page.goto("/");
|
||||
await page.getByTestId("settings-button").click();
|
||||
|
||||
// Button should be disabled without API key, so the error state
|
||||
// would only occur if someone bypasses the UI
|
||||
await expect(page.getByTestId("test-gemini-connection")).toBeDisabled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,256 +0,0 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
|
||||
test.describe("Gemini SDK Integration", () => {
|
||||
test.describe("Step 1: Configure Gemini API Key", () => {
|
||||
test("can navigate to settings and see Gemini API key input", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
|
||||
// Navigate to settings
|
||||
await page.getByTestId("settings-button").click();
|
||||
|
||||
// Should see settings view
|
||||
await expect(page.getByTestId("settings-view")).toBeVisible();
|
||||
|
||||
// Should see Google/Gemini API key input
|
||||
await expect(page.getByTestId("google-api-key-input")).toBeVisible();
|
||||
await expect(page.getByText("Google API Key (Gemini)")).toBeVisible();
|
||||
});
|
||||
|
||||
test("can enter and save Gemini API key", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await page.getByTestId("settings-button").click();
|
||||
|
||||
// Enter Gemini API key
|
||||
const testKey = "AIzaSyTestGeminiKey123";
|
||||
await page.getByTestId("google-api-key-input").fill(testKey);
|
||||
|
||||
// Save settings
|
||||
await page.getByTestId("save-settings").click();
|
||||
|
||||
// Should show saved confirmation
|
||||
await expect(page.getByText("Saved!")).toBeVisible();
|
||||
|
||||
// Reload and verify persistence
|
||||
await page.reload();
|
||||
await page.getByTestId("settings-button").click();
|
||||
|
||||
// Toggle visibility to verify saved key
|
||||
await page.getByTestId("toggle-google-visibility").click();
|
||||
await expect(page.getByTestId("google-api-key-input")).toHaveValue(testKey);
|
||||
});
|
||||
|
||||
test("Gemini API key input is password type by default for security", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await page.getByTestId("settings-button").click();
|
||||
|
||||
// Check input type is password (secure)
|
||||
await expect(page.getByTestId("google-api-key-input")).toHaveAttribute("type", "password");
|
||||
});
|
||||
|
||||
test("can toggle Gemini API key visibility", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await page.getByTestId("settings-button").click();
|
||||
|
||||
// Initially password type
|
||||
await expect(page.getByTestId("google-api-key-input")).toHaveAttribute("type", "password");
|
||||
|
||||
// Toggle to show
|
||||
await page.getByTestId("toggle-google-visibility").click();
|
||||
await expect(page.getByTestId("google-api-key-input")).toHaveAttribute("type", "text");
|
||||
|
||||
// Toggle back to hide
|
||||
await page.getByTestId("toggle-google-visibility").click();
|
||||
await expect(page.getByTestId("google-api-key-input")).toHaveAttribute("type", "password");
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Step 2: Send image/design prompt", () => {
|
||||
test("test Gemini connection button exists", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await page.getByTestId("settings-button").click();
|
||||
|
||||
// Should see the test Gemini connection button
|
||||
await expect(page.getByTestId("test-gemini-connection")).toBeVisible();
|
||||
});
|
||||
|
||||
test("test Gemini connection button is disabled without API key", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await page.getByTestId("settings-button").click();
|
||||
|
||||
// Clear any existing key
|
||||
await page.getByTestId("google-api-key-input").clear();
|
||||
|
||||
// Button should be disabled
|
||||
await expect(page.getByTestId("test-gemini-connection")).toBeDisabled();
|
||||
});
|
||||
|
||||
test("test Gemini connection button is enabled with API key", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await page.getByTestId("settings-button").click();
|
||||
|
||||
// Enter an API key
|
||||
await page.getByTestId("google-api-key-input").fill("AIzaSyTestKey123");
|
||||
|
||||
// Button should be enabled
|
||||
await expect(page.getByTestId("test-gemini-connection")).toBeEnabled();
|
||||
});
|
||||
|
||||
test("clicking test button shows loading state", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await page.getByTestId("settings-button").click();
|
||||
|
||||
// Enter API key
|
||||
await page.getByTestId("google-api-key-input").fill("AIzaSyTestKey123");
|
||||
|
||||
// Mock the API response with a delay to catch loading state
|
||||
await page.route("/api/gemini/test", async (route) => {
|
||||
// Delay to show loading state
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({
|
||||
success: true,
|
||||
message: "Connection successful!",
|
||||
model: "gemini-1.5-flash",
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
// Click test button
|
||||
await page.getByTestId("test-gemini-connection").click();
|
||||
|
||||
// Should show loading state
|
||||
await expect(page.getByText("Testing...")).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Step 3: Verify response received", () => {
|
||||
test("shows success message on successful Gemini API test", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await page.getByTestId("settings-button").click();
|
||||
|
||||
// Enter API key
|
||||
await page.getByTestId("google-api-key-input").fill("AIzaSyTestKey123");
|
||||
|
||||
// Mock successful response
|
||||
await page.route("/api/gemini/test", async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({
|
||||
success: true,
|
||||
message: 'Connection successful! Response: "Gemini SDK connection successful!"',
|
||||
model: "gemini-1.5-flash",
|
||||
hasImage: false,
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
// Click test button
|
||||
await page.getByTestId("test-gemini-connection").click();
|
||||
|
||||
// Should show success result
|
||||
await expect(page.getByTestId("gemini-test-connection-result")).toBeVisible();
|
||||
await expect(page.getByTestId("gemini-test-connection-message")).toContainText("Connection successful");
|
||||
});
|
||||
|
||||
test("shows error message on failed Gemini API test", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await page.getByTestId("settings-button").click();
|
||||
|
||||
// Enter API key
|
||||
await page.getByTestId("google-api-key-input").fill("invalid-key");
|
||||
|
||||
// Mock error response
|
||||
await page.route("/api/gemini/test", async (route) => {
|
||||
await route.fulfill({
|
||||
status: 401,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({
|
||||
success: false,
|
||||
error: "Invalid API key. Please check your Google API key.",
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
// Click test button
|
||||
await page.getByTestId("test-gemini-connection").click();
|
||||
|
||||
// Should show error result
|
||||
await expect(page.getByTestId("gemini-test-connection-result")).toBeVisible();
|
||||
await expect(page.getByTestId("gemini-test-connection-message")).toContainText("Invalid API key");
|
||||
});
|
||||
|
||||
test("shows network error message on connection failure", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await page.getByTestId("settings-button").click();
|
||||
|
||||
// Enter API key
|
||||
await page.getByTestId("google-api-key-input").fill("AIzaSyTestKey123");
|
||||
|
||||
// Mock network error
|
||||
await page.route("/api/gemini/test", async (route) => {
|
||||
await route.abort("connectionfailed");
|
||||
});
|
||||
|
||||
// Click test button
|
||||
await page.getByTestId("test-gemini-connection").click();
|
||||
|
||||
// Should show error result
|
||||
await expect(page.getByTestId("gemini-test-connection-result")).toBeVisible();
|
||||
await expect(page.getByTestId("gemini-test-connection-message")).toContainText("Network error");
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Gemini API Route - Image/Design Prompt Support", () => {
|
||||
test("API route accepts and processes image data for design prompts", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
|
||||
// Directly test the API endpoint with image data
|
||||
const response = await page.request.post("/api/gemini/test", {
|
||||
data: {
|
||||
apiKey: "test-key-for-mock",
|
||||
imageData: "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==", // 1x1 transparent PNG
|
||||
mimeType: "image/png",
|
||||
prompt: "Describe this image",
|
||||
},
|
||||
});
|
||||
|
||||
// We expect some response (even if error due to invalid key)
|
||||
const data = await response.json();
|
||||
// The endpoint should process the request (not crash)
|
||||
expect(data).toHaveProperty("success");
|
||||
});
|
||||
|
||||
test("API route handles text-only prompts", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
|
||||
// Test the API endpoint with text-only prompt
|
||||
const response = await page.request.post("/api/gemini/test", {
|
||||
data: {
|
||||
apiKey: "test-key",
|
||||
prompt: "Hello Gemini",
|
||||
},
|
||||
});
|
||||
|
||||
// Should return a valid response structure
|
||||
const data = await response.json();
|
||||
expect(data).toHaveProperty("success");
|
||||
});
|
||||
|
||||
test("API route returns error when no API key provided", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
|
||||
// Test the API endpoint without API key
|
||||
const response = await page.request.post("/api/gemini/test", {
|
||||
data: {},
|
||||
});
|
||||
|
||||
// Should return error about missing API key
|
||||
const data = await response.json();
|
||||
expect(data.success).toBe(false);
|
||||
expect(data.error).toContain("No API key");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,258 +0,0 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
|
||||
test.describe("Interactive New Project Interview", () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto("/");
|
||||
});
|
||||
|
||||
test("Step 1: Click 'New Project' -> 'Interactive Mode'", async ({ page }) => {
|
||||
// Click the Create Project button to open the dropdown
|
||||
await page.getByTestId("create-new-project").click();
|
||||
|
||||
// Verify the dropdown menu is visible
|
||||
await expect(page.getByTestId("interactive-mode-option")).toBeVisible();
|
||||
await expect(page.getByTestId("quick-setup-option")).toBeVisible();
|
||||
|
||||
// Click on Interactive Mode
|
||||
await page.getByTestId("interactive-mode-option").click();
|
||||
|
||||
// Verify we navigate to the interview view
|
||||
await expect(page.getByTestId("interview-view")).toBeVisible();
|
||||
});
|
||||
|
||||
test("Step 2: Chat interface appears asking 'What do you want to build?'", async ({ page }) => {
|
||||
// Navigate to interview view
|
||||
await page.getByTestId("create-new-project").click();
|
||||
await page.getByTestId("interactive-mode-option").click();
|
||||
|
||||
// Verify interview view is displayed
|
||||
await expect(page.getByTestId("interview-view")).toBeVisible();
|
||||
|
||||
// Verify the chat interface is present
|
||||
await expect(page.getByTestId("interview-messages")).toBeVisible();
|
||||
await expect(page.getByTestId("interview-input")).toBeVisible();
|
||||
|
||||
// Verify the first question is asking what to build
|
||||
await expect(page.getByText("What do you want to build?")).toBeVisible();
|
||||
});
|
||||
|
||||
test("Step 3: User replies 'A todo app'", async ({ page }) => {
|
||||
// Navigate to interview view
|
||||
await page.getByTestId("create-new-project").click();
|
||||
await page.getByTestId("interactive-mode-option").click();
|
||||
|
||||
// Wait for interview view
|
||||
await expect(page.getByTestId("interview-view")).toBeVisible();
|
||||
|
||||
// Type a response
|
||||
await page.getByTestId("interview-input").fill("A todo app");
|
||||
await page.getByTestId("interview-send").click();
|
||||
|
||||
// Verify user message appears in chat
|
||||
await expect(page.getByText("A todo app")).toBeVisible();
|
||||
});
|
||||
|
||||
test("Step 4: Agent asks clarifying questions (e.g. 'What tech stack?')", async ({ page }) => {
|
||||
// Navigate to interview view
|
||||
await page.getByTestId("create-new-project").click();
|
||||
await page.getByTestId("interactive-mode-option").click();
|
||||
|
||||
// Wait for interview view
|
||||
await expect(page.getByTestId("interview-view")).toBeVisible();
|
||||
|
||||
// Answer first question
|
||||
await page.getByTestId("interview-input").fill("A todo app with tasks and categories");
|
||||
await page.getByTestId("interview-send").click();
|
||||
|
||||
// Wait for the next question about tech stack
|
||||
await expect(page.getByText("What tech stack would you like to use?")).toBeVisible({ timeout: 3000 });
|
||||
});
|
||||
|
||||
test("Step 5: Agent generates draft app_spec.txt based on conversation", async ({ page }) => {
|
||||
// Navigate to interview view
|
||||
await page.getByTestId("create-new-project").click();
|
||||
await page.getByTestId("interactive-mode-option").click();
|
||||
|
||||
// Wait for interview view
|
||||
await expect(page.getByTestId("interview-view")).toBeVisible();
|
||||
|
||||
// Answer all questions
|
||||
// Question 1: What do you want to build?
|
||||
await page.getByTestId("interview-input").fill("A todo app");
|
||||
await page.getByTestId("interview-send").click();
|
||||
|
||||
// Wait for question 2
|
||||
await expect(page.getByText("What tech stack would you like to use?")).toBeVisible({ timeout: 3000 });
|
||||
|
||||
// Question 2: Tech stack
|
||||
await page.getByTestId("interview-input").fill("React, TypeScript, Tailwind CSS");
|
||||
await page.getByTestId("interview-send").click();
|
||||
|
||||
// Wait for question 3
|
||||
await expect(page.getByText("What are the core features you want to include?")).toBeVisible({ timeout: 3000 });
|
||||
|
||||
// Question 3: Core features
|
||||
await page.getByTestId("interview-input").fill("Add tasks, Mark complete, Delete tasks, Categories");
|
||||
await page.getByTestId("interview-send").click();
|
||||
|
||||
// Wait for question 4
|
||||
await expect(page.getByText("Any additional requirements or preferences?")).toBeVisible({ timeout: 3000 });
|
||||
|
||||
// Question 4: Additional requirements
|
||||
await page.getByTestId("interview-input").fill("Mobile responsive, Dark mode support");
|
||||
await page.getByTestId("interview-send").click();
|
||||
|
||||
// Wait for spec generation
|
||||
await expect(page.getByText("Generating specification")).toBeVisible({ timeout: 3000 });
|
||||
|
||||
// Wait for project setup form to appear
|
||||
await expect(page.getByTestId("project-setup-form")).toBeVisible({ timeout: 5000 });
|
||||
|
||||
// Verify spec preview contains expected content
|
||||
await expect(page.getByTestId("spec-preview")).toBeVisible();
|
||||
await expect(page.getByTestId("spec-preview")).toContainText("project_specification");
|
||||
await expect(page.getByTestId("spec-preview")).toContainText("A todo app");
|
||||
|
||||
// Verify we can enter project name and path
|
||||
await expect(page.getByTestId("interview-project-name-input")).toBeVisible();
|
||||
await expect(page.getByTestId("interview-project-path-input")).toBeVisible();
|
||||
});
|
||||
|
||||
test("shows progress indicator throughout interview", async ({ page }) => {
|
||||
// Navigate to interview view
|
||||
await page.getByTestId("create-new-project").click();
|
||||
await page.getByTestId("interactive-mode-option").click();
|
||||
|
||||
// Verify header shows question count
|
||||
await expect(page.getByText("Question 1 of 4")).toBeVisible();
|
||||
|
||||
// Answer first question
|
||||
await page.getByTestId("interview-input").fill("A todo app");
|
||||
await page.getByTestId("interview-send").click();
|
||||
|
||||
// Verify progress updates
|
||||
await expect(page.getByText("Question 2 of 4")).toBeVisible({ timeout: 3000 });
|
||||
});
|
||||
|
||||
test("can navigate back to welcome view", async ({ page }) => {
|
||||
// Navigate to interview view
|
||||
await page.getByTestId("create-new-project").click();
|
||||
await page.getByTestId("interactive-mode-option").click();
|
||||
|
||||
// Verify interview view is displayed
|
||||
await expect(page.getByTestId("interview-view")).toBeVisible();
|
||||
|
||||
// Click back button
|
||||
await page.getByTestId("interview-back-button").click();
|
||||
|
||||
// Verify we're back at welcome view
|
||||
await expect(page.getByTestId("welcome-view")).toBeVisible();
|
||||
});
|
||||
|
||||
test("dropdown shows both Quick Setup and Interactive Mode options", async ({ page }) => {
|
||||
// Click the Create Project button
|
||||
await page.getByTestId("create-new-project").click();
|
||||
|
||||
// Verify both options are present
|
||||
await expect(page.getByTestId("quick-setup-option")).toBeVisible();
|
||||
await expect(page.getByText("Quick Setup")).toBeVisible();
|
||||
|
||||
await expect(page.getByTestId("interactive-mode-option")).toBeVisible();
|
||||
await expect(page.getByText("Interactive Mode")).toBeVisible();
|
||||
});
|
||||
|
||||
test("Quick Setup option opens the original new project dialog", async ({ page }) => {
|
||||
// Click the Create Project button
|
||||
await page.getByTestId("create-new-project").click();
|
||||
|
||||
// Click Quick Setup
|
||||
await page.getByTestId("quick-setup-option").click();
|
||||
|
||||
// Verify the original dialog appears
|
||||
await expect(page.getByTestId("new-project-dialog")).toBeVisible();
|
||||
await expect(page.getByText("Create New Project")).toBeVisible();
|
||||
});
|
||||
|
||||
test("can create project after completing interview", async ({ page }) => {
|
||||
// Navigate to interview view
|
||||
await page.getByTestId("create-new-project").click();
|
||||
await page.getByTestId("interactive-mode-option").click();
|
||||
|
||||
// Complete all interview questions
|
||||
// Question 1
|
||||
await page.getByTestId("interview-input").fill("A todo app");
|
||||
await page.getByTestId("interview-send").click();
|
||||
await expect(page.getByText("What tech stack would you like to use?")).toBeVisible({ timeout: 3000 });
|
||||
|
||||
// Question 2
|
||||
await page.getByTestId("interview-input").fill("React, Node.js");
|
||||
await page.getByTestId("interview-send").click();
|
||||
await expect(page.getByText("What are the core features you want to include?")).toBeVisible({ timeout: 3000 });
|
||||
|
||||
// Question 3
|
||||
await page.getByTestId("interview-input").fill("Add tasks, Delete tasks");
|
||||
await page.getByTestId("interview-send").click();
|
||||
await expect(page.getByText("Any additional requirements or preferences?")).toBeVisible({ timeout: 3000 });
|
||||
|
||||
// Question 4
|
||||
await page.getByTestId("interview-input").fill("None");
|
||||
await page.getByTestId("interview-send").click();
|
||||
|
||||
// Wait for project setup form
|
||||
await expect(page.getByTestId("project-setup-form")).toBeVisible({ timeout: 5000 });
|
||||
|
||||
// Fill in project details
|
||||
await page.getByTestId("interview-project-name-input").fill("my-todo-app");
|
||||
await page.getByTestId("interview-project-path-input").fill("/Users/test/projects");
|
||||
|
||||
// Create project button should be enabled
|
||||
await expect(page.getByTestId("interview-create-project")).toBeEnabled();
|
||||
|
||||
// Click create project
|
||||
await page.getByTestId("interview-create-project").click();
|
||||
|
||||
// Should navigate to board view with the new project
|
||||
await expect(page.getByTestId("board-view")).toBeVisible({ timeout: 5000 });
|
||||
});
|
||||
|
||||
test("create project button is disabled without name and path", async ({ page }) => {
|
||||
// Navigate to interview view
|
||||
await page.getByTestId("create-new-project").click();
|
||||
await page.getByTestId("interactive-mode-option").click();
|
||||
|
||||
// Complete all interview questions quickly
|
||||
await page.getByTestId("interview-input").fill("A todo app");
|
||||
await page.getByTestId("interview-send").click();
|
||||
await expect(page.getByText("What tech stack")).toBeVisible({ timeout: 3000 });
|
||||
|
||||
await page.getByTestId("interview-input").fill("React");
|
||||
await page.getByTestId("interview-send").click();
|
||||
await expect(page.getByText("core features")).toBeVisible({ timeout: 3000 });
|
||||
|
||||
await page.getByTestId("interview-input").fill("Tasks");
|
||||
await page.getByTestId("interview-send").click();
|
||||
await expect(page.getByText("additional requirements")).toBeVisible({ timeout: 3000 });
|
||||
|
||||
await page.getByTestId("interview-input").fill("None");
|
||||
await page.getByTestId("interview-send").click();
|
||||
|
||||
// Wait for project setup form
|
||||
await expect(page.getByTestId("project-setup-form")).toBeVisible({ timeout: 5000 });
|
||||
|
||||
// Create button should be disabled initially
|
||||
await expect(page.getByTestId("interview-create-project")).toBeDisabled();
|
||||
|
||||
// Fill only name
|
||||
await page.getByTestId("interview-project-name-input").fill("my-project");
|
||||
await expect(page.getByTestId("interview-create-project")).toBeDisabled();
|
||||
|
||||
// Clear name and fill only path
|
||||
await page.getByTestId("interview-project-name-input").clear();
|
||||
await page.getByTestId("interview-project-path-input").fill("/some/path");
|
||||
await expect(page.getByTestId("interview-create-project")).toBeDisabled();
|
||||
|
||||
// Fill both - should be enabled
|
||||
await page.getByTestId("interview-project-name-input").fill("my-project");
|
||||
await expect(page.getByTestId("interview-create-project")).toBeEnabled();
|
||||
});
|
||||
});
|
||||
@@ -1,398 +0,0 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
|
||||
test.describe("Interactive New Project Interview", () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto("/");
|
||||
});
|
||||
|
||||
test("Step 1: Click 'New Project' -> 'Interactive Mode' navigates to interview view", async ({ page }) => {
|
||||
// Click the Create Project dropdown button
|
||||
await page.getByTestId("create-new-project").click();
|
||||
|
||||
// Wait for dropdown to appear and click Interactive Mode option
|
||||
await page.getByTestId("interactive-mode-option").click();
|
||||
|
||||
// Verify interview view is displayed
|
||||
await expect(page.getByTestId("interview-view")).toBeVisible();
|
||||
|
||||
// Verify the header shows "New Project Interview"
|
||||
await expect(page.getByText("New Project Interview")).toBeVisible();
|
||||
});
|
||||
|
||||
test("Step 2: Chat interface appears asking 'What do you want to build?'", async ({ page }) => {
|
||||
// Navigate to interview view
|
||||
await page.getByTestId("create-new-project").click();
|
||||
await page.getByTestId("interactive-mode-option").click();
|
||||
|
||||
// Verify interview view is displayed
|
||||
await expect(page.getByTestId("interview-view")).toBeVisible();
|
||||
|
||||
// Verify the first question is displayed
|
||||
await expect(page.getByTestId("interview-messages")).toBeVisible();
|
||||
await expect(page.getByText("What do you want to build?")).toBeVisible();
|
||||
|
||||
// Verify input field is available
|
||||
await expect(page.getByTestId("interview-input")).toBeVisible();
|
||||
await expect(page.getByTestId("interview-send")).toBeVisible();
|
||||
});
|
||||
|
||||
test("Step 3: User can reply 'A todo app'", async ({ page }) => {
|
||||
// Navigate to interview view
|
||||
await page.getByTestId("create-new-project").click();
|
||||
await page.getByTestId("interactive-mode-option").click();
|
||||
|
||||
// Verify interview view is displayed
|
||||
await expect(page.getByTestId("interview-view")).toBeVisible();
|
||||
|
||||
// Type the answer in the input field
|
||||
await page.getByTestId("interview-input").fill("A todo app");
|
||||
|
||||
// Click send button
|
||||
await page.getByTestId("interview-send").click();
|
||||
|
||||
// Verify user message appears in the chat
|
||||
await expect(page.getByTestId("interview-messages").getByText("A todo app")).toBeVisible();
|
||||
});
|
||||
|
||||
test("Step 4: Agent asks clarifying questions (tech stack)", async ({ page }) => {
|
||||
// Navigate to interview view
|
||||
await page.getByTestId("create-new-project").click();
|
||||
await page.getByTestId("interactive-mode-option").click();
|
||||
|
||||
// Wait for interview view
|
||||
await expect(page.getByTestId("interview-view")).toBeVisible();
|
||||
|
||||
// Answer first question
|
||||
await page.getByTestId("interview-input").fill("A todo app");
|
||||
await page.getByTestId("interview-send").click();
|
||||
|
||||
// Wait for the next question to appear (tech stack question)
|
||||
await expect(page.getByText("What tech stack would you like to use?")).toBeVisible({ timeout: 5000 });
|
||||
|
||||
// Verify progress text shows question 2 of 4
|
||||
await expect(page.getByText("Question 2 of 4")).toBeVisible();
|
||||
});
|
||||
|
||||
test("Step 5: Agent generates draft app_spec.txt based on conversation", async ({ page }) => {
|
||||
// Navigate to interview view
|
||||
await page.getByTestId("create-new-project").click();
|
||||
await page.getByTestId("interactive-mode-option").click();
|
||||
|
||||
// Wait for interview view
|
||||
await expect(page.getByTestId("interview-view")).toBeVisible();
|
||||
|
||||
// Answer first question - project description
|
||||
await page.getByTestId("interview-input").fill("A todo app");
|
||||
await page.getByTestId("interview-send").click();
|
||||
|
||||
// Wait for tech stack question
|
||||
await expect(page.getByText("What tech stack would you like to use?")).toBeVisible({ timeout: 5000 });
|
||||
|
||||
// Answer tech stack question
|
||||
await page.getByTestId("interview-input").fill("React, Next.js, TypeScript");
|
||||
await page.getByTestId("interview-send").click();
|
||||
|
||||
// Wait for features question
|
||||
await expect(page.getByText("What are the core features you want to include?")).toBeVisible({ timeout: 5000 });
|
||||
|
||||
// Answer features question
|
||||
await page.getByTestId("interview-input").fill("Add tasks, Mark complete, Delete tasks");
|
||||
await page.getByTestId("interview-send").click();
|
||||
|
||||
// Wait for additional requirements question
|
||||
await expect(page.getByText("Any additional requirements or preferences?")).toBeVisible({ timeout: 5000 });
|
||||
|
||||
// Answer additional requirements
|
||||
await page.getByTestId("interview-input").fill("Dark mode support");
|
||||
await page.getByTestId("interview-send").click();
|
||||
|
||||
// Wait for spec generation to complete
|
||||
await expect(page.getByTestId("project-setup-form")).toBeVisible({ timeout: 10000 });
|
||||
|
||||
// Verify spec preview is visible with generated content
|
||||
await expect(page.getByTestId("spec-preview")).toBeVisible();
|
||||
|
||||
// Verify the generated spec contains the project description
|
||||
const specPreview = page.getByTestId("spec-preview");
|
||||
await expect(specPreview).toContainText("A todo app");
|
||||
|
||||
// Verify it contains tech stack information
|
||||
await expect(specPreview).toContainText("React");
|
||||
|
||||
// Verify it contains features
|
||||
await expect(specPreview).toContainText("Add tasks");
|
||||
});
|
||||
|
||||
test("Interview shows progress indicator with correct number of steps", async ({ page }) => {
|
||||
// Navigate to interview view
|
||||
await page.getByTestId("create-new-project").click();
|
||||
await page.getByTestId("interactive-mode-option").click();
|
||||
|
||||
// Verify interview view is displayed
|
||||
await expect(page.getByTestId("interview-view")).toBeVisible();
|
||||
|
||||
// Verify progress text shows question 1 of 4
|
||||
await expect(page.getByText("Question 1 of 4")).toBeVisible();
|
||||
});
|
||||
|
||||
test("Send button is disabled when input is empty", async ({ page }) => {
|
||||
// Navigate to interview view
|
||||
await page.getByTestId("create-new-project").click();
|
||||
await page.getByTestId("interactive-mode-option").click();
|
||||
|
||||
// Verify send button is disabled when input is empty
|
||||
await expect(page.getByTestId("interview-send")).toBeDisabled();
|
||||
|
||||
// Type something
|
||||
await page.getByTestId("interview-input").fill("Test");
|
||||
|
||||
// Now button should be enabled
|
||||
await expect(page.getByTestId("interview-send")).toBeEnabled();
|
||||
|
||||
// Clear input
|
||||
await page.getByTestId("interview-input").fill("");
|
||||
|
||||
// Button should be disabled again
|
||||
await expect(page.getByTestId("interview-send")).toBeDisabled();
|
||||
});
|
||||
|
||||
test("Can submit answer by pressing Enter", async ({ page }) => {
|
||||
// Navigate to interview view
|
||||
await page.getByTestId("create-new-project").click();
|
||||
await page.getByTestId("interactive-mode-option").click();
|
||||
|
||||
// Verify interview view is displayed
|
||||
await expect(page.getByTestId("interview-view")).toBeVisible();
|
||||
|
||||
// Type answer and press Enter
|
||||
await page.getByTestId("interview-input").fill("A todo app");
|
||||
await page.getByTestId("interview-input").press("Enter");
|
||||
|
||||
// Verify user message appears
|
||||
await expect(page.getByTestId("interview-messages").getByText("A todo app")).toBeVisible();
|
||||
});
|
||||
|
||||
test("Back button returns to welcome view", async ({ page }) => {
|
||||
// Navigate to interview view
|
||||
await page.getByTestId("create-new-project").click();
|
||||
await page.getByTestId("interactive-mode-option").click();
|
||||
|
||||
// Verify interview view is displayed
|
||||
await expect(page.getByTestId("interview-view")).toBeVisible();
|
||||
|
||||
// Click back button
|
||||
await page.getByTestId("interview-back-button").click();
|
||||
|
||||
// Verify we're back on welcome view
|
||||
await expect(page.getByTestId("welcome-view")).toBeVisible();
|
||||
});
|
||||
|
||||
test("Project setup form appears after completing interview", async ({ page }) => {
|
||||
// Navigate to interview view
|
||||
await page.getByTestId("create-new-project").click();
|
||||
await page.getByTestId("interactive-mode-option").click();
|
||||
|
||||
// Complete all questions
|
||||
await page.getByTestId("interview-input").fill("A simple todo app");
|
||||
await page.getByTestId("interview-send").click();
|
||||
|
||||
await expect(page.getByText("What tech stack would you like to use?")).toBeVisible({ timeout: 5000 });
|
||||
await page.getByTestId("interview-input").fill("React");
|
||||
await page.getByTestId("interview-send").click();
|
||||
|
||||
await expect(page.getByText("What are the core features")).toBeVisible({ timeout: 5000 });
|
||||
await page.getByTestId("interview-input").fill("Add, edit, delete tasks");
|
||||
await page.getByTestId("interview-send").click();
|
||||
|
||||
await expect(page.getByText("Any additional requirements")).toBeVisible({ timeout: 5000 });
|
||||
await page.getByTestId("interview-input").fill("None");
|
||||
await page.getByTestId("interview-send").click();
|
||||
|
||||
// Wait for project setup form
|
||||
await expect(page.getByTestId("project-setup-form")).toBeVisible({ timeout: 10000 });
|
||||
|
||||
// Verify form has name input
|
||||
await expect(page.getByTestId("interview-project-name-input")).toBeVisible();
|
||||
|
||||
// Verify form has path input
|
||||
await expect(page.getByTestId("interview-project-path-input")).toBeVisible();
|
||||
|
||||
// Verify browse directory button
|
||||
await expect(page.getByTestId("interview-browse-directory")).toBeVisible();
|
||||
|
||||
// Verify create project button
|
||||
await expect(page.getByTestId("interview-create-project")).toBeVisible();
|
||||
});
|
||||
|
||||
test("Create project button is disabled without name and path", async ({ page }) => {
|
||||
// Navigate to interview view and complete interview
|
||||
await page.getByTestId("create-new-project").click();
|
||||
await page.getByTestId("interactive-mode-option").click();
|
||||
|
||||
// Complete all questions quickly
|
||||
await page.getByTestId("interview-input").fill("A simple todo app");
|
||||
await page.getByTestId("interview-send").click();
|
||||
|
||||
await expect(page.getByText("What tech stack")).toBeVisible({ timeout: 5000 });
|
||||
await page.getByTestId("interview-input").fill("React");
|
||||
await page.getByTestId("interview-send").click();
|
||||
|
||||
await expect(page.getByText("core features")).toBeVisible({ timeout: 5000 });
|
||||
await page.getByTestId("interview-input").fill("Tasks");
|
||||
await page.getByTestId("interview-send").click();
|
||||
|
||||
await expect(page.getByText("additional requirements")).toBeVisible({ timeout: 5000 });
|
||||
await page.getByTestId("interview-input").fill("None");
|
||||
await page.getByTestId("interview-send").click();
|
||||
|
||||
// Wait for project setup form
|
||||
await expect(page.getByTestId("project-setup-form")).toBeVisible({ timeout: 10000 });
|
||||
|
||||
// Create button should be disabled without name and path
|
||||
await expect(page.getByTestId("interview-create-project")).toBeDisabled();
|
||||
|
||||
// Enter project name
|
||||
await page.getByTestId("interview-project-name-input").fill("my-todo-app");
|
||||
|
||||
// Still disabled (no path)
|
||||
await expect(page.getByTestId("interview-create-project")).toBeDisabled();
|
||||
|
||||
// Enter path
|
||||
await page.getByTestId("interview-project-path-input").fill("/Users/test/projects");
|
||||
|
||||
// Now should be enabled
|
||||
await expect(page.getByTestId("interview-create-project")).toBeEnabled();
|
||||
});
|
||||
|
||||
test("Creates project and navigates to board view after interview", async ({ page }) => {
|
||||
// Navigate to interview view
|
||||
await page.getByTestId("create-new-project").click();
|
||||
await page.getByTestId("interactive-mode-option").click();
|
||||
|
||||
// Complete all questions
|
||||
await page.getByTestId("interview-input").fill("A simple todo app");
|
||||
await page.getByTestId("interview-send").click();
|
||||
|
||||
await expect(page.getByText("What tech stack")).toBeVisible({ timeout: 5000 });
|
||||
await page.getByTestId("interview-input").fill("React");
|
||||
await page.getByTestId("interview-send").click();
|
||||
|
||||
await expect(page.getByText("core features")).toBeVisible({ timeout: 5000 });
|
||||
await page.getByTestId("interview-input").fill("Tasks");
|
||||
await page.getByTestId("interview-send").click();
|
||||
|
||||
await expect(page.getByText("additional requirements")).toBeVisible({ timeout: 5000 });
|
||||
await page.getByTestId("interview-input").fill("None");
|
||||
await page.getByTestId("interview-send").click();
|
||||
|
||||
// Wait for project setup form
|
||||
await expect(page.getByTestId("project-setup-form")).toBeVisible({ timeout: 10000 });
|
||||
|
||||
// Fill in project details
|
||||
await page.getByTestId("interview-project-name-input").fill("interview-test-project");
|
||||
await page.getByTestId("interview-project-path-input").fill("/Users/test/projects");
|
||||
|
||||
// Click create
|
||||
await page.getByTestId("interview-create-project").click();
|
||||
|
||||
// Should navigate to board view
|
||||
await expect(page.getByTestId("board-view")).toBeVisible({ timeout: 5000 });
|
||||
|
||||
// Project name should be visible
|
||||
await expect(page.getByTestId("board-view").getByText("interview-test-project")).toBeVisible();
|
||||
});
|
||||
|
||||
test("Interview messages have timestamps", async ({ page }) => {
|
||||
// Navigate to interview view
|
||||
await page.getByTestId("create-new-project").click();
|
||||
await page.getByTestId("interactive-mode-option").click();
|
||||
|
||||
// Verify interview view is displayed
|
||||
await expect(page.getByTestId("interview-view")).toBeVisible();
|
||||
|
||||
// The welcome message should have a timestamp displayed
|
||||
// Timestamps are in format like "10:30:45 AM" or similar
|
||||
const messagesArea = page.getByTestId("interview-messages");
|
||||
await expect(messagesArea).toBeVisible();
|
||||
|
||||
// The welcome message should contain the first question
|
||||
await expect(messagesArea.getByText("What do you want to build?")).toBeVisible();
|
||||
|
||||
// The message area should contain timestamp text (time format like "10:30:45 AM")
|
||||
// We verify by checking that the welcome message exists and has content
|
||||
await expect(messagesArea.locator("p.text-sm").first()).toBeVisible();
|
||||
});
|
||||
|
||||
test("Input field is hidden after interview completes", async ({ page }) => {
|
||||
// Navigate to interview view
|
||||
await page.getByTestId("create-new-project").click();
|
||||
await page.getByTestId("interactive-mode-option").click();
|
||||
|
||||
// Complete all questions
|
||||
await page.getByTestId("interview-input").fill("A todo app");
|
||||
await page.getByTestId("interview-send").click();
|
||||
|
||||
await expect(page.getByText("What tech stack")).toBeVisible({ timeout: 5000 });
|
||||
await page.getByTestId("interview-input").fill("React");
|
||||
await page.getByTestId("interview-send").click();
|
||||
|
||||
await expect(page.getByText("core features")).toBeVisible({ timeout: 5000 });
|
||||
await page.getByTestId("interview-input").fill("Tasks");
|
||||
await page.getByTestId("interview-send").click();
|
||||
|
||||
await expect(page.getByText("additional requirements")).toBeVisible({ timeout: 5000 });
|
||||
await page.getByTestId("interview-input").fill("None");
|
||||
await page.getByTestId("interview-send").click();
|
||||
|
||||
// Wait for project setup form (interview complete)
|
||||
await expect(page.getByTestId("project-setup-form")).toBeVisible({ timeout: 10000 });
|
||||
|
||||
// Input field should no longer be visible
|
||||
await expect(page.getByTestId("interview-input")).not.toBeVisible();
|
||||
});
|
||||
|
||||
test("Generated spec contains proper XML structure", async ({ page }) => {
|
||||
// Navigate to interview view
|
||||
await page.getByTestId("create-new-project").click();
|
||||
await page.getByTestId("interactive-mode-option").click();
|
||||
|
||||
// Complete all questions
|
||||
await page.getByTestId("interview-input").fill("A todo app");
|
||||
await page.getByTestId("interview-send").click();
|
||||
|
||||
await expect(page.getByText("What tech stack")).toBeVisible({ timeout: 5000 });
|
||||
await page.getByTestId("interview-input").fill("React, TypeScript");
|
||||
await page.getByTestId("interview-send").click();
|
||||
|
||||
await expect(page.getByText("core features")).toBeVisible({ timeout: 5000 });
|
||||
await page.getByTestId("interview-input").fill("Add tasks, Delete tasks");
|
||||
await page.getByTestId("interview-send").click();
|
||||
|
||||
await expect(page.getByText("additional requirements")).toBeVisible({ timeout: 5000 });
|
||||
await page.getByTestId("interview-input").fill("Mobile responsive");
|
||||
await page.getByTestId("interview-send").click();
|
||||
|
||||
// Wait for spec preview
|
||||
await expect(page.getByTestId("spec-preview")).toBeVisible({ timeout: 10000 });
|
||||
|
||||
// Verify XML structure elements
|
||||
const specPreview = page.getByTestId("spec-preview");
|
||||
await expect(specPreview).toContainText("<project_specification>");
|
||||
await expect(specPreview).toContainText("<overview>");
|
||||
await expect(specPreview).toContainText("<technology_stack>");
|
||||
await expect(specPreview).toContainText("<core_capabilities>");
|
||||
await expect(specPreview).toContainText("<development_guidelines>");
|
||||
});
|
||||
|
||||
test("Quick Setup option still works from dropdown", async ({ page }) => {
|
||||
// Click the Create Project dropdown button
|
||||
await page.getByTestId("create-new-project").click();
|
||||
|
||||
// Click Quick Setup option
|
||||
await page.getByTestId("quick-setup-option").click();
|
||||
|
||||
// Verify dialog appears (not interview view)
|
||||
await expect(page.getByTestId("new-project-dialog")).toBeVisible();
|
||||
await expect(page.getByText("Create New Project")).toBeVisible();
|
||||
});
|
||||
});
|
||||
@@ -1,307 +0,0 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
|
||||
test.describe("Kanban Board", () => {
|
||||
// Helper to set up a mock project in localStorage
|
||||
async function setupMockProject(page: ReturnType<typeof test.step>) {
|
||||
await page.addInitScript(() => {
|
||||
const mockProject = {
|
||||
id: "test-project-1",
|
||||
name: "Test Project",
|
||||
path: "/mock/test-project",
|
||||
lastOpened: new Date().toISOString(),
|
||||
};
|
||||
localStorage.setItem(
|
||||
"automaker-storage",
|
||||
JSON.stringify({
|
||||
state: {
|
||||
projects: [mockProject],
|
||||
currentProject: mockProject,
|
||||
currentView: "board",
|
||||
sidebarOpen: true,
|
||||
theme: "dark",
|
||||
},
|
||||
version: 0,
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
test("shows Add Feature button", async ({ page }) => {
|
||||
await setupMockProject(page);
|
||||
await page.goto("/");
|
||||
|
||||
await expect(page.getByTestId("add-feature-button")).toBeVisible();
|
||||
});
|
||||
|
||||
test("opens add feature dialog", async ({ page }) => {
|
||||
await setupMockProject(page);
|
||||
await page.goto("/");
|
||||
|
||||
// Click add feature button
|
||||
await page.getByTestId("add-feature-button").click();
|
||||
|
||||
// Dialog should appear
|
||||
await expect(page.getByTestId("add-feature-dialog")).toBeVisible();
|
||||
await expect(page.getByTestId("feature-category-input")).toBeVisible();
|
||||
await expect(page.getByTestId("feature-description-input")).toBeVisible();
|
||||
});
|
||||
|
||||
test("can add a new feature", async ({ page }) => {
|
||||
await setupMockProject(page);
|
||||
await page.goto("/");
|
||||
|
||||
// Wait for board to load
|
||||
await expect(page.getByTestId("board-view")).toBeVisible();
|
||||
|
||||
// Click add feature button
|
||||
await page.getByTestId("add-feature-button").click();
|
||||
|
||||
// Fill in feature details
|
||||
await page.getByTestId("feature-category-input").fill("Test Category");
|
||||
await page
|
||||
.getByTestId("feature-description-input")
|
||||
.fill("Test Feature Description");
|
||||
await page.getByTestId("feature-step-0-input").fill("Step 1: First step");
|
||||
|
||||
// Submit the form
|
||||
await page.getByTestId("confirm-add-feature").click();
|
||||
|
||||
// Dialog should close
|
||||
await expect(page.getByTestId("add-feature-dialog")).not.toBeVisible();
|
||||
});
|
||||
|
||||
test("refresh button is visible", async ({ page }) => {
|
||||
await setupMockProject(page);
|
||||
await page.goto("/");
|
||||
|
||||
await expect(page.getByTestId("refresh-board")).toBeVisible();
|
||||
});
|
||||
|
||||
test("loads cards from .automaker/feature_list.json and displays them in correct columns", async ({
|
||||
page,
|
||||
}) => {
|
||||
await setupMockProject(page);
|
||||
await page.goto("/");
|
||||
|
||||
// Wait for board to load
|
||||
await expect(page.getByTestId("board-view")).toBeVisible();
|
||||
|
||||
// Wait for loading to complete (the mock IPC returns a sample feature)
|
||||
// The mock returns a feature in "backlog" column (passes: false)
|
||||
await expect(page.getByTestId("kanban-column-backlog")).toBeVisible();
|
||||
|
||||
// After loading, the backlog should show the sample feature from mock data
|
||||
// Looking at the electron.ts mock, it returns one feature with "Sample Feature"
|
||||
const backlogColumn = page.getByTestId("kanban-column-backlog");
|
||||
await expect(backlogColumn.getByText("Sample Feature")).toBeVisible();
|
||||
});
|
||||
|
||||
test("features with passes:true appear in verified column", async ({
|
||||
page,
|
||||
}) => {
|
||||
// Create a project and add a feature manually
|
||||
await setupMockProject(page);
|
||||
await page.goto("/");
|
||||
|
||||
// Wait for board to load
|
||||
await expect(page.getByTestId("board-view")).toBeVisible();
|
||||
|
||||
// Add a new feature
|
||||
await page.getByTestId("add-feature-button").click();
|
||||
await page.getByTestId("feature-category-input").fill("Core");
|
||||
await page
|
||||
.getByTestId("feature-description-input")
|
||||
.fill("Verified Test Feature");
|
||||
await page.getByTestId("confirm-add-feature").click();
|
||||
|
||||
// The new feature should appear in backlog
|
||||
await expect(
|
||||
page
|
||||
.getByTestId("kanban-column-backlog")
|
||||
.getByText("Verified Test Feature")
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test("can edit feature card details", async ({ page }) => {
|
||||
await setupMockProject(page);
|
||||
await page.goto("/");
|
||||
|
||||
// Wait for board to load
|
||||
await expect(page.getByTestId("board-view")).toBeVisible();
|
||||
|
||||
// Wait for features to load - the mock returns "Sample Feature"
|
||||
await expect(
|
||||
page.getByTestId("kanban-column-backlog").getByText("Sample Feature")
|
||||
).toBeVisible();
|
||||
|
||||
// Find and click the edit button on the card using specific testid pattern
|
||||
const backlogColumn = page.getByTestId("kanban-column-backlog");
|
||||
// The edit button has testid "edit-feature-{feature.id}" where feature.id contains "feature-0-"
|
||||
const editButton = backlogColumn.locator(
|
||||
'[data-testid^="edit-feature-feature-0-"]'
|
||||
);
|
||||
await editButton.click();
|
||||
|
||||
// Edit dialog should appear
|
||||
await expect(page.getByTestId("edit-feature-dialog")).toBeVisible();
|
||||
|
||||
// Edit the description
|
||||
await page
|
||||
.getByTestId("edit-feature-description")
|
||||
.fill("Updated Feature Description");
|
||||
|
||||
// Save the changes
|
||||
await page.getByTestId("confirm-edit-feature").click();
|
||||
|
||||
// Dialog should close
|
||||
await expect(page.getByTestId("edit-feature-dialog")).not.toBeVisible();
|
||||
|
||||
// The updated description should be visible
|
||||
await expect(page.getByText("Updated Feature Description")).toBeVisible();
|
||||
});
|
||||
|
||||
test("edit dialog shows existing feature data", async ({ page }) => {
|
||||
await setupMockProject(page);
|
||||
await page.goto("/");
|
||||
|
||||
// Wait for board to load
|
||||
await expect(page.getByTestId("board-view")).toBeVisible();
|
||||
|
||||
// Wait for features to load
|
||||
await expect(
|
||||
page.getByTestId("kanban-column-backlog").getByText("Sample Feature")
|
||||
).toBeVisible();
|
||||
|
||||
// Click edit button using specific testid pattern
|
||||
const backlogColumn = page.getByTestId("kanban-column-backlog");
|
||||
const editButton = backlogColumn.locator(
|
||||
'[data-testid^="edit-feature-feature-0-"]'
|
||||
);
|
||||
await editButton.click();
|
||||
|
||||
// Check that the dialog pre-populates with existing data
|
||||
await expect(page.getByTestId("edit-feature-description")).toHaveValue(
|
||||
"Sample Feature"
|
||||
);
|
||||
await expect(page.getByTestId("edit-feature-category")).toHaveValue("Core");
|
||||
});
|
||||
|
||||
test("can drag card from Backlog to In Progress column", async ({ page }) => {
|
||||
await setupMockProject(page);
|
||||
await page.goto("/");
|
||||
|
||||
// Wait for board to load
|
||||
await expect(page.getByTestId("board-view")).toBeVisible();
|
||||
|
||||
// Wait for features to load in Backlog
|
||||
const backlogColumn = page.getByTestId("kanban-column-backlog");
|
||||
const inProgressColumn = page.getByTestId("kanban-column-in_progress");
|
||||
|
||||
await expect(backlogColumn.getByText("Sample Feature")).toBeVisible();
|
||||
|
||||
// Find the drag handle specifically
|
||||
const dragHandle = backlogColumn.locator(
|
||||
'[data-testid^="drag-handle-feature-0-"]'
|
||||
);
|
||||
await expect(dragHandle).toBeVisible();
|
||||
|
||||
// Get drag handle and target positions
|
||||
const handleBox = await dragHandle.boundingBox();
|
||||
const targetBox = await inProgressColumn.boundingBox();
|
||||
if (!handleBox || !targetBox) throw new Error("Could not find elements");
|
||||
|
||||
// Use mouse events - start from center of drag handle
|
||||
const startX = handleBox.x + handleBox.width / 2;
|
||||
const startY = handleBox.y + handleBox.height / 2;
|
||||
const endX = targetBox.x + targetBox.width / 2;
|
||||
const endY = targetBox.y + 100;
|
||||
|
||||
await page.mouse.move(startX, startY);
|
||||
await page.mouse.down();
|
||||
|
||||
// Move in steps to trigger dnd-kit activation (needs >8px movement)
|
||||
await page.mouse.move(endX, endY, { steps: 20 });
|
||||
await page.mouse.up();
|
||||
|
||||
// Verify card moved to In Progress column
|
||||
await expect(inProgressColumn.getByText("Sample Feature")).toBeVisible();
|
||||
|
||||
// Verify card is no longer in Backlog
|
||||
await expect(backlogColumn.getByText("Sample Feature")).not.toBeVisible();
|
||||
});
|
||||
|
||||
test("displays delete button (trash icon) on feature card", async ({
|
||||
page,
|
||||
}) => {
|
||||
await setupMockProject(page);
|
||||
await page.goto("/");
|
||||
|
||||
// Wait for board to load
|
||||
await expect(page.getByTestId("board-view")).toBeVisible();
|
||||
|
||||
// Wait for features to load in Backlog
|
||||
const backlogColumn = page.getByTestId("kanban-column-backlog");
|
||||
await expect(backlogColumn.getByText("Sample Feature")).toBeVisible();
|
||||
|
||||
// Find the delete button on the card
|
||||
const deleteButton = backlogColumn.locator(
|
||||
'[data-testid^="delete-feature-feature-0-"]'
|
||||
);
|
||||
await expect(deleteButton).toBeVisible();
|
||||
});
|
||||
|
||||
test("can delete a feature from kanban board", async ({ page }) => {
|
||||
await setupMockProject(page);
|
||||
await page.goto("/");
|
||||
|
||||
// Wait for board to load
|
||||
await expect(page.getByTestId("board-view")).toBeVisible();
|
||||
|
||||
// Wait for features to load in Backlog
|
||||
const backlogColumn = page.getByTestId("kanban-column-backlog");
|
||||
await expect(backlogColumn.getByText("Sample Feature")).toBeVisible();
|
||||
|
||||
// Find and click the delete button
|
||||
const deleteButton = backlogColumn.locator(
|
||||
'[data-testid^="delete-feature-feature-0-"]'
|
||||
);
|
||||
await deleteButton.click();
|
||||
|
||||
// Verify the feature is removed from the board
|
||||
await expect(backlogColumn.getByText("Sample Feature")).not.toBeVisible();
|
||||
});
|
||||
|
||||
test("deleting feature removes it from all columns", async ({ page }) => {
|
||||
await setupMockProject(page);
|
||||
await page.goto("/");
|
||||
|
||||
// Wait for board to load
|
||||
await expect(page.getByTestId("board-view")).toBeVisible();
|
||||
|
||||
// Add a new feature first
|
||||
await page.getByTestId("add-feature-button").click();
|
||||
await page.getByTestId("feature-category-input").fill("Test Category");
|
||||
await page
|
||||
.getByTestId("feature-description-input")
|
||||
.fill("Feature to Delete");
|
||||
await page.getByTestId("confirm-add-feature").click();
|
||||
|
||||
// Wait for the new feature to appear in backlog
|
||||
const backlogColumn = page.getByTestId("kanban-column-backlog");
|
||||
await expect(backlogColumn.getByText("Feature to Delete")).toBeVisible();
|
||||
|
||||
// Find and click the delete button for the newly added feature
|
||||
const deleteButton = backlogColumn
|
||||
.locator('[data-testid^="delete-feature-feature-"]')
|
||||
.last();
|
||||
await deleteButton.click();
|
||||
|
||||
// Verify the feature is removed
|
||||
await expect(
|
||||
backlogColumn.getByText("Feature to Delete")
|
||||
).not.toBeVisible();
|
||||
|
||||
// Also verify it's not anywhere else on the board
|
||||
await expect(page.getByText("Feature to Delete")).not.toBeVisible();
|
||||
});
|
||||
});
|
||||
@@ -1,104 +0,0 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
|
||||
test.describe("New Chat Session Auto Focus", () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto("/");
|
||||
|
||||
// Create a new project first
|
||||
await page.getByTestId("new-project-card").click();
|
||||
await expect(page.getByTestId("new-project-dialog")).toBeVisible();
|
||||
|
||||
// Enter project details
|
||||
await page.getByTestId("project-name-input").fill("test-session-project");
|
||||
await page.getByTestId("project-path-input").fill("/Users/test/session-projects");
|
||||
|
||||
// Click create
|
||||
await page.getByTestId("confirm-create-project").click();
|
||||
|
||||
// Should navigate to board view
|
||||
await expect(page.getByTestId("board-view")).toBeVisible();
|
||||
|
||||
// Navigate to Agent view
|
||||
await page.getByTestId("nav-agent").click();
|
||||
await expect(page.getByTestId("agent-view")).toBeVisible();
|
||||
});
|
||||
|
||||
test("clicking new session button creates a session with random name", async ({ page }) => {
|
||||
// Click the "New" session button
|
||||
const newSessionButton = page.getByTestId("new-session-button");
|
||||
await expect(newSessionButton).toBeVisible();
|
||||
await newSessionButton.click();
|
||||
|
||||
// Wait for the session to be created - check for session item in the list
|
||||
const sessionList = page.getByTestId("session-list");
|
||||
await expect(sessionList).toBeVisible();
|
||||
|
||||
// The session should appear in the list
|
||||
await expect(sessionList.locator('[data-testid^="session-item-"]').first()).toBeVisible({ timeout: 5000 });
|
||||
|
||||
// The session name should follow the pattern of random names (contains letters and numbers)
|
||||
const sessionName = sessionList.locator('[data-testid^="session-item-"]').first().locator("h3");
|
||||
await expect(sessionName).toBeVisible();
|
||||
const nameText = await sessionName.textContent();
|
||||
expect(nameText).toBeTruthy();
|
||||
// Verify the name follows our pattern: "Adjective Noun Number"
|
||||
expect(nameText).toMatch(/^[A-Z][a-z]+ [A-Z][a-z]+ \d+$/);
|
||||
});
|
||||
|
||||
test("verify session was created and selected", async ({ page }) => {
|
||||
// Click the "New" session button
|
||||
const newSessionButton = page.getByTestId("new-session-button");
|
||||
await newSessionButton.click();
|
||||
|
||||
// Wait for session to be created
|
||||
const sessionList = page.getByTestId("session-list");
|
||||
await expect(sessionList.locator('[data-testid^="session-item-"]').first()).toBeVisible({ timeout: 5000 });
|
||||
|
||||
// Verify the session is selected (has the primary border class)
|
||||
const sessionItem = sessionList.locator('[data-testid^="session-item-"]').first();
|
||||
await expect(sessionItem).toHaveClass(/border-primary/);
|
||||
|
||||
// Verify the message list is visible (session is active)
|
||||
await expect(page.getByTestId("message-list")).toBeVisible();
|
||||
});
|
||||
|
||||
test("verify chat input is focused after creating new session", async ({ page }) => {
|
||||
// Click the "New" session button
|
||||
const newSessionButton = page.getByTestId("new-session-button");
|
||||
await newSessionButton.click();
|
||||
|
||||
// Wait for session to be created
|
||||
const sessionList = page.getByTestId("session-list");
|
||||
await expect(sessionList.locator('[data-testid^="session-item-"]').first()).toBeVisible({ timeout: 5000 });
|
||||
|
||||
// Wait for the input to be focused (there's a 200ms delay in the code)
|
||||
await page.waitForTimeout(300);
|
||||
|
||||
// Verify the chat input is focused
|
||||
const chatInput = page.getByTestId("agent-input");
|
||||
await expect(chatInput).toBeVisible();
|
||||
await expect(chatInput).toBeFocused();
|
||||
});
|
||||
|
||||
test("complete flow: click new session, verify session created, verify input focused", async ({ page }) => {
|
||||
// Step 1: Click new session
|
||||
const newSessionButton = page.getByTestId("new-session-button");
|
||||
await expect(newSessionButton).toBeVisible();
|
||||
await newSessionButton.click();
|
||||
|
||||
// Step 2: Verify session was created
|
||||
const sessionList = page.getByTestId("session-list");
|
||||
await expect(sessionList.locator('[data-testid^="session-item-"]').first()).toBeVisible({ timeout: 5000 });
|
||||
|
||||
// Verify the session has a randomly generated name
|
||||
const sessionName = sessionList.locator('[data-testid^="session-item-"]').first().locator("h3");
|
||||
const nameText = await sessionName.textContent();
|
||||
expect(nameText).toBeTruthy();
|
||||
expect(nameText).toMatch(/^[A-Z][a-z]+ [A-Z][a-z]+ \d+$/);
|
||||
|
||||
// Step 3: Verify chat input focused
|
||||
await page.waitForTimeout(300);
|
||||
const chatInput = page.getByTestId("agent-input");
|
||||
await expect(chatInput).toBeFocused();
|
||||
});
|
||||
});
|
||||
@@ -1,237 +0,0 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
|
||||
test.describe("New Project Workflow", () => {
|
||||
test("opens new project dialog when clicking Create Project", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto("/");
|
||||
|
||||
// Click the New Project card
|
||||
await page.getByTestId("new-project-card").click();
|
||||
|
||||
// Dialog should appear
|
||||
await expect(page.getByTestId("new-project-dialog")).toBeVisible();
|
||||
await expect(page.getByText("Create New Project")).toBeVisible();
|
||||
});
|
||||
|
||||
test("shows project name and directory inputs", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
|
||||
// Open dialog
|
||||
await page.getByTestId("new-project-card").click();
|
||||
|
||||
// Check inputs exist
|
||||
await expect(page.getByTestId("project-name-input")).toBeVisible();
|
||||
await expect(page.getByTestId("project-path-input")).toBeVisible();
|
||||
await expect(page.getByTestId("browse-directory")).toBeVisible();
|
||||
});
|
||||
|
||||
test("create button is disabled without name and path", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
|
||||
// Open dialog
|
||||
await page.getByTestId("new-project-card").click();
|
||||
|
||||
// Create button should be disabled
|
||||
await expect(page.getByTestId("confirm-create-project")).toBeDisabled();
|
||||
});
|
||||
|
||||
test("can enter project name", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
|
||||
// Open dialog
|
||||
await page.getByTestId("new-project-card").click();
|
||||
|
||||
// Enter project name
|
||||
await page.getByTestId("project-name-input").fill("my-test-project");
|
||||
await expect(page.getByTestId("project-name-input")).toHaveValue(
|
||||
"my-test-project"
|
||||
);
|
||||
});
|
||||
|
||||
test("can close dialog with cancel button", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
|
||||
// Open dialog
|
||||
await page.getByTestId("new-project-card").click();
|
||||
await expect(page.getByTestId("new-project-dialog")).toBeVisible();
|
||||
|
||||
// Close with cancel
|
||||
await page.getByRole("button", { name: "Cancel" }).click();
|
||||
await expect(page.getByTestId("new-project-dialog")).not.toBeVisible();
|
||||
});
|
||||
|
||||
test("create button enables when name and path are entered", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto("/");
|
||||
|
||||
// Open dialog
|
||||
await page.getByTestId("new-project-card").click();
|
||||
|
||||
// Create button should be disabled initially
|
||||
await expect(page.getByTestId("confirm-create-project")).toBeDisabled();
|
||||
|
||||
// Enter project name
|
||||
await page.getByTestId("project-name-input").fill("my-test-project");
|
||||
|
||||
// Still disabled (no path)
|
||||
await expect(page.getByTestId("confirm-create-project")).toBeDisabled();
|
||||
|
||||
// Enter path
|
||||
await page.getByTestId("project-path-input").fill("/Users/test/projects");
|
||||
|
||||
// Now should be enabled
|
||||
await expect(page.getByTestId("confirm-create-project")).toBeEnabled();
|
||||
});
|
||||
|
||||
test("creates project and navigates to board view", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
|
||||
// Open dialog
|
||||
await page.getByTestId("new-project-card").click();
|
||||
await expect(page.getByTestId("new-project-dialog")).toBeVisible();
|
||||
|
||||
// Enter project details
|
||||
await page.getByTestId("project-name-input").fill("test-new-project");
|
||||
await page.getByTestId("project-path-input").fill("/Users/test/projects");
|
||||
|
||||
// Click create
|
||||
await page.getByTestId("confirm-create-project").click();
|
||||
|
||||
// Dialog should close
|
||||
await expect(page.getByTestId("new-project-dialog")).not.toBeVisible();
|
||||
|
||||
// Should navigate to board view with the project
|
||||
await expect(page.getByTestId("board-view")).toBeVisible();
|
||||
|
||||
// Project name should be displayed in the board view header
|
||||
await expect(
|
||||
page.getByTestId("board-view").getByText("test-new-project")
|
||||
).toBeVisible();
|
||||
|
||||
// Kanban columns should be visible
|
||||
await expect(page.getByText("Backlog")).toBeVisible();
|
||||
await expect(page.getByText("In Progress")).toBeVisible();
|
||||
await expect(page.getByText("Verified")).toBeVisible();
|
||||
});
|
||||
|
||||
test("created project appears in recent projects on welcome view", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto("/");
|
||||
|
||||
// Create a project
|
||||
await page.getByTestId("new-project-card").click();
|
||||
await page.getByTestId("project-name-input").fill("recent-project-test");
|
||||
await page.getByTestId("project-path-input").fill("/Users/test/projects");
|
||||
await page.getByTestId("confirm-create-project").click();
|
||||
|
||||
// Verify we're on board view
|
||||
await expect(page.getByTestId("board-view")).toBeVisible();
|
||||
|
||||
// Go back to welcome view by clicking Automaker title (if there's a way)
|
||||
// For now, reload the page and check recent projects
|
||||
await page.goto("/");
|
||||
|
||||
// The project should appear in recent projects section (use role to be specific)
|
||||
await expect(
|
||||
page.getByRole("heading", { name: "Recent Projects" })
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page
|
||||
.getByTestId("welcome-view")
|
||||
.getByText("recent-project-test", { exact: true })
|
||||
).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Open Project Workflow", () => {
|
||||
test("clicking Open Project triggers directory selection", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto("/");
|
||||
|
||||
// In web mode, clicking Open Project card will show a prompt dialog
|
||||
// We can't fully test native dialogs, but we can verify the click works
|
||||
await expect(page.getByTestId("open-project-card")).toBeVisible();
|
||||
});
|
||||
|
||||
test("opens existing project and navigates to board view", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto("/");
|
||||
|
||||
// Mock the window.prompt response
|
||||
await page.evaluate(() => {
|
||||
window.prompt = () => "/mock/existing-project";
|
||||
});
|
||||
|
||||
// Click Open Project card
|
||||
await page.getByTestId("open-project-card").click();
|
||||
|
||||
// Should navigate to board view
|
||||
await expect(page.getByTestId("board-view")).toBeVisible();
|
||||
|
||||
// Project name should be derived from path
|
||||
await expect(
|
||||
page.getByTestId("board-view").getByText("existing-project")
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test("opened project loads into dashboard with features", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto("/");
|
||||
|
||||
// Mock the window.prompt response
|
||||
await page.evaluate(() => {
|
||||
window.prompt = () => "/mock/existing-project";
|
||||
});
|
||||
|
||||
// Click Open Project
|
||||
await page.getByTestId("open-project-card").click();
|
||||
|
||||
// Should show board view
|
||||
await expect(page.getByTestId("board-view")).toBeVisible();
|
||||
|
||||
// Should have loaded features from the mock .automaker/feature_list.json
|
||||
// The mock returns "Sample Feature" in backlog
|
||||
await expect(
|
||||
page.getByTestId("kanban-column-backlog").getByText("Sample Feature")
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test("can click on recent project to reopen it", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
|
||||
// First, create a project to have it in recent projects
|
||||
await page.getByTestId("new-project-card").click();
|
||||
await page.getByTestId("project-name-input").fill("reopenable-project");
|
||||
await page.getByTestId("project-path-input").fill("/Users/test/projects");
|
||||
await page.getByTestId("confirm-create-project").click();
|
||||
|
||||
// Verify on board view
|
||||
await expect(page.getByTestId("board-view")).toBeVisible();
|
||||
|
||||
// Go back to welcome view
|
||||
await page.goto("/");
|
||||
|
||||
// Wait for recent projects to appear
|
||||
await expect(
|
||||
page.getByRole("heading", { name: "Recent Projects" })
|
||||
).toBeVisible();
|
||||
|
||||
// Click on the recent project
|
||||
const recentProjectCard = page
|
||||
.getByText("reopenable-project", { exact: true })
|
||||
.first();
|
||||
await recentProjectCard.click();
|
||||
|
||||
// Should navigate to board view with that project
|
||||
await expect(page.getByTestId("board-view")).toBeVisible();
|
||||
await expect(
|
||||
page.getByTestId("board-view").getByText("reopenable-project")
|
||||
).toBeVisible();
|
||||
});
|
||||
});
|
||||
@@ -1,112 +0,0 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
|
||||
test.describe("Settings - API Key Management", () => {
|
||||
test("can navigate to settings page", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
|
||||
// Click settings button in sidebar
|
||||
await page.getByTestId("settings-button").click();
|
||||
|
||||
// Should show settings view
|
||||
await expect(page.getByTestId("settings-view")).toBeVisible();
|
||||
await expect(page.getByRole("heading", { name: "Settings" })).toBeVisible();
|
||||
await expect(page.getByText("API Keys", { exact: true })).toBeVisible();
|
||||
});
|
||||
|
||||
test("shows Anthropic and Google API key inputs", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await page.getByTestId("settings-button").click();
|
||||
|
||||
// Check input fields exist
|
||||
await expect(page.getByTestId("anthropic-api-key-input")).toBeVisible();
|
||||
await expect(page.getByTestId("google-api-key-input")).toBeVisible();
|
||||
});
|
||||
|
||||
test("can enter and save Anthropic API key", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await page.getByTestId("settings-button").click();
|
||||
|
||||
// Enter API key
|
||||
await page.getByTestId("anthropic-api-key-input").fill("sk-ant-test-key-123");
|
||||
|
||||
// Save
|
||||
await page.getByTestId("save-settings").click();
|
||||
|
||||
// Should show saved confirmation
|
||||
await expect(page.getByText("Saved!")).toBeVisible();
|
||||
|
||||
// Reload page and verify key persists
|
||||
await page.reload();
|
||||
await page.getByTestId("settings-button").click();
|
||||
|
||||
// Toggle visibility to see the key
|
||||
await page.getByTestId("toggle-anthropic-visibility").click();
|
||||
await expect(page.getByTestId("anthropic-api-key-input")).toHaveValue("sk-ant-test-key-123");
|
||||
});
|
||||
|
||||
test("can enter and save Google API key", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await page.getByTestId("settings-button").click();
|
||||
|
||||
// Enter API key
|
||||
await page.getByTestId("google-api-key-input").fill("AIzaSyTest123");
|
||||
|
||||
// Save
|
||||
await page.getByTestId("save-settings").click();
|
||||
|
||||
// Reload page and verify key persists
|
||||
await page.reload();
|
||||
await page.getByTestId("settings-button").click();
|
||||
|
||||
// Toggle visibility
|
||||
await page.getByTestId("toggle-google-visibility").click();
|
||||
await expect(page.getByTestId("google-api-key-input")).toHaveValue("AIzaSyTest123");
|
||||
});
|
||||
|
||||
test("API key inputs are password type by default", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await page.getByTestId("settings-button").click();
|
||||
|
||||
// Check input types are password
|
||||
await expect(page.getByTestId("anthropic-api-key-input")).toHaveAttribute("type", "password");
|
||||
await expect(page.getByTestId("google-api-key-input")).toHaveAttribute("type", "password");
|
||||
});
|
||||
|
||||
test("can toggle API key visibility", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await page.getByTestId("settings-button").click();
|
||||
|
||||
// Initially password type
|
||||
await expect(page.getByTestId("anthropic-api-key-input")).toHaveAttribute("type", "password");
|
||||
|
||||
// Toggle visibility
|
||||
await page.getByTestId("toggle-anthropic-visibility").click();
|
||||
|
||||
// Now should be text type
|
||||
await expect(page.getByTestId("anthropic-api-key-input")).toHaveAttribute("type", "text");
|
||||
|
||||
// Toggle back
|
||||
await page.getByTestId("toggle-anthropic-visibility").click();
|
||||
await expect(page.getByTestId("anthropic-api-key-input")).toHaveAttribute("type", "password");
|
||||
});
|
||||
|
||||
test("can navigate back to home from settings", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await page.getByTestId("settings-button").click();
|
||||
|
||||
// Click back to home
|
||||
await page.getByTestId("back-to-home").click();
|
||||
|
||||
// Should be back on welcome view
|
||||
await expect(page.getByTestId("welcome-view")).toBeVisible();
|
||||
});
|
||||
|
||||
test("shows security notice about local storage", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await page.getByTestId("settings-button").click();
|
||||
|
||||
// Should show security notice
|
||||
await expect(page.getByText("Security Notice")).toBeVisible();
|
||||
await expect(page.getByText(/stored in your browser's local storage/i)).toBeVisible();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user