mirror of
https://github.com/johndoe6345789/low-code-react-app-b.git
synced 2026-04-24 13:44:54 +00:00
Merge pull request #33 from johndoe6345789/codex/refactor-playwrightdesigner-into-subcomponents
Refactor PlaywrightDesigner: split into subcomponents and externalize UI copy
This commit is contained in:
@@ -2,15 +2,12 @@
|
||||
|
||||
import { useState } from 'react'
|
||||
import { PlaywrightTest, PlaywrightStep } from '@/types/project'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import { Plus, Trash, Play, Sparkle } from '@phosphor-icons/react'
|
||||
import { Play, Plus } from '@phosphor-icons/react'
|
||||
import { toast } from 'sonner'
|
||||
import copy from '@/data/playwright-designer.json'
|
||||
import { TestList } from '@/components/playwright-designer/TestList'
|
||||
import { TestEditor } from '@/components/playwright-designer/TestEditor'
|
||||
|
||||
interface PlaywrightDesignerProps {
|
||||
tests: PlaywrightTest[]
|
||||
@@ -24,7 +21,7 @@ export function PlaywrightDesigner({ tests, onTestsChange }: PlaywrightDesignerP
|
||||
const handleAddTest = () => {
|
||||
const newTest: PlaywrightTest = {
|
||||
id: `test-${Date.now()}`,
|
||||
name: 'New Test',
|
||||
name: copy.defaults.newTestName,
|
||||
description: '',
|
||||
pageUrl: '/',
|
||||
steps: []
|
||||
@@ -34,17 +31,15 @@ export function PlaywrightDesigner({ tests, onTestsChange }: PlaywrightDesignerP
|
||||
}
|
||||
|
||||
const handleDeleteTest = (testId: string) => {
|
||||
onTestsChange(tests.filter(t => t.id !== testId))
|
||||
const remaining = tests.filter(test => test.id !== testId)
|
||||
onTestsChange(remaining)
|
||||
if (selectedTestId === testId) {
|
||||
const remaining = tests.filter(t => t.id !== testId)
|
||||
setSelectedTestId(remaining[0]?.id || null)
|
||||
}
|
||||
}
|
||||
|
||||
const handleUpdateTest = (testId: string, updates: Partial<PlaywrightTest>) => {
|
||||
onTestsChange(
|
||||
tests.map(t => t.id === testId ? { ...t, ...updates } : t)
|
||||
)
|
||||
onTestsChange(tests.map(test => (test.id === testId ? { ...test, ...updates } : test)))
|
||||
}
|
||||
|
||||
const handleAddStep = () => {
|
||||
@@ -63,270 +58,66 @@ export function PlaywrightDesigner({ tests, onTestsChange }: PlaywrightDesignerP
|
||||
const handleUpdateStep = (stepId: string, updates: Partial<PlaywrightStep>) => {
|
||||
if (!selectedTest) return
|
||||
handleUpdateTest(selectedTest.id, {
|
||||
steps: selectedTest.steps.map(s => s.id === stepId ? { ...s, ...updates } : s)
|
||||
steps: selectedTest.steps.map(step => (step.id === stepId ? { ...step, ...updates } : step))
|
||||
})
|
||||
}
|
||||
|
||||
const handleDeleteStep = (stepId: string) => {
|
||||
if (!selectedTest) return
|
||||
handleUpdateTest(selectedTest.id, {
|
||||
steps: selectedTest.steps.filter(s => s.id !== stepId)
|
||||
steps: selectedTest.steps.filter(step => step.id !== stepId)
|
||||
})
|
||||
}
|
||||
|
||||
const handleGenerateWithAI = async () => {
|
||||
const description = prompt('Describe the E2E test you want to generate:')
|
||||
const description = prompt(copy.prompts.describeTest)
|
||||
if (!description) return
|
||||
|
||||
try {
|
||||
toast.info('Generating test with AI...')
|
||||
const promptText = `You are a Playwright test generator. Create an E2E test based on: "${description}"
|
||||
|
||||
Return a valid JSON object with a single property "test":
|
||||
{
|
||||
"test": {
|
||||
"id": "unique-id",
|
||||
"name": "Test Name",
|
||||
"description": "What this test does",
|
||||
"pageUrl": "/path",
|
||||
"steps": [
|
||||
{
|
||||
"id": "step-id",
|
||||
"action": "navigate" | "click" | "fill" | "expect" | "wait" | "select" | "check" | "uncheck",
|
||||
"selector": "css selector or text",
|
||||
"value": "value for fill/select actions",
|
||||
"assertion": "expected value for expect action",
|
||||
"timeout": 5000
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
Create a complete test flow with appropriate selectors and assertions.`
|
||||
|
||||
toast.info(copy.messages.generating)
|
||||
const promptText = copy.prompts.template.replace('{description}', description)
|
||||
const response = await window.spark.llm(promptText, 'gpt-4o', true)
|
||||
const parsed = JSON.parse(response)
|
||||
onTestsChange([...tests, parsed.test])
|
||||
setSelectedTestId(parsed.test.id)
|
||||
toast.success('Test generated successfully!')
|
||||
toast.success(copy.messages.generated)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
toast.error('Failed to generate test')
|
||||
toast.error(copy.messages.failed)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-full flex">
|
||||
<div className="w-80 border-r border-border bg-card">
|
||||
<div className="p-4 border-b border-border flex items-center justify-between">
|
||||
<h2 className="font-semibold text-sm">E2E Tests</h2>
|
||||
<div className="flex gap-1">
|
||||
<Button size="sm" variant="outline" onClick={handleGenerateWithAI}>
|
||||
<Sparkle size={14} weight="duotone" />
|
||||
</Button>
|
||||
<Button size="sm" onClick={handleAddTest}>
|
||||
<Plus size={14} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<ScrollArea className="h-[calc(100vh-200px)]">
|
||||
<div className="p-2 space-y-1">
|
||||
{tests.map(test => (
|
||||
<div
|
||||
key={test.id}
|
||||
className={`p-3 rounded-md cursor-pointer flex items-start justify-between group ${
|
||||
selectedTestId === test.id ? 'bg-accent text-accent-foreground' : 'hover:bg-muted'
|
||||
}`}
|
||||
onClick={() => setSelectedTestId(test.id)}
|
||||
>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="font-medium text-sm truncate">{test.name}</div>
|
||||
<div className="text-xs text-muted-foreground truncate">{test.pageUrl}</div>
|
||||
<div className="text-xs text-muted-foreground">{test.steps.length} steps</div>
|
||||
</div>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="opacity-0 group-hover:opacity-100"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
handleDeleteTest(test.id)
|
||||
}}
|
||||
>
|
||||
<Trash size={14} />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
{tests.length === 0 && (
|
||||
<div className="p-8 text-center text-sm text-muted-foreground">
|
||||
No tests yet. Click + to create one.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
<TestList
|
||||
tests={tests}
|
||||
selectedTestId={selectedTestId}
|
||||
onSelect={setSelectedTestId}
|
||||
onAddTest={handleAddTest}
|
||||
onDeleteTest={handleDeleteTest}
|
||||
onGenerateWithAI={handleGenerateWithAI}
|
||||
/>
|
||||
|
||||
<div className="flex-1 p-6">
|
||||
{selectedTest ? (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-2xl font-bold">Test Configuration</h2>
|
||||
<Button variant="outline">
|
||||
<Play size={16} className="mr-2" weight="fill" />
|
||||
Run Test
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Test Details</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="test-name">Test Name</Label>
|
||||
<Input
|
||||
id="test-name"
|
||||
value={selectedTest.name}
|
||||
onChange={e => handleUpdateTest(selectedTest.id, { name: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="test-description">Description</Label>
|
||||
<Textarea
|
||||
id="test-description"
|
||||
value={selectedTest.description}
|
||||
onChange={e => handleUpdateTest(selectedTest.id, { description: e.target.value })}
|
||||
placeholder="What does this test verify?"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="test-url">Page URL</Label>
|
||||
<Input
|
||||
id="test-url"
|
||||
value={selectedTest.pageUrl}
|
||||
onChange={e => handleUpdateTest(selectedTest.id, { pageUrl: e.target.value })}
|
||||
placeholder="/login"
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<CardTitle>Test Steps</CardTitle>
|
||||
<CardDescription>Define the actions for this test</CardDescription>
|
||||
</div>
|
||||
<Button size="sm" onClick={handleAddStep}>
|
||||
<Plus size={14} className="mr-1" />
|
||||
Add Step
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ScrollArea className="h-[400px]">
|
||||
<div className="space-y-4">
|
||||
{selectedTest.steps.map((step, index) => (
|
||||
<Card key={step.id}>
|
||||
<CardContent className="pt-4 space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm font-semibold">Step {index + 1}</span>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => handleDeleteStep(step.id)}
|
||||
>
|
||||
<Trash size={14} />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="space-y-2">
|
||||
<Label>Action</Label>
|
||||
<Select
|
||||
value={step.action}
|
||||
onValueChange={(value: any) => handleUpdateStep(step.id, { action: value })}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="navigate">Navigate</SelectItem>
|
||||
<SelectItem value="click">Click</SelectItem>
|
||||
<SelectItem value="fill">Fill</SelectItem>
|
||||
<SelectItem value="expect">Expect</SelectItem>
|
||||
<SelectItem value="wait">Wait</SelectItem>
|
||||
<SelectItem value="select">Select</SelectItem>
|
||||
<SelectItem value="check">Check</SelectItem>
|
||||
<SelectItem value="uncheck">Uncheck</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
{step.action !== 'navigate' && step.action !== 'wait' && (
|
||||
<div className="space-y-2">
|
||||
<Label>Selector</Label>
|
||||
<Input
|
||||
value={step.selector || ''}
|
||||
onChange={e => handleUpdateStep(step.id, { selector: e.target.value })}
|
||||
placeholder="button, #login, [data-testid='submit']"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{(step.action === 'fill' || step.action === 'select') && (
|
||||
<div className="space-y-2 col-span-2">
|
||||
<Label>Value</Label>
|
||||
<Input
|
||||
value={step.value || ''}
|
||||
onChange={e => handleUpdateStep(step.id, { value: e.target.value })}
|
||||
placeholder="Text to enter"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{step.action === 'expect' && (
|
||||
<div className="space-y-2 col-span-2">
|
||||
<Label>Assertion</Label>
|
||||
<Input
|
||||
value={step.assertion || ''}
|
||||
onChange={e => handleUpdateStep(step.id, { assertion: e.target.value })}
|
||||
placeholder="toBeVisible(), toHaveText('...')"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{step.action === 'wait' && (
|
||||
<div className="space-y-2">
|
||||
<Label>Timeout (ms)</Label>
|
||||
<Input
|
||||
type="number"
|
||||
value={step.timeout || 1000}
|
||||
onChange={e => handleUpdateStep(step.id, { timeout: parseInt(e.target.value) })}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
{selectedTest.steps.length === 0 && (
|
||||
<div className="py-12 text-center text-sm text-muted-foreground">
|
||||
No steps yet. Click "Add Step" to create test actions.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
<TestEditor
|
||||
test={selectedTest}
|
||||
onAddStep={handleAddStep}
|
||||
onUpdateTest={updates => handleUpdateTest(selectedTest.id, updates)}
|
||||
onUpdateStep={handleUpdateStep}
|
||||
onDeleteStep={handleDeleteStep}
|
||||
/>
|
||||
) : (
|
||||
<div className="h-full flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<Play size={48} className="mx-auto mb-4 text-muted-foreground" />
|
||||
<p className="text-lg font-medium mb-2">No test selected</p>
|
||||
<p className="text-lg font-medium mb-2">{copy.emptyStates.noTestSelectedTitle}</p>
|
||||
<p className="text-sm text-muted-foreground mb-4">
|
||||
Create or select a test to configure
|
||||
{copy.emptyStates.noTestSelectedBody}
|
||||
</p>
|
||||
<Button onClick={handleAddTest}>
|
||||
<Plus size={16} className="mr-2" />
|
||||
Create Test
|
||||
{copy.buttons.createTest}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
103
src/components/playwright-designer/StepEditor.tsx
Normal file
103
src/components/playwright-designer/StepEditor.tsx
Normal file
@@ -0,0 +1,103 @@
|
||||
import { PlaywrightStep } from '@/types/project'
|
||||
import { Card, CardContent } from '@/components/ui/card'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
||||
import { Trash } from '@phosphor-icons/react'
|
||||
import copy from '@/data/playwright-designer.json'
|
||||
|
||||
interface StepEditorProps {
|
||||
step: PlaywrightStep
|
||||
index: number
|
||||
onUpdate: (stepId: string, updates: Partial<PlaywrightStep>) => void
|
||||
onDelete: (stepId: string) => void
|
||||
}
|
||||
|
||||
const actionOptions = Object.entries(copy.actions)
|
||||
|
||||
export function StepEditor({ step, index, onUpdate, onDelete }: StepEditorProps) {
|
||||
const showSelector = step.action !== 'navigate' && step.action !== 'wait'
|
||||
const showValue = step.action === 'fill' || step.action === 'select'
|
||||
const showAssertion = step.action === 'expect'
|
||||
const showTimeout = step.action === 'wait'
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardContent className="pt-4 space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm font-semibold">
|
||||
{copy.labels.stepPrefix} {index + 1}
|
||||
</span>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => onDelete(step.id)}
|
||||
>
|
||||
<Trash size={14} />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="space-y-2">
|
||||
<Label>{copy.fields.action}</Label>
|
||||
<Select
|
||||
value={step.action}
|
||||
onValueChange={value => onUpdate(step.id, { action: value as PlaywrightStep['action'] })}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{actionOptions.map(([value, label]) => (
|
||||
<SelectItem key={value} value={value}>
|
||||
{label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
{showSelector && (
|
||||
<div className="space-y-2">
|
||||
<Label>{copy.fields.selector}</Label>
|
||||
<Input
|
||||
value={step.selector || ''}
|
||||
onChange={e => onUpdate(step.id, { selector: e.target.value })}
|
||||
placeholder={copy.placeholders.selector}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{showValue && (
|
||||
<div className="space-y-2 col-span-2">
|
||||
<Label>{copy.fields.value}</Label>
|
||||
<Input
|
||||
value={step.value || ''}
|
||||
onChange={e => onUpdate(step.id, { value: e.target.value })}
|
||||
placeholder={copy.placeholders.value}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{showAssertion && (
|
||||
<div className="space-y-2 col-span-2">
|
||||
<Label>{copy.fields.assertion}</Label>
|
||||
<Input
|
||||
value={step.assertion || ''}
|
||||
onChange={e => onUpdate(step.id, { assertion: e.target.value })}
|
||||
placeholder={copy.placeholders.assertion}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{showTimeout && (
|
||||
<div className="space-y-2">
|
||||
<Label>{copy.fields.timeout}</Label>
|
||||
<Input
|
||||
type="number"
|
||||
value={step.timeout ?? 1000}
|
||||
onChange={e => onUpdate(step.id, { timeout: Number.parseInt(e.target.value, 10) })}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
107
src/components/playwright-designer/TestEditor.tsx
Normal file
107
src/components/playwright-designer/TestEditor.tsx
Normal file
@@ -0,0 +1,107 @@
|
||||
import { PlaywrightStep, PlaywrightTest } from '@/types/project'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import { Plus, Play } from '@phosphor-icons/react'
|
||||
import copy from '@/data/playwright-designer.json'
|
||||
import { StepEditor } from './StepEditor'
|
||||
|
||||
interface TestEditorProps {
|
||||
test: PlaywrightTest
|
||||
onAddStep: () => void
|
||||
onUpdateTest: (updates: Partial<PlaywrightTest>) => void
|
||||
onUpdateStep: (stepId: string, updates: Partial<PlaywrightStep>) => void
|
||||
onDeleteStep: (stepId: string) => void
|
||||
}
|
||||
|
||||
export function TestEditor({
|
||||
test,
|
||||
onAddStep,
|
||||
onUpdateTest,
|
||||
onUpdateStep,
|
||||
onDeleteStep
|
||||
}: TestEditorProps) {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-2xl font-bold">{copy.headers.testConfiguration}</h2>
|
||||
<Button variant="outline">
|
||||
<Play size={16} className="mr-2" weight="fill" />
|
||||
{copy.buttons.runTest}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{copy.headers.testDetails}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="test-name">{copy.fields.testName}</Label>
|
||||
<Input
|
||||
id="test-name"
|
||||
value={test.name}
|
||||
onChange={e => onUpdateTest({ name: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="test-description">{copy.fields.description}</Label>
|
||||
<Textarea
|
||||
id="test-description"
|
||||
value={test.description}
|
||||
onChange={e => onUpdateTest({ description: e.target.value })}
|
||||
placeholder={copy.placeholders.description}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="test-url">{copy.fields.pageUrl}</Label>
|
||||
<Input
|
||||
id="test-url"
|
||||
value={test.pageUrl}
|
||||
onChange={e => onUpdateTest({ pageUrl: e.target.value })}
|
||||
placeholder={copy.placeholders.pageUrl}
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<CardTitle>{copy.headers.testSteps}</CardTitle>
|
||||
<CardDescription>{copy.descriptions.testSteps}</CardDescription>
|
||||
</div>
|
||||
<Button size="sm" onClick={onAddStep}>
|
||||
<Plus size={14} className="mr-1" />
|
||||
{copy.buttons.addStep}
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ScrollArea className="h-[400px]">
|
||||
<div className="space-y-4">
|
||||
{test.steps.map((step, index) => (
|
||||
<StepEditor
|
||||
key={step.id}
|
||||
step={step}
|
||||
index={index}
|
||||
onUpdate={onUpdateStep}
|
||||
onDelete={onDeleteStep}
|
||||
/>
|
||||
))}
|
||||
{test.steps.length === 0 && (
|
||||
<div className="py-12 text-center text-sm text-muted-foreground">
|
||||
{copy.emptyStates.noSteps}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
76
src/components/playwright-designer/TestList.tsx
Normal file
76
src/components/playwright-designer/TestList.tsx
Normal file
@@ -0,0 +1,76 @@
|
||||
import { PlaywrightTest } from '@/types/project'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||
import { Plus, Sparkle, Trash } from '@phosphor-icons/react'
|
||||
import copy from '@/data/playwright-designer.json'
|
||||
|
||||
interface TestListProps {
|
||||
tests: PlaywrightTest[]
|
||||
selectedTestId: string | null
|
||||
onSelect: (testId: string) => void
|
||||
onAddTest: () => void
|
||||
onDeleteTest: (testId: string) => void
|
||||
onGenerateWithAI: () => void
|
||||
}
|
||||
|
||||
export function TestList({
|
||||
tests,
|
||||
selectedTestId,
|
||||
onSelect,
|
||||
onAddTest,
|
||||
onDeleteTest,
|
||||
onGenerateWithAI
|
||||
}: TestListProps) {
|
||||
return (
|
||||
<div className="w-80 border-r border-border bg-card">
|
||||
<div className="p-4 border-b border-border flex items-center justify-between">
|
||||
<h2 className="font-semibold text-sm">{copy.headers.tests}</h2>
|
||||
<div className="flex gap-1">
|
||||
<Button size="sm" variant="outline" onClick={onGenerateWithAI}>
|
||||
<Sparkle size={14} weight="duotone" />
|
||||
</Button>
|
||||
<Button size="sm" onClick={onAddTest}>
|
||||
<Plus size={14} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<ScrollArea className="h-[calc(100vh-200px)]">
|
||||
<div className="p-2 space-y-1">
|
||||
{tests.map(test => (
|
||||
<div
|
||||
key={test.id}
|
||||
className={`p-3 rounded-md cursor-pointer flex items-start justify-between group ${
|
||||
selectedTestId === test.id ? 'bg-accent text-accent-foreground' : 'hover:bg-muted'
|
||||
}`}
|
||||
onClick={() => onSelect(test.id)}
|
||||
>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="font-medium text-sm truncate">{test.name}</div>
|
||||
<div className="text-xs text-muted-foreground truncate">{test.pageUrl}</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{test.steps.length} {copy.labels.steps}
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="opacity-0 group-hover:opacity-100"
|
||||
onClick={event => {
|
||||
event.stopPropagation()
|
||||
onDeleteTest(test.id)
|
||||
}}
|
||||
>
|
||||
<Trash size={14} />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
{tests.length === 0 && (
|
||||
<div className="p-8 text-center text-sm text-muted-foreground">
|
||||
{copy.emptyStates.noTests}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
65
src/data/playwright-designer.json
Normal file
65
src/data/playwright-designer.json
Normal file
@@ -0,0 +1,65 @@
|
||||
{
|
||||
"actions": {
|
||||
"navigate": "Navigate",
|
||||
"click": "Click",
|
||||
"fill": "Fill",
|
||||
"expect": "Expect",
|
||||
"wait": "Wait",
|
||||
"select": "Select",
|
||||
"check": "Check",
|
||||
"uncheck": "Uncheck"
|
||||
},
|
||||
"buttons": {
|
||||
"addStep": "Add Step",
|
||||
"createTest": "Create Test",
|
||||
"runTest": "Run Test"
|
||||
},
|
||||
"defaults": {
|
||||
"newTestName": "New Test"
|
||||
},
|
||||
"descriptions": {
|
||||
"testSteps": "Define the actions for this test"
|
||||
},
|
||||
"emptyStates": {
|
||||
"noSteps": "No steps yet. Click \"Add Step\" to create test actions.",
|
||||
"noTestSelectedBody": "Create or select a test to configure",
|
||||
"noTestSelectedTitle": "No test selected",
|
||||
"noTests": "No tests yet. Click + to create one."
|
||||
},
|
||||
"fields": {
|
||||
"action": "Action",
|
||||
"assertion": "Assertion",
|
||||
"description": "Description",
|
||||
"pageUrl": "Page URL",
|
||||
"selector": "Selector",
|
||||
"testName": "Test Name",
|
||||
"timeout": "Timeout (ms)",
|
||||
"value": "Value"
|
||||
},
|
||||
"headers": {
|
||||
"testConfiguration": "Test Configuration",
|
||||
"testDetails": "Test Details",
|
||||
"testSteps": "Test Steps",
|
||||
"tests": "E2E Tests"
|
||||
},
|
||||
"labels": {
|
||||
"steps": "steps",
|
||||
"stepPrefix": "Step"
|
||||
},
|
||||
"messages": {
|
||||
"generated": "Test generated successfully!",
|
||||
"generating": "Generating test with AI...",
|
||||
"failed": "Failed to generate test"
|
||||
},
|
||||
"placeholders": {
|
||||
"assertion": "toBeVisible(), toHaveText('...')",
|
||||
"description": "What does this test verify?",
|
||||
"pageUrl": "/login",
|
||||
"selector": "button, #login, [data-testid='submit']",
|
||||
"value": "Text to enter"
|
||||
},
|
||||
"prompts": {
|
||||
"describeTest": "Describe the E2E test you want to generate:",
|
||||
"template": "You are a Playwright test generator. Create an E2E test based on: \"{description}\"\n\nReturn a valid JSON object with a single property \"test\":\n{\n \"test\": {\n \"id\": \"unique-id\",\n \"name\": \"Test Name\",\n \"description\": \"What this test does\",\n \"pageUrl\": \"/path\",\n \"steps\": [\n {\n \"id\": \"step-id\",\n \"action\": \"navigate\" | \"click\" | \"fill\" | \"expect\" | \"wait\" | \"select\" | \"check\" | \"uncheck\",\n \"selector\": \"css selector or text\",\n \"value\": \"value for fill/select actions\",\n \"assertion\": \"expected value for expect action\",\n \"timeout\": 5000\n }\n ]\n }\n}\n\nCreate a complete test flow with appropriate selectors and assertions."
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user