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:
Abhimanyu Yadav
2025-08-11 18:08:00 +05:30
committed by GitHub
parent f4a732373b
commit e13e0d4376
3 changed files with 281 additions and 7 deletions

View File

@@ -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}

View 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}`);
}
}
}

View 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]);
}
});
});