feat(FN-2403): add project setup step to onboarding flow
- Introduce a new "project-setup" onboarding step between GitHub and first-task actions - Gate first-task navigation on project selection and add contextual setup-wizard guidance - Centralize ordered onboarding step definitions in shared state and reuse them for navigation/progress labels - Extract ModelOnboardingModal styles into a dedicated component CSS file and update tests/mocks for the new step order
This commit is contained in:
1307
packages/dashboard/app/components/ModelOnboardingModal.css
Normal file
1307
packages/dashboard/app/components/ModelOnboardingModal.css
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,4 @@
|
|||||||
|
import "./ModelOnboardingModal.css";
|
||||||
import { useState, useEffect, useCallback, useRef } from "react";
|
import { useState, useEffect, useCallback, useRef } from "react";
|
||||||
import { X, Loader2, CheckCircle, Key, Zap, GitPullRequest, Rocket, Plus, ChevronRight } from "lucide-react";
|
import { X, Loader2, CheckCircle, Key, Zap, GitPullRequest, Rocket, Plus, ChevronRight } from "lucide-react";
|
||||||
import type { AuthProvider, ModelInfo } from "../api";
|
import type { AuthProvider, ModelInfo } from "../api";
|
||||||
@@ -496,6 +497,7 @@ import {
|
|||||||
markStepSkipped,
|
markStepSkipped,
|
||||||
getSkippedSteps,
|
getSkippedSteps,
|
||||||
getStepData,
|
getStepData,
|
||||||
|
ONBOARDING_FLOW_STEPS,
|
||||||
type OnboardingStep,
|
type OnboardingStep,
|
||||||
} from "./model-onboarding-state";
|
} from "./model-onboarding-state";
|
||||||
import { trackOnboardingEvent } from "./onboarding-events";
|
import { trackOnboardingEvent } from "./onboarding-events";
|
||||||
@@ -541,7 +543,8 @@ const MAX_POLL_CYCLES = 150;
|
|||||||
* Multi-step onboarding modal that guides users through:
|
* Multi-step onboarding modal that guides users through:
|
||||||
* 1. AI Setup - Provider credential setup (OAuth login or API key entry) and default model selection
|
* 1. AI Setup - Provider credential setup (OAuth login or API key entry) and default model selection
|
||||||
* 2. GitHub (Optional) - GitHub connection status and login
|
* 2. GitHub (Optional) - GitHub connection status and login
|
||||||
* 3. First Task - CTA to create first task or import from GitHub
|
* 3. Project Setup - Register a project directory (or clone a repository URL via setup wizard)
|
||||||
|
* 4. First Task - CTA to create first task or import from GitHub
|
||||||
*
|
*
|
||||||
* Dismissing the modal marks onboarding as complete to prevent repeated popups.
|
* Dismissing the modal marks onboarding as complete to prevent repeated popups.
|
||||||
*/
|
*/
|
||||||
@@ -557,8 +560,12 @@ export function ModelOnboardingModal({
|
|||||||
}: ModelOnboardingModalProps) {
|
}: ModelOnboardingModalProps) {
|
||||||
// Initialize from persisted state if available (allows resume from last step)
|
// Initialize from persisted state if available (allows resume from last step)
|
||||||
const persistedState = getOnboardingState();
|
const persistedState = getOnboardingState();
|
||||||
const initialStep: OnboardingStep = persistedState && persistedState.currentStep !== "complete"
|
const persistedStep = persistedState?.currentStep;
|
||||||
? persistedState.currentStep as OnboardingStep
|
const initialStep: OnboardingStep =
|
||||||
|
persistedStep === "complete"
|
||||||
|
? "ai-setup"
|
||||||
|
: ONBOARDING_FLOW_STEPS.includes(persistedStep as (typeof ONBOARDING_FLOW_STEPS)[number])
|
||||||
|
? (persistedStep as OnboardingStep)
|
||||||
: "ai-setup";
|
: "ai-setup";
|
||||||
// Restore completed/skipped steps from persisted state
|
// Restore completed/skipped steps from persisted state
|
||||||
const persistedCompletedSteps = persistedState?.completedSteps ?? [];
|
const persistedCompletedSteps = persistedState?.completedSteps ?? [];
|
||||||
@@ -607,10 +614,12 @@ export function ModelOnboardingModal({
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
// Step definitions for progress indicator
|
// Step definitions for progress indicator.
|
||||||
|
// Keep labels aligned with the ordered ONBOARDING_FLOW_STEPS state contract.
|
||||||
const steps = [
|
const steps = [
|
||||||
{ key: "ai-setup" as const, label: "AI Setup" },
|
{ key: "ai-setup" as const, label: "AI Setup" },
|
||||||
{ key: "github" as const, label: "GitHub" },
|
{ key: "github" as const, label: "GitHub" },
|
||||||
|
{ key: "project-setup" as const, label: "Project" },
|
||||||
{ key: "first-task" as const, label: "First Task" },
|
{ key: "first-task" as const, label: "First Task" },
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -903,6 +912,13 @@ export function ModelOnboardingModal({
|
|||||||
});
|
});
|
||||||
}, [step, completedSteps]);
|
}, [step, completedSteps]);
|
||||||
|
|
||||||
|
const getFlowIndex = useCallback((value: OnboardingStep) => {
|
||||||
|
if (value === "complete") {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return ONBOARDING_FLOW_STEPS.indexOf(value);
|
||||||
|
}, []);
|
||||||
|
|
||||||
// Navigate to next step
|
// Navigate to next step
|
||||||
const handleNext = useCallback(() => {
|
const handleNext = useCallback(() => {
|
||||||
// Mark current step as completed before moving forward
|
// Mark current step as completed before moving forward
|
||||||
@@ -915,12 +931,11 @@ export function ModelOnboardingModal({
|
|||||||
setGitHubSkippedState(false);
|
setGitHubSkippedState(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (step === "ai-setup") {
|
const currentIndex = getFlowIndex(step);
|
||||||
setStep("github");
|
if (currentIndex >= 0 && currentIndex < ONBOARDING_FLOW_STEPS.length - 1) {
|
||||||
} else if (step === "github") {
|
setStep(ONBOARDING_FLOW_STEPS[currentIndex + 1]);
|
||||||
setStep("first-task");
|
|
||||||
}
|
}
|
||||||
}, [step, isGithubAuthenticated, setGitHubSkippedState]);
|
}, [step, isGithubAuthenticated, setGitHubSkippedState, getFlowIndex]);
|
||||||
|
|
||||||
// Navigate forward without marking completion
|
// Navigate forward without marking completion
|
||||||
const handleSkip = useCallback(() => {
|
const handleSkip = useCallback(() => {
|
||||||
@@ -932,12 +947,11 @@ export function ModelOnboardingModal({
|
|||||||
setGitHubSkippedState(true);
|
setGitHubSkippedState(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (step === "ai-setup") {
|
const currentIndex = getFlowIndex(step);
|
||||||
setStep("github");
|
if (currentIndex >= 0 && currentIndex < ONBOARDING_FLOW_STEPS.length - 1) {
|
||||||
} else if (step === "github") {
|
setStep(ONBOARDING_FLOW_STEPS[currentIndex + 1]);
|
||||||
setStep("first-task");
|
|
||||||
}
|
}
|
||||||
}, [step, isGithubAuthenticated, setGitHubSkippedState]);
|
}, [step, isGithubAuthenticated, setGitHubSkippedState, getFlowIndex]);
|
||||||
|
|
||||||
// Navigate to previous step
|
// Navigate to previous step
|
||||||
const handleBack = useCallback(() => {
|
const handleBack = useCallback(() => {
|
||||||
@@ -950,12 +964,11 @@ export function ModelOnboardingModal({
|
|||||||
setGitHubSkippedState(false);
|
setGitHubSkippedState(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (step === "github") {
|
const currentIndex = getFlowIndex(step);
|
||||||
setStep("ai-setup");
|
if (currentIndex > 0) {
|
||||||
} else if (step === "first-task") {
|
setStep(ONBOARDING_FLOW_STEPS[currentIndex - 1]);
|
||||||
setStep("github");
|
|
||||||
}
|
}
|
||||||
}, [step, isGithubAuthenticated, setGitHubSkippedState]);
|
}, [step, isGithubAuthenticated, setGitHubSkippedState, getFlowIndex]);
|
||||||
|
|
||||||
const handleSkipGitHubStep = useCallback(() => {
|
const handleSkipGitHubStep = useCallback(() => {
|
||||||
handleSkip();
|
handleSkip();
|
||||||
@@ -1737,6 +1750,11 @@ export function ModelOnboardingModal({
|
|||||||
<GitPullRequest size={24} /> Connect GitHub <span className="onboarding-optional-badge">Optional</span>
|
<GitPullRequest size={24} /> Connect GitHub <span className="onboarding-optional-badge">Optional</span>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
{step === "project-setup" && (
|
||||||
|
<>
|
||||||
|
<Rocket size={24} /> Set Up Your Project
|
||||||
|
</>
|
||||||
|
)}
|
||||||
{step === "first-task" && (
|
{step === "first-task" && (
|
||||||
<>
|
<>
|
||||||
<Rocket size={24} /> Create Your First Task
|
<Rocket size={24} /> Create Your First Task
|
||||||
@@ -1760,7 +1778,7 @@ export function ModelOnboardingModal({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Step indicator - 3 progress steps + complete */}
|
{/* Step indicator - 4 progress steps + complete */}
|
||||||
<div className="model-onboarding-steps">
|
<div className="model-onboarding-steps">
|
||||||
{steps.map((s, index) => {
|
{steps.map((s, index) => {
|
||||||
// A step is done/skipped only once we have progressed beyond it.
|
// A step is done/skipped only once we have progressed beyond it.
|
||||||
@@ -2057,7 +2075,7 @@ export function ModelOnboardingModal({
|
|||||||
)}
|
)}
|
||||||
<button
|
<button
|
||||||
className="btn btn-primary btn-sm"
|
className="btn btn-primary btn-sm"
|
||||||
onClick={() => setStep("first-task")}
|
onClick={() => setStep("project-setup")}
|
||||||
>
|
>
|
||||||
Continue without GitHub →
|
Continue without GitHub →
|
||||||
</button>
|
</button>
|
||||||
@@ -2182,12 +2200,49 @@ export function ModelOnboardingModal({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{step === "project-setup" && (
|
||||||
|
<div className="model-onboarding-project-setup">
|
||||||
|
<p className="model-onboarding-description">
|
||||||
|
Choose your first project before creating or importing tasks.
|
||||||
|
You can register an existing local directory or clone a GitHub repository URL through the setup wizard.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{!hasProjectSelected ? (
|
||||||
|
<div className="onboarding-project-prerequisite" data-testid="onboarding-project-prerequisite">
|
||||||
|
<p className="onboarding-helper-text">
|
||||||
|
A project is required before first-task actions are available.
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-primary"
|
||||||
|
onClick={onOpenSetupWizard}
|
||||||
|
data-testid="onboarding-open-setup-wizard"
|
||||||
|
>
|
||||||
|
Set Up Project
|
||||||
|
</button>
|
||||||
|
<p className="onboarding-helper-text">
|
||||||
|
In the setup wizard, pick an existing directory or paste a GitHub clone URL.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="onboarding-project-ready" data-testid="onboarding-project-ready" role="status">
|
||||||
|
<p>Project selected — task creation and imports are available.</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<OnboardingDisclosure summary="What does project setup do?">
|
||||||
|
<p className="onboarding-helper-text">
|
||||||
|
Project setup registers a workspace so Fusion knows where to read files,
|
||||||
|
run commands, and track task changes.
|
||||||
|
</p>
|
||||||
|
</OnboardingDisclosure>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{step === "first-task" && (
|
{step === "first-task" && (
|
||||||
<div className="model-onboarding-first-task">
|
<div className="model-onboarding-first-task">
|
||||||
<p className="model-onboarding-description">
|
<p className="model-onboarding-description">
|
||||||
{hasProjectSelected
|
Create your first task to start the board and launch AI execution.
|
||||||
? "Your workspace is ready. Here's how to get started:"
|
|
||||||
: "Before creating your first task, register a project directory."}
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{showTaskCreated && createdTaskForDisplay ? (
|
{showTaskCreated && createdTaskForDisplay ? (
|
||||||
@@ -2383,6 +2438,17 @@ export function ModelOnboardingModal({
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{step === "project-setup" && (
|
||||||
|
<>
|
||||||
|
<button className="btn btn-sm" onClick={handleBack}>
|
||||||
|
← Back
|
||||||
|
</button>
|
||||||
|
<button className="btn btn-primary" onClick={handleNext} disabled={!hasProjectSelected}>
|
||||||
|
Next →
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
{step === "first-task" && !showTaskCreated && (
|
{step === "first-task" && !showTaskCreated && (
|
||||||
<>
|
<>
|
||||||
<button className="btn btn-sm" onClick={handleBack}>
|
<button className="btn btn-sm" onClick={handleBack}>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import "./OnboardingResumeCard.css";
|
import "./OnboardingResumeCard.css";
|
||||||
import { Play, Sparkles } from "lucide-react";
|
import { Play, Sparkles } from "lucide-react";
|
||||||
import { getOnboardingResumeStep } from "./model-onboarding-state";
|
import { getOnboardingResumeStep, ONBOARDING_FLOW_STEPS } from "./model-onboarding-state";
|
||||||
import { trackOnboardingEvent } from "./onboarding-events";
|
import { trackOnboardingEvent } from "./onboarding-events";
|
||||||
|
|
||||||
interface OnboardingResumeCardProps {
|
interface OnboardingResumeCardProps {
|
||||||
@@ -22,7 +22,7 @@ export function OnboardingResumeCard({ onResume }: OnboardingResumeCardProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const completedCount = resumeStep.completedSteps.length;
|
const completedCount = resumeStep.completedSteps.length;
|
||||||
const totalSteps = 3; // ai-setup, github, first-task
|
const totalSteps = ONBOARDING_FLOW_STEPS.length;
|
||||||
const progressText = completedCount > 0
|
const progressText = completedCount > 0
|
||||||
? `${completedCount} of ${totalSteps} step${completedCount !== 1 ? "s" : ""} complete — You're on the `
|
? `${completedCount} of ${totalSteps} step${completedCount !== 1 ? "s" : ""} complete — You're on the `
|
||||||
: "You're on the ";
|
: "You're on the ";
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -132,6 +132,7 @@ vi.mock("../../components/model-onboarding-state", () => ({
|
|||||||
getOnboardingCompletedAt: (...args: unknown[]) => mockGetOnboardingCompletedAt(...args),
|
getOnboardingCompletedAt: (...args: unknown[]) => mockGetOnboardingCompletedAt(...args),
|
||||||
getSkippedSteps: (...args: unknown[]) => mockGetSkippedSteps(...args),
|
getSkippedSteps: (...args: unknown[]) => mockGetSkippedSteps(...args),
|
||||||
getStepData: (...args: unknown[]) => mockGetStepData(...args),
|
getStepData: (...args: unknown[]) => mockGetStepData(...args),
|
||||||
|
ONBOARDING_FLOW_STEPS: ["ai-setup", "github", "project-setup", "first-task"],
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Mock CustomModelDropdown for onboarding modal tests
|
// Mock CustomModelDropdown for onboarding modal tests
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ vi.mock("../model-onboarding-state", () => ({
|
|||||||
markStepSkipped: (...args: unknown[]) => mockMarkStepSkipped(...args),
|
markStepSkipped: (...args: unknown[]) => mockMarkStepSkipped(...args),
|
||||||
getSkippedSteps: (...args: unknown[]) => mockGetSkippedSteps(...args),
|
getSkippedSteps: (...args: unknown[]) => mockGetSkippedSteps(...args),
|
||||||
getStepData: (...args: unknown[]) => mockGetStepData(...args),
|
getStepData: (...args: unknown[]) => mockGetStepData(...args),
|
||||||
|
ONBOARDING_FLOW_STEPS: ["ai-setup", "github", "project-setup", "first-task"],
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const mockTrackOnboardingEvent = vi.fn();
|
const mockTrackOnboardingEvent = vi.fn();
|
||||||
@@ -122,9 +123,17 @@ async function navigateToGitHubStep() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function navigateToFirstTaskStep() {
|
async function navigateToProjectSetupStep() {
|
||||||
await navigateToGitHubStep();
|
await navigateToGitHubStep();
|
||||||
fireEvent.click(screen.getByText("Next →"));
|
fireEvent.click(screen.getByText("Next →"));
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText("Set Up Your Project")).toBeTruthy();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function navigateToFirstTaskStep() {
|
||||||
|
await navigateToProjectSetupStep();
|
||||||
|
fireEvent.click(screen.getByText("Next →"));
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(screen.getByText("Create Your First Task")).toBeTruthy();
|
expect(screen.getByText("Create Your First Task")).toBeTruthy();
|
||||||
});
|
});
|
||||||
@@ -1394,6 +1403,12 @@ describe("ModelOnboardingModal", () => {
|
|||||||
|
|
||||||
fireEvent.click(screen.getByText("Continue without GitHub →"));
|
fireEvent.click(screen.getByText("Continue without GitHub →"));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText("Set Up Your Project")).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByText("Next →"));
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(screen.getByText("Create Your First Task")).toBeTruthy();
|
expect(screen.getByText("Create Your First Task")).toBeTruthy();
|
||||||
});
|
});
|
||||||
@@ -1462,10 +1477,10 @@ describe("ModelOnboardingModal", () => {
|
|||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
await navigateToFirstTaskStep();
|
await navigateToProjectSetupStep();
|
||||||
|
|
||||||
expect(screen.getByTestId("onboarding-project-prerequisite")).toBeTruthy();
|
expect(screen.getByTestId("onboarding-project-prerequisite")).toBeTruthy();
|
||||||
expect(screen.getByText(/A project must be selected before you can create tasks or import from GitHub/)).toBeTruthy();
|
expect(screen.getByText(/A project is required before first-task actions are available/)).toBeTruthy();
|
||||||
expect(screen.queryByTestId("onboarding-first-task-input")).toBeNull();
|
expect(screen.queryByTestId("onboarding-first-task-input")).toBeNull();
|
||||||
expect(screen.queryByText("Create a New Task")).toBeNull();
|
expect(screen.queryByText("Create a New Task")).toBeNull();
|
||||||
expect(screen.queryByText("Import from GitHub")).toBeNull();
|
expect(screen.queryByText("Import from GitHub")).toBeNull();
|
||||||
@@ -1482,7 +1497,7 @@ describe("ModelOnboardingModal", () => {
|
|||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
await navigateToFirstTaskStep();
|
await navigateToProjectSetupStep();
|
||||||
|
|
||||||
fireEvent.click(screen.getByTestId("onboarding-open-setup-wizard"));
|
fireEvent.click(screen.getByTestId("onboarding-open-setup-wizard"));
|
||||||
expect(onOpenSetupWizard).toHaveBeenCalledTimes(1);
|
expect(onOpenSetupWizard).toHaveBeenCalledTimes(1);
|
||||||
@@ -1975,6 +1990,12 @@ describe("ModelOnboardingModal", () => {
|
|||||||
|
|
||||||
fireEvent.click(screen.getByText("← Back"));
|
fireEvent.click(screen.getByText("← Back"));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText("Set Up Your Project")).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByText("← Back"));
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(screen.getByText("Connect GitHub")).toBeTruthy();
|
expect(screen.getByText("Connect GitHub")).toBeTruthy();
|
||||||
});
|
});
|
||||||
@@ -2101,7 +2122,7 @@ describe("ModelOnboardingModal", () => {
|
|||||||
expect(aiSetupIndicator).toHaveClass("done");
|
expect(aiSetupIndicator).toHaveClass("done");
|
||||||
expect(githubIndicator).toHaveClass("done");
|
expect(githubIndicator).toHaveClass("done");
|
||||||
expect(firstTaskIndicator).toHaveClass("done");
|
expect(firstTaskIndicator).toHaveClass("done");
|
||||||
expect(document.querySelectorAll(".model-onboarding-step-connector.done")).toHaveLength(2);
|
expect(document.querySelectorAll(".model-onboarding-step-connector.done")).toHaveLength(3);
|
||||||
|
|
||||||
// Click Get Started to close
|
// Click Get Started to close
|
||||||
fireEvent.click(screen.getByText("Get Started"));
|
fireEvent.click(screen.getByText("Get Started"));
|
||||||
@@ -2396,6 +2417,12 @@ describe("ModelOnboardingModal", () => {
|
|||||||
// Navigate back to see if the model dropdown has the saved value
|
// Navigate back to see if the model dropdown has the saved value
|
||||||
fireEvent.click(screen.getByText("← Back"));
|
fireEvent.click(screen.getByText("← Back"));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText("Set Up Your Project")).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByText("← Back"));
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(screen.getByText("Connect GitHub")).toBeTruthy();
|
expect(screen.getByText("Connect GitHub")).toBeTruthy();
|
||||||
});
|
});
|
||||||
@@ -2715,7 +2742,14 @@ describe("ModelOnboardingModal", () => {
|
|||||||
// Click Next without connecting GitHub
|
// Click Next without connecting GitHub
|
||||||
fireEvent.click(screen.getByText("Next →"));
|
fireEvent.click(screen.getByText("Next →"));
|
||||||
|
|
||||||
// Should advance to First Task step
|
// Should advance to Project Setup first
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText("Set Up Your Project")).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByText("Next →"));
|
||||||
|
|
||||||
|
// Then advance to First Task step
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(screen.getByText("Create Your First Task")).toBeTruthy();
|
expect(screen.getByText("Create Your First Task")).toBeTruthy();
|
||||||
});
|
});
|
||||||
@@ -2772,9 +2806,13 @@ describe("ModelOnboardingModal", () => {
|
|||||||
expect(screen.getByText("Optional")).toBeTruthy();
|
expect(screen.getByText("Optional")).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Navigate to First Task step
|
// Navigate through Project Setup to First Task step
|
||||||
fireEvent.click(screen.getByText("Next →"));
|
fireEvent.click(screen.getByText("Next →"));
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText("Set Up Your Project")).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByText("Next →"));
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(screen.getByText("Create Your First Task")).toBeTruthy();
|
expect(screen.getByText("Create Your First Task")).toBeTruthy();
|
||||||
});
|
});
|
||||||
@@ -2815,7 +2853,14 @@ describe("ModelOnboardingModal", () => {
|
|||||||
// Click Skip GitHub
|
// Click Skip GitHub
|
||||||
fireEvent.click(screen.getByText("Skip GitHub →"));
|
fireEvent.click(screen.getByText("Skip GitHub →"));
|
||||||
|
|
||||||
// Should advance to First Task step
|
// Should advance to Project Setup first
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText("Set Up Your Project")).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByText("Next →"));
|
||||||
|
|
||||||
|
// Then advance to First Task step
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(screen.getByText("Create Your First Task")).toBeTruthy();
|
expect(screen.getByText("Create Your First Task")).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { OnboardingResumeCard } from "../OnboardingResumeCard";
|
|||||||
// Mock the model-onboarding-state module
|
// Mock the model-onboarding-state module
|
||||||
vi.mock("../model-onboarding-state", () => ({
|
vi.mock("../model-onboarding-state", () => ({
|
||||||
getOnboardingResumeStep: vi.fn(),
|
getOnboardingResumeStep: vi.fn(),
|
||||||
|
ONBOARDING_FLOW_STEPS: ["ai-setup", "github", "project-setup", "first-task"],
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const mockTrackOnboardingEvent = vi.fn();
|
const mockTrackOnboardingEvent = vi.fn();
|
||||||
@@ -183,7 +184,7 @@ describe("OnboardingResumeCard", () => {
|
|||||||
});
|
});
|
||||||
render(<OnboardingResumeCard onResume={vi.fn()} />);
|
render(<OnboardingResumeCard onResume={vi.fn()} />);
|
||||||
// Uses singular "step" for 1 completed
|
// Uses singular "step" for 1 completed
|
||||||
expect(screen.getByText(/1 of 3 step complete/)).toBeInTheDocument();
|
expect(screen.getByText(/1 of 4 step complete/)).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("shows completed step count text with 2 completed steps (plural)", () => {
|
it("shows completed step count text with 2 completed steps (plural)", () => {
|
||||||
@@ -194,7 +195,7 @@ describe("OnboardingResumeCard", () => {
|
|||||||
});
|
});
|
||||||
render(<OnboardingResumeCard onResume={vi.fn()} />);
|
render(<OnboardingResumeCard onResume={vi.fn()} />);
|
||||||
// Uses plural "steps" for 2 completed
|
// Uses plural "steps" for 2 completed
|
||||||
expect(screen.getByText(/2 of 3 steps complete/)).toBeInTheDocument();
|
expect(screen.getByText(/2 of 4 steps complete/)).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ vi.mock("../model-onboarding-state", () => ({
|
|||||||
isOnboardingCompleted: (...args: unknown[]) => mockIsOnboardingCompleted(...args),
|
isOnboardingCompleted: (...args: unknown[]) => mockIsOnboardingCompleted(...args),
|
||||||
isPostOnboardingDismissed: (...args: unknown[]) => mockIsPostOnboardingDismissed(...args),
|
isPostOnboardingDismissed: (...args: unknown[]) => mockIsPostOnboardingDismissed(...args),
|
||||||
dismissPostOnboardingRecommendations: (...args: unknown[]) => mockDismissPostOnboardingRecommendations(...args),
|
dismissPostOnboardingRecommendations: (...args: unknown[]) => mockDismissPostOnboardingRecommendations(...args),
|
||||||
|
ONBOARDING_FLOW_STEPS: ["ai-setup", "github", "project-setup", "first-task"],
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock("lucide-react", async (importOriginal) => {
|
vi.mock("lucide-react", async (importOriginal) => {
|
||||||
|
|||||||
@@ -1123,8 +1123,11 @@ describe("QuickChatFAB", () => {
|
|||||||
expect(screen.getByTestId("quick-chat-model-select")).toBeDefined();
|
expect(screen.getByTestId("quick-chat-model-select")).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Open the model dropdown
|
// Open the model dropdown after model loading completes
|
||||||
const trigger = screen.getByRole("button", { name: "Select model override" });
|
const trigger = screen.getByRole("button", { name: "Select model override" });
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(trigger).not.toBeDisabled();
|
||||||
|
});
|
||||||
fireEvent.click(trigger);
|
fireEvent.click(trigger);
|
||||||
|
|
||||||
const portalDropdown = await screen.findByTestId("model-combobox-portal");
|
const portalDropdown = await screen.findByTestId("model-combobox-portal");
|
||||||
|
|||||||
@@ -803,7 +803,7 @@ describe("TaskForm preset selection (FN-819)", () => {
|
|||||||
expect(fetchSettings).toHaveBeenCalled();
|
expect(fetchSettings).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
const overrideButton = screen.getByRole("button", { name: "Override" });
|
const overrideButton = await screen.findByRole("button", { name: "Override" });
|
||||||
fireEvent.click(overrideButton);
|
fireEvent.click(overrideButton);
|
||||||
|
|
||||||
expect(onPresetModeChange).toHaveBeenCalledWith("custom");
|
expect(onPresetModeChange).toHaveBeenCalledWith("custom");
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ vi.mock("../model-onboarding-state", () => ({
|
|||||||
isOnboardingCompleted: (...args: unknown[]) => mockIsOnboardingCompleted(...args),
|
isOnboardingCompleted: (...args: unknown[]) => mockIsOnboardingCompleted(...args),
|
||||||
isPostOnboardingDismissed: (...args: unknown[]) => mockIsPostOnboardingDismissed(...args),
|
isPostOnboardingDismissed: (...args: unknown[]) => mockIsPostOnboardingDismissed(...args),
|
||||||
dismissPostOnboardingRecommendations: (...args: unknown[]) => mockDismissPostOnboardingRecommendations(...args),
|
dismissPostOnboardingRecommendations: (...args: unknown[]) => mockDismissPostOnboardingRecommendations(...args),
|
||||||
|
ONBOARDING_FLOW_STEPS: ["ai-setup", "github", "project-setup", "first-task"],
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock("../CustomModelDropdown", () => ({
|
vi.mock("../CustomModelDropdown", () => ({
|
||||||
@@ -526,6 +527,11 @@ describe("onboarding flow integration", () => {
|
|||||||
expect(screen.getByText("Connect GitHub")).toBeInTheDocument();
|
expect(screen.getByText("Connect GitHub")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "Next →" }));
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByRole("button", { name: "Next →" })).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
fireEvent.click(screen.getByRole("button", { name: "Next →" }));
|
fireEvent.click(screen.getByRole("button", { name: "Next →" }));
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(screen.getByText("Create Your First Task")).toBeInTheDocument();
|
expect(screen.getByText("Create Your First Task")).toBeInTheDocument();
|
||||||
@@ -574,7 +580,7 @@ describe("onboarding flow integration", () => {
|
|||||||
it("dismissal flow: dismissing on first-task step saves skipped GitHub state", async () => {
|
it("dismissal flow: dismissing on first-task step saves skipped GitHub state", async () => {
|
||||||
const renderResult = renderModal();
|
const renderResult = renderModal();
|
||||||
|
|
||||||
await advanceThroughSteps(renderResult, ["next", "skip"]);
|
await advanceThroughSteps(renderResult, ["next", "skip", "next"]);
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(screen.getByText("Create Your First Task")).toBeInTheDocument();
|
expect(screen.getByText("Create Your First Task")).toBeInTheDocument();
|
||||||
@@ -614,7 +620,7 @@ describe("onboarding flow integration", () => {
|
|||||||
|
|
||||||
expect(screen.getByText("Continue Setup")).toBeInTheDocument();
|
expect(screen.getByText("Continue Setup")).toBeInTheDocument();
|
||||||
expect(screen.getByText(/GitHub/)).toBeInTheDocument();
|
expect(screen.getByText(/GitHub/)).toBeInTheDocument();
|
||||||
expect(screen.getByText(/1 of 3 step complete/)).toBeInTheDocument();
|
expect(screen.getByText(/1 of 4 step complete/)).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -670,7 +676,7 @@ describe("onboarding flow integration", () => {
|
|||||||
expect(screen.getByText("Connect GitHub")).toBeInTheDocument();
|
expect(screen.getByText("Connect GitHub")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
await advanceThroughSteps(renderResult, ["next"]);
|
await advanceThroughSteps(renderResult, ["next", "next"]);
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(screen.getByText("Create Your First Task")).toBeInTheDocument();
|
expect(screen.getByText("Create Your First Task")).toBeInTheDocument();
|
||||||
@@ -790,7 +796,7 @@ describe("onboarding flow integration", () => {
|
|||||||
|
|
||||||
const renderResult = renderModal();
|
const renderResult = renderModal();
|
||||||
|
|
||||||
await advanceThroughSteps(renderResult, ["next", "next"]);
|
await advanceThroughSteps(renderResult, ["next", "next", "next"]);
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(screen.getByText("Create Your First Task")).toBeInTheDocument();
|
expect(screen.getByText("Create Your First Task")).toBeInTheDocument();
|
||||||
@@ -809,7 +815,7 @@ describe("onboarding flow integration", () => {
|
|||||||
expect(screen.getByText("Anthropic")).toBeInTheDocument();
|
expect(screen.getByText("Anthropic")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
await advanceThroughSteps(renderResult, ["skip", "skip"]);
|
await advanceThroughSteps(renderResult, ["skip", "skip", "next"]);
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(screen.getByText("Create Your First Task")).toBeInTheDocument();
|
expect(screen.getByText("Create Your First Task")).toBeInTheDocument();
|
||||||
@@ -830,7 +836,7 @@ describe("onboarding flow integration", () => {
|
|||||||
|
|
||||||
const renderResult = renderModal();
|
const renderResult = renderModal();
|
||||||
|
|
||||||
await advanceThroughSteps(renderResult, ["next", "next"]);
|
await advanceThroughSteps(renderResult, ["next", "next", "next"]);
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(screen.getByText("Create Your First Task")).toBeInTheDocument();
|
expect(screen.getByText("Create Your First Task")).toBeInTheDocument();
|
||||||
@@ -857,7 +863,7 @@ describe("onboarding flow integration", () => {
|
|||||||
|
|
||||||
const renderResult = renderModal();
|
const renderResult = renderModal();
|
||||||
|
|
||||||
await advanceThroughSteps(renderResult, ["next", "next"]);
|
await advanceThroughSteps(renderResult, ["next", "next", "next"]);
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(screen.getByText("Create Your First Task")).toBeInTheDocument();
|
expect(screen.getByText("Create Your First Task")).toBeInTheDocument();
|
||||||
@@ -869,7 +875,7 @@ describe("onboarding flow integration", () => {
|
|||||||
it("skip warnings: Import from GitHub CTA shows connection requirement note when GitHub not connected", async () => {
|
it("skip warnings: Import from GitHub CTA shows connection requirement note when GitHub not connected", async () => {
|
||||||
const renderResult = renderModal();
|
const renderResult = renderModal();
|
||||||
|
|
||||||
await advanceThroughSteps(renderResult, ["next", "next"]);
|
await advanceThroughSteps(renderResult, ["next", "next", "next"]);
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(screen.getByText("Create Your First Task")).toBeInTheDocument();
|
expect(screen.getByText("Create Your First Task")).toBeInTheDocument();
|
||||||
@@ -888,7 +894,7 @@ describe("onboarding flow integration", () => {
|
|||||||
|
|
||||||
const renderResult = renderModal();
|
const renderResult = renderModal();
|
||||||
|
|
||||||
await advanceThroughSteps(renderResult, ["next", "next"]);
|
await advanceThroughSteps(renderResult, ["next", "next", "next"]);
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(screen.getByText("Create Your First Task")).toBeInTheDocument();
|
expect(screen.getByText("Create Your First Task")).toBeInTheDocument();
|
||||||
@@ -906,7 +912,7 @@ describe("onboarding flow integration", () => {
|
|||||||
it("task creation flow: inline task creation shows success state and marks onboarding complete", async () => {
|
it("task creation flow: inline task creation shows success state and marks onboarding complete", async () => {
|
||||||
const renderResult = renderModal();
|
const renderResult = renderModal();
|
||||||
|
|
||||||
await advanceThroughSteps(renderResult, ["next", "next"]);
|
await advanceThroughSteps(renderResult, ["next", "next", "next"]);
|
||||||
await simulateFirstTaskCreation(renderResult, "Ship onboarding telemetry");
|
await simulateFirstTaskCreation(renderResult, "Ship onboarding telemetry");
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
@@ -926,7 +932,7 @@ describe("onboarding flow integration", () => {
|
|||||||
it("task creation flow: View Task button navigates and completes onboarding", async () => {
|
it("task creation flow: View Task button navigates and completes onboarding", async () => {
|
||||||
const renderResult = renderModal();
|
const renderResult = renderModal();
|
||||||
|
|
||||||
await advanceThroughSteps(renderResult, ["next", "next"]);
|
await advanceThroughSteps(renderResult, ["next", "next", "next"]);
|
||||||
await simulateFirstTaskCreation(renderResult, "View task flow");
|
await simulateFirstTaskCreation(renderResult, "View task flow");
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
@@ -971,6 +977,12 @@ describe("onboarding flow integration", () => {
|
|||||||
|
|
||||||
fireEvent.click(screen.getByRole("button", { name: "Next →" }));
|
fireEvent.click(screen.getByRole("button", { name: "Next →" }));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByRole("button", { name: "Next →" })).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "Next →" }));
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(screen.getByText("Create Your First Task")).toBeInTheDocument();
|
expect(screen.getByText("Create Your First Task")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
@@ -995,7 +1007,7 @@ describe("onboarding flow integration", () => {
|
|||||||
it("task creation flow: Finish Setup button (without creating task) completes onboarding", async () => {
|
it("task creation flow: Finish Setup button (without creating task) completes onboarding", async () => {
|
||||||
const renderResult = renderModal();
|
const renderResult = renderModal();
|
||||||
|
|
||||||
await advanceThroughSteps(renderResult, ["next", "next"]);
|
await advanceThroughSteps(renderResult, ["next", "next", "next"]);
|
||||||
|
|
||||||
fireEvent.click(screen.getByRole("button", { name: "Finish Setup" }));
|
fireEvent.click(screen.getByRole("button", { name: "Finish Setup" }));
|
||||||
|
|
||||||
@@ -1009,7 +1021,7 @@ describe("onboarding flow integration", () => {
|
|||||||
it("task creation flow: Create a New Task CTA completes onboarding and triggers external task dialog", async () => {
|
it("task creation flow: Create a New Task CTA completes onboarding and triggers external task dialog", async () => {
|
||||||
const renderResult = renderModal();
|
const renderResult = renderModal();
|
||||||
|
|
||||||
await advanceThroughSteps(renderResult, ["next", "next"]);
|
await advanceThroughSteps(renderResult, ["next", "next", "next"]);
|
||||||
|
|
||||||
fireEvent.click(screen.getByRole("button", { name: /Create a New Task/i }));
|
fireEvent.click(screen.getByRole("button", { name: /Create a New Task/i }));
|
||||||
|
|
||||||
@@ -1031,7 +1043,7 @@ describe("onboarding flow integration", () => {
|
|||||||
|
|
||||||
const renderResult = renderModal();
|
const renderResult = renderModal();
|
||||||
|
|
||||||
await advanceThroughSteps(renderResult, ["next", "next"]);
|
await advanceThroughSteps(renderResult, ["next", "next", "next"]);
|
||||||
|
|
||||||
fireEvent.click(screen.getByRole("button", { name: /Import from GitHub/i }));
|
fireEvent.click(screen.getByRole("button", { name: /Import from GitHub/i }));
|
||||||
|
|
||||||
@@ -1046,7 +1058,7 @@ describe("onboarding flow integration", () => {
|
|||||||
it("task creation flow: validation error on empty description does not call createTask", async () => {
|
it("task creation flow: validation error on empty description does not call createTask", async () => {
|
||||||
const renderResult = renderModal();
|
const renderResult = renderModal();
|
||||||
|
|
||||||
await advanceThroughSteps(renderResult, ["next", "next"]);
|
await advanceThroughSteps(renderResult, ["next", "next", "next"]);
|
||||||
|
|
||||||
fireEvent.click(screen.getByTestId("onboarding-first-task-submit"));
|
fireEvent.click(screen.getByTestId("onboarding-first-task-submit"));
|
||||||
|
|
||||||
@@ -1058,7 +1070,7 @@ describe("onboarding flow integration", () => {
|
|||||||
mockCreateTask.mockRejectedValueOnce(new Error("Task API unavailable"));
|
mockCreateTask.mockRejectedValueOnce(new Error("Task API unavailable"));
|
||||||
const renderResult = renderModal();
|
const renderResult = renderModal();
|
||||||
|
|
||||||
await advanceThroughSteps(renderResult, ["next", "next"]);
|
await advanceThroughSteps(renderResult, ["next", "next", "next"]);
|
||||||
await simulateFirstTaskCreation(renderResult, "Retryable task");
|
await simulateFirstTaskCreation(renderResult, "Retryable task");
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
@@ -1072,7 +1084,7 @@ describe("onboarding flow integration", () => {
|
|||||||
it("task creation flow: Get Started button on complete step closes the modal", async () => {
|
it("task creation flow: Get Started button on complete step closes the modal", async () => {
|
||||||
const renderResult = renderModal();
|
const renderResult = renderModal();
|
||||||
|
|
||||||
await advanceThroughSteps(renderResult, ["next", "next"]);
|
await advanceThroughSteps(renderResult, ["next", "next", "next"]);
|
||||||
|
|
||||||
fireEvent.click(screen.getByRole("button", { name: "Finish Setup" }));
|
fireEvent.click(screen.getByRole("button", { name: "Finish Setup" }));
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
* from where they left off if they dismiss the modal without completing.
|
* from where they left off if they dismiss the modal without completing.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export type OnboardingStep = "ai-setup" | "github" | "first-task" | "complete";
|
export type OnboardingStep = "ai-setup" | "github" | "project-setup" | "first-task" | "complete";
|
||||||
|
|
||||||
interface OnboardingState {
|
interface OnboardingState {
|
||||||
currentStep: OnboardingStep | string; // string allows for future unknown steps
|
currentStep: OnboardingStep | string; // string allows for future unknown steps
|
||||||
@@ -38,6 +38,12 @@ const DEFAULT_COMPLETED = false;
|
|||||||
const DEFAULT_STEP_DATA: Partial<Record<OnboardingStep, Record<string, unknown>>> = {};
|
const DEFAULT_STEP_DATA: Partial<Record<OnboardingStep, Record<string, unknown>>> = {};
|
||||||
const DEFAULT_POST_ONBOARDING_DISMISSED_AT: string | undefined = undefined;
|
const DEFAULT_POST_ONBOARDING_DISMISSED_AT: string | undefined = undefined;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ordered onboarding flow steps before completion.
|
||||||
|
* Keep this list in sync with ModelOnboardingModal's stepper rendering and navigation.
|
||||||
|
*/
|
||||||
|
export const ONBOARDING_FLOW_STEPS = ["ai-setup", "github", "project-setup", "first-task"] as const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Step labels for display in the resume card.
|
* Step labels for display in the resume card.
|
||||||
* Fallback for unknown step IDs uses the raw key with title-case formatting.
|
* Fallback for unknown step IDs uses the raw key with title-case formatting.
|
||||||
@@ -45,6 +51,7 @@ const DEFAULT_POST_ONBOARDING_DISMISSED_AT: string | undefined = undefined;
|
|||||||
export const ONBOARDING_STEP_LABELS: Record<OnboardingStep, string> = {
|
export const ONBOARDING_STEP_LABELS: Record<OnboardingStep, string> = {
|
||||||
"ai-setup": "AI Setup",
|
"ai-setup": "AI Setup",
|
||||||
github: "GitHub",
|
github: "GitHub",
|
||||||
|
"project-setup": "Project",
|
||||||
"first-task": "First Task",
|
"first-task": "First Task",
|
||||||
complete: "Complete",
|
complete: "Complete",
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ vi.mock("../../api", () => ({
|
|||||||
|
|
||||||
vi.mock("../../components/model-onboarding-state", () => ({
|
vi.mock("../../components/model-onboarding-state", () => ({
|
||||||
isOnboardingCompleted: (...args: unknown[]) => mockIsOnboardingCompleted(...args),
|
isOnboardingCompleted: (...args: unknown[]) => mockIsOnboardingCompleted(...args),
|
||||||
|
ONBOARDING_FLOW_STEPS: ["ai-setup", "github", "project-setup", "first-task"],
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock("../../components/onboarding-events", () => ({
|
vi.mock("../../components/onboarding-events", () => ({
|
||||||
|
|||||||
Reference in New Issue
Block a user