feat(frontend): monitor tests (#8880)
<!-- Clearly explain the need for these changes: --> We want to be able to test the monitor page with importing and exporting agents ### Changes 🏗️ - Adds more test ids - Builds out monitor.page.ts - adds import export tests - Fixes #8791, fixes #8795, fixes #8792 <!-- Concisely describe all of the changes made in this pull request: --> ### Checklist 📋 #### For code changes: - [ ] I have clearly listed my changes in the PR description - [ ] I have made a test plan - [ ] I have tested my changes according to the test plan: Writing/Running the automated tests
This commit is contained in:
parent
eb79c04855
commit
b62f411518
|
@ -173,3 +173,4 @@ LICENSE.rtf
|
|||
autogpt_platform/backend/settings.py
|
||||
/.auth
|
||||
/autogpt_platform/frontend/.auth
|
||||
.test-contents
|
|
@ -84,7 +84,10 @@ const Monitor = () => {
|
|||
};
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-5 lg:grid-cols-4 xl:grid-cols-10">
|
||||
<div
|
||||
className="grid grid-cols-1 gap-4 md:grid-cols-5 lg:grid-cols-4 xl:grid-cols-10"
|
||||
data-testid="monitor-page"
|
||||
>
|
||||
<AgentFlowList
|
||||
className={column1}
|
||||
flows={flows}
|
||||
|
|
|
@ -132,6 +132,7 @@ export const AgentImportForm: React.FC<
|
|||
<Input
|
||||
type="file"
|
||||
accept="application/json"
|
||||
data-testid="import-agent-file-input"
|
||||
onChange={(e) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (file) {
|
||||
|
@ -181,7 +182,7 @@ export const AgentImportForm: React.FC<
|
|||
<FormItem>
|
||||
<FormLabel>Agent name</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
<Input {...field} data-testid="agent-name-input" />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
|
@ -195,7 +196,7 @@ export const AgentImportForm: React.FC<
|
|||
<FormItem>
|
||||
<FormLabel>Agent description</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea {...field} />
|
||||
<Textarea {...field} data-testid="agent-description-input" />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
|
@ -218,6 +219,7 @@ export const AgentImportForm: React.FC<
|
|||
Agent
|
||||
</span>
|
||||
<Switch
|
||||
data-testid="import-as-template-switch"
|
||||
disabled={field.disabled}
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
|
@ -235,7 +237,12 @@ export const AgentImportForm: React.FC<
|
|||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Button type="submit" className="w-full" disabled={!agentObject}>
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full"
|
||||
disabled={!agentObject}
|
||||
data-testid="import-agent-submit"
|
||||
>
|
||||
<EnterIcon className="mr-2" /> Import & Edit
|
||||
</Button>
|
||||
</form>
|
||||
|
|
|
@ -68,6 +68,7 @@ export const AgentFlowList = ({
|
|||
<Button
|
||||
variant="outline"
|
||||
className={"rounded-l-none border-l-0 px-2"}
|
||||
data-testid="create-agent-dropdown"
|
||||
>
|
||||
<ChevronDownIcon />
|
||||
</Button>
|
||||
|
@ -75,7 +76,7 @@ export const AgentFlowList = ({
|
|||
|
||||
<DropdownMenuContent>
|
||||
<DialogTrigger asChild>
|
||||
<DropdownMenuItem>
|
||||
<DropdownMenuItem data-testid="import-agent-from-file">
|
||||
<EnterIcon className="mr-2" /> Import from file
|
||||
</DropdownMenuItem>
|
||||
</DialogTrigger>
|
||||
|
@ -134,7 +135,7 @@ export const AgentFlowList = ({
|
|||
{flowRuns && <TableHead>Last run</TableHead>}
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableBody data-testid="agent-flow-list-body">
|
||||
{flows
|
||||
.map((flow) => {
|
||||
let runCount = 0,
|
||||
|
@ -162,6 +163,8 @@ export const AgentFlowList = ({
|
|||
.map(({ flow, runCount, lastRun }) => (
|
||||
<TableRow
|
||||
key={flow.id}
|
||||
data-testid={flow.id}
|
||||
data-name={flow.name}
|
||||
className="cursor-pointer"
|
||||
onClick={() => onSelectFlow(flow)}
|
||||
data-state={selectedFlow?.id == flow.id ? "selected" : null}
|
||||
|
|
|
@ -115,6 +115,7 @@ export const FlowInfo: React.FC<
|
|||
variant="outline"
|
||||
className="px-2.5"
|
||||
title="Export to a JSON-file"
|
||||
data-testid="export-button"
|
||||
onClick={async () =>
|
||||
exportAsJSONFile(
|
||||
safeCopyGraph(
|
||||
|
@ -129,7 +130,11 @@ export const FlowInfo: React.FC<
|
|||
>
|
||||
<ExitIcon className="mr-2" /> Export
|
||||
</Button>
|
||||
<Button variant="outline" onClick={() => setIsDeleteModalOpen(true)}>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setIsDeleteModalOpen(true)}
|
||||
data-testid="delete-button"
|
||||
>
|
||||
<Trash2Icon className="h-full" />
|
||||
</Button>
|
||||
</div>
|
||||
|
|
|
@ -35,10 +35,13 @@ export const FlowRunsList: React.FC<{
|
|||
<TableHead>Duration</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableBody data-testid="flow-runs-list-body">
|
||||
{runs.map((run) => (
|
||||
<TableRow
|
||||
key={run.id}
|
||||
data-testid={`flow-run-${run.id}-graph-${run.graphID}`}
|
||||
data-runid={run.id}
|
||||
data-graphid={run.graphID}
|
||||
className="cursor-pointer"
|
||||
onClick={() => onSelectRun(run)}
|
||||
data-state={selectedRun?.id == run.id ? "selected" : null}
|
||||
|
|
|
@ -28,13 +28,11 @@ test.describe("Build", () => { //(1)!
|
|||
test("user can add a block", async ({ page }) => { //(6)!
|
||||
await test.expect(buildPage.isLoaded()).resolves.toBeTruthy(); //(7)!
|
||||
await test.expect(page).toHaveURL(new RegExp("/.*build")); //(8)!
|
||||
|
||||
await buildPage.closeTutorial(); //(9)!
|
||||
await buildPage.openBlocksPanel(); //(10)!
|
||||
const block = {
|
||||
id: "31d1064e-7446-4693-a7d4-65e5ca1180d1",
|
||||
name: "Add to Dictionary",
|
||||
description: "Add to Dictionary",
|
||||
};
|
||||
const block = await buildPage.getBasicBlock();
|
||||
|
||||
await buildPage.addBlock(block); //(11)!
|
||||
await buildPage.closeBlocksPanel(); //(12)!
|
||||
await test.expect(buildPage.hasBlock(block)).resolves.toBeTruthy(); //(13)!
|
||||
|
|
|
@ -0,0 +1,125 @@
|
|||
import { expect, TestInfo } from "@playwright/test";
|
||||
import { test } from "./fixtures";
|
||||
import { BuildPage } from "./pages/build.page";
|
||||
import { MonitorPage } from "./pages/monitor.page";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import * as fs from "fs/promises";
|
||||
import path from "path";
|
||||
// --8<-- [start:AttachAgentId]
|
||||
|
||||
test.describe("Monitor", () => {
|
||||
let buildPage: BuildPage;
|
||||
let monitorPage: MonitorPage;
|
||||
|
||||
test.beforeEach(async ({ page, loginPage, testUser }, testInfo: TestInfo) => {
|
||||
buildPage = new BuildPage(page);
|
||||
monitorPage = new MonitorPage(page);
|
||||
|
||||
// Start each test with login using worker auth
|
||||
await page.goto("/login");
|
||||
await loginPage.login(testUser.email, testUser.password);
|
||||
await test.expect(page).toHaveURL("/");
|
||||
|
||||
// add a test agent
|
||||
const basicBlock = await buildPage.getBasicBlock();
|
||||
const id = uuidv4();
|
||||
await buildPage.createSingleBlockAgent(
|
||||
`test-agent-${id}`,
|
||||
`test-agent-description-${id}`,
|
||||
basicBlock,
|
||||
);
|
||||
await buildPage.runAgent();
|
||||
await monitorPage.navbar.clickMonitorLink();
|
||||
await monitorPage.waitForPageLoad();
|
||||
await test.expect(monitorPage.isLoaded()).resolves.toBeTruthy();
|
||||
testInfo.attach("agent-id", { body: id });
|
||||
});
|
||||
// --8<-- [end:AttachAgentId]
|
||||
|
||||
test.afterAll(async ({}) => {
|
||||
// clear out the downloads folder
|
||||
console.log(
|
||||
`clearing out the downloads folder ${monitorPage.downloadsFolder}`,
|
||||
);
|
||||
|
||||
await fs.rm(`${monitorPage.downloadsFolder}/monitor`, {
|
||||
recursive: true,
|
||||
force: true,
|
||||
});
|
||||
});
|
||||
|
||||
test("user can view agents", async ({ page }) => {
|
||||
const agents = await monitorPage.listAgents();
|
||||
// there should be at least one agent
|
||||
await test.expect(agents.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test("user can export and import agents", async ({
|
||||
page,
|
||||
}, testInfo: TestInfo) => {
|
||||
// --8<-- [start:ReadAgentId]
|
||||
if (testInfo.attachments.length === 0 || !testInfo.attachments[0].body) {
|
||||
throw new Error("No agent id attached to the test");
|
||||
}
|
||||
const id = testInfo.attachments[0].body.toString();
|
||||
// --8<-- [end:ReadAgentId]
|
||||
const agents = await monitorPage.listAgents();
|
||||
|
||||
const downloadPromise = page.waitForEvent("download");
|
||||
await monitorPage.exportToFile(
|
||||
agents.find((a) => a.id === id) || agents[0],
|
||||
);
|
||||
const download = await downloadPromise;
|
||||
|
||||
// Wait for the download process to complete and save the downloaded file somewhere.
|
||||
await download.saveAs(
|
||||
`${monitorPage.downloadsFolder}/monitor/${download.suggestedFilename()}`,
|
||||
);
|
||||
console.log(`downloaded file to ${download.suggestedFilename()}`);
|
||||
await test.expect(download.suggestedFilename()).toBeDefined();
|
||||
// test-agent-uuid-v1.json
|
||||
if (id) {
|
||||
await test.expect(download.suggestedFilename()).toContain(id);
|
||||
}
|
||||
await test.expect(download.suggestedFilename()).toContain("test-agent-");
|
||||
await test.expect(download.suggestedFilename()).toContain("v1.json");
|
||||
|
||||
// import the agent
|
||||
const preImportAgents = await monitorPage.listAgents();
|
||||
const filesInFolder = await fs.readdir(
|
||||
`${monitorPage.downloadsFolder}/monitor`,
|
||||
);
|
||||
const importFile = filesInFolder.find((f) => f.includes(id));
|
||||
if (!importFile) {
|
||||
throw new Error(`No import file found for agent ${id}`);
|
||||
}
|
||||
const baseName = importFile.split(".")[0];
|
||||
await monitorPage.importFromFile(
|
||||
path.resolve(monitorPage.downloadsFolder, "monitor"),
|
||||
importFile,
|
||||
baseName + "-imported",
|
||||
);
|
||||
|
||||
// You'll be dropped at the build page, so hit run and then go back to monitor
|
||||
await buildPage.runAgent();
|
||||
await monitorPage.navbar.clickMonitorLink();
|
||||
await monitorPage.waitForPageLoad();
|
||||
|
||||
const postImportAgents = await monitorPage.listAgents();
|
||||
await test
|
||||
.expect(postImportAgents.length)
|
||||
.toBeGreaterThan(preImportAgents.length);
|
||||
console.log(`postImportAgents: ${JSON.stringify(postImportAgents)}`);
|
||||
const importedAgent = postImportAgents.find(
|
||||
(a) => a.name === `${baseName}-imported`,
|
||||
);
|
||||
await test.expect(importedAgent).toBeDefined();
|
||||
});
|
||||
|
||||
test("user can view runs", async ({ page }) => {
|
||||
const runs = await monitorPage.listRuns();
|
||||
console.log(runs);
|
||||
// there should be at least one run
|
||||
await test.expect(runs.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
|
@ -3,6 +3,7 @@ import { NavBar } from "./navbar.page";
|
|||
|
||||
export class BasePage {
|
||||
readonly navbar: NavBar;
|
||||
readonly downloadsFolder = "./.test-contents";
|
||||
|
||||
constructor(protected page: Page) {
|
||||
this.navbar = new NavBar(page);
|
||||
|
@ -10,6 +11,7 @@ export class BasePage {
|
|||
|
||||
async waitForPageLoad() {
|
||||
// Common page load waiting logic
|
||||
console.log(`waiting for page to load`);
|
||||
await this.page.waitForLoadState("networkidle", { timeout: 10000 });
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,7 +13,12 @@ export class BuildPage extends BasePage {
|
|||
}
|
||||
|
||||
async closeTutorial(): Promise<void> {
|
||||
await this.page.getByRole("button", { name: "Skip Tutorial" }).click();
|
||||
console.log(`closing tutorial`);
|
||||
try {
|
||||
await this.page.getByRole("button", { name: "Skip Tutorial" }).click();
|
||||
} catch (error) {
|
||||
console.info("Error closing tutorial:", error);
|
||||
}
|
||||
}
|
||||
|
||||
async openBlocksPanel(): Promise<void> {
|
||||
|
@ -25,6 +30,7 @@ export class BuildPage extends BasePage {
|
|||
}
|
||||
|
||||
async closeBlocksPanel(): Promise<void> {
|
||||
console.log(`closing blocks panel`);
|
||||
if (
|
||||
await this.page.getByTestId("blocks-control-blocks-label").isVisible()
|
||||
) {
|
||||
|
@ -36,6 +42,7 @@ export class BuildPage extends BasePage {
|
|||
name: string = "Test Agent",
|
||||
description: string = "",
|
||||
): Promise<void> {
|
||||
console.log(`saving agent ${name} with description ${description}`);
|
||||
await this.page.getByTestId("blocks-control-save-button").click();
|
||||
await this.page.getByTestId("save-control-name-input").fill(name);
|
||||
await this.page
|
||||
|
@ -45,6 +52,7 @@ export class BuildPage extends BasePage {
|
|||
}
|
||||
|
||||
async getBlocks(): Promise<Block[]> {
|
||||
console.log(`getting blocks in sidebar panel`);
|
||||
try {
|
||||
const blocks = await this.page.locator('[data-id^="block-card-"]').all();
|
||||
|
||||
|
@ -89,10 +97,14 @@ export class BuildPage extends BasePage {
|
|||
}
|
||||
|
||||
async isRFNodeVisible(nodeId: string): Promise<boolean> {
|
||||
console.log(`checking if RF node ${nodeId} is visible on page`);
|
||||
return await this.page.getByTestId(`rf__node-${nodeId}`).isVisible();
|
||||
}
|
||||
|
||||
async hasBlock(block: Block): Promise<boolean> {
|
||||
console.log(
|
||||
`checking if block ${block.id} ${block.name} is visible on page`,
|
||||
);
|
||||
try {
|
||||
// Use both ID and name for most precise matching
|
||||
const node = await this.page
|
||||
|
@ -106,6 +118,7 @@ export class BuildPage extends BasePage {
|
|||
}
|
||||
|
||||
async getBlockInputs(blockId: string): Promise<string[]> {
|
||||
console.log(`getting block ${blockId} inputs`);
|
||||
try {
|
||||
const node = await this.page
|
||||
.locator(`[data-blockid="${blockId}"]`)
|
||||
|
@ -132,10 +145,7 @@ export class BuildPage extends BasePage {
|
|||
// }
|
||||
}
|
||||
|
||||
async build_block_selector(
|
||||
blockId: string,
|
||||
dataId?: string,
|
||||
): Promise<string> {
|
||||
async _buildBlockSelector(blockId: string, dataId?: string): Promise<string> {
|
||||
let selector = dataId
|
||||
? `[data-id="${dataId}"] [data-blockid="${blockId}"]`
|
||||
: `[data-blockid="${blockId}"]`;
|
||||
|
@ -143,8 +153,9 @@ export class BuildPage extends BasePage {
|
|||
}
|
||||
|
||||
async getBlockById(blockId: string, dataId?: string): Promise<Locator> {
|
||||
console.log(`getting block ${blockId} with dataId ${dataId}`);
|
||||
return await this.page.locator(
|
||||
await this.build_block_selector(blockId, dataId),
|
||||
await this._buildBlockSelector(blockId, dataId),
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -157,6 +168,9 @@ export class BuildPage extends BasePage {
|
|||
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 = await block.getByPlaceholder(placeholder);
|
||||
await input.fill(value);
|
||||
|
@ -168,8 +182,11 @@ export class BuildPage extends BasePage {
|
|||
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.build_block_selector(blockId, dataId);
|
||||
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"]`;
|
||||
|
@ -198,7 +215,7 @@ export class BuildPage extends BasePage {
|
|||
label: string,
|
||||
value: string,
|
||||
): Promise<void> {
|
||||
// throw new Error("Not implemented");
|
||||
console.log(`filling block input ${label} with value ${value}`);
|
||||
const block = await this.getBlockById(blockId);
|
||||
const input = await block.getByLabel(label);
|
||||
await input.fill(value);
|
||||
|
@ -208,6 +225,9 @@ export class BuildPage extends BasePage {
|
|||
blockOutputId: string,
|
||||
blockInputId: string,
|
||||
): Promise<void> {
|
||||
console.log(
|
||||
`connecting block output ${blockOutputId} to block input ${blockInputId}`,
|
||||
);
|
||||
try {
|
||||
// Locate the output element
|
||||
const outputElement = await this.page.locator(
|
||||
|
@ -232,11 +252,14 @@ export class BuildPage extends BasePage {
|
|||
startDataId?: string,
|
||||
endDataId?: string,
|
||||
): Promise<void> {
|
||||
const startBlockBase = await this.build_block_selector(
|
||||
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.build_block_selector(endBlockId, endDataId);
|
||||
const endBlockBase = await this._buildBlockSelector(endBlockId, endDataId);
|
||||
// Use descendant combinator to find test-id at any depth
|
||||
const startBlockOutputSelector = `${startBlockBase} [data-testid="output-handle-${startBlockOutputName.toLowerCase()}"]`;
|
||||
const endBlockInputSelector = `${endBlockBase} [data-testid="input-handle-${endBlockInputName.toLowerCase()}"]`;
|
||||
|
@ -251,6 +274,7 @@ export class BuildPage extends BasePage {
|
|||
}
|
||||
|
||||
async isLoaded(): Promise<boolean> {
|
||||
console.log(`checking if build page is loaded`);
|
||||
try {
|
||||
await this.page.waitForLoadState("networkidle", { timeout: 10_000 });
|
||||
return true;
|
||||
|
@ -260,40 +284,78 @@ export class BuildPage extends BasePage {
|
|||
}
|
||||
|
||||
async isRunButtonEnabled(): Promise<boolean> {
|
||||
console.log(`checking if run button is enabled`);
|
||||
const runButton = this.page.locator('[data-id="primary-action-run-agent"]');
|
||||
return await runButton.isEnabled();
|
||||
}
|
||||
|
||||
async runAgent(): Promise<void> {
|
||||
console.log(`clicking run button`);
|
||||
const runButton = this.page.locator('[data-id="primary-action-run-agent"]');
|
||||
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(`run-dialog-input-${key}`).fill(value);
|
||||
}
|
||||
}
|
||||
async clickRunDialogRunButton(): Promise<void> {
|
||||
console.log(`clicking run button`);
|
||||
await this.page.getByTestId("run-dialog-run-button").click();
|
||||
}
|
||||
|
||||
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="blocks-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 createSingleBlockAgent(
|
||||
name: string,
|
||||
description: string,
|
||||
block: Block,
|
||||
): Promise<void> {
|
||||
console.log(`creating single block agent ${name}`);
|
||||
await this.navbar.clickBuildLink();
|
||||
await this.closeTutorial();
|
||||
await this.openBlocksPanel();
|
||||
await this.addBlock(block);
|
||||
await this.saveAgent(name, description);
|
||||
await this.waitForVersionField();
|
||||
}
|
||||
|
||||
async getBasicBlock(): Promise<Block> {
|
||||
return {
|
||||
id: "31d1064e-7446-4693-a7d4-65e5ca1180d1",
|
||||
name: "Add to Dictionary",
|
||||
description: "Add to Dictionary",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,235 @@
|
|||
import { ElementHandle, Locator, Page } from "@playwright/test";
|
||||
import { BasePage } from "./base.page";
|
||||
import path from "path";
|
||||
|
||||
interface Agent {
|
||||
id: string;
|
||||
name: string;
|
||||
runCount: number;
|
||||
lastRun: string;
|
||||
}
|
||||
|
||||
interface Run {
|
||||
id: string;
|
||||
agentId: string;
|
||||
agentName: string;
|
||||
started: string;
|
||||
duration: number;
|
||||
status: string;
|
||||
}
|
||||
|
||||
interface AgentRun extends Agent {
|
||||
runs: Run[];
|
||||
}
|
||||
|
||||
interface Schedule {
|
||||
id: string;
|
||||
graphName: string;
|
||||
nextExecution: string;
|
||||
schedule: string;
|
||||
actions: string[];
|
||||
}
|
||||
|
||||
enum ImportType {
|
||||
AGENT = "agent",
|
||||
TEMPLATE = "template",
|
||||
}
|
||||
|
||||
export class MonitorPage extends BasePage {
|
||||
constructor(page: Page) {
|
||||
super(page);
|
||||
}
|
||||
|
||||
async isLoaded(): Promise<boolean> {
|
||||
console.log(`checking if monitor page is loaded`);
|
||||
try {
|
||||
// Wait for network to settle first
|
||||
await this.page.waitForLoadState("networkidle", { timeout: 10_000 });
|
||||
|
||||
// Wait for the monitor page container
|
||||
await this.page.getByTestId("monitor-page").waitFor({
|
||||
state: "visible",
|
||||
timeout: 10_000,
|
||||
});
|
||||
|
||||
// Wait for table headers to be visible (indicates table structure is ready)
|
||||
await this.page.locator("thead th").first().waitFor({
|
||||
state: "visible",
|
||||
timeout: 5_000,
|
||||
});
|
||||
|
||||
// Wait for either a table row or an empty tbody to be present
|
||||
await Promise.race([
|
||||
// Wait for at least one row
|
||||
this.page.locator("tbody tr[data-testid]").first().waitFor({
|
||||
state: "visible",
|
||||
timeout: 5_000,
|
||||
}),
|
||||
// OR wait for an empty tbody (indicating no agents but table is loaded)
|
||||
this.page
|
||||
.locator("tbody[data-testid='agent-flow-list-body']:empty")
|
||||
.waitFor({
|
||||
state: "visible",
|
||||
timeout: 5_000,
|
||||
}),
|
||||
]);
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async listAgents(): Promise<Agent[]> {
|
||||
console.log(`listing agents`);
|
||||
// Wait for table rows to be available
|
||||
const rows = await this.page.locator("tbody tr[data-testid]").all();
|
||||
|
||||
const agents: Agent[] = [];
|
||||
|
||||
for (const row of rows) {
|
||||
// Get the id from data-testid attribute
|
||||
const id = (await row.getAttribute("data-testid")) || "";
|
||||
|
||||
// Get columns - there are 3 cells per row (name, run count, last run)
|
||||
const cells = await row.locator("td").all();
|
||||
|
||||
// Extract name from first cell
|
||||
const name = (await row.getAttribute("data-name")) || "";
|
||||
|
||||
// Extract run count from second cell
|
||||
const runCountText = (await cells[1].textContent()) || "0";
|
||||
const runCount = parseInt(runCountText, 10);
|
||||
|
||||
// Extract last run from third cell's title attribute (contains full timestamp)
|
||||
// If no title, the cell will be empty indicating no last run
|
||||
const lastRunCell = cells[2];
|
||||
const lastRun = (await lastRunCell.getAttribute("title")) || "";
|
||||
|
||||
agents.push({
|
||||
id,
|
||||
name,
|
||||
runCount,
|
||||
lastRun,
|
||||
});
|
||||
}
|
||||
|
||||
return agents;
|
||||
}
|
||||
|
||||
async listRuns(filter?: Agent): Promise<Run[]> {
|
||||
console.log(`listing runs`);
|
||||
// Wait for the runs table to be loaded - look for table header "Agent"
|
||||
await this.page.locator("[data-testid='flow-runs-list-body']").waitFor();
|
||||
|
||||
// Get all run rows
|
||||
const rows = await this.page
|
||||
.locator('tbody tr[data-testid^="flow-run-"]')
|
||||
.all();
|
||||
|
||||
const runs: Run[] = [];
|
||||
|
||||
for (const row of rows) {
|
||||
const runId = (await row.getAttribute("data-runid")) || "";
|
||||
const agentId = (await row.getAttribute("data-graphid")) || "";
|
||||
|
||||
// Get columns
|
||||
const cells = await row.locator("td").all();
|
||||
|
||||
// Parse data from cells
|
||||
const agentName = (await cells[0].textContent()) || "";
|
||||
const started = (await cells[1].textContent()) || "";
|
||||
const status = (await cells[2].locator("div").textContent()) || "";
|
||||
const duration = (await cells[3].textContent()) || "";
|
||||
|
||||
// Only add if no filter or if matches filter
|
||||
if (!filter || filter.id === agentId) {
|
||||
runs.push({
|
||||
id: runId,
|
||||
agentId: agentId,
|
||||
agentName: agentName.trim(),
|
||||
started: started.trim(),
|
||||
duration: parseFloat(duration.replace("s", "")),
|
||||
status: status.toLowerCase().trim(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return runs;
|
||||
}
|
||||
async listSchedules(): Promise<Schedule[]> {
|
||||
console.log(`listing schedules`);
|
||||
return [];
|
||||
}
|
||||
|
||||
async clickAgent(id: string) {
|
||||
console.log(`selecting agent ${id}`);
|
||||
await this.page.getByTestId(id).click();
|
||||
}
|
||||
|
||||
async clickCreateAgent(): Promise<void> {
|
||||
console.log(`clicking create agent`);
|
||||
await this.page.getByRole("link", { name: "Create" }).click();
|
||||
}
|
||||
|
||||
async importFromFile(
|
||||
directory: string,
|
||||
file: string,
|
||||
name?: string,
|
||||
description?: string,
|
||||
importType: ImportType = ImportType.AGENT,
|
||||
) {
|
||||
console.log(
|
||||
`importing from directory: ${directory} file: ${file} name: ${name} description: ${description} importType: ${importType}`,
|
||||
);
|
||||
await this.page.getByTestId("create-agent-dropdown").click();
|
||||
await this.page.getByTestId("import-agent-from-file").click();
|
||||
|
||||
await this.page
|
||||
.getByTestId("import-agent-file-input")
|
||||
.setInputFiles(path.join(directory, file));
|
||||
if (name) {
|
||||
console.log(`filling agent name: ${name}`);
|
||||
await this.page.getByTestId("agent-name-input").fill(name);
|
||||
}
|
||||
if (description) {
|
||||
console.log(`filling agent description: ${description}`);
|
||||
await this.page.getByTestId("agent-description-input").fill(description);
|
||||
}
|
||||
if (importType === ImportType.TEMPLATE) {
|
||||
console.log(`clicking import as template switch`);
|
||||
await this.page.getByTestId("import-as-template-switch").click();
|
||||
}
|
||||
console.log(`clicking import agent submit`);
|
||||
await this.page.getByTestId("import-agent-submit").click();
|
||||
}
|
||||
|
||||
async deleteAgent(agent: Agent) {
|
||||
console.log(`deleting agent ${agent.id} ${agent.name}`);
|
||||
}
|
||||
|
||||
async clickAllVersions(agent: Agent) {
|
||||
console.log(`clicking all versions for agent ${agent.id} ${agent.name}`);
|
||||
}
|
||||
|
||||
async openInBuilder(agent: Agent) {
|
||||
console.log(`opening agent ${agent.id} ${agent.name} in builder`);
|
||||
}
|
||||
|
||||
async exportToFile(agent: Agent) {
|
||||
await this.clickAgent(agent.id);
|
||||
|
||||
console.log(`exporting agent ${agent.id} ${agent.name} to file`);
|
||||
await this.page.getByTestId("export-button").click();
|
||||
}
|
||||
|
||||
async selectRun(agent: Agent, run: Run) {
|
||||
console.log(`selecting run ${run.id} for agent ${agent.id} ${agent.name}`);
|
||||
}
|
||||
|
||||
async openOutputs(agent: Agent, run: Run) {
|
||||
console.log(
|
||||
`opening outputs for run ${run.id} of agent ${agent.id} ${agent.name}`,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,4 +1,3 @@
|
|||
// profile.spec.ts
|
||||
import { test } from "./fixtures";
|
||||
import { ProfilePage } from "./pages/profile.page";
|
||||
|
||||
|
|
|
@ -2,6 +2,10 @@
|
|||
|
||||
We use [Playwright](https://playwright.dev/) for our testing framework.
|
||||
|
||||
## Before you start
|
||||
|
||||
Almost all of the tests require that you are running the frontend and backend servers. You will hit strange and hard to debug errors if you don't have them running because the tests will try to interact with the application when it's not running in an interactable state.
|
||||
|
||||
## Running the tests
|
||||
|
||||
To run the tests, you can use the following commands:
|
||||
|
@ -142,6 +146,19 @@ A good test suite will have a healthy mix of focused and non-focused tests.
|
|||
12. The `await buildPage.closeBlocksPanel();` is used to close the blocks panel on the build page.
|
||||
13. The `await test.expect(buildPage.hasBlock(block)).resolves.toBeTruthy();` is used to check that the block has been added to the build page.
|
||||
|
||||
### Passing information between tests
|
||||
|
||||
You can pass information between tests using the `testInfo` object. This is useful for things like passing the id of an agent between beforeAll so that you can have a shared setup for multiple tests.
|
||||
|
||||
```typescript title="frontend/src/tests/monitor.spec.ts"
|
||||
--8<-- "autogpt_platform/frontend/src/tests/monitor.spec.ts:AttachAgentId"
|
||||
|
||||
test("test can read the agent id", async ({ page }, testInfo) => {
|
||||
--8<-- "autogpt_platform/frontend/src/tests/monitor.spec.ts:ReadAgentId"
|
||||
/// ... Do something with the agent id here
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## See Also
|
||||
|
||||
|
|
Loading…
Reference in New Issue