feat(frontend): refine AutoPilot onboarding — branding, auto-advance, soft cap, polish (#12686)

### Why / What / How

**Why:** The onboarding flow had inconsistent branding ("Autopilot" vs
"AutoPilot"), a heavy progress bar that dominated the header, an extra
click on the role screen, and no guidance on how many pain points to
select — leading to users selecting everything or nothing useful.

**What:** Copy & brand fixes, UX improvements (auto-advance, soft cap),
and visual polish (progress bar, checkmark badges, purple focus inputs).

**How:**
- Replaced all "Autopilot" with "AutoPilot" (capital P) across screens
1-3
- Removed the `?` tooltip on screen 1 (users will learn about AutoPilot
from the access email)
- Changed name label to conversational "What should I call you?"
- Screen 2: auto-advances 350ms after role selection (except "Other"
which still shows input + button)
- Screen 3: soft cap of 3 selections with green confirmation text and
shake animation on overflow attempt
- Thinned progress bar from ~10px to 3px (Linear/Notion style)
- Added purple checkmark badges on selected cards
- Updated Input atom focus state to purple ring

### Changes 🏗️

- **WelcomeStep**: "AutoPilot" branding, removed tooltip, conversational
label
- **RoleStep**: Updated subtitle, auto-advance on non-"Other" role
select, Continue button only for "Other"
- **PainPointsStep**: Soft cap of 3 with dynamic helper text and shake
animation
- **usePainPointsStep**: Added `atLimit`/`shaking` state, wrapped
`togglePainPoint` with cap logic
- **store.ts**: `togglePainPoint` returns early when at 3 and adding
- **ProgressBar**: 3px height, removed glow shadow
- **SelectableCard**: Added purple checkmark badge on selected state
- **Input atom**: Focus ring changed from zinc to purple
- **tailwind.config.ts**: Added `shake` keyframe and `animate-shake`
utility

### Checklist 📋

#### For code changes:
- [ ] I have clearly listed my changes in the PR description
- [ ] I have made a test plan
- [ ] I have tested my changes according to the test plan:
  - [ ] Navigate through full onboarding flow (screens 1→2→3→4)
  - [ ] Verify "AutoPilot" branding on all screens (no "Autopilot")
  - [ ] Verify screen 2 auto-advances after tapping a role (non-"Other")
  - [ ] Verify "Other" role still shows text input and Continue button
  - [ ] Verify Back button works correctly from screen 2 and 3
  - [ ] Select 3 pain points and verify green "3 selected" text
  - [ ] Attempt 4th selection and verify shake animation + swap message
  - [ ] Deselect one and verify can select a different one
  - [ ] Verify checkmark badges appear on selected cards
  - [ ] Verify progress bar is thin (3px) and subtle
  - [ ] Verify input focus state is purple across onboarding inputs
- [ ] Verify "Something else" + other text input still works on screen 3

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ubbe
2026-04-07 17:58:36 +07:00
committed by GitHub
parent 243b12778f
commit ca748ee12a
14 changed files with 422 additions and 88 deletions

View File

@@ -66,6 +66,29 @@ describe("useOnboardingWizardStore", () => {
"no tests",
]);
});
it("ignores new selections when at the max limit", () => {
useOnboardingWizardStore.getState().togglePainPoint("a");
useOnboardingWizardStore.getState().togglePainPoint("b");
useOnboardingWizardStore.getState().togglePainPoint("c");
useOnboardingWizardStore.getState().togglePainPoint("d");
expect(useOnboardingWizardStore.getState().painPoints).toEqual([
"a",
"b",
"c",
]);
});
it("still allows deselecting when at the max limit", () => {
useOnboardingWizardStore.getState().togglePainPoint("a");
useOnboardingWizardStore.getState().togglePainPoint("b");
useOnboardingWizardStore.getState().togglePainPoint("c");
useOnboardingWizardStore.getState().togglePainPoint("b");
expect(useOnboardingWizardStore.getState().painPoints).toEqual([
"a",
"c",
]);
});
});
describe("setOtherPainPoint", () => {

View File

@@ -7,9 +7,9 @@ export function ProgressBar({ currentStep, totalSteps }: Props) {
const percent = (currentStep / totalSteps) * 100;
return (
<div className="absolute left-0 top-0 h-[0.625rem] w-full bg-neutral-300">
<div className="absolute left-0 top-0 h-[3px] w-full bg-neutral-200">
<div
className="h-full bg-purple-400 shadow-[0_0_4px_2px_rgba(168,85,247,0.5)] transition-all duration-500 ease-out"
className="h-full bg-purple-400 transition-all duration-500 ease-out"
style={{ width: `${percent}%` }}
/>
</div>

View File

@@ -2,6 +2,7 @@
import { Text } from "@/components/atoms/Text/Text";
import { cn } from "@/lib/utils";
import { Check } from "@phosphor-icons/react";
interface Props {
icon: React.ReactNode;
@@ -24,13 +25,18 @@ export function SelectableCard({
onClick={onClick}
aria-pressed={selected}
className={cn(
"flex h-[9rem] w-[10.375rem] shrink-0 flex-col items-center justify-center gap-3 rounded-xl border-2 bg-white px-6 py-5 transition-all hover:shadow-sm md:shrink lg:gap-2 lg:px-10 lg:py-8",
"relative flex h-[9rem] w-[10.375rem] shrink-0 flex-col items-center justify-center gap-3 rounded-xl border-2 bg-white px-6 py-5 transition-all hover:shadow-sm md:shrink lg:gap-2 lg:px-10 lg:py-8",
className,
selected
? "border-purple-500 bg-purple-50 shadow-sm"
: "border-transparent",
)}
>
{selected && (
<span className="absolute right-2 top-2 flex h-5 w-5 items-center justify-center rounded-full bg-purple-500">
<Check size={12} weight="bold" className="text-white" />
</span>
)}
<Text
variant="lead"
as="span"

View File

@@ -3,6 +3,7 @@
import { Button } from "@/components/atoms/Button/Button";
import { Input } from "@/components/atoms/Input/Input";
import { Text } from "@/components/atoms/Text/Text";
import { cn } from "@/lib/utils";
import { ReactNode } from "react";
import { FadeIn } from "@/components/atoms/FadeIn/FadeIn";
@@ -73,6 +74,8 @@ export function PainPointsStep() {
togglePainPoint,
setOtherPainPoint,
hasSomethingElse,
atLimit,
shaking,
canContinue,
handleLaunch,
} = usePainPointsStep();
@@ -90,7 +93,7 @@ export function PainPointsStep() {
What&apos;s eating your time?
</Text>
<Text variant="lead" className="!text-zinc-500">
Pick the tasks you&apos;d love to hand off to Autopilot
Pick the tasks you&apos;d love to hand off to AutoPilot
</Text>
</div>
@@ -107,11 +110,22 @@ export function PainPointsStep() {
/>
))}
</div>
{!hasSomethingElse ? (
<Text variant="small" className="!text-zinc-500">
Pick as many as you want you can always change later
</Text>
) : null}
<Text
variant="small"
className={cn(
"transition-colors",
atLimit && canContinue ? "!text-green-600" : "!text-zinc-500",
shaking && "animate-shake",
)}
>
{shaking
? "You've picked 3 — tap one to swap it out"
: atLimit && canContinue
? "3 selected — you're all set!"
: atLimit && hasSomethingElse
? "Tell us what else takes up your time"
: "Pick up to 3 to start — AutoPilot can help with anything else later"}
</Text>
</div>
{hasSomethingElse && (
@@ -133,7 +147,7 @@ export function PainPointsStep() {
disabled={!canContinue}
className="w-full max-w-xs"
>
Launch Autopilot
Launch AutoPilot
</Button>
</div>
</FadeIn>

View File

@@ -8,6 +8,7 @@ import { FadeIn } from "@/components/atoms/FadeIn/FadeIn";
import { SelectableCard } from "../components/SelectableCard";
import { useOnboardingWizardStore } from "../store";
import { Emoji } from "@/components/atoms/Emoji/Emoji";
import { useEffect, useRef } from "react";
const IMG_SIZE = 42;
@@ -57,12 +58,26 @@ export function RoleStep() {
const setRole = useOnboardingWizardStore((s) => s.setRole);
const setOtherRole = useOnboardingWizardStore((s) => s.setOtherRole);
const nextStep = useOnboardingWizardStore((s) => s.nextStep);
const autoAdvanceTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
const isOther = role === "Other";
const canContinue = role && (!isOther || otherRole.trim());
function handleContinue() {
if (canContinue) {
useEffect(() => {
return () => {
if (autoAdvanceTimer.current) clearTimeout(autoAdvanceTimer.current);
};
}, []);
function handleRoleSelect(id: string) {
if (autoAdvanceTimer.current) clearTimeout(autoAdvanceTimer.current);
setRole(id);
if (id !== "Other") {
autoAdvanceTimer.current = setTimeout(nextStep, 350);
}
}
function handleOtherContinue() {
if (otherRole.trim()) {
nextStep();
}
}
@@ -78,7 +93,7 @@ export function RoleStep() {
What best describes you, {name}?
</Text>
<Text variant="lead" className="!text-zinc-500">
Autopilot will tailor automations to your world
So AutoPilot knows how to help you best
</Text>
</div>
@@ -89,33 +104,35 @@ export function RoleStep() {
icon={r.icon}
label={r.label}
selected={role === r.id}
onClick={() => setRole(r.id)}
onClick={() => handleRoleSelect(r.id)}
className="p-8"
/>
))}
</div>
{isOther && (
<div className="-mb-5 w-full px-8 md:px-0">
<Input
id="other-role"
label="Other role"
hideLabel
placeholder="Describe your role..."
value={otherRole}
onChange={(e) => setOtherRole(e.target.value)}
autoFocus
/>
</div>
)}
<>
<div className="-mb-5 w-full px-8 md:px-0">
<Input
id="other-role"
label="Other role"
hideLabel
placeholder="Describe your role..."
value={otherRole}
onChange={(e) => setOtherRole(e.target.value)}
autoFocus
/>
</div>
<Button
onClick={handleContinue}
disabled={!canContinue}
className="w-full max-w-xs"
>
Continue
</Button>
<Button
onClick={handleOtherContinue}
disabled={!otherRole.trim()}
className="w-full max-w-xs"
>
Continue
</Button>
</>
)}
</div>
</FadeIn>
);

View File

@@ -4,13 +4,6 @@ import { AutoGPTLogo } from "@/components/atoms/AutoGPTLogo/AutoGPTLogo";
import { Button } from "@/components/atoms/Button/Button";
import { Input } from "@/components/atoms/Input/Input";
import { Text } from "@/components/atoms/Text/Text";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/atoms/Tooltip/BaseTooltip";
import { Question } from "@phosphor-icons/react";
import { FadeIn } from "@/components/atoms/FadeIn/FadeIn";
import { useOnboardingWizardStore } from "../store";
@@ -40,36 +33,16 @@ export function WelcomeStep() {
<Text variant="h3">Welcome to AutoGPT</Text>
<Text variant="lead" as="span" className="!text-zinc-500">
Let&apos;s personalize your experience so{" "}
<span className="relative mr-3 inline-block bg-gradient-to-r from-purple-500 to-indigo-500 bg-clip-text text-transparent">
Autopilot
<span className="absolute -right-4 top-0">
<TooltipProvider delayDuration={400}>
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
aria-label="What is Autopilot?"
className="inline-flex text-purple-500"
>
<Question size={14} />
</button>
</TooltipTrigger>
<TooltipContent>
Autopilot is AutoGPT&apos;s AI assistant that watches your
connected apps, spots repetitive tasks you do every day
and runs them for you automatically.
</TooltipContent>
</Tooltip>
</TooltipProvider>
</span>
<span className="bg-gradient-to-r from-purple-500 to-indigo-500 bg-clip-text text-transparent">
AutoPilot
</span>{" "}
can start saving you time right away
can start saving you time
</Text>
</div>
<Input
id="first-name"
label="Your first name"
label="What should I call you?"
placeholder="e.g. John"
value={name}
onChange={(e) => setName(e.target.value)}

View File

@@ -0,0 +1,154 @@
import {
render,
screen,
fireEvent,
cleanup,
} from "@/tests/integrations/test-utils";
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
import { useOnboardingWizardStore } from "../../store";
import { PainPointsStep } from "../PainPointsStep";
vi.mock("@/components/atoms/Emoji/Emoji", () => ({
Emoji: ({ text }: { text: string }) => <span>{text}</span>,
}));
vi.mock("@/components/atoms/FadeIn/FadeIn", () => ({
FadeIn: ({ children }: { children: React.ReactNode }) => (
<div>{children}</div>
),
}));
function getCard(name: RegExp) {
return screen.getByRole("button", { name });
}
function clickCard(name: RegExp) {
fireEvent.click(getCard(name));
}
function getLaunchButton() {
return screen.getByRole("button", { name: /launch autopilot/i });
}
afterEach(cleanup);
beforeEach(() => {
useOnboardingWizardStore.getState().reset();
useOnboardingWizardStore.getState().setName("Alice");
useOnboardingWizardStore.getState().setRole("Founder/CEO");
useOnboardingWizardStore.getState().goToStep(3);
});
describe("PainPointsStep", () => {
test("renders all pain point cards", () => {
render(<PainPointsStep />);
expect(getCard(/finding leads/i)).toBeDefined();
expect(getCard(/email & outreach/i)).toBeDefined();
expect(getCard(/reports & data/i)).toBeDefined();
expect(getCard(/customer support/i)).toBeDefined();
expect(getCard(/social media/i)).toBeDefined();
expect(getCard(/something else/i)).toBeDefined();
});
test("shows default helper text", () => {
render(<PainPointsStep />);
expect(
screen.getAllByText(/pick up to 3 to start/i).length,
).toBeGreaterThan(0);
});
test("selecting a card marks it as pressed", () => {
render(<PainPointsStep />);
clickCard(/finding leads/i);
expect(getCard(/finding leads/i).getAttribute("aria-pressed")).toBe("true");
});
test("launch button is disabled when nothing is selected", () => {
render(<PainPointsStep />);
expect(getLaunchButton().hasAttribute("disabled")).toBe(true);
});
test("launch button is enabled after selecting a pain point", () => {
render(<PainPointsStep />);
clickCard(/finding leads/i);
expect(getLaunchButton().hasAttribute("disabled")).toBe(false);
});
test("shows success text when 3 items are selected", () => {
render(<PainPointsStep />);
clickCard(/finding leads/i);
clickCard(/email & outreach/i);
clickCard(/reports & data/i);
expect(screen.getAllByText(/3 selected/i).length).toBeGreaterThan(0);
});
test("does not select a 4th item when at the limit", () => {
render(<PainPointsStep />);
clickCard(/finding leads/i);
clickCard(/email & outreach/i);
clickCard(/reports & data/i);
clickCard(/customer support/i);
expect(getCard(/customer support/i).getAttribute("aria-pressed")).toBe(
"false",
);
});
test("can deselect when at the limit and select a different one", () => {
render(<PainPointsStep />);
clickCard(/finding leads/i);
clickCard(/email & outreach/i);
clickCard(/reports & data/i);
clickCard(/finding leads/i);
expect(getCard(/finding leads/i).getAttribute("aria-pressed")).toBe(
"false",
);
clickCard(/customer support/i);
expect(getCard(/customer support/i).getAttribute("aria-pressed")).toBe(
"true",
);
});
test("shows input when 'Something else' is selected", () => {
render(<PainPointsStep />);
clickCard(/something else/i);
expect(
screen.getByPlaceholderText(/what else takes up your time/i),
).toBeDefined();
});
test("launch button is disabled when 'Something else' selected but input empty", () => {
render(<PainPointsStep />);
clickCard(/something else/i);
expect(getLaunchButton().hasAttribute("disabled")).toBe(true);
});
test("launch button is enabled when 'Something else' selected and input filled", () => {
render(<PainPointsStep />);
clickCard(/something else/i);
fireEvent.change(
screen.getByPlaceholderText(/what else takes up your time/i),
{ target: { value: "Manual invoicing" } },
);
expect(getLaunchButton().hasAttribute("disabled")).toBe(false);
});
});

View File

@@ -0,0 +1,123 @@
import {
render,
screen,
fireEvent,
cleanup,
} from "@/tests/integrations/test-utils";
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
import { useOnboardingWizardStore } from "../../store";
import { RoleStep } from "../RoleStep";
vi.mock("@/components/atoms/Emoji/Emoji", () => ({
Emoji: ({ text }: { text: string }) => <span>{text}</span>,
}));
vi.mock("@/components/atoms/FadeIn/FadeIn", () => ({
FadeIn: ({ children }: { children: React.ReactNode }) => (
<div>{children}</div>
),
}));
afterEach(() => {
cleanup();
vi.useRealTimers();
});
beforeEach(() => {
vi.useFakeTimers();
useOnboardingWizardStore.getState().reset();
useOnboardingWizardStore.getState().setName("Alice");
useOnboardingWizardStore.getState().goToStep(2);
});
describe("RoleStep", () => {
test("renders all role cards", () => {
render(<RoleStep />);
expect(screen.getByText("Founder / CEO")).toBeDefined();
expect(screen.getByText("Operations")).toBeDefined();
expect(screen.getByText("Sales / BD")).toBeDefined();
expect(screen.getByText("Marketing")).toBeDefined();
expect(screen.getByText("Product / PM")).toBeDefined();
expect(screen.getByText("Engineering")).toBeDefined();
expect(screen.getByText("HR / People")).toBeDefined();
expect(screen.getByText("Other")).toBeDefined();
});
test("displays the user name in the heading", () => {
render(<RoleStep />);
expect(
screen.getAllByText(/what best describes you, alice/i).length,
).toBeGreaterThan(0);
});
test("selecting a non-Other role auto-advances after delay", () => {
render(<RoleStep />);
fireEvent.click(screen.getByRole("button", { name: /engineering/i }));
expect(useOnboardingWizardStore.getState().role).toBe("Engineering");
expect(useOnboardingWizardStore.getState().currentStep).toBe(2);
vi.advanceTimersByTime(350);
expect(useOnboardingWizardStore.getState().currentStep).toBe(3);
});
test("selecting 'Other' does not auto-advance", () => {
render(<RoleStep />);
fireEvent.click(screen.getByRole("button", { name: /\bother\b/i }));
vi.advanceTimersByTime(500);
expect(useOnboardingWizardStore.getState().currentStep).toBe(2);
});
test("selecting 'Other' shows text input and Continue button", () => {
render(<RoleStep />);
fireEvent.click(screen.getByRole("button", { name: /\bother\b/i }));
expect(screen.getByPlaceholderText(/describe your role/i)).toBeDefined();
expect(screen.getByRole("button", { name: /continue/i })).toBeDefined();
});
test("Continue button is disabled when Other input is empty", () => {
render(<RoleStep />);
fireEvent.click(screen.getByRole("button", { name: /\bother\b/i }));
const continueBtn = screen.getByRole("button", { name: /continue/i });
expect(continueBtn.hasAttribute("disabled")).toBe(true);
});
test("Continue button advances when Other role text is filled", () => {
render(<RoleStep />);
fireEvent.click(screen.getByRole("button", { name: /\bother\b/i }));
fireEvent.change(screen.getByPlaceholderText(/describe your role/i), {
target: { value: "Designer" },
});
const continueBtn = screen.getByRole("button", { name: /continue/i });
expect(continueBtn.hasAttribute("disabled")).toBe(false);
fireEvent.click(continueBtn);
expect(useOnboardingWizardStore.getState().currentStep).toBe(3);
});
test("switching from Other to a regular role cancels Other and auto-advances", () => {
render(<RoleStep />);
fireEvent.click(screen.getByRole("button", { name: /\bother\b/i }));
expect(screen.getByPlaceholderText(/describe your role/i)).toBeDefined();
fireEvent.click(screen.getByRole("button", { name: /marketing/i }));
expect(useOnboardingWizardStore.getState().role).toBe("Marketing");
vi.advanceTimersByTime(350);
expect(useOnboardingWizardStore.getState().currentStep).toBe(3);
});
});

View File

@@ -1,4 +1,5 @@
import { useOnboardingWizardStore } from "../store";
import { useEffect, useRef, useState } from "react";
import { MAX_PAIN_POINT_SELECTIONS, useOnboardingWizardStore } from "../store";
const ROLE_TOP_PICKS: Record<string, string[]> = {
"Founder/CEO": [
@@ -23,18 +24,38 @@ export function usePainPointsStep() {
const role = useOnboardingWizardStore((s) => s.role);
const painPoints = useOnboardingWizardStore((s) => s.painPoints);
const otherPainPoint = useOnboardingWizardStore((s) => s.otherPainPoint);
const togglePainPoint = useOnboardingWizardStore((s) => s.togglePainPoint);
const storeToggle = useOnboardingWizardStore((s) => s.togglePainPoint);
const setOtherPainPoint = useOnboardingWizardStore(
(s) => s.setOtherPainPoint,
);
const nextStep = useOnboardingWizardStore((s) => s.nextStep);
const [shaking, setShaking] = useState(false);
const shakeTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
useEffect(() => {
return () => {
if (shakeTimer.current) clearTimeout(shakeTimer.current);
};
}, []);
const topIDs = getTopPickIDs(role);
const hasSomethingElse = painPoints.includes("Something else");
const atLimit = painPoints.length >= MAX_PAIN_POINT_SELECTIONS;
const canContinue =
painPoints.length > 0 &&
(!hasSomethingElse || Boolean(otherPainPoint.trim()));
function togglePainPoint(id: string) {
const alreadySelected = painPoints.includes(id);
if (!alreadySelected && atLimit) {
if (shakeTimer.current) clearTimeout(shakeTimer.current);
setShaking(true);
shakeTimer.current = setTimeout(() => setShaking(false), 600);
return;
}
storeToggle(id);
}
function handleLaunch() {
if (canContinue) {
nextStep();
@@ -48,6 +69,8 @@ export function usePainPointsStep() {
togglePainPoint,
setOtherPainPoint,
hasSomethingElse,
atLimit,
shaking,
canContinue,
handleLaunch,
};

View File

@@ -1,5 +1,6 @@
import { create } from "zustand";
export const MAX_PAIN_POINT_SELECTIONS = 3;
export type Step = 1 | 2 | 3 | 4;
interface OnboardingWizardState {
@@ -40,6 +41,8 @@ export const useOnboardingWizardStore = create<OnboardingWizardState>(
togglePainPoint(painPoint) {
set((state) => {
const exists = state.painPoints.includes(painPoint);
if (!exists && state.painPoints.length >= MAX_PAIN_POINT_SELECTIONS)
return state;
return {
painPoints: exists
? state.painPoints.filter((p) => p !== painPoint)

View File

@@ -78,7 +78,7 @@ export function Input({
"font-normal text-black",
"placeholder:font-normal placeholder:text-zinc-400",
// Focus and hover states
"focus:border-zinc-400 focus:shadow-none focus:outline-none focus:ring-1 focus:ring-zinc-400 focus:ring-offset-0",
"focus:border-purple-400 focus:shadow-none focus:outline-none focus:ring-1 focus:ring-purple-400 focus:ring-offset-0",
className,
);

View File

@@ -32,7 +32,7 @@ test("onboarding wizard step navigation works", async ({ page }) => {
// Step 1: Welcome
await expect(page.getByText("Welcome to AutoGPT")).toBeVisible();
await page.getByLabel("Your first name").fill("Bob");
await page.getByLabel("What should I call you?").fill("Bob");
await page.getByRole("button", { name: "Continue" }).click();
// Step 2: Role — verify we're here, then go back
@@ -41,7 +41,7 @@ test("onboarding wizard step navigation works", async ({ page }) => {
// Should be back on step 1 with name preserved
await expect(page.getByText("Welcome to AutoGPT")).toBeVisible();
await expect(page.getByLabel("Your first name")).toHaveValue("Bob");
await expect(page.getByLabel("What should I call you?")).toHaveValue("Bob");
});
test("onboarding wizard validates required fields", async ({ page }) => {
@@ -53,18 +53,13 @@ test("onboarding wizard validates required fields", async ({ page }) => {
await expect(continueButton).toBeDisabled();
// Fill name — continue should become enabled
await page.getByLabel("Your first name").fill("Charlie");
await page.getByLabel("What should I call you?").fill("Charlie");
await expect(continueButton).toBeEnabled();
await continueButton.click();
// Step 2: Continue should be disabled without a role
const step2Continue = page.getByRole("button", { name: "Continue" });
await expect(step2Continue).toBeDisabled();
// Select role — continue should become enabled
// Step 2: Role — selecting auto-advances to step 3
await expect(page.getByText("What best describes you")).toBeVisible();
await page.getByText("Engineering").click();
await expect(step2Continue).toBeEnabled();
await step2Continue.click();
// Step 3: Launch Autopilot should be disabled without any pain points
const launchButton = page.getByRole("button", { name: "Launch Autopilot" });
@@ -95,7 +90,7 @@ test("onboarding URL params sync with steps", async ({ page }) => {
await expect(page.getByText("Welcome to AutoGPT")).toBeVisible();
// Fill name and go to step 2
await page.getByLabel("Your first name").fill("Test");
await page.getByLabel("What should I call you?").fill("Test");
await page.getByRole("button", { name: "Continue" }).click();
// URL should show step=2
@@ -106,12 +101,11 @@ test("role-based pain point ordering works", async ({ page }) => {
await signupTestUser(page, undefined, undefined, false);
// Complete step 1
await page.getByLabel("Your first name").fill("Test");
await page.getByLabel("What should I call you?").fill("Test");
await page.getByRole("button", { name: "Continue" }).click();
// Select Sales/BD role
// Select Sales/BD role (auto-advances to step 3)
await page.getByText("Sales / BD").click();
await page.getByRole("button", { name: "Continue" }).click();
// On pain points step, "Finding leads" should be visible (top pick for Sales)
await expect(page.getByText("What's eating your time?")).toBeVisible();

View File

@@ -52,15 +52,14 @@ export async function completeOnboardingWizard(
await expect(page.getByText("Welcome to AutoGPT")).toBeVisible({
timeout: 10000,
});
await page.getByLabel("Your first name").fill(name);
await page.getByLabel("What should I call you?").fill(name);
await page.getByRole("button", { name: "Continue" }).click();
// Step 2: Role — select a role
// Step 2: Role — select a role (auto-advances after selection)
await expect(page.getByText("What best describes you")).toBeVisible({
timeout: 5000,
});
await page.getByText(role, { exact: false }).click();
await page.getByRole("button", { name: "Continue" }).click();
// Step 3: Pain points — select tasks
await expect(page.getByText("What's eating your time?")).toBeVisible({
@@ -72,9 +71,6 @@ export async function completeOnboardingWizard(
await page.getByRole("button", { name: "Launch Autopilot" }).click();
// Step 4: Preparing — wait for animation to complete and redirect to /copilot
await expect(page.getByText("Preparing your workspace")).toBeVisible({
timeout: 5000,
});
await page.waitForURL(/\/copilot/, { timeout: 15000 });
return { name, role, painPoints };

View File

@@ -175,6 +175,13 @@ const config = {
boxShadow: "0 0 0 30px rgba(0, 0, 0, 0)",
},
},
shake: {
"0%, 100%": { transform: "translateX(0)" },
"20%": { transform: "translateX(-4px)" },
"40%": { transform: "translateX(4px)" },
"60%": { transform: "translateX(-3px)" },
"80%": { transform: "translateX(3px)" },
},
},
animation: {
"accordion-down": "accordion-down 0.2s ease-out",
@@ -182,6 +189,7 @@ const config = {
"fade-in": "fade-in 0.2s ease-out",
shimmer: "shimmer 4s ease-in-out infinite",
loader: "loader 1s infinite",
shake: "shake 0.5s ease-in-out",
},
transitionDuration: {
"2000": "2000ms",