mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-03-17 03:00:27 -04:00
Compare commits
5 Commits
dev
...
abhi/add-b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
54bf45656a | ||
|
|
2f32217c7c | ||
|
|
7b64fbc931 | ||
|
|
1a0234c946 | ||
|
|
1e14634d3d |
@@ -18,11 +18,8 @@ test.beforeEach(async ({ page }) => {
|
||||
await page.goto("/build");
|
||||
await buildPage.closeTutorial();
|
||||
|
||||
const [dictionaryBlock] = await buildPage.getFilteredBlocksFromAPI(
|
||||
(block) => block.name === "AddToDictionaryBlock",
|
||||
);
|
||||
|
||||
await buildPage.addBlock(dictionaryBlock);
|
||||
await buildPage.addBlockByClick("Add to Dictionary");
|
||||
await buildPage.waitForNodeOnCanvas(1);
|
||||
|
||||
await buildPage.saveAgent("Test Agent", "Test Description");
|
||||
await test
|
||||
|
||||
@@ -1,363 +1,134 @@
|
||||
// TODO: These tests were written for the old (legacy) builder.
|
||||
// They need to be updated to work with the new flow editor.
|
||||
|
||||
// Note: all the comments with //(number)! are for the docs
|
||||
//ignore them when reading the code, but if you change something,
|
||||
//make sure to update the docs! Your autoformmater will break this page,
|
||||
// so don't run it on this file.
|
||||
// --8<-- [start:BuildPageExample]
|
||||
|
||||
import test from "@playwright/test";
|
||||
import test, { expect } from "@playwright/test";
|
||||
import { BuildPage } from "./pages/build.page";
|
||||
import { LoginPage } from "./pages/login.page";
|
||||
import { hasUrl } from "./utils/assertion";
|
||||
import { getTestUser } from "./utils/auth";
|
||||
|
||||
// Reason Ignore: admonishment is in the wrong place visually with correct prettier rules
|
||||
// prettier-ignore
|
||||
test.describe.skip("Build", () => { //(1)!
|
||||
let buildPage: BuildPage; //(2)!
|
||||
test.describe("Builder", () => {
|
||||
let buildPage: BuildPage;
|
||||
|
||||
// Reason Ignore: admonishment is in the wrong place visually with correct prettier rules
|
||||
// prettier-ignore
|
||||
test.beforeEach(async ({ page }) => { //(3)! ts-ignore
|
||||
test.setTimeout(25000);
|
||||
test.beforeEach(async ({ page }) => {
|
||||
test.setTimeout(60000);
|
||||
const loginPage = new LoginPage(page);
|
||||
const testUser = await getTestUser();
|
||||
|
||||
buildPage = new BuildPage(page);
|
||||
|
||||
// Start each test with login using worker auth
|
||||
await page.goto("/login"); //(4)!
|
||||
await page.goto("/login");
|
||||
await loginPage.login(testUser.email, testUser.password);
|
||||
await hasUrl(page, "/marketplace"); //(5)!
|
||||
await buildPage.navbar.clickBuildLink();
|
||||
await hasUrl(page, "/build");
|
||||
await hasUrl(page, "/marketplace");
|
||||
|
||||
await page.goto("/build");
|
||||
await page.waitForLoadState("domcontentloaded");
|
||||
await buildPage.closeTutorial();
|
||||
});
|
||||
|
||||
// Helper function to add blocks starting with a specific letter, split into parts for parallelization
|
||||
async function addBlocksStartingWithSplit(letter: string, part: number, totalParts: number): Promise<void> {
|
||||
const blockIdsToSkip = await buildPage.getBlocksToSkip();
|
||||
const blockTypesToSkip = ["Input", "Output", "Agent", "AI"];
|
||||
const targetLetter = letter.toLowerCase();
|
||||
|
||||
const allBlocks = await buildPage.getFilteredBlocksFromAPI(block =>
|
||||
block.name[0].toLowerCase() === targetLetter &&
|
||||
!blockIdsToSkip.includes(block.id) &&
|
||||
!blockTypesToSkip.includes(block.type)
|
||||
);
|
||||
// --- Core tests ---
|
||||
|
||||
const blocksToAdd = allBlocks.filter((_, index) =>
|
||||
index % totalParts === (part - 1)
|
||||
);
|
||||
|
||||
console.log(`Adding ${blocksToAdd.length} blocks starting with "${letter}" (part ${part}/${totalParts})`);
|
||||
|
||||
for (const block of blocksToAdd) {
|
||||
await buildPage.addBlock(block);
|
||||
}
|
||||
}
|
||||
|
||||
// Reason Ignore: admonishment is in the wrong place visually with correct prettier rules
|
||||
// prettier-ignore
|
||||
test("user can add a block", async ({ page: _page }) => { //(6)!
|
||||
await buildPage.openBlocksPanel(); //(10)!
|
||||
const blocks = await buildPage.getFilteredBlocksFromAPI(block => block.name[0].toLowerCase() === "a");
|
||||
const block = blocks.at(-1);
|
||||
if (!block) throw new Error("No block found");
|
||||
|
||||
await buildPage.addBlock(block); //(11)!
|
||||
await buildPage.closeBlocksPanel(); //(12)!
|
||||
await buildPage.hasBlock(block); //(13)!
|
||||
});
|
||||
// --8<-- [end:BuildPageExample]
|
||||
|
||||
test("user can add blocks starting with a (part 1)", async () => {
|
||||
await addBlocksStartingWithSplit("a", 1, 2);
|
||||
test("build page loads successfully", async () => {
|
||||
await expect(buildPage.isLoaded()).resolves.toBeTruthy();
|
||||
await expect(
|
||||
buildPage.getPlaywrightPage().getByTestId("blocks-control-blocks-button"),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
buildPage.getPlaywrightPage().getByTestId("save-control-save-button"),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test("user can add blocks starting with a (part 2)", async () => {
|
||||
await addBlocksStartingWithSplit("a", 2, 2);
|
||||
test("user can add a block via block menu", async () => {
|
||||
const initialCount = await buildPage.getNodeCount();
|
||||
await buildPage.addBlockByClick("Store Value");
|
||||
await buildPage.waitForNodeOnCanvas(initialCount + 1);
|
||||
expect(await buildPage.getNodeCount()).toBe(initialCount + 1);
|
||||
});
|
||||
|
||||
test("user can add blocks starting with b", async () => {
|
||||
await addBlocksStartingWithSplit("b", 1, 1);
|
||||
test("user can add multiple blocks", async () => {
|
||||
await buildPage.addBlockByClick("Store Value");
|
||||
await buildPage.waitForNodeOnCanvas(1);
|
||||
|
||||
await buildPage.addBlockByClick("Store Value");
|
||||
await buildPage.waitForNodeOnCanvas(2);
|
||||
|
||||
expect(await buildPage.getNodeCount()).toBe(2);
|
||||
});
|
||||
|
||||
test("user can add blocks starting with c", async () => {
|
||||
await addBlocksStartingWithSplit("c", 1, 1);
|
||||
test("user can remove a block", async () => {
|
||||
await buildPage.addBlockByClick("Store Value");
|
||||
await buildPage.waitForNodeOnCanvas(1);
|
||||
|
||||
// Deselect, then re-select the node and delete
|
||||
await buildPage.clickCanvas();
|
||||
await buildPage.selectNode(0);
|
||||
await buildPage.deleteSelectedNodes();
|
||||
|
||||
await expect(buildPage.getNodeLocator()).toHaveCount(0, { timeout: 5000 });
|
||||
});
|
||||
|
||||
test("user can add blocks starting with d", async () => {
|
||||
await addBlocksStartingWithSplit("d", 1, 1);
|
||||
test("user can save an agent", async ({ page }) => {
|
||||
await buildPage.addBlockByClick("Store Value");
|
||||
await buildPage.waitForNodeOnCanvas(1);
|
||||
|
||||
await buildPage.saveAgent("E2E Test Agent", "Created by e2e test");
|
||||
await buildPage.waitForSaveComplete();
|
||||
|
||||
expect(page.url()).toContain("flowID=");
|
||||
});
|
||||
|
||||
test("user can add blocks starting with e", async () => {
|
||||
test.setTimeout(60000); // Increase timeout for many Exa blocks
|
||||
await addBlocksStartingWithSplit("e", 1, 2);
|
||||
});
|
||||
test("user can save and run button becomes enabled", async () => {
|
||||
await buildPage.addBlockByClick("Store Value");
|
||||
await buildPage.waitForNodeOnCanvas(1);
|
||||
|
||||
test("user can add blocks starting with e pt 2", async () => {
|
||||
test.setTimeout(60000); // Increase timeout for many Exa blocks
|
||||
await addBlocksStartingWithSplit("e", 2, 2);
|
||||
});
|
||||
|
||||
test("user can add blocks starting with f", async () => {
|
||||
await addBlocksStartingWithSplit("f", 1, 1);
|
||||
});
|
||||
|
||||
test("user can add blocks starting with g (part 1)", async () => {
|
||||
await addBlocksStartingWithSplit("g", 1, 3);
|
||||
});
|
||||
|
||||
test("user can add blocks starting with g (part 2)", async () => {
|
||||
await addBlocksStartingWithSplit("g", 2, 3);
|
||||
});
|
||||
|
||||
test("user can add blocks starting with g (part 3)", async () => {
|
||||
await addBlocksStartingWithSplit("g", 3, 3);
|
||||
});
|
||||
|
||||
test("user can add blocks starting with h", async () => {
|
||||
await addBlocksStartingWithSplit("h", 1, 1);
|
||||
});
|
||||
|
||||
test("user can add blocks starting with i", async () => {
|
||||
await addBlocksStartingWithSplit("i", 1, 1);
|
||||
});
|
||||
|
||||
test("user can add blocks starting with j", async () => {
|
||||
await addBlocksStartingWithSplit("j", 1, 1);
|
||||
});
|
||||
|
||||
test("user can add blocks starting with k", async () => {
|
||||
await addBlocksStartingWithSplit("k", 1, 1);
|
||||
});
|
||||
|
||||
test("user can add blocks starting with l", async () => {
|
||||
await addBlocksStartingWithSplit("l", 1, 1);
|
||||
});
|
||||
|
||||
test("user can add blocks starting with m", async () => {
|
||||
await addBlocksStartingWithSplit("m", 1, 1);
|
||||
});
|
||||
|
||||
test("user can add blocks starting with n", async () => {
|
||||
await addBlocksStartingWithSplit("n", 1, 1);
|
||||
});
|
||||
|
||||
test("user can add blocks starting with o", async () => {
|
||||
await addBlocksStartingWithSplit("o", 1, 1);
|
||||
});
|
||||
|
||||
test("user can add blocks starting with p", async () => {
|
||||
await addBlocksStartingWithSplit("p", 1, 1);
|
||||
});
|
||||
|
||||
test("user can add blocks starting with q", async () => {
|
||||
await addBlocksStartingWithSplit("q", 1, 1);
|
||||
});
|
||||
|
||||
test("user can add blocks starting with r", async () => {
|
||||
await addBlocksStartingWithSplit("r", 1, 1);
|
||||
});
|
||||
|
||||
test("user can add blocks starting with s (part 1)", async () => {
|
||||
await addBlocksStartingWithSplit("s", 1, 3);
|
||||
});
|
||||
|
||||
test("user can add blocks starting with s (part 2)", async () => {
|
||||
await addBlocksStartingWithSplit("s", 2, 3);
|
||||
});
|
||||
|
||||
test("user can add blocks starting with s (part 3)", async () => {
|
||||
await addBlocksStartingWithSplit("s", 3, 3);
|
||||
});
|
||||
|
||||
test("user can add blocks starting with t", async () => {
|
||||
await addBlocksStartingWithSplit("t", 1, 1);
|
||||
});
|
||||
|
||||
test("user can add blocks starting with u", async () => {
|
||||
await addBlocksStartingWithSplit("u", 1, 1);
|
||||
});
|
||||
|
||||
test("user can add blocks starting with v", async () => {
|
||||
await addBlocksStartingWithSplit("v", 1, 1);
|
||||
});
|
||||
|
||||
test("user can add blocks starting with w", async () => {
|
||||
await addBlocksStartingWithSplit("w", 1, 1);
|
||||
});
|
||||
|
||||
test("user can add blocks starting with x", async () => {
|
||||
await addBlocksStartingWithSplit("x", 1, 1);
|
||||
});
|
||||
|
||||
test("user can add blocks starting with y", async () => {
|
||||
await addBlocksStartingWithSplit("y", 1, 1);
|
||||
});
|
||||
|
||||
test("user can add blocks starting with z", async () => {
|
||||
await addBlocksStartingWithSplit("z", 1, 1);
|
||||
});
|
||||
|
||||
test("build navigation is accessible from navbar", async ({ page }) => {
|
||||
// Navigate somewhere else first
|
||||
await page.goto("/marketplace"); //(4)!
|
||||
|
||||
// Check that navigation to the Builder is available on the page
|
||||
await buildPage.navbar.clickBuildLink();
|
||||
|
||||
await hasUrl(page, "/build");
|
||||
await test.expect(buildPage.isLoaded()).resolves.toBeTruthy();
|
||||
});
|
||||
|
||||
test("user can add two blocks and connect them", async ({ page }) => {
|
||||
await buildPage.openBlocksPanel();
|
||||
|
||||
// Define the blocks to add
|
||||
const block1 = {
|
||||
id: "1ff065e9-88e8-4358-9d82-8dc91f622ba9",
|
||||
name: "Store Value 1",
|
||||
description: "Store Value Block 1",
|
||||
type: "Standard",
|
||||
};
|
||||
const block2 = {
|
||||
id: "1ff065e9-88e8-4358-9d82-8dc91f622ba9",
|
||||
name: "Store Value 2",
|
||||
description: "Store Value Block 2",
|
||||
type: "Standard",
|
||||
};
|
||||
|
||||
// Add the blocks
|
||||
await buildPage.addBlock(block1);
|
||||
await buildPage.addBlock(block2);
|
||||
await buildPage.closeBlocksPanel();
|
||||
|
||||
// Connect the blocks
|
||||
await buildPage.connectBlockOutputToBlockInputViaDataId(
|
||||
"1-1-output-source",
|
||||
"1-2-input-target",
|
||||
);
|
||||
|
||||
// Fill in the input for the first block
|
||||
await buildPage.fillBlockInputByPlaceholder(
|
||||
block1.id,
|
||||
"Enter input",
|
||||
"Test Value",
|
||||
"1",
|
||||
);
|
||||
|
||||
// Save the agent and wait for the URL to update
|
||||
await buildPage.saveAgent(
|
||||
"Connected Blocks Test",
|
||||
"Testing block connections",
|
||||
);
|
||||
await test.expect(page).toHaveURL(({ searchParams }) => !!searchParams.get("flowID"));
|
||||
|
||||
// Wait for the save button to be enabled again
|
||||
await buildPage.saveAgent("Runnable Agent", "Test run button");
|
||||
await buildPage.waitForSaveComplete();
|
||||
await buildPage.waitForSaveButton();
|
||||
|
||||
// Ensure the run button is enabled
|
||||
await test.expect(buildPage.isRunButtonEnabled()).resolves.toBeTruthy();
|
||||
await expect(buildPage.isRunButtonEnabled()).resolves.toBeTruthy();
|
||||
});
|
||||
|
||||
test.skip("user can build an agent with inputs and output blocks", async ({ page }, testInfo) => {
|
||||
test.setTimeout(testInfo.timeout * 10);
|
||||
// --- Copy / Paste test ---
|
||||
|
||||
// prep
|
||||
await buildPage.openBlocksPanel();
|
||||
test("user can copy and paste a node", async ({ context }) => {
|
||||
await context.grantPermissions(["clipboard-read", "clipboard-write"]);
|
||||
|
||||
// Get input block from Input category
|
||||
const inputBlocks = await buildPage.getBlocksForCategory("Input");
|
||||
const inputBlock = inputBlocks.find((b) => b.name === "Agent Input");
|
||||
if (!inputBlock) throw new Error("Input block not found");
|
||||
await buildPage.addBlock(inputBlock);
|
||||
await buildPage.addBlockByClick("Store Value");
|
||||
await buildPage.waitForNodeOnCanvas(1);
|
||||
|
||||
// Get output block from Output category
|
||||
const outputBlocks = await buildPage.getBlocksForCategory("Output");
|
||||
const outputBlock = outputBlocks.find((b) => b.name === "Agent Output");
|
||||
if (!outputBlock) throw new Error("Output block not found");
|
||||
await buildPage.addBlock(outputBlock);
|
||||
await buildPage.selectNode(0);
|
||||
await buildPage.copyViaKeyboard();
|
||||
await buildPage.pasteViaKeyboard();
|
||||
|
||||
// Get calculator block from Logic category
|
||||
const logicBlocks = await buildPage.getBlocksForCategory("Logic");
|
||||
const calculatorBlock = logicBlocks.find((b) => b.name === "Calculator");
|
||||
if (!calculatorBlock) throw new Error("Calculator block not found");
|
||||
await buildPage.addBlock(calculatorBlock);
|
||||
await buildPage.waitForNodeOnCanvas(2);
|
||||
expect(await buildPage.getNodeCount()).toBe(2);
|
||||
});
|
||||
|
||||
await buildPage.closeBlocksPanel();
|
||||
// --- Run agent test ---
|
||||
|
||||
// Wait for blocks to be fully loaded
|
||||
await page.waitForTimeout(1000);
|
||||
test("user can run an agent from the builder", async () => {
|
||||
await buildPage.addBlockByClick("Store Value");
|
||||
await buildPage.waitForNodeOnCanvas(1);
|
||||
|
||||
// Wait for blocks to be ready for connections
|
||||
await page.waitForTimeout(1000);
|
||||
// Save the agent (required before running)
|
||||
await buildPage.saveAgent("Run Test Agent", "Testing run from builder");
|
||||
await buildPage.waitForSaveComplete();
|
||||
await buildPage.waitForSaveButton();
|
||||
|
||||
await buildPage.connectBlockOutputToBlockInputViaName(
|
||||
inputBlock.id,
|
||||
"Result",
|
||||
calculatorBlock.id,
|
||||
"A",
|
||||
);
|
||||
await buildPage.connectBlockOutputToBlockInputViaName(
|
||||
inputBlock.id,
|
||||
"Result",
|
||||
calculatorBlock.id,
|
||||
"B",
|
||||
);
|
||||
await buildPage.connectBlockOutputToBlockInputViaName(
|
||||
calculatorBlock.id,
|
||||
"Result",
|
||||
outputBlock.id,
|
||||
"Value",
|
||||
);
|
||||
// Click run button
|
||||
await buildPage.clickRunButton();
|
||||
|
||||
// Wait for connections to stabilize
|
||||
await page.waitForTimeout(1000);
|
||||
// Either the run dialog appears or the agent starts running directly
|
||||
const runDialogOrRunning = await Promise.race([
|
||||
buildPage
|
||||
.getPlaywrightPage()
|
||||
.locator('[data-id="run-input-dialog-content"]')
|
||||
.waitFor({ state: "visible", timeout: 10000 })
|
||||
.then(() => "dialog"),
|
||||
buildPage
|
||||
.getPlaywrightPage()
|
||||
.locator('[data-id="stop-graph-button"]')
|
||||
.waitFor({ state: "visible", timeout: 10000 })
|
||||
.then(() => "running"),
|
||||
]).catch(() => "timeout");
|
||||
|
||||
await buildPage.fillBlockInputByPlaceholder(
|
||||
inputBlock.id,
|
||||
"Enter Name",
|
||||
"Value",
|
||||
);
|
||||
await buildPage.fillBlockInputByPlaceholder(
|
||||
outputBlock.id,
|
||||
"Enter Name",
|
||||
"Doubled",
|
||||
);
|
||||
|
||||
// Wait before changing dropdown
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
await buildPage.selectBlockInputValue(
|
||||
calculatorBlock.id,
|
||||
"Operation",
|
||||
"Add",
|
||||
);
|
||||
|
||||
// Wait before saving
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
await buildPage.saveAgent(
|
||||
"Input and Output Blocks Test",
|
||||
"Testing input and output blocks",
|
||||
);
|
||||
await test.expect(page).toHaveURL(({ searchParams }) => !!searchParams.get("flowID"));
|
||||
|
||||
// Wait for save to complete
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// await buildPage.runAgent();
|
||||
// await buildPage.fillRunDialog({
|
||||
// Value: "10",
|
||||
// });
|
||||
// await buildPage.clickRunDialogRunButton();
|
||||
// await buildPage.waitForCompletionBadge();
|
||||
// await test
|
||||
// .expect(buildPage.isCompletionBadgeVisible())
|
||||
// .resolves.toBeTruthy();
|
||||
expect(["dialog", "running"]).toContain(runDialogOrRunning);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,44 +1,47 @@
|
||||
import { Locator, Page } from "@playwright/test";
|
||||
import { Block as APIBlock } from "../../lib/autogpt-server-api/types";
|
||||
import { beautifyString } from "../../lib/utils";
|
||||
import { expect, Locator, Page } from "@playwright/test";
|
||||
import { BasePage } from "./base.page";
|
||||
|
||||
export interface Block {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
export class BuildPage extends BasePage {
|
||||
private cachedBlocks: Record<string, Block> = {};
|
||||
|
||||
constructor(page: Page) {
|
||||
super(page);
|
||||
}
|
||||
|
||||
private getDisplayName(blockName: string): string {
|
||||
return beautifyString(blockName).replace(/ Block$/, "");
|
||||
// --- Navigation ---
|
||||
|
||||
async goto(): Promise<void> {
|
||||
await this.page.goto("/build");
|
||||
await this.page.waitForLoadState("domcontentloaded");
|
||||
}
|
||||
|
||||
async isLoaded(): Promise<boolean> {
|
||||
try {
|
||||
await this.page.waitForLoadState("domcontentloaded", { timeout: 10_000 });
|
||||
await this.page
|
||||
.locator(".react-flow")
|
||||
.waitFor({ state: "visible", timeout: 10_000 });
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async closeTutorial(): Promise<void> {
|
||||
console.log(`closing tutorial`);
|
||||
try {
|
||||
await this.page
|
||||
.getByRole("button", { name: "Skip Tutorial", exact: true })
|
||||
.click({ timeout: 3000 });
|
||||
} catch (_error) {
|
||||
console.info("Tutorial not shown or already dismissed");
|
||||
} catch {
|
||||
// Tutorial not shown or already dismissed
|
||||
}
|
||||
}
|
||||
|
||||
// --- Block Menu ---
|
||||
|
||||
async openBlocksPanel(): Promise<void> {
|
||||
const popoverContent = this.page.locator(
|
||||
'[data-id="blocks-control-popover-content"]',
|
||||
);
|
||||
const isPanelOpen = await popoverContent.isVisible();
|
||||
|
||||
if (!isPanelOpen) {
|
||||
if (!(await popoverContent.isVisible())) {
|
||||
await this.page.getByTestId("blocks-control-blocks-button").click();
|
||||
await popoverContent.waitFor({ state: "visible", timeout: 5000 });
|
||||
}
|
||||
@@ -50,501 +53,258 @@ export class BuildPage extends BasePage {
|
||||
);
|
||||
if (await popoverContent.isVisible()) {
|
||||
await this.page.getByTestId("blocks-control-blocks-button").click();
|
||||
await popoverContent.waitFor({ state: "hidden", timeout: 5000 });
|
||||
}
|
||||
}
|
||||
|
||||
async searchBlock(searchTerm: string): Promise<void> {
|
||||
const searchInput = this.page.locator(
|
||||
'[data-id="blocks-control-search-bar"] input[type="text"]',
|
||||
);
|
||||
await searchInput.clear();
|
||||
await searchInput.fill(searchTerm);
|
||||
await this.page.waitForTimeout(300);
|
||||
}
|
||||
|
||||
private getBlockCardByName(name: string): Locator {
|
||||
const escapedName = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
const exactName = new RegExp(`^\\s*${escapedName}\\s*$`, "i");
|
||||
return this.page
|
||||
.locator('[data-id^="block-card-"]')
|
||||
.filter({ has: this.page.locator("span", { hasText: exactName }) })
|
||||
.first();
|
||||
}
|
||||
|
||||
async addBlockByClick(searchTerm: string): Promise<void> {
|
||||
await this.openBlocksPanel();
|
||||
await this.searchBlock(searchTerm);
|
||||
|
||||
// Wait for any search results to appear
|
||||
const anyCard = this.page.locator('[data-id^="block-card-"]').first();
|
||||
await anyCard.waitFor({ state: "visible", timeout: 10000 });
|
||||
|
||||
// Click the card matching the search term name
|
||||
const blockCard = this.getBlockCardByName(searchTerm);
|
||||
await blockCard.waitFor({ state: "visible", timeout: 5000 });
|
||||
await blockCard.click();
|
||||
|
||||
// Close the panel so it doesn't overlay the canvas
|
||||
await this.closeBlocksPanel();
|
||||
}
|
||||
|
||||
async dragBlockToCanvas(searchTerm: string): Promise<void> {
|
||||
await this.openBlocksPanel();
|
||||
await this.searchBlock(searchTerm);
|
||||
|
||||
const anyCard = this.page.locator('[data-id^="block-card-"]').first();
|
||||
await anyCard.waitFor({ state: "visible", timeout: 10000 });
|
||||
|
||||
const blockCard = this.getBlockCardByName(searchTerm);
|
||||
await blockCard.waitFor({ state: "visible", timeout: 5000 });
|
||||
|
||||
const canvas = this.page.locator(".react-flow__pane").first();
|
||||
await blockCard.dragTo(canvas);
|
||||
}
|
||||
|
||||
// --- Nodes on Canvas ---
|
||||
|
||||
getNodeLocator(index?: number): Locator {
|
||||
const locator = this.page.locator('[data-id^="custom-node-"]');
|
||||
return index !== undefined ? locator.nth(index) : locator;
|
||||
}
|
||||
|
||||
async getNodeCount(): Promise<number> {
|
||||
return await this.getNodeLocator().count();
|
||||
}
|
||||
|
||||
async waitForNodeOnCanvas(expectedCount?: number): Promise<void> {
|
||||
if (expectedCount !== undefined) {
|
||||
await expect(this.getNodeLocator()).toHaveCount(expectedCount, {
|
||||
timeout: 10000,
|
||||
});
|
||||
} else {
|
||||
await this.getNodeLocator()
|
||||
.first()
|
||||
.waitFor({ state: "visible", timeout: 10000 });
|
||||
}
|
||||
}
|
||||
|
||||
async selectNode(index: number = 0): Promise<void> {
|
||||
const node = this.getNodeLocator(index);
|
||||
await node.click();
|
||||
}
|
||||
|
||||
async selectAllNodes(): Promise<void> {
|
||||
await this.page.locator(".react-flow__pane").first().click();
|
||||
const isMac = process.platform === "darwin";
|
||||
await this.page.keyboard.press(isMac ? "Meta+a" : "Control+a");
|
||||
}
|
||||
|
||||
async deleteSelectedNodes(): Promise<void> {
|
||||
await this.page.keyboard.press("Backspace");
|
||||
}
|
||||
|
||||
// --- Connections (Edges) ---
|
||||
|
||||
async connectNodes(
|
||||
sourceNodeIndex: number,
|
||||
targetNodeIndex: number,
|
||||
): Promise<void> {
|
||||
// Get the node wrapper elements to scope handle search
|
||||
const sourceNode = this.getNodeLocator(sourceNodeIndex);
|
||||
const targetNode = this.getNodeLocator(targetNodeIndex);
|
||||
|
||||
// ReactFlow renders Handle components as .react-flow__handle elements
|
||||
// Output handles have class .react-flow__handle-right (Position.Right)
|
||||
// Input handles have class .react-flow__handle-left (Position.Left)
|
||||
const sourceHandle = sourceNode
|
||||
.locator(".react-flow__handle-right")
|
||||
.first();
|
||||
const targetHandle = targetNode.locator(".react-flow__handle-left").first();
|
||||
|
||||
// Get precise center coordinates using evaluate to avoid CSS transform issues
|
||||
const getHandleCenter = async (locator: Locator) => {
|
||||
const el = await locator.elementHandle();
|
||||
if (!el) throw new Error("Handle element not found");
|
||||
const rect = await el.evaluate((node) => {
|
||||
const r = node.getBoundingClientRect();
|
||||
return { x: r.x + r.width / 2, y: r.y + r.height / 2 };
|
||||
});
|
||||
return rect;
|
||||
};
|
||||
|
||||
const source = await getHandleCenter(sourceHandle);
|
||||
const target = await getHandleCenter(targetHandle);
|
||||
|
||||
// ReactFlow requires a proper drag sequence with intermediate moves
|
||||
await this.page.mouse.move(source.x, source.y);
|
||||
await this.page.mouse.down();
|
||||
// Move in steps to trigger ReactFlow's connection detection
|
||||
const steps = 20;
|
||||
for (let i = 1; i <= steps; i++) {
|
||||
const ratio = i / steps;
|
||||
await this.page.mouse.move(
|
||||
source.x + (target.x - source.x) * ratio,
|
||||
source.y + (target.y - source.y) * ratio,
|
||||
);
|
||||
}
|
||||
await this.page.mouse.up();
|
||||
}
|
||||
|
||||
async getEdgeCount(): Promise<number> {
|
||||
return await this.page.locator(".react-flow__edge").count();
|
||||
}
|
||||
|
||||
// --- Save ---
|
||||
|
||||
async saveAgent(
|
||||
name: string = "Test Agent",
|
||||
description: string = "",
|
||||
): Promise<void> {
|
||||
console.log(`Saving agent '${name}' with description '${description}'`);
|
||||
await this.page.getByTestId("save-control-save-button").click();
|
||||
await this.page.getByTestId("save-control-name-input").fill(name);
|
||||
await this.page
|
||||
.getByTestId("save-control-description-input")
|
||||
.fill(description);
|
||||
|
||||
const nameInput = this.page.getByTestId("save-control-name-input");
|
||||
await nameInput.waitFor({ state: "visible", timeout: 5000 });
|
||||
await nameInput.fill(name);
|
||||
|
||||
if (description) {
|
||||
await this.page
|
||||
.getByTestId("save-control-description-input")
|
||||
.fill(description);
|
||||
}
|
||||
|
||||
await this.page.getByTestId("save-control-save-agent-button").click();
|
||||
}
|
||||
|
||||
async getBlocksFromAPI(): Promise<Block[]> {
|
||||
if (Object.keys(this.cachedBlocks).length > 0) {
|
||||
return Object.values(this.cachedBlocks);
|
||||
}
|
||||
async waitForSaveComplete(): Promise<void> {
|
||||
await expect(this.page).toHaveURL(/flowID=/, { timeout: 15000 });
|
||||
}
|
||||
|
||||
console.log(`Getting blocks from API request`);
|
||||
|
||||
// Make direct API request using the page's request context
|
||||
const response = await this.page.request.get(
|
||||
"http://localhost:3000/api/proxy/api/blocks",
|
||||
async waitForSaveButton(): Promise<void> {
|
||||
await this.page.waitForSelector(
|
||||
'[data-testid="save-control-save-button"]:not([disabled])',
|
||||
{ timeout: 10000 },
|
||||
);
|
||||
const apiBlocks: APIBlock[] = await response.json();
|
||||
|
||||
console.log(`Found ${apiBlocks.length} blocks from API`);
|
||||
|
||||
// Convert API blocks to test Block format
|
||||
const blocks = apiBlocks.map((block) => ({
|
||||
id: block.id,
|
||||
name: block.name,
|
||||
description: block.description,
|
||||
type: block.uiType,
|
||||
}));
|
||||
|
||||
this.cachedBlocks = blocks.reduce(
|
||||
(acc, block) => {
|
||||
acc[block.id] = block;
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, Block>,
|
||||
);
|
||||
return blocks;
|
||||
}
|
||||
|
||||
async getFilteredBlocksFromAPI(
|
||||
filterFn: (block: Block) => boolean,
|
||||
): Promise<Block[]> {
|
||||
console.log(`Getting filtered blocks from API`);
|
||||
const blocks = await this.getBlocksFromAPI();
|
||||
return blocks.filter(filterFn);
|
||||
}
|
||||
|
||||
async addBlock(block: Block): Promise<void> {
|
||||
console.log(`Adding block ${block.name} (${block.id}) to agent`);
|
||||
|
||||
await this.openBlocksPanel();
|
||||
|
||||
const searchInput = this.page.locator(
|
||||
'[data-id="blocks-control-search-bar"] input[type="text"]',
|
||||
);
|
||||
|
||||
const displayName = this.getDisplayName(block.name);
|
||||
await searchInput.clear();
|
||||
await searchInput.fill(displayName);
|
||||
|
||||
const blockCardId = block.id.replace(/[^a-zA-Z0-9]/g, "");
|
||||
const blockCard = this.page.locator(
|
||||
`[data-id="block-card-${blockCardId}"]`,
|
||||
);
|
||||
|
||||
await blockCard.waitFor({ state: "visible", timeout: 10000 });
|
||||
await blockCard.click();
|
||||
}
|
||||
|
||||
async hasBlock(_block: Block) {
|
||||
// In the new flow editor, verify a node exists on the canvas
|
||||
const node = this.page.locator('[data-id^="custom-node-"]').first();
|
||||
await node.isVisible();
|
||||
}
|
||||
|
||||
async getBlockInputs(blockId: string): Promise<string[]> {
|
||||
console.log(`Getting block ${blockId} inputs`);
|
||||
try {
|
||||
const node = this.page.locator(`[data-blockid="${blockId}"]`).first();
|
||||
const inputsData = await node.getAttribute("data-inputs");
|
||||
return inputsData ? JSON.parse(inputsData) : [];
|
||||
} catch (error) {
|
||||
console.error("Error getting block inputs:", error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async selectBlockCategory(category: string): Promise<void> {
|
||||
console.log(`Selecting block category: ${category}`);
|
||||
await this.page.getByText(category, { exact: true }).click();
|
||||
// Wait for the blocks to load after category selection
|
||||
await this.page.waitForTimeout(3000);
|
||||
}
|
||||
|
||||
async getBlocksForCategory(category: string): Promise<Block[]> {
|
||||
console.log(`Getting blocks for category: ${category}`);
|
||||
|
||||
// Clear any existing search to ensure we see all blocks in the category
|
||||
const searchInput = this.page.locator(
|
||||
'[data-id="blocks-control-search-bar"] input[type="text"]',
|
||||
);
|
||||
await searchInput.clear();
|
||||
|
||||
// Wait for search to clear
|
||||
await this.page.waitForTimeout(300);
|
||||
|
||||
// Select the category first
|
||||
await this.selectBlockCategory(category);
|
||||
|
||||
try {
|
||||
const blockFinder = this.page.locator('[data-id^="block-card-"]');
|
||||
await blockFinder.first().waitFor();
|
||||
const blocks = await blockFinder.all();
|
||||
|
||||
console.log(`found ${blocks.length} blocks in category ${category}`);
|
||||
|
||||
const results = await Promise.all(
|
||||
blocks.map(async (block) => {
|
||||
try {
|
||||
const fullId = (await block.getAttribute("data-id")) || "";
|
||||
const id = fullId.replace("block-card-", "");
|
||||
const nameElement = block.locator('[data-testid^="block-name-"]');
|
||||
const descriptionElement = block.locator(
|
||||
'[data-testid^="block-description-"]',
|
||||
);
|
||||
|
||||
const name = (await nameElement.textContent()) || "";
|
||||
const description = (await descriptionElement.textContent()) || "";
|
||||
const type = (await nameElement.getAttribute("data-type")) || "";
|
||||
|
||||
return {
|
||||
id,
|
||||
name: name.trim(),
|
||||
type: type.trim(),
|
||||
description: description.trim(),
|
||||
};
|
||||
} catch (elementError) {
|
||||
console.error("Error processing block:", elementError);
|
||||
return null;
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
// Filter out any null results from errors
|
||||
return results.filter((block): block is Block => block !== null);
|
||||
} catch (error) {
|
||||
console.error(`Error getting blocks for category ${category}:`, error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async _buildBlockSelector(blockId: string, dataId?: string): Promise<string> {
|
||||
const selector = dataId
|
||||
? `[data-id="${dataId}"] [data-blockid="${blockId}"]`
|
||||
: `[data-blockid="${blockId}"]`;
|
||||
return selector;
|
||||
}
|
||||
|
||||
private async moveBlockToViewportPosition(
|
||||
blockSelector: string,
|
||||
options: { xRatio?: number; yRatio?: number } = {},
|
||||
): Promise<void> {
|
||||
const { xRatio = 0.5, yRatio = 0.5 } = options;
|
||||
const blockLocator = this.page.locator(blockSelector).first();
|
||||
|
||||
await blockLocator.waitFor({ state: "visible" });
|
||||
|
||||
const boundingBox = await blockLocator.boundingBox();
|
||||
const viewport = this.page.viewportSize();
|
||||
|
||||
if (!boundingBox || !viewport) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentX = boundingBox.x + boundingBox.width / 2;
|
||||
const currentY = boundingBox.y + boundingBox.height / 2;
|
||||
|
||||
const targetX = viewport.width * xRatio;
|
||||
const targetY = viewport.height * yRatio;
|
||||
|
||||
const distance = Math.hypot(targetX - currentX, targetY - currentY);
|
||||
|
||||
if (distance < 5) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.page.mouse.move(currentX, currentY);
|
||||
await this.page.mouse.down();
|
||||
await this.page.mouse.move(targetX, targetY, { steps: 15 });
|
||||
await this.page.mouse.up();
|
||||
await this.page.waitForTimeout(200);
|
||||
}
|
||||
|
||||
async getBlockById(blockId: string, dataId?: string): Promise<Locator> {
|
||||
console.log(`getting block ${blockId} with dataId ${dataId}`);
|
||||
return this.page.locator(await this._buildBlockSelector(blockId, dataId));
|
||||
}
|
||||
|
||||
// dataId is optional, if provided, it will start the search with that container, otherwise it will start with the blockId
|
||||
// this is useful if you have multiple blocks with the same id, but different dataIds which you should have when adding a block to the graph.
|
||||
// Do note that once you run an agent, the dataId will change, so you will need to update the tests to use the new dataId or not use the same block in tests that run an agent
|
||||
async fillBlockInputByPlaceholder(
|
||||
blockId: string,
|
||||
placeholder: string,
|
||||
value: string,
|
||||
dataId?: string,
|
||||
): Promise<void> {
|
||||
console.log(
|
||||
`filling block input ${placeholder} with value ${value} of block ${blockId}`,
|
||||
);
|
||||
const block = await this.getBlockById(blockId, dataId);
|
||||
const input = block.getByPlaceholder(placeholder);
|
||||
await input.fill(value);
|
||||
}
|
||||
|
||||
async selectBlockInputValue(
|
||||
blockId: string,
|
||||
inputName: string,
|
||||
value: string,
|
||||
dataId?: string,
|
||||
): Promise<void> {
|
||||
console.log(
|
||||
`selecting value ${value} for input ${inputName} of block ${blockId}`,
|
||||
);
|
||||
// First get the button that opens the dropdown
|
||||
const baseSelector = await this._buildBlockSelector(blockId, dataId);
|
||||
|
||||
// Find the combobox button within the input handle container
|
||||
const comboboxSelector = `${baseSelector} [data-id="input-handle-${inputName.toLowerCase()}"] button[role="combobox"]`;
|
||||
|
||||
try {
|
||||
// Click the combobox to open it
|
||||
await this.page.click(comboboxSelector);
|
||||
|
||||
// Wait a moment for the dropdown to open
|
||||
await this.page.waitForTimeout(100);
|
||||
|
||||
// Select the option from the dropdown
|
||||
// The actual selector for the option might need adjustment based on the dropdown structure
|
||||
await this.page.getByRole("option", { name: value }).click();
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`Error selecting value "${value}" for input "${inputName}":`,
|
||||
error,
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async fillBlockInputByLabel(
|
||||
blockId: string,
|
||||
label: string,
|
||||
value: string,
|
||||
): Promise<void> {
|
||||
console.log(`filling block input ${label} with value ${value}`);
|
||||
const block = await this.getBlockById(blockId);
|
||||
const input = block.getByLabel(label);
|
||||
await input.fill(value);
|
||||
}
|
||||
|
||||
async connectBlockOutputToBlockInputViaDataId(
|
||||
blockOutputId: string,
|
||||
blockInputId: string,
|
||||
): Promise<void> {
|
||||
console.log(
|
||||
`connecting block output ${blockOutputId} to block input ${blockInputId}`,
|
||||
);
|
||||
try {
|
||||
// Locate the output element
|
||||
const outputElement = this.page.locator(`[data-id="${blockOutputId}"]`);
|
||||
// Locate the input element
|
||||
const inputElement = this.page.locator(`[data-id="${blockInputId}"]`);
|
||||
|
||||
await outputElement.dragTo(inputElement);
|
||||
} catch (error) {
|
||||
console.error("Error connecting block output to input:", error);
|
||||
}
|
||||
}
|
||||
|
||||
async connectBlockOutputToBlockInputViaName(
|
||||
startBlockId: string,
|
||||
startBlockOutputName: string,
|
||||
endBlockId: string,
|
||||
endBlockInputName: string,
|
||||
startDataId?: string,
|
||||
endDataId?: string,
|
||||
): Promise<void> {
|
||||
console.log(
|
||||
`connecting block output ${startBlockOutputName} of block ${startBlockId} to block input ${endBlockInputName} of block ${endBlockId}`,
|
||||
);
|
||||
|
||||
const startBlockBase = await this._buildBlockSelector(
|
||||
startBlockId,
|
||||
startDataId,
|
||||
);
|
||||
|
||||
const endBlockBase = await this._buildBlockSelector(endBlockId, endDataId);
|
||||
|
||||
await this.moveBlockToViewportPosition(startBlockBase, { xRatio: 0.35 });
|
||||
await this.moveBlockToViewportPosition(endBlockBase, { xRatio: 0.65 });
|
||||
|
||||
const startBlockOutputSelector = `${startBlockBase} [data-testid="output-handle-${startBlockOutputName.toLowerCase()}"]`;
|
||||
const endBlockInputSelector = `${endBlockBase} [data-testid="input-handle-${endBlockInputName.toLowerCase()}"]`;
|
||||
|
||||
console.log("Start block selector:", startBlockOutputSelector);
|
||||
console.log("End block selector:", endBlockInputSelector);
|
||||
|
||||
const startElement = this.page.locator(startBlockOutputSelector);
|
||||
const endElement = this.page.locator(endBlockInputSelector);
|
||||
|
||||
await startElement.scrollIntoViewIfNeeded();
|
||||
await this.page.waitForTimeout(200);
|
||||
|
||||
await endElement.scrollIntoViewIfNeeded();
|
||||
await this.page.waitForTimeout(200);
|
||||
|
||||
await startElement.dragTo(endElement);
|
||||
}
|
||||
|
||||
async isLoaded(): Promise<boolean> {
|
||||
console.log(`checking if build page is loaded`);
|
||||
try {
|
||||
await this.page.waitForLoadState("domcontentloaded", { timeout: 10_000 });
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// --- Run ---
|
||||
|
||||
async isRunButtonEnabled(): Promise<boolean> {
|
||||
console.log(`checking if run button is enabled`);
|
||||
const runButton = this.page.locator('[data-id="run-graph-button"]');
|
||||
return await runButton.isEnabled();
|
||||
}
|
||||
|
||||
async runAgent(): Promise<void> {
|
||||
console.log(`clicking run button`);
|
||||
async clickRunButton(): Promise<void> {
|
||||
const runButton = this.page.locator('[data-id="run-graph-button"]');
|
||||
await runButton.click();
|
||||
await this.page.waitForTimeout(1000);
|
||||
await runButton.click();
|
||||
}
|
||||
|
||||
async fillRunDialog(inputs: Record<string, string>): Promise<void> {
|
||||
console.log(`filling run dialog`);
|
||||
for (const [key, value] of Object.entries(inputs)) {
|
||||
await this.page.getByTestId(`agent-input-${key}`).fill(value);
|
||||
// --- Undo / Redo ---
|
||||
|
||||
async isUndoEnabled(): Promise<boolean> {
|
||||
const btn = this.page.locator('[data-id="undo-button"]');
|
||||
return !(await btn.isDisabled());
|
||||
}
|
||||
|
||||
async isRedoEnabled(): Promise<boolean> {
|
||||
const btn = this.page.locator('[data-id="redo-button"]');
|
||||
return !(await btn.isDisabled());
|
||||
}
|
||||
|
||||
async clickUndo(): Promise<void> {
|
||||
await this.page.locator('[data-id="undo-button"]').click();
|
||||
}
|
||||
|
||||
async clickRedo(): Promise<void> {
|
||||
await this.page.locator('[data-id="redo-button"]').click();
|
||||
}
|
||||
|
||||
// --- Copy / Paste ---
|
||||
|
||||
async copyViaKeyboard(): Promise<void> {
|
||||
const isMac = process.platform === "darwin";
|
||||
await this.page.keyboard.press(isMac ? "Meta+c" : "Control+c");
|
||||
}
|
||||
|
||||
async pasteViaKeyboard(): Promise<void> {
|
||||
const isMac = process.platform === "darwin";
|
||||
await this.page.keyboard.press(isMac ? "Meta+v" : "Control+v");
|
||||
}
|
||||
|
||||
// --- Helpers ---
|
||||
|
||||
async fillBlockInputByPlaceholder(
|
||||
placeholder: string,
|
||||
value: string,
|
||||
nodeIndex: number = 0,
|
||||
): Promise<void> {
|
||||
const node = this.getNodeLocator(nodeIndex);
|
||||
const input = node.getByPlaceholder(placeholder);
|
||||
await input.fill(value);
|
||||
}
|
||||
|
||||
async clickCanvas(): Promise<void> {
|
||||
const pane = this.page.locator(".react-flow__pane").first();
|
||||
const box = await pane.boundingBox();
|
||||
if (box) {
|
||||
// Click in the center of the canvas to avoid sidebar/toolbar overlaps
|
||||
await pane.click({
|
||||
position: { x: box.width / 2, y: box.height / 2 },
|
||||
});
|
||||
} else {
|
||||
await pane.click();
|
||||
}
|
||||
}
|
||||
async clickRunDialogRunButton(): Promise<void> {
|
||||
console.log(`clicking run button`);
|
||||
await this.page.getByTestId("agent-run-button").click();
|
||||
|
||||
getPlaywrightPage(): Page {
|
||||
return this.page;
|
||||
}
|
||||
|
||||
async waitForCompletionBadge(): Promise<void> {
|
||||
console.log(`waiting for completion badge`);
|
||||
await this.page.waitForSelector(
|
||||
'[data-id^="badge-"][data-id$="-COMPLETED"]',
|
||||
);
|
||||
}
|
||||
|
||||
async waitForSaveButton(): Promise<void> {
|
||||
console.log(`waiting for save button`);
|
||||
await this.page.waitForSelector(
|
||||
'[data-testid="save-control-save-button"]:not([disabled])',
|
||||
);
|
||||
}
|
||||
|
||||
async isCompletionBadgeVisible(): Promise<boolean> {
|
||||
console.log(`checking for completion badge`);
|
||||
const completionBadge = this.page
|
||||
.locator('[data-id^="badge-"][data-id$="-COMPLETED"]')
|
||||
.first();
|
||||
return await completionBadge.isVisible();
|
||||
}
|
||||
|
||||
async waitForVersionField(): Promise<void> {
|
||||
console.log(`waiting for version field`);
|
||||
|
||||
// wait for the url to have the flowID
|
||||
await this.page.waitForSelector(
|
||||
'[data-testid="save-control-version-output"]',
|
||||
);
|
||||
}
|
||||
|
||||
async getDictionaryBlockDetails(): Promise<Block> {
|
||||
return {
|
||||
id: "dummy-id-1",
|
||||
name: "Add to Dictionary",
|
||||
description: "Add to Dictionary",
|
||||
type: "Standard",
|
||||
};
|
||||
}
|
||||
|
||||
async getCalculatorBlockDetails(): Promise<Block> {
|
||||
return {
|
||||
id: "dummy-id-2",
|
||||
name: "Calculator",
|
||||
description: "Calculator",
|
||||
type: "Standard",
|
||||
};
|
||||
}
|
||||
|
||||
async waitForSaveDialogClose(): Promise<void> {
|
||||
console.log(`waiting for save dialog to close`);
|
||||
|
||||
await this.page.waitForSelector(
|
||||
'[data-id="save-control-popover-content"]',
|
||||
{ state: "hidden" },
|
||||
);
|
||||
}
|
||||
|
||||
async getGithubTriggerBlockDetails(): Promise<Block[]> {
|
||||
return [
|
||||
{
|
||||
id: "6c60ec01-8128-419e-988f-96a063ee2fea",
|
||||
name: "Github Trigger",
|
||||
description:
|
||||
"This block triggers on pull request events and outputs the event type and payload.",
|
||||
type: "Standard",
|
||||
},
|
||||
{
|
||||
id: "551e0a35-100b-49b7-89b8-3031322239b6",
|
||||
name: "Github Star Trigger",
|
||||
description:
|
||||
"This block triggers on star events and outputs the event type and payload.",
|
||||
type: "Standard",
|
||||
},
|
||||
{
|
||||
id: "2052dd1b-74e1-46ac-9c87-c7a0e057b60b",
|
||||
name: "Github Release Trigger",
|
||||
description:
|
||||
"This block triggers on release events and outputs the event type and payload.",
|
||||
type: "Standard",
|
||||
},
|
||||
{
|
||||
id: "b2605464-e486-4bf4-aad3-d8a213c8a48a",
|
||||
name: "Github Issue Trigger",
|
||||
description:
|
||||
"This block triggers on issue events and outputs the event type and payload.",
|
||||
type: "Standard",
|
||||
},
|
||||
{
|
||||
id: "87f847b3-d81a-424e-8e89-acadb5c9d52b",
|
||||
name: "Github Discussion Trigger",
|
||||
description:
|
||||
"This block triggers on discussion events and outputs the event type and payload.",
|
||||
type: "Standard",
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
async nextTutorialStep(): Promise<void> {
|
||||
console.log(`clicking next tutorial step`);
|
||||
await this.page.getByRole("button", { name: "Next" }).click();
|
||||
}
|
||||
|
||||
async getBlocksToSkip(): Promise<string[]> {
|
||||
return [
|
||||
(await this.getGithubTriggerBlockDetails()).map((b) => b.id),
|
||||
// MCP Tool block requires an interactive dialog (server URL + OAuth) before
|
||||
// it can be placed, so it can't be tested via the standard "add block" flow.
|
||||
"a0a4b1c2-d3e4-4f56-a7b8-c9d0e1f2a3b4",
|
||||
].flat();
|
||||
}
|
||||
|
||||
async createDummyAgent() {
|
||||
async createDummyAgent(): Promise<void> {
|
||||
await this.closeTutorial();
|
||||
await this.openBlocksPanel();
|
||||
|
||||
const searchInput = this.page.locator(
|
||||
'[data-id="blocks-control-search-bar"] input[type="text"]',
|
||||
);
|
||||
|
||||
await searchInput.clear();
|
||||
await searchInput.fill("Add to Dictionary");
|
||||
|
||||
const blockCard = this.page.locator('[data-id^="block-card-"]').first();
|
||||
try {
|
||||
await blockCard.waitFor({ state: "visible", timeout: 10000 });
|
||||
await blockCard.click();
|
||||
} catch (error) {
|
||||
console.log("Could not find Add to Dictionary block:", error);
|
||||
}
|
||||
|
||||
await this.addBlockByClick("Add to Dictionary");
|
||||
await this.waitForNodeOnCanvas(1);
|
||||
await this.saveAgent("Test Agent", "Test Description");
|
||||
await this.waitForSaveComplete();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user