diff --git a/.github/workflows/platform-frontend-ci.yml b/.github/workflows/platform-frontend-ci.yml index ce3633013b..7c1f792917 100644 --- a/.github/workflows/platform-frontend-ci.yml +++ b/.github/workflows/platform-frontend-ci.yml @@ -69,6 +69,10 @@ jobs: run: | cp ../supabase/docker/.env.example ../.env + - name: Copy backend .env + run: | + cp ../backend/.env.example ../backend/.env + - name: Run docker compose run: | docker compose -f ../docker-compose.yml up -d diff --git a/autogpt_platform/frontend/package.json b/autogpt_platform/frontend/package.json index d2586fa4fc..f1e6ee6c48 100644 --- a/autogpt_platform/frontend/package.json +++ b/autogpt_platform/frontend/package.json @@ -23,6 +23,7 @@ "defaults" ], "dependencies": { + "@faker-js/faker": "^9.2.0", "@hookform/resolvers": "^3.9.1", "@next/third-parties": "^15.0.3", "@radix-ui/react-avatar": "^1.1.1", diff --git a/autogpt_platform/frontend/playwright.config.ts b/autogpt_platform/frontend/playwright.config.ts index 75c9f68d4c..e1bcd8d98a 100644 --- a/autogpt_platform/frontend/playwright.config.ts +++ b/autogpt_platform/frontend/playwright.config.ts @@ -4,10 +4,10 @@ import { defineConfig, devices } from "@playwright/test"; * Read environment variables from file. * https://github.com/motdotla/dotenv */ -// import dotenv from 'dotenv'; -// import path from 'path'; -// dotenv.config({ path: path.resolve(__dirname, '.env') }); - +import dotenv from "dotenv"; +import path from "path"; +dotenv.config({ path: path.resolve(__dirname, ".env") }); +dotenv.config({ path: path.resolve(__dirname, "../backend/.env") }); /** * See https://playwright.dev/docs/test-configuration. */ diff --git a/autogpt_platform/frontend/src/tests/auth.spec.ts b/autogpt_platform/frontend/src/tests/auth.spec.ts new file mode 100644 index 0000000000..dc082b2c2c --- /dev/null +++ b/autogpt_platform/frontend/src/tests/auth.spec.ts @@ -0,0 +1,46 @@ +import { test, expect } 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 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(); + }); + + test("user can logout successfully", async ({ + page, + loginPage, + testUser, + }) => { + await page.goto("/login"); // Make sure we're on the login page + await loginPage.login(testUser.email, testUser.password); + + // Expect to be on the home page + await 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"); + }); + + 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 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 loginPage.login(testUser.email, testUser.password); + await expect(page).toHaveURL("/"); + await expect(page.getByText("Monitor")).toBeVisible(); + }); +}); diff --git a/autogpt_platform/frontend/src/tests/fixtures/index.ts b/autogpt_platform/frontend/src/tests/fixtures/index.ts new file mode 100644 index 0000000000..7436ced681 --- /dev/null +++ b/autogpt_platform/frontend/src/tests/fixtures/index.ts @@ -0,0 +1,18 @@ +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 { LoginPage } from "../pages/login.page"; + +type Fixtures = { + testUser: TestUser; + loginPage: LoginPage; +}; + +// Combine fixtures +export const test = base.extend({ + testUser: createTestUserFixture, + loginPage: createLoginPageFixture, +}); + +export { expect } from "@playwright/test"; diff --git a/autogpt_platform/frontend/src/tests/fixtures/login-page.fixture.ts b/autogpt_platform/frontend/src/tests/fixtures/login-page.fixture.ts new file mode 100644 index 0000000000..fad6c1c7b0 --- /dev/null +++ b/autogpt_platform/frontend/src/tests/fixtures/login-page.fixture.ts @@ -0,0 +1,14 @@ +/* eslint-disable react-hooks/rules-of-hooks */ +import { test as base } from "@playwright/test"; +import { LoginPage } from "../pages/login.page"; + +export const loginPageFixture = base.extend<{ loginPage: LoginPage }>({ + loginPage: async ({ page }, use) => { + await use(new LoginPage(page)); + }, +}); + +// Export just the fixture function +export const createLoginPageFixture = async ({ page }, use) => { + await use(new LoginPage(page)); +}; diff --git a/autogpt_platform/frontend/src/tests/fixtures/test-user.fixture.ts b/autogpt_platform/frontend/src/tests/fixtures/test-user.fixture.ts new file mode 100644 index 0000000000..76a935aa98 --- /dev/null +++ b/autogpt_platform/frontend/src/tests/fixtures/test-user.fixture.ts @@ -0,0 +1,83 @@ +/* eslint-disable react-hooks/rules-of-hooks */ +import { createClient, SupabaseClient } from "@supabase/supabase-js"; +import { faker } from "@faker-js/faker"; + +export type TestUser = { + email: string; + password: string; + id?: string; +}; + +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; +} + +async function createTestUser(userData: TestUser): Promise { + const supabase = getSupabaseAdmin(); + + const { data: authUser, error: authError } = await supabase.auth.signUp({ + email: userData.email, + password: userData.password, + }); + + if (authError) { + throw new Error(`Failed to create test user: ${authError.message}`); + } + + return { + ...userData, + id: authUser.user?.id, + }; +} + +async function deleteTestUser(userId: string) { + const supabase = getSupabaseAdmin(); + + try { + const { error } = await supabase.auth.admin.deleteUser(userId); + + if (error) { + console.warn(`Warning: Failed to delete test user: ${error.message}`); + } + } catch (error) { + console.warn( + `Warning: Error during user cleanup: ${(error as Error).message}`, + ); + } +} + +function generateUserData(): TestUser { + return { + email: `test.${faker.string.uuid()}@example.com`, + password: faker.internet.password({ length: 12 }), + }; +} + +// Export just the fixture function +export const createTestUserFixture = async ({}, use) => { + let user: TestUser | null = null; + + try { + const userData = generateUserData(); + user = await createTestUser(userData); + await use(user); + } finally { + if (user?.id) { + await deleteTestUser(user.id); + } + } +}; diff --git a/autogpt_platform/frontend/src/tests/pages/login.page.ts b/autogpt_platform/frontend/src/tests/pages/login.page.ts new file mode 100644 index 0000000000..8abc87588d --- /dev/null +++ b/autogpt_platform/frontend/src/tests/pages/login.page.ts @@ -0,0 +1,51 @@ +import { Page } from "@playwright/test"; + +export class LoginPage { + constructor(private page: Page) {} + + async login(email: string, password: string) { + console.log("Attempting login with:", { email, password }); // Debug log + + // Fill email + const emailInput = this.page.getByPlaceholder("user@email.com"); + await emailInput.waitFor({ state: "visible" }); + await emailInput.fill(email); + + // Fill password + const passwordInput = this.page.getByPlaceholder("password"); + await passwordInput.waitFor({ state: "visible" }); + await passwordInput.fill(password); + + // Check terms + const termsCheckbox = this.page.getByLabel("I agree to the Terms of Use"); + await termsCheckbox.waitFor({ state: "visible" }); + await termsCheckbox.click(); + + // TODO: This is a workaround to wait for the page to load after filling the email and password + const emailInput2 = this.page.getByPlaceholder("user@email.com"); + await emailInput2.waitFor({ state: "visible" }); + await emailInput2.fill(email); + + // Fill password + const passwordInput2 = this.page.getByPlaceholder("password"); + await passwordInput2.waitFor({ state: "visible" }); + await passwordInput2.fill(password); + + // Wait for the button to be ready + const loginButton = this.page.getByRole("button", { name: "Log in" }); + await loginButton.waitFor({ state: "visible" }); + + // Start waiting for navigation before clicking + const navigationPromise = this.page.waitForURL("/", { timeout: 60000 }); + + console.log("About to click login button"); // Debug log + await loginButton.click(); + + console.log("Waiting for navigation"); // Debug log + await navigationPromise; + + console.log("Navigation complete, waiting for network idle"); // Debug log + await this.page.waitForLoadState("networkidle", { timeout: 60000 }); + console.log("Login process complete"); // Debug log + } +} diff --git a/autogpt_platform/frontend/src/tests/title.spec.ts b/autogpt_platform/frontend/src/tests/title.spec.ts index e7e95c949b..c80123cb7f 100644 --- a/autogpt_platform/frontend/src/tests/title.spec.ts +++ b/autogpt_platform/frontend/src/tests/title.spec.ts @@ -1,4 +1,4 @@ -import { test, expect } from "@playwright/test"; +import { test, expect } from "./fixtures"; test("has title", async ({ page }) => { await page.goto("/"); diff --git a/autogpt_platform/frontend/src/tests/util.spec.ts b/autogpt_platform/frontend/src/tests/util.spec.ts index eebc5aae02..84fb9a3de6 100644 --- a/autogpt_platform/frontend/src/tests/util.spec.ts +++ b/autogpt_platform/frontend/src/tests/util.spec.ts @@ -1,4 +1,4 @@ -import { test, expect } from "@playwright/test"; +import { test, expect } from "./fixtures"; import { setNestedProperty } from "../lib/utils"; const testCases = [ diff --git a/autogpt_platform/frontend/src/tests/utils/user-generator.ts b/autogpt_platform/frontend/src/tests/utils/user-generator.ts new file mode 100644 index 0000000000..dd8775fc07 --- /dev/null +++ b/autogpt_platform/frontend/src/tests/utils/user-generator.ts @@ -0,0 +1,9 @@ +import { faker } from "@faker-js/faker"; + +export function generateUser() { + return { + email: faker.internet.email(), + password: faker.internet.password(), + name: faker.person.fullName(), + }; +} diff --git a/autogpt_platform/frontend/yarn.lock b/autogpt_platform/frontend/yarn.lock index 53d88538f2..2445587c69 100644 --- a/autogpt_platform/frontend/yarn.lock +++ b/autogpt_platform/frontend/yarn.lock @@ -1202,6 +1202,11 @@ resolved "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz" integrity sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g== +"@faker-js/faker@^9.2.0": + version "9.2.0" + resolved "https://registry.yarnpkg.com/@faker-js/faker/-/faker-9.2.0.tgz#269ee3a5d2442e88e10d984e106028422bcb9551" + integrity sha512-ulqQu4KMr1/sTFIYvqSdegHT8NIkt66tFAkugGnHA+1WAfEn6hMzNR+svjXGFRVLnapxvej67Z/LwchFrnLBUg== + "@floating-ui/core@^1.6.0": version "1.6.7" resolved "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.7.tgz"