diff --git a/apps/web/playwright/change-username.e2e.ts b/apps/web/playwright/change-username.e2e.ts index b2f611714f..46bf03e778 100644 --- a/apps/web/playwright/change-username.e2e.ts +++ b/apps/web/playwright/change-username.e2e.ts @@ -43,9 +43,40 @@ test.describe("Change username on settings", () => { id: user.id, }, }); + expect(newUpdatedUser.username).toBe("demousernamex"); }); + test("User can change username to include periods(or dots)", async ({ page, users, prisma }) => { + const user = await users.create(); + + await user.apiLogin(); + // Try to go homepage + await page.goto("/settings/my-account/profile"); + // Change username from normal to normal + const usernameInput = page.locator("[data-testid=username-input]"); + // User can change username to include dots(or periods) + await usernameInput.fill("demo.username"); + await page.click("[data-testid=update-username-btn]"); + await Promise.all([ + page.click("[data-testid=save-username]"), + page.getByTestId("toast-success").waitFor(), + ]); + await page.waitForLoadState("networkidle"); + + const updatedUser = await prisma.user.findUniqueOrThrow({ + where: { + id: user.id, + }, + }); + + expect(updatedUser.username).toBe("demo.username"); + + // Check if user avatar can be accessed and response headers contain 'image/' in the content type + const response = await page.goto("/demo.username/avatar.png"); + expect(response?.headers()?.["content-type"]).toContain("image/"); + }); + test("User can update to PREMIUM username", async ({ page, users }, testInfo) => { // eslint-disable-next-line playwright/no-skipped-test test.skip(!IS_STRIPE_ENABLED, "It should only run if Stripe is installed"); diff --git a/apps/web/playwright/dynamic-booking-pages.e2e.ts b/apps/web/playwright/dynamic-booking-pages.e2e.ts index eddb68be20..f41fe4c91b 100644 --- a/apps/web/playwright/dynamic-booking-pages.e2e.ts +++ b/apps/web/playwright/dynamic-booking-pages.e2e.ts @@ -13,7 +13,7 @@ test("dynamic booking", async ({ page, users }) => { const pro = await users.create(); await pro.apiLogin(); - const free = await users.create({ username: "free" }); + const free = await users.create({ username: "free.example" }); await page.goto(`/${pro.username}+${free.username}`); await test.step("book an event first day in next month", async () => { diff --git a/apps/web/test/utils/bookingScenario/bookingScenario.ts b/apps/web/test/utils/bookingScenario/bookingScenario.ts index f8ddc1c735..5f95c6afdd 100644 --- a/apps/web/test/utils/bookingScenario/bookingScenario.ts +++ b/apps/web/test/utils/bookingScenario/bookingScenario.ts @@ -635,7 +635,7 @@ export const TestData = { example: { name: "Example", email: "example@example.com", - username: "example", + username: "example.username", defaultScheduleId: 1, timeZone: Timezones["+5:30"], }, diff --git a/packages/lib/slugify.test.ts b/packages/lib/slugify.test.ts index 0cf9760303..634e147c84 100644 --- a/packages/lib/slugify.test.ts +++ b/packages/lib/slugify.test.ts @@ -30,6 +30,21 @@ describe("slugify", () => { expect(slugify("$hello-there_")).toEqual("hello-there"); }); + it("should keep periods as is except the start and end", () => { + expect(slugify("hello.there")).toEqual("hello.there"); + expect(slugify("h.e.l.l.o.t.h.e.r.e")).toEqual("h.e.l.l.o.t.h.e.r.e"); + }); + it("should remove consecutive periods", () => { + expect(slugify("hello...there")).toEqual("hello.there"); + expect(slugify("hello....there")).toEqual("hello.there"); + expect(slugify("hello..there")).toEqual("hello.there"); + }); + it("should remove periods from start and end", () => { + expect(slugify(".hello.there")).toEqual("hello.there"); + expect(slugify(".hello.there.")).toEqual("hello.there"); + expect(slugify("hellothere.")).toEqual("hellothere"); + }); + // This is failing, if we want to fix it, one approach is as used in getValidRhfFieldName it.skip("should remove unicode and emoji characters", () => { expect(slugify("Hello 📚🕯️®️ There")).toEqual("hello---------there"); diff --git a/packages/lib/slugify.ts b/packages/lib/slugify.ts index e43d8d57e3..71cd8a7ac8 100644 --- a/packages/lib/slugify.ts +++ b/packages/lib/slugify.ts @@ -7,11 +7,13 @@ export const slugify = (str: string, forDisplayingInput?: boolean) => { .trim() // Remove whitespace from both sides .normalize("NFD") // Normalize to decomposed form for handling accents .replace(/\p{Diacritic}/gu, "") // Remove any diacritics (accents) from characters - .replace(/[^\p{L}\p{N}\p{Zs}\p{Emoji}]+/gu, "-") // Replace any non-alphanumeric characters (including Unicode) with a dash + .replace(/[^.\p{L}\p{N}\p{Zs}\p{Emoji}]+/gu, "-") // Replace any non-alphanumeric characters (including Unicode and except "." period) with a dash .replace(/[\s_#]+/g, "-") // Replace whitespace, # and underscores with a single dash - .replace(/^-+/, ""); // Remove dashes from start + .replace(/^-+/, "") // Remove dashes from start + .replace(/\.{2,}/g, ".") // Replace consecutive periods with a single period + .replace(/^\.+/, ""); // Remove periods from the start - return forDisplayingInput ? s : s.replace(/-+$/, ""); // Remove dashes from end + return forDisplayingInput ? s : s.replace(/-+$/, "").replace(/\.*$/, ""); // Remove dashes and period from end }; export default slugify;