Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions .github/workflows/ci-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,27 @@ jobs:

- name: Run unit tests
run: npm run test:run

e2e-tests:
name: Run Playwright e2e tests
runs-on: ubuntu-latest
needs: unit-tests
steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
cache-dependency-path: package-lock.json

- name: Install dependencies
run: npm ci

- name: Install Playwright browsers
run: npx playwright install --with-deps

- name: Run e2e tests
run: npm run test:e2e
env:
CI: true
12 changes: 7 additions & 5 deletions app/existing/[repoUrl]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,14 @@ import { absoluteUrl } from "@/lib/site-metadata"
import type { RepoScanRouteParams } from "@/types/repo-scan"

type RepoScanPageProps = {
params: RepoScanRouteParams
params: Promise<RepoScanRouteParams>
}

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

export function generateMetadata({ params }: RepoScanPageProps): Metadata {
const decoded = decodeRepoRouteParam(params.repoUrl)
export async function generateMetadata({ params }: RepoScanPageProps): Promise<Metadata> {
const resolvedParams = await params
const decoded = decodeRepoRouteParam(resolvedParams.repoUrl)
const normalized = decoded ? normalizeGitHubRepoInput(decoded) ?? decoded : null
const repoSlug = normalized ? toSlug(normalized) : null
const title = repoSlug ? `Repo scan · ${repoSlug}` : "Repo scan"
Expand Down Expand Up @@ -47,8 +48,9 @@ export function generateMetadata({ params }: RepoScanPageProps): Metadata {
}
}

export default function RepoScanPage({ params }: RepoScanPageProps) {
const decoded = decodeRepoRouteParam(params.repoUrl)
export default async function RepoScanPage({ params }: RepoScanPageProps) {
const resolvedParams = await params
const decoded = decodeRepoRouteParam(resolvedParams.repoUrl)
const normalized = decoded ? normalizeGitHubRepoInput(decoded) ?? decoded : null

return <RepoScanClient initialRepoUrl={normalized ?? decoded ?? null} />
Expand Down
34 changes: 27 additions & 7 deletions app/existing/[repoUrl]/repo-scan-client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,10 @@ export default function RepoScanClient({ initialRepoUrl }: RepoScanClientProps)
}, [scanResult])

return (
<div className="relative flex min-h-screen flex-col bg-background text-foreground">
<div
className="relative flex min-h-screen flex-col bg-background text-foreground"
data-testid="repo-scan-page"
>
<div className="absolute inset-0 bg-gradient-to-b from-background via-background/95 to-background" aria-hidden="true" />
<header className="relative z-10 flex items-center justify-between px-6 py-6 lg:px-12 lg:py-8">
<Link href="/" className="text-sm font-semibold text-foreground transition hover:text-primary">
Expand Down Expand Up @@ -178,28 +181,42 @@ export default function RepoScanClient({ initialRepoUrl }: RepoScanClientProps)
</div>
</div>
) : promptVisible ? (
<div className="space-y-4">
<div className="space-y-4" data-testid="repo-scan-prompt">
<h3 className="text-xl font-semibold text-foreground">Scan {repoSlug ?? repoUrlForScan}?</h3>
<p className="text-sm text-muted-foreground">We will detect languages, frameworks, tooling, and testing info.</p>
<Button onClick={handleStartScan} className="w-full sm:w-auto">Yes, scan this repo</Button>
<Button
onClick={handleStartScan}
className="w-full sm:w-auto"
data-testid="repo-scan-confirm-button"
>
Yes, scan this repo
</Button>
</div>
) : isLoading ? (
<div className="flex justify-center py-16">
<RepoScanLoader />
</div>
) : error ? (
<div className="flex flex-col items-center gap-3 py-10 text-center">
<div
className="flex flex-col items-center gap-3 py-10 text-center"
data-testid="repo-scan-error"
>
<AlertTriangle className="size-8 text-destructive" aria-hidden="true" />
<div>
<p className="text-base font-semibold text-foreground">Unable to scan repository</p>
<p className="mt-1 text-sm text-muted-foreground">{error}</p>
</div>
{canRetry ? (
<Button onClick={handleRetryScan}>Try again</Button>
<Button
onClick={handleRetryScan}
data-testid="repo-scan-retry-button"
>
Try again
</Button>
) : null}
</div>
) : scanResult ? (
<div className="space-y-8">
<div className="space-y-8" data-testid="repo-scan-results">
<section className="space-y-2">
<h3 className="text-lg font-semibold text-foreground">Detected snapshot</h3>
<p className="text-sm text-muted-foreground">
Expand Down Expand Up @@ -291,7 +308,10 @@ export default function RepoScanClient({ initialRepoUrl }: RepoScanClientProps)
</div>
</div>
) : null}
<div className="rounded-2xl border border-border/60 bg-background/70 p-5">
<div
className="rounded-2xl border border-border/60 bg-background/70 p-5"
data-testid="repo-scan-raw-json"
>
<h3 className="text-sm font-semibold uppercase tracking-wide text-muted-foreground">Raw response</h3>
<pre className="mt-3 max-h-72 overflow-auto rounded-md bg-muted p-3 text-xs text-muted-foreground">
{JSON.stringify(scanResult, null, 2)}
Expand Down
25 changes: 21 additions & 4 deletions app/existing/existing-repo-entry-client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,10 @@ export function ExistingRepoEntryClient() {
}

return (
<div className="relative flex min-h-screen flex-col bg-background text-foreground">
<div
className="relative flex min-h-screen flex-col bg-background text-foreground"
data-testid="existing-repo-page"
>
<div className="absolute inset-0 bg-gradient-to-b from-background via-background/95 to-background" aria-hidden="true" />
<header className="relative z-10 flex items-center justify-between px-6 py-6 lg:px-12 lg:py-8">
<Link href="/" className="text-sm font-semibold text-foreground transition hover:text-primary">
Expand All @@ -53,7 +56,11 @@ export function ExistingRepoEntryClient() {
</CardDescription>
</CardHeader>
<CardContent>
<form className="flex flex-col gap-4" onSubmit={handleSubmit}>
<form
className="flex flex-col gap-4"
onSubmit={handleSubmit}
data-testid="existing-repo-form"
>
<div className="flex flex-col gap-2">
<label htmlFor="githubUrl" className="text-sm font-medium text-foreground">
GitHub repository URL
Expand All @@ -68,17 +75,27 @@ export function ExistingRepoEntryClient() {
onChange={(event) => setValue(event.target.value)}
aria-describedby={error ? "github-url-error" : undefined}
aria-invalid={error ? true : undefined}
data-testid="existing-repo-input"
/>
<p className="text-xs text-muted-foreground">
You can also paste an <code>owner/repo</code> slug and we will normalize it for you.
</p>
{error ? (
<p id="github-url-error" className="text-sm text-destructive">
<p
id="github-url-error"
className="text-sm text-destructive"
data-testid="existing-repo-error"
>
{error}
</p>
) : null}
</div>
<Button type="submit" disabled={isSubmitting} className="w-full">
<Button
type="submit"
disabled={isSubmitting}
className="w-full"
data-testid="existing-repo-submit"
>
{isSubmitting ? "Redirecting…" : "Analyze repository"}
</Button>
</form>
Expand Down
5 changes: 3 additions & 2 deletions app/new/stack/[[...stackSegments]]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ type MetadataProps = {
}

type PageProps = {
params: PageParams
params: Promise<PageParams>
}

export async function generateMetadata({ params }: MetadataProps): Promise<Metadata> {
Expand Down Expand Up @@ -92,7 +92,8 @@ export async function generateMetadata({ params }: MetadataProps): Promise<Metad
}

export default async function StackRoutePage({ params }: PageProps) {
const { stackSegments } = params
const resolvedParams = await params
const { stackSegments } = resolvedParams
let stackIdFromRoute: string | null = null
let summaryMode: "default" | "user" | null = null

Expand Down
15 changes: 12 additions & 3 deletions app/new/stack/stack-summary-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -240,15 +240,21 @@ export function StackSummaryPage({ stackId, mode }: StackSummaryPageProps) {

if (isLoading) {
return (
<div className="flex flex-1 items-center justify-center text-sm text-muted-foreground">
<div
className="flex flex-1 items-center justify-center text-sm text-muted-foreground"
data-testid="stack-summary-loading"
>
Preparing your summary…
</div>
)
}

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

return (
<div className="mx-auto flex w-full max-w-5xl flex-col gap-6">
<div
className="mx-auto flex w-full max-w-5xl flex-col gap-6"
data-testid="stack-summary-page"
>
<section className="rounded-3xl border border-border/80 bg-card/95 p-8 shadow-lg">
<div className="space-y-4">
<div className="space-y-2">
Expand Down
15 changes: 10 additions & 5 deletions app/stacks/[stack]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -102,8 +102,9 @@ export function generateStaticParams() {
.map((answer) => ({ stack: answer.value }))
}

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

if (!stackEntry) {
Expand All @@ -126,8 +127,9 @@ export function generateMetadata({ params }: { params: { stack: string } }): Met
}
}

export default function StackLandingPage({ params }: { params: { stack: string } }) {
const slug = params.stack.toLowerCase()
export default async function StackLandingPage({ params }: { params: Promise<{ stack: string }> }) {
const resolvedParams = await params
const slug = resolvedParams.stack.toLowerCase()
const stackEntry = stackAnswers.find((answer) => answer.value === slug)

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

return (
<main className="mx-auto flex min-h-screen max-w-3xl flex-col gap-10 px-6 py-16 text-foreground">
<main
className="mx-auto flex min-h-screen max-w-3xl flex-col gap-10 px-6 py-16 text-foreground"
data-testid="stack-detail-page"
>
<header className="space-y-6">
<div className="flex items-center justify-between">
<Link href="/" className="text-sm font-semibold text-foreground transition hover:text-primary">
Expand Down
6 changes: 5 additions & 1 deletion app/stacks/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,10 @@ export const metadata: Metadata = {

export default function StacksIndexPage() {
return (
<main className="mx-auto flex min-h-screen max-w-4xl flex-col gap-10 px-6 py-16 text-foreground">
<main
className="mx-auto flex min-h-screen max-w-4xl flex-col gap-10 px-6 py-16 text-foreground"
data-testid="stacks-index-page"
>
<header className="space-y-6">
<div className="flex items-center justify-between">
<Link href="/" className="text-sm font-semibold text-foreground transition hover:text-primary">
Expand All @@ -52,6 +55,7 @@ export default function StacksIndexPage() {
<article
key={answer.value}
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"
data-testid={`stack-card-${answer.value}`}
>
<div className="space-y-2">
<h2 className="text-2xl font-semibold tracking-tight">{answer.label}</h2>
Expand Down
12 changes: 10 additions & 2 deletions components/Hero.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ export function Hero() {
variants={containerVariants}
initial="hidden"
animate="show"
data-testid="hero-section"
>
<motion.div className="space-y-10" variants={itemVariants}>
<motion.div
Expand Down Expand Up @@ -126,6 +127,7 @@ export function Hero() {
type="button"
onClick={() => handleStackClick(stack.value)}
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"
data-testid={`hero-stack-${stack.value}`}
>
<span
className="flex h-8 w-8 items-center justify-center rounded-full ring-1 ring-border/40"
Expand Down Expand Up @@ -160,6 +162,7 @@ export function Hero() {
type="button"
onClick={handleMoreStacks}
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"
data-testid="hero-more-stacks"
>
More stacks
<ArrowRight className="h-3.5 w-3.5" />
Expand All @@ -177,7 +180,11 @@ export function Hero() {
</span>
</div>

<form onSubmit={handleGithubSubmit} className={`${selectionCardClass} flex flex-col gap-4`}>
<form
onSubmit={handleGithubSubmit}
className={`${selectionCardClass} flex flex-col gap-4`}
data-testid="hero-scan-form"
>
<div className="space-y-2 text-left">
<p className="text-sm font-semibold uppercase tracking-wide text-muted-foreground">
Scan a GitHub repository
Expand All @@ -193,8 +200,9 @@ export function Hero() {
onChange={(event) => setGithubRepo(event.target.value)}
placeholder="github.com/owner/repo"
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]"
data-testid="hero-repo-input"
/>
<Button type="submit" size="sm" className="gap-2">
<Button type="submit" size="sm" className="gap-2" data-testid="hero-scan-button">
Scan repo
<Github className="h-4 w-4" />
</Button>
Expand Down
2 changes: 2 additions & 0 deletions components/final-output-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ export default function FinalOutputView({ fileName, fileContent, mimeType, onClo
aria-labelledby="final-output-title"
aria-describedby="final-output-description"
onClick={handleBackdropClick}
data-testid="final-output-dialog"
>
<div
ref={dialogRef}
Expand Down Expand Up @@ -215,6 +216,7 @@ export default function FinalOutputView({ fileName, fileContent, mimeType, onClo
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"
aria-label="Generated instructions content"
placeholder="No content available."
data-testid="final-output-textarea"
/>
</div>
</div>
Expand Down
3 changes: 3 additions & 0 deletions components/instructions-answer-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export type InstructionsAnswerCardProps = {
disabled?: boolean
disabledLabel?: string
onClick?: () => void
testId?: string
} & Omit<ComponentPropsWithoutRef<"button">, "children">

export function InstructionsAnswerCard({
Expand All @@ -31,6 +32,7 @@ export function InstructionsAnswerCard({
disabled = false,
disabledLabel,
onClick,
testId,
...buttonProps
}: InstructionsAnswerCardProps) {
const [isTooltipOpen, setIsTooltipOpen] = useState(false)
Expand All @@ -51,6 +53,7 @@ export function InstructionsAnswerCard({
type="button"
onClick={onClick}
aria-disabled={disabled}
data-testid={testId}
className={cn(
"group relative flex h-full items-center justify-between rounded-2xl border border-border/60 bg-background/90 px-5 py-4 text-left transition-all hover:border-primary/40 hover:shadow-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
disabled && "cursor-not-allowed opacity-60 hover:border-border/60 hover:shadow-none focus-visible:ring-0",
Expand Down
Loading