@@ -74,6 +89,16 @@ function ProviderTable({ data, rateOverrides, onRateOverride }: Props) {
@@ -124,7 +149,7 @@ function ProviderTable({ data, rateOverrides, onRateOverride }: Props) {
{data.length === 0 && (
@@ -61,13 +58,12 @@ function UserTable({ data }: Props) {
{formatTokens(row.total_output_tokens)}
@@ -75,7 +71,7 @@ function UserTable({ data }: Props) {
{data.length === 0 && (
No cost data yet
diff --git a/autogpt_platform/frontend/src/app/(platform)/marketplace/components/MainAgentPage/__tests__/main.test.tsx b/autogpt_platform/frontend/src/app/(platform)/marketplace/components/MainAgentPage/__tests__/main.test.tsx
new file mode 100644
index 0000000000..f9a9d76f12
--- /dev/null
+++ b/autogpt_platform/frontend/src/app/(platform)/marketplace/components/MainAgentPage/__tests__/main.test.tsx
@@ -0,0 +1,96 @@
+import {
+ getGetV2GetSpecificAgentMockHandler,
+ getGetV2GetSpecificAgentResponseMock,
+ getGetV2ListStoreAgentsMockHandler,
+ getGetV2ListStoreAgentsResponseMock,
+} from "@/app/api/__generated__/endpoints/store/store.msw";
+import { server } from "@/mocks/mock-server";
+import { render, screen } from "@/tests/integrations/test-utils";
+import { MainAgentPage } from "../MainAgentPage";
+import { beforeEach, describe, expect, test, vi } from "vitest";
+
+const mockUseSupabase = vi.hoisted(() => vi.fn());
+
+vi.mock("@/lib/supabase/hooks/useSupabase", () => ({
+ useSupabase: mockUseSupabase,
+}));
+
+describe("MainAgentPage", () => {
+ beforeEach(() => {
+ mockUseSupabase.mockReturnValue({
+ user: null,
+ });
+ });
+
+ test("renders the marketplace agent details and related sections", async () => {
+ const agentDetails = getGetV2GetSpecificAgentResponseMock({
+ agent_name: "Deterministic Agent",
+ creator: "AutoGPT",
+ creator_avatar: "",
+ sub_heading: "A stable marketplace listing",
+ description: "This agent is used for integration coverage.",
+ categories: ["demo", "test"],
+ versions: ["1", "2"],
+ active_version_id: "store-version-1",
+ store_listing_version_id: "listing-1",
+ agent_image: ["https://example.com/agent.png"],
+ agent_output_demo: "",
+ agent_video: "",
+ });
+ const otherAgents = getGetV2ListStoreAgentsResponseMock({
+ agents: [
+ {
+ ...getGetV2ListStoreAgentsResponseMock().agents[0],
+ slug: "other-agent",
+ agent_name: "Other Agent",
+ creator: "AutoGPT",
+ },
+ ],
+ });
+ const similarAgents = getGetV2ListStoreAgentsResponseMock({
+ agents: [
+ {
+ ...getGetV2ListStoreAgentsResponseMock().agents[0],
+ slug: "similar-agent",
+ agent_name: "Similar Agent",
+ creator: "Another Creator",
+ },
+ ],
+ });
+
+ server.use(
+ getGetV2GetSpecificAgentMockHandler(agentDetails),
+ getGetV2ListStoreAgentsMockHandler(({ request }) => {
+ const url = new URL(request.url);
+
+ if (url.searchParams.get("creator") === "autogpt") {
+ return otherAgents;
+ }
+
+ if (url.searchParams.get("search_query") === "deterministic agent") {
+ return similarAgents;
+ }
+
+ return getGetV2ListStoreAgentsResponseMock({ agents: [] });
+ }),
+ );
+
+ render(
+ ,
+ );
+
+ expect((await screen.findByTestId("agent-title")).textContent).toContain(
+ "Deterministic Agent",
+ );
+ expect(screen.getByTestId("agent-description").textContent).toContain(
+ "This agent is used for integration coverage.",
+ );
+ expect(screen.getByTestId("agent-creator").textContent).toContain(
+ "AutoGPT",
+ );
+ expect(screen.getByText("Other agents by AutoGPT")).toBeDefined();
+ expect(screen.getByText("Similar agents")).toBeDefined();
+ });
+});
diff --git a/autogpt_platform/frontend/src/app/(platform)/marketplace/components/MainMarketplacePage/__tests__/main.test.tsx b/autogpt_platform/frontend/src/app/(platform)/marketplace/components/MainMarketplacePage/__tests__/main.test.tsx
index bee227a7af..0e902abe44 100644
--- a/autogpt_platform/frontend/src/app/(platform)/marketplace/components/MainMarketplacePage/__tests__/main.test.tsx
+++ b/autogpt_platform/frontend/src/app/(platform)/marketplace/components/MainMarketplacePage/__tests__/main.test.tsx
@@ -1,15 +1,64 @@
-import { expect, test } from "vitest";
+import {
+ getGetV2ListStoreAgentsResponseMock,
+ getGetV2ListStoreCreatorsResponseMock,
+} from "@/app/api/__generated__/endpoints/store/store.msw";
import { render, screen } from "@/tests/integrations/test-utils";
import { MainMarkeplacePage } from "../MainMarketplacePage";
-import { server } from "@/mocks/mock-server";
-import { getDeleteV2DeleteStoreSubmissionMockHandler422 } from "@/app/api/__generated__/endpoints/store/store.msw";
+import { beforeEach, describe, expect, test, vi } from "vitest";
-// Only for CI testing purpose, will remove it in future PR
-test("MainMarketplacePage", async () => {
- server.use(getDeleteV2DeleteStoreSubmissionMockHandler422());
+const mockUseMainMarketplacePage = vi.hoisted(() => vi.fn());
- render();
- expect(
- await screen.findByText("Featured agents", { exact: false }),
- ).toBeDefined();
+vi.mock("../useMainMarketplacePage", () => ({
+ useMainMarketplacePage: mockUseMainMarketplacePage,
+}));
+
+describe("MainMarketplacePage", () => {
+ beforeEach(() => {
+ mockUseMainMarketplacePage.mockReturnValue({
+ featuredAgents: getGetV2ListStoreAgentsResponseMock({
+ agents: [
+ {
+ ...getGetV2ListStoreAgentsResponseMock().agents[0],
+ slug: "featured-agent",
+ agent_name: "Featured Agent",
+ creator: "AutoGPT",
+ },
+ ],
+ }),
+ topAgents: getGetV2ListStoreAgentsResponseMock({
+ agents: [
+ {
+ ...getGetV2ListStoreAgentsResponseMock().agents[0],
+ slug: "top-agent",
+ agent_name: "Top Agent",
+ creator: "AutoGPT",
+ },
+ ],
+ }),
+ featuredCreators: getGetV2ListStoreCreatorsResponseMock({
+ creators: [
+ {
+ ...getGetV2ListStoreCreatorsResponseMock().creators[0],
+ name: "Creator One",
+ username: "creator-one",
+ },
+ ],
+ }),
+ isLoading: false,
+ hasError: false,
+ });
+ });
+
+ test("renders featured agents, all agents, and creators", () => {
+ render();
+
+ expect(screen.getByText(/Featured agents/i)).toBeDefined();
+ expect(screen.getByText("Featured Agent")).toBeDefined();
+ expect(screen.getByText("All Agents")).toBeDefined();
+ expect(screen.getAllByText("Top Agent").length).toBeGreaterThan(0);
+ expect(screen.getByText("Creator One")).toBeDefined();
+ expect(
+ screen.getByRole("button", { name: "Become a Creator" }),
+ ).toBeDefined();
+ });
});
diff --git a/autogpt_platform/frontend/src/app/(platform)/marketplace/creator/[creator]/components/MainCreatorPage/__tests__/main.test.tsx b/autogpt_platform/frontend/src/app/(platform)/marketplace/creator/[creator]/components/MainCreatorPage/__tests__/main.test.tsx
new file mode 100644
index 0000000000..b3224fa3ce
--- /dev/null
+++ b/autogpt_platform/frontend/src/app/(platform)/marketplace/creator/[creator]/components/MainCreatorPage/__tests__/main.test.tsx
@@ -0,0 +1,57 @@
+import { render, screen } from "@/tests/integrations/test-utils";
+import {
+ getGetV2GetCreatorDetailsResponseMock,
+ getGetV2ListStoreAgentsResponseMock,
+} from "@/app/api/__generated__/endpoints/store/store.msw";
+import { MainCreatorPage } from "../MainCreatorPage";
+import { beforeEach, describe, expect, test, vi } from "vitest";
+
+const mockUseMainCreatorPage = vi.hoisted(() => vi.fn());
+
+vi.mock("../useMainCreatorPage", () => ({
+ useMainCreatorPage: mockUseMainCreatorPage,
+}));
+
+describe("MainCreatorPage", () => {
+ beforeEach(() => {
+ const creator = getGetV2GetCreatorDetailsResponseMock({
+ name: "Creator One",
+ username: "creator-one",
+ description: "Creator profile used for integration coverage.",
+ avatar_url: "",
+ top_categories: ["automation", "productivity"],
+ links: ["https://example.com/creator"],
+ });
+
+ const creatorAgents = getGetV2ListStoreAgentsResponseMock({
+ agents: [
+ {
+ ...getGetV2ListStoreAgentsResponseMock().agents[0],
+ slug: "creator-agent",
+ agent_name: "Creator Agent",
+ creator: "Creator One",
+ },
+ ],
+ });
+
+ mockUseMainCreatorPage.mockReturnValue({
+ creatorAgents,
+ creator,
+ isLoading: false,
+ hasError: false,
+ });
+ });
+
+ test("renders creator details and their agents", () => {
+ render();
+
+ expect(screen.getByTestId("creator-title").textContent).toContain(
+ "Creator One",
+ );
+ expect(screen.getByTestId("creator-description").textContent).toContain(
+ "Creator profile used for integration coverage.",
+ );
+ expect(screen.getByText("Agents by Creator One")).toBeDefined();
+ expect(screen.getAllByText("Creator Agent").length).toBeGreaterThan(0);
+ });
+});
diff --git a/autogpt_platform/frontend/src/app/(platform)/profile/(user)/__tests__/page.test.tsx b/autogpt_platform/frontend/src/app/(platform)/profile/(user)/__tests__/page.test.tsx
new file mode 100644
index 0000000000..c6cd516c26
--- /dev/null
+++ b/autogpt_platform/frontend/src/app/(platform)/profile/(user)/__tests__/page.test.tsx
@@ -0,0 +1,83 @@
+import type { ReactNode } from "react";
+import {
+ render,
+ screen,
+ fireEvent,
+ waitFor,
+} from "@/tests/integrations/test-utils";
+import {
+ getGetV2GetUserProfileMockHandler,
+ getPostV2UpdateUserProfileMockHandler,
+} from "@/app/api/__generated__/endpoints/store/store.msw";
+import { server } from "@/mocks/mock-server";
+import UserProfilePage from "../page";
+import { beforeEach, describe, expect, test, vi } from "vitest";
+
+const mockUseSupabase = vi.hoisted(() => vi.fn());
+
+vi.mock("@/providers/onboarding/onboarding-provider", () => ({
+ default: ({ children }: { children: ReactNode }) => <>{children}>,
+}));
+
+vi.mock("@/lib/supabase/hooks/useSupabase", () => ({
+ useSupabase: mockUseSupabase,
+}));
+
+const testUser = {
+ id: "user-1",
+ email: "user@example.com",
+ app_metadata: {},
+ user_metadata: {},
+ aud: "authenticated",
+ created_at: "2026-01-01T00:00:00.000Z",
+};
+
+describe("UserProfilePage", () => {
+ beforeEach(() => {
+ mockUseSupabase.mockReturnValue({
+ user: testUser,
+ isLoggedIn: true,
+ isUserLoading: false,
+ supabase: {},
+ });
+ });
+
+ test("renders the existing profile and saves changes", async () => {
+ let profile = {
+ name: "Original Name",
+ username: "original-user",
+ description: "Original bio",
+ links: ["https://example.com/1", "", "", "", ""],
+ avatar_url: "",
+ is_featured: false,
+ };
+
+ server.use(
+ getGetV2GetUserProfileMockHandler(() => profile),
+ getPostV2UpdateUserProfileMockHandler(async ({ request }) => {
+ profile = (await request.json()) as typeof profile;
+ return profile;
+ }),
+ );
+
+ render();
+
+ const displayName = await screen.findByLabelText("Display name");
+ const handle = screen.getByLabelText("Handle");
+ const bio = screen.getByLabelText("Bio");
+
+ expect((displayName as HTMLInputElement).value).toBe("Original Name");
+ expect((handle as HTMLInputElement).value).toBe("original-user");
+
+ fireEvent.change(displayName, { target: { value: "Updated Name" } });
+ fireEvent.change(handle, { target: { value: "updated-user" } });
+ fireEvent.change(bio, { target: { value: "Updated bio" } });
+ fireEvent.click(screen.getByRole("button", { name: "Save changes" }));
+
+ await waitFor(() => {
+ expect(profile.name).toBe("Updated Name");
+ expect(profile.username).toBe("updated-user");
+ expect(profile.description).toBe("Updated bio");
+ });
+ });
+});
diff --git a/autogpt_platform/frontend/src/app/(platform)/profile/(user)/api-keys/__tests__/page.test.tsx b/autogpt_platform/frontend/src/app/(platform)/profile/(user)/api-keys/__tests__/page.test.tsx
new file mode 100644
index 0000000000..404957e4c0
--- /dev/null
+++ b/autogpt_platform/frontend/src/app/(platform)/profile/(user)/api-keys/__tests__/page.test.tsx
@@ -0,0 +1,138 @@
+import {
+ fireEvent,
+ render,
+ screen,
+ waitFor,
+} from "@/tests/integrations/test-utils";
+import {
+ getDeleteV1RevokeApiKeyMockHandler,
+ getGetV1ListUserApiKeysMockHandler,
+ getPostV1CreateNewApiKeyMockHandler,
+} from "@/app/api/__generated__/endpoints/api-keys/api-keys.msw";
+import { APIKeyPermission } from "@/app/api/__generated__/models/aPIKeyPermission";
+import { APIKeyStatus } from "@/app/api/__generated__/models/aPIKeyStatus";
+import { server } from "@/mocks/mock-server";
+import ApiKeysPage from "../page";
+import { beforeEach, describe, expect, test } from "vitest";
+
+type ApiKeyRecord = {
+ id: string;
+ name: string;
+ head: string;
+ tail: string;
+ status: APIKeyStatus;
+};
+
+function toApiKeyResponse(key: ApiKeyRecord) {
+ return {
+ id: key.id,
+ user_id: "user-1",
+ scopes: [APIKeyPermission.EXECUTE_GRAPH],
+ type: "api_key" as const,
+ created_at: new Date("2026-01-01T00:00:00.000Z"),
+ expires_at: null,
+ last_used_at: null,
+ revoked_at: null,
+ name: key.name,
+ head: key.head,
+ tail: key.tail,
+ status: key.status,
+ description: null,
+ };
+}
+
+describe("ApiKeysPage", () => {
+ let apiKeys: ApiKeyRecord[];
+ let revokedKeyId: string;
+
+ beforeEach(() => {
+ apiKeys = [];
+ revokedKeyId = "";
+
+ server.use(
+ getGetV1ListUserApiKeysMockHandler(() =>
+ apiKeys.map((key) => toApiKeyResponse(key)),
+ ),
+ getPostV1CreateNewApiKeyMockHandler(async ({ request }) => {
+ const body = (await request.json()) as {
+ name: string;
+ description?: string;
+ permissions?: APIKeyPermission[];
+ };
+
+ const createdKey: ApiKeyRecord = {
+ id: `key-${apiKeys.length + 1}`,
+ name: body.name,
+ head: "head",
+ tail: "tail",
+ status: APIKeyStatus.ACTIVE,
+ };
+
+ apiKeys = [...apiKeys, createdKey];
+
+ return {
+ api_key: toApiKeyResponse(createdKey),
+ plain_text_key: "plain-text-key",
+ };
+ }),
+ getDeleteV1RevokeApiKeyMockHandler(({ params }) => {
+ const keyId = String(params.keyId);
+ const removedKey = apiKeys.find((key) => key.id === keyId);
+
+ revokedKeyId = keyId;
+ apiKeys = apiKeys.filter((key) => key.id !== keyId);
+
+ return toApiKeyResponse(
+ removedKey ?? {
+ id: keyId,
+ name: "Unknown key",
+ head: "head",
+ tail: "tail",
+ status: APIKeyStatus.REVOKED,
+ },
+ );
+ }),
+ );
+ });
+
+ test("creates a new API key", async () => {
+ render();
+
+ fireEvent.click(await screen.findByText("Create Key"));
+ fireEvent.change(screen.getByLabelText("Name"), {
+ target: { value: "CLI Key" },
+ });
+ fireEvent.click(screen.getByText("Create"));
+
+ expect(
+ await screen.findByText("AutoGPT Platform API Key Created"),
+ ).toBeDefined();
+
+ await waitFor(() => {
+ expect(apiKeys[0]?.name).toBe("CLI Key");
+ });
+ });
+
+ test("revokes an existing API key", async () => {
+ apiKeys = [
+ {
+ id: "key-1",
+ name: "Existing Key",
+ head: "head",
+ tail: "tail",
+ status: APIKeyStatus.ACTIVE,
+ },
+ ];
+
+ render();
+
+ expect(await screen.findByText("Existing Key")).toBeDefined();
+
+ fireEvent.pointerDown(screen.getByTestId("api-key-actions"));
+ fireEvent.click(await screen.findByRole("menuitem", { name: "Revoke" }));
+
+ await waitFor(() => {
+ expect(revokedKeyId).toBe("key-1");
+ });
+ });
+});
diff --git a/autogpt_platform/frontend/src/app/(platform)/profile/(user)/dashboard/components/AgentTableRow/__tests__/AgentTableRow.test.tsx b/autogpt_platform/frontend/src/app/(platform)/profile/(user)/dashboard/components/AgentTableRow/__tests__/AgentTableRow.test.tsx
new file mode 100644
index 0000000000..04e1d4ad1e
--- /dev/null
+++ b/autogpt_platform/frontend/src/app/(platform)/profile/(user)/dashboard/components/AgentTableRow/__tests__/AgentTableRow.test.tsx
@@ -0,0 +1,76 @@
+import { render, screen, fireEvent } from "@testing-library/react";
+import { getGetV2ListMySubmissionsResponseMock } from "@/app/api/__generated__/endpoints/store/store.msw";
+import { SubmissionStatus } from "@/app/api/__generated__/models/submissionStatus";
+import { AgentTableRow } from "../AgentTableRow";
+import { beforeEach, describe, expect, test, vi } from "vitest";
+
+function makeSubmission(status: SubmissionStatus) {
+ const submission = getGetV2ListMySubmissionsResponseMock().submissions[0];
+
+ return {
+ ...submission,
+ graph_id: "graph-1",
+ graph_version: 7,
+ listing_version_id: `listing-${status.toLowerCase()}`,
+ name: `Agent ${status}`,
+ description: `Description ${status}`,
+ status,
+ image_urls: [],
+ submitted_at: new Date("2026-01-01T00:00:00.000Z"),
+ };
+}
+
+describe("AgentTableRow", () => {
+ const onViewSubmission = vi.fn();
+ const onDeleteSubmission = vi.fn();
+ const onEditSubmission = vi.fn();
+
+ beforeEach(() => {
+ onViewSubmission.mockReset();
+ onDeleteSubmission.mockReset();
+ onEditSubmission.mockReset();
+ });
+
+ test("shows edit and delete actions for pending submissions", async () => {
+ render(
+ ,
+ );
+
+ fireEvent.pointerDown(screen.getByTestId("agent-table-row-actions"));
+
+ fireEvent.click(await screen.findByText("Edit"));
+ expect(onEditSubmission).toHaveBeenCalledTimes(1);
+
+ fireEvent.pointerDown(screen.getByTestId("agent-table-row-actions"));
+ fireEvent.click(await screen.findByText("Delete"));
+ expect(onDeleteSubmission).toHaveBeenCalledWith("listing-pending");
+ expect(onViewSubmission).not.toHaveBeenCalled();
+ });
+
+ test("shows view only for non-pending submissions", async () => {
+ const approvedSubmission = makeSubmission(SubmissionStatus.APPROVED);
+
+ render(
+ ,
+ );
+
+ fireEvent.pointerDown(screen.getByTestId("agent-table-row-actions"));
+
+ const viewAction = await screen.findByText("View");
+ fireEvent.click(viewAction);
+
+ expect(onViewSubmission).toHaveBeenCalledWith(approvedSubmission);
+ expect(screen.queryByText("Edit")).toBeNull();
+ expect(screen.queryByText("Delete")).toBeNull();
+ });
+});
diff --git a/autogpt_platform/frontend/src/app/(platform)/profile/(user)/settings/__tests__/page.test.tsx b/autogpt_platform/frontend/src/app/(platform)/profile/(user)/settings/__tests__/page.test.tsx
new file mode 100644
index 0000000000..75c706dbcb
--- /dev/null
+++ b/autogpt_platform/frontend/src/app/(platform)/profile/(user)/settings/__tests__/page.test.tsx
@@ -0,0 +1,147 @@
+import type { ReactNode } from "react";
+import {
+ render,
+ screen,
+ fireEvent,
+ waitFor,
+} from "@/tests/integrations/test-utils";
+import {
+ getGetV1GetNotificationPreferencesMockHandler,
+ getGetV1GetUserTimezoneMockHandler,
+ getPostV1UpdateNotificationPreferencesMockHandler,
+ getPostV1UpdateUserEmailMockHandler,
+} from "@/app/api/__generated__/endpoints/auth/auth.msw";
+import { server } from "@/mocks/mock-server";
+import SettingsPage from "../page";
+import { beforeEach, describe, expect, test, vi } from "vitest";
+
+const mockUseSupabase = vi.hoisted(() => vi.fn());
+
+vi.mock("@/providers/onboarding/onboarding-provider", () => ({
+ default: ({ children }: { children: ReactNode }) => <>{children}>,
+}));
+
+vi.mock("@/lib/supabase/hooks/useSupabase", () => ({
+ useSupabase: mockUseSupabase,
+}));
+
+const testUser = {
+ id: "user-1",
+ email: "user@example.com",
+ app_metadata: {},
+ user_metadata: {},
+ aud: "authenticated",
+ created_at: "2026-01-01T00:00:00.000Z",
+};
+
+describe("SettingsPage", () => {
+ beforeEach(() => {
+ mockUseSupabase.mockReturnValue({
+ user: testUser,
+ isLoggedIn: true,
+ isUserLoading: false,
+ supabase: {},
+ });
+ });
+
+ test("renders the account actions", async () => {
+ server.use(
+ getGetV1GetNotificationPreferencesMockHandler({
+ user_id: "user-1",
+ email: "user@example.com",
+ preferences: {
+ AGENT_RUN: true,
+ ZERO_BALANCE: false,
+ LOW_BALANCE: false,
+ BLOCK_EXECUTION_FAILED: true,
+ CONTINUOUS_AGENT_ERROR: false,
+ DAILY_SUMMARY: false,
+ WEEKLY_SUMMARY: true,
+ MONTHLY_SUMMARY: false,
+ AGENT_APPROVED: true,
+ AGENT_REJECTED: true,
+ },
+ daily_limit: 0,
+ emails_sent_today: 0,
+ last_reset_date: new Date("2026-01-01T00:00:00.000Z"),
+ }),
+ getGetV1GetUserTimezoneMockHandler({ timezone: "Asia/Kolkata" }),
+ getPostV1UpdateUserEmailMockHandler({}),
+ getPostV1UpdateNotificationPreferencesMockHandler({
+ user_id: "user-1",
+ email: "user@example.com",
+ preferences: {},
+ daily_limit: 0,
+ emails_sent_today: 0,
+ last_reset_date: new Date("2026-01-01T00:00:00.000Z"),
+ }),
+ );
+
+ render();
+
+ const emailInput = await screen.findByLabelText("Email");
+ expect((emailInput as HTMLInputElement).value).toBe("user@example.com");
+ expect(
+ screen.getByRole("link", { name: "Reset password" }).getAttribute("href"),
+ ).toBe("/reset-password");
+ });
+
+ test("saves notification preference changes", async () => {
+ let submittedPreferences:
+ | {
+ email: string;
+ preferences: Record;
+ }
+ | undefined;
+
+ server.use(
+ getGetV1GetNotificationPreferencesMockHandler({
+ user_id: "user-1",
+ email: "user@example.com",
+ preferences: {
+ AGENT_RUN: false,
+ ZERO_BALANCE: false,
+ LOW_BALANCE: false,
+ BLOCK_EXECUTION_FAILED: false,
+ CONTINUOUS_AGENT_ERROR: false,
+ DAILY_SUMMARY: false,
+ WEEKLY_SUMMARY: false,
+ MONTHLY_SUMMARY: false,
+ AGENT_APPROVED: false,
+ AGENT_REJECTED: false,
+ },
+ daily_limit: 0,
+ emails_sent_today: 0,
+ last_reset_date: new Date("2026-01-01T00:00:00.000Z"),
+ }),
+ getGetV1GetUserTimezoneMockHandler({ timezone: "Asia/Kolkata" }),
+ getPostV1UpdateUserEmailMockHandler({}),
+ getPostV1UpdateNotificationPreferencesMockHandler(async ({ request }) => {
+ submittedPreferences = (await request.json()) as {
+ email: string;
+ preferences: Record;
+ };
+
+ return {
+ user_id: "user-1",
+ email: submittedPreferences.email,
+ preferences: submittedPreferences.preferences,
+ daily_limit: 0,
+ emails_sent_today: 0,
+ last_reset_date: new Date("2026-01-01T00:00:00.000Z"),
+ };
+ }),
+ );
+
+ render();
+
+ fireEvent.click(
+ await screen.findByRole("switch", { name: "Agent Run Notifications" }),
+ );
+ fireEvent.click(screen.getByRole("button", { name: "Save preferences" }));
+
+ await waitFor(() => {
+ expect(submittedPreferences?.preferences.AGENT_RUN).toBe(true);
+ });
+ });
+});
diff --git a/autogpt_platform/frontend/src/app/(platform)/profile/(user)/settings/components/SettingsForm/components/EmailForm/__tests__/EmailForm.test.tsx b/autogpt_platform/frontend/src/app/(platform)/profile/(user)/settings/components/SettingsForm/components/EmailForm/__tests__/EmailForm.test.tsx
new file mode 100644
index 0000000000..fb7e4d397a
--- /dev/null
+++ b/autogpt_platform/frontend/src/app/(platform)/profile/(user)/settings/components/SettingsForm/components/EmailForm/__tests__/EmailForm.test.tsx
@@ -0,0 +1,97 @@
+import {
+ fireEvent,
+ render,
+ screen,
+ waitFor,
+} from "@/tests/integrations/test-utils";
+import type { ReactNode } from "react";
+import type { User } from "@supabase/supabase-js";
+import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
+import { EmailForm } from "../EmailForm";
+
+const mockToast = vi.hoisted(() => vi.fn());
+const mockMutateAsync = vi.hoisted(() => vi.fn());
+
+vi.mock("@/components/molecules/Toast/use-toast", () => ({
+ useToast: () => ({ toast: mockToast }),
+}));
+
+vi.mock("@/app/api/__generated__/endpoints/auth/auth", () => ({
+ usePostV1UpdateUserEmail: () => ({
+ mutateAsync: mockMutateAsync,
+ isPending: false,
+ }),
+}));
+
+vi.mock("@/providers/onboarding/onboarding-provider", () => ({
+ default: ({ children }: { children: ReactNode }) => <>{children}>,
+}));
+
+const testUser = {
+ id: "user-1",
+ email: "user@example.com",
+ app_metadata: {},
+ user_metadata: {},
+ aud: "authenticated",
+ created_at: "2026-01-01T00:00:00.000Z",
+} as User;
+
+describe("EmailForm", () => {
+ beforeEach(() => {
+ mockToast.mockReset();
+ mockMutateAsync.mockReset();
+ mockMutateAsync.mockResolvedValue({});
+ });
+
+ afterEach(() => {
+ vi.unstubAllGlobals();
+ });
+
+ test("submits a changed email to both update endpoints", async () => {
+ const fetchMock = vi.fn().mockResolvedValue({
+ ok: true,
+ json: async () => ({}),
+ });
+
+ vi.stubGlobal("fetch", fetchMock);
+
+ render();
+
+ fireEvent.change(screen.getByLabelText("Email"), {
+ target: { value: "updated@example.com" },
+ });
+ fireEvent.click(screen.getByRole("button", { name: "Update email" }));
+
+ await waitFor(() => {
+ expect(fetchMock).toHaveBeenCalledWith("/api/auth/user", {
+ method: "PUT",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({ email: "updated@example.com" }),
+ });
+ });
+ await waitFor(() => {
+ expect(mockMutateAsync).toHaveBeenCalledWith({
+ data: "updated@example.com",
+ });
+ });
+ expect(mockToast).toHaveBeenCalledWith(
+ expect.objectContaining({
+ title: "Successfully updated email",
+ }),
+ );
+ });
+
+ test("keeps submit disabled when the email has not changed", () => {
+ render();
+
+ expect(
+ (
+ screen.getByRole("button", {
+ name: "Update email",
+ }) as HTMLButtonElement
+ ).disabled,
+ ).toBe(true);
+ });
+});
diff --git a/autogpt_platform/frontend/src/app/(platform)/profile/(user)/settings/components/SettingsForm/components/NotificationForm/NotificationForm.tsx b/autogpt_platform/frontend/src/app/(platform)/profile/(user)/settings/components/SettingsForm/components/NotificationForm/NotificationForm.tsx
index 38473234ab..8b85488cf5 100644
--- a/autogpt_platform/frontend/src/app/(platform)/profile/(user)/settings/components/SettingsForm/components/NotificationForm/NotificationForm.tsx
+++ b/autogpt_platform/frontend/src/app/(platform)/profile/(user)/settings/components/SettingsForm/components/NotificationForm/NotificationForm.tsx
@@ -55,6 +55,7 @@ export function NotificationForm({ preferences, user }: NotificationFormProps) {
diff --git a/autogpt_platform/frontend/src/app/(platform)/signup/__tests__/page.test.tsx b/autogpt_platform/frontend/src/app/(platform)/signup/__tests__/page.test.tsx
new file mode 100644
index 0000000000..4ac1e3dc50
--- /dev/null
+++ b/autogpt_platform/frontend/src/app/(platform)/signup/__tests__/page.test.tsx
@@ -0,0 +1,73 @@
+import type { ReactNode } from "react";
+import {
+ render,
+ screen,
+ fireEvent,
+ waitFor,
+} from "@/tests/integrations/test-utils";
+import SignupPage from "../page";
+import { beforeEach, describe, expect, test, vi } from "vitest";
+
+const mockUseSupabase = vi.hoisted(() => vi.fn());
+const mockSignupAction = vi.hoisted(() => vi.fn());
+
+vi.mock("@/providers/onboarding/onboarding-provider", () => ({
+ default: ({ children }: { children: ReactNode }) => <>{children}>,
+}));
+
+vi.mock("@/lib/supabase/hooks/useSupabase", () => ({
+ useSupabase: mockUseSupabase,
+}));
+
+vi.mock("../actions", () => ({
+ signup: mockSignupAction,
+}));
+
+describe("SignupPage", () => {
+ beforeEach(() => {
+ mockUseSupabase.mockReturnValue({
+ supabase: {},
+ user: null,
+ isUserLoading: false,
+ isLoggedIn: false,
+ });
+ mockSignupAction.mockReset();
+ });
+
+ test("shows existing user feedback from signup action", async () => {
+ mockSignupAction.mockResolvedValue({
+ success: false,
+ error: "user_already_exists",
+ });
+
+ render();
+
+ fireEvent.change(screen.getByLabelText("Email"), {
+ target: { value: "existing@example.com" },
+ });
+ fireEvent.change(screen.getByLabelText("Password", { selector: "input" }), {
+ target: { value: "validpassword123" },
+ });
+ fireEvent.change(
+ screen.getByLabelText("Confirm Password", { selector: "input" }),
+ {
+ target: { value: "validpassword123" },
+ },
+ );
+ fireEvent.click(screen.getByRole("checkbox"));
+ fireEvent.click(screen.getByRole("button", { name: "Sign up" }));
+
+ await waitFor(() => {
+ expect(mockSignupAction).toHaveBeenCalledWith(
+ "existing@example.com",
+ "validpassword123",
+ "validpassword123",
+ true,
+ );
+ });
+
+ expect(
+ await screen.findByText("User with this email already exists"),
+ ).toBeDefined();
+ });
+});
diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/blockOutputResponse.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/blockOutputResponse.ts
deleted file mode 100644
index a25b1a04d3..0000000000
--- a/autogpt_platform/frontend/src/app/api/__generated__/models/blockOutputResponse.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-/**
- * Generated by orval v7.13.0 🍺
- * Do not edit manually.
- * AutoGPT Agent Server
- * This server is used to execute agents that are created by the AutoGPT system.
- * OpenAPI spec version: 0.1
- */
-import type { ResponseType } from "./responseType";
-import type { BlockOutputResponseSessionId } from "./blockOutputResponseSessionId";
-import type { BlockOutputResponseOutputs } from "./blockOutputResponseOutputs";
-import type { BlockOutputResponseIsDryRun } from "./blockOutputResponseIsDryRun";
-
-/**
- * Response for run_block tool.
- */
-export interface BlockOutputResponse {
- type?: ResponseType;
- message: string;
- session_id?: BlockOutputResponseSessionId;
- block_id: string;
- block_name: string;
- outputs: BlockOutputResponseOutputs;
- success?: boolean;
- is_dry_run?: BlockOutputResponseIsDryRun;
-}
diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/graphExecutionMeta.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/graphExecutionMeta.ts
deleted file mode 100644
index c8bf7115ce..0000000000
--- a/autogpt_platform/frontend/src/app/api/__generated__/models/graphExecutionMeta.ts
+++ /dev/null
@@ -1,36 +0,0 @@
-/**
- * Generated by orval v7.13.0 🍺
- * Do not edit manually.
- * AutoGPT Agent Server
- * This server is used to execute agents that are created by the AutoGPT system.
- * OpenAPI spec version: 0.1
- */
-import type { GraphExecutionMetaInputs } from "./graphExecutionMetaInputs";
-import type { GraphExecutionMetaCredentialInputs } from "./graphExecutionMetaCredentialInputs";
-import type { GraphExecutionMetaNodesInputMasks } from "./graphExecutionMetaNodesInputMasks";
-import type { GraphExecutionMetaPresetId } from "./graphExecutionMetaPresetId";
-import type { AgentExecutionStatus } from "./agentExecutionStatus";
-import type { GraphExecutionMetaStartedAt } from "./graphExecutionMetaStartedAt";
-import type { GraphExecutionMetaEndedAt } from "./graphExecutionMetaEndedAt";
-import type { GraphExecutionMetaShareToken } from "./graphExecutionMetaShareToken";
-import type { GraphExecutionMetaStats } from "./graphExecutionMetaStats";
-
-export interface GraphExecutionMeta {
- id: string;
- user_id: string;
- graph_id: string;
- graph_version: number;
- inputs: GraphExecutionMetaInputs;
- credential_inputs: GraphExecutionMetaCredentialInputs;
- nodes_input_masks: GraphExecutionMetaNodesInputMasks;
- preset_id: GraphExecutionMetaPresetId;
- status: AgentExecutionStatus;
- /** When execution started running. Null if not yet started (QUEUED). */
- started_at?: GraphExecutionMetaStartedAt;
- /** When execution finished. Null if not yet completed (QUEUED, RUNNING, INCOMPLETE, REVIEW). */
- ended_at?: GraphExecutionMetaEndedAt;
- is_shared?: boolean;
- share_token?: GraphExecutionMetaShareToken;
- is_dry_run?: boolean;
- stats: GraphExecutionMetaStats;
-}
diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/suggestedPromptsResponse.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/suggestedPromptsResponse.ts
deleted file mode 100644
index 9f8b44c585..0000000000
--- a/autogpt_platform/frontend/src/app/api/__generated__/models/suggestedPromptsResponse.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-/**
- * Generated by orval v7.13.0 🍺
- * Do not edit manually.
- * AutoGPT Agent Server
- * This server is used to execute agents that are created by the AutoGPT system.
- * OpenAPI spec version: 0.1
- */
-import type { SuggestedTheme } from "./suggestedTheme";
-
-/**
- * Response model for user-specific suggested prompts grouped by theme.
- */
-export interface SuggestedPromptsResponse {
- themes: SuggestedTheme[];
-}
diff --git a/autogpt_platform/frontend/src/app/api/__generated__/models/suggestedTheme.ts b/autogpt_platform/frontend/src/app/api/__generated__/models/suggestedTheme.ts
deleted file mode 100644
index 5fec92e394..0000000000
--- a/autogpt_platform/frontend/src/app/api/__generated__/models/suggestedTheme.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-/**
- * Generated by orval v7.13.0 🍺
- * Do not edit manually.
- * AutoGPT Agent Server
- * This server is used to execute agents that are created by the AutoGPT system.
- * OpenAPI spec version: 0.1
- */
-
-/**
- * A themed group of suggested prompts.
- */
-export interface SuggestedTheme {
- name: string;
- prompts: string[];
-}
diff --git a/autogpt_platform/frontend/src/app/api/openapi.json b/autogpt_platform/frontend/src/app/api/openapi.json
index f2c85f789f..e3883feb69 100644
--- a/autogpt_platform/frontend/src/app/api/openapi.json
+++ b/autogpt_platform/frontend/src/app/api/openapi.json
@@ -9123,6 +9123,15 @@
],
"title": "ContentType"
},
+ "CostBucket": {
+ "properties": {
+ "bucket": { "type": "string", "title": "Bucket" },
+ "count": { "type": "integer", "title": "Count" }
+ },
+ "type": "object",
+ "required": ["bucket", "count"],
+ "title": "CostBucket"
+ },
"CostLogRow": {
"properties": {
"id": { "type": "string", "title": "Id" },
@@ -12141,7 +12150,58 @@
"title": "Total Cost Microdollars"
},
"total_requests": { "type": "integer", "title": "Total Requests" },
- "total_users": { "type": "integer", "title": "Total Users" }
+ "total_users": { "type": "integer", "title": "Total Users" },
+ "total_input_tokens": {
+ "type": "integer",
+ "title": "Total Input Tokens",
+ "default": 0
+ },
+ "total_output_tokens": {
+ "type": "integer",
+ "title": "Total Output Tokens",
+ "default": 0
+ },
+ "avg_input_tokens_per_request": {
+ "type": "number",
+ "title": "Avg Input Tokens Per Request",
+ "default": 0.0
+ },
+ "avg_output_tokens_per_request": {
+ "type": "number",
+ "title": "Avg Output Tokens Per Request",
+ "default": 0.0
+ },
+ "avg_cost_microdollars_per_request": {
+ "type": "number",
+ "title": "Avg Cost Microdollars Per Request",
+ "default": 0.0
+ },
+ "cost_p50_microdollars": {
+ "type": "number",
+ "title": "Cost P50 Microdollars",
+ "default": 0.0
+ },
+ "cost_p75_microdollars": {
+ "type": "number",
+ "title": "Cost P75 Microdollars",
+ "default": 0.0
+ },
+ "cost_p95_microdollars": {
+ "type": "number",
+ "title": "Cost P95 Microdollars",
+ "default": 0.0
+ },
+ "cost_p99_microdollars": {
+ "type": "number",
+ "title": "Cost P99 Microdollars",
+ "default": 0.0
+ },
+ "cost_buckets": {
+ "items": { "$ref": "#/components/schemas/CostBucket" },
+ "type": "array",
+ "title": "Cost Buckets",
+ "default": []
+ }
},
"type": "object",
"required": [
@@ -15589,7 +15649,12 @@
"title": "Total Cache Creation Tokens",
"default": 0
},
- "request_count": { "type": "integer", "title": "Request Count" }
+ "request_count": { "type": "integer", "title": "Request Count" },
+ "cost_bearing_request_count": {
+ "type": "integer",
+ "title": "Cost Bearing Request Count",
+ "default": 0
+ }
},
"type": "object",
"required": [
diff --git a/autogpt_platform/frontend/src/components/__legacy__/__tests__/ProfileInfoForm.test.tsx b/autogpt_platform/frontend/src/components/__legacy__/__tests__/ProfileInfoForm.test.tsx
new file mode 100644
index 0000000000..3ee732912c
--- /dev/null
+++ b/autogpt_platform/frontend/src/components/__legacy__/__tests__/ProfileInfoForm.test.tsx
@@ -0,0 +1,94 @@
+import { describe, expect, it } from "vitest";
+import {
+ fireEvent,
+ render,
+ screen,
+ waitFor,
+} from "@/tests/integrations/test-utils";
+import {
+ getPostV2UpdateUserProfileMockHandler200,
+ getPostV2UpdateUserProfileMockHandler422,
+ getPostV2UpdateUserProfileResponseMock422,
+} from "@/app/api/__generated__/endpoints/store/store.msw";
+import { server } from "@/mocks/mock-server";
+import type { ProfileDetails } from "@/app/api/__generated__/models/profileDetails";
+import { ProfileInfoForm } from "../ProfileInfoForm";
+
+function makeProfile(overrides: Partial = {}): ProfileDetails {
+ return {
+ name: "Initial Name",
+ username: "initial-user",
+ description: "Initial description",
+ links: [],
+ avatar_url: "",
+ ...overrides,
+ } as ProfileDetails;
+}
+
+describe("ProfileInfoForm", () => {
+ it("renders the existing profile values into editable fields", () => {
+ render();
+ const nameInput = screen.getByTestId(
+ "profile-info-form-display-name",
+ ) as HTMLInputElement;
+ expect(nameInput.defaultValue).toBe("Hello World");
+ });
+
+ it("submits the new display name to POST /api/store/profile and reflects the response", async () => {
+ let receivedBody: Record | null = null;
+
+ server.use(
+ getPostV2UpdateUserProfileMockHandler200(async ({ request }) => {
+ receivedBody = (await request.json()) as Record;
+ return makeProfile({ name: receivedBody?.name as string });
+ }),
+ );
+
+ render();
+
+ const nameInput = screen.getByTestId("profile-info-form-display-name");
+ fireEvent.change(nameInput, { target: { value: "Brand New Name" } });
+
+ fireEvent.click(screen.getByRole("button", { name: "Save changes" }));
+
+ await waitFor(() => {
+ expect(
+ receivedBody,
+ "POST /api/store/profile must fire when the user clicks Save",
+ ).not.toBeNull();
+ });
+
+ expect(receivedBody!.name).toBe("Brand New Name");
+ });
+
+ it("does not silently swallow the request when the API returns 422", async () => {
+ let calls = 0;
+ server.use(
+ getPostV2UpdateUserProfileMockHandler422(() => {
+ calls += 1;
+ return getPostV2UpdateUserProfileResponseMock422({
+ detail: [
+ {
+ loc: ["body", "name"],
+ msg: "validation error",
+ type: "value_error",
+ },
+ ],
+ });
+ }),
+ );
+
+ render();
+
+ const nameInput = screen.getByTestId("profile-info-form-display-name");
+ fireEvent.change(nameInput, { target: { value: "Anything" } });
+ fireEvent.click(screen.getByRole("button", { name: "Save changes" }));
+
+ await waitFor(() => {
+ expect(
+ calls,
+ "save click must hit the backend even when validation fails",
+ ).toBeGreaterThan(0);
+ });
+ });
+});
diff --git a/autogpt_platform/frontend/src/components/layout/Navbar/components/AgentActivityDropdown/__tests__/AgentActivityDropdown.test.tsx b/autogpt_platform/frontend/src/components/layout/Navbar/components/AgentActivityDropdown/__tests__/AgentActivityDropdown.test.tsx
new file mode 100644
index 0000000000..5c45af03f4
--- /dev/null
+++ b/autogpt_platform/frontend/src/components/layout/Navbar/components/AgentActivityDropdown/__tests__/AgentActivityDropdown.test.tsx
@@ -0,0 +1,76 @@
+import { render, screen } from "@/tests/integrations/test-utils";
+import { AgentExecutionStatus } from "@/app/api/__generated__/models/agentExecutionStatus";
+import { AgentActivityDropdown } from "../AgentActivityDropdown";
+import { AgentExecutionWithInfo } from "../helpers";
+import { beforeEach, describe, expect, test, vi } from "vitest";
+
+const mockUseAgentActivityDropdown = vi.hoisted(() => vi.fn());
+
+vi.mock("../useAgentActivityDropdown", () => ({
+ useAgentActivityDropdown: mockUseAgentActivityDropdown,
+}));
+
+function makeExecution(
+ overrides: Partial = {},
+): AgentExecutionWithInfo {
+ return {
+ id: "exec-1",
+ graph_id: "graph-1",
+ status: AgentExecutionStatus.RUNNING,
+ started_at: new Date(),
+ ended_at: null,
+ user_id: "user-1",
+ graph_version: 1,
+ inputs: {},
+ credential_inputs: {},
+ nodes_input_masks: {},
+ preset_id: null,
+ stats: null,
+ agent_name: "Test Agent",
+ agent_description: "A running agent",
+ library_agent_id: "library-1",
+ ...overrides,
+ };
+}
+
+describe("AgentActivityDropdown", () => {
+ beforeEach(() => {
+ mockUseAgentActivityDropdown.mockReturnValue({
+ activeExecutions: [makeExecution(), makeExecution({ id: "exec-2" })],
+ recentCompletions: [],
+ recentFailures: [],
+ totalCount: 2,
+ isReady: true,
+ error: null,
+ isOpen: false,
+ setIsOpen: vi.fn(),
+ });
+ });
+
+ test("shows the active execution badge count", () => {
+ render();
+
+ expect(screen.getByTestId("agent-activity-badge").textContent).toContain(
+ "2",
+ );
+ expect(screen.getByTestId("agent-activity-button")).toBeDefined();
+ });
+
+ test("renders the dropdown content when open", async () => {
+ mockUseAgentActivityDropdown.mockReturnValue({
+ activeExecutions: [makeExecution()],
+ recentCompletions: [],
+ recentFailures: [],
+ totalCount: 1,
+ isReady: true,
+ error: null,
+ isOpen: true,
+ setIsOpen: vi.fn(),
+ });
+
+ render();
+
+ expect(screen.getByTestId("agent-activity-dropdown")).toBeDefined();
+ expect(await screen.findByText("Test Agent")).toBeDefined();
+ });
+});
diff --git a/autogpt_platform/frontend/src/lib/utils.test.ts b/autogpt_platform/frontend/src/lib/utils.test.ts
new file mode 100644
index 0000000000..62742ac574
--- /dev/null
+++ b/autogpt_platform/frontend/src/lib/utils.test.ts
@@ -0,0 +1,97 @@
+import { describe, expect, test } from "vitest";
+import { setNestedProperty } from "./utils";
+
+const testCases = [
+ {
+ name: "simple property assignment",
+ path: "name",
+ value: "John",
+ expected: { name: "John" },
+ },
+ {
+ name: "nested property with dot notation",
+ path: "user.settings.theme",
+ value: "dark",
+ expected: { user: { settings: { theme: "dark" } } },
+ },
+ {
+ name: "nested property with slash notation",
+ path: "user/settings/language",
+ value: "en",
+ expected: { user: { settings: { language: "en" } } },
+ },
+ {
+ name: "mixed dot and slash notation",
+ path: "user.settings/preferences.color",
+ value: "blue",
+ expected: { user: { settings: { preferences: { color: "blue" } } } },
+ },
+ {
+ name: "overwrite primitive with object",
+ path: "user.details",
+ value: { age: 30 },
+ expected: { user: { details: { age: 30 } } },
+ },
+];
+
+describe("setNestedProperty", () => {
+ for (const { name, path, value, expected } of testCases) {
+ test(name, () => {
+ const obj = {};
+ setNestedProperty(obj, path, value);
+ expect(obj).toEqual(expected);
+ });
+ }
+
+ test("throws for null object", () => {
+ expect(() => {
+ setNestedProperty(null, "test", "value");
+ }).toThrow("Target must be a non-null object");
+ });
+
+ test("throws for undefined object", () => {
+ expect(() => {
+ setNestedProperty(undefined, "test", "value");
+ }).toThrow("Target must be a non-null object");
+ });
+
+ test("throws for non-object target", () => {
+ expect(() => {
+ setNestedProperty("string", "test", "value");
+ }).toThrow("Target must be a non-null object");
+ });
+
+ test("throws for empty path", () => {
+ expect(() => {
+ setNestedProperty({}, "", "value");
+ }).toThrow("Path must be a non-empty string");
+ });
+
+ test("throws for __proto__ access", () => {
+ expect(() => {
+ setNestedProperty({}, "__proto__.malicious", "attack");
+ }).toThrow("Invalid property name: __proto__");
+ });
+
+ test("throws for constructor access", () => {
+ expect(() => {
+ setNestedProperty({}, "constructor.prototype.malicious", "attack");
+ }).toThrow("Invalid property name: constructor");
+ });
+
+ test("throws for prototype access", () => {
+ expect(() => {
+ setNestedProperty({}, "obj.prototype.malicious", "attack");
+ }).toThrow("Invalid property name: prototype");
+ });
+
+ test("prevents prototype pollution", () => {
+ const obj = {};
+
+ expect(() => {
+ setNestedProperty(obj, "__proto__.polluted", true);
+ }).toThrow("Invalid property name: __proto__");
+
+ expect(({} as { polluted?: boolean }).polluted).toBeUndefined();
+ });
+});
diff --git a/autogpt_platform/frontend/src/playwright/api-keys-happy-path.spec.ts b/autogpt_platform/frontend/src/playwright/api-keys-happy-path.spec.ts
new file mode 100644
index 0000000000..9d0cbf8afc
--- /dev/null
+++ b/autogpt_platform/frontend/src/playwright/api-keys-happy-path.spec.ts
@@ -0,0 +1,100 @@
+import { randomUUID } from "crypto";
+import { expect, test } from "./coverage-fixture";
+import { E2E_AUTH_STATES } from "./credentials/accounts";
+
+test.use({ storageState: E2E_AUTH_STATES.parallelB });
+
+test("api keys happy path: user can create, copy, and revoke an API key", async ({
+ page,
+ context,
+}) => {
+ test.setTimeout(120000);
+
+ await context.grantPermissions(["clipboard-read", "clipboard-write"]);
+
+ const keyName = `E2E CLI Key ${randomUUID().slice(0, 8)}`;
+
+ await page.goto("/profile/api-keys");
+ await expect(page).toHaveURL(/\/profile\/api-keys/);
+ await expect(
+ page.getByText(
+ "Manage your AutoGPT Platform API keys for programmatic access",
+ ),
+ ).toBeVisible();
+
+ await page.getByRole("button", { name: "Create Key" }).click();
+ await page.getByLabel("Name").fill(keyName);
+ const executeGraphCheckbox = page.getByRole("checkbox", {
+ name: /EXECUTE_GRAPH/i,
+ });
+ const executeGraphChecked =
+ (await executeGraphCheckbox.getAttribute("aria-checked")) === "true";
+ if (!executeGraphChecked) {
+ await executeGraphCheckbox.click();
+ }
+ await expect(executeGraphCheckbox).toHaveAttribute("aria-checked", "true");
+
+ await page.getByRole("button", { name: "Create" }).click();
+
+ const secretDialog = page.getByRole("dialog", {
+ name: "AutoGPT Platform API Key Created",
+ });
+ await expect
+ .poll(
+ async () => {
+ if (await secretDialog.isVisible().catch(() => false)) {
+ return "created";
+ }
+
+ const creationFailed = await page
+ .getByText("Failed to create AutoGPT Platform API key")
+ .isVisible()
+ .catch(() => false);
+ if (creationFailed) {
+ return "failed";
+ }
+
+ return "pending";
+ },
+ {
+ timeout: 30000,
+ message:
+ "API key creation should either open the created-key dialog or surface an explicit failure toast",
+ },
+ )
+ .toBe("created");
+ await expect(secretDialog).toBeVisible();
+
+ const createdSecret = (
+ (await secretDialog.locator("code").textContent()) ?? ""
+ ).trim();
+ expect(createdSecret.length).toBeGreaterThan(0);
+
+ await secretDialog.getByRole("button").first().click();
+ await expect(page.getByText("Copied", { exact: true })).toBeVisible({
+ timeout: 15000,
+ });
+ await expect
+ .poll(() => page.evaluate(() => navigator.clipboard.readText()), {
+ timeout: 10000,
+ })
+ .toBe(createdSecret);
+
+ await secretDialog.getByRole("button", { name: "Close" }).first().click();
+
+ const createdKeyRow = page
+ .getByTestId("api-key-row")
+ .filter({ hasText: keyName })
+ .first();
+ await expect(createdKeyRow).toBeVisible({ timeout: 15000 });
+
+ await createdKeyRow.getByTestId("api-key-actions").click();
+ await page.getByRole("menuitem", { name: "Revoke" }).click();
+
+ await expect(
+ page.getByText("AutoGPT Platform API key revoked successfully"),
+ ).toBeVisible({ timeout: 15000 });
+ await expect(
+ page.getByTestId("api-key-row").filter({ hasText: keyName }),
+ ).toHaveCount(0);
+});
diff --git a/autogpt_platform/frontend/src/tests/assets/testing_agent.json b/autogpt_platform/frontend/src/playwright/assets/testing_agent.json
similarity index 100%
rename from autogpt_platform/frontend/src/tests/assets/testing_agent.json
rename to autogpt_platform/frontend/src/playwright/assets/testing_agent.json
diff --git a/autogpt_platform/frontend/src/playwright/auth-happy-path.spec.ts b/autogpt_platform/frontend/src/playwright/auth-happy-path.spec.ts
new file mode 100644
index 0000000000..a7872cb706
--- /dev/null
+++ b/autogpt_platform/frontend/src/playwright/auth-happy-path.spec.ts
@@ -0,0 +1,158 @@
+import { expect, test } from "./coverage-fixture";
+import { getSeededTestUser } from "./credentials/accounts";
+import { BuildPage } from "./pages/build.page";
+import { LoginPage } from "./pages/login.page";
+import {
+ completeOnboardingWizard,
+ skipOnboardingIfPresent,
+} from "./utils/onboarding";
+import { signupTestUser } from "./utils/signup";
+
+test("auth happy path: user can sign up with a fresh account", async ({
+ page,
+}) => {
+ test.setTimeout(60000);
+
+ await signupTestUser(page, undefined, undefined, false);
+ await expect(page).toHaveURL(/\/onboarding/);
+ await expect(page.getByText("Welcome to AutoGPT")).toBeVisible();
+});
+
+test("auth happy path: user can sign up, enter the app, and log out", async ({
+ page,
+}) => {
+ test.setTimeout(90000);
+
+ await signupTestUser(page, undefined, undefined, false);
+ await expect(page).toHaveURL(/\/onboarding/);
+ await expect(page.getByText("Welcome to AutoGPT")).toBeVisible();
+
+ await skipOnboardingIfPresent(page, "/marketplace");
+ await expect(page).toHaveURL(/\/marketplace/);
+ await expect(page.getByTestId("profile-popout-menu-trigger")).toBeVisible();
+
+ await page.getByTestId("profile-popout-menu-trigger").click();
+ await page.getByRole("button", { name: "Log out" }).click();
+
+ await expect(page).toHaveURL(/\/login/);
+
+ await page.goto("/library");
+ await expect(page).toHaveURL(/\/login\?next=%2Flibrary/);
+});
+
+test("auth happy path: seeded user can log in", async ({ page }) => {
+ test.setTimeout(60000);
+
+ const testUser = getSeededTestUser("smokeAuth");
+ const loginPage = new LoginPage(page);
+
+ await page.goto("/login");
+ await loginPage.login(testUser.email, testUser.password);
+
+ await expect(page).toHaveURL(/\/marketplace/);
+ await expect(page.getByTestId("profile-popout-menu-trigger")).toBeVisible();
+});
+
+test("auth happy path: seeded user can log out and protected routes redirect to login", async ({
+ page,
+}) => {
+ test.setTimeout(60000);
+
+ const testUser = getSeededTestUser("primary");
+ const loginPage = new LoginPage(page);
+
+ await page.goto("/login");
+ await loginPage.login(testUser.email, testUser.password);
+
+ await expect(page).toHaveURL(/\/marketplace/);
+ await page.getByTestId("profile-popout-menu-trigger").click();
+ await page.getByRole("button", { name: "Log out" }).click();
+
+ await expect(page).toHaveURL(/\/login/, { timeout: 15000 });
+
+ await page.goto("/profile");
+ await expect(page).toHaveURL(/\/login\?next=%2Fprofile/);
+});
+
+test("auth happy path: user can complete onboarding and land in the app", async ({
+ page,
+}) => {
+ test.setTimeout(60000);
+
+ await signupTestUser(page, undefined, undefined, false);
+ await expect(page).toHaveURL(/\/onboarding/);
+
+ await completeOnboardingWizard(page, {
+ name: "Smoke User",
+ role: "Engineering",
+ painPoints: ["Research", "Reports & data"],
+ });
+
+ await expect(page).toHaveURL(/\/copilot/);
+ await expect(page.getByTestId("profile-popout-menu-trigger")).toBeVisible();
+});
+
+test("auth happy path: multi-tab logout clears shared builder sessions", async ({
+ context,
+}) => {
+ // Two pages + builder load + logout sequence justifies a higher timeout
+ test.setTimeout(90000);
+
+ const consoleErrors: string[] = [];
+
+ const page1 = await context.newPage();
+ const page2 = await context.newPage();
+ const buildPage = new BuildPage(page1);
+
+ const recordWebSocketErrors =
+ (label: string) => (msg: { type: () => string; text: () => string }) => {
+ if (msg.type() === "error" && msg.text().includes("WebSocket")) {
+ consoleErrors.push(`${label}: ${msg.text()}`);
+ }
+ };
+
+ page1.on("console", recordWebSocketErrors("page1"));
+ page2.on("console", recordWebSocketErrors("page2"));
+
+ await signupTestUser(page1, undefined, undefined, false);
+ await expect(page1).toHaveURL(/\/onboarding/);
+ await skipOnboardingIfPresent(page1, "/build");
+
+ await page1.goto("/build");
+ await expect(page1).toHaveURL(/\/build/);
+ await buildPage.closeTutorial();
+ await expect(page1.getByTestId("profile-popout-menu-trigger")).toBeVisible();
+
+ await page2.goto("/build");
+ await expect(page2).toHaveURL(/\/build/);
+ await expect(page2.getByTestId("profile-popout-menu-trigger")).toBeVisible();
+
+ await page1.getByTestId("profile-popout-menu-trigger").click();
+ await page1.getByRole("button", { name: "Log out" }).click();
+ await expect(page1).toHaveURL(/\/login/);
+
+ await page2.reload();
+ await expect(page2).toHaveURL(/\/login\?next=%2Fbuild/);
+ await expect(page2.getByTestId("profile-popout-menu-trigger")).toBeHidden();
+
+ expect(consoleErrors).toHaveLength(0);
+
+ // Prove the auth token is actually gone, not just the UI hidden. Supabase
+ // overwrites the cookie on signout with an empty value + past expiry
+ // rather than deleting it. An assertion that is silently skipped when the
+ // cookie is missing under the expected name would hide a real regression,
+ // so we assert on every non-empty sb-*auth-token* cookie explicitly.
+ const cookiesAfterLogout = await context.cookies();
+ const authCookies = cookiesAfterLogout.filter(
+ (c) => c.name.startsWith("sb-") && c.name.includes("auth-token"),
+ );
+ for (const cookie of authCookies) {
+ expect(
+ cookie.value,
+ `supabase auth cookie ${cookie.name} must be empty after logout`,
+ ).toBe("");
+ }
+
+ await page1.close();
+ await page2.close();
+});
diff --git a/autogpt_platform/frontend/src/playwright/builder-happy-path.spec.ts b/autogpt_platform/frontend/src/playwright/builder-happy-path.spec.ts
new file mode 100644
index 0000000000..b6c2f8d8c2
--- /dev/null
+++ b/autogpt_platform/frontend/src/playwright/builder-happy-path.spec.ts
@@ -0,0 +1,83 @@
+import { expect, test } from "./coverage-fixture";
+import { E2E_AUTH_STATES } from "./credentials/accounts";
+import { BuildPage } from "./pages/build.page";
+
+test.use({ storageState: E2E_AUTH_STATES.builder });
+
+test("builder happy path: user can walk through the builder tutorial and cancel midway, persisting canceled state", async ({
+ page,
+}) => {
+ test.setTimeout(180000);
+
+ const buildPage = new BuildPage(page);
+ await buildPage.startTutorial();
+ await buildPage.walkWelcomeToBlockMenu();
+ await buildPage.walkSearchAndAddCalculator();
+ await buildPage.cancelTutorial();
+
+ expect(await buildPage.getTutorialStateFromStorage()).toBe("canceled");
+ expect(await buildPage.getNodeCount()).toBeGreaterThanOrEqual(1);
+});
+
+test("builder happy path: user can skip the builder tutorial from the welcome step", async ({
+ page,
+}) => {
+ test.setTimeout(60000);
+
+ const buildPage = new BuildPage(page);
+ await buildPage.startTutorial();
+ await buildPage.skipTutorialFromWelcome();
+});
+
+test("builder happy path: user can create a simple agent in builder with core blocks", async ({
+ page,
+}) => {
+ test.setTimeout(120000);
+
+ const buildPage = new BuildPage(page);
+ await buildPage.open();
+ await buildPage.addSimpleAgentBlocks();
+
+ await expect(buildPage.getNodeLocator()).toHaveCount(2);
+ await expect(
+ buildPage
+ .getNodeLocator(0)
+ .locator('input[placeholder="Enter string value..."]'),
+ ).toHaveValue("smoke-value");
+ await expect(buildPage.getNodeTextInput("Add to Dictionary", 0)).toHaveValue(
+ "smoke-key",
+ );
+ await expect(buildPage.getNodeTextInput("Add to Dictionary", 1)).toHaveValue(
+ "smoke-value",
+ );
+});
+
+test("builder happy path: user can save the created agent", async ({
+ page,
+}) => {
+ test.setTimeout(120000);
+
+ const buildPage = new BuildPage(page);
+ await buildPage.createAndSaveSimpleAgent("Smoke Save Agent");
+
+ await expect(page).toHaveURL(/flowID=/);
+ expect(await buildPage.isRunButtonEnabled()).toBeTruthy();
+});
+
+test("builder happy path: user can run the saved agent from builder and see execution state", async ({
+ page,
+}) => {
+ test.setTimeout(120000);
+
+ const buildPage = new BuildPage(page);
+ await buildPage.createAndSaveSimpleAgent("Smoke Run Agent");
+
+ await buildPage.startRun();
+ await expect(
+ page.locator('[data-id="stop-graph-button"], [data-id="run-graph-button"]'),
+ ).toBeVisible({ timeout: 15000 });
+
+ await expect
+ .poll(() => buildPage.getExecutionState(), { timeout: 15000 })
+ .not.toBe("unknown");
+});
diff --git a/autogpt_platform/frontend/src/playwright/copilot-happy-path.spec.ts b/autogpt_platform/frontend/src/playwright/copilot-happy-path.spec.ts
new file mode 100644
index 0000000000..5af1fc7a86
--- /dev/null
+++ b/autogpt_platform/frontend/src/playwright/copilot-happy-path.spec.ts
@@ -0,0 +1,44 @@
+import { expect, test } from "./coverage-fixture";
+import { E2E_AUTH_STATES } from "./credentials/accounts";
+import { CopilotPage } from "./pages/copilot.page";
+
+test.use({ storageState: E2E_AUTH_STATES.marketplace });
+
+test("copilot happy path: user can create a deterministic AutoPilot session and keep it after reload", async ({
+ page,
+}) => {
+ test.setTimeout(120000);
+
+ const copilotPage = new CopilotPage(page);
+ await copilotPage.open();
+
+ const sessionId = await copilotPage.createSessionViaApi();
+
+ await copilotPage.open(sessionId);
+ await copilotPage.waitForChatInput();
+
+ await page.reload();
+ await page.waitForLoadState("domcontentloaded");
+ await copilotPage.dismissNotificationPrompt();
+
+ await expect
+ .poll(() => new URL(page.url()).searchParams.get("sessionId"), {
+ timeout: 15000,
+ })
+ .toBe(sessionId);
+ await copilotPage.waitForChatInput();
+
+ // Sending a message must render the user's prompt in the conversation
+ // immediately. This catches a regression where the chat input accepts
+ // text but Enter is a no-op, without depending on knowing the exact
+ // backend endpoint name (which has shifted historically).
+ const userPrompt = `ping from e2e ${Date.now().toString().slice(-6)}`;
+ const chatInput = copilotPage.getChatInput();
+ await chatInput.fill(userPrompt);
+ await chatInput.press("Enter");
+
+ await expect(
+ page.getByText(userPrompt, { exact: false }).first(),
+ "user's typed prompt must appear in the chat after pressing Enter",
+ ).toBeVisible({ timeout: 15000 });
+});
diff --git a/autogpt_platform/frontend/src/tests/coverage-fixture.ts b/autogpt_platform/frontend/src/playwright/coverage-fixture.ts
similarity index 100%
rename from autogpt_platform/frontend/src/tests/coverage-fixture.ts
rename to autogpt_platform/frontend/src/playwright/coverage-fixture.ts
diff --git a/autogpt_platform/frontend/src/playwright/credentials/accounts.ts b/autogpt_platform/frontend/src/playwright/credentials/accounts.ts
new file mode 100644
index 0000000000..f0fef0cfea
--- /dev/null
+++ b/autogpt_platform/frontend/src/playwright/credentials/accounts.ts
@@ -0,0 +1,85 @@
+import path from "path";
+
+export const SEEDED_TEST_PASSWORD =
+ process.env.SEEDED_TEST_PASSWORD || "testpassword123";
+export const SEEDED_USER_POOL_VERSION = "2.0.0";
+
+export const SEEDED_TEST_ACCOUNTS = {
+ primary: {
+ key: "primary",
+ email: "test123@example.com",
+ password: SEEDED_TEST_PASSWORD,
+ },
+ smokeAuth: {
+ key: "smokeAuth",
+ email: "e2e.qa.auth@example.com",
+ password: SEEDED_TEST_PASSWORD,
+ },
+ smokeBuilder: {
+ key: "smokeBuilder",
+ email: "e2e.qa.builder@example.com",
+ password: SEEDED_TEST_PASSWORD,
+ },
+ smokeLibrary: {
+ key: "smokeLibrary",
+ email: "e2e.qa.library@example.com",
+ password: SEEDED_TEST_PASSWORD,
+ },
+ smokeMarketplace: {
+ key: "smokeMarketplace",
+ email: "e2e.qa.marketplace@example.com",
+ password: SEEDED_TEST_PASSWORD,
+ },
+ smokeSettings: {
+ key: "smokeSettings",
+ email: "e2e.qa.settings@example.com",
+ password: SEEDED_TEST_PASSWORD,
+ },
+ parallelA: {
+ key: "parallelA",
+ email: "e2e.qa.parallel.a@example.com",
+ password: SEEDED_TEST_PASSWORD,
+ },
+ parallelB: {
+ key: "parallelB",
+ email: "e2e.qa.parallel.b@example.com",
+ password: SEEDED_TEST_PASSWORD,
+ },
+} as const;
+
+export type SeededTestAccountKey = keyof typeof SEEDED_TEST_ACCOUNTS;
+export type SeededTestAccount =
+ (typeof SEEDED_TEST_ACCOUNTS)[SeededTestAccountKey];
+
+export const SEEDED_TEST_USERS = Object.values(SEEDED_TEST_ACCOUNTS);
+export const SEEDED_AUTH_STATE_ACCOUNT_KEYS = [
+ "smokeBuilder",
+ "smokeLibrary",
+ "smokeMarketplace",
+ "smokeSettings",
+ "parallelA",
+ "parallelB",
+] as const;
+
+export const AUTH_DIRECTORY = path.resolve(process.cwd(), ".auth");
+
+export function getAuthStatePath(accountKey: SeededTestAccountKey) {
+ return path.join(AUTH_DIRECTORY, "states", `${accountKey}.json`);
+}
+
+export const E2E_AUTH_STATES = {
+ builder: getAuthStatePath("smokeBuilder"),
+ library: getAuthStatePath("smokeLibrary"),
+ marketplace: getAuthStatePath("smokeMarketplace"),
+ settings: getAuthStatePath("smokeSettings"),
+ parallelA: getAuthStatePath("parallelA"),
+ parallelB: getAuthStatePath("parallelB"),
+} as const;
+
+export const SMOKE_AUTH_STATES = E2E_AUTH_STATES;
+
+export function getSeededTestUser(
+ accountKey: SeededTestAccountKey = "primary",
+): SeededTestAccount {
+ return SEEDED_TEST_ACCOUNTS[accountKey];
+}
diff --git a/autogpt_platform/frontend/src/playwright/credentials/index.ts b/autogpt_platform/frontend/src/playwright/credentials/index.ts
new file mode 100644
index 0000000000..cefa3931cb
--- /dev/null
+++ b/autogpt_platform/frontend/src/playwright/credentials/index.ts
@@ -0,0 +1,27 @@
+import { getSeededTestUser } from "./accounts";
+
+// E2E Test Credentials and Constants
+export const TEST_CREDENTIALS = getSeededTestUser("primary");
+
+export function getTestUserWithLibraryAgents() {
+ return TEST_CREDENTIALS;
+}
+
+// Dummy constant to help developers identify agents that don't need input
+export const DummyInput = "DummyInput";
+
+// This will be used for testing agent submission for test123@example.com
+export const TEST_AGENT_DATA = {
+ name: "E2E Calculator Agent",
+ description:
+ "A deterministic marketplace agent built from Calculator and Agent Output blocks for frontend E2E coverage.",
+ image_urls: [
+ "https://picsum.photos/seed/e2e-marketplace-1/200/300",
+ "https://picsum.photos/seed/e2e-marketplace-2/200/301",
+ "https://picsum.photos/seed/e2e-marketplace-3/200/302",
+ ],
+ video_url: "https://www.youtube.com/watch?v=test123",
+ sub_heading: "A deterministic calculator agent for PR E2E coverage",
+ categories: ["test", "demo", "frontend"],
+ changes_summary: "Initial deterministic calculator submission",
+} as const;
diff --git a/autogpt_platform/frontend/src/playwright/credentials/storage-state.ts b/autogpt_platform/frontend/src/playwright/credentials/storage-state.ts
new file mode 100644
index 0000000000..1dbaaa1616
--- /dev/null
+++ b/autogpt_platform/frontend/src/playwright/credentials/storage-state.ts
@@ -0,0 +1,23 @@
+export function buildCookieConsentStorageState(
+ origin: string = "http://localhost:3000",
+) {
+ return {
+ cookies: [],
+ origins: [
+ {
+ origin,
+ localStorage: [
+ {
+ name: "autogpt_cookie_consent",
+ value: JSON.stringify({
+ hasConsented: true,
+ timestamp: Date.now(),
+ analytics: true,
+ monitoring: true,
+ }),
+ },
+ ],
+ },
+ ],
+ };
+}
diff --git a/autogpt_platform/frontend/src/playwright/global-setup.ts b/autogpt_platform/frontend/src/playwright/global-setup.ts
new file mode 100644
index 0000000000..90270d32a0
--- /dev/null
+++ b/autogpt_platform/frontend/src/playwright/global-setup.ts
@@ -0,0 +1,49 @@
+import { FullConfig } from "@playwright/test";
+import {
+ ensureSeededAuthStates,
+ getInvalidSeededAuthStateKeys,
+} from "./utils/auth";
+
+function resolveBaseURL(config: FullConfig) {
+ const configuredBaseURL =
+ config.projects[0]?.use?.baseURL ?? "http://localhost:3000";
+
+ if (typeof configuredBaseURL !== "string") {
+ throw new Error(
+ `Playwright baseURL must be a string during global setup. Received ${String(
+ configuredBaseURL,
+ )}.`,
+ );
+ }
+
+ return configuredBaseURL;
+}
+
+async function globalSetup(config: FullConfig) {
+ console.log("🚀 Starting global test setup...");
+
+ try {
+ const baseURL = resolveBaseURL(config);
+ const invalidKeys = await getInvalidSeededAuthStateKeys(baseURL);
+
+ if (invalidKeys.length === 0) {
+ console.log("♻️ Reusing stored seeded auth states");
+ return;
+ }
+
+ console.log(
+ `🔐 Refreshing seeded auth states for: ${invalidKeys.join(", ")}`,
+ );
+ await ensureSeededAuthStates(baseURL);
+
+ console.log("✅ Global setup completed successfully!");
+ } catch (error) {
+ console.error("❌ Global setup failed:", error);
+ console.error(
+ "💡 Run backend/test/e2e_test_data.py to seed the deterministic Playwright accounts before retrying.",
+ );
+ throw error;
+ }
+}
+
+export default globalSetup;
diff --git a/autogpt_platform/frontend/src/playwright/library-happy-path.spec.ts b/autogpt_platform/frontend/src/playwright/library-happy-path.spec.ts
new file mode 100644
index 0000000000..f7ed0e796c
--- /dev/null
+++ b/autogpt_platform/frontend/src/playwright/library-happy-path.spec.ts
@@ -0,0 +1,559 @@
+import path from "path";
+import type { Page } from "@playwright/test";
+import { expect, test } from "./coverage-fixture";
+import { E2E_AUTH_STATES } from "./credentials/accounts";
+import { BuildPage, createUniqueAgentName } from "./pages/build.page";
+import {
+ clickRunButton,
+ dismissFeedbackDialog,
+ getActiveItemId,
+ importAgentFromFile,
+ LibraryPage,
+} from "./pages/library.page";
+
+test.use({ storageState: E2E_AUTH_STATES.library });
+
+const TEST_AGENT_PATH = path.resolve(__dirname, "assets", "testing_agent.json");
+const CALCULATOR_BLOCK_ID = "b1ab9b19-67a6-406d-abf5-2dba76d00c79";
+const AGENT_OUTPUT_BLOCK_ID = "363ae599-353e-4804-937e-b2ee3cef3da4";
+const STOPPED_RUN_STATUSES = new Set([
+ "terminated",
+ "failed",
+ "incomplete",
+ "completed",
+]);
+
+type UploadedGraphNode = {
+ id: string;
+ block_id: string;
+ input_default: Record;
+ metadata: {
+ position: {
+ x: number;
+ y: number;
+ };
+ };
+ input_links: unknown[];
+ output_links: unknown[];
+};
+
+function createLongRunningCalculatorGraph(
+ agentName: string,
+ calculatorCount: number = 150,
+) {
+ const nodes: UploadedGraphNode[] = Array.from(
+ { length: calculatorCount },
+ (_, index) => ({
+ id: `calc-${index + 1}`,
+ block_id: CALCULATOR_BLOCK_ID,
+ input_default:
+ index === 0
+ ? {
+ operation: "Add",
+ a: 1,
+ b: 1,
+ round_result: false,
+ }
+ : {
+ operation: "Add",
+ b: 1,
+ round_result: false,
+ },
+ metadata: {
+ position: { x: 320 * index, y: 120 },
+ },
+ input_links: [],
+ output_links: [],
+ }),
+ );
+
+ const links = Array.from({ length: calculatorCount - 1 }, (_, index) => ({
+ source_id: `calc-${index + 1}`,
+ sink_id: `calc-${index + 2}`,
+ source_name: "result",
+ sink_name: "a",
+ }));
+
+ nodes.push({
+ id: "final-output",
+ block_id: AGENT_OUTPUT_BLOCK_ID,
+ input_default: {
+ name: "Final result",
+ description: "Long-running calculator chain output",
+ },
+ metadata: {
+ position: { x: 320 * calculatorCount, y: 120 },
+ },
+ input_links: [],
+ output_links: [],
+ });
+ links.push({
+ source_id: `calc-${calculatorCount}`,
+ sink_id: "final-output",
+ source_name: "result",
+ sink_name: "value",
+ });
+
+ return {
+ name: agentName,
+ description:
+ "Deterministic long-running calculator chain for runner stop coverage",
+ is_active: true,
+ nodes,
+ links,
+ };
+}
+
+async function createLongRunningSavedAgent(
+ page: Page,
+ agentName: string,
+): Promise<{ graphId: string; graphVersion: number }> {
+ const response = await page.request.post("/api/proxy/api/graphs", {
+ data: {
+ graph: createLongRunningCalculatorGraph(agentName),
+ source: "upload",
+ },
+ });
+ expect(response.ok(), "expected graph creation API request to succeed").toBe(
+ true,
+ );
+
+ const body = (await response.json()) as {
+ id?: string;
+ version?: number;
+ data?: { id?: string; version?: number };
+ };
+ expect(
+ body.data?.id ?? body.id,
+ "graph creation should return a graph id",
+ ).toBeTruthy();
+
+ return {
+ graphId: String(body.data?.id ?? body.id),
+ graphVersion: Number(body.data?.version ?? body.version ?? 1),
+ };
+}
+
+async function createDeterministicCalculatorSavedAgent(
+ page: Page,
+ agentName: string,
+ outputName: string,
+): Promise {
+ const response = await page.request.post("/api/proxy/api/graphs", {
+ data: {
+ graph: {
+ name: agentName,
+ description:
+ "Deterministic calculator output for run-result assertions",
+ is_active: true,
+ nodes: [
+ {
+ id: "calc-1",
+ block_id: CALCULATOR_BLOCK_ID,
+ input_default: {
+ operation: "Add",
+ a: 1,
+ b: 1,
+ round_result: false,
+ },
+ metadata: {
+ position: { x: 120, y: 160 },
+ },
+ input_links: [],
+ output_links: [],
+ },
+ {
+ id: "final-output",
+ block_id: AGENT_OUTPUT_BLOCK_ID,
+ input_default: {
+ name: outputName,
+ description: "Deterministic result output",
+ },
+ metadata: {
+ position: { x: 520, y: 160 },
+ },
+ input_links: [],
+ output_links: [],
+ },
+ ],
+ links: [
+ {
+ source_id: "calc-1",
+ sink_id: "final-output",
+ source_name: "result",
+ sink_name: "value",
+ },
+ ],
+ },
+ source: "upload",
+ },
+ });
+ expect(
+ response.ok(),
+ "expected deterministic calculator graph creation API request to succeed",
+ ).toBe(true);
+}
+
+async function getExecutionStatusFromApi(
+ page: Page,
+ graphId: string,
+ runId: string,
+): Promise {
+ const response = await page.request.get(
+ `/api/proxy/api/graphs/${graphId}/executions/${runId}`,
+ );
+ expect(response.ok(), "execution details API should succeed").toBe(true);
+
+ const body = (await response.json()) as { status?: string };
+ return body.status?.toLowerCase() ?? "unknown";
+}
+
+async function createAndSaveDeterministicOutputAgent(
+ page: Page,
+ prefix: string,
+): Promise<{ agentName: string; expectedOutput: string; outputName: string }> {
+ const buildPage = new BuildPage(page);
+ const agentName = createUniqueAgentName(prefix);
+ const expectedOutput = `e2e-output-${Date.now()}`;
+ const outputName = `e2e-result-${Date.now()}`;
+
+ await buildPage.open();
+ await buildPage.addBlockByClick("Store Value");
+ await buildPage.waitForNodeOnCanvas(1);
+ await buildPage.fillBlockInputByPlaceholder(
+ "Enter string value...",
+ expectedOutput,
+ 0,
+ );
+
+ await buildPage.addBlockByClick("Agent Output");
+ await buildPage.waitForNodeOnCanvas(2);
+ await buildPage.connectNodes(0, 1);
+ await buildPage.fillLastNodeTextInput("Agent Output", outputName);
+
+ await buildPage.saveAgent(
+ agentName,
+ "Deterministic output agent for library run verification",
+ );
+ await buildPage.waitForSaveComplete();
+ await buildPage.waitForSaveButton();
+
+ return { agentName, expectedOutput, outputName };
+}
+
+test("library happy path: user can import an agent file into Library", async ({
+ page,
+}) => {
+ test.setTimeout(120000);
+
+ const { importedAgent } = await importAgentFromFile(
+ page,
+ TEST_AGENT_PATH,
+ createUniqueAgentName("E2E Import Agent"),
+ );
+
+ expect(importedAgent.name).toContain("E2E Import Agent");
+});
+
+test("library happy path: user can open the imported or saved agent from Library in builder", async ({
+ page,
+}) => {
+ test.setTimeout(120000);
+
+ const { libraryPage, importedAgent } = await importAgentFromFile(
+ page,
+ TEST_AGENT_PATH,
+ createUniqueAgentName("E2E Open Agent"),
+ );
+
+ // Register the popup listener before clicking so we don't miss a fast open.
+ // A short timeout covers the case where the link opens in the current tab.
+ const popupPromise = page
+ .context()
+ .waitForEvent("page", { timeout: 10000 })
+ .catch(() => null);
+ await libraryPage.clickOpenInBuilder(importedAgent);
+ const builderPage = (await popupPromise) ?? page;
+
+ await builderPage.waitForLoadState("domcontentloaded");
+ await expect(builderPage).toHaveURL(/\/build/);
+ const importedBuildPage = new BuildPage(builderPage);
+ await importedBuildPage.waitForNodeOnCanvas();
+ expect(await importedBuildPage.getNodeCount()).toBeGreaterThan(0);
+ if (builderPage !== page) {
+ await builderPage.close();
+ }
+});
+
+test("library happy path: user can start and stop a saved task from runner UI", async ({
+ page,
+}) => {
+ test.setTimeout(180000);
+
+ const agentName = createUniqueAgentName("E2E Stop Task Agent");
+ const { graphId } = await createLongRunningSavedAgent(page, agentName);
+
+ const libraryPage = new LibraryPage(page);
+ await libraryPage.openSavedAgent(agentName);
+ await clickRunButton(page);
+
+ await expect
+ .poll(() => getActiveItemId(page), { timeout: 45000 })
+ .not.toBe(null);
+ const runId = getActiveItemId(page);
+ expect(runId, "run id should be present after starting task").toBeTruthy();
+ await expect
+ .poll(() => libraryPage.getRunStatus(), { timeout: 45000 })
+ .toBe("running");
+
+ const stopTaskButton = page.getByRole("button", { name: /Stop task/i });
+ await expect(stopTaskButton).toBeVisible({ timeout: 30000 });
+ const stopResponsePromise = page.waitForResponse(
+ (response) =>
+ response.request().method() === "POST" &&
+ response
+ .url()
+ .includes(`/api/graphs/${graphId}/executions/${runId}/stop`),
+ { timeout: 15000 },
+ );
+ await stopTaskButton.click();
+ const stopResponse = await stopResponsePromise;
+
+ expect(stopResponse.ok(), "stop run API should succeed").toBe(true);
+ await expect(page.getByText("Run stopped")).toBeVisible({ timeout: 15000 });
+ await expect
+ .poll(
+ async () => {
+ const status = await getExecutionStatusFromApi(
+ page,
+ graphId,
+ String(runId),
+ );
+ return STOPPED_RUN_STATUSES.has(status) ? status : "running";
+ },
+ { timeout: 45000 },
+ )
+ .not.toBe("running");
+});
+
+test("library happy path: user can run a saved agent and verify expected output", async ({
+ page,
+}) => {
+ test.setTimeout(150000);
+
+ const agentName = createUniqueAgentName("E2E Expected Output Agent");
+ const outputName = `e2e-result-${Date.now()}`;
+ await createDeterministicCalculatorSavedAgent(page, agentName, outputName);
+
+ const libraryPage = new LibraryPage(page);
+ await libraryPage.openSavedAgent(agentName);
+ await clickRunButton(page);
+ await libraryPage.waitForRunToComplete();
+ await dismissFeedbackDialog(page);
+
+ await libraryPage.assertRunProducedOutput();
+ await libraryPage.assertRunOutputValue(outputName, /^2(?:\.0+)?$/);
+ await expect
+ .poll(() => libraryPage.getRunStatus(), { timeout: 15000 })
+ .toBe("completed");
+});
+
+test("library happy path: user can edit a saved agent from Library and keep changes after refresh", async ({
+ page,
+}) => {
+ test.setTimeout(150000);
+
+ const { agentName } = await createAndSaveDeterministicOutputAgent(
+ page,
+ "E2E Edit Persist Agent",
+ );
+ const editedValue = `edited-value-${Date.now()}`;
+
+ const libraryPage = new LibraryPage(page);
+ await page.goto("/library");
+ await libraryPage.waitForAgentsToLoad();
+ await libraryPage.searchAgents(agentName);
+ await libraryPage.waitForAgentsToLoad();
+
+ const agentCard = page
+ .getByTestId("library-agent-card")
+ .filter({ hasText: agentName })
+ .first();
+ await expect(agentCard).toBeVisible({ timeout: 15000 });
+
+ const popupPromise = page
+ .context()
+ .waitForEvent("page", { timeout: 10000 })
+ .catch(() => null);
+ await agentCard
+ .getByTestId("library-agent-card-open-in-builder-link")
+ .first()
+ .click();
+ const builderPage = (await popupPromise) ?? page;
+
+ const builderTabPage = new BuildPage(builderPage);
+ await builderTabPage.waitForNodeOnCanvas();
+ await builderTabPage.fillBlockInputByPlaceholder(
+ "Enter string value...",
+ editedValue,
+ 0,
+ );
+
+ await builderPage.getByTestId("save-control-save-button").click();
+ const saveAgentButton = builderPage.getByRole("button", {
+ name: "Save Agent",
+ });
+ if (await saveAgentButton.isVisible({ timeout: 3000 }).catch(() => false)) {
+ await expect(saveAgentButton).toBeEnabled({ timeout: 10000 });
+ await saveAgentButton.click();
+ await expect(saveAgentButton).toBeHidden({ timeout: 15000 });
+ }
+
+ await builderPage.reload();
+ await builderTabPage.waitForNodeOnCanvas();
+ await expect(
+ builderTabPage
+ .getNodeLocator(0)
+ .locator('input[placeholder="Enter string value..."]'),
+ ).toHaveValue(editedValue);
+
+ if (builderPage !== page) {
+ await builderPage.close();
+ }
+});
+
+test("library happy path: user can rerun a completed task from the Library agent page", async ({
+ page,
+}) => {
+ test.setTimeout(120000);
+
+ const buildPage = new BuildPage(page);
+ const { agentName } =
+ await buildPage.createAndSaveSimpleAgent("E2E Rerun Agent");
+
+ const libraryPage = new LibraryPage(page);
+ await libraryPage.openSavedAgent(agentName);
+ await clickRunButton(page);
+ await libraryPage.waitForRunToComplete();
+ await dismissFeedbackDialog(page);
+
+ const rerunTaskButton = page.getByRole("button", { name: /Rerun task/i });
+ await expect(rerunTaskButton).toBeVisible({ timeout: 45000 });
+
+ await expect
+ .poll(() => getActiveItemId(page), { timeout: 45000 })
+ .not.toBe(null);
+
+ const initialRunId = getActiveItemId(page);
+ expect(initialRunId).toBeTruthy();
+
+ await rerunTaskButton.click();
+
+ await expect(page.getByText("Run started", { exact: true })).toBeVisible({
+ timeout: 15000,
+ });
+
+ await expect
+ .poll(() => getActiveItemId(page), { timeout: 45000 })
+ .not.toBe(initialRunId);
+
+ await libraryPage.waitForRunToComplete();
+
+ // Simple agent has no AgentOutputBlock — verify run completion only.
+ const runStatus = await libraryPage.getRunStatus();
+ expect(runStatus).toBe("completed");
+});
+
+test("library happy path: user can delete a completed task from the run sidebar", async ({
+ page,
+}) => {
+ test.setTimeout(120000);
+
+ const buildPage = new BuildPage(page);
+ const { agentName } = await buildPage.createAndSaveSimpleAgent(
+ "E2E Delete Task Agent",
+ );
+
+ const libraryPage = new LibraryPage(page);
+ await libraryPage.openSavedAgent(agentName);
+ await clickRunButton(page);
+ await libraryPage.waitForRunToComplete();
+ await dismissFeedbackDialog(page);
+
+ // Open the per-task actions dropdown ("More actions" three-dot button)
+ // and use the menu's Delete task option to remove the run.
+ const moreActionsButton = page
+ .getByRole("button", { name: "More actions" })
+ .first();
+ await expect(moreActionsButton).toBeVisible({ timeout: 15000 });
+ await moreActionsButton.click();
+
+ await page.getByRole("menuitem", { name: /Delete( this)? task/i }).click();
+
+ const confirmDialog = page.getByRole("dialog", { name: /Delete task/i });
+ await expect(confirmDialog).toBeVisible({ timeout: 10000 });
+ await confirmDialog.getByRole("button", { name: /^Delete Task$/ }).click();
+
+ // Toast confirms the backend actually deleted (not just dialog closed).
+ await expect(page.getByText("Task deleted", { exact: true })).toBeVisible({
+ timeout: 15000,
+ });
+
+ // Sidebar should drop the only run, returning the page to initial
+ // task-entry state.
+ await expect(
+ page.getByRole("button", { name: /^(Setup your task|New task)$/i }),
+ ).toBeVisible({ timeout: 15000 });
+});
+
+test("library happy path: user can open the agent in builder from the exact runner customise-agent path", async ({
+ page,
+ context,
+}) => {
+ test.setTimeout(120000);
+
+ const buildPage = new BuildPage(page);
+ const { agentName } = await buildPage.createAndSaveSimpleAgent(
+ "E2E View Task Agent",
+ );
+
+ const libraryPage = new LibraryPage(page);
+ await libraryPage.openSavedAgent(agentName);
+ await clickRunButton(page);
+ await libraryPage.waitForRunToComplete();
+ await dismissFeedbackDialog(page);
+
+ // The "View task details" eye-icon button on a completed run opens the
+ // agent in the builder in a new tab. This exercises the runner → builder
+ // navigation that QA item #22 ("Customise Agent" from Runner UI) covers.
+ const selectedRunId = getActiveItemId(page);
+ expect(selectedRunId).toBeTruthy();
+
+ const viewTaskButton = page
+ .locator('[aria-label="View task details"]')
+ .first();
+ await expect(viewTaskButton).toBeVisible({ timeout: 15000 });
+ const customiseAgentHref = await viewTaskButton.getAttribute("href");
+ expect(customiseAgentHref).toContain("flowID=");
+ expect(customiseAgentHref).toContain("flowVersion=");
+ expect(customiseAgentHref).toContain(`flowExecutionID=${selectedRunId}`);
+
+ const popupPromise = context.waitForEvent("page", { timeout: 15000 });
+ await viewTaskButton.click();
+ const builderTab = await popupPromise;
+
+ await builderTab.waitForLoadState("domcontentloaded");
+ await expect(builderTab).toHaveURL(/\/build/);
+ await expect(builderTab).toHaveURL(
+ new RegExp(`flowExecutionID=${selectedRunId}`),
+ );
+
+ // Verify the builder canvas actually rendered with the agent's nodes —
+ // a navigation that lands on /build but never paints the graph would
+ // otherwise pass on URL alone.
+ const builderTabPage = new BuildPage(builderTab);
+ await builderTabPage.waitForNodeOnCanvas();
+ expect(await builderTabPage.getNodeCount()).toBeGreaterThan(0);
+
+ await builderTab.close();
+});
diff --git a/autogpt_platform/frontend/src/playwright/marketplace-happy-path.spec.ts b/autogpt_platform/frontend/src/playwright/marketplace-happy-path.spec.ts
new file mode 100644
index 0000000000..f81386ea40
--- /dev/null
+++ b/autogpt_platform/frontend/src/playwright/marketplace-happy-path.spec.ts
@@ -0,0 +1,48 @@
+import { expect, test } from "./coverage-fixture";
+import { E2E_AUTH_STATES } from "./credentials/accounts";
+import {
+ clickRunButton,
+ dismissFeedbackDialog,
+ LibraryPage,
+} from "./pages/library.page";
+import { MarketplacePage } from "./pages/marketplace.page";
+
+test.use({ storageState: E2E_AUTH_STATES.marketplace });
+
+test("marketplace happy path: user can browse Marketplace and open an agent detail page", async ({
+ page,
+}) => {
+ test.setTimeout(90000);
+
+ const marketplacePage = new MarketplacePage(page);
+ await marketplacePage.openFeaturedAgent();
+
+ await expect(page.getByTestId("agent-description")).toBeVisible();
+});
+
+test("marketplace happy path: user can add a Marketplace agent to Library and run it", async ({
+ page,
+}) => {
+ test.setTimeout(120000);
+
+ const marketplacePage = new MarketplacePage(page);
+ await marketplacePage.openRunnableAgent();
+
+ const agentName = await page.getByTestId("agent-title").innerText();
+
+ await page.getByTestId("agent-add-library-button").click();
+ await expect(page.getByText("Redirecting to your library...")).toBeVisible();
+ await expect(page).toHaveURL(/\/library\/agents\//);
+
+ const libraryPage = new LibraryPage(page);
+ await libraryPage.openSavedAgent(agentName);
+ await clickRunButton(page);
+
+ await libraryPage.waitForRunToComplete();
+ await dismissFeedbackDialog(page);
+
+ const runStatus = await libraryPage.getRunStatus();
+ expect(runStatus).toBe("completed");
+ await libraryPage.assertRunProducedOutput();
+ await libraryPage.assertFirstRunOutputValue(/^\d+(?:\.0+)?$/);
+});
diff --git a/autogpt_platform/frontend/src/tests/pages/base.page.ts b/autogpt_platform/frontend/src/playwright/pages/base.page.ts
similarity index 100%
rename from autogpt_platform/frontend/src/tests/pages/base.page.ts
rename to autogpt_platform/frontend/src/playwright/pages/base.page.ts
diff --git a/autogpt_platform/frontend/src/playwright/pages/build.page.ts b/autogpt_platform/frontend/src/playwright/pages/build.page.ts
new file mode 100644
index 0000000000..7c3649201f
--- /dev/null
+++ b/autogpt_platform/frontend/src/playwright/pages/build.page.ts
@@ -0,0 +1,642 @@
+import { randomUUID } from "crypto";
+import { expect, Locator, Page } from "@playwright/test";
+import { BasePage } from "./base.page";
+
+export function createUniqueAgentName(prefix: string): string {
+ return `${prefix} ${Date.now()}-${randomUUID().slice(0, 8)}`;
+}
+
+function escapeRegex(text: string): string {
+ return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
+}
+
+export class BuildPage extends BasePage {
+ constructor(page: Page) {
+ super(page);
+ }
+
+ // --- Navigation ---
+
+ async goto(): Promise {
+ await this.page.goto("/build");
+ await this.page.waitForLoadState("domcontentloaded");
+ }
+
+ async isLoaded(): Promise {
+ 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 {
+ try {
+ await this.page
+ .getByRole("button", { name: "Skip Tutorial", exact: true })
+ .click({ timeout: 3000 });
+ } catch {
+ // Tutorial not shown or already dismissed
+ }
+ }
+
+ // --- Block Menu ---
+
+ async openBlocksPanel(): Promise {
+ const popoverContent = this.page.locator(
+ '[data-id="blocks-control-popover-content"]',
+ );
+ if (!(await popoverContent.isVisible())) {
+ await this.page.getByTestId("blocks-control-blocks-button").click();
+ await popoverContent.waitFor({ state: "visible", timeout: 5000 });
+ }
+ }
+
+ async closeBlocksPanel(): Promise {
+ const popoverContent = this.page.locator(
+ '[data-id="blocks-control-popover-content"]',
+ );
+ 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 {
+ const searchInput = this.page.locator(
+ '[data-id="blocks-control-search-bar"] input[type="text"]',
+ );
+ await searchInput.clear();
+ await searchInput.fill(searchTerm);
+ await expect(searchInput).toHaveValue(searchTerm);
+ }
+
+ 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 {
+ await this.openBlocksPanel();
+ const blockCard = this.getBlockCardByName(searchTerm);
+
+ for (let attempt = 0; attempt < 2; attempt++) {
+ await this.searchBlock(searchTerm);
+
+ const cardVisible = await blockCard
+ .waitFor({
+ state: "visible",
+ timeout: attempt === 0 ? 15000 : 5000,
+ })
+ .then(() => true)
+ .catch(() => false);
+
+ if (cardVisible) {
+ break;
+ }
+ }
+
+ await expect(blockCard).toBeVisible({ timeout: 5000 });
+ await blockCard.click();
+
+ // Close the panel so it doesn't overlay the canvas
+ await this.closeBlocksPanel();
+ }
+
+ async dragBlockToCanvas(searchTerm: string): Promise {
+ 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;
+ }
+
+ getNodeLocatorByTitle(title: string): Locator {
+ const exactTitle = new RegExp(`^\\s*${escapeRegex(title)}\\s*$`, "i");
+ return this.page
+ .locator('[data-id^="custom-node-"]')
+ .filter({ has: this.page.getByText(exactTitle) })
+ .first();
+ }
+
+ getNodeTextInputs(nodeTitle: string): Locator {
+ return this.getNodeLocatorByTitle(nodeTitle).locator(
+ 'input[placeholder="Enter string value..."]:visible',
+ );
+ }
+
+ getNodeTextInput(nodeTitle: string, inputIndex = 0): Locator {
+ return this.getNodeTextInputs(nodeTitle).nth(inputIndex);
+ }
+
+ async fillNodeTextInput(
+ nodeTitle: string,
+ value: string,
+ inputIndex = 0,
+ ): Promise {
+ const node = this.getNodeLocatorByTitle(nodeTitle);
+ await expect(node).toBeVisible({ timeout: 15000 });
+ await expect
+ .poll(async () => await this.getNodeTextInputs(nodeTitle).count(), {
+ timeout: 15000,
+ })
+ .toBeGreaterThan(inputIndex);
+ const input = this.getNodeTextInput(nodeTitle, inputIndex);
+ await input.scrollIntoViewIfNeeded();
+ await input.fill(value);
+ }
+
+ async fillLastNodeTextInput(nodeTitle: string, value: string): Promise {
+ const node = this.getNodeLocatorByTitle(nodeTitle);
+ await expect(node).toBeVisible({ timeout: 15000 });
+ await expect
+ .poll(async () => await this.getNodeTextInputs(nodeTitle).count(), {
+ timeout: 15000,
+ })
+ .toBeGreaterThan(0);
+ const input = this.getNodeTextInputs(nodeTitle).last();
+ await input.scrollIntoViewIfNeeded();
+ await input.fill(value);
+ }
+
+ async getNodeCount(): Promise {
+ return await this.getNodeLocator().count();
+ }
+
+ async waitForNodeOnCanvas(expectedCount?: number): Promise {
+ 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 {
+ const node = this.getNodeLocator(index);
+ await node.click();
+ }
+
+ async selectAllNodes(): Promise {
+ 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 {
+ await this.page.keyboard.press("Backspace");
+ }
+
+ // --- Connections (Edges) ---
+
+ async connectNodes(
+ sourceNodeIndex: number,
+ targetNodeIndex: number,
+ ): Promise {
+ // 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 {
+ return await this.page.locator(".react-flow__edge").count();
+ }
+
+ // --- Save ---
+
+ async saveAgent(
+ name: string = "Test Agent",
+ description: string = "",
+ ): Promise {
+ await this.page.getByTestId("save-control-save-button").click();
+
+ 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 waitForSaveComplete(): Promise {
+ await expect(this.page).toHaveURL(/flowID=/, { timeout: 15000 });
+ }
+
+ async waitForSaveButton(): Promise {
+ await this.page.waitForSelector(
+ '[data-testid="save-control-save-button"]:not([disabled])',
+ { timeout: 10000 },
+ );
+ }
+
+ // --- Run ---
+
+ async isRunButtonEnabled(): Promise {
+ const runButton = this.page.locator('[data-id="run-graph-button"]');
+ return await runButton.isEnabled();
+ }
+
+ async clickRunButton(): Promise {
+ // Dismiss any post-save toast that may be intercepting pointer events on
+ // the run button. Actively close it rather than waiting for Sonner's
+ // default auto-dismiss — the auto-dismiss + fade-out routinely runs over
+ // 5s and caused flakes here. The toast is optional (only after save), so
+ // the dismissal is guarded.
+ await this.dismissSaveToast();
+ const runButton = this.page.locator('[data-id="run-graph-button"]');
+ await runButton.click();
+ }
+
+ // --- Undo / Redo ---
+
+ async isUndoEnabled(): Promise {
+ const btn = this.page.locator('[data-id="undo-button"]');
+ return !(await btn.isDisabled());
+ }
+
+ async isRedoEnabled(): Promise {
+ const btn = this.page.locator('[data-id="redo-button"]');
+ return !(await btn.isDisabled());
+ }
+
+ async clickUndo(): Promise {
+ await this.page.locator('[data-id="undo-button"]').click();
+ }
+
+ async clickRedo(): Promise {
+ await this.page.locator('[data-id="redo-button"]').click();
+ }
+
+ // --- Copy / Paste ---
+
+ async copyViaKeyboard(): Promise {
+ const isMac = process.platform === "darwin";
+ await this.page.keyboard.press(isMac ? "Meta+c" : "Control+c");
+ }
+
+ async pasteViaKeyboard(): Promise {
+ 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 {
+ const node = this.getNodeLocator(nodeIndex);
+ const input = node.getByPlaceholder(placeholder);
+ await input.fill(value);
+ }
+
+ async clickCanvas(): Promise {
+ 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();
+ }
+ }
+
+ getPlaywrightPage(): Page {
+ return this.page;
+ }
+
+ getSavedGraphRef(): { graphId: string; graphVersion: number } {
+ const currentUrl = new URL(this.page.url());
+ const graphId = currentUrl.searchParams.get("flowID");
+ const graphVersion = Number(currentUrl.searchParams.get("flowVersion"));
+
+ if (!graphId || Number.isNaN(graphVersion)) {
+ throw new Error(
+ `Saved graph reference missing from builder URL: ${this.page.url()}`,
+ );
+ }
+
+ return { graphId, graphVersion };
+ }
+
+ async createDummyAgent(): Promise {
+ await this.closeTutorial();
+ await this.addBlockByClick("Add to Dictionary");
+ await this.waitForNodeOnCanvas(1);
+ await this.saveAgent("Test Agent", "Test Description");
+ await this.waitForSaveComplete();
+ }
+
+ // --- Happy-path flows shared across PR smoke specs ---
+
+ async open(): Promise {
+ await this.goto();
+ await this.closeTutorial();
+ await expect(this.page.locator(".react-flow")).toBeVisible({
+ timeout: 15000,
+ });
+ await expect(
+ this.page.getByTestId("blocks-control-blocks-button"),
+ ).toBeVisible({ timeout: 15000 });
+ }
+
+ async addSimpleAgentBlocks(): Promise {
+ await this.addBlockByClick("Store Value");
+ await this.waitForNodeOnCanvas(1);
+ await this.fillBlockInputByPlaceholder(
+ "Enter string value...",
+ "smoke-value",
+ 0,
+ );
+
+ await this.addBlockByClick("Add to Dictionary");
+ await this.waitForNodeOnCanvas(2);
+
+ await this.fillNodeTextInput("Add to Dictionary", "smoke-key", 0);
+ await this.fillNodeTextInput("Add to Dictionary", "smoke-value", 1);
+
+ // Connect Store Value's output to Add to Dictionary so the graph has a
+ // real edge and actually produces output when run. Without this edge the
+ // graph runs but emits no output, and `assertRunProducedOutput` rightly
+ // fails — catching exactly the "I forgot to connect the blocks" bug
+ // manual QA would catch.
+ await this.connectNodes(0, 1);
+ }
+
+ async createAndSaveSimpleAgent(
+ prefix: string,
+ ): Promise<{ agentName: string; graphId: string; graphVersion: number }> {
+ await this.open();
+ const agentName = createUniqueAgentName(prefix);
+
+ await this.addSimpleAgentBlocks();
+ await this.saveAgent(agentName, "PR E2E builder coverage");
+ await this.waitForSaveComplete();
+ await this.waitForSaveButton();
+ const { graphId, graphVersion } = this.getSavedGraphRef();
+
+ return { agentName, graphId, graphVersion };
+ }
+
+ async dismissSaveToast(): Promise {
+ const closeToastButton = this.page.getByRole("button", {
+ name: "Close toast",
+ });
+ // Toast is optional — only shown after a save action
+ if (await closeToastButton.isVisible({ timeout: 1000 })) {
+ await closeToastButton.click();
+ }
+
+ // If the toast appeared but is not yet hidden, wait for it. If it never
+ // appeared at all the locator is simply hidden already — no-op.
+ const savedToast = this.page.getByText("Graph saved successfully");
+ if (await savedToast.isVisible({ timeout: 500 })) {
+ await expect(savedToast).toBeHidden({ timeout: 10000 });
+ }
+ }
+
+ async startRun(): Promise {
+ await this.clickRunButton();
+
+ // The run-input dialog is optional — agents without required inputs skip it
+ const runDialog = this.page.locator('[data-id="run-input-dialog-content"]');
+ if (await runDialog.isVisible({ timeout: 5000 })) {
+ await this.page
+ .locator('[data-id="run-input-manual-run-button"]')
+ .click();
+ }
+ }
+
+ async getExecutionState(): Promise<"running" | "idle" | "unknown"> {
+ const stopButton = this.page.locator('[data-id="stop-graph-button"]');
+ if (await stopButton.isVisible().catch(() => false)) {
+ return "running";
+ }
+
+ const runButton = this.page.locator('[data-id="run-graph-button"]');
+ if (await runButton.isVisible().catch(() => false)) {
+ return "idle";
+ }
+
+ return "unknown";
+ }
+
+ // --- Tutorial (Shepherd.js tour) ---
+
+ // Each Shepherd step's title has id="-label"; using it avoids
+ // title-overlap collisions like "Open the Block Menu" vs "The Block Menu".
+ private getShepherdStep(stepId: string): Locator {
+ return this.page.locator(`#${stepId}-label`);
+ }
+
+ // Scope to .shepherd-enabled so we don't click buttons on hidden-but-still-
+ // attached previous steps.
+ private getShepherdButton(name: string | RegExp): Locator {
+ return this.page
+ .locator(".shepherd-element.shepherd-enabled")
+ .getByRole("button", { name });
+ }
+
+ async startTutorial(): Promise {
+ // Tutorial only starts from pristine /build; a flowID query param routes
+ // the tutorial button to /build?view=new instead.
+ await this.page.goto("/build");
+ await this.page.waitForLoadState("domcontentloaded");
+ await expect(this.page.locator(".react-flow")).toBeVisible({
+ timeout: 15000,
+ });
+
+ await this.page.evaluate(() => {
+ window.localStorage.removeItem("shepherd-tour");
+ });
+
+ const tutorialButton = this.page.locator('[data-id="tutorial-button"]');
+ await expect(tutorialButton).toBeVisible({ timeout: 15000 });
+ await expect(tutorialButton).toBeEnabled({ timeout: 15000 });
+ await tutorialButton.click();
+
+ await expect(this.getShepherdStep("welcome")).toBeVisible({
+ timeout: 15000,
+ });
+ }
+
+ async walkWelcomeToBlockMenu(): Promise {
+ await this.getShepherdButton("Let's Begin").click();
+
+ await expect(this.getShepherdStep("open-block-menu")).toBeVisible({
+ timeout: 10000,
+ });
+ await this.page
+ .locator('[data-id="blocks-control-popover-trigger"]')
+ .click();
+
+ await expect(this.getShepherdStep("block-menu-overview")).toBeVisible({
+ timeout: 10000,
+ });
+ await this.getShepherdButton("Next").click();
+ }
+
+ async walkSearchAndAddCalculator(): Promise {
+ // search-calculator auto-advances once the Calculator block card appears
+ // in the filtered results; select-calculator auto-advances once the
+ // Calculator is added to the node store.
+ await expect(this.getShepherdStep("search-calculator")).toBeVisible({
+ timeout: 10000,
+ });
+ await this.page
+ .locator('[data-id="blocks-control-search-bar"] input[type="text"]')
+ .fill("Calculator");
+
+ const calculatorCard = this.page.locator(
+ '[data-id="blocks-control-search-results"] [data-id="block-card-b1ab9b1967a6406dabf52dba76d00c79"]',
+ );
+ await expect(calculatorCard).toBeVisible({ timeout: 15000 });
+
+ await expect(this.getShepherdStep("select-calculator")).toBeVisible({
+ timeout: 15000,
+ });
+ await calculatorCard.scrollIntoViewIfNeeded();
+ await calculatorCard.click();
+
+ await expect(this.getShepherdStep("focus-new-block")).toBeVisible({
+ timeout: 10000,
+ });
+ await this.waitForNodeOnCanvas(1);
+ }
+
+ // Use dispatchEvent — the Shepherd cancel icon sits inside a step that's
+ // pinned to an off-screen React Flow node, so Playwright's visibility
+ // checks reject a normal click. A synthetic click event still triggers
+ // tour.cancel() via Shepherd's listener.
+ async cancelTutorial(): Promise {
+ await this.page
+ .locator(".shepherd-element.shepherd-enabled .shepherd-cancel-icon")
+ .first()
+ .dispatchEvent("click");
+ await expect(
+ this.page.locator(".shepherd-element.shepherd-enabled"),
+ ).toHaveCount(0, { timeout: 10000 });
+ }
+
+ // NOTE: welcome.ts "Skip Tutorial" only calls handleTutorialSkip, which
+ // writes localStorage but does NOT call tour.cancel(). The tour UI stays
+ // open — the skip state is persisted so the next /build visit knows the
+ // user already dismissed the tour. Callers that want the UI closed must
+ // also call cancelTutorial().
+ async skipTutorialFromWelcome(): Promise {
+ await expect(this.getShepherdStep("welcome")).toBeVisible({
+ timeout: 10000,
+ });
+ await this.getShepherdButton(/Skip Tutorial/i).click();
+ await expect
+ .poll(() => this.getTutorialStateFromStorage(), { timeout: 5000 })
+ .toBe("skipped");
+ }
+
+ async getTutorialStateFromStorage(): Promise {
+ return this.page.evaluate(() =>
+ window.localStorage.getItem("shepherd-tour"),
+ );
+ }
+
+ async createScheduleForSavedAgent(agentName: string): Promise {
+ await this.dismissSaveToast();
+
+ const { graphId, graphVersion } = this.getSavedGraphRef();
+ const scheduleName = `Daily ${agentName}`;
+ const scheduleCreateUrl = `/api/proxy/api/graphs/${graphId}/schedules`;
+ const timeoutAt = Date.now() + 45000;
+ let lastFailure = "schedule request did not run";
+
+ while (Date.now() < timeoutAt) {
+ const createResponse = await this.page.request.post(scheduleCreateUrl, {
+ data: {
+ name: scheduleName,
+ graph_version: graphVersion,
+ cron: "0 10 * * *",
+ inputs: {},
+ credentials: {},
+ timezone: "UTC",
+ },
+ });
+
+ const createResponseBody = await createResponse.text();
+ if (createResponse.ok()) {
+ return;
+ }
+
+ lastFailure = `${createResponse.status()} ${createResponseBody}`;
+ await this.page.waitForTimeout(1000);
+ }
+
+ throw new Error(`schedule creation API should succeed: ${lastFailure}`);
+ }
+}
diff --git a/autogpt_platform/frontend/src/playwright/pages/copilot.page.ts b/autogpt_platform/frontend/src/playwright/pages/copilot.page.ts
new file mode 100644
index 0000000000..d67e20ef6e
--- /dev/null
+++ b/autogpt_platform/frontend/src/playwright/pages/copilot.page.ts
@@ -0,0 +1,44 @@
+import { expect, Locator, Page } from "@playwright/test";
+import { BasePage } from "./base.page";
+
+export class CopilotPage extends BasePage {
+ constructor(page: Page) {
+ super(page);
+ }
+
+ async open(sessionId?: string): Promise {
+ const url = sessionId ? `/copilot?sessionId=${sessionId}` : "/copilot";
+ await this.page.goto(url);
+ await expect(this.page).toHaveURL(/\/copilot/);
+ await this.dismissNotificationPrompt();
+ }
+
+ async dismissNotificationPrompt(): Promise {
+ // Notification permission prompt is optional — only shown on first visit
+ const notNowButton = this.page.getByRole("button", { name: "Not now" });
+ if (await notNowButton.isVisible({ timeout: 3000 })) {
+ await notNowButton.click();
+ }
+ }
+
+ async createSessionViaApi(): Promise {
+ const response = await this.page.request.post(
+ "/api/proxy/api/chat/sessions",
+ { data: null },
+ );
+ expect(response.ok()).toBeTruthy();
+
+ const session = await response.json();
+ const sessionId = session?.id;
+ expect(sessionId).toBeTruthy();
+ return sessionId as string;
+ }
+
+ getChatInput(): Locator {
+ return this.page.locator("#chat-input-session");
+ }
+
+ async waitForChatInput(): Promise {
+ await expect(this.getChatInput()).toBeVisible({ timeout: 15000 });
+ }
+}
diff --git a/autogpt_platform/frontend/src/tests/pages/header.page.ts b/autogpt_platform/frontend/src/playwright/pages/header.page.ts
similarity index 100%
rename from autogpt_platform/frontend/src/tests/pages/header.page.ts
rename to autogpt_platform/frontend/src/playwright/pages/header.page.ts
diff --git a/autogpt_platform/frontend/src/playwright/pages/library.page.ts b/autogpt_platform/frontend/src/playwright/pages/library.page.ts
new file mode 100644
index 0000000000..85c3f3978a
--- /dev/null
+++ b/autogpt_platform/frontend/src/playwright/pages/library.page.ts
@@ -0,0 +1,1342 @@
+import { expect, Locator, Page } from "@playwright/test";
+import { getSeededTestUser } from "../credentials/accounts";
+import { getSelectors } from "../utils/selectors";
+import { BasePage } from "./base.page";
+
+export interface Agent {
+ id: string;
+ name: string;
+ description: string;
+ imageUrl?: string;
+ seeRunsUrl: string;
+ openInBuilderUrl: string;
+}
+
+export class LibraryPage extends BasePage {
+ constructor(page: Page) {
+ super(page);
+ }
+
+ async isLoaded(): Promise {
+ console.log(`checking if library page is loaded`);
+ try {
+ await this.page.waitForLoadState("domcontentloaded", { timeout: 10_000 });
+
+ await this.page.waitForSelector('[data-testid="library-textbox"]', {
+ state: "visible",
+ timeout: 10_000,
+ });
+
+ console.log("Library page is loaded successfully");
+ return true;
+ } catch (error) {
+ console.log("Library page failed to load:", error);
+ return false;
+ }
+ }
+
+ async navigateToLibrary(): Promise {
+ await this.page.goto("/library");
+ await this.isLoaded();
+ }
+
+ async openSavedAgent(agentName: string): Promise {
+ await openSavedAgentInLibrary(this.page, agentName);
+ }
+
+ async waitForRunToComplete(timeout = 45000): Promise {
+ await waitForRunToComplete(this.page, timeout);
+ }
+
+ async getRunStatus(): Promise {
+ return getRunStatus(this.page);
+ }
+
+ async assertRunProducedOutput(timeout = 15000): Promise {
+ await assertRunProducedOutput(this.page, timeout);
+ }
+
+ async assertRunOutputValue(
+ outputName: string,
+ expectedValue: RegExp | string,
+ timeout = 15000,
+ ): Promise {
+ await assertRunOutputValue(this.page, outputName, expectedValue, timeout);
+ }
+
+ async assertFirstRunOutputValue(
+ expectedValue: RegExp | string,
+ timeout = 15000,
+ ): Promise {
+ await assertRunOutputContainsText(this.page, expectedValue, timeout);
+ }
+
+ async clickExportAgent(): Promise {
+ await clickExportAgent(this.page);
+ }
+
+ async getAgentCount(): Promise {
+ const { getId } = getSelectors(this.page);
+ const countText = await getId("agents-count").textContent();
+ const match = countText?.match(/^(\d+)/);
+ return match ? parseInt(match[1], 10) : 0;
+ }
+
+ async getAgentCountByListLength(): Promise {
+ const { getId } = getSelectors(this.page);
+ const agentCards = await getId("library-agent-card").all();
+ return agentCards.length;
+ }
+
+ async searchAgents(searchTerm: string): Promise {
+ console.log(`searching for agents with term: ${searchTerm}`);
+ const { getRole } = getSelectors(this.page);
+ const searchInput = getRole("textbox", "Search agents");
+ await searchInput.fill(searchTerm);
+ await expect(searchInput).toHaveValue(searchTerm);
+ }
+
+ async clearSearch(): Promise {
+ console.log(`clearing search`);
+ // Look for the clear button (X icon)
+ const clearButton = this.page.locator(".lucide.lucide-x");
+ const searchInput = this.page.getByRole("textbox", {
+ name: "Search agents",
+ });
+ if (await clearButton.isVisible()) {
+ await clearButton.click();
+ } else {
+ // If no clear button, clear the search input directly
+ await searchInput.fill("");
+ }
+ await expect(searchInput).toHaveValue("");
+ }
+
+ async selectSortOption(
+ page: Page,
+ sortOption: "Creation Date" | "Last Modified",
+ ): Promise {
+ const { getRole } = getSelectors(page);
+ await getRole("combobox").click();
+
+ await getRole("option", sortOption).click();
+ }
+
+ async getCurrentSortOption(): Promise {
+ console.log(`getting current sort option`);
+ try {
+ const sortCombobox = this.page.getByRole("combobox");
+ const currentOption = await sortCombobox.textContent();
+ return currentOption?.trim() || "";
+ } catch (error) {
+ console.error("Error getting current sort option:", error);
+ return "";
+ }
+ }
+
+ async openUploadDialog(): Promise {
+ console.log(`opening upload dialog`);
+ // Open the unified Import dialog first
+ await this.page.getByRole("button", { name: "Import" }).click();
+
+ // Wait for dialog to appear
+ await this.page.getByRole("dialog", { name: "Import" }).waitFor({
+ state: "visible",
+ timeout: 5_000,
+ });
+
+ // Click the "AutoGPT agent" tab
+ await this.page.getByRole("tab", { name: "AutoGPT agent" }).click();
+ }
+
+ async closeUploadDialog(): Promise {
+ await this.page.getByRole("button", { name: "Close" }).click();
+
+ await this.page.getByRole("dialog", { name: "Import" }).waitFor({
+ state: "hidden",
+ timeout: 5_000,
+ });
+ }
+
+ async isUploadDialogVisible(): Promise {
+ console.log(`checking if upload dialog is visible`);
+ try {
+ const dialog = this.page.getByRole("dialog", { name: "Import" });
+ return await dialog.isVisible();
+ } catch {
+ return false;
+ }
+ }
+
+ async fillUploadForm(agentName: string, description: string): Promise {
+ console.log(
+ `filling upload form with name: ${agentName}, description: ${description}`,
+ );
+
+ // Fill agent name
+ await this.page
+ .getByRole("textbox", { name: "Agent name" })
+ .fill(agentName);
+
+ // Fill description
+ await this.page
+ .getByRole("textbox", { name: "Agent description" })
+ .fill(description);
+ }
+
+ async isUploadButtonEnabled(): Promise {
+ console.log(`checking if upload button is enabled`);
+ try {
+ const uploadButton = this.page.getByRole("button", {
+ name: "Upload",
+ });
+ return await uploadButton.isEnabled();
+ } catch {
+ return false;
+ }
+ }
+
+ async getAgents(): Promise {
+ const { getId } = getSelectors(this.page);
+ const agents: Agent[] = [];
+
+ await getId("library-agent-card")
+ .first()
+ .waitFor({ state: "visible", timeout: 10_000 });
+ const agentCards = await getId("library-agent-card").all();
+
+ for (const card of agentCards) {
+ const name = await getId("library-agent-card-name", card).textContent();
+ const seeRunsLink = getId("library-agent-card-see-runs-link", card);
+ const openInBuilderLink = getId(
+ "library-agent-card-open-in-builder-link",
+ card,
+ );
+
+ const seeRunsUrl = await seeRunsLink.getAttribute("href");
+
+ // Check if the "Open in builder" link exists before getting its href
+ const openInBuilderLinkCount = await openInBuilderLink.count();
+ const openInBuilderUrl =
+ openInBuilderLinkCount > 0
+ ? await openInBuilderLink.getAttribute("href")
+ : null;
+
+ if (name && seeRunsUrl) {
+ const idMatch = seeRunsUrl.match(/\/library\/agents\/([^\/]+)/);
+ const id = idMatch ? idMatch[1] : "";
+
+ agents.push({
+ id,
+ name: name.trim(),
+ description: "", // Description is not currently rendered in the card
+ seeRunsUrl,
+ openInBuilderUrl: openInBuilderUrl || "",
+ });
+ }
+ }
+
+ console.log(`found ${agents.length} agents`);
+ return agents;
+ }
+
+ async clickAgent(agent: Agent): Promise {
+ const { getId } = getSelectors(this.page);
+ const nameElement = getId("library-agent-card-name").filter({
+ hasText: agent.name,
+ });
+ await nameElement.first().click();
+ }
+
+ async clickSeeRuns(agent: Agent): Promise {
+ console.log(`clicking see runs for agent: ${agent.name}`);
+
+ const { getId } = getSelectors(this.page);
+ const agentCard = getId("library-agent-card").filter({
+ hasText: agent.name,
+ });
+ const seeRunsLink = getId("library-agent-card-see-runs-link", agentCard);
+ await seeRunsLink.first().click();
+ }
+
+ async clickOpenInBuilder(agent: Agent): Promise {
+ console.log(`clicking open in builder for agent: ${agent.name}`);
+
+ const { getId } = getSelectors(this.page);
+ const agentCard = getId("library-agent-card").filter({
+ hasText: agent.name,
+ });
+ const builderLink = getId(
+ "library-agent-card-open-in-builder-link",
+ agentCard,
+ );
+ await builderLink.first().click();
+ }
+
+ async waitForAgentsToLoad(): Promise {
+ const { getId } = getSelectors(this.page);
+ await expect
+ .poll(
+ async () => {
+ const [agentCardVisible, agentsCountVisible] = await Promise.all([
+ getId("library-agent-card")
+ .first()
+ .isVisible()
+ .catch(() => false),
+ getId("agents-count")
+ .isVisible()
+ .catch(() => false),
+ ]);
+
+ return agentCardVisible || agentsCountVisible;
+ },
+ { timeout: 10_000 },
+ )
+ .toBe(true);
+ }
+
+ async getSearchValue(): Promise {
+ console.log(`getting search input value`);
+ try {
+ const searchInput = this.page.getByRole("textbox", {
+ name: "Search agents",
+ });
+ return await searchInput.inputValue();
+ } catch {
+ return "";
+ }
+ }
+
+ async hasNoAgentsMessage(): Promise {
+ const { getText } = getSelectors(this.page);
+ const noAgentsText = getText("0 agents");
+ return noAgentsText.isVisible();
+ }
+
+ async scrollToBottom(): Promise {
+ console.log(`scrolling to bottom to trigger pagination`);
+ await this.page.keyboard.press("End");
+ }
+
+ async scrollDown(): Promise {
+ console.log(`scrolling down to trigger pagination`);
+ await this.page.keyboard.press("PageDown");
+ }
+
+ // Returns true if more agents loaded, false if we're on the last page.
+ // Callers must distinguish these cases so a broken pagination pipeline
+ // doesn't quietly look like "we reached the end".
+ async scrollToLoadMore(): Promise {
+ const initialCount = await this.getAgentCountByListLength();
+ console.log(`Initial agent count (DOM cards): ${initialCount}`);
+
+ await this.scrollToBottom();
+
+ try {
+ await this.page.waitForFunction(
+ (prevCount) =>
+ document.querySelectorAll('[data-testid="library-agent-card"]')
+ .length > prevCount,
+ initialCount,
+ { timeout: 10000 },
+ );
+ return true;
+ } catch {
+ // No new cards — caller should verify this is actually the last page
+ // (e.g., by comparing against `getAgentCount()`), not a broken fetch.
+ return false;
+ }
+ }
+
+ async testPagination(): Promise<{
+ initialCount: number;
+ finalCount: number;
+ hasMore: boolean;
+ }> {
+ const initialCount = await this.getAgentCountByListLength();
+ await this.scrollToLoadMore();
+ const finalCount = await this.getAgentCountByListLength();
+
+ const hasMore = finalCount > initialCount;
+ return {
+ initialCount,
+ finalCount,
+ hasMore,
+ };
+ }
+
+ async getAgentsWithPagination(): Promise {
+ console.log(`getting all agents with pagination`);
+
+ let allAgents: Agent[] = [];
+ let previousCount = 0;
+ let currentCount = 0;
+ const maxAttempts = 5; // Prevent infinite loop
+ let attempts = 0;
+
+ do {
+ previousCount = currentCount;
+
+ // Get current agents
+ const currentAgents = await this.getAgents();
+ allAgents = currentAgents;
+ currentCount = currentAgents.length;
+
+ console.log(`Attempt ${attempts + 1}: Found ${currentCount} agents`);
+
+ // Try to load more by scrolling
+ await this.scrollToLoadMore();
+
+ attempts++;
+ } while (currentCount > previousCount && attempts < maxAttempts);
+
+ console.log(`Total agents found with pagination: ${allAgents.length}`);
+ return allAgents;
+ }
+
+ async waitForPaginationLoad(): Promise {
+ // Wait until the agent count header stops changing. Poll every 500ms
+ // and declare stable after two consecutive equal reads, capped at 10s.
+ // The previous implementation had no delay between reads and so hit
+ // "stable" instantly — effectively a no-op.
+ const deadline = Date.now() + 10000;
+ let previousCount = -1;
+ let stableChecks = 0;
+
+ while (Date.now() < deadline && stableChecks < 2) {
+ const currentCount = await this.getAgentCount();
+ if (currentCount === previousCount) {
+ stableChecks += 1;
+ } else {
+ stableChecks = 0;
+ previousCount = currentCount;
+ }
+ await this.page.waitForTimeout(500);
+ }
+ }
+
+ async scrollAndWaitForNewAgents(): Promise {
+ const initialCount = await this.getAgentCountByListLength();
+
+ await this.scrollDown();
+
+ await this.waitForPaginationLoad();
+
+ const finalCount = await this.getAgentCountByListLength();
+ const newAgentsLoaded = finalCount - initialCount;
+
+ console.log(
+ `Loaded ${newAgentsLoaded} new agents (${initialCount} -> ${finalCount})`,
+ );
+
+ return newAgentsLoaded;
+ }
+
+ async isPaginationWorking(): Promise {
+ const newAgentsLoaded = await this.scrollAndWaitForNewAgents();
+ return newAgentsLoaded > 0;
+ }
+}
+
+// Locator functions
+export function getLibraryTab(page: Page): Locator {
+ return page.locator('a[href="/library"]');
+}
+
+export function getAgentCards(page: Page): Locator {
+ return page.getByTestId("library-agent-card");
+}
+
+export function getNewRunButton(page: Page): Locator {
+ return page.getByRole("button", { name: "New run" });
+}
+
+export function getAgentTitle(page: Page): Locator {
+ return page.locator("h1").first();
+}
+
+// Action functions
+export async function navigateToLibrary(page: Page): Promise {
+ await getLibraryTab(page).click();
+ await page.waitForURL(/.*\/library/);
+}
+
+export async function clickFirstAgent(page: Page): Promise {
+ const firstAgent = getAgentCards(page).first();
+ await firstAgent.click();
+}
+
+export async function navigateToAgentByName(
+ page: Page,
+ agentName: string,
+): Promise {
+ const agentCard = getAgentCards(page).filter({ hasText: agentName }).first();
+ // Wait for the agent card to be visible before clicking
+ // This handles async loading of agents after page navigation
+ await agentCard.waitFor({ state: "visible", timeout: 15000 });
+ // Click the link inside the card to navigate reliably through
+ // the motion.div + draggable wrapper layers.
+ const link = agentCard.locator('a[href*="/library/agents/"]').first();
+ await link.click();
+}
+
+export async function clickRunButton(page: Page): Promise {
+ const setupTaskButton = page.getByRole("button", {
+ name: /Setup your task/i,
+ });
+ const newTaskButton = page.getByRole("button", { name: /^New task$/i });
+ const rerunTaskButton = page.getByRole("button", { name: /Rerun task/i });
+ const runNowButton = page.getByRole("button", { name: /Run now/i });
+ const actionButtons = [
+ setupTaskButton,
+ newTaskButton,
+ rerunTaskButton,
+ runNowButton,
+ ];
+
+ await page.waitForLoadState("domcontentloaded");
+ await page.waitForLoadState("networkidle").catch(() => undefined);
+
+ const timeoutAt = Date.now() + 20000;
+
+ while (Date.now() < timeoutAt) {
+ if (
+ await setupTaskButton
+ .first()
+ .isVisible()
+ .catch(() => false)
+ ) {
+ const clicked = await clickActionButton(setupTaskButton.first());
+ if (!clicked) {
+ await page.waitForTimeout(250);
+ continue;
+ }
+
+ const runDialog = await waitForRunDialog(page);
+ await fillVisibleTaskInputs(runDialog);
+ await clickStartOrSimulateTask(page, runDialog);
+ return;
+ }
+
+ if (
+ await newTaskButton
+ .first()
+ .isVisible()
+ .catch(() => false)
+ ) {
+ const clicked = await clickActionButton(newTaskButton.first());
+ if (!clicked) {
+ await page.waitForTimeout(250);
+ continue;
+ }
+
+ const runDialog = await waitForRunDialog(page);
+ await fillVisibleTaskInputs(runDialog);
+ await clickStartOrSimulateTask(page, runDialog);
+ return;
+ }
+
+ if (
+ await rerunTaskButton
+ .first()
+ .isVisible()
+ .catch(() => false)
+ ) {
+ const clicked = await clickActionButton(rerunTaskButton.first());
+ if (!clicked) {
+ await page.waitForTimeout(250);
+ continue;
+ }
+
+ return;
+ }
+
+ if (
+ await runNowButton
+ .first()
+ .isVisible()
+ .catch(() => false)
+ ) {
+ const clicked = await clickActionButton(runNowButton.first());
+ if (!clicked) {
+ await page.waitForTimeout(250);
+ continue;
+ }
+
+ return;
+ }
+
+ await page.waitForTimeout(250);
+ }
+
+ const visibleButtons = await page
+ .getByRole("button")
+ .evaluateAll((elements) =>
+ elements
+ .filter((element) => {
+ const htmlElement = element as HTMLElement;
+ const rect = htmlElement.getBoundingClientRect();
+ return rect.width > 0 && rect.height > 0;
+ })
+ .map((element) => element.textContent?.trim())
+ .filter(Boolean),
+ );
+
+ throw new Error(
+ `Could not find run/start task button. URL: ${page.url()}. Visible buttons: ${visibleButtons.join(", ") || "none"}. Expected one of: ${actionButtons
+ .map((button) => button.toString())
+ .join(", ")}`,
+ );
+}
+
+async function clickActionButton(button: Locator): Promise {
+ try {
+ await expect(button).toBeVisible({ timeout: 2000 });
+ await expect(button).toBeEnabled({ timeout: 2000 });
+ await button.click({ timeout: 3000 });
+ return true;
+ } catch {
+ return false;
+ }
+}
+
+async function waitForRunDialog(page: Page): Promise {
+ const runDialog = page
+ .locator("[data-dialog-content]")
+ .filter({
+ has: page.getByRole("button", { name: /^Start Task$/i }),
+ })
+ .last();
+ await expect(runDialog).toBeVisible({ timeout: 15000 });
+ return runDialog;
+}
+
+async function dismissRunSafetyPopup(page: Page): Promise {
+ const safetyPopup = page
+ .locator("[data-dialog-content]")
+ .filter({
+ has: page.getByText("Safety Checks Enabled", { exact: true }),
+ })
+ .last();
+
+ if (!(await safetyPopup.isVisible({ timeout: 2000 }).catch(() => false))) {
+ return;
+ }
+
+ await safetyPopup.getByRole("button", { name: /^Got it$/i }).click();
+ await expect(safetyPopup).toBeHidden({ timeout: 10000 });
+}
+
+async function clickStartOrSimulateTask(
+ page: Page,
+ runDialog: Locator,
+): Promise {
+ const startBtn = runDialog.getByRole("button", { name: /^Start Task$/i });
+ // Happy-path tests must exercise a real run — do NOT fall back to the
+ // "Simulate" button if Start fails, because a broken Start code path is
+ // exactly the regression these tests exist to catch.
+ await expect(startBtn).toBeVisible({ timeout: 10000 });
+ await expect(startBtn).toBeEnabled({ timeout: 10000 });
+ await startBtn.click();
+ await dismissRunSafetyPopup(page);
+
+ await expect
+ .poll(
+ () => {
+ const currentUrl = new URL(page.url());
+ return (
+ currentUrl.searchParams.get("activeTab") === "runs" &&
+ currentUrl.searchParams.get("activeItem") !== null
+ );
+ },
+ {
+ timeout: 15000,
+ message:
+ "Start Task click did not navigate to a run detail (?activeTab=runs&activeItem=...)",
+ },
+ )
+ .toBe(true);
+}
+
+async function fillVisibleTaskInputs(container: Page | Locator): Promise {
+ const seededEmail = getSeededTestUser("smokeMarketplace").email;
+ const inputs = container.locator(
+ 'input:visible:not([type="hidden"]):not([type="file"]):not([disabled]), textarea:visible:not([disabled])',
+ );
+ const inputCount = await inputs.count();
+
+ for (let index = 0; index < inputCount; index += 1) {
+ const input = inputs.nth(index);
+ const currentValue = await input.inputValue().catch(() => "");
+ if (currentValue.trim()) {
+ continue;
+ }
+
+ const type = (await input.getAttribute("type"))?.toLowerCase() ?? "text";
+ const inputMetadata = await input.evaluate((element) => {
+ const formField = element as HTMLInputElement | HTMLTextAreaElement;
+ const closestLabel = formField.closest("label")?.textContent ?? "";
+ const forLabel = formField.id
+ ? (document.querySelector(`label[for="${CSS.escape(formField.id)}"]`)
+ ?.textContent ?? "")
+ : "";
+
+ return {
+ placeholder: formField.getAttribute("placeholder") ?? "",
+ ariaLabel: formField.getAttribute("aria-label") ?? "",
+ name: formField.getAttribute("name") ?? "",
+ labelText: `${closestLabel} ${forLabel}`.trim(),
+ };
+ });
+ const fieldDescriptor = [
+ inputMetadata.placeholder,
+ inputMetadata.ariaLabel,
+ inputMetadata.name,
+ inputMetadata.labelText,
+ ]
+ .join(" ")
+ .toLowerCase();
+
+ if (type === "checkbox" || type === "radio") {
+ continue;
+ }
+
+ const value =
+ type === "email" || fieldDescriptor.includes("email")
+ ? seededEmail
+ : type === "number" ||
+ /\b(a|b)\b/.test(fieldDescriptor) ||
+ fieldDescriptor.includes("number")
+ ? "1"
+ : "e2e-input";
+
+ await input.fill(value).catch(() => {});
+ }
+}
+
+export async function clickNewRunButton(page: Page): Promise {
+ await getNewRunButton(page).click();
+}
+
+export async function runAgent(page: Page): Promise {
+ await clickRunButton(page);
+}
+
+export async function waitForAgentPageLoad(
+ page: Page,
+ agentName?: string,
+): Promise {
+ await page.waitForURL(/.*\/library\/agents\/[^/]+/);
+ // Wait for the primary content area to be present so the page has settled
+ // into its final state (empty view vs sidebar view)
+ await page.waitForLoadState("domcontentloaded");
+
+ // Transient "Something went wrong — All connection attempts failed" error
+ // boundary appears when the library agent page loads before the backend
+ // has indexed a newly-cloned agent (race between marketplace "Add to
+ // Library" and backend availability). Click "Try Again" and re-settle.
+ const errorHeading = page.getByText("Something went wrong", {
+ exact: false,
+ });
+ let errorResolved = false;
+ for (let attempt = 0; attempt < 3; attempt += 1) {
+ if (!(await errorHeading.isVisible({ timeout: 300 }).catch(() => false))) {
+ errorResolved = true;
+ break;
+ }
+ const tryAgain = page.getByRole("button", { name: "Try Again" });
+ if (await tryAgain.isVisible({ timeout: 500 }).catch(() => false)) {
+ await tryAgain.click();
+ } else {
+ await page.reload();
+ }
+ await page.waitForLoadState("domcontentloaded");
+ }
+
+ if (!errorResolved) {
+ errorResolved = !(await errorHeading
+ .isVisible({ timeout: 300 })
+ .catch(() => false));
+ }
+
+ if (!errorResolved) {
+ throw new Error(
+ "Library agent page remained on the connection-failure screen after 3 retries",
+ );
+ }
+
+ await waitForAgentDetailShell(page, agentName);
+}
+
+async function waitForLibraryListToLeave(page: Page): Promise {
+ const librarySearch = page.getByTestId("library-textbox");
+ await expect
+ .poll(
+ async () => {
+ const count = await librarySearch.count();
+ if (count === 0) {
+ return "gone";
+ }
+
+ if (
+ !(await librarySearch
+ .first()
+ .isVisible()
+ .catch(() => false))
+ ) {
+ return "gone";
+ }
+
+ return "visible";
+ },
+ { timeout: 15000 },
+ )
+ .toBe("gone");
+}
+
+async function getVisibleAgentDetailSurface(page: Page): Promise {
+ const visibleSurfaces: Array<[string, Locator]> = [
+ [
+ "about-agent",
+ page.getByText("About this agent", { exact: true }).first(),
+ ],
+ [
+ "setup-task",
+ page.getByRole("button", { name: /^Setup your task$/i }).first(),
+ ],
+ ["new-task", page.getByRole("button", { name: /^New task$/i }).first()],
+ ["scheduled-tab", page.getByRole("tab", { name: /^Scheduled$/i }).first()],
+ ];
+
+ for (const [surface, locator] of visibleSurfaces) {
+ if (await locator.isVisible().catch(() => false)) {
+ return surface;
+ }
+ }
+
+ return "pending";
+}
+
+async function waitForAgentDetailShell(
+ page: Page,
+ agentName?: string,
+): Promise {
+ await waitForLibraryListToLeave(page);
+
+ await expect(
+ page.getByRole("link", { name: "My Library" }).first(),
+ ).toBeVisible({
+ timeout: 15000,
+ });
+
+ if (agentName) {
+ await expect(
+ page
+ .locator(`a[href*="/library/agents/"]`)
+ .filter({ hasText: agentName })
+ .first(),
+ ).toBeVisible({ timeout: 15000 });
+ }
+
+ await expect
+ .poll(() => getVisibleAgentDetailSurface(page), { timeout: 15000 })
+ .not.toBe("pending");
+}
+
+export async function getAgentName(page: Page): Promise {
+ return (await getAgentTitle(page).textContent()) || "";
+}
+
+export async function isLoaded(page: Page): Promise {
+ return await page.locator("h1").isVisible();
+}
+
+const SUCCESS_RUN_STATUS = "completed";
+const FAILURE_RUN_STATUSES = new Set(["failed", "terminated", "incomplete"]);
+const RUN_ERROR_RECOVERY_GRACE_PERIOD_MS = 1500;
+const RUN_ERROR_RECOVERY_ATTEMPTS = 2;
+
+/**
+ * Assert that a completed run actually produced output.
+ *
+ * The Library run-detail Output panel renders "No output from this run." when
+ * the run object has no `outputs` field. There's a brief window after the run
+ * reaches "completed" status where the run object is loaded without outputs,
+ * then outputs arrive and the panel re-renders. We poll for up to `timeout`
+ * ms waiting for the "No output" placeholder to GO AWAY before concluding
+ * the run genuinely produced nothing.
+ *
+ * This catches the "agent runs but produces nothing" failure mode
+ * (disconnected edges, broken graph, runtime crash before any output node
+ * fired) — the exact regression that ACCEPTED_RUN_STATUSES previously hid.
+ */
+export async function assertRunProducedOutput(
+ page: Page,
+ timeout = 15000,
+): Promise {
+ await openRunOutputTab(page);
+
+ // A completed run must surface output on the CURRENT render without a
+ // page reload. Reloading to "rule out stale cache" would mask a real
+ // user-visible regression where the frontend only shows output after a
+ // manual refresh.
+ const noOutput = page.getByText("No output from this run.", { exact: true });
+ await expect(noOutput, {
+ message:
+ 'run completed but produced no output ("No output from this run." still shown) — broken graph, missing output node, or stale React Query cache',
+ }).toBeHidden({ timeout });
+}
+
+function escapeRegex(text: string): string {
+ return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
+}
+
+async function openRunOutputTab(page: Page): Promise {
+ const outputTab = page.getByRole("tab", { name: /^Output$/i }).first();
+ if (await outputTab.isVisible().catch(() => false)) {
+ await outputTab.click();
+ return;
+ }
+
+ const outputButton = page.getByRole("button", { name: /^Output$/i }).first();
+ if (await outputButton.isVisible().catch(() => false)) {
+ await outputButton.click();
+ }
+}
+
+export async function assertRunOutputValue(
+ page: Page,
+ outputName: string,
+ expectedValue: RegExp | string,
+ timeout = 15000,
+): Promise {
+ await openRunOutputTab(page);
+
+ const outputLabel = page.locator("p.capitalize:visible").filter({
+ hasText: new RegExp(`^${escapeRegex(outputName)}$`, "i"),
+ });
+
+ await expect(
+ outputLabel,
+ `run output should include output key "${outputName}"`,
+ ).toBeVisible({ timeout });
+
+ const outputValue = outputLabel.locator("xpath=following-sibling::*[1]");
+ if (expectedValue instanceof RegExp) {
+ await expect(
+ outputValue,
+ `run output value for "${outputName}" should match ${expectedValue.toString()}`,
+ ).toHaveText(expectedValue, { timeout });
+ return;
+ }
+
+ await expect(
+ outputValue,
+ `run output value for "${outputName}" should be "${expectedValue}"`,
+ ).toHaveText(expectedValue, { timeout });
+}
+
+export async function assertFirstRunOutputValue(
+ page: Page,
+ expectedValue: RegExp | string,
+ timeout = 15000,
+): Promise {
+ await assertRunOutputContainsText(page, expectedValue, timeout);
+}
+
+export async function assertRunOutputContainsText(
+ page: Page,
+ expectedValue: RegExp | string,
+ timeout = 15000,
+): Promise {
+ await openRunOutputTab(page);
+
+ const outputCard = page
+ .locator("div")
+ .filter({
+ has: page.getByRole("button", { name: "Copy all text outputs" }),
+ })
+ .first();
+ await expect(outputCard, "run output card should be visible").toBeVisible({
+ timeout,
+ });
+
+ if (expectedValue instanceof RegExp) {
+ await expect(
+ outputCard.getByText(expectedValue).first(),
+ `run output should contain text matching ${expectedValue.toString()}`,
+ ).toBeVisible({ timeout });
+ return;
+ }
+
+ await expect(
+ outputCard.getByText(expectedValue, { exact: true }).first(),
+ `run output should contain "${expectedValue}"`,
+ ).toBeVisible({ timeout });
+}
+
+export async function waitForRunToComplete(
+ page: Page,
+ timeout = 45000,
+): Promise {
+ const start = Date.now();
+ let lastStatus = "unknown";
+ let runErrorDetectedAt: number | null = null;
+ let recoveryAttempts = 0;
+ while (Date.now() - start < timeout) {
+ lastStatus = await getRunStatus(page);
+ if (lastStatus === SUCCESS_RUN_STATUS) {
+ return;
+ }
+ if (lastStatus === "error") {
+ runErrorDetectedAt ??= Date.now();
+ if (
+ Date.now() - runErrorDetectedAt >=
+ RUN_ERROR_RECOVERY_GRACE_PERIOD_MS
+ ) {
+ if (recoveryAttempts >= RUN_ERROR_RECOVERY_ATTEMPTS) {
+ throw new Error(`Run reached terminal failure state "${lastStatus}"`);
+ }
+ recoveryAttempts += 1;
+ runErrorDetectedAt = null;
+ await page.reload();
+ await waitForAgentPageLoad(page);
+ continue;
+ }
+ } else {
+ runErrorDetectedAt = null;
+ }
+ if (FAILURE_RUN_STATUSES.has(lastStatus)) {
+ throw new Error(`Run reached terminal failure state "${lastStatus}"`);
+ }
+ await page.waitForTimeout(250);
+ }
+ throw new Error(
+ `waitForRunToComplete timed out after ${timeout}ms — last status was "${lastStatus}" (expected "${SUCCESS_RUN_STATUS}")`,
+ );
+}
+
+export function getActiveItemId(page: Page): string | null {
+ return new URL(page.url()).searchParams.get("activeItem");
+}
+
+export async function dismissFeedbackDialog(page: Page): Promise {
+ const feedbackDialog = page.getByRole("dialog", {
+ name: "We'd love your feedback",
+ });
+ // Dialog is genuinely optional — it only appears on some run completions.
+ // Give it a realistic window to animate in; 500ms races the dialog
+ // transition and causes later clicks to land on it instead of the button
+ // behind it.
+ if (!(await feedbackDialog.isVisible({ timeout: 3000 }).catch(() => false))) {
+ return;
+ }
+
+ const cancelButton = feedbackDialog.getByRole("button", { name: "Cancel" });
+ if (await cancelButton.isVisible()) {
+ await cancelButton.click();
+ await expect(feedbackDialog).toBeHidden({ timeout: 15000 });
+ return;
+ }
+
+ await feedbackDialog.getByRole("button", { name: "Close" }).click();
+ await expect(feedbackDialog).toBeHidden({ timeout: 15000 });
+}
+
+export async function importAgentFromFile(
+ page: Page,
+ filePath: string,
+ agentName: string,
+ description: string = "PR E2E library coverage",
+): Promise<{ libraryPage: LibraryPage; importedAgent: Agent }> {
+ const libraryPage = new LibraryPage(page);
+ const importDialog = page.getByRole("dialog", { name: "Import" });
+
+ await page.goto("/library");
+ await libraryPage.openUploadDialog();
+ await libraryPage.fillUploadForm(agentName, description);
+
+ const fileInput = importDialog.locator('input[type="file"]');
+ await fileInput.setInputFiles(filePath);
+ const uploadButton = importDialog.getByRole("button", { name: "Upload" });
+ await expect(uploadButton).toBeEnabled({
+ timeout: 10000,
+ });
+ await uploadButton.click();
+ const uploadingButton = importDialog.getByRole("button", {
+ name: /Uploading\.\.\./i,
+ });
+ const sawUploadingState = await uploadingButton
+ .waitFor({ state: "visible", timeout: 2000 })
+ .then(() => true)
+ .catch(() => false);
+ if (sawUploadingState) {
+ await expect
+ .poll(
+ async () => {
+ if (/\/build/.test(page.url())) {
+ return "build";
+ }
+ if (!(await uploadingButton.isVisible().catch(() => false))) {
+ return "gone";
+ }
+ return (await uploadingButton.isDisabled().catch(() => false))
+ ? "disabled"
+ : "enabled";
+ },
+ {
+ timeout: 5000,
+ message:
+ 'upload button should either stay disabled while "Uploading..." is visible or disappear because navigation already started',
+ },
+ )
+ .not.toBe("enabled");
+ }
+
+ // Upload → backend creates the graph → router pushes /build?flowID=...
+ // This pipeline includes file parsing plus a backend graph creation call.
+ // On a cold stack it can take longer than a normal UI transition, so poll
+ // for the real terminal states: builder navigation or an explicit error.
+ await expect
+ .poll(
+ async () => {
+ if (/\/build/.test(page.url())) {
+ return "build";
+ }
+
+ const uploadFailed = await page
+ .getByText("Error Uploading agent")
+ .isVisible()
+ .catch(() => false);
+ if (uploadFailed) {
+ return "failed";
+ }
+
+ return "pending";
+ },
+ {
+ timeout: 60000,
+ message:
+ "agent import should either navigate to /build or surface an explicit upload error toast",
+ },
+ )
+ .toBe("build");
+ await expect(page).toHaveURL(/\/build/, { timeout: 15000 });
+
+ // Import should produce a real graph, not an empty canvas. Lazy-import
+ // BuildPage locally to avoid a circular dependency between the two
+ // page-object modules.
+ const { BuildPage } = await import("./build.page");
+ const importedBuildPage = new BuildPage(page);
+ await importedBuildPage.waitForNodeOnCanvas();
+ const importedNodeCount = await importedBuildPage.getNodeCount();
+ expect(
+ importedNodeCount,
+ "imported agent must render at least one node on canvas",
+ ).toBeGreaterThan(0);
+
+ await page.goto("/library");
+ await libraryPage.searchAgents(agentName);
+ await libraryPage.waitForAgentsToLoad();
+
+ // Look up the specific imported card directly rather than calling
+ // getAgents() in a loop. getAgents() iterates every visible card and
+ // reads hrefs via `.getAttribute`, which deadlocks if the library list
+ // re-renders mid-iteration (previously caused this test to hang 120s on
+ // the 8th card). A filter-based lookup on the agent name is both faster
+ // and immune to list churn.
+ const { getId } = getSelectors(page);
+ const importedCard = getId("library-agent-card")
+ .filter({ hasText: agentName })
+ .first();
+ await expect(
+ importedCard,
+ `imported agent card "${agentName}" must appear in the library search results`,
+ ).toBeVisible({ timeout: 15000 });
+
+ const seeRunsLink = getId("library-agent-card-see-runs-link", importedCard);
+ const seeRunsUrl = (await seeRunsLink.getAttribute("href")) ?? "";
+ const openInBuilderLink = getId(
+ "library-agent-card-open-in-builder-link",
+ importedCard,
+ );
+ const openInBuilderUrl =
+ (await openInBuilderLink.count()) > 0
+ ? ((await openInBuilderLink.getAttribute("href")) ?? "")
+ : "";
+
+ const idMatch = seeRunsUrl.match(/\/library\/agents\/([^/]+)/);
+ const importedAgent: Agent = {
+ id: idMatch ? idMatch[1] : "",
+ name:
+ (
+ await getId("library-agent-card-name", importedCard).textContent()
+ )?.trim() ?? agentName,
+ description: "",
+ seeRunsUrl,
+ openInBuilderUrl,
+ };
+
+ expect(
+ importedAgent.name,
+ "imported agent name should contain the requested name",
+ ).toContain(agentName);
+
+ return { libraryPage, importedAgent };
+}
+
+export async function openSavedAgentInLibrary(
+ page: Page,
+ agentName: string,
+): Promise {
+ const libraryPage = new LibraryPage(page);
+
+ await page.goto("/library");
+ await libraryPage.waitForAgentsToLoad();
+ await libraryPage.searchAgents(agentName);
+ await libraryPage.waitForAgentsToLoad();
+ await navigateToAgentByName(page, agentName);
+ await waitForAgentPageLoad(page, agentName);
+}
+
+async function waitForExportActionSurface(
+ page: Page,
+): Promise<"direct" | "menu"> {
+ await expect
+ .poll(
+ async () => {
+ if (
+ await getFirstVisibleLocator(page, "button", "Export agent to file")
+ ) {
+ return "direct";
+ }
+
+ if (await getFirstVisibleLocator(page, "button", "More actions")) {
+ return "menu";
+ }
+
+ return "pending";
+ },
+ { timeout: 30000 },
+ )
+ .not.toBe("pending");
+
+ if (await getFirstVisibleLocator(page, "button", "Export agent to file")) {
+ return "direct";
+ }
+
+ return "menu";
+}
+
+async function getFirstVisibleLocator(
+ page: Page,
+ role: "button" | "menuitem",
+ name: string,
+): Promise {
+ const locator = page.getByRole(role, { name });
+ const count = await locator.count();
+
+ for (let index = 0; index < count; index += 1) {
+ const candidate = locator.nth(index);
+ if (await candidate.isVisible().catch(() => false)) {
+ return candidate;
+ }
+ }
+
+ return null;
+}
+
+export async function clickExportAgent(page: Page): Promise {
+ const exportSurface = await waitForExportActionSurface(page);
+
+ if (exportSurface === "direct") {
+ const directExportButton = await getFirstVisibleLocator(
+ page,
+ "button",
+ "Export agent to file",
+ );
+ if (!directExportButton) {
+ throw new Error(
+ "Export button was not visible after export surface resolved",
+ );
+ }
+
+ await directExportButton.click({ timeout: 15000 });
+ return;
+ }
+
+ const moreActionsButtons = page.getByRole("button", { name: "More actions" });
+ const moreActionsCount = await moreActionsButtons.count();
+
+ for (let index = 0; index < moreActionsCount; index += 1) {
+ const moreActionsButton = moreActionsButtons.nth(index);
+
+ if (!(await moreActionsButton.isVisible().catch(() => false))) {
+ continue;
+ }
+
+ await moreActionsButton.click({ timeout: 15000 });
+
+ const exportMenuItem = await getFirstVisibleLocator(
+ page,
+ "menuitem",
+ "Export agent to file",
+ );
+ if (exportMenuItem) {
+ await exportMenuItem.click({ timeout: 15000 });
+ return;
+ }
+
+ await page.keyboard.press("Escape").catch(() => {});
+ }
+
+ throw new Error(
+ "Export action was not available from any visible More actions menu",
+ );
+}
+
+// The run status is rendered by RunStatusBadge as lowercase text inside a
+// `.capitalize` element (uppercased via CSS). Scoping to that class prevents
+// false positives from free-text occurrences of words like "completed"
+// elsewhere on the page (filter chips, tooltips, etc.).
+const RUN_STATUS_WORDS = [
+ "completed",
+ "failed",
+ "terminated",
+ "incomplete",
+ "queued",
+ "review",
+ "running",
+] as const;
+
+export async function getRunStatus(page: Page): Promise {
+ // 1. Detect React error boundary first — fast loud failure if the page
+ // crashed mid-run, instead of polling until timeout.
+ const errorBoundary = page.getByText(
+ /Something went wrong|We had the following error|Application error/i,
+ );
+ if (
+ await errorBoundary
+ .first()
+ .isVisible({ timeout: 200 })
+ .catch(() => false)
+ ) {
+ return "error";
+ }
+
+ // 2. Read the status from the scoped RunStatusBadge element. This is the
+ // only source of truth — no free-text matching across the whole page,
+ // no spinner heuristics that confuse a skeleton loader with a live run.
+ const badges = page.locator(".capitalize");
+ const badgeCount = await badges.count().catch(() => 0);
+ for (let i = 0; i < badgeCount; i += 1) {
+ const badge = badges.nth(i);
+ if (!(await badge.isVisible().catch(() => false))) continue;
+ const text = ((await badge.textContent()) ?? "").trim().toLowerCase();
+ if ((RUN_STATUS_WORDS as readonly string[]).includes(text)) {
+ return text;
+ }
+ }
+
+ return "unknown";
+}
diff --git a/autogpt_platform/frontend/src/playwright/pages/login.page.ts b/autogpt_platform/frontend/src/playwright/pages/login.page.ts
new file mode 100644
index 0000000000..e5aab2d678
--- /dev/null
+++ b/autogpt_platform/frontend/src/playwright/pages/login.page.ts
@@ -0,0 +1,123 @@
+import { Page } from "@playwright/test";
+import {
+ getSeededTestUser,
+ type SeededTestAccountKey,
+} from "../credentials/accounts";
+import { skipOnboardingIfPresent } from "../utils/onboarding";
+
+export class LoginPage {
+ constructor(private page: Page) {}
+
+ async goto() {
+ await this.page.goto("/login");
+ }
+
+ async loginAsSeededUser(userKey: SeededTestAccountKey): Promise {
+ const user = getSeededTestUser(userKey);
+ await this.page.goto("/login");
+ await this.login(user.email, user.password);
+ }
+
+ async login(email: string, password: string) {
+ console.log(`ℹ️ Attempting login on ${this.page.url()} with`, {
+ email,
+ password,
+ });
+
+ // Wait for the form to be ready
+ await this.page.waitForSelector("form", { state: "visible" });
+
+ // Fill email using input selector instead of label
+ const emailInput = this.page.locator('input[type="email"]');
+ await emailInput.waitFor({ state: "visible" });
+ await emailInput.fill(email);
+
+ // Fill password using input selector instead of label
+ const passwordInput = this.page.locator('input[type="password"]');
+ await passwordInput.waitFor({ state: "visible" });
+ await passwordInput.fill(password);
+
+ // Wait for the button to be ready
+ const loginButton = this.page.getByRole("button", {
+ name: "Login",
+ exact: true,
+ });
+ await loginButton.waitFor({ state: "visible" });
+
+ // Attach navigation logger for debug purposes
+ this.page.once("load", (page) =>
+ console.log(`ℹ️ Now at URL: ${page.url()}`),
+ );
+
+ const hasReachedPostLoginRoute = () =>
+ this.page.waitForFunction(
+ () => {
+ const pathname = window.location.pathname;
+ return /^\/(marketplace|onboarding(\/.*)?|library|copilot)$/.test(
+ pathname,
+ );
+ },
+ { timeout: 15_000 },
+ );
+
+ console.log(`🖱️ Clicking login button...`);
+ for (let attempt = 0; attempt < 2; attempt += 1) {
+ await loginButton.click();
+
+ console.log("⏳ Waiting for navigation away from /login ...");
+ try {
+ await hasReachedPostLoginRoute();
+ break;
+ } catch (reason) {
+ const currentPathname = new URL(this.page.url()).pathname;
+ if (attempt === 1 || currentPathname !== "/login") {
+ console.error(
+ `🚨 Navigation away from /login timed out (current URL: ${this.page.url()}):`,
+ reason,
+ );
+ throw reason;
+ }
+ }
+ }
+
+ console.log(`⌛ Post-login redirected to ${this.page.url()}`);
+
+ await this.page.waitForLoadState("load", { timeout: 10_000 });
+
+ // If redirected to onboarding, complete it via API so tests can proceed
+ await skipOnboardingIfPresent(this.page, "/marketplace");
+
+ console.log("➡️ Navigating to /marketplace ...");
+ await this.page.goto("/marketplace", { timeout: 20_000 });
+ console.log("✅ Login process complete");
+
+ // If Wallet popover auto-opens, close it to avoid blocking account menu interactions.
+ // The popover is genuinely optional — only appears on some accounts/environments.
+ const walletPanel = this.page.getByText("Your credits").first();
+ const walletPanelVisible = await walletPanel
+ .waitFor({ state: "visible", timeout: 2500 })
+ .then(() => true)
+ .catch(() => false);
+ if (walletPanelVisible) {
+ const closeWalletButton = this.page.getByRole("button", {
+ name: /Close wallet/i,
+ });
+ const closeWalletButtonVisible = await closeWalletButton
+ .waitFor({ state: "visible", timeout: 1000 })
+ .then(() => true)
+ .catch(() => false);
+ if (closeWalletButtonVisible) {
+ await closeWalletButton.click();
+ } else {
+ await this.page.keyboard.press("Escape");
+ }
+ const walletStillVisible = await walletPanel
+ .waitFor({ state: "hidden", timeout: 3000 })
+ .then(() => false)
+ .catch(() => true);
+ if (walletStillVisible) {
+ await this.page.mouse.click(5, 5);
+ }
+ }
+ }
+}
diff --git a/autogpt_platform/frontend/src/playwright/pages/marketplace.page.ts b/autogpt_platform/frontend/src/playwright/pages/marketplace.page.ts
new file mode 100644
index 0000000000..b0d334449f
--- /dev/null
+++ b/autogpt_platform/frontend/src/playwright/pages/marketplace.page.ts
@@ -0,0 +1,294 @@
+import { expect, Page } from "@playwright/test";
+import { BasePage } from "./base.page";
+import { dismissFeedbackDialog } from "./library.page";
+import { getSelectors } from "../utils/selectors";
+
+const DETERMINISTIC_MARKETPLACE_AGENT_SEARCH = "E2E Calculator Agent";
+
+export class MarketplacePage extends BasePage {
+ constructor(page: Page) {
+ super(page);
+ }
+
+ async goto(page: Page) {
+ await page.goto("/marketplace");
+ await page
+ .locator(
+ '[data-testid="store-card"], [data-testid="featured-store-card"]',
+ )
+ .first()
+ .waitFor({ state: "visible", timeout: 20000 });
+ }
+
+ async getMarketplaceTitle(page: Page) {
+ const { getText } = getSelectors(page);
+ return getText("Explore AI agents", { exact: false });
+ }
+
+ async getCreatorsSection(page: Page) {
+ const { getId, getText } = getSelectors(page);
+ return getId("creators-section") || getText("Creators", { exact: false });
+ }
+
+ async getAgentsSection(page: Page) {
+ const { getId, getText } = getSelectors(page);
+ return getId("agents-section") || getText("Agents", { exact: false });
+ }
+
+ async getCreatorsLink(page: Page) {
+ const { getLink } = getSelectors(page);
+ return getLink(/creators/i);
+ }
+
+ async getAgentsLink(page: Page) {
+ const { getLink } = getSelectors(page);
+ return getLink(/agents/i);
+ }
+
+ async getSearchInput(page: Page) {
+ const visibleSearchInput = page
+ .locator('[data-testid="store-search-input"]:visible')
+ .first();
+ if (await visibleSearchInput.isVisible().catch(() => false)) {
+ return visibleSearchInput;
+ }
+
+ const { getField, getId } = getSelectors(page);
+ return getId("store-search-input").first() || getField(/search/i).first();
+ }
+
+ async getFilterDropdown(page: Page) {
+ const { getId, getButton } = getSelectors(page);
+ return getId("filter-dropdown") || getButton(/filter/i);
+ }
+
+ async searchFor(query: string, page: Page) {
+ const searchInput = await this.getSearchInput(page);
+ await searchInput.fill(query);
+ await searchInput.press("Enter");
+ }
+
+ async clickCreators(page: Page) {
+ const creatorsLink = await this.getCreatorsLink(page);
+ await creatorsLink.click();
+ }
+
+ async clickAgents(page: Page) {
+ const agentsLink = await this.getAgentsLink(page);
+ await agentsLink.click();
+ }
+
+ async openFilter(page: Page) {
+ const filterDropdown = await this.getFilterDropdown(page);
+ await filterDropdown.click();
+ }
+
+ async getFeaturedAgentsSection(page: Page) {
+ const { getText } = getSelectors(page);
+ return getText("Featured agents");
+ }
+
+ async getTopAgentsSection(page: Page) {
+ const { getText } = getSelectors(page);
+ return getText("All Agents");
+ }
+
+ async getFeaturedCreatorsSection(page: Page) {
+ const { getText } = getSelectors(page);
+ return getText("Featured Creators");
+ }
+
+ async getFeaturedAgentCards(page: Page) {
+ const { getId } = getSelectors(page);
+ return getId("featured-store-card");
+ }
+
+ async getTopAgentCards(page: Page) {
+ const { getId } = getSelectors(page);
+ return getId("store-card");
+ }
+
+ async getCreatorProfiles(page: Page) {
+ const { getId } = getSelectors(page);
+ return getId("creator-card");
+ }
+
+ async searchAndNavigate(query: string, page: Page) {
+ const searchInput = (await this.getSearchInput(page)).first();
+ await searchInput.fill(query);
+ await searchInput.press("Enter");
+ }
+
+ async waitForSearchResults() {
+ await this.page.waitForURL("**/marketplace/search**");
+ }
+
+ async getFirstFeaturedAgent(page: Page) {
+ const { getId } = getSelectors(page);
+ const card = getId("featured-store-card").first();
+ await card.waitFor({ state: "visible", timeout: 15000 });
+ return card;
+ }
+
+ async getFirstTopAgent() {
+ const card = this.page
+ .locator('[data-testid="store-card"]:visible')
+ .first();
+ await card.waitFor({ state: "visible", timeout: 15000 });
+ return card;
+ }
+
+ async getFirstCreatorProfile(page: Page) {
+ const { getId } = getSelectors(page);
+ const card = getId("creator-card").first();
+ await card.waitFor({ state: "visible", timeout: 15000 });
+ return card;
+ }
+
+ async getSearchResultsCount(page: Page) {
+ const { getId } = getSelectors(page);
+ const storeCards = getId("store-card");
+ return await storeCards.count();
+ }
+
+ // --- Happy-path flows shared across PR smoke specs ---
+
+ async openRunnableAgent(): Promise<{ path: string }> {
+ await this.searchAndOpenAgent(DETERMINISTIC_MARKETPLACE_AGENT_SEARCH);
+
+ await expect(this.page.getByTestId("agent-add-library-button")).toBeVisible(
+ {
+ timeout: 15000,
+ },
+ );
+
+ return { path: this.page.url() };
+ }
+
+ async openFeaturedAgent(): Promise {
+ await this.searchAndOpenAgent(DETERMINISTIC_MARKETPLACE_AGENT_SEARCH);
+ await dismissFeedbackDialog(this.page);
+ }
+
+ private async searchAndOpenAgent(agentName: string): Promise {
+ const searchURL = `/marketplace/search?searchTerm=${encodeURIComponent(agentName)}`;
+
+ const agentCard = this.page
+ .locator('[data-testid="store-card"]:visible')
+ .filter({ hasText: agentName })
+ .first();
+
+ for (let attempt = 0; attempt < 3; attempt++) {
+ await this.page.goto(searchURL);
+ await this.page.waitForLoadState("networkidle");
+
+ const visible = await agentCard
+ .waitFor({ state: "visible", timeout: 15000 })
+ .then(() => true)
+ .catch(() => false);
+
+ if (visible) break;
+
+ if (attempt === 2) {
+ await expect(agentCard).toBeVisible({ timeout: 15000 });
+ }
+ }
+
+ await agentCard.click();
+
+ await expect(this.page).toHaveURL(/\/marketplace\/agent\//, {
+ timeout: 15000,
+ });
+ await expect(this.page.getByTestId("agent-title")).toBeVisible({
+ timeout: 15000,
+ });
+ }
+
+ async submitAgentForReview(publishableAgentName: string): Promise<{
+ agentTitle: string;
+ agentSlug: string;
+ }> {
+ await this.page.goto("/marketplace");
+ await this.page.getByRole("button", { name: "Become a Creator" }).click();
+
+ const publishAgentModal = this.page.getByTestId("publish-agent-modal");
+ await expect(publishAgentModal).toBeVisible();
+ await expect(
+ publishAgentModal.getByText(
+ "Select your project that you'd like to publish",
+ ),
+ ).toBeVisible();
+
+ const publishableAgentCard = publishAgentModal
+ .getByTestId("agent-card")
+ .filter({ hasText: publishableAgentName })
+ .first();
+ await expect(publishableAgentCard).toBeVisible({ timeout: 15000 });
+ await publishableAgentCard.click();
+ await publishAgentModal
+ .getByRole("button", { name: "Next", exact: true })
+ .click();
+
+ await expect(
+ publishAgentModal.getByText("Write a bit of details about your agent"),
+ ).toBeVisible();
+
+ const suffix = Date.now().toString().slice(-6);
+ const agentTitle = `Publish Flow ${suffix}`;
+ const agentSlug = `publish-flow-${suffix}`;
+
+ await publishAgentModal.getByLabel("Title").fill(agentTitle);
+ await publishAgentModal
+ .getByLabel("Subheader")
+ .fill("A deterministic marketplace submission");
+ await publishAgentModal.getByLabel("Slug").fill(agentSlug);
+ await publishAgentModal
+ .getByLabel("YouTube video link")
+ .fill("https://www.youtube.com/watch?v=test123");
+
+ await publishAgentModal.getByRole("combobox", { name: "Category" }).click();
+ await this.page.getByRole("option", { name: "Other" }).click();
+
+ await publishAgentModal
+ .getByLabel("Description")
+ .fill(
+ "A deterministic publish flow for consolidated Playwright coverage.",
+ );
+
+ const submitButton = publishAgentModal.getByRole("button", {
+ name: "Submit for review",
+ });
+ await expect(submitButton).toBeEnabled();
+ await submitButton.click();
+
+ await expect(
+ publishAgentModal.getByText("Agent is awaiting review"),
+ ).toBeVisible();
+ await expect(
+ publishAgentModal.getByTestId("view-progress-button"),
+ ).toBeVisible();
+
+ return { agentTitle, agentSlug };
+ }
+
+ async waitForDashboardSubmission(agentTitle: string) {
+ for (let attempt = 0; attempt < 3; attempt += 1) {
+ const submissionRow = this.page
+ .getByTestId("agent-table-row")
+ .filter({ hasText: agentTitle })
+ .first();
+
+ // Row may not appear immediately after redirect — allow a short render
+ // window before deciding the submission is absent on this attempt.
+ if (await submissionRow.isVisible({ timeout: 5000 }).catch(() => false)) {
+ return submissionRow;
+ }
+
+ await this.page.reload();
+ await expect(this.page).toHaveURL(/\/profile\/dashboard/);
+ await expect(this.page.getByText("Agent dashboard")).toBeVisible();
+ }
+
+ throw new Error(`Submission row for "${agentTitle}" did not appear`);
+ }
+}
diff --git a/autogpt_platform/frontend/src/tests/pages/navbar.page.ts b/autogpt_platform/frontend/src/playwright/pages/navbar.page.ts
similarity index 100%
rename from autogpt_platform/frontend/src/tests/pages/navbar.page.ts
rename to autogpt_platform/frontend/src/playwright/pages/navbar.page.ts
diff --git a/autogpt_platform/frontend/src/tests/pages/profile-form.page.ts b/autogpt_platform/frontend/src/playwright/pages/profile-form.page.ts
similarity index 100%
rename from autogpt_platform/frontend/src/tests/pages/profile-form.page.ts
rename to autogpt_platform/frontend/src/playwright/pages/profile-form.page.ts
diff --git a/autogpt_platform/frontend/src/tests/pages/profile.page.ts b/autogpt_platform/frontend/src/playwright/pages/profile.page.ts
similarity index 100%
rename from autogpt_platform/frontend/src/tests/pages/profile.page.ts
rename to autogpt_platform/frontend/src/playwright/pages/profile.page.ts
diff --git a/autogpt_platform/frontend/src/playwright/pages/settings.page.ts b/autogpt_platform/frontend/src/playwright/pages/settings.page.ts
new file mode 100644
index 0000000000..7d32ccc23a
--- /dev/null
+++ b/autogpt_platform/frontend/src/playwright/pages/settings.page.ts
@@ -0,0 +1,29 @@
+import { expect, Locator, Page } from "@playwright/test";
+import { BasePage } from "./base.page";
+
+export class SettingsPage extends BasePage {
+ constructor(page: Page) {
+ super(page);
+ }
+
+ async open(): Promise {
+ await this.page.goto("/profile/settings");
+ await expect(this.page).toHaveURL(/\/profile\/settings/);
+ await expect(
+ this.page.getByText("Manage your account settings and preferences."),
+ ).toBeVisible();
+ }
+
+ getAgentRunNotificationsSwitch(): Locator {
+ return this.page.getByRole("switch", {
+ name: "Agent Run Notifications",
+ });
+ }
+
+ async savePreferences(): Promise {
+ await this.page.getByRole("button", { name: "Save preferences" }).click();
+ await expect(
+ this.page.getByText("Successfully updated notification preferences"),
+ ).toBeVisible({ timeout: 15000 });
+ }
+}
diff --git a/autogpt_platform/frontend/src/playwright/publish-happy-path.spec.ts b/autogpt_platform/frontend/src/playwright/publish-happy-path.spec.ts
new file mode 100644
index 0000000000..00fcbaf1d4
--- /dev/null
+++ b/autogpt_platform/frontend/src/playwright/publish-happy-path.spec.ts
@@ -0,0 +1,77 @@
+import { expect, test } from "./coverage-fixture";
+import { E2E_AUTH_STATES } from "./credentials/accounts";
+import { BuildPage } from "./pages/build.page";
+import { LibraryPage } from "./pages/library.page";
+import { MarketplacePage } from "./pages/marketplace.page";
+
+test.use({ storageState: E2E_AUTH_STATES.parallelA });
+
+test("publish happy path: user can submit, track, and delete an agent submission from the dashboard", async ({
+ page,
+}) => {
+ test.setTimeout(180000);
+
+ const buildPage = new BuildPage(page);
+ const libraryPage = new LibraryPage(page);
+ const marketplacePage = new MarketplacePage(page);
+
+ const { agentName: publishableAgentName } =
+ await buildPage.createAndSaveSimpleAgent("Publish Flow Agent");
+
+ await page.goto("/library");
+ await libraryPage.waitForAgentsToLoad();
+ await libraryPage.searchAgents(publishableAgentName);
+ await libraryPage.waitForAgentsToLoad();
+
+ const createdAgent = page
+ .getByTestId("library-agent-card")
+ .filter({ hasText: publishableAgentName })
+ .first();
+ await expect(createdAgent).toBeVisible({ timeout: 15000 });
+
+ const { agentTitle, agentSlug } =
+ await marketplacePage.submitAgentForReview(publishableAgentName);
+
+ await page.getByTestId("view-progress-button").click();
+ await expect(page).toHaveURL(/\/profile\/dashboard/);
+ await expect(page.getByText("Agent dashboard")).toBeVisible();
+
+ const submissionRow =
+ await marketplacePage.waitForDashboardSubmission(agentTitle);
+ await expect(
+ submissionRow.getByTestId("agent-status"),
+ `submission "${agentTitle}" should appear in the dashboard review-pending state`,
+ ).toContainText(/awaiting review/i);
+ await submissionRow.getByTestId("agent-table-row-actions").click();
+ await expect(page.getByRole("menuitem", { name: "Edit" })).toBeVisible();
+
+ // Delete the submission via the actions menu. The dashboard does not show
+ // a confirmation dialog — clicking Delete fires the API directly. We then
+ // assert the row is gone, proving the backend actually removed it (not
+ // just the menu item disappeared).
+ await page.getByRole("menuitem", { name: "Delete" }).click();
+
+ await expect(
+ page.getByTestId("agent-table-row").filter({ hasText: agentTitle }),
+ `submission row "${agentTitle}" must be removed from the dashboard after delete`,
+ ).toHaveCount(0, { timeout: 15000 });
+
+ // Validate the deleted submission is no longer discoverable in Marketplace.
+ await page.goto("/marketplace");
+ const searchInput = page
+ .locator('[data-testid="store-search-input"]:visible')
+ .first();
+ await expect(searchInput).toBeVisible({ timeout: 15000 });
+ await searchInput.fill(agentSlug);
+ await searchInput.press("Enter");
+ await expect(page).toHaveURL(/\/marketplace\/search/);
+
+ await expect(
+ page
+ .locator(
+ '[data-testid="store-card"], [data-testid="featured-store-card"]',
+ )
+ .filter({ hasText: agentTitle }),
+ `deleted submission "${agentTitle}" should not appear in marketplace results`,
+ ).toHaveCount(0, { timeout: 15000 });
+});
diff --git a/autogpt_platform/frontend/src/playwright/settings-happy-path.spec.ts b/autogpt_platform/frontend/src/playwright/settings-happy-path.spec.ts
new file mode 100644
index 0000000000..29dcd5187d
--- /dev/null
+++ b/autogpt_platform/frontend/src/playwright/settings-happy-path.spec.ts
@@ -0,0 +1,75 @@
+import { expect, test } from "./coverage-fixture";
+import { LoginPage } from "./pages/login.page";
+import { ProfileFormPage } from "./pages/profile-form.page";
+import { SettingsPage } from "./pages/settings.page";
+
+test("settings happy path: user can save notification preferences and keep them after reload and re-login", async ({
+ page,
+}) => {
+ test.setTimeout(90000);
+
+ const loginPage = new LoginPage(page);
+ const settingsPage = new SettingsPage(page);
+
+ await loginPage.loginAsSeededUser("smokeSettings");
+ await settingsPage.open();
+
+ const agentRunSwitch = settingsPage.getAgentRunNotificationsSwitch();
+ // Assert the attribute exists before reading it — defaulting to "false"
+ // would silently pass a regression that removes `aria-checked` entirely.
+ await expect(agentRunSwitch).toHaveAttribute(
+ "aria-checked",
+ /^(true|false)$/,
+ );
+ const initialState = await agentRunSwitch.getAttribute("aria-checked");
+ const expectedState = initialState === "true" ? "false" : "true";
+
+ await agentRunSwitch.click();
+ await settingsPage.savePreferences();
+ await expect(agentRunSwitch).toHaveAttribute("aria-checked", expectedState);
+
+ await page.reload();
+ await settingsPage.open();
+ await expect(settingsPage.getAgentRunNotificationsSwitch()).toHaveAttribute(
+ "aria-checked",
+ expectedState,
+ );
+
+ await page.getByTestId("profile-popout-menu-trigger").click();
+ await page.getByRole("button", { name: "Log out" }).click();
+ await expect(page).toHaveURL(/\/login/);
+
+ await loginPage.loginAsSeededUser("smokeSettings");
+ await settingsPage.open();
+ await expect(settingsPage.getAgentRunNotificationsSwitch()).toHaveAttribute(
+ "aria-checked",
+ expectedState,
+ );
+});
+
+test("settings happy path: user can edit display name and keep it after refresh", async ({
+ page,
+}) => {
+ test.setTimeout(90000);
+
+ const loginPage = new LoginPage(page);
+ const profileFormPage = new ProfileFormPage(page);
+ const updatedDisplayName = `E2E Display ${Date.now()}`;
+
+ await loginPage.loginAsSeededUser("smokeSettings");
+ await page.goto("/profile");
+ await expect(await profileFormPage.isLoaded()).toBe(true);
+
+ await profileFormPage.setDisplayName(updatedDisplayName);
+ await profileFormPage.saveChanges();
+
+ await expect
+ .poll(() => profileFormPage.getDisplayName(), { timeout: 15000 })
+ .toBe(updatedDisplayName);
+
+ await page.reload();
+ await expect(await profileFormPage.isLoaded()).toBe(true);
+ await expect
+ .poll(() => profileFormPage.getDisplayName(), { timeout: 15000 })
+ .toBe(updatedDisplayName);
+});
diff --git a/autogpt_platform/frontend/src/tests/utils/assertion.ts b/autogpt_platform/frontend/src/playwright/utils/assertion.ts
similarity index 100%
rename from autogpt_platform/frontend/src/tests/utils/assertion.ts
rename to autogpt_platform/frontend/src/playwright/utils/assertion.ts
diff --git a/autogpt_platform/frontend/src/playwright/utils/auth.ts b/autogpt_platform/frontend/src/playwright/utils/auth.ts
new file mode 100644
index 0000000000..2e737aa780
--- /dev/null
+++ b/autogpt_platform/frontend/src/playwright/utils/auth.ts
@@ -0,0 +1,284 @@
+import fs from "fs";
+import path from "path";
+import { LoginPage } from "../pages/login.page";
+import {
+ SEEDED_AUTH_STATE_ACCOUNT_KEYS,
+ SEEDED_TEST_ACCOUNTS,
+ SEEDED_TEST_USERS,
+ getAuthStatePath,
+} from "../credentials/accounts";
+import { buildCookieConsentStorageState } from "../credentials/storage-state";
+import { signupTestUser } from "./signup";
+import { getBrowser } from "./get-browser";
+import { skipOnboardingIfPresent } from "./onboarding";
+
+export interface TestUser {
+ email: string;
+ password: string;
+ id?: string;
+ createdAt?: string;
+}
+
+export interface UserPool {
+ users: TestUser[];
+ createdAt: string;
+ version: string;
+}
+
+const AUTH_STATE_KEYS = [...SEEDED_AUTH_STATE_ACCOUNT_KEYS];
+
+export async function createTestUser(
+ email?: string,
+ password?: string,
+ ignoreOnboarding: boolean = true,
+): Promise {
+ const { faker } = await import("@faker-js/faker");
+ const userEmail = email || faker.internet.email();
+ const userPassword = password || faker.internet.password({ length: 12 });
+
+ try {
+ const browser = await getBrowser();
+ const context = await browser.newContext();
+ const page = await context.newPage();
+
+ // Auto-accept cookies in test environment to prevent banner from appearing
+ await page.addInitScript(() => {
+ window.localStorage.setItem(
+ "autogpt_cookie_consent",
+ JSON.stringify({
+ hasConsented: true,
+ timestamp: Date.now(),
+ analytics: true,
+ monitoring: true,
+ }),
+ );
+ });
+
+ try {
+ const testUser = await signupTestUser(
+ page,
+ userEmail,
+ userPassword,
+ ignoreOnboarding,
+ false,
+ );
+ return testUser;
+ } finally {
+ await page.close();
+ await context.close();
+ await browser.close();
+ }
+ } catch (error) {
+ console.error(`❌ Error creating test user ${userEmail}:`, error);
+ throw error;
+ }
+}
+
+export async function createTestUsers(count: number): Promise {
+ console.log(`👥 Creating ${count} test users...`);
+
+ const users: TestUser[] = [];
+ let consecutiveFailures = 0;
+
+ for (let i = 0; i < count; i++) {
+ try {
+ const user = await createTestUser();
+ users.push(user);
+ consecutiveFailures = 0; // Reset failure counter on success
+ console.log(`✅ Created user ${i + 1}/${count}: ${user.email}`);
+ } catch (error) {
+ consecutiveFailures++;
+ console.error(`❌ Failed to create user ${i + 1}/${count}:`, error);
+
+ // If we have too many consecutive failures, stop trying
+ if (consecutiveFailures >= 3) {
+ console.error(
+ `⚠️ Stopping after ${consecutiveFailures} consecutive failures`,
+ );
+ break;
+ }
+ }
+ }
+
+ console.log(`🎉 Successfully created ${users.length}/${count} test users`);
+ return users;
+}
+
+export async function getTestUser(accountKey?: string): Promise {
+ if (SEEDED_TEST_USERS.length === 0) {
+ throw new Error("No seeded E2E users are configured");
+ }
+
+ if (accountKey) {
+ const matchedUser = SEEDED_TEST_USERS.find(
+ (user) => user.key === accountKey || user.email === accountKey,
+ );
+
+ if (!matchedUser) {
+ throw new Error(
+ `No seeded E2E user found for account key or email: ${accountKey}`,
+ );
+ }
+
+ return { email: matchedUser.email, password: matchedUser.password };
+ }
+
+ const rawWorkerIndex = Number.parseInt(
+ process.env.TEST_WORKER_INDEX ?? process.env.PLAYWRIGHT_WORKER_INDEX ?? "0",
+ 10,
+ );
+ const workerIndex = Number.isNaN(rawWorkerIndex) ? 0 : rawWorkerIndex;
+ const deterministicIndex =
+ ((workerIndex % SEEDED_TEST_USERS.length) + SEEDED_TEST_USERS.length) %
+ SEEDED_TEST_USERS.length;
+ const { email, password } = SEEDED_TEST_USERS[deterministicIndex];
+ return { email, password };
+}
+
+function hasStoredAuthState(accountKey: (typeof AUTH_STATE_KEYS)[number]) {
+ return fs.existsSync(getAuthStatePath(accountKey));
+}
+
+function authStateMatchesOrigin(
+ accountKey: (typeof AUTH_STATE_KEYS)[number],
+ origin: string,
+): boolean {
+ const statePath = getAuthStatePath(accountKey);
+ if (!fs.existsSync(statePath)) {
+ return false;
+ }
+
+ try {
+ const state = JSON.parse(fs.readFileSync(statePath, "utf8")) as {
+ origins?: Array<{ origin?: string }>;
+ };
+ return (
+ state.origins?.some((storedOrigin) => storedOrigin.origin === origin) ??
+ false
+ );
+ } catch {
+ return false;
+ }
+}
+
+export function hasSeededAuthStates(baseURL: string): boolean {
+ const origin = new URL(baseURL).origin;
+ return AUTH_STATE_KEYS.every(
+ (accountKey) =>
+ hasStoredAuthState(accountKey) &&
+ authStateMatchesOrigin(accountKey, origin),
+ );
+}
+
+async function authStateHasLiveSession(
+ baseURL: string,
+ accountKey: (typeof AUTH_STATE_KEYS)[number],
+): Promise {
+ const browser = await getBrowser();
+
+ try {
+ const context = await browser.newContext({
+ baseURL,
+ storageState: getAuthStatePath(accountKey),
+ });
+ const page = await context.newPage();
+
+ try {
+ await page.goto("/marketplace");
+ await page.waitForLoadState("domcontentloaded");
+ await skipOnboardingIfPresent(page, "/marketplace");
+ return await page
+ .getByTestId("profile-popout-menu-trigger")
+ .waitFor({ state: "visible", timeout: 10_000 })
+ .then(() => true)
+ .catch(() => false);
+ } finally {
+ await page.close();
+ await context.close();
+ }
+ } catch {
+ return false;
+ } finally {
+ await browser.close();
+ }
+}
+
+export async function getInvalidSeededAuthStateKeys(
+ baseURL: string,
+): Promise<(typeof AUTH_STATE_KEYS)[number][]> {
+ const origin = new URL(baseURL).origin;
+ const invalidKeys = await Promise.all(
+ AUTH_STATE_KEYS.map(async (accountKey) => {
+ if (
+ !hasStoredAuthState(accountKey) ||
+ !authStateMatchesOrigin(accountKey, origin)
+ ) {
+ return accountKey;
+ }
+
+ return (await authStateHasLiveSession(baseURL, accountKey))
+ ? null
+ : accountKey;
+ }),
+ );
+
+ return invalidKeys.filter(
+ (accountKey): accountKey is (typeof AUTH_STATE_KEYS)[number] =>
+ accountKey !== null,
+ );
+}
+
+async function createAuthStateForUser(
+ baseURL: string,
+ accountKey: (typeof AUTH_STATE_KEYS)[number],
+): Promise {
+ const browser = await getBrowser();
+
+ try {
+ const { email, password } = SEEDED_TEST_ACCOUNTS[accountKey];
+ const origin = new URL(baseURL).origin;
+ const context = await browser.newContext({
+ baseURL,
+ storageState: buildCookieConsentStorageState(origin),
+ });
+ const page = await context.newPage();
+ const loginPage = new LoginPage(page);
+
+ await page.goto("/login");
+ await loginPage.login(email, password);
+ await page.waitForURL(
+ (url: URL) =>
+ /\/(onboarding|marketplace|copilot|library)/.test(url.pathname),
+ { timeout: 20000 },
+ );
+ await skipOnboardingIfPresent(page, "/marketplace");
+ await page.getByTestId("profile-popout-menu-trigger").waitFor({
+ state: "visible",
+ timeout: 10000,
+ });
+
+ const statePath = getAuthStatePath(accountKey);
+ fs.mkdirSync(path.dirname(statePath), { recursive: true });
+ await context.storageState({ path: statePath });
+ await context.close();
+ } catch (error) {
+ const { email } = SEEDED_TEST_ACCOUNTS[accountKey];
+ throw new Error(
+ `Failed to create auth state for ${email}: ${String(
+ error,
+ )}. If these seeded QA accounts are missing, seed them with backend/test/e2e_test_data.py before running Playwright.`,
+ );
+ } finally {
+ await browser.close();
+ }
+}
+
+export async function ensureSeededAuthStates(baseURL: string): Promise {
+ const invalidKeys = await getInvalidSeededAuthStateKeys(baseURL);
+
+ await Promise.all(
+ invalidKeys.map((accountKey) =>
+ createAuthStateForUser(baseURL, accountKey),
+ ),
+ );
+}
diff --git a/autogpt_platform/frontend/src/tests/utils/get-browser.ts b/autogpt_platform/frontend/src/playwright/utils/get-browser.ts
similarity index 100%
rename from autogpt_platform/frontend/src/tests/utils/get-browser.ts
rename to autogpt_platform/frontend/src/playwright/utils/get-browser.ts
diff --git a/autogpt_platform/frontend/src/tests/utils/onboarding.ts b/autogpt_platform/frontend/src/playwright/utils/onboarding.ts
similarity index 70%
rename from autogpt_platform/frontend/src/tests/utils/onboarding.ts
rename to autogpt_platform/frontend/src/playwright/utils/onboarding.ts
index 375babc743..b5fa79abda 100644
--- a/autogpt_platform/frontend/src/tests/utils/onboarding.ts
+++ b/autogpt_platform/frontend/src/playwright/utils/onboarding.ts
@@ -1,5 +1,14 @@
import { Page, expect } from "@playwright/test";
+function resolveAppUrl(page: Page, destination: string) {
+ const baseURL =
+ page.url().startsWith("http://") || page.url().startsWith("https://")
+ ? page.url()
+ : (process.env.PLAYWRIGHT_BASE_URL ?? "http://localhost:3000");
+
+ return new URL(destination, baseURL).toString();
+}
+
/**
* Complete the onboarding wizard via API.
* Use this when a test needs an authenticated user who has already finished onboarding
@@ -10,8 +19,11 @@ import { Page, expect } from "@playwright/test";
*/
export async function completeOnboardingViaAPI(page: Page) {
await page.request.post(
- "http://localhost:3000/api/proxy/api/onboarding/step?step=VISIT_COPILOT",
- { headers: { "Content-Type": "application/json" } },
+ resolveAppUrl(page, "/api/proxy/api/onboarding/step"),
+ {
+ headers: { "Content-Type": "application/json" },
+ params: { step: "VISIT_COPILOT" },
+ },
);
}
@@ -28,7 +40,7 @@ export async function skipOnboardingIfPresent(
if (!url.includes("/onboarding")) return;
await completeOnboardingViaAPI(page);
- await page.goto(`http://localhost:3000${destination}`);
+ await page.goto(resolveAppUrl(page, destination));
await page.waitForLoadState("domcontentloaded", { timeout: 10000 });
}
@@ -70,8 +82,15 @@ export async function completeOnboardingWizard(
}
await page.getByRole("button", { name: "Launch Autopilot" }).click();
- // Step 4: Preparing — wait for animation to complete and redirect to /copilot
- await page.waitForURL(/\/copilot/, { timeout: 15000 });
+ // Step 4: Preparing — require the real transition state to appear first,
+ // then wait for the app shell on /copilot rather than racing the redirect.
+ await expect(
+ page.getByText("Preparing your workspace...", { exact: false }),
+ ).toBeVisible({ timeout: 10000 });
+ await page.waitForURL(/\/copilot/, { timeout: 30000 });
+ await expect(page.getByTestId("profile-popout-menu-trigger")).toBeVisible({
+ timeout: 15000,
+ });
return { name, role, painPoints };
}
diff --git a/autogpt_platform/frontend/src/tests/utils/selectors.ts b/autogpt_platform/frontend/src/playwright/utils/selectors.ts
similarity index 100%
rename from autogpt_platform/frontend/src/tests/utils/selectors.ts
rename to autogpt_platform/frontend/src/playwright/utils/selectors.ts
diff --git a/autogpt_platform/frontend/src/tests/utils/signin.ts b/autogpt_platform/frontend/src/playwright/utils/signin.ts
similarity index 100%
rename from autogpt_platform/frontend/src/tests/utils/signin.ts
rename to autogpt_platform/frontend/src/playwright/utils/signin.ts
diff --git a/autogpt_platform/frontend/src/tests/utils/signup.ts b/autogpt_platform/frontend/src/playwright/utils/signup.ts
similarity index 98%
rename from autogpt_platform/frontend/src/tests/utils/signup.ts
rename to autogpt_platform/frontend/src/playwright/utils/signup.ts
index 6b7802db9d..c83c760102 100644
--- a/autogpt_platform/frontend/src/tests/utils/signup.ts
+++ b/autogpt_platform/frontend/src/playwright/utils/signup.ts
@@ -19,7 +19,7 @@ export async function signupTestUser(
try {
// Navigate to signup page
- await page.goto("http://localhost:3000/signup");
+ await page.goto("/signup");
// Wait for page to load
getText("Create a new account");
@@ -122,7 +122,7 @@ export async function signupAndNavigateToMarketplace(
export async function validateSignupForm(page: any): Promise {
console.log("🧪 Validating signup form...");
- await page.goto("http://localhost:3000/signup");
+ await page.goto("/signup");
// Test empty form submission
console.log("❌ Testing empty form submission...");
diff --git a/autogpt_platform/frontend/src/tests/AGENTS.md b/autogpt_platform/frontend/src/tests/AGENTS.md
index 1969708e8c..87222559af 100644
--- a/autogpt_platform/frontend/src/tests/AGENTS.md
+++ b/autogpt_platform/frontend/src/tests/AGENTS.md
@@ -22,7 +22,7 @@
- Flows requiring real browser APIs (clipboard, downloads)
- Cross-page navigation that must work end-to-end
-**Location:** `src/tests/*.spec.ts` (centralized, as there will be fewer of them)
+**Location:** `src/playwright/*.spec.ts` (centralized, as there will be fewer of them)
**Import:** Always import `test` and `expect` from `./coverage-fixture` instead of `@playwright/test`. This auto-collects V8 coverage per test for Codecov reporting.
@@ -74,6 +74,10 @@ Start with a `main.test.tsx` file and split into smaller files as it grows.
2. Mock API requests via MSW
3. Assert UI scenarios via Testing Library
+**Prefer the UI surface over direct hook tests:** if a `use*.ts` hook only exists to support a page/component, test that page/component instead of adding a `renderHook()` test. Reserve direct hook tests for shared hooks with standalone business logic that cannot be exercised cleanly through the UI.
+
+**Prefer Orval-generated mocks:** use the generated MSW handlers and response builders from `src/app/api/__generated__/endpoints/*/*.msw.ts` instead of hand-built API response objects or mocking a page/component hook.
+
```tsx
// Example: Test page renders data from API
import { server } from "@/mocks/mock-server";
@@ -98,7 +102,7 @@ test("shows error when submission fails", async () => {
- Pure utility functions (`lib/utils.ts`)
- Component rendering with various props
- Component state changes
-- Custom hooks
+- Shared hooks with standalone business logic
**Location:** Co-located with the file: `Component.test.tsx` next to `Component.tsx`
@@ -172,25 +176,29 @@ src/
├── mocks/
│ ├── mock-handlers.ts # MSW handlers (auto-generated via Orval)
│ └── mock-server.ts # MSW server setup
+├── playwright/
+│ ├── *.spec.ts # E2E tests (Playwright) - centralized
+│ ├── pages/ # Playwright page objects
+│ └── utils/ # Playwright helpers/fixtures
└── tests/
├── integrations/
│ ├── test-utils.tsx # Testing utilities
│ └── vitest.setup.tsx # Integration test setup
- └── *.spec.ts # E2E tests (Playwright) - centralized
+ └── AGENTS.md # Testing guidance for agents
```
---
## Priority Matrix
-| Component Type | Test Priority | Recommended Test |
-| ------------------- | ------------- | ---------------- |
-| Pages/Features | **Highest** | Integration |
-| Custom Hooks | High | Unit |
-| Utility Functions | High | Unit |
-| Organisms (complex) | High | Integration |
-| Molecules | Medium | Unit + Storybook |
-| Atoms | Medium | Storybook only\* |
+| Component Type | Test Priority | Recommended Test |
+| ------------------- | ------------- | -------------------------------------- |
+| Pages/Features | **Highest** | Integration |
+| Custom Hooks | Medium | Parent integration or shared-hook unit |
+| Utility Functions | High | Unit |
+| Organisms (complex) | High | Integration |
+| Molecules | Medium | Unit + Storybook |
+| Atoms | Medium | Storybook only\* |
\*Atoms are typically simple enough that Storybook visual tests suffice.
@@ -218,6 +226,8 @@ test("shows error when deletion fails", async () => {
**Generated handlers location:** `src/app/api/__generated__/endpoints/*/` - each endpoint has handlers for different status codes.
+For Playwright support code, keep browser-only helpers in `src/playwright/` rather than `src/tests/`.
+
---
## Golden Rules
@@ -228,3 +238,5 @@ test("shows error when deletion fails", async () => {
4. **Co-locate integration tests** - Keep `__tests__/` folder next to the component
5. **E2E is expensive** - Only for critical happy paths; prefer integration tests
6. **AI agents are good at writing integration tests** - Start with these when adding test coverage
+7. **Prefer component/page tests over hook tests** - Don't add `renderHook()` coverage for component implementation details
+8. **Use generated API mocks** - Prefer Orval MSW helpers over manual API object stubs
diff --git a/autogpt_platform/frontend/src/tests/agent-activity.spec.ts b/autogpt_platform/frontend/src/tests/agent-activity.spec.ts
deleted file mode 100644
index 4ae4a11d0c..0000000000
--- a/autogpt_platform/frontend/src/tests/agent-activity.spec.ts
+++ /dev/null
@@ -1,96 +0,0 @@
-import { test, expect } from "./coverage-fixture";
-import { BuildPage } from "./pages/build.page";
-import * as LibraryPage from "./pages/library.page";
-import { LoginPage } from "./pages/login.page";
-import { hasTextContent, hasUrl, isVisible } from "./utils/assertion";
-import { getTestUser } from "./utils/auth";
-import { getSelectors } from "./utils/selectors";
-
-test.beforeEach(async ({ page }) => {
- const loginPage = new LoginPage(page);
- const buildPage = new BuildPage(page);
- const testUser = await getTestUser();
-
- await page.goto("/login");
- await loginPage.login(testUser.email, testUser.password);
- await hasUrl(page, "/marketplace");
-
- await page.goto("/build");
- await buildPage.closeTutorial();
-
- await buildPage.addBlockByClick("Add to Dictionary");
- await buildPage.waitForNodeOnCanvas(1);
-
- await buildPage.saveAgent("Test Agent", "Test Description");
- await test
- .expect(page)
- .toHaveURL(({ searchParams }) => !!searchParams.get("flowID"));
-
- // Wait for save to complete
- await page.waitForTimeout(1000);
-
- await page.goto("/library");
- // Navigate to the specific agent we just created, not just the first one
- await LibraryPage.navigateToAgentByName(page, "Test Agent");
- await LibraryPage.waitForAgentPageLoad(page);
-});
-
-test("shows badge with count when agent is running", async ({ page }) => {
- const { getId } = getSelectors(page);
-
- // Start the agent run
- await LibraryPage.clickRunButton(page);
-
- // Wait for the badge to appear and check it has a valid count
- const badge = getId("agent-activity-badge");
- await isVisible(badge);
-
- // Check that badge shows a positive number (more flexible than exact count)
- await expect(async () => {
- const badgeText = await badge.textContent();
- const count = parseInt(badgeText || "0");
-
- if (count < 1) {
- throw new Error(`Expected badge count >= 1, got: ${badgeText}`);
- }
- }).toPass({ timeout: 10000 });
-});
-
-test("displays the runs on the activity dropdown", async ({ page }) => {
- const { getId } = getSelectors(page);
-
- const activityBtn = getId("agent-activity-button");
- await isVisible(activityBtn);
-
- // Start the agent run
- await LibraryPage.clickRunButton(page);
-
- // Wait for the activity badge to appear (indicating execution started)
- const badge = getId("agent-activity-badge");
- await isVisible(badge);
-
- // Click to open the dropdown
- await activityBtn.click();
-
- const dropdown = getId("agent-activity-dropdown");
- await isVisible(dropdown);
-
- // Check that the agent name appears in the dropdown
- await hasTextContent(dropdown, "Test Agent");
-
- // Check for execution status - be more flexible with text matching
- await expect(async () => {
- const dropdownText = await dropdown.textContent();
- const hasAgentName = dropdownText?.includes("Test Agent");
- const hasExecutionStatus =
- dropdownText?.includes("queued") ||
- dropdownText?.includes("running") ||
- dropdownText?.includes("Started");
-
- if (!hasAgentName || !hasExecutionStatus) {
- throw new Error(
- `Expected agent name and execution status, got: ${dropdownText}`,
- );
- }
- }).toPass({ timeout: 8000 });
-});
diff --git a/autogpt_platform/frontend/src/tests/agent-dashboard.spec.ts b/autogpt_platform/frontend/src/tests/agent-dashboard.spec.ts
deleted file mode 100644
index ec7ac3bfa0..0000000000
--- a/autogpt_platform/frontend/src/tests/agent-dashboard.spec.ts
+++ /dev/null
@@ -1,260 +0,0 @@
-import { test, expect } from "./coverage-fixture";
-import { getTestUserWithLibraryAgents } from "./credentials";
-import { LoginPage } from "./pages/login.page";
-import { hasUrl, isHidden } from "./utils/assertion";
-import { getSelectors } from "./utils/selectors";
-
-test.beforeEach(async ({ page }) => {
- const loginPage = new LoginPage(page);
- await page.goto("/login");
- const richUser = getTestUserWithLibraryAgents();
- await loginPage.login(richUser.email, richUser.password);
- await hasUrl(page, "/marketplace");
-});
-
-test("dashboard page loads successfully", async ({ page }) => {
- const { getText } = getSelectors(page);
- await page.goto("/profile/dashboard");
-
- await expect(getText("Agent dashboard")).toBeVisible();
- await expect(getText("Submit a New Agent")).toBeVisible();
- await expect(getText("Your uploaded agents")).toBeVisible();
-});
-
-test("submit agent button works correctly", async ({ page }) => {
- const { getId, getText } = getSelectors(page);
-
- await page.goto("/profile/dashboard");
- const submitAgentButton = getId("submit-agent-button");
- await expect(submitAgentButton).toBeVisible();
- await submitAgentButton.click();
-
- await expect(getText("Publish Agent")).toBeVisible();
- await expect(
- getText("Select your project that you'd like to publish"),
- ).toBeVisible();
-
- await page.locator('button[aria-label="Close"]').click();
- await expect(getText("Publish Agent")).not.toBeVisible();
-});
-
-test("agent table view action works correctly for rejected agents", async ({
- page,
-}) => {
- await page.goto("/profile/dashboard");
-
- const agentTable = page.getByTestId("agent-table");
- await expect(agentTable).toBeVisible();
-
- const rows = agentTable.getByTestId("agent-table-row");
-
- // Find a row with rejected status
- const rejectedRow = rows.filter({ hasText: "Rejected" }).first();
- if (!(await rejectedRow.count())) {
- console.log("No rejected agents available; skipping view test.");
- return;
- }
-
- await rejectedRow.scrollIntoViewIfNeeded();
-
- const actionsButton = rejectedRow.getByTestId("agent-table-row-actions");
- await actionsButton.waitFor({ state: "visible", timeout: 10000 });
- await actionsButton.scrollIntoViewIfNeeded();
- await actionsButton.click();
-
- // View button testing
- const viewButton = page.getByRole("menuitem", { name: "View" });
- await expect(viewButton).toBeVisible();
- await viewButton.click();
-
- const modal = page.getByTestId("publish-agent-modal");
- await expect(modal).toBeVisible();
- const viewAgentName = modal.getByText("Agent is awaiting review");
- await expect(viewAgentName).toBeVisible();
-
- await page.getByRole("button", { name: "Done" }).click();
- await expect(modal).not.toBeVisible();
-});
-
-test("agent table delete action works correctly", async ({ page }) => {
- await page.goto("/profile/dashboard");
-
- const agentTable = page.getByTestId("agent-table");
- await expect(agentTable).toBeVisible();
-
- const rows = agentTable.getByTestId("agent-table-row");
-
- // Delete button testing — only works for PENDING submissions
- const beforeCount = await rows.count();
-
- if (beforeCount === 0) {
- console.log("No agents available; skipping delete flow.");
- return;
- }
-
- // Find a PENDING submission to delete
- const pendingRow = rows.filter({ hasText: "Pending" }).first();
- if (!(await pendingRow.count())) {
- console.log("No pending agents available; skipping delete flow.");
- return;
- }
-
- const deletedSubmissionId =
- await pendingRow.getAttribute("data-submission-id");
- await pendingRow.scrollIntoViewIfNeeded();
-
- const delActionsButton = pendingRow.getByTestId("agent-table-row-actions");
- await delActionsButton.waitFor({ state: "visible", timeout: 10000 });
- await delActionsButton.scrollIntoViewIfNeeded();
- await delActionsButton.click();
-
- const deleteButton = page.getByRole("menuitem", { name: "Delete" });
- await expect(deleteButton).toBeVisible();
- await deleteButton.click();
-
- // Assert that the card with the deleted agent ID is not visible
- await isHidden(page.locator(`[data-submission-id="${deletedSubmissionId}"]`));
-});
-
-test("edit and delete actions are unavailable for non-pending submissions", async ({
- page,
-}) => {
- await page.goto("/profile/dashboard");
-
- const agentTable = page.getByTestId("agent-table");
- await expect(agentTable).toBeVisible();
-
- const rows = agentTable.getByTestId("agent-table-row");
-
- // Test with rejected submissions (view only)
- const rejectedRow = rows.filter({ hasText: "Rejected" }).first();
- if (await rejectedRow.count()) {
- await rejectedRow.scrollIntoViewIfNeeded();
- const actionsButton = rejectedRow.getByTestId("agent-table-row-actions");
- await actionsButton.waitFor({ state: "visible", timeout: 10000 });
- await actionsButton.scrollIntoViewIfNeeded();
- await actionsButton.click();
-
- await expect(page.getByRole("menuitem", { name: "View" })).toBeVisible();
- await expect(page.getByRole("menuitem", { name: "Edit" })).toHaveCount(0);
- await expect(page.getByRole("menuitem", { name: "Delete" })).toHaveCount(0);
-
- // Close the menu
- await page.keyboard.press("Escape");
- }
-
- // Test with approved submissions (view only)
- const approvedRow = rows.filter({ hasText: "Approved" }).first();
- if (await approvedRow.count()) {
- await approvedRow.scrollIntoViewIfNeeded();
- const actionsButton = approvedRow.getByTestId("agent-table-row-actions");
- await actionsButton.waitFor({ state: "visible", timeout: 10000 });
- await actionsButton.scrollIntoViewIfNeeded();
- await actionsButton.click();
-
- await expect(page.getByRole("menuitem", { name: "View" })).toBeVisible();
- await expect(page.getByRole("menuitem", { name: "Edit" })).toHaveCount(0);
- await expect(page.getByRole("menuitem", { name: "Delete" })).toHaveCount(0);
- }
-});
-
-test("editing a pending submission works correctly", async ({ page }) => {
- await page.goto("/profile/dashboard");
-
- const agentTable = page.getByTestId("agent-table");
- await expect(agentTable).toBeVisible();
-
- const rows = agentTable.getByTestId("agent-table-row");
-
- // Find a PENDING submission to edit (only PENDING submissions can be edited)
- const pendingRow = rows.filter({ hasText: "Pending" }).first();
- if (!(await pendingRow.count())) {
- console.log("No pending agents available; skipping edit test.");
- return;
- }
-
- const beforeCount = await rows.count();
-
- await pendingRow.scrollIntoViewIfNeeded();
- const actionsButton = pendingRow.getByTestId("agent-table-row-actions");
- await actionsButton.waitFor({ state: "visible", timeout: 10000 });
- await actionsButton.scrollIntoViewIfNeeded();
- await actionsButton.click();
-
- const editButton = page.getByRole("menuitem", { name: "Edit" });
- await expect(editButton).toBeVisible();
- await editButton.click();
-
- const editModal = page.getByTestId("edit-agent-modal");
- await expect(editModal).toBeVisible();
-
- const newTitle = `E2E Edit Pending ${Date.now()}`;
- await page.getByRole("textbox", { name: "Title" }).fill(newTitle);
- await page
- .getByRole("textbox", { name: "Changes Summary" })
- .fill("E2E change - updating pending submission");
-
- await page.getByRole("button", { name: "Update submission" }).click();
- await expect(editModal).not.toBeVisible();
-
- // A new submission should appear with pending state
- await expect(async () => {
- const afterCount = await rows.count();
- expect(afterCount).toBeGreaterThan(beforeCount);
- }).toPass();
-
- const newRow = rows.filter({ hasText: newTitle }).first();
- await expect(newRow).toBeVisible();
- await expect(newRow).toContainText(/Awaiting review/);
-});
-
-test("editing a pending agent updates the same submission in place", async ({
- page,
-}) => {
- await page.goto("/profile/dashboard");
-
- const agentTable = page.getByTestId("agent-table");
- await expect(agentTable).toBeVisible();
-
- const rows = agentTable.getByTestId("agent-table-row");
-
- const pendingRow = rows.filter({ hasText: /Awaiting review/ }).first();
- if (!(await pendingRow.count())) {
- console.log("No pending agents available; skipping pending edit test.");
- return;
- }
-
- const beforeCount = await rows.count();
-
- await pendingRow.scrollIntoViewIfNeeded();
- const actionsButton = pendingRow.getByTestId("agent-table-row-actions");
- await actionsButton.waitFor({ state: "visible", timeout: 10000 });
- await actionsButton.scrollIntoViewIfNeeded();
- await actionsButton.click();
-
- const editButton = page.getByRole("menuitem", { name: "Edit" });
- await expect(editButton).toBeVisible();
- await editButton.click();
-
- const editModal = page.getByTestId("edit-agent-modal");
- await expect(editModal).toBeVisible();
-
- const newTitle = `E2E Edit Pending ${Date.now()}`;
- await page.getByRole("textbox", { name: "Title" }).fill(newTitle);
- await page
- .getByRole("textbox", { name: "Changes Summary" })
- .fill("E2E change - pending -> same submission");
-
- await page.getByRole("button", { name: "Update submission" }).click();
- await expect(editModal).not.toBeVisible();
-
- // Count should remain the same
- await expect(async () => {
- const afterCount = await rows.count();
- expect(afterCount).toBe(beforeCount);
- }).toPass();
-
- const updatedRow = rows.filter({ hasText: newTitle }).first();
- await expect(updatedRow).toBeVisible();
- await expect(updatedRow).toContainText(/Awaiting review/);
-});
diff --git a/autogpt_platform/frontend/src/tests/api-keys.spec.ts b/autogpt_platform/frontend/src/tests/api-keys.spec.ts
deleted file mode 100644
index 8c59ced981..0000000000
--- a/autogpt_platform/frontend/src/tests/api-keys.spec.ts
+++ /dev/null
@@ -1,65 +0,0 @@
-import { expect, test } from "./coverage-fixture";
-import { getTestUserWithLibraryAgents } from "./credentials";
-import { LoginPage } from "./pages/login.page";
-import { hasUrl } from "./utils/assertion";
-import { getSelectors } from "./utils/selectors";
-
-test.describe("API Keys Page", () => {
- test.beforeEach(async ({ page }) => {
- const loginPage = new LoginPage(page);
- await page.goto("/login");
- const richUser = getTestUserWithLibraryAgents();
- await loginPage.login(richUser.email, richUser.password);
- await hasUrl(page, "/marketplace");
- });
-
- test("should redirect to login page when user is not authenticated", async ({
- browser,
- }) => {
- const context = await browser.newContext();
- const page = await context.newPage();
-
- try {
- await page.goto("/profile/api-keys");
- await hasUrl(page, "/login?next=%2Fprofile%2Fapi-keys");
- } finally {
- await page.close();
- await context.close();
- }
- });
-
- test("should create a new API key successfully", async ({ page }) => {
- const { getButton, getField } = getSelectors(page);
- await page.goto("/profile/api-keys");
- await getButton("Create Key").click();
-
- await getField("Name").fill("Test Key");
- await getButton("Create").click();
-
- await expect(
- page.getByText("AutoGPT Platform API Key Created"),
- ).toBeVisible();
- await getButton("Close").first().click();
-
- await expect(page.getByText("Test Key").first()).toBeVisible();
- });
-
- test("should revoke an existing API key", async ({ page }) => {
- const { getRole, getId } = getSelectors(page);
- await page.goto("/profile/api-keys");
-
- const apiKeyRow = getId("api-key-row").first();
- const apiKeyContent = await apiKeyRow
- .getByTestId("api-key-id")
- .textContent();
- const apiKeyActions = apiKeyRow.getByTestId("api-key-actions").first();
-
- await apiKeyActions.click();
- await getRole("menuitem", "Revoke").click();
- await expect(
- page.getByText("AutoGPT Platform API key revoked successfully"),
- ).toBeVisible();
-
- await expect(page.getByText(apiKeyContent!)).not.toBeVisible();
- });
-});
diff --git a/autogpt_platform/frontend/src/tests/build.spec.ts b/autogpt_platform/frontend/src/tests/build.spec.ts
deleted file mode 100644
index ad0b9524d0..0000000000
--- a/autogpt_platform/frontend/src/tests/build.spec.ts
+++ /dev/null
@@ -1,134 +0,0 @@
-import { test, expect } from "./coverage-fixture";
-import { BuildPage } from "./pages/build.page";
-import { LoginPage } from "./pages/login.page";
-import { hasUrl } from "./utils/assertion";
-import { getTestUser } from "./utils/auth";
-
-test.describe("Builder", () => {
- let buildPage: BuildPage;
-
- test.beforeEach(async ({ page }) => {
- test.setTimeout(60000);
- const loginPage = new LoginPage(page);
- const testUser = await getTestUser();
-
- buildPage = new BuildPage(page);
-
- await page.goto("/login");
- await loginPage.login(testUser.email, testUser.password);
- await hasUrl(page, "/marketplace");
-
- await page.goto("/build");
- await page.waitForLoadState("domcontentloaded");
- await buildPage.closeTutorial();
- });
-
- // --- Core tests ---
-
- 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 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 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 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 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 save and run button becomes enabled", async () => {
- await buildPage.addBlockByClick("Store Value");
- await buildPage.waitForNodeOnCanvas(1);
-
- await buildPage.saveAgent("Runnable Agent", "Test run button");
- await buildPage.waitForSaveComplete();
- await buildPage.waitForSaveButton();
-
- await expect(buildPage.isRunButtonEnabled()).resolves.toBeTruthy();
- });
-
- // --- Copy / Paste test ---
-
- test("user can copy and paste a node", async ({ context }) => {
- await context.grantPermissions(["clipboard-read", "clipboard-write"]);
-
- await buildPage.addBlockByClick("Store Value");
- await buildPage.waitForNodeOnCanvas(1);
-
- await buildPage.selectNode(0);
- await buildPage.copyViaKeyboard();
- await buildPage.pasteViaKeyboard();
-
- await buildPage.waitForNodeOnCanvas(2);
- expect(await buildPage.getNodeCount()).toBe(2);
- });
-
- // --- Run agent test ---
-
- test("user can run an agent from the builder", async () => {
- await buildPage.addBlockByClick("Store Value");
- await buildPage.waitForNodeOnCanvas(1);
-
- // Save the agent (required before running)
- await buildPage.saveAgent("Run Test Agent", "Testing run from builder");
- await buildPage.waitForSaveComplete();
- await buildPage.waitForSaveButton();
-
- // Click run button
- await buildPage.clickRunButton();
-
- // 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");
-
- expect(["dialog", "running"]).toContain(runDialogOrRunning);
- });
-});
diff --git a/autogpt_platform/frontend/src/tests/credentials/index.ts b/autogpt_platform/frontend/src/tests/credentials/index.ts
deleted file mode 100644
index bc4663a045..0000000000
--- a/autogpt_platform/frontend/src/tests/credentials/index.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-// E2E Test Credentials and Constants
-export const TEST_CREDENTIALS = {
- email: "test123@gmail.com",
- password: "testpassword123",
-} as const;
-
-export function getTestUserWithLibraryAgents() {
- return TEST_CREDENTIALS;
-}
-
-// Dummy constant to help developers identify agents that don't need input
-export const DummyInput = "DummyInput";
-
-// This will be used for testing agent submission for test123@gmail.com
-export const TEST_AGENT_DATA = {
- name: "Test Agent Submission",
- description:
- "This is a test agent submission specifically created for frontend testing purposes.",
- image_urls: [
- "https://picsum.photos/200/300",
- "https://picsum.photos/200/301",
- "https://picsum.photos/200/302",
- ],
- video_url: "https://www.youtube.com/watch?v=test123",
- sub_heading: "A test agent for frontend testing",
- categories: ["test", "demo", "frontend"],
- changes_summary: "Initial test submission",
-} as const;
diff --git a/autogpt_platform/frontend/src/tests/global-setup.ts b/autogpt_platform/frontend/src/tests/global-setup.ts
deleted file mode 100644
index 901eb117ef..0000000000
--- a/autogpt_platform/frontend/src/tests/global-setup.ts
+++ /dev/null
@@ -1,52 +0,0 @@
-import { FullConfig } from "@playwright/test";
-import { createTestUsers, saveUserPool, loadUserPool } from "./utils/auth";
-
-async function globalSetup(config: FullConfig) {
- console.log("🚀 Starting global test setup...");
-
- try {
- const existingUserPool = await loadUserPool();
-
- if (existingUserPool && existingUserPool.users.length > 0) {
- console.log(
- `♻️ Found existing user pool with ${existingUserPool.users.length} users`,
- );
- console.log("✅ Using existing user pool");
- return;
- }
-
- // Create test users using signup page
- const numberOfUsers = (config.workers || 1) + 8; // workers + buffer
- console.log(`👥 Creating ${numberOfUsers} test users via signup...`);
- console.log("⏳ Note: This may take a few minutes in CI environments");
-
- const users = await createTestUsers(numberOfUsers);
-
- if (users.length === 0) {
- throw new Error("Failed to create any test users");
- }
-
- // Require at least a minimum number of users for tests to work
- const minUsers = Math.max(config.workers || 1, 2);
- if (users.length < minUsers) {
- throw new Error(
- `Only created ${users.length} users but need at least ${minUsers} for tests to run properly`,
- );
- }
-
- // Save user pool
- await saveUserPool(users);
-
- console.log("✅ Global setup completed successfully!");
- console.log(`📊 Created ${users.length} test users via signup page`);
- } catch (error) {
- console.error("❌ Global setup failed:", error);
- console.error("💡 This is likely due to:");
- console.error(" 1. Backend services not fully ready");
- console.error(" 2. Network timeouts in CI environment");
- console.error(" 3. Database or authentication issues");
- throw error;
- }
-}
-
-export default globalSetup;
diff --git a/autogpt_platform/frontend/src/tests/integrations/vitest.setup.tsx b/autogpt_platform/frontend/src/tests/integrations/vitest.setup.tsx
index bda6a2679d..c4931856bc 100644
--- a/autogpt_platform/frontend/src/tests/integrations/vitest.setup.tsx
+++ b/autogpt_platform/frontend/src/tests/integrations/vitest.setup.tsx
@@ -2,11 +2,15 @@ import { beforeAll, afterAll, afterEach } from "vitest";
import { server } from "@/mocks/mock-server";
import { mockNextjsModules } from "./setup-nextjs-mocks";
import { mockSupabaseRequest } from "./mock-supabase-request";
+import { cleanup } from "@testing-library/react";
beforeAll(() => {
mockNextjsModules();
mockSupabaseRequest(); // If you need user's data - please mock supabase actions in your specific test - it sends null user [It's only to avoid cookies() call]
return server.listen({ onUnhandledRequest: "error" });
});
-afterEach(() => server.resetHandlers());
+afterEach(() => {
+ cleanup();
+ server.resetHandlers();
+});
afterAll(() => server.close());
diff --git a/autogpt_platform/frontend/src/tests/library.spec.ts b/autogpt_platform/frontend/src/tests/library.spec.ts
deleted file mode 100644
index 98ba698398..0000000000
--- a/autogpt_platform/frontend/src/tests/library.spec.ts
+++ /dev/null
@@ -1,250 +0,0 @@
-import { test, expect } from "./coverage-fixture";
-import path from "path";
-import { getTestUserWithLibraryAgents } from "./credentials";
-import { LibraryPage } from "./pages/library.page";
-import { LoginPage } from "./pages/login.page";
-import { hasUrl } from "./utils/assertion";
-import { getSelectors } from "./utils/selectors";
-
-test.describe("Library", () => {
- let libraryPage: LibraryPage;
-
- test.beforeEach(async ({ page }) => {
- libraryPage = new LibraryPage(page);
-
- await page.goto("/login");
- const loginPage = new LoginPage(page);
- const richUser = getTestUserWithLibraryAgents();
- await loginPage.login(richUser.email, richUser.password);
- await hasUrl(page, "/marketplace");
- });
-
- test("library page loads successfully", async ({ page }) => {
- const { getId } = getSelectors(page);
- await page.goto("/library");
-
- await expect(getId("search-bar").first()).toBeVisible();
- await expect(getId("import-button").first()).toBeVisible();
- await expect(getId("sort-by-dropdown").first()).toBeVisible();
- });
-
- test("agents are visible and cards work correctly", async ({ page }) => {
- await page.goto("/library");
-
- const agents = await libraryPage.getAgents();
- expect(agents.length).toBeGreaterThan(0);
-
- const firstAgent = agents[0];
- expect(firstAgent).toBeTruthy();
-
- await libraryPage.clickAgent(firstAgent);
- await hasUrl(page, `/library/agents/${firstAgent.id}`);
-
- await libraryPage.navigateToLibrary();
-
- const updatedAgents = await libraryPage.getAgents();
- const agentWithBuilder = updatedAgents.find((agent) =>
- agent.openInBuilderUrl.includes("/build"),
- );
-
- if (agentWithBuilder) {
- const [newPage] = await Promise.all([
- page.context().waitForEvent("page"),
- libraryPage.clickOpenInBuilder(agentWithBuilder),
- ]);
- await newPage.waitForLoadState();
- test.expect(newPage.url()).toContain(`/build`);
- await newPage.close();
- }
- });
-
- test("pagination works correctly", async ({ page }, testInfo) => {
- test.setTimeout(testInfo.timeout * 3);
- await page.goto("/library");
-
- const PAGE_SIZE = 20;
- const paginationResult = await libraryPage.testPagination();
-
- if (paginationResult.initialCount >= PAGE_SIZE) {
- expect(paginationResult.finalCount).toBeGreaterThanOrEqual(
- paginationResult.initialCount,
- );
- expect(paginationResult.hasMore).toBeTruthy();
- }
-
- await libraryPage.isPaginationWorking();
-
- const allAgents = await libraryPage.getAgentsWithPagination();
- test.expect(allAgents.length).toBeGreaterThan(0);
-
- const displayedCount = await libraryPage.getAgentCount();
- test.expect(allAgents.length).toEqual(displayedCount);
- });
-
- test("searching works correctly", async ({ page }) => {
- await page.goto("/library");
-
- const allAgents = await libraryPage.getAgents();
- expect(allAgents.length).toBeGreaterThan(0);
-
- const initialAgentCount = await libraryPage.getAgentCount();
- expect(initialAgentCount).toBeGreaterThan(0);
-
- const firstAgent = allAgents[0];
- await libraryPage.searchAgents(firstAgent.name);
- await libraryPage.waitForAgentsToLoad();
-
- const searchResults = await libraryPage.getAgents();
- expect(searchResults.length).toBeGreaterThan(0);
-
- const foundAgent = searchResults.find(
- (agent) => agent.name === firstAgent.name,
- );
- expect(foundAgent).toBeTruthy();
-
- const searchValue = await libraryPage.getSearchValue();
- expect(searchValue).toBe(firstAgent.name);
-
- const partialSearchTerm = firstAgent.name.substring(0, 3);
- await libraryPage.searchAgents(partialSearchTerm);
- await libraryPage.waitForAgentsToLoad();
-
- const partialSearchResults = await libraryPage.getAgents();
- expect(partialSearchResults.length).toBeGreaterThan(0);
-
- const matchingAgents = partialSearchResults.filter((agent) =>
- agent.name.toLowerCase().includes(partialSearchTerm.toLowerCase()),
- );
- expect(matchingAgents.length).toBeGreaterThan(0);
-
- await libraryPage.searchAgents("nonexistentagentnamethatdoesnotexist");
- const noResults = await libraryPage.getAgentCount();
- expect(noResults).toBe(0);
-
- const hasNoAgentsMessage = await libraryPage.hasNoAgentsMessage();
- expect(hasNoAgentsMessage).toBeTruthy();
-
- await libraryPage.clearSearch();
- await libraryPage.waitForAgentsToLoad();
-
- const clearedSearchCount = await libraryPage.getAgentCount();
- test.expect(clearedSearchCount).toEqual(initialAgentCount);
-
- const clearedSearchValue = await libraryPage.getSearchValue();
- test.expect(clearedSearchValue).toBe("");
- });
-
- test("pagination while searching works correctly", async ({
- page,
- }, testInfo) => {
- test.setTimeout(testInfo.timeout * 3);
- await page.goto("/library");
-
- const allAgents = await libraryPage.getAgents();
- test.expect(allAgents.length).toBeGreaterThan(0);
-
- const searchTerm = "Agent";
-
- await libraryPage.searchAgents(searchTerm);
- await libraryPage.waitForAgentsToLoad();
-
- const initialSearchResults = await libraryPage.getAgents();
- expect(initialSearchResults.length).toBeGreaterThan(0);
-
- const matchingResults = initialSearchResults.filter((agent) =>
- agent.name.toLowerCase().includes(searchTerm.toLowerCase()),
- );
- expect(matchingResults.length).toEqual(initialSearchResults.length);
-
- const PAGE_SIZE = 20;
- const searchPaginationResult = await libraryPage.testPagination();
-
- if (searchPaginationResult.initialCount >= PAGE_SIZE) {
- expect(searchPaginationResult.finalCount).toBeGreaterThanOrEqual(
- searchPaginationResult.initialCount,
- );
-
- const allPaginatedResults = await libraryPage.getAgentsWithPagination();
- const matchingPaginatedResults = allPaginatedResults.filter((agent) =>
- agent.name.toLowerCase().includes(searchTerm.toLowerCase()),
- );
- expect(matchingPaginatedResults.length).toEqual(
- allPaginatedResults.length,
- );
- }
-
- await libraryPage.scrollAndWaitForNewAgents();
-
- const finalSearchResults = await libraryPage.getAgents();
- const finalMatchingResults = finalSearchResults.filter((agent) =>
- agent.name.toLowerCase().includes(searchTerm.toLowerCase()),
- );
- expect(finalMatchingResults.length).toEqual(finalSearchResults.length);
-
- const preservedSearchValue = await libraryPage.getSearchValue();
- expect(preservedSearchValue).toBe(searchTerm);
-
- await libraryPage.clearSearch();
- await libraryPage.waitForAgentsToLoad();
-
- const clearedResults = await libraryPage.getAgents();
- expect(clearedResults.length).toBeGreaterThanOrEqual(
- initialSearchResults.length,
- );
- });
-
- test("uploading an agent works correctly", async ({ page }) => {
- await page.goto("/library");
-
- await libraryPage.openUploadDialog();
-
- expect(await libraryPage.isUploadDialogVisible()).toBeTruthy();
- expect(await libraryPage.isUploadButtonEnabled()).toBeFalsy();
-
- const testAgentName = "Test Upload Agent";
- const testAgentDescription = "This is a test agent uploaded via automation";
- await libraryPage.fillUploadForm(testAgentName, testAgentDescription);
-
- const fileInput = page.locator('input[type="file"]');
- const testAgentPath = path.resolve(
- __dirname,
- "assets",
- "testing_agent.json",
- );
- await fileInput.setInputFiles(testAgentPath);
-
- // Wait for file to be processed and upload button to be enabled
- const uploadButton = page.getByRole("button", { name: "Upload" });
- await uploadButton.waitFor({ state: "visible", timeout: 10000 });
- await expect(uploadButton).toBeEnabled({ timeout: 10000 });
-
- expect(await libraryPage.isUploadButtonEnabled()).toBeTruthy();
-
- await page.getByRole("button", { name: "Upload" }).click();
-
- await page.waitForURL("**/build**", { timeout: 10000 });
- expect(page.url()).toContain("/build");
-
- await page.goto("/library");
-
- await libraryPage.searchAgents(testAgentName);
- await libraryPage.waitForAgentsToLoad();
-
- const searchResults = await libraryPage.getAgents();
- test.expect(searchResults.length).toBeGreaterThan(0);
-
- const uploadedAgent = searchResults.find((agent) =>
- agent.name.includes(testAgentName),
- );
- test.expect(uploadedAgent).toBeTruthy();
-
- if (uploadedAgent) {
- test.expect(uploadedAgent.name).toContain(testAgentName);
- test.expect(uploadedAgent.seeRunsUrl).toBeTruthy();
- test.expect(uploadedAgent.openInBuilderUrl).toBeTruthy();
- }
-
- await libraryPage.clearSearch();
- await libraryPage.waitForAgentsToLoad();
- });
-});
diff --git a/autogpt_platform/frontend/src/tests/marketplace-agent.spec.ts b/autogpt_platform/frontend/src/tests/marketplace-agent.spec.ts
deleted file mode 100644
index fb38b90d63..0000000000
--- a/autogpt_platform/frontend/src/tests/marketplace-agent.spec.ts
+++ /dev/null
@@ -1,120 +0,0 @@
-import { expect, test } from "./coverage-fixture";
-import { getTestUserWithLibraryAgents } from "./credentials";
-import { LoginPage } from "./pages/login.page";
-import { MarketplacePage } from "./pages/marketplace.page";
-import { hasUrl, isVisible, matchesUrl } from "./utils/assertion";
-import { getSelectors } from "./utils/selectors";
-
-function escapeRegExp(value: string) {
- return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
-}
-
-test.describe("Marketplace Agent Page - Basic Functionality", () => {
- test("User can access agent page when logged out", async ({ page }) => {
- const marketplacePage = new MarketplacePage(page);
-
- await marketplacePage.goto(page);
- await hasUrl(page, "/marketplace");
-
- const firstStoreCard = await marketplacePage.getFirstTopAgent();
- await firstStoreCard.click();
-
- await page.waitForURL("**/marketplace/agent/**");
- await matchesUrl(page, /\/marketplace\/agent\/.+/);
- });
-
- test("User can access agent page when logged in", async ({ page }) => {
- const loginPage = new LoginPage(page);
- const marketplacePage = new MarketplacePage(page);
-
- await loginPage.goto();
- const richUser = getTestUserWithLibraryAgents();
- await loginPage.login(richUser.email, richUser.password);
- await hasUrl(page, "/marketplace");
- await marketplacePage.goto(page);
- await hasUrl(page, "/marketplace");
-
- const firstStoreCard = await marketplacePage.getFirstTopAgent();
- await firstStoreCard.click();
-
- await page.waitForURL("**/marketplace/agent/**");
- await matchesUrl(page, /\/marketplace\/agent\/.+/);
- });
-
- test("Agent page details are visible", async ({ page }) => {
- const { getId } = getSelectors(page);
-
- const marketplacePage = new MarketplacePage(page);
- await marketplacePage.goto(page);
-
- const firstStoreCard = await marketplacePage.getFirstTopAgent();
- await firstStoreCard.click();
- await page.waitForURL("**/marketplace/agent/**");
-
- const agentTitle = getId("agent-title");
- await isVisible(agentTitle);
-
- const agentDescription = getId("agent-description");
- await isVisible(agentDescription);
-
- const creatorInfo = getId("agent-creator");
- await isVisible(creatorInfo);
- });
-
- test("Download button functionality works", async ({ page }) => {
- const { getId, getText } = getSelectors(page);
-
- const marketplacePage = new MarketplacePage(page);
- await marketplacePage.goto(page);
-
- const firstStoreCard = await marketplacePage.getFirstTopAgent();
- await firstStoreCard.click();
- await page.waitForURL("**/marketplace/agent/**");
-
- const downloadButton = getId("agent-download-button");
- await isVisible(downloadButton);
- await downloadButton.click();
-
- const downloadSuccessMessage = getText(
- "Your agent has been successfully downloaded.",
- );
- await isVisible(downloadSuccessMessage);
- });
-
- test("Add to library button works and agent appears in library", async ({
- page,
- }) => {
- const { getId, getText } = getSelectors(page);
-
- const loginPage = new LoginPage(page);
- const marketplacePage = new MarketplacePage(page);
-
- await loginPage.goto();
- const richUser = getTestUserWithLibraryAgents();
- await loginPage.login(richUser.email, richUser.password);
- await hasUrl(page, "/marketplace");
- await marketplacePage.goto(page);
-
- const firstStoreCard = await marketplacePage.getFirstTopAgent();
- await firstStoreCard.click();
- await page.waitForURL("**/marketplace/agent/**");
-
- const agentTitle = await getId("agent-title").textContent();
- if (!agentTitle || !agentTitle.trim()) {
- throw new Error("Agent title not found on marketplace agent page");
- }
- const agentName = agentTitle.trim();
-
- const addToLibraryButton = getId("agent-add-library-button");
- await isVisible(addToLibraryButton);
- await addToLibraryButton.click();
-
- const addSuccessMessage = getText("Redirecting to your library...");
- await isVisible(addSuccessMessage);
-
- await page.waitForURL("**/library/agents/**");
- await expect(page).toHaveTitle(
- new RegExp(`${escapeRegExp(agentName)} - Library - AutoGPT Platform`),
- );
- });
-});
diff --git a/autogpt_platform/frontend/src/tests/marketplace-creator.spec.ts b/autogpt_platform/frontend/src/tests/marketplace-creator.spec.ts
deleted file mode 100644
index 6fbf4d39be..0000000000
--- a/autogpt_platform/frontend/src/tests/marketplace-creator.spec.ts
+++ /dev/null
@@ -1,82 +0,0 @@
-import { test } from "./coverage-fixture";
-import { getTestUserWithLibraryAgents } from "./credentials";
-import { LoginPage } from "./pages/login.page";
-import { MarketplacePage } from "./pages/marketplace.page";
-import { hasUrl, isVisible, matchesUrl } from "./utils/assertion";
-import { getSelectors } from "./utils/selectors";
-
-test.describe("Marketplace Creator Page – Basic Functionality", () => {
- test("User can access creator's page when logged out", async ({ page }) => {
- const marketplacePage = new MarketplacePage(page);
-
- await marketplacePage.goto(page);
- await hasUrl(page, "/marketplace");
-
- const firstCreatorProfile =
- await marketplacePage.getFirstCreatorProfile(page);
- await firstCreatorProfile.click();
-
- await page.waitForURL("**/marketplace/creator/**");
- await matchesUrl(page, /\/marketplace\/creator\/.+/);
- });
-
- test("User can access creator's page when logged in", async ({ page }) => {
- const loginPage = new LoginPage(page);
- const marketplacePage = new MarketplacePage(page);
-
- await loginPage.goto();
- const richUser = getTestUserWithLibraryAgents();
- await loginPage.login(richUser.email, richUser.password);
- await hasUrl(page, "/marketplace");
-
- await marketplacePage.goto(page);
- await hasUrl(page, "/marketplace");
-
- const firstCreatorProfile =
- await marketplacePage.getFirstCreatorProfile(page);
- await firstCreatorProfile.click();
-
- await page.waitForURL("**/marketplace/creator/**");
- await matchesUrl(page, /\/marketplace\/creator\/.+/);
- });
-
- test("Creator page details are visible", async ({ page }) => {
- const { getId } = getSelectors(page);
- const marketplacePage = new MarketplacePage(page);
-
- await marketplacePage.goto(page);
- await hasUrl(page, "/marketplace");
-
- const firstCreatorProfile =
- await marketplacePage.getFirstCreatorProfile(page);
- await firstCreatorProfile.click();
- await page.waitForURL("**/marketplace/creator/**");
-
- const creatorTitle = getId("creator-title");
- await isVisible(creatorTitle);
-
- const creatorDescription = getId("creator-description");
- await isVisible(creatorDescription);
- });
-
- test("Agents in agent by sections navigation works", async ({ page }) => {
- const marketplacePage = new MarketplacePage(page);
-
- await marketplacePage.goto(page);
- await hasUrl(page, "/marketplace");
-
- const firstCreatorProfile =
- await marketplacePage.getFirstCreatorProfile(page);
- await firstCreatorProfile.click();
- await page.waitForURL("**/marketplace/creator/**");
-
- const firstAgent = page
- .locator('[data-testid="store-card"]:visible')
- .first();
- await firstAgent.waitFor({ state: "visible", timeout: 15000 });
-
- await firstAgent.click();
- await page.waitForURL("**/marketplace/agent/**");
- await matchesUrl(page, /\/marketplace\/agent\/.+/);
- });
-});
diff --git a/autogpt_platform/frontend/src/tests/marketplace.spec.ts b/autogpt_platform/frontend/src/tests/marketplace.spec.ts
deleted file mode 100644
index 83b0d81d92..0000000000
--- a/autogpt_platform/frontend/src/tests/marketplace.spec.ts
+++ /dev/null
@@ -1,168 +0,0 @@
-import { expect, test } from "./coverage-fixture";
-import { getTestUserWithLibraryAgents } from "./credentials";
-import { LoginPage } from "./pages/login.page";
-import { MarketplacePage } from "./pages/marketplace.page";
-import { hasMinCount, hasUrl, isVisible, matchesUrl } from "./utils/assertion";
-
-// Marketplace tests for store agent search functionality
-test.describe("Marketplace – Basic Functionality", () => {
- test("User can access marketplace page when logged out", async ({ page }) => {
- const marketplacePage = new MarketplacePage(page);
-
- await marketplacePage.goto(page);
- await hasUrl(page, "/marketplace");
-
- const marketplaceTitle = await marketplacePage.getMarketplaceTitle(page);
- await isVisible(marketplaceTitle);
-
- console.log(
- "User can access marketplace page when logged out test passed ✅",
- );
- });
-
- test("User can access marketplace page when logged in", async ({ page }) => {
- const loginPage = new LoginPage(page);
- const marketplacePage = new MarketplacePage(page);
-
- await loginPage.goto();
- const richUser = getTestUserWithLibraryAgents();
- await loginPage.login(richUser.email, richUser.password);
- await hasUrl(page, "/marketplace");
-
- await marketplacePage.goto(page);
- await hasUrl(page, "/marketplace");
-
- const marketplaceTitle = await marketplacePage.getMarketplaceTitle(page);
- await isVisible(marketplaceTitle);
-
- console.log(
- "User can access marketplace page when logged in test passed ✅",
- );
- });
-
- test("Featured agents, top agents, and featured creators are visible", async ({
- page,
- }) => {
- const marketplacePage = new MarketplacePage(page);
- await marketplacePage.goto(page);
-
- const featuredAgentsSection =
- await marketplacePage.getFeaturedAgentsSection(page);
- await isVisible(featuredAgentsSection);
- const featuredAgentCards =
- await marketplacePage.getFeaturedAgentCards(page);
- await hasMinCount(featuredAgentCards, 1);
-
- const topAgentsSection = await marketplacePage.getTopAgentsSection(page);
- await isVisible(topAgentsSection);
- const topAgentCards = await marketplacePage.getTopAgentCards(page);
- await hasMinCount(topAgentCards, 1);
-
- const featuredCreatorsSection =
- await marketplacePage.getFeaturedCreatorsSection(page);
- await isVisible(featuredCreatorsSection);
- const creatorProfiles = await marketplacePage.getCreatorProfiles(page);
- await hasMinCount(creatorProfiles, 1);
-
- console.log(
- "Featured agents, top agents, and featured creators are visible test passed ✅",
- );
- });
-
- test("Can navigate and interact with marketplace elements", async ({
- page,
- }) => {
- const marketplacePage = new MarketplacePage(page);
- await marketplacePage.goto(page);
-
- const firstFeaturedAgent =
- await marketplacePage.getFirstFeaturedAgent(page);
- await firstFeaturedAgent.click();
- await page.waitForURL("**/marketplace/agent/**");
- await matchesUrl(page, /\/marketplace\/agent\/.+/);
- await marketplacePage.goto(page);
-
- const firstTopAgent = await marketplacePage.getFirstTopAgent();
- await firstTopAgent.click();
- await page.waitForURL("**/marketplace/agent/**");
- await matchesUrl(page, /\/marketplace\/agent\/.+/);
- await marketplacePage.goto(page);
-
- const firstCreatorProfile =
- await marketplacePage.getFirstCreatorProfile(page);
- await firstCreatorProfile.click();
- await page.waitForURL("**/marketplace/creator/**");
- await matchesUrl(page, /\/marketplace\/creator\/.+/);
-
- console.log(
- "Can navigate and interact with marketplace elements test passed ✅",
- );
- });
-
- test("Complete search flow works correctly", async ({ page }) => {
- const marketplacePage = new MarketplacePage(page);
- await marketplacePage.goto(page);
-
- await marketplacePage.searchAndNavigate("DummyInput", page);
-
- await marketplacePage.waitForSearchResults();
-
- await matchesUrl(page, /\/marketplace\/search\?searchTerm=/);
-
- const resultsHeading = page.getByText("Results for:");
- await isVisible(resultsHeading);
-
- const searchTerm = page.getByText("DummyInput").first();
- await isVisible(searchTerm);
-
- await expect
- .poll(() => marketplacePage.getSearchResultsCount(page), {
- timeout: 15000,
- })
- .toBeGreaterThan(0);
-
- console.log("Complete search flow works correctly test passed ✅");
- });
-
- // We need to add a test search with filters, but the current business logic for filters doesn't work as expected. We'll add it once we modify that.
-});
-
-test.describe("Marketplace – Edge Cases", () => {
- test("Search for non-existent item renders search page correctly", async ({
- page,
- }) => {
- const marketplacePage = new MarketplacePage(page);
- await marketplacePage.goto(page);
-
- await marketplacePage.searchAndNavigate("xyznonexistentitemxyz123", page);
-
- await marketplacePage.waitForSearchResults();
-
- await matchesUrl(page, /\/marketplace\/search\?searchTerm=/);
-
- const resultsHeading = page.getByText("Results for:");
- await isVisible(resultsHeading);
-
- const searchTerm = page.getByText("xyznonexistentitemxyz123");
- await isVisible(searchTerm);
-
- // The search page should render either results or a "No results found" message
- await expect
- .poll(
- async () => {
- const hasResults =
- (await page.locator('[data-testid="store-card"]').count()) > 0;
- const hasNoResultsMsg = await page
- .getByText("No results found")
- .isVisible();
- return hasResults || hasNoResultsMsg;
- },
- { timeout: 15000 },
- )
- .toBe(true);
-
- console.log(
- "Search for non-existent item renders search page correctly test passed ✅",
- );
- });
-});
diff --git a/autogpt_platform/frontend/src/tests/onboarding.spec.ts b/autogpt_platform/frontend/src/tests/onboarding.spec.ts
deleted file mode 100644
index 321469c268..0000000000
--- a/autogpt_platform/frontend/src/tests/onboarding.spec.ts
+++ /dev/null
@@ -1,114 +0,0 @@
-import { test, expect } from "./coverage-fixture";
-import { signupTestUser } from "./utils/signup";
-import { completeOnboardingWizard } from "./utils/onboarding";
-import { getSelectors } from "./utils/selectors";
-
-test("new user completes full onboarding wizard", async ({ page }) => {
- // Signup WITHOUT skipping onboarding (ignoreOnboarding=false)
- await signupTestUser(page, undefined, undefined, false);
-
- // Should be on onboarding
- await expect(page).toHaveURL(/\/onboarding/);
-
- // Complete the wizard
- await completeOnboardingWizard(page, {
- name: "Alice",
- role: "Marketing",
- painPoints: ["Social media", "Email & outreach"],
- });
-
- // Should have been redirected to /copilot
- await expect(page).toHaveURL(/\/copilot/);
-
- // User should be authenticated
- await page
- .getByTestId("profile-popout-menu-trigger")
- .waitFor({ state: "visible", timeout: 10000 });
-});
-
-test("onboarding wizard step navigation works", async ({ page }) => {
- await signupTestUser(page, undefined, undefined, false);
- await expect(page).toHaveURL(/\/onboarding/);
-
- // Step 1: Welcome
- await expect(page.getByText("Welcome to AutoGPT")).toBeVisible();
- await page.getByLabel("What should I call you?").fill("Bob");
- await page.getByRole("button", { name: "Continue" }).click();
-
- // Step 2: Role — verify we're here, then go back
- await expect(page.getByText("What best describes you")).toBeVisible();
- await page.getByText("Back").click();
-
- // Should be back on step 1 with name preserved
- await expect(page.getByText("Welcome to AutoGPT")).toBeVisible();
- await expect(page.getByLabel("What should I call you?")).toHaveValue("Bob");
-});
-
-test("onboarding wizard validates required fields", async ({ page }) => {
- await signupTestUser(page, undefined, undefined, false);
- await expect(page).toHaveURL(/\/onboarding/);
-
- // Step 1: Continue should be disabled without a name
- const continueButton = page.getByRole("button", { name: "Continue" });
- await expect(continueButton).toBeDisabled();
-
- // Fill name — continue should become enabled
- await page.getByLabel("What should I call you?").fill("Charlie");
- await expect(continueButton).toBeEnabled();
- await continueButton.click();
-
- // Step 2: Role — selecting auto-advances to step 3
- await expect(page.getByText("What best describes you")).toBeVisible();
- await page.getByText("Engineering").click();
-
- // Step 3: Launch Autopilot should be disabled without any pain points
- const launchButton = page.getByRole("button", { name: "Launch Autopilot" });
- await expect(launchButton).toBeDisabled();
-
- // Select a pain point — button should become enabled
- await page.getByText("Research", { exact: true }).click();
- await expect(launchButton).toBeEnabled();
-});
-
-test("completed onboarding redirects away from /onboarding", async ({
- page,
-}) => {
- // Create user and complete onboarding
- await signupTestUser(page, undefined, undefined, false);
- await completeOnboardingWizard(page);
-
- // Try to navigate back to onboarding — should be redirected to /copilot
- await page.goto("http://localhost:3000/onboarding");
- await page.waitForURL(/\/copilot/, { timeout: 10000 });
-});
-
-test("onboarding URL params sync with steps", async ({ page }) => {
- await signupTestUser(page, undefined, undefined, false);
- await expect(page).toHaveURL(/\/onboarding/);
-
- // Step 1: URL may or may not include step=1 on initial load (no param is equivalent to step 1)
- await expect(page.getByText("Welcome to AutoGPT")).toBeVisible();
-
- // Fill name and go to step 2
- await page.getByLabel("What should I call you?").fill("Test");
- await page.getByRole("button", { name: "Continue" }).click();
-
- // URL should show step=2
- await expect(page).toHaveURL(/step=2/);
-});
-
-test("role-based pain point ordering works", async ({ page }) => {
- await signupTestUser(page, undefined, undefined, false);
-
- // Complete step 1
- await page.getByLabel("What should I call you?").fill("Test");
- await page.getByRole("button", { name: "Continue" }).click();
-
- // Select Sales/BD role (auto-advances to step 3)
- await page.getByText("Sales / BD").click();
-
- // On pain points step, "Finding leads" should be visible (top pick for Sales)
- await expect(page.getByText("What's eating your time?")).toBeVisible();
- const { getText } = getSelectors(page);
- await expect(getText("Finding leads")).toBeVisible();
-});
diff --git a/autogpt_platform/frontend/src/tests/pages/build.page.ts b/autogpt_platform/frontend/src/tests/pages/build.page.ts
deleted file mode 100644
index ad44f94f94..0000000000
--- a/autogpt_platform/frontend/src/tests/pages/build.page.ts
+++ /dev/null
@@ -1,310 +0,0 @@
-import { expect, Locator, Page } from "@playwright/test";
-import { BasePage } from "./base.page";
-
-export class BuildPage extends BasePage {
- constructor(page: Page) {
- super(page);
- }
-
- // --- Navigation ---
-
- async goto(): Promise {
- await this.page.goto("/build");
- await this.page.waitForLoadState("domcontentloaded");
- }
-
- async isLoaded(): Promise {
- 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 {
- try {
- await this.page
- .getByRole("button", { name: "Skip Tutorial", exact: true })
- .click({ timeout: 3000 });
- } catch {
- // Tutorial not shown or already dismissed
- }
- }
-
- // --- Block Menu ---
-
- async openBlocksPanel(): Promise {
- const popoverContent = this.page.locator(
- '[data-id="blocks-control-popover-content"]',
- );
- if (!(await popoverContent.isVisible())) {
- await this.page.getByTestId("blocks-control-blocks-button").click();
- await popoverContent.waitFor({ state: "visible", timeout: 5000 });
- }
- }
-
- async closeBlocksPanel(): Promise {
- const popoverContent = this.page.locator(
- '[data-id="blocks-control-popover-content"]',
- );
- 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 {
- 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 {
- 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 {
- 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 {
- return await this.getNodeLocator().count();
- }
-
- async waitForNodeOnCanvas(expectedCount?: number): Promise {
- 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 {
- const node = this.getNodeLocator(index);
- await node.click();
- }
-
- async selectAllNodes(): Promise {
- 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 {
- await this.page.keyboard.press("Backspace");
- }
-
- // --- Connections (Edges) ---
-
- async connectNodes(
- sourceNodeIndex: number,
- targetNodeIndex: number,
- ): Promise {
- // 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 {
- return await this.page.locator(".react-flow__edge").count();
- }
-
- // --- Save ---
-
- async saveAgent(
- name: string = "Test Agent",
- description: string = "",
- ): Promise {
- await this.page.getByTestId("save-control-save-button").click();
-
- 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 waitForSaveComplete(): Promise {
- await expect(this.page).toHaveURL(/flowID=/, { timeout: 15000 });
- }
-
- async waitForSaveButton(): Promise {
- await this.page.waitForSelector(
- '[data-testid="save-control-save-button"]:not([disabled])',
- { timeout: 10000 },
- );
- }
-
- // --- Run ---
-
- async isRunButtonEnabled(): Promise {
- const runButton = this.page.locator('[data-id="run-graph-button"]');
- return await runButton.isEnabled();
- }
-
- async clickRunButton(): Promise {
- const runButton = this.page.locator('[data-id="run-graph-button"]');
- await runButton.click();
- }
-
- // --- Undo / Redo ---
-
- async isUndoEnabled(): Promise {
- const btn = this.page.locator('[data-id="undo-button"]');
- return !(await btn.isDisabled());
- }
-
- async isRedoEnabled(): Promise {
- const btn = this.page.locator('[data-id="redo-button"]');
- return !(await btn.isDisabled());
- }
-
- async clickUndo(): Promise {
- await this.page.locator('[data-id="undo-button"]').click();
- }
-
- async clickRedo(): Promise {
- await this.page.locator('[data-id="redo-button"]').click();
- }
-
- // --- Copy / Paste ---
-
- async copyViaKeyboard(): Promise {
- const isMac = process.platform === "darwin";
- await this.page.keyboard.press(isMac ? "Meta+c" : "Control+c");
- }
-
- async pasteViaKeyboard(): Promise {
- 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 {
- const node = this.getNodeLocator(nodeIndex);
- const input = node.getByPlaceholder(placeholder);
- await input.fill(value);
- }
-
- async clickCanvas(): Promise {
- 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();
- }
- }
-
- getPlaywrightPage(): Page {
- return this.page;
- }
-
- async createDummyAgent(): Promise {
- await this.closeTutorial();
- await this.addBlockByClick("Add to Dictionary");
- await this.waitForNodeOnCanvas(1);
- await this.saveAgent("Test Agent", "Test Description");
- await this.waitForSaveComplete();
- }
-}
diff --git a/autogpt_platform/frontend/src/tests/pages/library.page.ts b/autogpt_platform/frontend/src/tests/pages/library.page.ts
deleted file mode 100644
index 716e6c3188..0000000000
--- a/autogpt_platform/frontend/src/tests/pages/library.page.ts
+++ /dev/null
@@ -1,559 +0,0 @@
-import { Locator, Page } from "@playwright/test";
-import { getSelectors } from "../utils/selectors";
-import { BasePage } from "./base.page";
-
-export interface Agent {
- id: string;
- name: string;
- description: string;
- imageUrl?: string;
- seeRunsUrl: string;
- openInBuilderUrl: string;
-}
-
-export class LibraryPage extends BasePage {
- constructor(page: Page) {
- super(page);
- }
-
- async isLoaded(): Promise {
- console.log(`checking if library page is loaded`);
- try {
- await this.page.waitForLoadState("domcontentloaded", { timeout: 10_000 });
-
- await this.page.waitForSelector('[data-testid="library-textbox"]', {
- state: "visible",
- timeout: 10_000,
- });
-
- console.log("Library page is loaded successfully");
- return true;
- } catch (error) {
- console.log("Library page failed to load:", error);
- return false;
- }
- }
-
- async navigateToLibrary(): Promise {
- await this.page.goto("/library");
- await this.isLoaded();
- }
-
- async getAgentCount(): Promise {
- const { getId } = getSelectors(this.page);
- const countText = await getId("agents-count").textContent();
- const match = countText?.match(/^(\d+)/);
- return match ? parseInt(match[1], 10) : 0;
- }
-
- async getAgentCountByListLength(): Promise {
- const { getId } = getSelectors(this.page);
- const agentCards = await getId("library-agent-card").all();
- return agentCards.length;
- }
-
- async searchAgents(searchTerm: string): Promise {
- console.log(`searching for agents with term: ${searchTerm}`);
- const { getRole } = getSelectors(this.page);
- const searchInput = getRole("textbox", "Search agents");
- await searchInput.fill(searchTerm);
-
- await this.page.waitForTimeout(500);
- }
-
- async clearSearch(): Promise {
- console.log(`clearing search`);
- try {
- // Look for the clear button (X icon)
- const clearButton = this.page.locator(".lucide.lucide-x");
- if (await clearButton.isVisible()) {
- await clearButton.click();
- } else {
- // If no clear button, clear the search input directly
- const searchInput = this.page.getByRole("textbox", {
- name: "Search agents",
- });
- await searchInput.fill("");
- }
-
- // Wait for results to update
- await this.page.waitForTimeout(500);
- } catch (error) {
- console.error("Error clearing search:", error);
- }
- }
-
- async selectSortOption(
- page: Page,
- sortOption: "Creation Date" | "Last Modified",
- ): Promise {
- const { getRole } = getSelectors(page);
- await getRole("combobox").click();
-
- await getRole("option", sortOption).click();
-
- await this.page.waitForTimeout(500);
- }
-
- async getCurrentSortOption(): Promise {
- console.log(`getting current sort option`);
- try {
- const sortCombobox = this.page.getByRole("combobox");
- const currentOption = await sortCombobox.textContent();
- return currentOption?.trim() || "";
- } catch (error) {
- console.error("Error getting current sort option:", error);
- return "";
- }
- }
-
- async openUploadDialog(): Promise {
- console.log(`opening upload dialog`);
- // Open the unified Import dialog first
- await this.page.getByRole("button", { name: "Import" }).click();
-
- // Wait for dialog to appear
- await this.page.getByRole("dialog", { name: "Import" }).waitFor({
- state: "visible",
- timeout: 5_000,
- });
-
- // Click the "AutoGPT agent" tab
- await this.page.getByRole("tab", { name: "AutoGPT agent" }).click();
- }
-
- async closeUploadDialog(): Promise {
- await this.page.getByRole("button", { name: "Close" }).click();
-
- await this.page.getByRole("dialog", { name: "Import" }).waitFor({
- state: "hidden",
- timeout: 5_000,
- });
- }
-
- async isUploadDialogVisible(): Promise {
- console.log(`checking if upload dialog is visible`);
- try {
- const dialog = this.page.getByRole("dialog", { name: "Import" });
- return await dialog.isVisible();
- } catch {
- return false;
- }
- }
-
- async fillUploadForm(agentName: string, description: string): Promise {
- console.log(
- `filling upload form with name: ${agentName}, description: ${description}`,
- );
-
- // Fill agent name
- await this.page
- .getByRole("textbox", { name: "Agent name" })
- .fill(agentName);
-
- // Fill description
- await this.page
- .getByRole("textbox", { name: "Agent description" })
- .fill(description);
- }
-
- async isUploadButtonEnabled(): Promise {
- console.log(`checking if upload button is enabled`);
- try {
- const uploadButton = this.page.getByRole("button", {
- name: "Upload",
- });
- return await uploadButton.isEnabled();
- } catch {
- return false;
- }
- }
-
- async getAgents(): Promise {
- const { getId } = getSelectors(this.page);
- const agents: Agent[] = [];
-
- await getId("library-agent-card")
- .first()
- .waitFor({ state: "visible", timeout: 10_000 });
- const agentCards = await getId("library-agent-card").all();
-
- for (const card of agentCards) {
- const name = await getId("library-agent-card-name", card).textContent();
- const seeRunsLink = getId("library-agent-card-see-runs-link", card);
- const openInBuilderLink = getId(
- "library-agent-card-open-in-builder-link",
- card,
- );
-
- const seeRunsUrl = await seeRunsLink.getAttribute("href");
-
- // Check if the "Open in builder" link exists before getting its href
- const openInBuilderLinkCount = await openInBuilderLink.count();
- const openInBuilderUrl =
- openInBuilderLinkCount > 0
- ? await openInBuilderLink.getAttribute("href")
- : null;
-
- if (name && seeRunsUrl) {
- const idMatch = seeRunsUrl.match(/\/library\/agents\/([^\/]+)/);
- const id = idMatch ? idMatch[1] : "";
-
- agents.push({
- id,
- name: name.trim(),
- description: "", // Description is not currently rendered in the card
- seeRunsUrl,
- openInBuilderUrl: openInBuilderUrl || "",
- });
- }
- }
-
- console.log(`found ${agents.length} agents`);
- return agents;
- }
-
- async clickAgent(agent: Agent): Promise {
- const { getId } = getSelectors(this.page);
- const nameElement = getId("library-agent-card-name").filter({
- hasText: agent.name,
- });
- await nameElement.first().click();
- }
-
- async clickSeeRuns(agent: Agent): Promise {
- console.log(`clicking see runs for agent: ${agent.name}`);
-
- const { getId } = getSelectors(this.page);
- const agentCard = getId("library-agent-card").filter({
- hasText: agent.name,
- });
- const seeRunsLink = getId("library-agent-card-see-runs-link", agentCard);
- await seeRunsLink.first().click();
- }
-
- async clickOpenInBuilder(agent: Agent): Promise {
- console.log(`clicking open in builder for agent: ${agent.name}`);
-
- const { getId } = getSelectors(this.page);
- const agentCard = getId("library-agent-card").filter({
- hasText: agent.name,
- });
- const builderLink = getId(
- "library-agent-card-open-in-builder-link",
- agentCard,
- );
- await builderLink.first().click();
- }
-
- async waitForAgentsToLoad(): Promise {
- const { getId } = getSelectors(this.page);
- await Promise.race([
- getId("library-agent-card")
- .first()
- .waitFor({ state: "visible", timeout: 10_000 }),
- getId("agents-count").waitFor({ state: "visible", timeout: 10_000 }),
- ]);
- }
-
- async getSearchValue(): Promise {
- console.log(`getting search input value`);
- try {
- const searchInput = this.page.getByRole("textbox", {
- name: "Search agents",
- });
- return await searchInput.inputValue();
- } catch {
- return "";
- }
- }
-
- async hasNoAgentsMessage(): Promise {
- const { getText } = getSelectors(this.page);
- const noAgentsText = getText("0 agents");
- return noAgentsText !== null;
- }
-
- async scrollToBottom(): Promise {
- console.log(`scrolling to bottom to trigger pagination`);
- await this.page.keyboard.press("End");
- await this.page.waitForTimeout(1000);
- }
-
- async scrollDown(): Promise {
- console.log(`scrolling down to trigger pagination`);
- await this.page.keyboard.press("PageDown");
- await this.page.waitForTimeout(1000);
- }
-
- async scrollToLoadMore(): Promise {
- console.log(`scrolling to load more agents`);
-
- const initialCount = await this.getAgentCountByListLength();
- console.log(`Initial agent count (DOM cards): ${initialCount}`);
-
- await this.scrollToBottom();
-
- await this.page
- .waitForLoadState("networkidle", { timeout: 10000 })
- .catch(() => console.log("Network idle timeout, continuing..."));
-
- await this.page
- .waitForFunction(
- (prevCount) =>
- document.querySelectorAll('[data-testid="library-agent-card"]')
- .length > prevCount,
- initialCount,
- { timeout: 5000 },
- )
- .catch(() => {});
-
- const newCount = await this.getAgentCountByListLength();
- console.log(`New agent count after scroll (DOM cards): ${newCount}`);
- }
-
- async testPagination(): Promise<{
- initialCount: number;
- finalCount: number;
- hasMore: boolean;
- }> {
- const initialCount = await this.getAgentCountByListLength();
- await this.scrollToLoadMore();
- const finalCount = await this.getAgentCountByListLength();
-
- const hasMore = finalCount > initialCount;
- return {
- initialCount,
- finalCount,
- hasMore,
- };
- }
-
- async getAgentsWithPagination(): Promise {
- console.log(`getting all agents with pagination`);
-
- let allAgents: Agent[] = [];
- let previousCount = 0;
- let currentCount = 0;
- const maxAttempts = 5; // Prevent infinite loop
- let attempts = 0;
-
- do {
- previousCount = currentCount;
-
- // Get current agents
- const currentAgents = await this.getAgents();
- allAgents = currentAgents;
- currentCount = currentAgents.length;
-
- console.log(`Attempt ${attempts + 1}: Found ${currentCount} agents`);
-
- // Try to load more by scrolling
- await this.scrollToLoadMore();
-
- attempts++;
- } while (currentCount > previousCount && attempts < maxAttempts);
-
- console.log(`Total agents found with pagination: ${allAgents.length}`);
- return allAgents;
- }
-
- async waitForPaginationLoad(): Promise {
- console.log(`waiting for pagination to load`);
-
- // Wait for any loading states to complete
- await this.page.waitForTimeout(1000);
-
- // Wait for agent count to stabilize
- let previousCount = 0;
- let currentCount = 0;
- let stableChecks = 0;
- const maxChecks = 5; // Reduced from 10 to prevent excessive waiting
-
- while (stableChecks < 2 && stableChecks < maxChecks) {
- currentCount = await this.getAgentCount();
-
- if (currentCount === previousCount) {
- stableChecks++;
- } else {
- stableChecks = 0;
- }
-
- previousCount = currentCount;
- if (stableChecks < 2) {
- // Only wait if we haven't stabilized yet
- await this.page.waitForTimeout(500);
- }
- }
-
- console.log(`Pagination load stabilized with ${currentCount} agents`);
- }
-
- async scrollAndWaitForNewAgents(): Promise {
- const initialCount = await this.getAgentCountByListLength();
-
- await this.scrollDown();
-
- await this.waitForPaginationLoad();
-
- const finalCount = await this.getAgentCountByListLength();
- const newAgentsLoaded = finalCount - initialCount;
-
- console.log(
- `Loaded ${newAgentsLoaded} new agents (${initialCount} -> ${finalCount})`,
- );
-
- return newAgentsLoaded;
- }
-
- async isPaginationWorking(): Promise {
- const newAgentsLoaded = await this.scrollAndWaitForNewAgents();
- return newAgentsLoaded > 0;
- }
-}
-
-// Locator functions
-export function getLibraryTab(page: Page): Locator {
- return page.locator('a[href="/library"]');
-}
-
-export function getAgentCards(page: Page): Locator {
- return page.getByTestId("library-agent-card");
-}
-
-export function getNewRunButton(page: Page): Locator {
- return page.getByRole("button", { name: "New run" });
-}
-
-export function getAgentTitle(page: Page): Locator {
- return page.locator("h1").first();
-}
-
-// Action functions
-export async function navigateToLibrary(page: Page): Promise {
- await getLibraryTab(page).click();
- await page.waitForURL(/.*\/library/);
-}
-
-export async function clickFirstAgent(page: Page): Promise {
- const firstAgent = getAgentCards(page).first();
- await firstAgent.click();
-}
-
-export async function navigateToAgentByName(
- page: Page,
- agentName: string,
-): Promise {
- const agentCard = getAgentCards(page).filter({ hasText: agentName }).first();
- // Wait for the agent card to be visible before clicking
- // This handles async loading of agents after page navigation
- await agentCard.waitFor({ state: "visible", timeout: 15000 });
- // Click the link inside the card to navigate reliably through
- // the motion.div + draggable wrapper layers.
- const link = agentCard.locator('a[href*="/library/agents/"]').first();
- await link.click();
-}
-
-export async function clickRunButton(page: Page): Promise {
- const { getId } = getSelectors(page);
-
- // Wait for sidebar loading to complete before detecting buttons.
- // During sidebar loading, the "New task" button appears transiently
- // even for agents with no items, then switches to "Setup your task"
- // once loading finishes. Waiting for network idle ensures the page
- // has settled into its final state.
- await page.waitForLoadState("networkidle");
-
- const setupTaskButton = page.getByRole("button", {
- name: /Setup your task/i,
- });
- const newTaskButton = page.getByRole("button", { name: /New task/i });
- const runButton = getId("agent-run-button");
- const runAgainButton = getId("run-again-button");
-
- // Wait for any of the buttons to appear
- try {
- await Promise.race([
- setupTaskButton.waitFor({ state: "visible", timeout: 15000 }),
- newTaskButton.waitFor({ state: "visible", timeout: 15000 }),
- runButton.waitFor({ state: "visible", timeout: 15000 }),
- runAgainButton.waitFor({ state: "visible", timeout: 15000 }),
- ]);
- } catch {
- throw new Error(
- "Could not find run/start task button - none of the expected buttons appeared",
- );
- }
-
- // Check which button is visible and click it
- if (await setupTaskButton.isVisible()) {
- await setupTaskButton.click();
- const startBtn = page.getByRole("button", { name: /Start Task/i }).first();
- await startBtn.waitFor({ state: "visible", timeout: 15000 });
- await startBtn.click();
- return;
- }
-
- if (await newTaskButton.isVisible()) {
- await newTaskButton.click();
- const startBtn = page.getByRole("button", { name: /Start Task/i }).first();
- await startBtn.waitFor({ state: "visible", timeout: 15000 });
- await startBtn.click();
- return;
- }
-
- if (await runButton.isVisible()) {
- await runButton.click();
- return;
- }
-
- if (await runAgainButton.isVisible()) {
- await runAgainButton.click();
- return;
- }
-
- throw new Error("Could not find run/start task button");
-}
-
-export async function clickNewRunButton(page: Page): Promise {
- await getNewRunButton(page).click();
-}
-
-export async function runAgent(page: Page): Promise {
- await clickRunButton(page);
-}
-
-export async function waitForAgentPageLoad(page: Page): Promise {
- await page.waitForURL(/.*\/library\/agents\/[^/]+/);
- // Wait for sidebar data to finish loading so the page settles
- // into its final state (empty view vs sidebar view)
- await page.waitForLoadState("networkidle");
-}
-
-export async function getAgentName(page: Page): Promise {
- return (await getAgentTitle(page).textContent()) || "";
-}
-
-export async function isLoaded(page: Page): Promise {
- return await page.locator("h1").isVisible();
-}
-
-export async function waitForRunToComplete(
- page: Page,
- timeout = 30000,
-): Promise {
- await page.waitForSelector(".bg-green-500, .bg-red-500, .bg-purple-500", {
- timeout,
- });
-}
-
-export async function getRunStatus(page: Page): Promise {
- if (await page.locator(".animate-spin").isVisible()) {
- return "running";
- } else if (await page.locator(".bg-green-500").isVisible()) {
- return "completed";
- } else if (await page.locator(".bg-red-500").isVisible()) {
- return "failed";
- }
- return "unknown";
-}
diff --git a/autogpt_platform/frontend/src/tests/pages/login.page.ts b/autogpt_platform/frontend/src/tests/pages/login.page.ts
deleted file mode 100644
index 8472de06ed..0000000000
--- a/autogpt_platform/frontend/src/tests/pages/login.page.ts
+++ /dev/null
@@ -1,102 +0,0 @@
-import { Page } from "@playwright/test";
-import { skipOnboardingIfPresent } from "../utils/onboarding";
-
-export class LoginPage {
- constructor(private page: Page) {}
-
- async goto() {
- await this.page.goto("/login");
- }
-
- async login(email: string, password: string) {
- console.log(`ℹ️ Attempting login on ${this.page.url()} with`, {
- email,
- password,
- });
-
- // Wait for the form to be ready
- await this.page.waitForSelector("form", { state: "visible" });
-
- // Fill email using input selector instead of label
- const emailInput = this.page.locator('input[type="email"]');
- await emailInput.waitFor({ state: "visible" });
- await emailInput.fill(email);
-
- // Fill password using input selector instead of label
- const passwordInput = this.page.locator('input[type="password"]');
- await passwordInput.waitFor({ state: "visible" });
- await passwordInput.fill(password);
-
- // Wait for the button to be ready
- const loginButton = this.page.getByRole("button", {
- name: "Login",
- exact: true,
- });
- await loginButton.waitFor({ state: "visible" });
-
- // Attach navigation logger for debug purposes
- this.page.on("load", (page) => console.log(`ℹ️ Now at URL: ${page.url()}`));
-
- // Start waiting for navigation before clicking
- // Wait for redirect to marketplace, onboarding, library, or copilot (new landing pages)
- const leaveLoginPage = this.page
- .waitForURL(
- (url: URL) =>
- /^\/(marketplace|onboarding(\/.*)?|library|copilot)?$/.test(
- url.pathname,
- ),
- { timeout: 10_000 },
- )
- .catch((reason) => {
- console.error(
- `🚨 Navigation away from /login timed out (current URL: ${this.page.url()}):`,
- reason,
- );
- throw reason;
- });
-
- console.log(`🖱️ Clicking login button...`);
- await loginButton.click();
-
- console.log("⏳ Waiting for navigation away from /login ...");
- await leaveLoginPage;
- console.log(`⌛ Post-login redirected to ${this.page.url()}`);
-
- await new Promise((resolve) => setTimeout(resolve, 200)); // allow time for client-side redirect
- await this.page.waitForLoadState("load", { timeout: 10_000 });
-
- // If redirected to onboarding, complete it via API so tests can proceed
- await skipOnboardingIfPresent(this.page, "/marketplace");
-
- console.log("➡️ Navigating to /marketplace ...");
- await this.page.goto("/marketplace", { timeout: 20_000 });
- console.log("✅ Login process complete");
-
- // If Wallet popover auto-opens, close it to avoid blocking account menu interactions
- try {
- const walletPanel = this.page.getByText("Your credits").first();
- // Wait briefly for wallet to appear after navigation (it may open asynchronously)
- const appeared = await walletPanel
- .waitFor({ state: "visible", timeout: 2500 })
- .then(() => true)
- .catch(() => false);
- if (appeared) {
- const closeWalletButton = this.page.getByRole("button", {
- name: /Close wallet/i,
- });
- await closeWalletButton.click({ timeout: 3000 }).catch(async () => {
- // Fallbacks: try Escape, then click outside
- await this.page.keyboard.press("Escape").catch(() => {});
- });
- await walletPanel
- .waitFor({ state: "hidden", timeout: 3000 })
- .catch(async () => {
- await this.page.mouse.click(5, 5).catch(() => {});
- });
- }
- } catch (_e) {
- // Non-fatal in tests; continue
- console.log("(info) Wallet popover not present or already closed");
- }
- }
-}
diff --git a/autogpt_platform/frontend/src/tests/pages/marketplace.page.ts b/autogpt_platform/frontend/src/tests/pages/marketplace.page.ts
deleted file mode 100644
index 51c2935abf..0000000000
--- a/autogpt_platform/frontend/src/tests/pages/marketplace.page.ts
+++ /dev/null
@@ -1,143 +0,0 @@
-import { Page } from "@playwright/test";
-import { BasePage } from "./base.page";
-import { getSelectors } from "../utils/selectors";
-
-export class MarketplacePage extends BasePage {
- constructor(page: Page) {
- super(page);
- }
-
- async goto(page: Page) {
- await page.goto("/marketplace");
- await page
- .locator(
- '[data-testid="store-card"], [data-testid="featured-store-card"]',
- )
- .first()
- .waitFor({ state: "visible", timeout: 20000 });
- }
-
- async getMarketplaceTitle(page: Page) {
- const { getText } = getSelectors(page);
- return getText("Explore AI agents", { exact: false });
- }
-
- async getCreatorsSection(page: Page) {
- const { getId, getText } = getSelectors(page);
- return getId("creators-section") || getText("Creators", { exact: false });
- }
-
- async getAgentsSection(page: Page) {
- const { getId, getText } = getSelectors(page);
- return getId("agents-section") || getText("Agents", { exact: false });
- }
-
- async getCreatorsLink(page: Page) {
- const { getLink } = getSelectors(page);
- return getLink(/creators/i);
- }
-
- async getAgentsLink(page: Page) {
- const { getLink } = getSelectors(page);
- return getLink(/agents/i);
- }
-
- async getSearchInput(page: Page) {
- const { getField, getId } = getSelectors(page);
- return getId("store-search-input") || getField(/search/i);
- }
-
- async getFilterDropdown(page: Page) {
- const { getId, getButton } = getSelectors(page);
- return getId("filter-dropdown") || getButton(/filter/i);
- }
-
- async searchFor(query: string, page: Page) {
- const searchInput = await this.getSearchInput(page);
- await searchInput.fill(query);
- await searchInput.press("Enter");
- }
-
- async clickCreators(page: Page) {
- const creatorsLink = await this.getCreatorsLink(page);
- await creatorsLink.click();
- }
-
- async clickAgents(page: Page) {
- const agentsLink = await this.getAgentsLink(page);
- await agentsLink.click();
- }
-
- async openFilter(page: Page) {
- const filterDropdown = await this.getFilterDropdown(page);
- await filterDropdown.click();
- }
-
- async getFeaturedAgentsSection(page: Page) {
- const { getText } = getSelectors(page);
- return getText("Featured agents");
- }
-
- async getTopAgentsSection(page: Page) {
- const { getText } = getSelectors(page);
- return getText("All Agents");
- }
-
- async getFeaturedCreatorsSection(page: Page) {
- const { getText } = getSelectors(page);
- return getText("Featured Creators");
- }
-
- async getFeaturedAgentCards(page: Page) {
- const { getId } = getSelectors(page);
- return getId("featured-store-card");
- }
-
- async getTopAgentCards(page: Page) {
- const { getId } = getSelectors(page);
- return getId("store-card");
- }
-
- async getCreatorProfiles(page: Page) {
- const { getId } = getSelectors(page);
- return getId("creator-card");
- }
-
- async searchAndNavigate(query: string, page: Page) {
- const searchInput = (await this.getSearchInput(page)).first();
- await searchInput.fill(query);
- await searchInput.press("Enter");
- }
-
- async waitForSearchResults() {
- await this.page.waitForURL("**/marketplace/search**");
- }
-
- async getFirstFeaturedAgent(page: Page) {
- const { getId } = getSelectors(page);
- const card = getId("featured-store-card").first();
- await card.waitFor({ state: "visible", timeout: 15000 });
- return card;
- }
-
- async getFirstTopAgent() {
- const card = this.page
- .locator('[data-testid="store-card"]:visible')
- .first();
- await card.waitFor({ state: "visible", timeout: 15000 });
- return card;
- }
-
- async getFirstCreatorProfile(page: Page) {
- const { getId } = getSelectors(page);
- const card = getId("creator-card").first();
- await card.waitFor({ state: "visible", timeout: 15000 });
- return card;
- }
-
- async getSearchResultsCount(page: Page) {
- const { getId } = getSelectors(page);
- const storeCards = getId("store-card");
- return await storeCards.count();
- }
-}
diff --git a/autogpt_platform/frontend/src/tests/profile-form.spec.ts b/autogpt_platform/frontend/src/tests/profile-form.spec.ts
deleted file mode 100644
index 3ca593809c..0000000000
--- a/autogpt_platform/frontend/src/tests/profile-form.spec.ts
+++ /dev/null
@@ -1,109 +0,0 @@
-import { test, expect } from "./coverage-fixture";
-import { getTestUserWithLibraryAgents } from "./credentials";
-import { LoginPage } from "./pages/login.page";
-import { ProfileFormPage } from "./pages/profile-form.page";
-import { hasUrl } from "./utils/assertion";
-
-test.describe("Profile Form", () => {
- let profileFormPage: ProfileFormPage;
-
- test.beforeEach(async ({ page }) => {
- profileFormPage = new ProfileFormPage(page);
-
- const loginPage = new LoginPage(page);
- await loginPage.goto();
- const richUser = getTestUserWithLibraryAgents();
- await loginPage.login(richUser.email, richUser.password);
- await hasUrl(page, "/marketplace");
- });
-
- test("redirects to login when user is not authenticated", async ({
- browser,
- }) => {
- const context = await browser.newContext();
- const page = await context.newPage();
-
- try {
- await page.goto("/profile");
- await hasUrl(page, "/login?next=%2Fprofile");
- } finally {
- await page.close();
- await context.close();
- }
- });
-
- test("can save profile changes successfully", async ({ page }) => {
- await profileFormPage.navbar.clickProfileLink();
-
- await expect(profileFormPage.isLoaded()).resolves.toBeTruthy();
- await hasUrl(page, new RegExp("/profile"));
-
- const suffix = Date.now().toString().slice(-6);
- const newDisplayName = `E2E Name ${suffix}`;
- const newHandle = `e2euser${suffix}`;
- const newBio = `E2E bio ${suffix}`;
- const newLinks = [
- `https://example.com/${suffix}/1`,
- `https://example.com/${suffix}/2`,
- `https://example.com/${suffix}/3`,
- `https://example.com/${suffix}/4`,
- `https://example.com/${suffix}/5`,
- ];
-
- await profileFormPage.setDisplayName(newDisplayName);
- await profileFormPage.setHandle(newHandle);
- await profileFormPage.setBio(newBio);
- await profileFormPage.setLinks(newLinks);
- await profileFormPage.saveChanges();
-
- expect(await profileFormPage.getDisplayName()).toBe(newDisplayName);
- expect(await profileFormPage.getHandle()).toBe(newHandle);
- expect(await profileFormPage.getBio()).toBe(newBio);
- for (let i = 1; i <= 5; i++) {
- expect(await profileFormPage.getLink(i)).toBe(newLinks[i - 1]);
- }
-
- await page.reload();
- await expect(profileFormPage.isLoaded()).resolves.toBeTruthy();
-
- expect(await profileFormPage.getDisplayName()).toBe(newDisplayName);
- expect(await profileFormPage.getHandle()).toBe(newHandle);
- expect(await profileFormPage.getBio()).toBe(newBio);
- for (let i = 1; i <= 5; i++) {
- expect(await profileFormPage.getLink(i)).toBe(newLinks[i - 1]);
- }
- });
-
- // Currently we are not using hook form inside the profile form, so cancel button is not working as expected, once that's fixed, we can unskip this test
- test.skip("can cancel profile changes", async ({ page }) => {
- await profileFormPage.navbar.clickProfileLink();
-
- await expect(profileFormPage.isLoaded()).resolves.toBeTruthy();
- await hasUrl(page, new RegExp("/profile"));
-
- const originalDisplayName = await profileFormPage.getDisplayName();
- const originalHandle = await profileFormPage.getHandle();
- const originalBio = await profileFormPage.getBio();
- const originalLinks: string[] = [];
- for (let i = 1; i <= 5; i++) {
- originalLinks.push(await profileFormPage.getLink(i));
- }
-
- const suffix = `${Date.now().toString().slice(-6)}_cancel`;
- await profileFormPage.setDisplayName(`Tmp Name ${suffix}`);
- await profileFormPage.setHandle(`tmpuser${suffix}`);
- await profileFormPage.setBio(`Tmp bio ${suffix}`);
- for (let i = 1; i <= 5; i++) {
- await profileFormPage.setLink(i, `https://tmp.example/${suffix}/${i}`);
- }
-
- await profileFormPage.clickCancel();
-
- expect(await profileFormPage.getDisplayName()).toBe(originalDisplayName);
- expect(await profileFormPage.getHandle()).toBe(originalHandle);
- expect(await profileFormPage.getBio()).toBe(originalBio);
- for (let i = 1; i <= 5; i++) {
- expect(await profileFormPage.getLink(i)).toBe(originalLinks[i - 1]);
- }
- });
-});
diff --git a/autogpt_platform/frontend/src/tests/profile.spec.ts b/autogpt_platform/frontend/src/tests/profile.spec.ts
deleted file mode 100644
index 60f28e7372..0000000000
--- a/autogpt_platform/frontend/src/tests/profile.spec.ts
+++ /dev/null
@@ -1,47 +0,0 @@
-import { LoginPage } from "./pages/login.page";
-import { ProfilePage } from "./pages/profile.page";
-import { test, expect } from "./coverage-fixture";
-import { getTestUser } from "./utils/auth";
-import { hasUrl } from "./utils/assertion";
-
-test.beforeEach(async ({ page }) => {
- const loginPage = new LoginPage(page);
- const testUser = await getTestUser();
-
- await page.goto("/login");
- await loginPage.login(testUser.email, testUser.password);
- await hasUrl(page, "/marketplace");
-});
-
-test("user can view their profile information", async ({ page }) => {
- const profilePage = new ProfilePage(page);
-
- await profilePage.navbar.clickProfileLink();
-
- // workaround for #8788
- // sleep for 10 seconds to allow page to load due to bug in our system
- await page.waitForTimeout(10000);
- await page.reload();
- await page.reload();
- await expect(profilePage.isLoaded()).resolves.toBeTruthy();
- await hasUrl(page, new RegExp("/profile"));
-
- // Verify email matches test worker's email
- const displayedHandle = await profilePage.getDisplayedName();
- expect(displayedHandle).not.toBeNull();
- expect(displayedHandle).not.toBe("");
- expect(displayedHandle).toBeDefined();
-});
-
-test("profile navigation is accessible from navbar", async ({ page }) => {
- const profilePage = new ProfilePage(page);
-
- await profilePage.navbar.clickProfileLink();
- await hasUrl(page, new RegExp("/profile"));
- await expect(profilePage.isLoaded()).resolves.toBeTruthy();
-});
-
-test("profile displays user Credential providers", async ({ page }) => {
- const profilePage = new ProfilePage(page);
- await profilePage.navbar.clickProfileLink();
-});
diff --git a/autogpt_platform/frontend/src/tests/publish-agent.spec.ts b/autogpt_platform/frontend/src/tests/publish-agent.spec.ts
deleted file mode 100644
index e2dafef873..0000000000
--- a/autogpt_platform/frontend/src/tests/publish-agent.spec.ts
+++ /dev/null
@@ -1,276 +0,0 @@
-import { test } from "./coverage-fixture";
-import { getTestUserWithLibraryAgents } from "./credentials";
-import { LoginPage } from "./pages/login.page";
-import {
- hasUrl,
- isDisabled,
- isEnabled,
- isHidden,
- isVisible,
-} from "./utils/assertion";
-import { getSelectors } from "./utils/selectors";
-
-test("user can publish an agent through the complete flow", async ({
- page,
-}) => {
- const { getId, getText, getButton } = getSelectors(page);
-
- const loginPage = new LoginPage(page);
- await page.goto("/login");
- const richUser = getTestUserWithLibraryAgents();
- await loginPage.login(richUser.email, richUser.password);
- await hasUrl(page, "/marketplace");
-
- await page.goto("/marketplace");
- await getButton("Become a creator").click();
-
- const publishAgentModal = getId("publish-agent-modal");
- await isVisible(publishAgentModal, 10000);
-
- await isVisible(
- publishAgentModal.getByText(
- "Select your project that you'd like to publish",
- ),
- );
-
- const agentToSelect = publishAgentModal.getByTestId("agent-card").first();
- await agentToSelect.click();
-
- const nextButton = publishAgentModal.getByRole("button", {
- name: "Next",
- exact: true,
- });
-
- await isEnabled(nextButton);
- await nextButton.click();
-
- // 2. Adding details of agent
- await isVisible(getText("Write a bit of details about your agent"));
-
- const agentName = "Test Agent Name";
-
- const agentTitle = publishAgentModal.getByLabel("Title");
- await agentTitle.fill(agentName);
-
- const agentSubheader = publishAgentModal.getByLabel("Subheader");
- await agentSubheader.fill("Test Agent Subheader");
-
- const agentSlug = publishAgentModal.getByLabel("Slug");
- await agentSlug.fill("test-agent-slug");
-
- const youtubeInput = publishAgentModal.getByLabel("Youtube video link");
- await youtubeInput.fill("https://www.youtube.com/watch?v=test");
-
- const categorySelect = publishAgentModal.locator(
- 'select[aria-hidden="true"]',
- );
- await categorySelect.selectOption({ value: "other" });
-
- const descriptionInput = publishAgentModal.getByLabel("Description");
- await descriptionInput.fill(
- "This is a test agent description for the automated test.",
- );
-
- await isEnabled(publishAgentModal.getByRole("button", { name: "Submit" }));
-});
-
-test("should display appropriate content in agent creation modal when user is logged out", async ({
- page,
-}) => {
- const { getText, getButton } = getSelectors(page);
-
- await page.goto("/marketplace");
- await getButton("Become a creator").click();
-
- await isVisible(
- getText(
- "Log in or create an account to publish your agents to the marketplace and join a community of creators",
- ),
- );
-});
-
-test("should validate all form fields in publish agent form", async ({
- page,
-}) => {
- const { getId, getText, getButton } = getSelectors(page);
-
- const loginPage = new LoginPage(page);
- await page.goto("/login");
- const richUser = getTestUserWithLibraryAgents();
- await loginPage.login(richUser.email, richUser.password);
- await hasUrl(page, "/marketplace");
-
- await page.goto("/marketplace");
- await getButton("Become a creator").click();
-
- const publishAgentModal = getId("publish-agent-modal");
- await isVisible(publishAgentModal, 10000);
-
- const agentToSelect = publishAgentModal.getByTestId("agent-card").first();
- await agentToSelect.click();
-
- const nextButton = publishAgentModal.getByRole("button", {
- name: "Next",
- exact: true,
- });
- await nextButton.click();
-
- await isVisible(getText("Write a bit of details about your agent"));
-
- // Get form elements
- const agentTitle = publishAgentModal.getByLabel("Title");
- const agentSubheader = publishAgentModal.getByLabel("Subheader");
- const agentSlug = publishAgentModal.getByLabel("Slug");
- const youtubeInput = publishAgentModal.getByLabel("Youtube video link");
- const categorySelect = publishAgentModal.locator(
- 'select[aria-hidden="true"]',
- );
- const descriptionInput = publishAgentModal.getByLabel("Description");
- const submitButton = publishAgentModal.getByRole("button", {
- name: "Submit",
- });
-
- async function clearForm() {
- await agentTitle.clear();
- await agentSubheader.clear();
- await agentSlug.clear();
- await youtubeInput.clear();
- await descriptionInput.clear();
- }
-
- // 1. Test required field validations
- await clearForm();
- await submitButton.click();
-
- await isVisible(publishAgentModal.getByText("Title is required"));
- await isVisible(publishAgentModal.getByText("Subheader is required"));
- await isVisible(publishAgentModal.getByText("Slug is required"));
- await isVisible(publishAgentModal.getByText("Category is required"));
- await isVisible(publishAgentModal.getByText("Description is required"));
-
- // 2. Test field length limits
- await clearForm();
-
- // Test title length limit (100 characters)
- const longTitle = "a".repeat(101);
- await agentTitle.fill(longTitle);
- await agentTitle.blur();
- await isVisible(
- publishAgentModal.getByText("Title must be less than 100 characters"),
- );
-
- // Test subheader length limit (200 characters)
- const longSubheader = "b".repeat(201);
- await agentSubheader.fill(longSubheader);
- await agentSubheader.blur();
- await isVisible(
- publishAgentModal.getByText("Subheader must be less than 200 characters"),
- );
-
- // Test slug length limit (50 characters)
- const longSlug = "c".repeat(51);
- await agentSlug.fill(longSlug);
- await agentSlug.blur();
- await isVisible(
- publishAgentModal.getByText("Slug must be less than 50 characters"),
- );
-
- // Test description length limit (1000 characters)
- const longDescription = "d".repeat(1001);
- await descriptionInput.fill(longDescription);
- await descriptionInput.blur();
- await isVisible(
- publishAgentModal.getByText(
- "Description must be less than 1000 characters",
- ),
- );
-
- // Test invalid characters in slug
- await agentSlug.fill("Invalid Slug With Spaces");
- await agentSlug.blur();
- await isVisible(
- publishAgentModal.getByText(
- "Slug can only contain lowercase letters, numbers, and hyphens",
- ),
- );
-
- await agentSlug.clear();
- await agentSlug.fill("InvalidSlugWithCapitals");
- await agentSlug.blur();
- await isVisible(
- publishAgentModal.getByText(
- "Slug can only contain lowercase letters, numbers, and hyphens",
- ),
- );
-
- await agentSlug.clear();
- await agentSlug.fill("invalid-slug-with-@#$");
- await agentSlug.blur();
- await isVisible(
- publishAgentModal.getByText(
- "Slug can only contain lowercase letters, numbers, and hyphens",
- ),
- );
-
- // Test valid slug format should not show error
- await agentSlug.clear();
- await agentSlug.fill("valid-slug-123");
- await agentSlug.blur();
- await page.waitForTimeout(500);
-
- await isHidden(
- publishAgentModal.getByText(
- "Slug can only contain lowercase letters, numbers, and hyphens",
- ),
- );
-
- // Test invalid YouTube URL
- await youtubeInput.fill("https://www.google.com/invalid-url");
- await youtubeInput.blur();
- await isVisible(
- publishAgentModal.getByText("Please enter a valid YouTube URL"),
- );
-
- await youtubeInput.clear();
- await youtubeInput.fill("not-a-url-at-all");
- await youtubeInput.blur();
- await isVisible(
- publishAgentModal.getByText("Please enter a valid YouTube URL"),
- );
-
- // Test valid YouTube URLs should not show error
- await youtubeInput.clear();
- await youtubeInput.fill("https://www.youtube.com/watch?v=test");
- await youtubeInput.blur();
- await page.waitForTimeout(500);
-
- await isHidden(
- publishAgentModal.getByText("Please enter a valid YouTube URL"),
- );
-
- await youtubeInput.clear();
- await youtubeInput.fill("https://youtu.be/test123");
- await youtubeInput.blur();
- await page.waitForTimeout(500);
-
- await isHidden(
- publishAgentModal.getByText("Please enter a valid YouTube URL"),
- );
-
- // 5. Test submit button enabled/disabled state
- await clearForm();
-
- // Submit button should be disabled when form is empty
- await page.waitForTimeout(1000);
- await isDisabled(submitButton);
-
- // Fill all required fields with valid data
- await agentTitle.fill("Valid Title");
- await agentSubheader.fill("Valid Subheader");
- await agentSlug.fill("valid-slug");
- await categorySelect.selectOption({ value: "other" });
- await descriptionInput.fill("Valid description text");
-
- // Submit button should now be enabled
- await isEnabled(submitButton);
-});
diff --git a/autogpt_platform/frontend/src/tests/settings.spec.ts b/autogpt_platform/frontend/src/tests/settings.spec.ts
deleted file mode 100644
index 25ca0c337a..0000000000
--- a/autogpt_platform/frontend/src/tests/settings.spec.ts
+++ /dev/null
@@ -1,144 +0,0 @@
-import { test, expect } from "./coverage-fixture";
-import { getTestUser } from "./utils/auth";
-import { LoginPage } from "./pages/login.page";
-import { hasAttribute, hasUrl, isHidden, isVisible } from "./utils/assertion";
-import { getSelectors } from "./utils/selectors";
-
-test.beforeEach(async ({ page }) => {
- const testUser = await getTestUser();
- const loginPage = new LoginPage(page);
-
- // Login and navigate to settings
- await page.goto("/login");
- await loginPage.login(testUser.email, testUser.password);
- await hasUrl(page, "/marketplace");
-
- // Navigate to settings page
- await page.goto("/profile/settings");
- await hasUrl(page, "/profile/settings");
-});
-
-test("should display email form elements correctly", async ({ page }) => {
- const { getField, getButton, getText, getLink } = getSelectors(page);
-
- // Check email form elements are displayed
- await isVisible(getText("Security & Access"));
- await isVisible(getField("Email"));
- await isVisible(getLink("Reset password"));
- await isVisible(getButton("Update email"));
-
- const updateEmailButton = getButton("Update email");
- const resetPasswordButton = getLink("Reset password");
-
- // Button should be disabled initially (no changes)
- await expect(updateEmailButton).toBeDisabled();
-
- // Test reset password navigation
- await hasAttribute(resetPasswordButton, "href", "/reset-password");
-});
-
-test("should show validation error for empty email", async ({ page }) => {
- const { getField, getButton } = getSelectors(page);
-
- const emailField = getField("Email");
- const updateEmailButton = getButton("Update email");
-
- await emailField.fill("");
- await updateEmailButton.click();
- await isVisible(page.getByText("Email is required"));
-});
-
-test("should show validation error for invalid email", async ({ page }) => {
- const { getField, getButton } = getSelectors(page);
-
- const emailField = getField("Email");
- const updateEmailButton = getButton("Update email");
-
- await emailField.fill("invalid email");
- await updateEmailButton.click();
- await isVisible(page.getByText("Please enter a valid email address"));
-});
-
-test("should handle valid email", async ({ page }) => {
- const { getField, getButton } = getSelectors(page);
-
- const emailField = getField("Email");
- const updateEmailButton = getButton("Update email");
-
- // Test successful email update
- const newEmail = `test+${Date.now()}@example.com`;
- await emailField.fill(newEmail);
- await expect(updateEmailButton).toBeEnabled();
- await updateEmailButton.click();
- await isHidden(page.getByText("Email is required"));
- await isHidden(page.getByText("Please enter a valid email address"));
-});
-
-test("should handle complete notification form functionality and form interactions", async ({
- page,
-}) => {
- const { getButton } = getSelectors(page);
-
- // Check notification form elements are displayed
- await isVisible(
- page.getByRole("heading", { name: "Notifications", exact: true }),
- );
-
- await isVisible(getButton("Cancel"));
- await isVisible(getButton("Save preferences"));
-
- // Check all notification switches are present - get all switches on page
- const switches = await page.getByRole("switch").all();
-
- for (const switchElement of switches) {
- await isVisible(switchElement);
- }
-
- const savePreferencesButton = getButton("Save preferences");
- const cancelButton = getButton("Cancel");
-
- // Button should be disabled initially (no changes)
- await expect(savePreferencesButton).toBeDisabled();
-
- // Test switch toggling functionality
- for (const switchElement of switches) {
- const initialState = await switchElement.isChecked();
- await switchElement.click();
- const newState = await switchElement.isChecked();
- expect(newState).toBe(!initialState);
- }
-
- // Test button enabling when changes are made
- if (switches.length > 0) {
- await expect(savePreferencesButton).toBeEnabled();
- }
-
- // Test cancel functionality
- await cancelButton.click();
- // Wait for form state to update after cancel
- await page.waitForTimeout(100);
- await expect(savePreferencesButton).toBeDisabled();
-
- // Test successful save with multiple switches
- const testSwitches = switches.slice(0, Math.min(3, switches.length));
- for (const switchElement of testSwitches) {
- await switchElement.click();
- }
- await expect(savePreferencesButton).toBeEnabled();
- await savePreferencesButton.click();
- await isVisible(getButton("Saving..."));
- await isVisible(
- page.getByText("Successfully updated notification preferences"),
- );
-
- // Test persistence after page reload
- if (testSwitches.length > 0) {
- const finalState = await testSwitches[0].isChecked();
- await page.reload();
- await hasUrl(page, "/profile/settings");
- const reloadedSwitches = await page.getByRole("switch").all();
- if (reloadedSwitches.length > 0) {
- expect(await reloadedSwitches[0].isChecked()).toBe(finalState);
- }
- }
-});
diff --git a/autogpt_platform/frontend/src/tests/signin.spec.ts b/autogpt_platform/frontend/src/tests/signin.spec.ts
deleted file mode 100644
index f7249ca059..0000000000
--- a/autogpt_platform/frontend/src/tests/signin.spec.ts
+++ /dev/null
@@ -1,199 +0,0 @@
-// auth.spec.ts
-
-import { test } from "./coverage-fixture";
-import { BuildPage } from "./pages/build.page";
-import { LoginPage } from "./pages/login.page";
-import { hasUrl, isHidden, isVisible } from "./utils/assertion";
-import { getTestUser } from "./utils/auth";
-import { getSelectors } from "./utils/selectors";
-
-test.beforeEach(async ({ page }) => {
- await page.goto("/login");
-});
-
-test("check the navigation when logged out", async ({ page }) => {
- const { getButton, getText, getLink } = getSelectors(page);
-
- // Test marketplace link
- const marketplaceLink = getLink("Marketplace");
- await isVisible(marketplaceLink);
- await marketplaceLink.click();
- await hasUrl(page, "/marketplace");
- await isVisible(getText("Explore AI agents", { exact: false }));
-
- // Test login button
- const loginBtn = getButton("Log In");
- await isVisible(loginBtn);
- await loginBtn.click();
- await hasUrl(page, "/login");
- await isHidden(loginBtn);
-});
-
-test("user can login successfully", async ({ page }) => {
- const testUser = await getTestUser();
- const loginPage = new LoginPage(page);
- const { getId, getButton, getRole } = getSelectors(page);
-
- await loginPage.login(testUser.email, testUser.password);
- await hasUrl(page, "/marketplace");
-
- const accountMenuTrigger = getId("profile-popout-menu-trigger");
-
- await isVisible(accountMenuTrigger);
-
- await accountMenuTrigger.click();
- const accountMenuPopover = getRole("dialog");
- await isVisible(accountMenuPopover);
-
- const accountMenuUserEmail = getId("account-menu-user-email");
- await isVisible(accountMenuUserEmail);
- await test
- .expect(accountMenuUserEmail)
- .toHaveText(testUser.email.split("@")[0].toLowerCase());
-
- const logoutBtn = getButton("Log out");
- await isVisible(logoutBtn);
- await logoutBtn.click();
-});
-
-test("user can logout successfully", async ({ page }) => {
- const testUser = await getTestUser();
- const loginPage = new LoginPage(page);
- const { getButton, getId } = getSelectors(page);
-
- await loginPage.login(testUser.email, testUser.password);
- await hasUrl(page, "/marketplace");
-
- // Open account menu
- await getId("profile-popout-menu-trigger").click();
-
- // Logout
- await getButton("Log out").click();
- await hasUrl(page, "/login");
-});
-
-test("login in, then out, then in again", async ({ page }) => {
- const testUser = await getTestUser();
- const loginPage = new LoginPage(page);
- const { getButton, getId } = getSelectors(page);
-
- await loginPage.login(testUser.email, testUser.password);
- await hasUrl(page, "/marketplace");
-
- // Click on the profile menu trigger to open account menu
- await getId("profile-popout-menu-trigger").click();
-
- // Click the logout button in the popout menu
- await getButton("Log out").click();
-
- await test.expect(page).toHaveURL("/login");
- await loginPage.login(testUser.email, testUser.password);
- await test.expect(page).toHaveURL("/marketplace");
- await test
- .expect(page.getByTestId("profile-popout-menu-trigger"))
- .toBeVisible();
-});
-
-test("multi-tab logout with WebSocket cleanup", async ({ context }) => {
- const testUser = await getTestUser();
-
- // Tab 1
- const page1 = await context.newPage();
- const builderPage1 = new BuildPage(page1);
-
- // Capture console errors to ensure WebSocket cleanup prevents errors
- const consoleErrors: string[] = [];
- page1.on("console", (msg) => {
- if (msg.type() === "error" && msg.text().includes("WebSocket")) {
- consoleErrors.push(`Page1: ${msg.text()}`);
- }
- });
-
- const loginPage1 = new LoginPage(page1);
- const { getButton: getButton1, getId: getId1 } = getSelectors(page1);
-
- // Login
- await page1.goto("/login");
- await loginPage1.login(testUser.email, testUser.password);
- await hasUrl(page1, "/marketplace");
-
- // Navigate to builder + wait for WebSocket connection
- await page1.goto("/build");
- await hasUrl(page1, "/build");
- await builderPage1.closeTutorial();
- await page1.waitForTimeout(1000);
- await isVisible(getId1("profile-popout-menu-trigger"));
-
- // Tab 2
- const page2 = await context.newPage();
-
- const { getId: getId2 } = getSelectors(page2);
-
- page2.on("console", (msg) => {
- if (msg.type() === "error" && msg.text().includes("WebSocket")) {
- consoleErrors.push(`Page2: ${msg.text()}`);
- }
- });
-
- // Navigate to builder + wait for WebSocket connection
- await page2.goto("/build");
- await hasUrl(page2, "/build");
- await page2.waitForTimeout(1000);
- await isVisible(getId2("profile-popout-menu-trigger"));
-
- // Tab 1: Logout
- await getId1("profile-popout-menu-trigger").click();
- await getButton1("Log out").click();
- await hasUrl(page1, "/login");
-
- // Tab 2: Wait for cross-tab logout to take effect and check if redirected to login
- await page2.waitForTimeout(2000); // Give time for cross-tab logout mechanism
-
- // Check if Tab 2 has been redirected to login or refresh the page to trigger redirect
- try {
- await page2.reload();
- await hasUrl(page2, "/login?next=%2Fbuild");
- } catch {
- // If reload fails, the page might already be redirecting
- await hasUrl(page2, "/login?next=%2Fbuild");
- }
-
- // Verify the profile menu is no longer visible (user is logged out)
- await isHidden(getId2("profile-popout-menu-trigger"));
-
- // Verify no WebSocket connection errors occurred during logout
- test.expect(consoleErrors).toHaveLength(0);
- if (consoleErrors.length > 0) {
- console.log("WebSocket errors during logout:", consoleErrors);
- }
-
- // Clean up
- await page1.close();
- await page2.close();
-});
-
-test("logged in user is redirected from /login to /copilot", async ({
- page,
-}) => {
- const testUser = await getTestUser();
- const loginPage = new LoginPage(page);
-
- await loginPage.login(testUser.email, testUser.password);
- await hasUrl(page, "/marketplace");
-
- await page.goto("/login");
- await hasUrl(page, "/copilot");
-});
-
-test("logged in user is redirected from /signup to /copilot", async ({
- page,
-}) => {
- const testUser = await getTestUser();
- const loginPage = new LoginPage(page);
-
- await loginPage.login(testUser.email, testUser.password);
- await hasUrl(page, "/marketplace");
-
- await page.goto("/signup");
- await hasUrl(page, "/copilot");
-});
diff --git a/autogpt_platform/frontend/src/tests/signup.spec.ts b/autogpt_platform/frontend/src/tests/signup.spec.ts
deleted file mode 100644
index bcf5ea3725..0000000000
--- a/autogpt_platform/frontend/src/tests/signup.spec.ts
+++ /dev/null
@@ -1,126 +0,0 @@
-import { test, expect } from "./coverage-fixture";
-import {
- generateTestEmail,
- generateTestPassword,
- signupTestUser,
- validateSignupForm,
-} from "./utils/signup";
-import { getSelectors } from "./utils/selectors";
-import { hasUrl, isVisible } from "./utils/assertion";
-
-test("user can signup successfully", async ({ page }) => {
- try {
- const testUser = await signupTestUser(page);
- const { getText, getId } = getSelectors(page);
-
- // Verify user was created
- expect(testUser.email).toBeTruthy();
- expect(testUser.password).toBeTruthy();
- expect(testUser.createdAt).toBeTruthy();
-
- const marketplaceText = getText(
- "Bringing you AI agents designed by thinkers from around the world",
- ).first();
-
- // Verify we're on marketplace and authenticated
- await hasUrl(page, "/marketplace");
- await isVisible(marketplaceText);
- await isVisible(getId("profile-popout-menu-trigger"));
- } catch (error) {
- console.error("❌ Signup test failed:", error);
- }
-});
-
-test("signup form validation works", async ({ page }) => {
- const { getField, getRole, getButton } = getSelectors(page);
- const emailInput = getField("Email");
- const passwordInput = page.locator("#password");
- const confirmPasswordInput = page.locator("#confirmPassword");
- const signupButton = getButton("Sign up");
- const termsCheckbox = getRole("checkbox");
-
- await validateSignupForm(page);
-
- // Additional validation tests
- await page.goto("/signup");
-
- // Test with mismatched passwords
- await emailInput.fill(generateTestEmail());
- await passwordInput.fill("password1");
- await confirmPasswordInput.fill("password2");
- await termsCheckbox.click();
- await signupButton.click();
-
- // Should still be on signup page
- await hasUrl(page, /\/signup/);
-});
-
-test("user can signup with custom credentials", async ({ page }) => {
- const { getId } = getSelectors(page);
-
- try {
- const customEmail = generateTestEmail();
- const customPassword = await generateTestPassword();
-
- const testUser = await signupTestUser(page, customEmail, customPassword);
-
- // Verify correct credentials were used
- expect(testUser.email).toBe(customEmail);
- expect(testUser.password).toBe(customPassword);
-
- // Verify successful signup
- await hasUrl(page, "/marketplace");
- await isVisible(getId("profile-popout-menu-trigger"));
- } catch (error) {
- console.error("❌ Custom credentials signup test failed:", error);
- }
-});
-
-test("user can signup with existing email handling", async ({
- page,
- browser,
-}) => {
- try {
- const testEmail = generateTestEmail();
- const testPassword = await generateTestPassword();
-
- // First signup
- const firstUser = await signupTestUser(page, testEmail, testPassword);
- expect(firstUser.email).toBe(testEmail);
-
- // Create new browser context for second signup (simulates new browser window)
- const newContext = await browser.newContext();
- const newPage = await newContext.newPage();
-
- try {
- const { getText, getField, getRole, getButton } = getSelectors(newPage);
-
- // Second signup attempt with same email in new browser context
- // Navigate to signup page
- await newPage.goto("http://localhost:3000/signup");
-
- // Wait for page to load
- getText("Create a new account");
-
- // Fill form
- const emailInput = getField("Email");
- await emailInput.fill(testEmail);
- const passwordInput = newPage.locator("#password");
- await passwordInput.fill(testPassword);
- const confirmPasswordInput = newPage.locator("#confirmPassword");
- await confirmPasswordInput.fill(testPassword);
-
- // Agree to terms and submit
- await getRole("checkbox").click();
- const signupButton = getButton("Sign up");
- await signupButton.click();
- await isVisible(getText("User with this email already exists"));
- } catch (_error) {
- } finally {
- // Clean up new browser context
- await newContext.close();
- }
- } catch (error) {
- console.error("❌ Duplicate email handling test failed:", error);
- }
-});
diff --git a/autogpt_platform/frontend/src/tests/title.spec.ts b/autogpt_platform/frontend/src/tests/title.spec.ts
deleted file mode 100644
index 87cac8fe53..0000000000
--- a/autogpt_platform/frontend/src/tests/title.spec.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-import { test, expect } from "./coverage-fixture";
-
-test("has title", async ({ page }) => {
- await page.goto("/");
- await expect(page).toHaveTitle(/AutoGPT Platform/);
-});
diff --git a/autogpt_platform/frontend/src/tests/util.spec.ts b/autogpt_platform/frontend/src/tests/util.spec.ts
deleted file mode 100644
index 7e766457ac..0000000000
--- a/autogpt_platform/frontend/src/tests/util.spec.ts
+++ /dev/null
@@ -1,97 +0,0 @@
-import { test, expect } from "./coverage-fixture";
-import { setNestedProperty } from "../lib/utils";
-
-const testCases = [
- {
- name: "simple property assignment",
- path: "name",
- value: "John",
- expected: { name: "John" },
- },
- {
- name: "nested property with dot notation",
- path: "user.settings.theme",
- value: "dark",
- expected: { user: { settings: { theme: "dark" } } },
- },
- {
- name: "nested property with slash notation",
- path: "user/settings/language",
- value: "en",
- expected: { user: { settings: { language: "en" } } },
- },
- {
- name: "mixed dot and slash notation",
- path: "user.settings/preferences.color",
- value: "blue",
- expected: { user: { settings: { preferences: { color: "blue" } } } },
- },
- {
- name: "overwrite primitive with object",
- path: "user.details",
- value: { age: 30 },
- expected: { user: { details: { age: 30 } } },
- },
-];
-
-for (const { name, path, value, expected } of testCases) {
- test(name, () => {
- const obj = {};
- setNestedProperty(obj, path, value);
- expect(obj).toEqual(expected);
- });
-}
-
-test("should throw error for null object", () => {
- expect(() => {
- setNestedProperty(null, "test", "value");
- }).toThrow("Target must be a non-null object");
-});
-
-test("should throw error for undefined object", () => {
- expect(() => {
- setNestedProperty(undefined, "test", "value");
- }).toThrow("Target must be a non-null object");
-});
-
-test("should throw error for non-object target", () => {
- expect(() => {
- setNestedProperty("string", "test", "value");
- }).toThrow("Target must be a non-null object");
-});
-
-test("should throw error for empty path", () => {
- expect(() => {
- setNestedProperty({}, "", "value");
- }).toThrow("Path must be a non-empty string");
-});
-
-test("should throw error for __proto__ access", () => {
- expect(() => {
- setNestedProperty({}, "__proto__.malicious", "attack");
- }).toThrow("Invalid property name: __proto__");
-});
-
-test("should throw error for constructor access", () => {
- expect(() => {
- setNestedProperty({}, "constructor.prototype.malicious", "attack");
- }).toThrow("Invalid property name: constructor");
-});
-
-test("should throw error for prototype access", () => {
- expect(() => {
- setNestedProperty({}, "obj.prototype.malicious", "attack");
- }).toThrow("Invalid property name: prototype");
-});
-
-test("secure implementation prevents prototype pollution", () => {
- const obj = {};
- expect(() => {
- setNestedProperty(obj, "__proto__.polluted", true);
- }).toThrow("Invalid property name: __proto__");
-
- // Verify no pollution occurred
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
- // @ts-ignore
- expect({}.polluted).toBeUndefined();
-});
diff --git a/autogpt_platform/frontend/src/tests/utils/auth.ts b/autogpt_platform/frontend/src/tests/utils/auth.ts
deleted file mode 100644
index 8e5c0a90f7..0000000000
--- a/autogpt_platform/frontend/src/tests/utils/auth.ts
+++ /dev/null
@@ -1,175 +0,0 @@
-import fs from "fs";
-import path from "path";
-import { signupTestUser } from "./signup";
-import { getBrowser } from "./get-browser";
-
-export interface TestUser {
- email: string;
- password: string;
- id?: string;
- createdAt?: string;
-}
-
-export interface UserPool {
- users: TestUser[];
- createdAt: string;
- version: string;
-}
-
-export async function createTestUser(
- email?: string,
- password?: string,
- ignoreOnboarding: boolean = true,
-): Promise {
- const { faker } = await import("@faker-js/faker");
- const userEmail = email || faker.internet.email();
- const userPassword = password || faker.internet.password({ length: 12 });
-
- try {
- const browser = await getBrowser();
- const context = await browser.newContext();
- const page = await context.newPage();
-
- // Auto-accept cookies in test environment to prevent banner from appearing
- await page.addInitScript(() => {
- window.localStorage.setItem(
- "autogpt_cookie_consent",
- JSON.stringify({
- hasConsented: true,
- timestamp: Date.now(),
- analytics: true,
- monitoring: true,
- }),
- );
- });
-
- try {
- const testUser = await signupTestUser(
- page,
- userEmail,
- userPassword,
- ignoreOnboarding,
- false,
- );
- return testUser;
- } finally {
- await page.close();
- await context.close();
- await browser.close();
- }
- } catch (error) {
- console.error(`❌ Error creating test user ${userEmail}:`, error);
- throw error;
- }
-}
-
-export async function createTestUsers(count: number): Promise {
- console.log(`👥 Creating ${count} test users...`);
-
- const users: TestUser[] = [];
- let consecutiveFailures = 0;
-
- for (let i = 0; i < count; i++) {
- try {
- const user = await createTestUser();
- users.push(user);
- consecutiveFailures = 0; // Reset failure counter on success
- console.log(`✅ Created user ${i + 1}/${count}: ${user.email}`);
-
- // Small delay to prevent overwhelming the system
- if (i < count - 1) {
- await new Promise((resolve) => setTimeout(resolve, 500));
- }
- } catch (error) {
- consecutiveFailures++;
- console.error(`❌ Failed to create user ${i + 1}/${count}:`, error);
-
- // If we have too many consecutive failures, stop trying
- if (consecutiveFailures >= 3) {
- console.error(
- `⚠️ Stopping after ${consecutiveFailures} consecutive failures`,
- );
- break;
- }
-
- // Add a longer delay after failure to let system recover
- await new Promise((resolve) => setTimeout(resolve, 1000));
- }
- }
-
- console.log(`🎉 Successfully created ${users.length}/${count} test users`);
- return users;
-}
-
-export async function saveUserPool(
- users: TestUser[],
- filePath?: string,
-): Promise {
- const defaultPath = path.resolve(process.cwd(), ".auth", "user-pool.json");
- const finalPath = filePath || defaultPath;
-
- // Ensure .auth directory exists
- const dirPath = path.dirname(finalPath);
- if (!fs.existsSync(dirPath)) {
- fs.mkdirSync(dirPath, { recursive: true });
- }
-
- const userPool: UserPool = {
- users,
- createdAt: new Date().toISOString(),
- version: "1.0.0",
- };
-
- try {
- fs.writeFileSync(finalPath, JSON.stringify(userPool, null, 2));
- console.log(`✅ Successfully saved user pool to: ${finalPath}`);
- } catch (error) {
- console.error(`❌ Failed to save user pool to ${finalPath}:`, error);
- throw error;
- }
-}
-
-export async function loadUserPool(
- filePath?: string,
-): Promise {
- const defaultPath = path.resolve(process.cwd(), ".auth", "user-pool.json");
- const finalPath = filePath || defaultPath;
-
- console.log(`📖 Loading user pool from: ${finalPath}`);
-
- try {
- if (!fs.existsSync(finalPath)) {
- console.log(`⚠️ User pool file not found: ${finalPath}`);
- return null;
- }
-
- const fileContent = fs.readFileSync(finalPath, "utf-8");
- const userPool: UserPool = JSON.parse(fileContent);
-
- console.log(
- `✅ Successfully loaded ${userPool.users.length} users from: ${finalPath}`,
- );
- console.log(`📅 User pool created at: ${userPool.createdAt}`);
- console.log(`🔖 User pool version: ${userPool.version}`);
-
- return userPool;
- } catch (error) {
- console.error(`❌ Failed to load user pool from ${finalPath}:`, error);
- return null;
- }
-}
-
-export async function getTestUser(): Promise {
- const userPool = await loadUserPool();
- if (!userPool) {
- throw new Error("User pool not found");
- }
-
- if (userPool.users.length === 0) {
- throw new Error("No users available in the pool");
- }
-
- // Return a random user from the pool
- const randomIndex = Math.floor(Math.random() * userPool.users.length);
- return userPool.users[randomIndex];
-}
diff --git a/autogpt_platform/frontend/src/types/auth.test.ts b/autogpt_platform/frontend/src/types/auth.test.ts
new file mode 100644
index 0000000000..ef5c0b38e1
--- /dev/null
+++ b/autogpt_platform/frontend/src/types/auth.test.ts
@@ -0,0 +1,41 @@
+import { describe, expect, test } from "vitest";
+import { signupFormSchema } from "./auth";
+
+describe("signupFormSchema", () => {
+ test("rejects invalid signup input", () => {
+ const result = signupFormSchema.safeParse({
+ email: "not-an-email",
+ password: "short",
+ confirmPassword: "different",
+ agreeToTerms: false,
+ });
+
+ expect(result.success).toBe(false);
+
+ if (result.success) {
+ return;
+ }
+
+ const { fieldErrors } = result.error.flatten();
+
+ expect(fieldErrors.email?.length).toBeGreaterThan(0);
+ expect(fieldErrors.password).toContain(
+ "Password must contain at least 12 characters",
+ );
+ expect(fieldErrors.confirmPassword).toContain("Passwords don't match");
+ expect(fieldErrors.agreeToTerms).toContain(
+ "You must agree to the Terms of Use and Privacy Policy",
+ );
+ });
+
+ test("accepts a valid signup payload", () => {
+ const result = signupFormSchema.safeParse({
+ email: "valid@example.com",
+ password: "validpassword123",
+ confirmPassword: "validpassword123",
+ agreeToTerms: true,
+ });
+
+ expect(result.success).toBe(true);
+ });
+});
diff --git a/autogpt_platform/frontend/vitest.config.mts b/autogpt_platform/frontend/vitest.config.mts
index f91fc7442e..4e8c035673 100644
--- a/autogpt_platform/frontend/vitest.config.mts
+++ b/autogpt_platform/frontend/vitest.config.mts
@@ -16,6 +16,7 @@ export default defineConfig({
exclude: [
"src/**/*.test.{ts,tsx}",
"src/**/*.stories.{ts,tsx}",
+ "src/playwright/**",
"src/tests/**",
],
},
|