diff --git a/src/frontend/src/pages/MainPage/pages/deploymentsPage/__tests__/deployment-stepper-footer.test.tsx b/src/frontend/src/pages/MainPage/pages/deploymentsPage/__tests__/deployment-stepper-footer.test.tsx new file mode 100644 index 0000000000..7e721a9c74 --- /dev/null +++ b/src/frontend/src/pages/MainPage/pages/deploymentsPage/__tests__/deployment-stepper-footer.test.tsx @@ -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 ; + }, +); + +describe("DeploymentStepperFooter", () => { + it("shows done state controls when deployed", () => { + render( + , + ); + + 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( + , + ); + + await user.click(screen.getByRole("button", { name: "Done" })); + + expect(onClose).toHaveBeenCalled(); + }); +}); diff --git a/src/frontend/src/pages/MainPage/pages/deploymentsPage/__tests__/deployment-stepper-modal.test.tsx b/src/frontend/src/pages/MainPage/pages/deploymentsPage/__tests__/deployment-stepper-modal.test.tsx index 4640affa27..a719aa06d8 100644 --- a/src/frontend/src/pages/MainPage/pages/deploymentsPage/__tests__/deployment-stepper-modal.test.tsx +++ b/src/frontend/src/pages/MainPage/pages/deploymentsPage/__tests__/deployment-stepper-modal.test.tsx @@ -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(); }); }); diff --git a/src/frontend/src/pages/MainPage/pages/deploymentsPage/__tests__/deployment-success-content.test.tsx b/src/frontend/src/pages/MainPage/pages/deploymentsPage/__tests__/deployment-success-content.test.tsx new file mode 100644 index 0000000000..2fab3d6881 --- /dev/null +++ b/src/frontend/src/pages/MainPage/pages/deploymentsPage/__tests__/deployment-success-content.test.tsx @@ -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 ; + }, +); + +describe("DeploymentSuccessContent", () => { + it("renders success copy and provider link", () => { + render( + , + ); + + 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( + , + ); + + await user.click(screen.getByRole("button", { name: "Test Deployment" })); + + expect(onTest).toHaveBeenCalled(); + }); + + it("hides test button without deployment name", () => { + render( + , + ); + + expect( + screen.queryByRole("button", { name: "Test Deployment" }), + ).not.toBeInTheDocument(); + }); + + it("hides test button when showTestButton is false", () => { + render( + , + ); + + expect( + screen.queryByRole("button", { name: "Test Deployment" }), + ).not.toBeInTheDocument(); + }); +}); diff --git a/src/frontend/src/pages/MainPage/pages/deploymentsPage/__tests__/step-attach-flows.test.tsx b/src/frontend/src/pages/MainPage/pages/deploymentsPage/__tests__/step-attach-flows.test.tsx index 49284ece5f..aa0375dd3a 100644 --- a/src/frontend/src/pages/MainPage/pages/deploymentsPage/__tests__/step-attach-flows.test.tsx +++ b/src/frontend/src/pages/MainPage/pages/deploymentsPage/__tests__/step-attach-flows.test.tsx @@ -230,14 +230,14 @@ beforeEach(() => { // --------------------------------------------------------------------------- describe("Three-panel layout rendering", () => { - it("renders the Attach Flows heading", () => { + it("renders the Flows heading", () => { render(); - 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(); - expect(screen.getByText("Available Flows")).toBeInTheDocument(); + expect(screen.getByText("Available")).toBeInTheDocument(); }); it("renders flow items in the list", () => { diff --git a/src/frontend/src/pages/MainPage/pages/deploymentsPage/components/deployment-stepper-footer.tsx b/src/frontend/src/pages/MainPage/pages/deploymentsPage/components/deployment-stepper-footer.tsx new file mode 100644 index 0000000000..ef0a056d99 --- /dev/null +++ b/src/frontend/src/pages/MainPage/pages/deploymentsPage/components/deployment-stepper-footer.tsx @@ -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 ( +
+ {isDeployed ? ( +
+ ) : ( + + )} +
+ {!isDeployed && ( + + )} + {!isInDeployPhase && ( + + )} + {isDeploying && ( + + )} + {isDeployed && } +
+
+ ); +} diff --git a/src/frontend/src/pages/MainPage/pages/deploymentsPage/components/deployment-stepper-modal.tsx b/src/frontend/src/pages/MainPage/pages/deploymentsPage/components/deployment-stepper-modal.tsx index fc4d5c99ee..7bfabc79a3 100644 --- a/src/frontend/src/pages/MainPage/pages/deploymentsPage/components/deployment-stepper-modal.tsx +++ b/src/frontend/src/pages/MainPage/pages/deploymentsPage/components/deployment-stepper-modal.tsx @@ -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"} - +
{/* Content box: step content + footer */}
{isInDeployPhase ? ( - + isDeployed && !isEditMode ? ( + + ) : ( + + ) ) : ( <> {logicalStep === 1 && } @@ -339,61 +363,23 @@ function DeploymentStepperModalContent({ )}
- {/* Footer */} -
- -
- {!isDeployed && ( - - )} - {!isInDeployPhase && ( - - )} - {isDeploying && ( - - )} - {isDeployed && onTestDeployment && !isEditMode && ( - - )} -
-
+ setOpen(false)} + onClose={() => setOpen(false)} + onPrimaryAction={isFinalStep ? handleDeploy : handleStepNext} + />
); diff --git a/src/frontend/src/pages/MainPage/pages/deploymentsPage/components/deployment-stepper.tsx b/src/frontend/src/pages/MainPage/pages/deploymentsPage/components/deployment-stepper.tsx index 903abf6144..3abc44b849 100644 --- a/src/frontend/src/pages/MainPage/pages/deploymentsPage/components/deployment-stepper.tsx +++ b/src/frontend/src/pages/MainPage/pages/deploymentsPage/components/deployment-stepper.tsx @@ -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 (
@@ -35,7 +51,7 @@ export default function DeploymentStepper() {
= step.number + activeStep >= step.number ? "bg-foreground text-background" : "bg-muted text-muted-foreground", )} @@ -45,7 +61,7 @@ export default function DeploymentStepper() { = step.number && "font-medium", + activeStep >= step.number && "font-medium", )} > {step.label} diff --git a/src/frontend/src/pages/MainPage/pages/deploymentsPage/components/deployment-success-content.tsx b/src/frontend/src/pages/MainPage/pages/deploymentsPage/components/deployment-success-content.tsx new file mode 100644 index 0000000000..1cf6280749 --- /dev/null +++ b/src/frontend/src/pages/MainPage/pages/deploymentsPage/components/deployment-success-content.tsx @@ -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 ( +
+
+ +
+ +
+

+ Deployment successful +

+

+ Deployed to {providerName} as draft +

+
+ +
+ Publish from Draft to Live in + + {providerName} + + +
+ + {showTestButton && deploymentName && ( + + )} +
+ ); +} diff --git a/src/frontend/src/pages/MainPage/pages/deploymentsPage/components/step-attach-flows-flow-list-panel.tsx b/src/frontend/src/pages/MainPage/pages/deploymentsPage/components/step-attach-flows-flow-list-panel.tsx index 6303ed25d3..6a5ba993bd 100644 --- a/src/frontend/src/pages/MainPage/pages/deploymentsPage/components/step-attach-flows-flow-list-panel.tsx +++ b/src/frontend/src/pages/MainPage/pages/deploymentsPage/components/step-attach-flows-flow-list-panel.tsx @@ -29,7 +29,7 @@ export const FlowListPanel = memo(function FlowListPanel({ return (
- Available Flows + Available
{flows.map((flow) => { diff --git a/src/frontend/src/pages/MainPage/pages/deploymentsPage/components/step-attach-flows.tsx b/src/frontend/src/pages/MainPage/pages/deploymentsPage/components/step-attach-flows.tsx index 1beac14672..07141246e8 100644 --- a/src/frontend/src/pages/MainPage/pages/deploymentsPage/components/step-attach-flows.tsx +++ b/src/frontend/src/pages/MainPage/pages/deploymentsPage/components/step-attach-flows.tsx @@ -259,7 +259,7 @@ export default function StepAttachFlows() { return (
-

Attach Flows

+

Flows

{ 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", diff --git a/src/frontend/tests/core/features/deployment-edit.spec.ts b/src/frontend/tests/core/features/deployment-edit.spec.ts index fbec55b3f2..ba8062ccae 100644 --- a/src/frontend/tests/core/features/deployment-edit.spec.ts +++ b/src/frontend/tests/core/features/deployment-edit.spec.ts @@ -83,9 +83,23 @@ async function openEditDialog(page: Parameters[2]["page"]) { await page.waitForSelector('[data-testid="stepper-modal-title"]'); } +async function expectDeploymentTypeStep( + page: Parameters[2]["page"], +) { + await expect( + page.getByRole("heading", { name: /Deployment Type/i }), + ).toBeVisible(); +} + +async function expectFlowsStep(page: Parameters[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"]');