mirror of
https://github.com/langflow-ai/langflow.git
synced 2026-07-26 10:59:28 +08:00
feat: refresh deployed success step (#12853)
* feat(deploy): refresh deployed success step * refactor(deploy): split modal sections * test(deploy): cover success step * fix(deploy): rename flows step copy * fix(tests): update deployment test selectors for renamed step heading Tests were failing because the UI step heading changed from "Attach Flows" to "Flows". Updated three test files to use the new selector text. Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com> * test(deployments): fix stepper e2e selectors --------- Co-authored-by: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
@ -0,0 +1,75 @@
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import DeploymentStepperFooter from "../components/deployment-stepper-footer";
|
||||
|
||||
jest.mock(
|
||||
"@/components/common/genericIconComponent",
|
||||
() =>
|
||||
function MockIcon({ name }: { name: string }) {
|
||||
return <span data-testid={`icon-${name}`} />;
|
||||
},
|
||||
);
|
||||
|
||||
describe("DeploymentStepperFooter", () => {
|
||||
it("shows done state controls when deployed", () => {
|
||||
render(
|
||||
<DeploymentStepperFooter
|
||||
canGoNext={true}
|
||||
currentStep={4}
|
||||
isCreatingAccount={false}
|
||||
isDeployed={true}
|
||||
isDeploying={false}
|
||||
isInDeployPhase={true}
|
||||
isFinalStep={true}
|
||||
minStep={1}
|
||||
actionIcon="Rocket"
|
||||
actionLabel="Deploy"
|
||||
progressLabel="Deploying..."
|
||||
onBack={jest.fn()}
|
||||
onCancel={jest.fn()}
|
||||
onClose={jest.fn()}
|
||||
onPrimaryAction={jest.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByRole("button", { name: "Done" })).toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByRole("button", { name: "Cancel" }),
|
||||
).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByRole("button", { name: "Back" }),
|
||||
).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByTestId("deployment-stepper-next"),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("calls onClose from done button", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onClose = jest.fn();
|
||||
|
||||
render(
|
||||
<DeploymentStepperFooter
|
||||
canGoNext={true}
|
||||
currentStep={4}
|
||||
isCreatingAccount={false}
|
||||
isDeployed={true}
|
||||
isDeploying={false}
|
||||
isInDeployPhase={true}
|
||||
isFinalStep={true}
|
||||
minStep={1}
|
||||
actionIcon="Rocket"
|
||||
actionLabel="Deploy"
|
||||
progressLabel="Deploying..."
|
||||
onBack={jest.fn()}
|
||||
onCancel={jest.fn()}
|
||||
onClose={onClose}
|
||||
onPrimaryAction={jest.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
await user.click(screen.getByRole("button", { name: "Done" }));
|
||||
|
||||
expect(onClose).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@ -345,7 +345,7 @@ describe("Step labels", () => {
|
||||
renderModal();
|
||||
expect(screen.getByText("Provider")).toBeInTheDocument();
|
||||
expect(screen.getByText("Type")).toBeInTheDocument();
|
||||
expect(screen.getByText("Attach Flows")).toBeInTheDocument();
|
||||
expect(screen.getByText("Flows")).toBeInTheDocument();
|
||||
expect(screen.getByText("Review")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
@ -355,7 +355,7 @@ describe("Step labels", () => {
|
||||
renderModal({ editingDeployment: makeDeployment() });
|
||||
expect(screen.queryByText("Provider")).not.toBeInTheDocument();
|
||||
expect(screen.getByText("Type")).toBeInTheDocument();
|
||||
expect(screen.getByText("Attach Flows")).toBeInTheDocument();
|
||||
expect(screen.getByText("Flows")).toBeInTheDocument();
|
||||
expect(screen.getByText("Review")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@ -0,0 +1,86 @@
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import DeploymentSuccessContent from "../components/deployment-success-content";
|
||||
|
||||
jest.mock(
|
||||
"@/components/common/genericIconComponent",
|
||||
() =>
|
||||
function MockIcon({ name }: { name: string }) {
|
||||
return <span data-testid={`icon-${name}`} />;
|
||||
},
|
||||
);
|
||||
|
||||
describe("DeploymentSuccessContent", () => {
|
||||
it("renders success copy and provider link", () => {
|
||||
render(
|
||||
<DeploymentSuccessContent
|
||||
deploymentName="My Agent"
|
||||
providerName="watsonx Orchestrate"
|
||||
providerUrl="https://www.ibm.com/products/watsonx-orchestrate"
|
||||
showTestButton={true}
|
||||
onTest={jest.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByText("Deployment successful")).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText("Deployed to watsonx Orchestrate as draft"),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByRole("link", { name: /watsonx Orchestrate/i }),
|
||||
).toHaveAttribute(
|
||||
"href",
|
||||
"https://www.ibm.com/products/watsonx-orchestrate",
|
||||
);
|
||||
});
|
||||
|
||||
it("shows test button and calls handler", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onTest = jest.fn();
|
||||
|
||||
render(
|
||||
<DeploymentSuccessContent
|
||||
deploymentName="My Agent"
|
||||
providerName="watsonx Orchestrate"
|
||||
providerUrl="https://www.ibm.com/products/watsonx-orchestrate"
|
||||
showTestButton={true}
|
||||
onTest={onTest}
|
||||
/>,
|
||||
);
|
||||
|
||||
await user.click(screen.getByRole("button", { name: "Test Deployment" }));
|
||||
|
||||
expect(onTest).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("hides test button without deployment name", () => {
|
||||
render(
|
||||
<DeploymentSuccessContent
|
||||
providerName="watsonx Orchestrate"
|
||||
providerUrl="https://www.ibm.com/products/watsonx-orchestrate"
|
||||
showTestButton={true}
|
||||
onTest={jest.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(
|
||||
screen.queryByRole("button", { name: "Test Deployment" }),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("hides test button when showTestButton is false", () => {
|
||||
render(
|
||||
<DeploymentSuccessContent
|
||||
deploymentName="My Agent"
|
||||
providerName="watsonx Orchestrate"
|
||||
providerUrl="https://www.ibm.com/products/watsonx-orchestrate"
|
||||
showTestButton={false}
|
||||
onTest={jest.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(
|
||||
screen.queryByRole("button", { name: "Test Deployment" }),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@ -230,14 +230,14 @@ beforeEach(() => {
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("Three-panel layout rendering", () => {
|
||||
it("renders the Attach Flows heading", () => {
|
||||
it("renders the Flows heading", () => {
|
||||
render(<StepAttachFlows />);
|
||||
expect(screen.getByText("Attach Flows")).toBeInTheDocument();
|
||||
expect(screen.getByText("Flows")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders the Available Flows panel header", () => {
|
||||
it("renders the Available panel header", () => {
|
||||
render(<StepAttachFlows />);
|
||||
expect(screen.getByText("Available Flows")).toBeInTheDocument();
|
||||
expect(screen.getByText("Available")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders flow items in the list", () => {
|
||||
|
||||
@ -0,0 +1,89 @@
|
||||
import ForwardedIconComponent from "@/components/common/genericIconComponent";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
interface DeploymentStepperFooterProps {
|
||||
canGoNext: boolean;
|
||||
currentStep: number;
|
||||
isCreatingAccount: boolean;
|
||||
isDeployed: boolean;
|
||||
isDeploying: boolean;
|
||||
isInDeployPhase: boolean;
|
||||
isFinalStep: boolean;
|
||||
minStep: number;
|
||||
actionIcon: string;
|
||||
actionLabel: string;
|
||||
progressLabel: string;
|
||||
onBack: () => void;
|
||||
onCancel: () => void;
|
||||
onClose: () => void;
|
||||
onPrimaryAction: () => void;
|
||||
}
|
||||
|
||||
export default function DeploymentStepperFooter({
|
||||
canGoNext,
|
||||
currentStep,
|
||||
isCreatingAccount,
|
||||
isDeployed,
|
||||
isDeploying,
|
||||
isInDeployPhase,
|
||||
isFinalStep,
|
||||
minStep,
|
||||
actionIcon,
|
||||
actionLabel,
|
||||
progressLabel,
|
||||
onBack,
|
||||
onCancel,
|
||||
onClose,
|
||||
onPrimaryAction,
|
||||
}: DeploymentStepperFooterProps) {
|
||||
return (
|
||||
<div className="flex items-center justify-between border-t border-border px-6 py-4">
|
||||
{isDeployed ? (
|
||||
<div />
|
||||
) : (
|
||||
<Button variant="ghost" onClick={onCancel}>
|
||||
Cancel
|
||||
</Button>
|
||||
)}
|
||||
<div className="flex items-center gap-3">
|
||||
{!isDeployed && (
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={onBack}
|
||||
disabled={currentStep === minStep || isDeploying}
|
||||
>
|
||||
Back
|
||||
</Button>
|
||||
)}
|
||||
{!isInDeployPhase && (
|
||||
<Button
|
||||
onClick={onPrimaryAction}
|
||||
disabled={!canGoNext || isCreatingAccount}
|
||||
data-testid="deployment-stepper-next"
|
||||
>
|
||||
{isFinalStep ? (
|
||||
<>
|
||||
<ForwardedIconComponent name={actionIcon} className="h-4 w-4" />
|
||||
{actionLabel}
|
||||
</>
|
||||
) : isCreatingAccount ? (
|
||||
"Connecting..."
|
||||
) : (
|
||||
"Next"
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
{isDeploying && (
|
||||
<Button disabled data-testid="deployment-stepper-next">
|
||||
<ForwardedIconComponent
|
||||
name={actionIcon}
|
||||
className="h-4 w-4 animate-pulse"
|
||||
/>
|
||||
{progressLabel}
|
||||
</Button>
|
||||
)}
|
||||
{isDeployed && <Button onClick={onClose}>Done</Button>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -1,7 +1,5 @@
|
||||
import { useMemo, useState } from "react";
|
||||
import { useParams } from "react-router-dom";
|
||||
import ForwardedIconComponent from "@/components/common/genericIconComponent";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@ -21,7 +19,9 @@ import {
|
||||
} from "../contexts/deployment-stepper-context";
|
||||
import { useErrorAlert } from "../hooks/use-error-alert";
|
||||
import type { Deployment, DeploymentProvider, ProviderAccount } from "../types";
|
||||
import DeploymentStepper from "./deployment-stepper";
|
||||
import DeploymentStepper, { CREATE_DEPLOYED_STEPS } from "./deployment-stepper";
|
||||
import DeploymentStepperFooter from "./deployment-stepper-footer";
|
||||
import DeploymentSuccessContent from "./deployment-success-content";
|
||||
import StepAttachFlows from "./step-attach-flows";
|
||||
import StepDeployStatus from "./step-deploy-status";
|
||||
import StepProvider from "./step-provider";
|
||||
@ -206,6 +206,7 @@ function DeploymentStepperModalContent({
|
||||
canGoNext,
|
||||
handleNext,
|
||||
handleBack,
|
||||
selectedProvider,
|
||||
selectedInstance,
|
||||
setSelectedInstance,
|
||||
needsProviderAccountCreation,
|
||||
@ -225,6 +226,8 @@ function DeploymentStepperModalContent({
|
||||
const isDeploying = deploymentPhase === "deploying";
|
||||
const isDeployed = deploymentPhase === "deployed";
|
||||
const isInDeployPhase = isDeploying || isDeployed;
|
||||
const providerConsoleUrl = "https://www.ibm.com/products/watsonx-orchestrate";
|
||||
const providerDisplayName = selectedProvider?.name ?? "watsonx Orchestrate";
|
||||
|
||||
// In edit mode, steps are shifted: 1=Type, 2=Attach, 3=Review.
|
||||
const logicalStep = isEditMode ? currentStep + 1 : currentStep;
|
||||
@ -316,19 +319,40 @@ function DeploymentStepperModalContent({
|
||||
className="text-center text-2xl font-semibold"
|
||||
data-testid="stepper-modal-title"
|
||||
>
|
||||
{isEditMode ? "Update Deployment" : "Create New Deployment"}
|
||||
{isDeployed && !isEditMode
|
||||
? "Deployed"
|
||||
: isEditMode
|
||||
? "Update Deployment"
|
||||
: "Create New Deployment"}
|
||||
</h2>
|
||||
<DeploymentStepper />
|
||||
<DeploymentStepper
|
||||
steps={!isEditMode && isDeployed ? CREATE_DEPLOYED_STEPS : undefined}
|
||||
currentStepOverride={!isEditMode && isDeployed ? 4 : undefined}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Content box: step content + footer */}
|
||||
<div className="mx-4 mb-4 mt-4 flex flex-1 flex-col overflow-hidden rounded-lg border border-border bg-background">
|
||||
<div className="flex min-h-0 flex-1 flex-col overflow-y-auto px-6 py-2">
|
||||
{isInDeployPhase ? (
|
||||
<StepDeployStatus
|
||||
phase={isDeploying ? "deploying" : "deployed"}
|
||||
deploymentName={createdDeployment?.name}
|
||||
/>
|
||||
isDeployed && !isEditMode ? (
|
||||
<DeploymentSuccessContent
|
||||
deploymentName={createdDeployment?.name}
|
||||
providerName={providerDisplayName}
|
||||
providerUrl={providerConsoleUrl}
|
||||
showTestButton={
|
||||
!!onTestDeployment &&
|
||||
!!createdDeployment &&
|
||||
!!selectedInstance?.id
|
||||
}
|
||||
onTest={handleTest}
|
||||
/>
|
||||
) : (
|
||||
<StepDeployStatus
|
||||
phase={isDeploying ? "deploying" : "deployed"}
|
||||
deploymentName={createdDeployment?.name}
|
||||
/>
|
||||
)
|
||||
) : (
|
||||
<>
|
||||
{logicalStep === 1 && <StepProvider />}
|
||||
@ -339,61 +363,23 @@ function DeploymentStepperModalContent({
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="flex items-center justify-between border-t border-border px-6 py-4">
|
||||
<Button variant="ghost" onClick={() => setOpen(false)}>
|
||||
{isDeployed ? "Close" : "Cancel"}
|
||||
</Button>
|
||||
<div className="flex items-center gap-3">
|
||||
{!isDeployed && (
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleBack}
|
||||
disabled={currentStep === minStep || isDeploying}
|
||||
>
|
||||
Back
|
||||
</Button>
|
||||
)}
|
||||
{!isInDeployPhase && (
|
||||
<Button
|
||||
onClick={isFinalStep ? handleDeploy : handleStepNext}
|
||||
disabled={!canGoNext || isCreatingAccount}
|
||||
data-testid="deployment-stepper-next"
|
||||
>
|
||||
{isFinalStep ? (
|
||||
<>
|
||||
<ForwardedIconComponent
|
||||
name={actionIcon}
|
||||
className="h-4 w-4"
|
||||
/>
|
||||
{actionLabel}
|
||||
</>
|
||||
) : isCreatingAccount ? (
|
||||
"Connecting..."
|
||||
) : (
|
||||
"Next"
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
{isDeploying && (
|
||||
<Button disabled data-testid="deployment-stepper-next">
|
||||
<ForwardedIconComponent
|
||||
name={actionIcon}
|
||||
className="h-4 w-4 animate-pulse"
|
||||
/>
|
||||
{progressLabel}
|
||||
</Button>
|
||||
)}
|
||||
{isDeployed && onTestDeployment && !isEditMode && (
|
||||
<Button
|
||||
data-testid="deployment-stepper-test"
|
||||
onClick={handleTest}
|
||||
>
|
||||
Test
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<DeploymentStepperFooter
|
||||
canGoNext={canGoNext}
|
||||
currentStep={currentStep}
|
||||
isCreatingAccount={isCreatingAccount}
|
||||
isDeployed={isDeployed}
|
||||
isDeploying={isDeploying}
|
||||
isInDeployPhase={isInDeployPhase}
|
||||
isFinalStep={isFinalStep}
|
||||
minStep={minStep}
|
||||
actionIcon={actionIcon}
|
||||
actionLabel={actionLabel}
|
||||
progressLabel={progressLabel}
|
||||
onBack={handleBack}
|
||||
onCancel={() => setOpen(false)}
|
||||
onClose={() => setOpen(false)}
|
||||
onPrimaryAction={isFinalStep ? handleDeploy : handleStepNext}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@ -1,25 +1,41 @@
|
||||
import { cn } from "@/utils/utils";
|
||||
import { useDeploymentStepper } from "../contexts/deployment-stepper-context";
|
||||
|
||||
const CREATE_STEPS = [
|
||||
export const CREATE_STEPS = [
|
||||
{ number: 1, label: "Provider" },
|
||||
{ number: 2, label: "Type" },
|
||||
{ number: 3, label: "Attach Flows" },
|
||||
{ number: 3, label: "Flows" },
|
||||
{ number: 4, label: "Review" },
|
||||
] as const;
|
||||
|
||||
export const CREATE_DEPLOYED_STEPS = [
|
||||
{ number: 1, label: "Provider" },
|
||||
{ number: 2, label: "Type" },
|
||||
{ number: 3, label: "Flows" },
|
||||
{ number: 4, label: "Deployed" },
|
||||
] as const;
|
||||
|
||||
const EDIT_STEPS = [
|
||||
{ number: 1, label: "Type" },
|
||||
{ number: 2, label: "Attach Flows" },
|
||||
{ number: 2, label: "Flows" },
|
||||
{ number: 3, label: "Review" },
|
||||
] as const;
|
||||
|
||||
export const DEPLOYMENT_STEPS = CREATE_STEPS;
|
||||
|
||||
export default function DeploymentStepper() {
|
||||
interface DeploymentStepperProps {
|
||||
steps?: readonly { number: number; label: string }[];
|
||||
currentStepOverride?: number;
|
||||
}
|
||||
|
||||
export default function DeploymentStepper({
|
||||
steps: stepsProp,
|
||||
currentStepOverride,
|
||||
}: DeploymentStepperProps) {
|
||||
const { currentStep, isEditMode } = useDeploymentStepper();
|
||||
const steps = isEditMode ? EDIT_STEPS : CREATE_STEPS;
|
||||
const progressPercent = ((currentStep - 1) / (steps.length - 1)) * 100;
|
||||
const steps = stepsProp ?? (isEditMode ? EDIT_STEPS : CREATE_STEPS);
|
||||
const activeStep = currentStepOverride ?? currentStep;
|
||||
const progressPercent = ((activeStep - 1) / (steps.length - 1)) * 100;
|
||||
|
||||
return (
|
||||
<div className="relative mx-auto h-[52px] w-full max-w-[700px]">
|
||||
@ -35,7 +51,7 @@ export default function DeploymentStepper() {
|
||||
<div
|
||||
className={cn(
|
||||
"flex h-8 w-8 items-center justify-center rounded-full text-sm font-medium transition-colors",
|
||||
currentStep >= step.number
|
||||
activeStep >= step.number
|
||||
? "bg-foreground text-background"
|
||||
: "bg-muted text-muted-foreground",
|
||||
)}
|
||||
@ -45,7 +61,7 @@ export default function DeploymentStepper() {
|
||||
<span
|
||||
className={cn(
|
||||
"whitespace-nowrap text-xs text-foreground",
|
||||
currentStep >= step.number && "font-medium",
|
||||
activeStep >= step.number && "font-medium",
|
||||
)}
|
||||
>
|
||||
{step.label}
|
||||
|
||||
@ -0,0 +1,63 @@
|
||||
import ForwardedIconComponent from "@/components/common/genericIconComponent";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
interface DeploymentSuccessContentProps {
|
||||
deploymentName?: string;
|
||||
providerName: string;
|
||||
providerUrl: string;
|
||||
showTestButton: boolean;
|
||||
onTest: () => void;
|
||||
}
|
||||
|
||||
export default function DeploymentSuccessContent({
|
||||
deploymentName,
|
||||
providerName,
|
||||
providerUrl,
|
||||
showTestButton,
|
||||
onTest,
|
||||
}: DeploymentSuccessContentProps) {
|
||||
return (
|
||||
<div className="flex flex-1 flex-col items-center justify-center gap-8 py-10 text-center">
|
||||
<div className="flex h-16 w-16 items-center justify-center rounded-full border-2 border-accent-emerald">
|
||||
<ForwardedIconComponent
|
||||
name="Check"
|
||||
className="h-8 w-8 text-accent-emerald-foreground"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<h3 className="font-sans text-2xl font-semibold tracking-normal text-foreground">
|
||||
Deployment successful
|
||||
</h3>
|
||||
<p className="font-sans text-base font-normal text-muted-foreground">
|
||||
Deployed to {providerName} as draft
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-1 font-sans text-sm font-normal text-muted-foreground">
|
||||
<span>Publish from Draft to Live in</span>
|
||||
<a
|
||||
href={providerUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-1 font-semibold text-foreground hover:underline"
|
||||
>
|
||||
{providerName}
|
||||
<ForwardedIconComponent name="ArrowUpRight" className="h-4 w-4" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{showTestButton && deploymentName && (
|
||||
<Button
|
||||
variant="outline"
|
||||
className="h-11 w-full max-w-lg gap-2 rounded-lg border-input font-sans text-base font-semibold text-foreground hover:bg-input"
|
||||
data-testid="deployment-stepper-test"
|
||||
onClick={onTest}
|
||||
>
|
||||
<ForwardedIconComponent name="Play" className="h-5 w-5" />
|
||||
Test Deployment
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -29,7 +29,7 @@ export const FlowListPanel = memo(function FlowListPanel({
|
||||
return (
|
||||
<div className="flex w-[280px] flex-shrink-0 flex-col border-r border-border">
|
||||
<div className="border-b border-border p-4 text-sm text-muted-foreground">
|
||||
Available Flows
|
||||
Available
|
||||
</div>
|
||||
<div className="flex-1 space-y-1 overflow-y-auto p-2">
|
||||
{flows.map((flow) => {
|
||||
|
||||
@ -259,7 +259,7 @@ export default function StepAttachFlows() {
|
||||
|
||||
return (
|
||||
<div className="flex min-h-0 flex-1 flex-col gap-4 py-3">
|
||||
<h2 className="text-lg font-semibold">Attach Flows</h2>
|
||||
<h2 className="text-lg font-semibold">Flows</h2>
|
||||
|
||||
<div className="flex min-h-0 flex-1 overflow-hidden rounded-xl border border-border">
|
||||
<FlowListPanel
|
||||
|
||||
@ -150,12 +150,13 @@ async function selectProvider(page: Page) {
|
||||
async function goToStepType(page: Page) {
|
||||
await selectProvider(page);
|
||||
await page.getByTestId("deployment-stepper-next").click();
|
||||
// Wait for the Type step heading
|
||||
await page.waitForSelector("text=Deployment Type");
|
||||
await expect(
|
||||
page.getByRole("heading", { name: /Deployment Type/i }),
|
||||
).toBeVisible();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helper: navigate steps 1 → 2 → 3 (provider → type → attach flows)
|
||||
// Helper: navigate steps 1 → 2 → 3 (provider → type → flows)
|
||||
// ---------------------------------------------------------------------------
|
||||
async function goToStepAttachFlows(page: Page) {
|
||||
await goToStepType(page);
|
||||
@ -165,13 +166,13 @@ async function goToStepAttachFlows(page: Page) {
|
||||
// Select the LLM model
|
||||
await page.getByRole("combobox").click();
|
||||
await page.getByRole("option", { name: "ibm/granite-13b-chat" }).click();
|
||||
// Advance to attach flows
|
||||
// Advance to flows
|
||||
await page.getByTestId("deployment-stepper-next").click();
|
||||
await page.waitForSelector("text=Attach Flows");
|
||||
await expect(page.getByRole("heading", { name: /^Flows$/i })).toBeVisible();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helper: navigate all steps through attach flows and select a flow+version
|
||||
// Helper: navigate all steps through flows and select a flow+version
|
||||
// ---------------------------------------------------------------------------
|
||||
async function goToStepReview(page: Page) {
|
||||
await goToStepAttachFlows(page);
|
||||
@ -189,7 +190,9 @@ async function goToStepReview(page: Page) {
|
||||
}
|
||||
// Advance to review
|
||||
await page.getByTestId("deployment-stepper-next").click();
|
||||
await page.waitForSelector("text=Review & Confirm");
|
||||
await expect(
|
||||
page.getByRole("heading", { name: /Review & Confirm/i }),
|
||||
).toBeVisible();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@ -197,7 +200,9 @@ async function goToStepReview(page: Page) {
|
||||
// ---------------------------------------------------------------------------
|
||||
test(
|
||||
"deployment-create: opens stepper on New Deployment click",
|
||||
{ tag: ["@deployment", "@workspace"] },
|
||||
{
|
||||
tag: ["@deployment", "@workspace"],
|
||||
},
|
||||
async ({ page }) => {
|
||||
test.skip(
|
||||
process.env.LANGFLOW_FEATURE_WXO_DEPLOYMENTS !== "true",
|
||||
@ -220,7 +225,9 @@ test(
|
||||
// ---------------------------------------------------------------------------
|
||||
test(
|
||||
"deployment-create: step 1 provider - Next disabled without selection, enabled after selecting",
|
||||
{ tag: ["@deployment", "@workspace"] },
|
||||
{
|
||||
tag: ["@deployment", "@workspace"],
|
||||
},
|
||||
async ({ page }) => {
|
||||
test.skip(
|
||||
process.env.LANGFLOW_FEATURE_WXO_DEPLOYMENTS !== "true",
|
||||
@ -245,7 +252,9 @@ test(
|
||||
// ---------------------------------------------------------------------------
|
||||
test(
|
||||
"deployment-create: step 2 type - fill name and select type to enable Next",
|
||||
{ tag: ["@deployment", "@workspace"] },
|
||||
{
|
||||
tag: ["@deployment", "@workspace"],
|
||||
},
|
||||
async ({ page }) => {
|
||||
test.skip(
|
||||
process.env.LANGFLOW_FEATURE_WXO_DEPLOYMENTS !== "true",
|
||||
@ -280,11 +289,13 @@ test(
|
||||
);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Test 4: Step 3 (Attach Flows) — select a flow and version, Next enables
|
||||
// Test 4: Step 3 (Flows) — select a flow and version, Next enables
|
||||
// ---------------------------------------------------------------------------
|
||||
test(
|
||||
"deployment-create: step 3 attach flows - select flow and version enables Next",
|
||||
{ tag: ["@deployment", "@workspace"] },
|
||||
{
|
||||
tag: ["@deployment", "@workspace"],
|
||||
},
|
||||
async ({ page }) => {
|
||||
test.skip(
|
||||
process.env.LANGFLOW_FEATURE_WXO_DEPLOYMENTS !== "true",
|
||||
@ -331,7 +342,9 @@ test(
|
||||
// ---------------------------------------------------------------------------
|
||||
test(
|
||||
"deployment-create: step 4 review - shows review content and Deploy button text",
|
||||
{ tag: ["@deployment", "@workspace"] },
|
||||
{
|
||||
tag: ["@deployment", "@workspace"],
|
||||
},
|
||||
async ({ page }) => {
|
||||
test.skip(
|
||||
process.env.LANGFLOW_FEATURE_WXO_DEPLOYMENTS !== "true",
|
||||
@ -361,7 +374,9 @@ test(
|
||||
// ---------------------------------------------------------------------------
|
||||
test(
|
||||
"deployment-create: clicking Deploy triggers POST and shows deploy status",
|
||||
{ tag: ["@deployment", "@workspace"] },
|
||||
{
|
||||
tag: ["@deployment", "@workspace"],
|
||||
},
|
||||
async ({ page }) => {
|
||||
test.skip(
|
||||
process.env.LANGFLOW_FEATURE_WXO_DEPLOYMENTS !== "true",
|
||||
@ -414,7 +429,9 @@ test(
|
||||
// ---------------------------------------------------------------------------
|
||||
test(
|
||||
"deployment-create: user can change tool name on review step",
|
||||
{ tag: ["@deployment", "@workspace"] },
|
||||
{
|
||||
tag: ["@deployment", "@workspace"],
|
||||
},
|
||||
async ({ page }) => {
|
||||
test.skip(
|
||||
process.env.LANGFLOW_FEATURE_WXO_DEPLOYMENTS !== "true",
|
||||
@ -451,7 +468,9 @@ test(
|
||||
// ---------------------------------------------------------------------------
|
||||
test(
|
||||
"deployment-create: review step shows error when tool name already exists in provider",
|
||||
{ tag: ["@deployment", "@workspace"] },
|
||||
{
|
||||
tag: ["@deployment", "@workspace"],
|
||||
},
|
||||
async ({ page }) => {
|
||||
test.skip(
|
||||
process.env.LANGFLOW_FEATURE_WXO_DEPLOYMENTS !== "true",
|
||||
@ -476,7 +495,9 @@ test(
|
||||
// ---------------------------------------------------------------------------
|
||||
test(
|
||||
"deployment-create: review step shows no error when tool name is unique",
|
||||
{ tag: ["@deployment", "@workspace"] },
|
||||
{
|
||||
tag: ["@deployment", "@workspace"],
|
||||
},
|
||||
async ({ page }) => {
|
||||
test.skip(
|
||||
process.env.LANGFLOW_FEATURE_WXO_DEPLOYMENTS !== "true",
|
||||
@ -501,7 +522,9 @@ test(
|
||||
// ---------------------------------------------------------------------------
|
||||
test(
|
||||
"deployment-create: editing tool name to unique value clears duplicate error",
|
||||
{ tag: ["@deployment", "@workspace"] },
|
||||
{
|
||||
tag: ["@deployment", "@workspace"],
|
||||
},
|
||||
async ({ page }) => {
|
||||
test.skip(
|
||||
process.env.LANGFLOW_FEATURE_WXO_DEPLOYMENTS !== "true",
|
||||
|
||||
@ -83,9 +83,23 @@ async function openEditDialog(page: Parameters<typeof test>[2]["page"]) {
|
||||
await page.waitForSelector('[data-testid="stepper-modal-title"]');
|
||||
}
|
||||
|
||||
async function expectDeploymentTypeStep(
|
||||
page: Parameters<typeof test>[2]["page"],
|
||||
) {
|
||||
await expect(
|
||||
page.getByRole("heading", { name: /Deployment Type/i }),
|
||||
).toBeVisible();
|
||||
}
|
||||
|
||||
async function expectFlowsStep(page: Parameters<typeof test>[2]["page"]) {
|
||||
await expect(page.getByRole("heading", { name: /^Flows$/i })).toBeVisible();
|
||||
}
|
||||
|
||||
test(
|
||||
"Opens edit stepper from actions menu",
|
||||
{ tag: ["@release", "@workspace", "@api"] },
|
||||
{
|
||||
tag: ["@release", "@workspace", "@api"],
|
||||
},
|
||||
async ({ page }) => {
|
||||
test.skip(
|
||||
process.env.LANGFLOW_FEATURE_WXO_DEPLOYMENTS !== "true",
|
||||
@ -110,7 +124,9 @@ test(
|
||||
|
||||
test(
|
||||
"Edit mode skips provider step — starts at Type",
|
||||
{ tag: ["@release", "@workspace", "@api"] },
|
||||
{
|
||||
tag: ["@release", "@workspace", "@api"],
|
||||
},
|
||||
async ({ page }) => {
|
||||
test.skip(
|
||||
process.env.LANGFLOW_FEATURE_WXO_DEPLOYMENTS !== "true",
|
||||
@ -126,7 +142,7 @@ test(
|
||||
await openEditDialog(page);
|
||||
|
||||
// Wait for stepper body to render (parallel fetches must complete)
|
||||
await page.waitForSelector('h2:has-text("Deployment Type")');
|
||||
await expectDeploymentTypeStep(page);
|
||||
|
||||
await expect(page.getByText("Deployment Type")).toBeVisible();
|
||||
|
||||
@ -141,7 +157,9 @@ test(
|
||||
|
||||
test(
|
||||
"Name field pre-populated in edit mode",
|
||||
{ tag: ["@release", "@workspace", "@api"] },
|
||||
{
|
||||
tag: ["@release", "@workspace", "@api"],
|
||||
},
|
||||
async ({ page }) => {
|
||||
test.skip(
|
||||
process.env.LANGFLOW_FEATURE_WXO_DEPLOYMENTS !== "true",
|
||||
@ -157,7 +175,7 @@ test(
|
||||
await openEditDialog(page);
|
||||
|
||||
// Wait for stepper body to render
|
||||
await page.waitForSelector('h2:has-text("Deployment Type")');
|
||||
await expectDeploymentTypeStep(page);
|
||||
|
||||
const nameInput = page.getByPlaceholder("e.g., Sales Bot");
|
||||
await expect(nameInput).toHaveValue("Test Deployment");
|
||||
@ -166,7 +184,9 @@ test(
|
||||
|
||||
test(
|
||||
"Submitting PATCH closes modal",
|
||||
{ tag: ["@release", "@workspace", "@api"] },
|
||||
{
|
||||
tag: ["@release", "@workspace", "@api"],
|
||||
},
|
||||
async ({ page }) => {
|
||||
test.skip(
|
||||
process.env.LANGFLOW_FEATURE_WXO_DEPLOYMENTS !== "true",
|
||||
@ -182,13 +202,13 @@ test(
|
||||
await openEditDialog(page);
|
||||
|
||||
// Wait for stepper body to render (parallel fetches must complete)
|
||||
await page.waitForSelector('h2:has-text("Deployment Type")');
|
||||
await expectDeploymentTypeStep(page);
|
||||
|
||||
// Navigate through the stepper steps to reach Review
|
||||
// Step: Type → click Next
|
||||
await page.getByTestId("deployment-stepper-next").click();
|
||||
|
||||
// Step: Attach Flows → click Next
|
||||
// Step: Flows → click Next
|
||||
await page.getByTestId("deployment-stepper-next").click();
|
||||
|
||||
// Step: Review → click Update (final step)
|
||||
@ -209,7 +229,9 @@ test(
|
||||
|
||||
test(
|
||||
"Cancel during edit closes modal without calling PATCH",
|
||||
{ tag: ["@release", "@workspace", "@api"] },
|
||||
{
|
||||
tag: ["@release", "@workspace", "@api"],
|
||||
},
|
||||
async ({ page }) => {
|
||||
test.skip(
|
||||
process.env.LANGFLOW_FEATURE_WXO_DEPLOYMENTS !== "true",
|
||||
@ -225,7 +247,7 @@ test(
|
||||
await openEditDialog(page);
|
||||
|
||||
// Wait for stepper body to render
|
||||
await page.waitForSelector('h2:has-text("Deployment Type")');
|
||||
await expectDeploymentTypeStep(page);
|
||||
|
||||
let patchCalled = false;
|
||||
page.on("request", (req) => {
|
||||
@ -356,7 +378,9 @@ async function setupRoutesWithConnections(
|
||||
// ---------------------------------------------------------------------------
|
||||
test(
|
||||
"Edit mode includes new connections in PATCH request",
|
||||
{ tag: ["@release", "@workspace", "@api"] },
|
||||
{
|
||||
tag: ["@release", "@workspace", "@api"],
|
||||
},
|
||||
async ({ page }) => {
|
||||
test.skip(
|
||||
process.env.LANGFLOW_FEATURE_WXO_DEPLOYMENTS !== "true",
|
||||
@ -399,11 +423,11 @@ test(
|
||||
await page.waitForSelector('[data-testid="stepper-modal-title"]');
|
||||
|
||||
// Step 1 (Type) → Next
|
||||
await page.waitForSelector('h2:has-text("Deployment Type")');
|
||||
await expectDeploymentTypeStep(page);
|
||||
await page.getByTestId("deployment-stepper-next").click();
|
||||
|
||||
// Step 2 (Attach Flows) — flow "f1" should already be attached
|
||||
await page.waitForSelector("text=Attach Flows");
|
||||
// Step 2 (Flows) — flow "f1" should already be attached
|
||||
await expectFlowsStep(page);
|
||||
await page.waitForSelector('[data-testid="flow-item-f1"]');
|
||||
|
||||
// Click the pre-attached flow and its version to open the connection panel
|
||||
@ -455,7 +479,9 @@ test(
|
||||
// ---------------------------------------------------------------------------
|
||||
test(
|
||||
"Edit mode includes removed connections in PATCH request",
|
||||
{ tag: ["@release", "@workspace", "@api"] },
|
||||
{
|
||||
tag: ["@release", "@workspace", "@api"],
|
||||
},
|
||||
async ({ page }) => {
|
||||
test.skip(
|
||||
process.env.LANGFLOW_FEATURE_WXO_DEPLOYMENTS !== "true",
|
||||
@ -497,11 +523,11 @@ test(
|
||||
await page.waitForSelector('[data-testid="stepper-modal-title"]');
|
||||
|
||||
// Step 1 (Type) → Next
|
||||
await page.waitForSelector('h2:has-text("Deployment Type")');
|
||||
await expectDeploymentTypeStep(page);
|
||||
await page.getByTestId("deployment-stepper-next").click();
|
||||
|
||||
// Step 2 (Attach Flows)
|
||||
await page.waitForSelector("text=Attach Flows");
|
||||
// Step 2 (Flows)
|
||||
await expectFlowsStep(page);
|
||||
await page.waitForSelector('[data-testid="flow-item-f1"]');
|
||||
await page.getByTestId("flow-item-f1").click();
|
||||
await page.waitForSelector('[data-testid="version-item-fv1"]');
|
||||
|
||||
Reference in New Issue
Block a user