diff --git a/.yarn/versions/6e890e70.yml b/.yarn/versions/6e890e70.yml new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/.yarn/versions/d1e07a23.yml b/.yarn/versions/d1e07a23.yml new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/apps/web/lib/plain/plainChat.tsx b/apps/web/lib/plain/plainChat.tsx index d80699c3638f09..f3bba3fdf5eae9 100644 --- a/apps/web/lib/plain/plainChat.tsx +++ b/apps/web/lib/plain/plainChat.tsx @@ -79,11 +79,16 @@ const PlainChat = () => { const userEmail = session?.user?.email; const isAppDomain = useMemo(() => { - const restrictedPaths = process.env.NEXT_PUBLIC_PLAIN_CHAT_EXCLUDED_PATHS?.split(",") || []; + const restrictedPathsSet = new Set( + (process.env.NEXT_PUBLIC_PLAIN_CHAT_EXCLUDED_PATHS?.split(",") || []).map((path) => path.trim()) + ); + + const pathSegments = pathname?.split("/").filter(Boolean) || []; + return ( typeof window !== "undefined" && window.location.origin === process.env.NEXT_PUBLIC_WEBAPP_URL && - !restrictedPaths.some((path) => pathname?.startsWith(path.trim())) + !pathSegments.some((segment) => restrictedPathsSet.has(segment)) ); }, [pathname]); diff --git a/apps/web/package.json b/apps/web/package.json index 8e672d2b7d3780..615518fbee669f 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,6 +1,6 @@ { "name": "@calcom/web", - "version": "4.8.9", + "version": "4.8.11", "private": true, "scripts": { "analyze": "ANALYZE=true next build", diff --git a/apps/web/playwright/lib/testUtils.ts b/apps/web/playwright/lib/testUtils.ts index 7cc89f19f3172f..2b5b199e61856a 100644 --- a/apps/web/playwright/lib/testUtils.ts +++ b/apps/web/playwright/lib/testUtils.ts @@ -1,4 +1,4 @@ -import type { Frame, Locator, Page, Request as PlaywrightRequest } from "@playwright/test"; +import type { Frame, Page, Request as PlaywrightRequest } from "@playwright/test"; import { expect } from "@playwright/test"; import { createHash } from "crypto"; import EventEmitter from "events"; @@ -538,31 +538,3 @@ export async function expectPageToBeNotFound({ page, url }: { page: Page; url: s await page.goto(`${url}`); await expect(page.getByTestId(`404-page`)).toBeVisible(); } - -export async function clickUntilDialogVisible( - dialogOpenButton: Locator, - visibleLocatorOnDialog: Locator, - page: Page, - matchUrl: string, - retries = 3, - delay = 2000 -) { - for (let i = 0; i < retries; i++) { - try { - const responsePromise = page.waitForResponse( - (response) => response.url().includes(matchUrl) && response.status() === 200 - ); - await dialogOpenButton.click(); - await responsePromise; - await visibleLocatorOnDialog.waitFor({ state: "visible", timeout: delay }); - return; - } catch { - console.warn(`clickUntilDialogVisible: Attempt ${i + 1} failed to open dialog`); - if (i === retries - 1) { - console.log("clickUntilDialogVisible: Dialog did not appear after multiple attempts."); - return; - } - await new Promise((resolve) => setTimeout(resolve, delay)); - } - } -} diff --git a/apps/web/playwright/onboarding.e2e.ts b/apps/web/playwright/onboarding.e2e.ts index 94aecd6faca589..dd93e58ff57e3c 100644 --- a/apps/web/playwright/onboarding.e2e.ts +++ b/apps/web/playwright/onboarding.e2e.ts @@ -1,93 +1,86 @@ -/* eslint-disable playwright/no-skipped-test */ import { expect } from "@playwright/test"; import { IdentityProvider } from "@calcom/prisma/enums"; import { test } from "./lib/fixtures"; -test.describe.configure({ mode: "serial" }); +test.describe.configure({ mode: "parallel" }); test.afterEach(({ users }) => users.deleteAll()); test.describe("Onboarding", () => { - test.describe("Onboarding v2", () => { - const testOnboarding = (identityProvider: IdentityProvider) => { - test(`Onboarding Flow - ${identityProvider} user`, async ({ page, users }) => { - const user = await users.create({ - completedOnboarding: false, - name: null, - identityProvider, - }); - await user.apiLogin(); - await page.goto("/getting-started"); - // tests whether the user makes it to /getting-started - // after login with completedOnboarding false - await page.waitForURL("/getting-started"); - - await test.step("step 1 - User Settings", async () => { - // Check required fields - await page.locator("button[type=submit]").click(); - await expect(page.locator("data-testid=required")).toBeVisible(); - - // happy path - await page.locator("input[name=username]").fill("new user onboarding"); - await page.locator("input[name=name]").fill("new user 2"); - await page.locator("input[role=combobox]").click(); - await page - .locator("*") - .filter({ hasText: /^Europe\/London/ }) - .first() - .click(); - await page.locator("button[type=submit]").click(); - - await expect(page).toHaveURL(/.*connected-calendar/); - - const userComplete = await user.self(); - expect(userComplete.name).toBe("new user 2"); - }); - - await test.step("step 2 - Connected Calendar", async () => { - const isDisabled = await page.locator("button[data-testid=save-calendar-button]").isDisabled(); - await expect(isDisabled).toBe(true); - // tests skip button, we don't want to test entire flow. - await page.locator("button[data-testid=skip-step]").click(); - - await expect(page).toHaveURL(/.*connected-video/); - }); - - await test.step("step 3 - Connected Video", async () => { - const isDisabled = await page.locator("button[data-testid=save-video-button]").isDisabled(); - await expect(isDisabled).toBe(true); - // tests skip button, we don't want to test entire flow. - await page.locator("button[data-testid=skip-step]").click(); - - await expect(page).toHaveURL(/.*setup-availability/); - }); - - await test.step("step 4 - Setup Availability", async () => { - const isDisabled = await page.locator("button[data-testid=save-availability]").isDisabled(); - await expect(isDisabled).toBe(false); - // same here, skip this step. - await page.locator("button[data-testid=save-availability]").click(); - - await expect(page).toHaveURL(/.*user-profile/); - }); - - await test.step("step 5- User Profile", async () => { - await page.locator("button[type=submit]").click(); - - // should redirect to /event-types after onboarding - await page.waitForURL("/event-types"); - - const userComplete = await user.self(); - - expect(userComplete.bio?.replace("


", "").length).toBe(0); - }); + const testOnboarding = (identityProvider: IdentityProvider) => { + test(`Onboarding Flow - ${identityProvider} user`, async ({ page, users }) => { + const user = await users.create({ + completedOnboarding: false, + name: null, + identityProvider, }); - }; + await user.apiLogin(); + await page.goto("/getting-started"); + // tests whether the user makes it to /getting-started + // after login with completedOnboarding false + await page.waitForURL("/getting-started"); + + await test.step("step 1 - User Settings", async () => { + // Check required fields + await page.locator("button[type=submit]").click(); + await expect(page.locator("data-testid=required")).toBeVisible(); + + // happy path + await page.locator("input[name=username]").fill("new user onboarding"); + await page.locator("input[name=name]").fill("new user 2"); + await page.locator("input[role=combobox]").click(); + await page + .locator("*") + .filter({ hasText: /^Europe\/London/ }) + .first() + .click(); + await page.locator("button[type=submit]").click(); + + await expect(page).toHaveURL(/.*connected-calendar/); + + const userComplete = await user.self(); + expect(userComplete.name).toBe("new user 2"); + }); + + await test.step("step 2 - Connected Calendar", async () => { + const isDisabled = await page.locator("button[data-testid=save-calendar-button]").isDisabled(); + await expect(isDisabled).toBe(true); + // tests skip button, we don't want to test entire flow. + await page.locator("button[data-testid=skip-step]").click(); + await expect(page).toHaveURL(/.*connected-video/); + }); + + await test.step("step 3 - Connected Video", async () => { + const isDisabled = await page.locator("button[data-testid=save-video-button]").isDisabled(); + await expect(isDisabled).toBe(true); + // tests skip button, we don't want to test entire flow. + await page.locator("button[data-testid=skip-step]").click(); + await expect(page).toHaveURL(/.*setup-availability/); + }); + + await test.step("step 4 - Setup Availability", async () => { + const isDisabled = await page.locator("button[data-testid=save-availability]").isDisabled(); + await expect(isDisabled).toBe(false); + // same here, skip this step. + + await page.locator("button[data-testid=save-availability]").click(); + await expect(page).toHaveURL(/.*user-profile/); + }); + + await test.step("step 5- User Profile", async () => { + await page.locator("button[type=submit]").click(); + // should redirect to /event-types after onboarding + await page.waitForURL("/event-types"); + + const userComplete = await user.self(); + expect(userComplete.bio?.replace("


", "").length).toBe(0); + }); + }); + }; - testOnboarding(IdentityProvider.GOOGLE); - testOnboarding(IdentityProvider.CAL); - testOnboarding(IdentityProvider.SAML); - }); + testOnboarding(IdentityProvider.GOOGLE); + testOnboarding(IdentityProvider.CAL); + testOnboarding(IdentityProvider.SAML); }); diff --git a/apps/web/playwright/out-of-office.e2e.ts b/apps/web/playwright/out-of-office.e2e.ts index c251ccaef6689c..9152db47dba834 100644 --- a/apps/web/playwright/out-of-office.e2e.ts +++ b/apps/web/playwright/out-of-office.e2e.ts @@ -7,7 +7,7 @@ import { randomString } from "@calcom/lib/random"; import prisma from "@calcom/prisma"; import { test } from "./lib/fixtures"; -import { submitAndWaitForResponse, localize, clickUntilDialogVisible } from "./lib/testUtils"; +import { submitAndWaitForResponse, localize } from "./lib/testUtils"; test.describe.configure({ mode: "parallel" }); test.afterEach(async ({ users }) => { @@ -20,12 +20,18 @@ test.describe("Out of office", () => { await user.apiLogin(); + const entriesListRespPromise = page.waitForResponse( + (response) => response.url().includes("outOfOfficeEntriesList") && response.status() === 200 + ); await page.goto("/settings/my-account/out-of-office"); await page.waitForLoadState("domcontentloaded"); + await entriesListRespPromise; - const addOOOButton = page.getByTestId("add_entry_ooo"); - const dateButton = page.locator('[data-testid="date-range"]'); - await clickUntilDialogVisible(addOOOButton, dateButton, page, "outOfOfficeReasonList?batch=1"); + const reasonListRespPromise = page.waitForResponse( + (response) => response.url().includes("outOfOfficeReasonList?batch=1") && response.status() === 200 + ); + await page.getByTestId("add_entry_ooo").click(); + await reasonListRespPromise; await page.getByTestId("reason_select").click(); @@ -71,12 +77,18 @@ test.describe("Out of office", () => { await user.apiLogin(); - await page.goto(`/settings/my-account/out-of-office`); + const entriesListRespPromise = page.waitForResponse( + (response) => response.url().includes("outOfOfficeEntriesList") && response.status() === 200 + ); + await page.goto("/settings/my-account/out-of-office"); await page.waitForLoadState("domcontentloaded"); + await entriesListRespPromise; - const addOOOButton = page.getByTestId("add_entry_ooo"); - const dateButton = page.locator('[data-testid="date-range"]'); - await clickUntilDialogVisible(addOOOButton, dateButton, page, "outOfOfficeReasonList?batch=1"); + const reasonListRespPromise = page.waitForResponse( + (response) => response.url().includes("outOfOfficeReasonList?batch=1") && response.status() === 200 + ); + await page.getByTestId("add_entry_ooo").click(); + await reasonListRespPromise; await page.getByTestId("reason_select").click(); @@ -152,7 +164,12 @@ test.describe("Out of office", () => { await user.apiLogin(); - await page.goto(`/settings/my-account/out-of-office`); + const entriesListRespPromise = page.waitForResponse( + (response) => response.url().includes("outOfOfficeEntriesList") && response.status() === 200 + ); + await page.goto("/settings/my-account/out-of-office"); + await page.waitForLoadState("domcontentloaded"); + await entriesListRespPromise; // expect table-redirect-toUserId to be visible await expect(page.locator(`data-testid=table-redirect-${userTo.username}`)).toBeVisible(); @@ -211,14 +228,20 @@ test.describe("Out of office", () => { await user.apiLogin(); + const entriesListRespPromise = page.waitForResponse( + (response) => response.url().includes("outOfOfficeEntriesList") && response.status() === 200 + ); await page.goto("/settings/my-account/out-of-office"); await page.waitForLoadState("domcontentloaded"); + await entriesListRespPromise; - const addOOOButton = page.getByTestId("add_entry_ooo"); - const dateButton = page.locator('[data-testid="date-range"]'); - await clickUntilDialogVisible(addOOOButton, dateButton, page, "outOfOfficeReasonList?batch=1"); + const reasonListRespPromise = page.waitForResponse( + (response) => response.url().includes("outOfOfficeReasonList?batch=1") && response.status() === 200 + ); + await page.getByTestId("add_entry_ooo").click(); + await reasonListRespPromise; - await dateButton.click(); + await page.locator('[data-testid="date-range"]').click(); await selectToAndFromDates(page, "13", "22", true); @@ -254,14 +277,20 @@ test.describe("Out of office", () => { await user.apiLogin(); + const entriesListRespPromise = page.waitForResponse( + (response) => response.url().includes("outOfOfficeEntriesList") && response.status() === 200 + ); await page.goto("/settings/my-account/out-of-office"); await page.waitForLoadState("domcontentloaded"); + await entriesListRespPromise; - const addOOOButton = page.getByTestId("add_entry_ooo"); - const dateButton = page.locator('[data-testid="date-range"]'); - await clickUntilDialogVisible(addOOOButton, dateButton, page, "outOfOfficeReasonList?batch=1"); + const reasonListRespPromise = page.waitForResponse( + (response) => response.url().includes("outOfOfficeReasonList?batch=1") && response.status() === 200 + ); + await page.getByTestId("add_entry_ooo").click(); + await reasonListRespPromise; - await dateButton.click(); + await page.locator('[data-testid="date-range"]').click(); await selectToAndFromDates(page, "13", "22"); @@ -270,8 +299,11 @@ test.describe("Out of office", () => { await expect(page.locator(`data-testid=table-redirect-n-a`)).toBeVisible(); // add another entry - await clickUntilDialogVisible(addOOOButton, dateButton, page, "outOfOfficeReasonList?batch=1"); - await dateButton.click(); + await entriesListRespPromise; + await page.getByTestId("add_entry_ooo").click(); + await reasonListRespPromise; + + await page.locator('[data-testid="date-range"]').click(); await selectToAndFromDates(page, "11", "24"); @@ -286,14 +318,20 @@ test.describe("Out of office", () => { await user.apiLogin(); + const entriesListRespPromise = page.waitForResponse( + (response) => response.url().includes("outOfOfficeEntriesList") && response.status() === 200 + ); await page.goto("/settings/my-account/out-of-office"); await page.waitForLoadState("domcontentloaded"); + await entriesListRespPromise; - const addOOOButton = page.getByTestId("add_entry_ooo"); - const dateButton = page.locator('[data-testid="date-range"]'); - await clickUntilDialogVisible(addOOOButton, dateButton, page, "outOfOfficeReasonList?batch=1"); + const reasonListRespPromise = page.waitForResponse( + (response) => response.url().includes("outOfOfficeReasonList?batch=1") && response.status() === 200 + ); + await page.getByTestId("add_entry_ooo").click(); + await reasonListRespPromise; - await dateButton.click(); + await page.locator('[data-testid="date-range"]').click(); await selectToAndFromDates(page, "13", "22"); @@ -302,8 +340,11 @@ test.describe("Out of office", () => { await expect(page.locator(`data-testid=table-redirect-n-a`)).toBeVisible(); // add another entry - await clickUntilDialogVisible(addOOOButton, dateButton, page, "outOfOfficeReasonList?batch=1"); - await dateButton.click(); + await entriesListRespPromise; + await page.getByTestId("add_entry_ooo").click(); + await reasonListRespPromise; + + await page.locator('[data-testid="date-range"]').click(); await selectToAndFromDates(page, "13", "22"); @@ -315,21 +356,31 @@ test.describe("Out of office", () => { const user = await users.create({ name: "userOne" }); await user.apiLogin(); + const entriesListRespPromise = page.waitForResponse( + (response) => response.url().includes("outOfOfficeEntriesList") && response.status() === 200 + ); await page.goto("/settings/my-account/out-of-office"); - await page.waitForLoadState(); + await page.waitForLoadState("domcontentloaded"); + await entriesListRespPromise; - const addOOOButton = await page.getByTestId("add_entry_ooo"); - const dateButton = await page.locator('[data-testid="date-range"]'); + const addOOOButton = page.getByTestId("add_entry_ooo"); + const dateButton = page.locator('[data-testid="date-range"]'); + const reasonListRespPromise = page.waitForResponse( + (response) => response.url().includes("outOfOfficeReasonList?batch=1") && response.status() === 200 + ); + await addOOOButton.click(); + await reasonListRespPromise; //Creates 2 OOO entries: //First OOO is created on Next month 1st - 3rd - await clickUntilDialogVisible(addOOOButton, dateButton, page, "outOfOfficeReasonList?batch=1"); await dateButton.click(); await selectDateAndCreateOOO(page, "1", "3"); await expect(page.locator(`data-testid=table-redirect-n-a`).nth(0)).toBeVisible(); //Second OOO is created on Next month 4th - 6th - await clickUntilDialogVisible(addOOOButton, dateButton, page, "outOfOfficeReasonList?batch=1"); + await entriesListRespPromise; + await addOOOButton.click(); + await reasonListRespPromise; await dateButton.click(); await selectDateAndCreateOOO(page, "4", "6"); await expect(page.locator(`data-testid=table-redirect-n-a`).nth(1)).toBeVisible(); @@ -349,14 +400,22 @@ test.describe("Out of office", () => { await owner.apiLogin(); + const entriesListRespPromise = page.waitForResponse( + (response) => response.url().includes("outOfOfficeEntriesList") && response.status() === 200 + ); await page.goto("/settings/my-account/out-of-office"); - await page.waitForLoadState(); + await page.waitForLoadState("domcontentloaded"); + await entriesListRespPromise; - const addOOOButton = await page.getByTestId("add_entry_ooo"); - const dateButton = await page.locator('[data-testid="date-range"]'); + const addOOOButton = page.getByTestId("add_entry_ooo"); + const dateButton = page.locator('[data-testid="date-range"]'); + const reasonListRespPromise = page.waitForResponse( + (response) => response.url().includes("outOfOfficeReasonList?batch=1") && response.status() === 200 + ); + await addOOOButton.click(); + await reasonListRespPromise; //As owner,OOO is created on Next month 1st - 3rd, forwarding to 'member-1' - await clickUntilDialogVisible(addOOOButton, dateButton, page, "outOfOfficeReasonList?batch=1"); await dateButton.click(); await selectDateAndCreateOOO(page, "1", "3", "member-1"); await expect( @@ -366,8 +425,10 @@ test.describe("Out of office", () => { //As member1, OOO is created on Next month 4th - 5th, forwarding to 'owner' await member1User?.apiLogin(); await page.goto("/settings/my-account/out-of-office"); - await page.waitForLoadState(); - await clickUntilDialogVisible(addOOOButton, dateButton, page, "outOfOfficeReasonList?batch=1"); + await page.waitForLoadState("domcontentloaded"); + await entriesListRespPromise; + await addOOOButton.click(); + await reasonListRespPromise; await dateButton.click(); await selectDateAndCreateOOO(page, "4", "5", "owner"); await expect(page.locator(`data-testid=table-redirect-${owner.username ?? "n-a"}`).nth(0)).toBeVisible(); @@ -388,14 +449,22 @@ test.describe("Out of office", () => { await owner.apiLogin(); + const entriesListRespPromise = page.waitForResponse( + (response) => response.url().includes("outOfOfficeEntriesList") && response.status() === 200 + ); await page.goto("/settings/my-account/out-of-office"); - await page.waitForLoadState(); + await page.waitForLoadState("domcontentloaded"); + await entriesListRespPromise; - const addOOOButton = await page.getByTestId("add_entry_ooo"); - const dateButton = await page.locator('[data-testid="date-range"]'); + const addOOOButton = page.getByTestId("add_entry_ooo"); + const dateButton = page.locator('[data-testid="date-range"]'); + const reasonListRespPromise = page.waitForResponse( + (response) => response.url().includes("outOfOfficeReasonList?batch=1") && response.status() === 200 + ); + await addOOOButton.click(); + await reasonListRespPromise; //As owner,OOO is created on Next month 1st - 3rd, forwarding to 'member-1' - await clickUntilDialogVisible(addOOOButton, dateButton, page, "outOfOfficeReasonList?batch=1"); await dateButton.click(); await selectDateAndCreateOOO(page, "1", "3", "member-1"); await expect( @@ -405,8 +474,10 @@ test.describe("Out of office", () => { //As member1, expect error while OOO is created on Next month 2nd - 5th, forwarding to 'owner' await member1User?.apiLogin(); await page.goto("/settings/my-account/out-of-office"); - await page.waitForLoadState(); - await clickUntilDialogVisible(addOOOButton, dateButton, page, "outOfOfficeReasonList?batch=1"); + await page.waitForLoadState("domcontentloaded"); + await entriesListRespPromise; + await addOOOButton.click(); + await reasonListRespPromise; await dateButton.click(); await selectDateAndCreateOOO(page, "2", "5", "owner", 400); await expect(page.locator(`text=${t("booking_redirect_infinite_not_allowed")}`)).toBeTruthy(); diff --git a/packages/app-store/typeform/pages/how-to-use/[...appPages].tsx b/packages/app-store/typeform/pages/how-to-use/[...appPages].tsx index f455de2b9a10b3..c4fd176dc38dff 100644 --- a/packages/app-store/typeform/pages/how-to-use/[...appPages].tsx +++ b/packages/app-store/typeform/pages/how-to-use/[...appPages].tsx @@ -10,7 +10,7 @@ export default function HowToUse() {
- Zapier Logo + Typeform Logo
How to route a Typeform with Cal.com Routing
diff --git a/packages/app-store/typeform/static/icon.svg b/packages/app-store/typeform/static/icon.svg new file mode 100644 index 00000000000000..5ff1b4ae2060cf --- /dev/null +++ b/packages/app-store/typeform/static/icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/features/ee/payments/components/PaymentPage.tsx b/packages/features/ee/payments/components/PaymentPage.tsx index 626acdafcd3210..136d7fdf585645 100644 --- a/packages/features/ee/payments/components/PaymentPage.tsx +++ b/packages/features/ee/payments/components/PaymentPage.tsx @@ -113,7 +113,7 @@ const PaymentPage: FC = (props) => {
{eventName}
{t("when")}
- {date.format("dddd, DD MMMM YYYY")} + {date.locale(i18n.language).format("dddd, DD MMMM YYYY")}
{date.format(is24h ? "H:mm" : "h:mma")} - {props.eventType.length} mins{" "} ({timezone}) diff --git a/packages/features/shell/navigation/Navigation.tsx b/packages/features/shell/navigation/Navigation.tsx index 7d71117f19fff0..101734fbc5db0f 100644 --- a/packages/features/shell/navigation/Navigation.tsx +++ b/packages/features/shell/navigation/Navigation.tsx @@ -219,7 +219,7 @@ const MobileNavigation = ({ isPlatformNavigation = false }: { isPlatformNavigati <>