Skip to content

Commit 8ba2205

Browse files
authored
Merge pull request #17 from spivx/add-e2e-playwright
feat: add Playwright e2e tests and update CI workflow
2 parents 24e544e + 0634fff commit 8ba2205

28 files changed

+511
-58
lines changed

.github/workflows/ci-tests.yml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,27 @@ jobs:
2626

2727
- name: Run unit tests
2828
run: npm run test:run
29+
30+
e2e-tests:
31+
name: Run Playwright e2e tests
32+
runs-on: ubuntu-latest
33+
needs: unit-tests
34+
steps:
35+
- uses: actions/checkout@v4
36+
37+
- uses: actions/setup-node@v4
38+
with:
39+
node-version: 20
40+
cache: npm
41+
cache-dependency-path: package-lock.json
42+
43+
- name: Install dependencies
44+
run: npm ci
45+
46+
- name: Install Playwright browsers
47+
run: npx playwright install --with-deps
48+
49+
- name: Run e2e tests
50+
run: npm run test:e2e
51+
env:
52+
CI: true

app/existing/[repoUrl]/page.tsx

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,14 @@ import { absoluteUrl } from "@/lib/site-metadata"
77
import type { RepoScanRouteParams } from "@/types/repo-scan"
88

99
type RepoScanPageProps = {
10-
params: RepoScanRouteParams
10+
params: Promise<RepoScanRouteParams>
1111
}
1212

1313
const toSlug = (repoUrl: string) => repoUrl.replace(/^https:\/\/github.com\//, "")
1414

15-
export function generateMetadata({ params }: RepoScanPageProps): Metadata {
16-
const decoded = decodeRepoRouteParam(params.repoUrl)
15+
export async function generateMetadata({ params }: RepoScanPageProps): Promise<Metadata> {
16+
const resolvedParams = await params
17+
const decoded = decodeRepoRouteParam(resolvedParams.repoUrl)
1718
const normalized = decoded ? normalizeGitHubRepoInput(decoded) ?? decoded : null
1819
const repoSlug = normalized ? toSlug(normalized) : null
1920
const title = repoSlug ? `Repo scan · ${repoSlug}` : "Repo scan"
@@ -47,8 +48,9 @@ export function generateMetadata({ params }: RepoScanPageProps): Metadata {
4748
}
4849
}
4950

50-
export default function RepoScanPage({ params }: RepoScanPageProps) {
51-
const decoded = decodeRepoRouteParam(params.repoUrl)
51+
export default async function RepoScanPage({ params }: RepoScanPageProps) {
52+
const resolvedParams = await params
53+
const decoded = decodeRepoRouteParam(resolvedParams.repoUrl)
5254
const normalized = decoded ? normalizeGitHubRepoInput(decoded) ?? decoded : null
5355

5456
return <RepoScanClient initialRepoUrl={normalized ?? decoded ?? null} />

app/existing/[repoUrl]/repo-scan-client.tsx

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,10 @@ export default function RepoScanClient({ initialRepoUrl }: RepoScanClientProps)
148148
}, [scanResult])
149149

150150
return (
151-
<div className="relative flex min-h-screen flex-col bg-background text-foreground">
151+
<div
152+
className="relative flex min-h-screen flex-col bg-background text-foreground"
153+
data-testid="repo-scan-page"
154+
>
152155
<div className="absolute inset-0 bg-gradient-to-b from-background via-background/95 to-background" aria-hidden="true" />
153156
<header className="relative z-10 flex items-center justify-between px-6 py-6 lg:px-12 lg:py-8">
154157
<Link href="/" className="text-sm font-semibold text-foreground transition hover:text-primary">
@@ -178,28 +181,42 @@ export default function RepoScanClient({ initialRepoUrl }: RepoScanClientProps)
178181
</div>
179182
</div>
180183
) : promptVisible ? (
181-
<div className="space-y-4">
184+
<div className="space-y-4" data-testid="repo-scan-prompt">
182185
<h3 className="text-xl font-semibold text-foreground">Scan {repoSlug ?? repoUrlForScan}?</h3>
183186
<p className="text-sm text-muted-foreground">We will detect languages, frameworks, tooling, and testing info.</p>
184-
<Button onClick={handleStartScan} className="w-full sm:w-auto">Yes, scan this repo</Button>
187+
<Button
188+
onClick={handleStartScan}
189+
className="w-full sm:w-auto"
190+
data-testid="repo-scan-confirm-button"
191+
>
192+
Yes, scan this repo
193+
</Button>
185194
</div>
186195
) : isLoading ? (
187196
<div className="flex justify-center py-16">
188197
<RepoScanLoader />
189198
</div>
190199
) : error ? (
191-
<div className="flex flex-col items-center gap-3 py-10 text-center">
200+
<div
201+
className="flex flex-col items-center gap-3 py-10 text-center"
202+
data-testid="repo-scan-error"
203+
>
192204
<AlertTriangle className="size-8 text-destructive" aria-hidden="true" />
193205
<div>
194206
<p className="text-base font-semibold text-foreground">Unable to scan repository</p>
195207
<p className="mt-1 text-sm text-muted-foreground">{error}</p>
196208
</div>
197209
{canRetry ? (
198-
<Button onClick={handleRetryScan}>Try again</Button>
210+
<Button
211+
onClick={handleRetryScan}
212+
data-testid="repo-scan-retry-button"
213+
>
214+
Try again
215+
</Button>
199216
) : null}
200217
</div>
201218
) : scanResult ? (
202-
<div className="space-y-8">
219+
<div className="space-y-8" data-testid="repo-scan-results">
203220
<section className="space-y-2">
204221
<h3 className="text-lg font-semibold text-foreground">Detected snapshot</h3>
205222
<p className="text-sm text-muted-foreground">
@@ -291,7 +308,10 @@ export default function RepoScanClient({ initialRepoUrl }: RepoScanClientProps)
291308
</div>
292309
</div>
293310
) : null}
294-
<div className="rounded-2xl border border-border/60 bg-background/70 p-5">
311+
<div
312+
className="rounded-2xl border border-border/60 bg-background/70 p-5"
313+
data-testid="repo-scan-raw-json"
314+
>
295315
<h3 className="text-sm font-semibold uppercase tracking-wide text-muted-foreground">Raw response</h3>
296316
<pre className="mt-3 max-h-72 overflow-auto rounded-md bg-muted p-3 text-xs text-muted-foreground">
297317
{JSON.stringify(scanResult, null, 2)}

app/existing/existing-repo-entry-client.tsx

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,10 @@ export function ExistingRepoEntryClient() {
3434
}
3535

3636
return (
37-
<div className="relative flex min-h-screen flex-col bg-background text-foreground">
37+
<div
38+
className="relative flex min-h-screen flex-col bg-background text-foreground"
39+
data-testid="existing-repo-page"
40+
>
3841
<div className="absolute inset-0 bg-gradient-to-b from-background via-background/95 to-background" aria-hidden="true" />
3942
<header className="relative z-10 flex items-center justify-between px-6 py-6 lg:px-12 lg:py-8">
4043
<Link href="/" className="text-sm font-semibold text-foreground transition hover:text-primary">
@@ -53,7 +56,11 @@ export function ExistingRepoEntryClient() {
5356
</CardDescription>
5457
</CardHeader>
5558
<CardContent>
56-
<form className="flex flex-col gap-4" onSubmit={handleSubmit}>
59+
<form
60+
className="flex flex-col gap-4"
61+
onSubmit={handleSubmit}
62+
data-testid="existing-repo-form"
63+
>
5764
<div className="flex flex-col gap-2">
5865
<label htmlFor="githubUrl" className="text-sm font-medium text-foreground">
5966
GitHub repository URL
@@ -68,17 +75,27 @@ export function ExistingRepoEntryClient() {
6875
onChange={(event) => setValue(event.target.value)}
6976
aria-describedby={error ? "github-url-error" : undefined}
7077
aria-invalid={error ? true : undefined}
78+
data-testid="existing-repo-input"
7179
/>
7280
<p className="text-xs text-muted-foreground">
7381
You can also paste an <code>owner/repo</code> slug and we will normalize it for you.
7482
</p>
7583
{error ? (
76-
<p id="github-url-error" className="text-sm text-destructive">
84+
<p
85+
id="github-url-error"
86+
className="text-sm text-destructive"
87+
data-testid="existing-repo-error"
88+
>
7789
{error}
7890
</p>
7991
) : null}
8092
</div>
81-
<Button type="submit" disabled={isSubmitting} className="w-full">
93+
<Button
94+
type="submit"
95+
disabled={isSubmitting}
96+
className="w-full"
97+
data-testid="existing-repo-submit"
98+
>
8299
{isSubmitting ? "Redirecting…" : "Analyze repository"}
83100
</Button>
84101
</form>

app/new/stack/[[...stackSegments]]/page.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ type MetadataProps = {
2020
}
2121

2222
type PageProps = {
23-
params: PageParams
23+
params: Promise<PageParams>
2424
}
2525

2626
export async function generateMetadata({ params }: MetadataProps): Promise<Metadata> {
@@ -92,7 +92,8 @@ export async function generateMetadata({ params }: MetadataProps): Promise<Metad
9292
}
9393

9494
export default async function StackRoutePage({ params }: PageProps) {
95-
const { stackSegments } = params
95+
const resolvedParams = await params
96+
const { stackSegments } = resolvedParams
9697
let stackIdFromRoute: string | null = null
9798
let summaryMode: "default" | "user" | null = null
9899

app/new/stack/stack-summary-page.tsx

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -240,15 +240,21 @@ export function StackSummaryPage({ stackId, mode }: StackSummaryPageProps) {
240240

241241
if (isLoading) {
242242
return (
243-
<div className="flex flex-1 items-center justify-center text-sm text-muted-foreground">
243+
<div
244+
className="flex flex-1 items-center justify-center text-sm text-muted-foreground"
245+
data-testid="stack-summary-loading"
246+
>
244247
Preparing your summary…
245248
</div>
246249
)
247250
}
248251

249252
if (errorMessage) {
250253
return (
251-
<div className="flex flex-1 flex-col items-center justify-center gap-4 text-center">
254+
<div
255+
className="flex flex-1 flex-col items-center justify-center gap-4 text-center"
256+
data-testid="stack-summary-error"
257+
>
252258
<p className="text-base text-muted-foreground">{errorMessage}</p>
253259
<Button asChild>
254260
<Link href={stackId ? `/new/stack/${stackId}` : "/new/stack"}>Back to wizard</Link>
@@ -262,7 +268,10 @@ export function StackSummaryPage({ stackId, mode }: StackSummaryPageProps) {
262268
}
263269

264270
return (
265-
<div className="mx-auto flex w-full max-w-5xl flex-col gap-6">
271+
<div
272+
className="mx-auto flex w-full max-w-5xl flex-col gap-6"
273+
data-testid="stack-summary-page"
274+
>
266275
<section className="rounded-3xl border border-border/80 bg-card/95 p-8 shadow-lg">
267276
<div className="space-y-4">
268277
<div className="space-y-2">

app/stacks/[stack]/page.tsx

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -102,8 +102,9 @@ export function generateStaticParams() {
102102
.map((answer) => ({ stack: answer.value }))
103103
}
104104

105-
export function generateMetadata({ params }: { params: { stack: string } }): Metadata {
106-
const slug = params.stack.toLowerCase()
105+
export async function generateMetadata({ params }: { params: Promise<{ stack: string }> }): Promise<Metadata> {
106+
const resolvedParams = await params
107+
const slug = resolvedParams.stack.toLowerCase()
107108
const stackEntry = stackAnswers.find((answer) => answer.value === slug)
108109

109110
if (!stackEntry) {
@@ -126,8 +127,9 @@ export function generateMetadata({ params }: { params: { stack: string } }): Met
126127
}
127128
}
128129

129-
export default function StackLandingPage({ params }: { params: { stack: string } }) {
130-
const slug = params.stack.toLowerCase()
130+
export default async function StackLandingPage({ params }: { params: Promise<{ stack: string }> }) {
131+
const resolvedParams = await params
132+
const slug = resolvedParams.stack.toLowerCase()
131133
const stackEntry = stackAnswers.find((answer) => answer.value === slug)
132134

133135
if (!stackEntry) {
@@ -142,7 +144,10 @@ export default function StackLandingPage({ params }: { params: { stack: string }
142144
const defaultSummaryUrl = `/new/stack/${slug}/default/summary`
143145

144146
return (
145-
<main className="mx-auto flex min-h-screen max-w-3xl flex-col gap-10 px-6 py-16 text-foreground">
147+
<main
148+
className="mx-auto flex min-h-screen max-w-3xl flex-col gap-10 px-6 py-16 text-foreground"
149+
data-testid="stack-detail-page"
150+
>
146151
<header className="space-y-6">
147152
<div className="flex items-center justify-between">
148153
<Link href="/" className="text-sm font-semibold text-foreground transition hover:text-primary">

app/stacks/page.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,10 @@ export const metadata: Metadata = {
2929

3030
export default function StacksIndexPage() {
3131
return (
32-
<main className="mx-auto flex min-h-screen max-w-4xl flex-col gap-10 px-6 py-16 text-foreground">
32+
<main
33+
className="mx-auto flex min-h-screen max-w-4xl flex-col gap-10 px-6 py-16 text-foreground"
34+
data-testid="stacks-index-page"
35+
>
3336
<header className="space-y-6">
3437
<div className="flex items-center justify-between">
3538
<Link href="/" className="text-sm font-semibold text-foreground transition hover:text-primary">
@@ -52,6 +55,7 @@ export default function StacksIndexPage() {
5255
<article
5356
key={answer.value}
5457
className="flex flex-col gap-4 rounded-2xl border border-border/70 bg-card/95 p-6 shadow-sm transition hover:-translate-y-1 hover:shadow-lg"
58+
data-testid={`stack-card-${answer.value}`}
5559
>
5660
<div className="space-y-2">
5761
<h2 className="text-2xl font-semibold tracking-tight">{answer.label}</h2>

components/Hero.tsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ export function Hero() {
6767
variants={containerVariants}
6868
initial="hidden"
6969
animate="show"
70+
data-testid="hero-section"
7071
>
7172
<motion.div className="space-y-10" variants={itemVariants}>
7273
<motion.div
@@ -126,6 +127,7 @@ export function Hero() {
126127
type="button"
127128
onClick={() => handleStackClick(stack.value)}
128129
className="group inline-flex items-center gap-3 rounded-full border border-border/70 bg-background/90 px-4 py-2 text-sm font-medium text-foreground shadow-sm transition hover:-translate-y-0.5 hover:border-primary/40 hover:shadow-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/60"
130+
data-testid={`hero-stack-${stack.value}`}
129131
>
130132
<span
131133
className="flex h-8 w-8 items-center justify-center rounded-full ring-1 ring-border/40"
@@ -160,6 +162,7 @@ export function Hero() {
160162
type="button"
161163
onClick={handleMoreStacks}
162164
className="inline-flex items-center gap-2 rounded-full border border-border/70 bg-background/80 px-4 py-2 text-sm font-semibold text-foreground shadow-sm transition hover:-translate-y-0.5 hover:border-primary/40 hover:shadow-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/60"
165+
data-testid="hero-more-stacks"
163166
>
164167
More stacks
165168
<ArrowRight className="h-3.5 w-3.5" />
@@ -177,7 +180,11 @@ export function Hero() {
177180
</span>
178181
</div>
179182

180-
<form onSubmit={handleGithubSubmit} className={`${selectionCardClass} flex flex-col gap-4`}>
183+
<form
184+
onSubmit={handleGithubSubmit}
185+
className={`${selectionCardClass} flex flex-col gap-4`}
186+
data-testid="hero-scan-form"
187+
>
181188
<div className="space-y-2 text-left">
182189
<p className="text-sm font-semibold uppercase tracking-wide text-muted-foreground">
183190
Scan a GitHub repository
@@ -193,8 +200,9 @@ export function Hero() {
193200
onChange={(event) => setGithubRepo(event.target.value)}
194201
placeholder="github.com/owner/repo"
195202
className="w-full rounded-xl border border-border/70 bg-background px-4 py-2 text-sm text-foreground shadow-sm transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/60 sm:min-w-[260px]"
203+
data-testid="hero-repo-input"
196204
/>
197-
<Button type="submit" size="sm" className="gap-2">
205+
<Button type="submit" size="sm" className="gap-2" data-testid="hero-scan-button">
198206
Scan repo
199207
<Github className="h-4 w-4" />
200208
</Button>

components/final-output-view.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,7 @@ export default function FinalOutputView({ fileName, fileContent, mimeType, onClo
158158
aria-labelledby="final-output-title"
159159
aria-describedby="final-output-description"
160160
onClick={handleBackdropClick}
161+
data-testid="final-output-dialog"
161162
>
162163
<div
163164
ref={dialogRef}
@@ -215,6 +216,7 @@ export default function FinalOutputView({ fileName, fileContent, mimeType, onClo
215216
className="min-h-0 h-full w-full resize-none rounded-2xl bg-transparent p-6 font-mono text-sm leading-relaxed text-foreground focus:outline-none"
216217
aria-label="Generated instructions content"
217218
placeholder="No content available."
219+
data-testid="final-output-textarea"
218220
/>
219221
</div>
220222
</div>

0 commit comments

Comments
 (0)