Skip to content

Commit 5d5fb23

Browse files
committed
Add Suite Live View, Add Test Duplication
1 parent 8a4d3c4 commit 5d5fb23

File tree

7 files changed

+128
-15
lines changed

7 files changed

+128
-15
lines changed

eslint.config.mjs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,15 @@ const eslintConfig = [
6969
caughtErrorsIgnorePattern: '^_$',
7070
},
7171
],
72+
73+
'eslint/no-unused-vars': [
74+
'error',
75+
{
76+
argsIgnorePattern: '^_$',
77+
varsIgnorePattern: '^_$',
78+
caughtErrorsIgnorePattern: '^_$',
79+
},
80+
],
7281
},
7382
},
7483
]

src/app/page.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ function CreateSuiteDialog() {
8787
<Dialog>
8888
<DialogTrigger asChild>
8989
<Button variant="outline" className="ml-auto">
90-
<Plus className="w-4 h-4" />
90+
<Plus className="size-4" />
9191
Create Suite
9292
</Button>
9393
</DialogTrigger>
@@ -130,7 +130,7 @@ function ImportSuiteDialog() {
130130
<Dialog>
131131
<DialogTrigger asChild>
132132
<Button variant="outline" className="ml-auto">
133-
<Plus className="w-4 h-4" />
133+
<Plus className="size-4" />
134134
Import Suite
135135
</Button>
136136
</DialogTrigger>

src/app/suite/[suiteId]/actions.tsx

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,3 +193,39 @@ export async function setNotificationsEmailAddressAction(suiteId: number, formDa
193193
revalidatePath(`/suite/${data.suiteId}`)
194194
redirect(`/suite/${data.suiteId}`, RedirectType.push)
195195
}
196+
197+
export async function duplicateTestAction(suiteId: number, testId: number, _form: FormData) {
198+
const test = await db.query.test.findFirst({
199+
where: eq(schema.test.id, testId),
200+
with: {
201+
steps: true,
202+
},
203+
})
204+
205+
if (!test) {
206+
throw new Error(`Test not found: ${testId}`)
207+
}
208+
209+
const _ = await db.transaction(async (tx) => {
210+
const [newTest] = await tx
211+
.insert(schema.test)
212+
.values({
213+
label: `${test.label} (Copy)`,
214+
evaluation: test.evaluation,
215+
suiteId: suiteId,
216+
})
217+
.returning()
218+
219+
await tx.insert(schema.testStep).values(
220+
test.steps.map((step) => ({
221+
testId: newTest.id,
222+
description: step.description,
223+
order: step.order,
224+
})),
225+
)
226+
227+
return newTest
228+
})
229+
230+
revalidatePath(`/suite/${suiteId}`)
231+
}

src/app/suite/[suiteId]/page.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { SuiteDetails } from '@/components/suite/SuiteDetails'
55
import {
66
createTestAction,
77
deleteSuiteAction,
8+
duplicateTestAction,
89
runSuiteAction,
910
setCronCadenceAction,
1011
setNotificationsEmailAddressAction,
@@ -31,6 +32,7 @@ export default async function SuitePage({ params }: { params: Promise<{ suiteId:
3132
const createTest = createTestAction.bind(null, suiteIdNum)
3233
const setCronCadence = setCronCadenceAction.bind(null, suiteIdNum)
3334
const setNotificationsEmailAddress = setNotificationsEmailAddressAction.bind(null, suiteIdNum)
35+
const duplicateTest = duplicateTestAction.bind(null, suiteIdNum)
3436

3537
return (
3638
<SuiteDetails
@@ -40,6 +42,7 @@ export default async function SuitePage({ params }: { params: Promise<{ suiteId:
4042
createTest={createTest}
4143
setCronCadence={setCronCadence}
4244
setNotificationsEmailAddress={setNotificationsEmailAddress}
45+
duplicateTest={duplicateTest}
4346
/>
4447
)
4548
}

src/components/suite/SuiteDetails.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,13 +33,15 @@ export function SuiteDetails({
3333
createTest,
3434
setCronCadence,
3535
setNotificationsEmailAddress,
36+
duplicateTest,
3637
}: {
3738
suite: TSuite
3839
runSuite: (formData: FormData) => Promise<void>
3940
deleteSuite: (formData: FormData) => Promise<void>
4041
createTest: (formData: FormData) => Promise<void>
4142
setCronCadence: (cadence: 'hourly' | 'daily' | null, formData: FormData) => Promise<void>
4243
setNotificationsEmailAddress: (formData: FormData) => Promise<void>
44+
duplicateTest: (testId: number, formData: FormData) => Promise<void>
4345
}) {
4446
const [_cadence, _setCadence] = useState<'hourly' | 'daily' | null>(suite.cronCadence)
4547

@@ -90,7 +92,7 @@ export function SuiteDetails({
9092
actions={[<CreateTestDialog key="create-test-dialog" suiteId={suite.id} createTest={createTest} />]}
9193
/>
9294

93-
<TestsTab suite={suite} suiteId={suite.id} />
95+
<TestsTab suite={suite} suiteId={suite.id} duplicate={duplicateTest} />
9496
</div>
9597

9698
<div className="col-span-1">
@@ -99,7 +101,7 @@ export function SuiteDetails({
99101
actions={[
100102
<form key="run-suite-form" action={runSuite}>
101103
<Button type="submit" variant="outline">
102-
<Play className="w-4 h-4" />
104+
<Play className="size-4" />
103105
Run Suite
104106
</Button>
105107
,
@@ -181,7 +183,7 @@ function CreateTestDialog({ createTest }: { suiteId: number; createTest: (formDa
181183
<Dialog>
182184
<DialogTrigger asChild>
183185
<Button className="ml-auto" variant="outline">
184-
<Plus className="w-4 h-4" />
186+
<Plus className="size-4" />
185187
Create Test
186188
</Button>
187189
</DialogTrigger>

src/components/suite/TestsTab.tsx

Lines changed: 40 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,32 @@
11
'use client'
22

3+
import { CopyPlus } from 'lucide-react'
34
import Link from 'next/link'
45

56
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
67

78
import type { TSuite } from '../../app/suite/[suiteId]/loader'
8-
import { formatDate } from '../shared/utils'
9+
import { Button } from '../ui/button'
910

1011
/**
1112
* Shows a list of tests in a suite.
1213
*/
13-
export function TestsTab({ suite, suiteId }: { suite: TSuite; suiteId: number }) {
14+
export function TestsTab({
15+
suite,
16+
suiteId,
17+
18+
duplicate,
19+
}: {
20+
suite: TSuite
21+
suiteId: number
22+
23+
duplicate: (testId: number, formData: FormData) => void
24+
}) {
1425
return (
1526
<Table>
1627
<TableHeader>
1728
<TableRow>
1829
<TableHead>Test</TableHead>
19-
<TableHead>Created At</TableHead>
2030
<TableHead>{/* Actions */}</TableHead>
2131
</TableRow>
2232
</TableHeader>
@@ -30,15 +40,35 @@ export function TestsTab({ suite, suiteId }: { suite: TSuite; suiteId: number })
3040
)}
3141

3242
{suite.tests.map((test) => (
33-
<TableRow key={test.id}>
34-
<TableCell>{test.label}</TableCell>
35-
<TableCell suppressHydrationWarning>{formatDate(test.createdAt)}</TableCell>
36-
<TableCell className="text-right">
37-
<Link href={`/suite/${suiteId}/test/${test.id}`}>View</Link>
38-
</TableCell>
39-
</TableRow>
43+
<TestRow key={test.id} test={test} suiteId={suiteId} duplicate={duplicate} />
4044
))}
4145
</TableBody>
4246
</Table>
4347
)
4448
}
49+
50+
const TestRow = ({
51+
test,
52+
suiteId,
53+
duplicate,
54+
}: {
55+
test: TSuite['tests'][number]
56+
suiteId: number
57+
duplicate: (testId: number, formData: FormData) => void
58+
}) => {
59+
const action = duplicate.bind(null, test.id)
60+
61+
return (
62+
<TableRow key={test.id}>
63+
<TableCell>{test.label}</TableCell>
64+
<TableCell className="text-right flex justify-end gap-1">
65+
<form action={action}>
66+
<Button variant="ghost" type="submit" className="-my-2">
67+
<CopyPlus className="size-3.5" />
68+
</Button>
69+
</form>
70+
<Link href={`/suite/${suiteId}/test/${test.id}`}>View</Link>
71+
</TableCell>
72+
</TableRow>
73+
)
74+
}

src/components/suite/run/SuiteRunDetails.tsx

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { RunStatusIcon } from '@/components/shared/RunStatusIcon'
99
import { SectionHeader } from '@/components/shared/SectionHeader'
1010
import { formatDate } from '@/components/shared/utils'
1111
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
12+
import { DeepRequired } from '@/lib/types'
1213

1314
export function SuiteRunDetails({ run }: { run: TSuiteRun }) {
1415
const { nOfPassingTests, nOfFailedTests } = useMemo(() => {
@@ -22,6 +23,19 @@ export function SuiteRunDetails({ run }: { run: TSuiteRun }) {
2223
)
2324
}, [run.testRuns])
2425

26+
const liveViews = useMemo(
27+
() =>
28+
run.testRuns
29+
.map((testRun) => ({
30+
id: testRun.id,
31+
label: testRun.test.label,
32+
status: testRun.status,
33+
liveUrl: testRun.liveUrl,
34+
}))
35+
.filter((testRun): testRun is DeepRequired<typeof testRun> => testRun.liveUrl != null),
36+
[run.testRuns],
37+
)
38+
2539
return (
2640
<Fragment>
2741
<PageHeader
@@ -71,6 +85,25 @@ export function SuiteRunDetails({ run }: { run: TSuiteRun }) {
7185
</TableBody>
7286
</Table>
7387

88+
<SectionHeader title="Live View" actions={[]} />
89+
90+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
91+
{liveViews.map((liveView) => (
92+
<Link
93+
key={liveView.id}
94+
href={`/suite/${run.suite.id}/test/${liveView.id}/run/${liveView.id}`}
95+
className="block col-span-1 w-full relative border border-gray-300 rounded-xs overflow-hidden"
96+
>
97+
<iframe src={liveView.liveUrl} className="w-full h-auto" style={{ aspectRatio: '1280/1050' }} />
98+
99+
<div className="absolute bottom-0 right-0 p-2 bg-white flex items-center gap-2 rounded-tl-xs">
100+
<RunStatusIcon status={liveView.status} />
101+
<span className="text-sm font-medium">{liveView.label}</span>
102+
</div>
103+
</Link>
104+
))}
105+
</div>
106+
74107
<Polling poll={run.status === 'running' || run.status === 'pending'} />
75108
</Fragment>
76109
)

0 commit comments

Comments
 (0)