Skip to content

Commit 32ab6d2

Browse files
committed
Fixes
1 parent e72db3e commit 32ab6d2

25 files changed

+861
-661
lines changed

drizzle/0000_yellow_chronomancer.sql renamed to drizzle/0000_unknown_swordsman.sql

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ CREATE TABLE "suite" (
99
CREATE TABLE "suite_run" (
1010
"id" serial PRIMARY KEY NOT NULL,
1111
"created_at" timestamp DEFAULT now() NOT NULL,
12+
"started_at" timestamp DEFAULT now(),
13+
"finished_at" timestamp,
1214
"suite_id" integer NOT NULL,
1315
"status" "run_status" NOT NULL
1416
);
@@ -25,6 +27,8 @@ CREATE TABLE "test" (
2527
CREATE TABLE "test_run" (
2628
"id" serial PRIMARY KEY NOT NULL,
2729
"created_at" timestamp DEFAULT now() NOT NULL,
30+
"started_at" timestamp DEFAULT now(),
31+
"finished_at" timestamp,
2832
"test_id" integer NOT NULL,
2933
"suite_run_id" integer NOT NULL,
3034
"status" "run_status" NOT NULL,

drizzle/meta/0000_snapshot.json

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"id": "3d9e5c8a-add3-48ee-a684-71ee2cad865e",
2+
"id": "f958ebed-6ec4-405b-b868-19f8baec5c46",
33
"prevId": "00000000-0000-0000-0000-000000000000",
44
"version": "7",
55
"dialect": "postgresql",
@@ -59,6 +59,19 @@
5959
"notNull": true,
6060
"default": "now()"
6161
},
62+
"started_at": {
63+
"name": "started_at",
64+
"type": "timestamp",
65+
"primaryKey": false,
66+
"notNull": false,
67+
"default": "now()"
68+
},
69+
"finished_at": {
70+
"name": "finished_at",
71+
"type": "timestamp",
72+
"primaryKey": false,
73+
"notNull": false
74+
},
6275
"suite_id": {
6376
"name": "suite_id",
6477
"type": "integer",
@@ -176,6 +189,19 @@
176189
"notNull": true,
177190
"default": "now()"
178191
},
192+
"started_at": {
193+
"name": "started_at",
194+
"type": "timestamp",
195+
"primaryKey": false,
196+
"notNull": false,
197+
"default": "now()"
198+
},
199+
"finished_at": {
200+
"name": "finished_at",
201+
"type": "timestamp",
202+
"primaryKey": false,
203+
"notNull": false
204+
},
179205
"test_id": {
180206
"name": "test_id",
181207
"type": "integer",

drizzle/meta/_journal.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
{
66
"idx": 0,
77
"version": "7",
8-
"when": 1753371490636,
9-
"tag": "0000_yellow_chronomancer",
8+
"when": 1753436163092,
9+
"tag": "0000_unknown_swordsman",
1010
"breakpoints": true
1111
}
1212
]

src/app/layout.tsx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ const geistMono = Geist_Mono({
1515
})
1616

1717
export const metadata: Metadata = {
18-
title: 'Create Next App',
19-
description: 'Generated by create next app',
18+
title: 'Browser Use | Use QA',
19+
description: 'Create delightful browser tests with ease!',
2020
}
2121

2222
export default function RootLayout({
@@ -26,7 +26,11 @@ export default function RootLayout({
2626
}>) {
2727
return (
2828
<html lang="en">
29-
<body className={`${geistSans.variable} ${geistMono.variable} antialiased`}>{children}</body>
29+
<body className={`${geistSans.variable} ${geistMono.variable} antialiased bg-gray-50`}>
30+
<div className="min-h-screen">
31+
<div className="max-w-6xl mx-auto px-3 py-12 md:py-24">{children}</div>
32+
</div>
33+
</body>
3034
</html>
3135
)
3236
}

src/app/page.tsx

Lines changed: 52 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import { Plus } from 'lucide-react'
2-
import { Suspense } from 'react'
2+
import Link from 'next/link'
3+
import { Fragment } from 'react'
34

4-
import { SuiteList } from '@/components/SuiteList'
5+
import { PageHeader } from '@/components/shared/PageHeader'
6+
import { SectionHeader } from '@/components/shared/SectionHeader'
7+
import { formatDate } from '@/components/shared/utils'
58
import { Button } from '@/components/ui/button'
69
import {
710
Dialog,
@@ -12,6 +15,7 @@ import {
1215
DialogTrigger,
1316
} from '@/components/ui/dialog'
1417
import { Input } from '@/components/ui/input'
18+
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
1519

1620
import { createSuiteAction, seedSuiteAction } from './actions'
1721
import { loader } from './loader'
@@ -20,30 +24,58 @@ export default async function Page() {
2024
const suites = await loader()
2125

2226
return (
23-
<div className="min-h-screen bg-gray-50">
24-
<div className="max-w-4xl mx-auto px-6 py-8">
25-
{/* Header */}
26-
<div className="flex items-center gap-4 mb-8">
27-
<h1 className="text-5xl font-bold text-gray-900 mr-auto">QA-Use</h1>
27+
<Fragment>
28+
<PageHeader
29+
title="All Suites"
30+
subtitle="Use QA"
31+
actions={[{ link: 'https://github.com/browser-use/use-qa', label: 'Star on GitHub' }]}
32+
/>
33+
{/* Header */}
2834

29-
<CreateSuiteDialog />
35+
{/* Suites List */}
3036

31-
<form action={seedSuiteAction}>
37+
<SectionHeader
38+
title="Test Suites"
39+
actions={[
40+
<CreateSuiteDialog key="create-suite-dialog" />,
41+
<form key="seed-suite-form" action={seedSuiteAction}>
3242
<Button variant="outline" type="submit">
3343
Seed
3444
</Button>
35-
</form>
36-
</div>
45+
</form>,
46+
]}
47+
/>
3748

38-
{/* Suites List */}
39-
<div className="space-y-4">
40-
<h2 className="text-xl font-semibold text-gray-800 mb-4">Test Suites</h2>
41-
<Suspense fallback={<div className="text-gray-500">Loading suites...</div>}>
42-
<SuiteList data={suites} />
43-
</Suspense>
44-
</div>
45-
</div>
46-
</div>
49+
<Table>
50+
<TableHeader>
51+
<TableRow>
52+
<TableHead className="w-[100px]">Name</TableHead>
53+
<TableHead>Domain</TableHead>
54+
<TableHead>Created At</TableHead>
55+
<TableHead>{/* Actions */}</TableHead>
56+
</TableRow>
57+
</TableHeader>
58+
<TableBody>
59+
{suites.length === 0 && (
60+
<TableRow>
61+
<TableCell colSpan={4} className="">
62+
No suites found
63+
</TableCell>
64+
</TableRow>
65+
)}
66+
{suites.map((suite) => (
67+
<TableRow key={suite.id}>
68+
<TableCell className="font-medium">{suite.name}</TableCell>
69+
<TableCell>{suite.domain}</TableCell>
70+
<TableCell>{formatDate(suite.createdAt)}</TableCell>
71+
<TableCell className="text-right">
72+
<Link href={`/suite/${suite.id}`}>View</Link>
73+
</TableCell>
74+
</TableRow>
75+
))}
76+
</TableBody>
77+
</Table>
78+
</Fragment>
4779
)
4880
}
4981

src/app/suite/[suiteId]/loader.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
'use server'
22

3-
import { eq } from 'drizzle-orm'
3+
import { desc, eq } from 'drizzle-orm'
44

55
import { db } from '@/lib/db/db'
66
import * as schema from '@/lib/db/schema'
@@ -23,7 +23,7 @@ export async function loader(suiteId: number) {
2323
},
2424
},
2525
},
26-
orderBy: [schema.suiteRun.createdAt],
26+
orderBy: [desc(schema.suiteRun.createdAt)],
2727
},
2828
},
2929
})

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

Lines changed: 3 additions & 120 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,7 @@
1-
import { ArrowLeft, Plus } from 'lucide-react'
2-
import Link from 'next/link'
31
import { notFound } from 'next/navigation'
42

5-
import { Button } from '@/components/ui/button'
6-
import {
7-
Dialog,
8-
DialogContent,
9-
DialogDescription,
10-
DialogHeader,
11-
DialogTitle,
12-
DialogTrigger,
13-
} from '@/components/ui/dialog'
14-
import { Input } from '@/components/ui/input'
15-
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
3+
import { SuiteDetails } from '@/components/suite/SuiteDetails'
164

17-
import { HistoryTab } from '../../../components/suite/HistoryTab'
18-
import { TestsTab } from '../../../components/suite/TestsTab'
195
import { createTestAction, deleteSuiteAction, runSuiteAction } from './actions'
206
import { loader } from './loader'
217

@@ -34,110 +20,7 @@ export default async function SuitePage({ params }: { params: Promise<{ suiteId:
3420

3521
const runSuite = runSuiteAction.bind(null, suiteIdNum)
3622
const deleteSuite = deleteSuiteAction.bind(null, suiteIdNum)
23+
const createTest = createTestAction.bind(null, suiteIdNum)
3724

38-
return (
39-
<div className="min-h-screen bg-gray-50">
40-
<div className="max-w-6xl mx-auto px-6 py-8">
41-
{/* Header */}
42-
<div className="mb-8">
43-
<Link
44-
href="/"
45-
className="inline-flex items-center gap-2 text-gray-600 hover:text-gray-800 mb-4 transition-colors"
46-
>
47-
<ArrowLeft className="w-4 h-4" />
48-
Back to Suites
49-
</Link>
50-
51-
<div className="flex items-start gap-4">
52-
<div className="mr-auto">
53-
<h1 className="text-4xl font-bold text-gray-900 mb-2">{suite.name}</h1>
54-
<p className="text-md text-gray-500">{suite.domain}</p>
55-
</div>
56-
57-
<form action={runSuite}>
58-
<Button type="submit">Run Suite</Button>
59-
</form>
60-
61-
<form action={deleteSuite}>
62-
<Button type="submit" variant="destructive">
63-
Delete
64-
</Button>
65-
</form>
66-
</div>
67-
</div>
68-
69-
{/* Body with Tabs */}
70-
<Tabs defaultValue="history" className="space-y-6">
71-
<div className="flex items-center justify-between">
72-
<TabsList>
73-
<TabsTrigger value="history">History</TabsTrigger>
74-
<TabsTrigger value="tests">Tests</TabsTrigger>
75-
</TabsList>
76-
77-
<CreateTestDialog suiteId={suiteIdNum} />
78-
</div>
79-
80-
<TabsContent value="history">
81-
<HistoryTab suite={suite} />
82-
</TabsContent>
83-
84-
<TabsContent value="tests">
85-
<TestsTab suite={suite} suiteId={suiteIdNum} />
86-
</TabsContent>
87-
</Tabs>
88-
</div>
89-
</div>
90-
)
91-
}
92-
93-
function CreateTestDialog({ suiteId }: { suiteId: number }) {
94-
const action = createTestAction.bind(null, suiteId)
95-
96-
return (
97-
<Dialog>
98-
<DialogTrigger asChild>
99-
<Button variant="outline" className="ml-auto">
100-
<Plus className="w-4 h-4" />
101-
Create Test
102-
</Button>
103-
</DialogTrigger>
104-
105-
<DialogContent>
106-
<DialogHeader>
107-
<DialogTitle>Create New Test</DialogTitle>
108-
<DialogDescription>
109-
Add a new test to this suite. Provide a name and description of what should be tested.
110-
</DialogDescription>
111-
</DialogHeader>
112-
113-
<form action={action} className="space-y-4">
114-
<div>
115-
<label htmlFor="label" className="block text-sm font-medium text-gray-700 mb-1">
116-
Test Name
117-
</label>
118-
<Input id="label" name="label" type="text" placeholder="e.g., Login functionality test" required />
119-
</div>
120-
121-
<div>
122-
<label htmlFor="task" className="block text-sm font-medium text-gray-700 mb-1">
123-
Task Description
124-
</label>
125-
<Input
126-
id="task"
127-
name="task"
128-
type="text"
129-
placeholder="e.g., Verify user can log in with valid credentials"
130-
required
131-
/>
132-
</div>
133-
134-
<input type="hidden" name="suiteId" value={suiteId} />
135-
136-
<div className="flex justify-end gap-2">
137-
<Button type="submit">Create Test</Button>
138-
</div>
139-
</form>
140-
</DialogContent>
141-
</Dialog>
142-
)
25+
return <SuiteDetails suite={suite} runSuite={runSuite} deleteSuite={deleteSuite} createTest={createTest} />
14326
}

src/app/suite/[suiteId]/run/[suiteRunId]/loader.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,20 @@
11
'use server'
22

3-
import { eq } from 'drizzle-orm'
3+
import { asc, eq } from 'drizzle-orm'
44

55
import { db } from '@/lib/db/db'
6-
import { suiteRun } from '@/lib/db/schema'
6+
import * as schema from '@/lib/db/schema'
77

88
export async function loader({ suiteRunId }: { suiteRunId: number }) {
99
const run = await db.query.suiteRun.findFirst({
10-
where: eq(suiteRun.id, suiteRunId),
10+
where: eq(schema.suiteRun.id, suiteRunId),
1111
with: {
1212
suite: true,
1313
testRuns: {
1414
with: {
1515
test: true,
1616
},
17+
orderBy: [asc(schema.testRun.createdAt)],
1718
},
1819
},
1920
})
Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { eq } from 'drizzle-orm'
1+
import { asc, desc, eq } from 'drizzle-orm'
22

33
import { db } from '@/lib/db/db'
44
import * as schema from '@/lib/db/schema'
@@ -7,10 +7,16 @@ export async function loader(testId: number) {
77
const test = await db.query.test.findFirst({
88
where: eq(schema.test.id, testId),
99
with: {
10-
steps: true,
11-
runs: true,
10+
steps: {
11+
orderBy: [asc(schema.testStep.order)],
12+
},
13+
runs: {
14+
orderBy: [desc(schema.testRun.createdAt)],
15+
},
1216
},
1317
})
1418

1519
return test
1620
}
21+
22+
export type TTest = NonNullable<Awaited<ReturnType<typeof loader>>>

0 commit comments

Comments
 (0)