Skip to content

Commit

Permalink
feat(tests): add baseline utility for integration testing from fronte…
Browse files Browse the repository at this point in the history
…nd ui (#8765)
  • Loading branch information
ntindle authored Nov 27, 2024
1 parent 86fbbae commit 5dd151b
Show file tree
Hide file tree
Showing 8 changed files with 279 additions and 24 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -171,3 +171,5 @@ ig*
.github_access_token
LICENSE.rtf
autogpt_platform/backend/settings.py
/.auth
/autogpt_platform/frontend/.auth
4 changes: 3 additions & 1 deletion autogpt_platform/frontend/src/app/profile/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,9 @@ export default function PrivatePage() {
return (
<div className="mx-auto max-w-3xl md:py-8">
<div className="flex items-center justify-between">
<p>Hello {user.email}</p>
<p>
Hello <span data-testid="profile-email">{user.email}</span>
</p>
<Button onClick={() => supabase.auth.signOut()}>
<LogOutIcon className="mr-1.5 size-4" />
Log out
Expand Down
29 changes: 14 additions & 15 deletions autogpt_platform/frontend/src/tests/auth.spec.ts
Original file line number Diff line number Diff line change
@@ -1,46 +1,45 @@
import { test, expect } from "./fixtures";
// auth.spec.ts
import { test } from "./fixtures";

test.describe("Authentication", () => {
test("user can login successfully", async ({ page, loginPage, testUser }) => {
await page.goto("/login"); // Make sure we're on the login page
await page.goto("/login");
await loginPage.login(testUser.email, testUser.password);
// expect to be redirected to the home page
await expect(page).toHaveURL("/");
// expect to see the Monitor text
await expect(page.getByText("Monitor")).toBeVisible();
await test.expect(page).toHaveURL("/");
await test.expect(page.getByText("Monitor")).toBeVisible();
});

test("user can logout successfully", async ({
page,
loginPage,
testUser,
}) => {
await page.goto("/login"); // Make sure we're on the login page
await page.goto("/login");
await loginPage.login(testUser.email, testUser.password);

// Expect to be on the home page
await expect(page).toHaveURL("/");
await test.expect(page).toHaveURL("/");

// Click on the user menu
await page.getByRole("button", { name: "CN" }).click();
// Click on the logout menu item
await page.getByRole("menuitem", { name: "Log out" }).click();
// Expect to be redirected to the login page
await expect(page).toHaveURL("/login");

await test.expect(page).toHaveURL("/login");
});

test("login in, then out, then in again", async ({
page,
loginPage,
testUser,
}) => {
await page.goto("/login"); // Make sure we're on the login page
await page.goto("/login");
await loginPage.login(testUser.email, testUser.password);
await page.goto("/");
await page.getByRole("button", { name: "CN" }).click();
await page.getByRole("menuitem", { name: "Log out" }).click();
await expect(page).toHaveURL("/login");
await test.expect(page).toHaveURL("/login");
await loginPage.login(testUser.email, testUser.password);
await expect(page).toHaveURL("/");
await expect(page.getByText("Monitor")).toBeVisible();
await test.expect(page).toHaveURL("/");
await test.expect(page.getByText("Monitor")).toBeVisible();
});
});
107 changes: 99 additions & 8 deletions autogpt_platform/frontend/src/tests/fixtures/index.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,109 @@
/* eslint-disable react-hooks/rules-of-hooks */
import { test as base } from "@playwright/test";
import { createTestUserFixture } from "./test-user.fixture";
import { createLoginPageFixture } from "./login-page.fixture";
import type { TestUser } from "./test-user.fixture";
import { createClient, SupabaseClient } from "@supabase/supabase-js";
import { faker } from "@faker-js/faker";
import fs from "fs";
import path from "path";
import { TestUser } from "./test-user.fixture";
import { LoginPage } from "../pages/login.page";

type Fixtures = {
// Extend both worker state and test-specific fixtures
type WorkerFixtures = {
workerAuth: TestUser;
};

type TestFixtures = {
testUser: TestUser;
loginPage: LoginPage;
};

// Combine fixtures
export const test = base.extend<Fixtures>({
testUser: createTestUserFixture,
loginPage: createLoginPageFixture,
let supabase: SupabaseClient;

function getSupabaseAdmin() {
if (!supabase) {
supabase = createClient(
process.env.SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY!,
{
auth: {
autoRefreshToken: false,
persistSession: false,
},
},
);
}
return supabase;
}

export const test = base.extend<TestFixtures, WorkerFixtures>({
// Define the worker-level fixture that creates and manages worker-specific auth
workerAuth: [
async ({}, use, workerInfo) => {
const workerId = workerInfo.workerIndex;
const fileName = path.resolve(
process.cwd(),
`.auth/worker-${workerId}.json`,
);

// Create directory if it doesn't exist
const dirPath = path.dirname(fileName);
if (!fs.existsSync(dirPath)) {
fs.mkdirSync(dirPath, { recursive: true });
}

let auth: TestUser;
if (fs.existsSync(fileName)) {
auth = JSON.parse(fs.readFileSync(fileName, "utf-8"));
} else {
// Generate new worker-specific test user
auth = {
email: `test.worker.${workerId}.${Date.now()}@example.com`,
password: faker.internet.password({ length: 12 }),
};

const supabase = getSupabaseAdmin();
const {
data: { user },
error: signUpError,
} = await supabase.auth.signUp({
email: auth.email,
password: auth.password,
});

if (signUpError) {
throw signUpError;
}

auth.id = user?.id;
fs.writeFileSync(fileName, JSON.stringify(auth));
}

await use(auth);

// Cleanup code is commented out to preserve test users during development
/*
if (workerInfo.project.metadata.teardown) {
if (auth.id) {
await deleteTestUser(auth.id);
}
if (fs.existsSync(fileName)) {
fs.unlinkSync(fileName);
}
}
*/
},
{ scope: "worker" },
],

// Define the test-level fixture that provides access to the worker auth
testUser: async ({ workerAuth }, use) => {
await use(workerAuth);
},

// Update login page fixture to use worker auth by default
loginPage: async ({ page }, use) => {
await use(new LoginPage(page));
},
});

export { expect } from "@playwright/test";
15 changes: 15 additions & 0 deletions autogpt_platform/frontend/src/tests/pages/base.page.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Page } from "@playwright/test";
import { NavBar } from "./navbar.page";

export class BasePage {
readonly navbar: NavBar;

constructor(protected page: Page) {
this.navbar = new NavBar(page);
}

async waitForPageLoad() {
// Common page load waiting logic
await this.page.waitForLoadState("networkidle", { timeout: 10000 });
}
}
51 changes: 51 additions & 0 deletions autogpt_platform/frontend/src/tests/pages/navbar.page.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { Page } from "@playwright/test";

export class NavBar {
constructor(private page: Page) {}

async clickProfileLink() {
// await this.page.getByTestId("profile-link").click();

await this.page.getByRole("button", { name: "CN" }).click();
await this.page.getByRole("menuitem", { name: "Profile" }).click();
}

async clickMonitorLink() {
await this.page.getByTestId("monitor-link").click();
}

async clickBuildLink() {
await this.page.getByTestId("build-link").click();
}

async clickMarketplaceLink() {
await this.page.getByTestId("marketplace-link").click();
}

async getUserMenuButton() {
return this.page.getByRole("button", { name: "CN" });
}

async clickUserMenu() {
await (await this.getUserMenuButton()).click();
}

async logout() {
await this.clickUserMenu();
await this.page.getByRole("menuitem", { name: "Log out" }).click();
}

async isLoggedIn(): Promise<boolean> {
try {
await (
await this.getUserMenuButton()
).waitFor({
state: "visible",
timeout: 5000,
});
return true;
} catch {
return false;
}
}
}
38 changes: 38 additions & 0 deletions autogpt_platform/frontend/src/tests/pages/profile.page.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { Page } from "@playwright/test";
import { BasePage } from "./base.page";

export class ProfilePage extends BasePage {
constructor(page: Page) {
super(page);
}

async getDisplayedEmail(): Promise<string> {
await this.waitForPageToLoad();
const email = await this.page.getByTestId("profile-email").textContent();
if (!email) {
throw new Error("Email not found");
}
return email;
}

async isLoaded(): Promise<boolean> {
try {
await this.waitForPageToLoad();
return true;
} catch (error) {
console.error("Error loading profile page", error);
return false;
}
}

private async waitForPageToLoad(): Promise<void> {
await this.page.waitForLoadState("networkidle", { timeout: 60_000 });

await this.page.getByTestId("profile-email").waitFor({
state: "visible",
timeout: 60_000,
});

await this.page.waitForLoadState("networkidle", { timeout: 60_000 });
}
}
57 changes: 57 additions & 0 deletions autogpt_platform/frontend/src/tests/profile.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// profile.spec.ts
import { test } from "./fixtures";
import { ProfilePage } from "./pages/profile.page";

test.describe("Profile", () => {
let profilePage: ProfilePage;

test.beforeEach(async ({ page, loginPage, testUser }) => {
profilePage = new ProfilePage(page);

// Start each test with login using worker auth
await page.goto("/login");
await loginPage.login(testUser.email, testUser.password);
await test.expect(page).toHaveURL("/");
});

test("user can view their profile information", async ({
page,
testUser,
}) => {
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 test.expect(profilePage.isLoaded()).resolves.toBeTruthy();
await test.expect(page).toHaveURL(new RegExp("/profile"));

// Verify email matches test worker's email
const displayedEmail = await profilePage.getDisplayedEmail();
test.expect(displayedEmail).toBe(testUser.email);
});

test("profile navigation is accessible from navbar", async ({ page }) => {
await profilePage.navbar.clickProfileLink();
await test.expect(page).toHaveURL(new RegExp("/profile"));
// workaround for #8788
await page.reload();
await page.reload();
await test.expect(profilePage.isLoaded()).resolves.toBeTruthy();
});

test("profile displays user Credential providers", async ({ page }) => {
await profilePage.navbar.clickProfileLink();

// await test
// .expect(page.getByTestId("profile-section-personal"))
// .toBeVisible();
// await test
// .expect(page.getByTestId("profile-section-settings"))
// .toBeVisible();
// await test
// .expect(page.getByTestId("profile-section-security"))
// .toBeVisible();
});
});

0 comments on commit 5dd151b

Please sign in to comment.