Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion QA-CHECKLIST.md
Original file line number Diff line number Diff line change
Expand Up @@ -586,7 +586,7 @@
- [-] Save flow components as template

#### 12.6 Flow Execution
- [-] Execute flow via Run button → `core/features/run-flow.spec.ts`
- [x] Run Flow component executes another flow → `flow-functionality/run-flow.spec.ts`
- [-] Stop building flow → `core/features/stop-building.spec.ts`
- [!] Playground button disabled with empty flow — needs review → `regression/flow-functionality/generalBugs-shard-3.spec.ts` (**test skipped: assertion was a no-op, current Langflow behavior to confirm**)

Expand Down
196 changes: 109 additions & 87 deletions tests/tests-automations/regression/flow-functionality/run-flow.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,132 +3,154 @@ import path from "path";
import { expect, test } from "../../../fixtures/fixtures";
import { adjustScreenView } from "../../../helpers/ui/adjust-screen-view";
import { awaitBootstrapTest } from "../../../helpers/other/await-bootstrap-test";
import { getAuthToken } from "../../../helpers/auth/get-auth-token";
import { zoomOut } from "../../../helpers/ui/zoom-out";

test(
"user should be able to use Run Flow without any issues",
{ tag: ["@release", "@workspace", "@api"] },
async ({ page }) => {
{ tag: ["@stable", "@release", "@workspace", "@api", "@regression"] },
async ({ page, request }) => {
if (!process.env.CI) {
dotenv.config({ path: path.resolve(__dirname, "../../../.env") });
}

await awaitBootstrapTest(page);

await page.waitForSelector('[data-testid="blank-flow"]', {
timeout: 30000,
});

await page.getByTestId("blank-flow").click();
try {
await page.waitForSelector('[data-testid="blank-flow"]', {
timeout: 30000,
});

await page.getByTestId("sidebar-search-input").click();
await page.getByTestId("sidebar-search-input").fill("chat output");
await page.waitForSelector('[data-testid="input_outputChat Output"]', {
timeout: 100000,
});
await page.getByTestId("blank-flow").click();

await page
.getByTestId("input_outputChat Output")
.hover()
.then(async () => {
await page.getByTestId("add-component-button-chat-output").click();
await page.getByTestId("sidebar-search-input").click();
await page.getByTestId("sidebar-search-input").fill("chat output");
await page.waitForSelector('[data-testid="input_outputChat Output"]', {
timeout: 30000,
});

await zoomOut(page, 2);
await page
.getByTestId("input_outputChat Output")
.hover()
.then(async () => {
await page.getByTestId("add-component-button-chat-output").click();
});

await page.getByTestId("sidebar-search-input").click();
await page.getByTestId("sidebar-search-input").fill("chat input");
await page.waitForSelector('[data-testid="input_outputChat Input"]', {
timeout: 100000,
});
await zoomOut(page, 2);

await page
.getByTestId("input_outputChat Input")
.dragTo(page.locator('//*[@id="react-flow-id"]'), {
targetPosition: { x: 100, y: 100 },
await page.getByTestId("sidebar-search-input").click();
await page.getByTestId("sidebar-search-input").fill("chat input");
await page.waitForSelector('[data-testid="input_outputChat Input"]', {
timeout: 30000,
});

await page.getByTestId("sidebar-search-input").click();
await page.getByTestId("sidebar-search-input").fill("text output");
await page.waitForSelector('[data-testid="input_outputText Output"]', {
timeout: 100000,
});
await page
.getByTestId("input_outputChat Input")
.dragTo(page.locator('//*[@id="react-flow-id"]'), {
targetPosition: { x: 100, y: 100 },
});

await page
.getByTestId("input_outputText Output")
.dragTo(page.locator('//*[@id="react-flow-id"]'), {
targetPosition: { x: 300, y: 300 },
await page.getByTestId("sidebar-search-input").click();
await page.getByTestId("sidebar-search-input").fill("text output");
await page.waitForSelector('[data-testid="input_outputText Output"]', {
timeout: 30000,
});

await adjustScreenView(page);
await page
.getByTestId("input_outputText Output")
.dragTo(page.locator('//*[@id="react-flow-id"]'), {
targetPosition: { x: 300, y: 300 },
});

await page
.getByTestId("handle-chatinput-noshownode-chat message-source")
.click();
await adjustScreenView(page);

await page.getByTestId("handle-textoutput-shownode-inputs-left").click();
await page
.getByTestId("handle-chatinput-noshownode-chat message-source")
.click();

await page
.getByTestId("handle-textoutput-shownode-output text-right")
.click();
await page
.getByTestId("handle-chatoutput-noshownode-inputs-target")
.click();
await page.getByTestId("handle-textoutput-shownode-inputs-left").click();

await page.getByTestId("icon-ChevronLeft").click();
await page
.getByTestId("handle-textoutput-shownode-output text-right")
.click();
await page
.getByTestId("handle-chatoutput-noshownode-inputs-target")
.click();

await page.getByText("New Flow").isVisible();
await page.getByTestId("new-project-btn").click();
await page.getByTestId("icon-ChevronLeft").click();

await page.getByTestId("blank-flow").click();
await expect(page.getByText("New Flow")).toBeVisible({ timeout: 10000 });
await page.getByTestId("new-project-btn").click();

await page.getByTestId("sidebar-search-input").click();
await page.getByTestId("sidebar-search-input").fill("run flow");
await page.waitForSelector('[data-testid="flow_controlsRun Flow"]', {
timeout: 100000,
});
await page.getByTestId("blank-flow").click();

await page
.getByTestId("flow_controlsRun Flow")
.hover()
.then(async () => {
await page.getByTestId("add-component-button-run-flow").click();
await page.getByTestId("sidebar-search-input").click();
await page.getByTestId("sidebar-search-input").fill("run flow");
await page.waitForSelector('[data-testid="flow_controlsRun Flow"]', {
timeout: 30000,
});

await page
.getByTestId("value-dropdown-dropdown_str_flow_name_selected")
.click();

await page.getByTestId("refresh-dropdown-list-flow_name_selected").click();
await page
.getByTestId("flow_controlsRun Flow")
.hover()
.then(async () => {
await page.getByTestId("add-component-button-run-flow").click();
});

await page.waitForSelector("text=Loading", { timeout: 30000 });
await page.waitForSelector("text=Select an option", { timeout: 30000 });
await page
.getByTestId("value-dropdown-dropdown_str_flow_name_selected")
.click();

await page
.getByTestId("value-dropdown-dropdown_str_flow_name_selected")
.click();
await page.getByTestId("refresh-dropdown-list-flow_name_selected").click();

await page.getByTestId("dropdown-option-0-container").click();
await page.waitForSelector("text=Loading", { timeout: 30000 });
await page.waitForSelector("text=Select an option", { timeout: 30000 });

await page.getByTestId(/^textarea_str_chatinput.*/).click();
await page
.getByTestId(/^textarea_str_chatinput.*/)
.fill("THIS IS A TEST FOR RUN FLOW COMPONENT");
await page
.getByTestId("value-dropdown-dropdown_str_flow_name_selected")
.click();

await page.getByTestId("button_run_run flow").click();
await page.waitForSelector("text=built successfully", {
timeout: 30000,
});
await page.getByTestId("dropdown-option-0-container").click();

// Wait for and click the output inspection button using partial match
await page.waitForSelector('[data-testid^="output-inspection-"]', {
timeout: 30000,
});
await page.getByTestId(/^textarea_str_chatinput.*/).click();
await page
.getByTestId(/^textarea_str_chatinput.*/)
.fill("THIS IS A TEST FOR RUN FLOW COMPONENT");

await page.locator('[data-testid^="output-inspection-"]').first().click();
await page.getByTestId("button_run_run flow").click();
await page.waitForSelector("text=built successfully", {
timeout: 30000,
});

const value = await page.getByPlaceholder("Empty").inputValue();
// Wait for and click the output inspection button using partial match
await page.waitForSelector('[data-testid^="output-inspection-"]', {
timeout: 30000,
});

expect(value).toBe("THIS IS A TEST FOR RUN FLOW COMPONENT");
await page.locator('[data-testid^="output-inspection-"]').first().click();

const value = page.getByPlaceholder("Empty");

await expect(value).toHaveValue("THIS IS A TEST FOR RUN FLOW COMPONENT");
} finally {
// Best-effort cleanup of the 2 flows created by the test.
// Uses the API for speed and to avoid cascading UI failures.
try {
const headers = { Authorization: await getAuthToken(request) };
const listRes = await request.get("/api/v1/flows/", { headers });
if (listRes.ok()) {
const body = await listRes.json();
const items = (Array.isArray(body) ? body : body?.items ?? []).slice(

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Valid — fixed in b89103e. You're right that the object-form fallback used the wrong key (body?.items vs the actual body.flows shape established by tests/helpers/flows/clean-all-flows.ts:33). The bug never bit because Langflow currently returns the array form on this endpoint, but a future change to the response shape would have silently skipped cleanup.

The whole listing call is gone now — replaced with the same ID-scoping pattern landed in PR #267 (bulk-actions): the test captures the flow ID from page.url() after each blank-flow.click() and the finally block iterates only those captured IDs. No more list-then-slice; no more object-form ambiguity.

0,
2,
);
for (const f of items) {
await request.delete(`/api/v1/flows/${f.id}`, { headers });
}

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Valid — fixed in b89103e. Same root issue as comment 1 (positional slice(0, 2) over the listing). Two concrete risks the previous code had:

  1. The list did not use remove_example_flows=true, so any example/starter flow that the API returns ahead of user-created flows would have been deleted.
  2. Under fullyParallel: true, a sibling worker could have created flows that ended up in the top-2 positions during the window between request.get(...) and request.delete(...), taking collateral damage.

Fix is the same pattern from PR #267:

  • Capture createdFlowIds: string[] populated by page.waitForURL(/\/flow\/[0-9a-f-]+/i) + regex extraction after each blank-flow.click().
  • finally block iterates only those IDs and calls DELETE /api/v1/flows/{id} for each. 404s for IDs the test already deleted on the happy path are silenced.

Validated against the live API: 182 flows before the run, 182 after — the 2 captured IDs were deleted, nothing else was touched.

}
} catch {
// Cleanup is best-effort — do not mask original test failure.
}
}
},
);
Loading