feat(tests): add baseline utility for integration testing from frontend ui (#8765)
parent
86fbbae65c
commit
5dd151b41e
|
@ -171,3 +171,5 @@ ig*
|
||||||
.github_access_token
|
.github_access_token
|
||||||
LICENSE.rtf
|
LICENSE.rtf
|
||||||
autogpt_platform/backend/settings.py
|
autogpt_platform/backend/settings.py
|
||||||
|
/.auth
|
||||||
|
/autogpt_platform/frontend/.auth
|
||||||
|
|
|
@ -143,7 +143,9 @@ export default function PrivatePage() {
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto max-w-3xl md:py-8">
|
<div className="mx-auto max-w-3xl md:py-8">
|
||||||
<div className="flex items-center justify-between">
|
<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()}>
|
<Button onClick={() => supabase.auth.signOut()}>
|
||||||
<LogOutIcon className="mr-1.5 size-4" />
|
<LogOutIcon className="mr-1.5 size-4" />
|
||||||
Log out
|
Log out
|
||||||
|
|
|
@ -1,13 +1,12 @@
|
||||||
import { test, expect } from "./fixtures";
|
// auth.spec.ts
|
||||||
|
import { test } from "./fixtures";
|
||||||
|
|
||||||
test.describe("Authentication", () => {
|
test.describe("Authentication", () => {
|
||||||
test("user can login successfully", async ({ page, loginPage, testUser }) => {
|
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);
|
await loginPage.login(testUser.email, testUser.password);
|
||||||
// expect to be redirected to the home page
|
await test.expect(page).toHaveURL("/");
|
||||||
await expect(page).toHaveURL("/");
|
await test.expect(page.getByText("Monitor")).toBeVisible();
|
||||||
// expect to see the Monitor text
|
|
||||||
await expect(page.getByText("Monitor")).toBeVisible();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("user can logout successfully", async ({
|
test("user can logout successfully", async ({
|
||||||
|
@ -15,17 +14,17 @@ test.describe("Authentication", () => {
|
||||||
loginPage,
|
loginPage,
|
||||||
testUser,
|
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 loginPage.login(testUser.email, testUser.password);
|
||||||
|
|
||||||
// Expect to be on the home page
|
await test.expect(page).toHaveURL("/");
|
||||||
await expect(page).toHaveURL("/");
|
|
||||||
// Click on the user menu
|
// Click on the user menu
|
||||||
await page.getByRole("button", { name: "CN" }).click();
|
await page.getByRole("button", { name: "CN" }).click();
|
||||||
// Click on the logout menu item
|
// Click on the logout menu item
|
||||||
await page.getByRole("menuitem", { name: "Log out" }).click();
|
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 ({
|
test("login in, then out, then in again", async ({
|
||||||
|
@ -33,14 +32,14 @@ test.describe("Authentication", () => {
|
||||||
loginPage,
|
loginPage,
|
||||||
testUser,
|
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 loginPage.login(testUser.email, testUser.password);
|
||||||
await page.goto("/");
|
await page.goto("/");
|
||||||
await page.getByRole("button", { name: "CN" }).click();
|
await page.getByRole("button", { name: "CN" }).click();
|
||||||
await page.getByRole("menuitem", { name: "Log out" }).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 loginPage.login(testUser.email, testUser.password);
|
||||||
await expect(page).toHaveURL("/");
|
await test.expect(page).toHaveURL("/");
|
||||||
await expect(page.getByText("Monitor")).toBeVisible();
|
await test.expect(page.getByText("Monitor")).toBeVisible();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,18 +1,109 @@
|
||||||
|
/* eslint-disable react-hooks/rules-of-hooks */
|
||||||
import { test as base } from "@playwright/test";
|
import { test as base } from "@playwright/test";
|
||||||
import { createTestUserFixture } from "./test-user.fixture";
|
import { createClient, SupabaseClient } from "@supabase/supabase-js";
|
||||||
import { createLoginPageFixture } from "./login-page.fixture";
|
import { faker } from "@faker-js/faker";
|
||||||
import type { TestUser } from "./test-user.fixture";
|
import fs from "fs";
|
||||||
|
import path from "path";
|
||||||
|
import { TestUser } from "./test-user.fixture";
|
||||||
import { LoginPage } from "../pages/login.page";
|
import { LoginPage } from "../pages/login.page";
|
||||||
|
|
||||||
type Fixtures = {
|
// Extend both worker state and test-specific fixtures
|
||||||
|
type WorkerFixtures = {
|
||||||
|
workerAuth: TestUser;
|
||||||
|
};
|
||||||
|
|
||||||
|
type TestFixtures = {
|
||||||
testUser: TestUser;
|
testUser: TestUser;
|
||||||
loginPage: LoginPage;
|
loginPage: LoginPage;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Combine fixtures
|
let supabase: SupabaseClient;
|
||||||
export const test = base.extend<Fixtures>({
|
|
||||||
testUser: createTestUserFixture,
|
function getSupabaseAdmin() {
|
||||||
loginPage: createLoginPageFixture,
|
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";
|
export { expect } from "@playwright/test";
|
||||||
|
|
|
@ -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 });
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 });
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
});
|
||||||
|
});
|
Loading…
Reference in New Issue