mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-04-08 03:00:28 -04:00
test(frontend): add e2e test for profile form page (#10596)
This PR has added end-to-end tests for the profile form page. These tests include: - Redirects to the login page when the user is not authenticated. - Can save profile changes successfully. - Can cancel profile changes (skipped because we need to fix the form for this test). ### Changes 🏗️ - Added test-id's inside the ProfileInfoForm. - Created a page object for the profile form page. - Added a test for this page in `profile-form.spec.ts`. ### Checklist 📋 #### For code changes: - [x] I have clearly listed my changes in the PR description - [x] I have made a test plan - [x] I have tested my changes according to the test plan: - [x] All test are working perfectly locally
This commit is contained in:
@@ -56,7 +56,10 @@ export function ProfileInfoForm({ profile }: { profile: ProfileDetails }) {
|
||||
|
||||
return (
|
||||
<div className="w-full min-w-[800px] px-4 sm:px-8">
|
||||
<h1 className="font-circular mb-6 text-[28px] font-normal text-neutral-900 dark:text-neutral-100 sm:mb-8 sm:text-[35px]">
|
||||
<h1
|
||||
data-testid="profile-info-form-title"
|
||||
className="font-circular mb-6 text-[28px] font-normal text-neutral-900 dark:text-neutral-100 sm:mb-8 sm:text-[35px]"
|
||||
>
|
||||
Profile
|
||||
</h1>
|
||||
|
||||
@@ -92,13 +95,18 @@ export function ProfileInfoForm({ profile }: { profile: ProfileDetails }) {
|
||||
|
||||
<form className="space-y-4 sm:space-y-6" onSubmit={submitForm}>
|
||||
<div className="w-full">
|
||||
<label className="font-circular mb-1.5 block text-base font-normal leading-tight text-neutral-700 dark:text-neutral-300">
|
||||
<label
|
||||
htmlFor="displayName"
|
||||
className="font-circular mb-1.5 block text-base font-normal leading-tight text-neutral-700 dark:text-neutral-300"
|
||||
>
|
||||
Display name
|
||||
</label>
|
||||
<div className="rounded-[55px] border border-slate-200 px-4 py-2.5 dark:border-slate-700 dark:bg-slate-800">
|
||||
<input
|
||||
type="text"
|
||||
id="displayName"
|
||||
name="displayName"
|
||||
data-testid="profile-info-form-display-name"
|
||||
defaultValue={profileData.name}
|
||||
placeholder="Enter your display name"
|
||||
className="font-circular w-full border-none bg-transparent text-base font-normal text-neutral-900 placeholder:text-neutral-400 focus:outline-none dark:text-white dark:placeholder:text-neutral-500"
|
||||
@@ -114,13 +122,17 @@ export function ProfileInfoForm({ profile }: { profile: ProfileDetails }) {
|
||||
</div>
|
||||
|
||||
<div className="w-full">
|
||||
<label className="font-circular mb-1.5 block text-base font-normal leading-tight text-neutral-700 dark:text-neutral-300">
|
||||
<label
|
||||
htmlFor="handle"
|
||||
className="font-circular mb-1.5 block text-base font-normal leading-tight text-neutral-700 dark:text-neutral-300"
|
||||
>
|
||||
Handle
|
||||
</label>
|
||||
<div className="rounded-[55px] border border-slate-200 px-4 py-2.5 dark:border-slate-700 dark:bg-slate-800">
|
||||
<input
|
||||
type="text"
|
||||
name="handle"
|
||||
id="handle"
|
||||
defaultValue={profileData.username}
|
||||
placeholder="@username"
|
||||
className="font-circular w-full border-none bg-transparent text-base font-normal text-neutral-900 placeholder:text-neutral-400 focus:outline-none dark:text-white dark:placeholder:text-neutral-500"
|
||||
@@ -136,12 +148,16 @@ export function ProfileInfoForm({ profile }: { profile: ProfileDetails }) {
|
||||
</div>
|
||||
|
||||
<div className="w-full">
|
||||
<label className="font-circular mb-1.5 block text-base font-normal leading-tight text-neutral-700 dark:text-neutral-300">
|
||||
<label
|
||||
htmlFor="bio"
|
||||
className="font-circular mb-1.5 block text-base font-normal leading-tight text-neutral-700 dark:text-neutral-300"
|
||||
>
|
||||
Bio
|
||||
</label>
|
||||
<div className="h-[220px] rounded-2xl border border-slate-200 py-2.5 pl-4 pr-4 dark:border-slate-700 dark:bg-slate-800">
|
||||
<textarea
|
||||
name="bio"
|
||||
id="bio"
|
||||
defaultValue={profileData.description}
|
||||
placeholder="Tell us about yourself..."
|
||||
className="font-circular h-full w-full resize-none border-none bg-transparent text-base font-normal text-neutral-900 placeholder:text-neutral-400 focus:outline-none dark:text-white dark:placeholder:text-neutral-500"
|
||||
@@ -169,13 +185,17 @@ export function ProfileInfoForm({ profile }: { profile: ProfileDetails }) {
|
||||
const link = profileData.links[linkNum - 1];
|
||||
return (
|
||||
<div key={linkNum} className="w-full">
|
||||
<label className="font-circular mb-1.5 block text-base font-normal leading-tight text-neutral-700 dark:text-neutral-300">
|
||||
<label
|
||||
htmlFor={`link${linkNum}`}
|
||||
className="font-circular mb-1.5 block text-base font-normal leading-tight text-neutral-700 dark:text-neutral-300"
|
||||
>
|
||||
Link {linkNum}
|
||||
</label>
|
||||
<div className="rounded-[55px] border border-slate-200 px-4 py-2.5 dark:border-slate-700 dark:bg-slate-800">
|
||||
<input
|
||||
type="text"
|
||||
name={`link${linkNum}`}
|
||||
id={`link${linkNum}`}
|
||||
placeholder="https://"
|
||||
defaultValue={link || ""}
|
||||
className="font-circular w-full border-none bg-transparent text-base font-normal text-neutral-900 placeholder:text-neutral-400 focus:outline-none dark:text-white dark:placeholder:text-neutral-500"
|
||||
@@ -199,7 +219,8 @@ export function ProfileInfoForm({ profile }: { profile: ProfileDetails }) {
|
||||
<Separator />
|
||||
|
||||
<div className="flex h-[50px] items-center justify-end gap-3 py-8">
|
||||
<Button
|
||||
{/* FRONTEND-TODO: Need to fix it */}
|
||||
{/* <Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
className="font-circular h-[50px] rounded-[35px] bg-neutral-200 px-6 py-3 text-base font-medium text-neutral-800 transition-colors hover:bg-neutral-300 dark:border-neutral-700 dark:bg-neutral-700 dark:text-neutral-200 dark:hover:border-neutral-600 dark:hover:bg-neutral-600"
|
||||
@@ -208,7 +229,7 @@ export function ProfileInfoForm({ profile }: { profile: ProfileDetails }) {
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</Button> */}
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={isSubmitting}
|
||||
|
||||
145
autogpt_platform/frontend/src/tests/pages/profile-form.page.ts
Normal file
145
autogpt_platform/frontend/src/tests/pages/profile-form.page.ts
Normal file
@@ -0,0 +1,145 @@
|
||||
import { Locator, Page } from "@playwright/test";
|
||||
import { BasePage } from "./base.page";
|
||||
import { getSelectors } from "../utils/selectors";
|
||||
|
||||
export class ProfileFormPage extends BasePage {
|
||||
constructor(page: Page) {
|
||||
super(page);
|
||||
}
|
||||
|
||||
private getId(id: string | RegExp): Locator {
|
||||
const { getId } = getSelectors(this.page);
|
||||
return getId(id);
|
||||
}
|
||||
|
||||
private async hideFloatingWidgets(): Promise<void> {
|
||||
await this.page.addStyleTag({
|
||||
content: `
|
||||
[data-tally-open] { display: none !important; }
|
||||
`,
|
||||
});
|
||||
}
|
||||
|
||||
// Locators
|
||||
title(): Locator {
|
||||
return this.getId("profile-info-form-title");
|
||||
}
|
||||
|
||||
displayNameField(): Locator {
|
||||
const { getField } = getSelectors(this.page);
|
||||
return getField("Display name");
|
||||
}
|
||||
|
||||
handleField(): Locator {
|
||||
const { getField } = getSelectors(this.page);
|
||||
return getField("Handle");
|
||||
}
|
||||
|
||||
bioField(): Locator {
|
||||
const { getField } = getSelectors(this.page);
|
||||
return getField("Bio");
|
||||
}
|
||||
|
||||
linkField(index: number): Locator {
|
||||
this.assertValidLinkIndex(index);
|
||||
const { getField } = getSelectors(this.page);
|
||||
return getField(`Link ${index}`);
|
||||
}
|
||||
|
||||
cancelButton(): Locator {
|
||||
const { getButton } = getSelectors(this.page);
|
||||
return getButton("Cancel");
|
||||
}
|
||||
|
||||
saveButton(): Locator {
|
||||
const { getButton } = getSelectors(this.page);
|
||||
return getButton("Save changes");
|
||||
}
|
||||
|
||||
// State
|
||||
async isLoaded(): Promise<boolean> {
|
||||
try {
|
||||
await this.title().waitFor({ state: "visible", timeout: 10_000 });
|
||||
await this.displayNameField().waitFor({
|
||||
state: "visible",
|
||||
timeout: 10_000,
|
||||
});
|
||||
await this.handleField().waitFor({ state: "visible", timeout: 10_000 });
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Actions
|
||||
async setDisplayName(name: string): Promise<void> {
|
||||
await this.displayNameField().fill(name);
|
||||
}
|
||||
|
||||
async getDisplayName(): Promise<string> {
|
||||
return this.displayNameField().inputValue();
|
||||
}
|
||||
|
||||
async setHandle(handle: string): Promise<void> {
|
||||
await this.handleField().fill(handle);
|
||||
}
|
||||
|
||||
async getHandle(): Promise<string> {
|
||||
return this.handleField().inputValue();
|
||||
}
|
||||
|
||||
async setBio(bio: string): Promise<void> {
|
||||
await this.bioField().fill(bio);
|
||||
}
|
||||
|
||||
async getBio(): Promise<string> {
|
||||
return this.bioField().inputValue();
|
||||
}
|
||||
|
||||
async setLink(index: number, url: string): Promise<void> {
|
||||
await this.linkField(index).fill(url);
|
||||
}
|
||||
|
||||
async getLink(index: number): Promise<string> {
|
||||
return this.linkField(index).inputValue();
|
||||
}
|
||||
|
||||
async setLinks(links: Array<string | undefined>): Promise<void> {
|
||||
for (let i = 1; i <= 5; i++) {
|
||||
const val = links[i - 1] ?? "";
|
||||
await this.setLink(i, val);
|
||||
}
|
||||
}
|
||||
|
||||
async clickCancel(): Promise<void> {
|
||||
await this.cancelButton().click();
|
||||
}
|
||||
|
||||
async clickSave(): Promise<void> {
|
||||
await this.saveButton().click();
|
||||
}
|
||||
|
||||
async saveChanges(): Promise<void> {
|
||||
await this.hideFloatingWidgets();
|
||||
await this.clickSave();
|
||||
await this.waitForSaveComplete();
|
||||
}
|
||||
|
||||
async waitForSaveComplete(timeoutMs: number = 15_000): Promise<void> {
|
||||
const { getButton } = getSelectors(this.page);
|
||||
await getButton("Save changes").waitFor({
|
||||
state: "attached",
|
||||
timeout: timeoutMs,
|
||||
});
|
||||
await getButton("Save changes").waitFor({
|
||||
state: "visible",
|
||||
timeout: timeoutMs,
|
||||
});
|
||||
}
|
||||
|
||||
private assertValidLinkIndex(index: number) {
|
||||
if (index < 1 || index > 5) {
|
||||
throw new Error(`Link index must be between 1 and 5. Received: ${index}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
108
autogpt_platform/frontend/src/tests/profile-form.spec.ts
Normal file
108
autogpt_platform/frontend/src/tests/profile-form.spec.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
import test, { expect } from "@playwright/test";
|
||||
import { ProfileFormPage } from "./pages/profile-form.page";
|
||||
import { LoginPage } from "./pages/login.page";
|
||||
import { hasUrl } from "./utils/assertion";
|
||||
import { TEST_CREDENTIALS } from "./credentials";
|
||||
|
||||
test.describe("Profile Form", () => {
|
||||
let profileFormPage: ProfileFormPage;
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
profileFormPage = new ProfileFormPage(page);
|
||||
|
||||
const loginPage = new LoginPage(page);
|
||||
await loginPage.goto();
|
||||
await loginPage.login(TEST_CREDENTIALS.email, TEST_CREDENTIALS.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");
|
||||
} 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]);
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user