From 370345da229efa35b0c190b41ba0d7b490ae3845 Mon Sep 17 00:00:00 2001 From: Vijay Date: Tue, 10 Dec 2024 19:46:28 +0530 Subject: [PATCH] fix: Not able to create consecutive ooo (#17388) * fix: able to create consecutive ooo * added e2e test * chore * update to allow overlapping ooo and test for same * refactor to reduce repeating code in tests * nit * added testcases for consecutive and reverse redirects * fix:duplicate OOO creation * prevent duplicate entries, also added test * chore * update after merges * correct datetype in prisma query * update to changes in rdp after merge --------- Co-authored-by: Prashant Varma --- apps/web/playwright/lib/testUtils.ts | 19 ++- apps/web/playwright/out-of-office.e2e.ts | 126 +++++++++++++++++- .../components/CreateEventTypeDialog.tsx | 10 +- packages/types/Calendar.d.ts | 2 +- 4 files changed, 145 insertions(+), 12 deletions(-) diff --git a/apps/web/playwright/lib/testUtils.ts b/apps/web/playwright/lib/testUtils.ts index 2b5b199e61..43005af702 100644 --- a/apps/web/playwright/lib/testUtils.ts +++ b/apps/web/playwright/lib/testUtils.ts @@ -1,4 +1,4 @@ -import type { Frame, Page, Request as PlaywrightRequest } from "@playwright/test"; +import type { Frame, Locator, Page, Request as PlaywrightRequest } from "@playwright/test"; import { expect } from "@playwright/test"; import { createHash } from "crypto"; import EventEmitter from "events"; @@ -538,3 +538,20 @@ 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, + retries = 3, + delay = 500 +) { + for (let i = 0; i < retries; i++) { + await dialogOpenButton.click(); + try { + await visibleLocatorOnDialog.waitFor({ state: "visible", timeout: delay }); + return; + } catch { + if (i === retries - 1) throw new Error("Dialog did not appear after multiple attempts."); + } + } +} diff --git a/apps/web/playwright/out-of-office.e2e.ts b/apps/web/playwright/out-of-office.e2e.ts index 0c5a9c9f37..8f93ae7577 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 } from "./lib/testUtils"; +import { submitAndWaitForResponse, localize, clickUntilDialogVisible } from "./lib/testUtils"; test.describe.configure({ mode: "parallel" }); test.afterEach(async ({ users }) => { @@ -295,6 +295,107 @@ test.describe("Out of office", () => { // send request await saveAndWaitForResponse(page, 409); }); + + test("User can create separate out of office entries for consecutive dates", async ({ page, users }) => { + const user = await users.create({ name: "userOne" }); + await user.apiLogin(); + + await page.goto("/settings/my-account/out-of-office"); + await page.waitForLoadState(); + + const addOOOButton = await page.getByTestId("add_entry_ooo"); + const dateButton = await page.locator('[data-testid="date-range"]'); + + //Creates 2 OOO entries: + //First OOO is created on Next month 1st - 3rd + await clickUntilDialogVisible(addOOOButton, dateButton); + 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); + await dateButton.click(); + await selectDateAndCreateOOO(page, "4", "6"); + await expect(page.locator(`data-testid=table-redirect-n-a`).nth(1)).toBeVisible(); + }); + + test("User can create consecutive reverse redirect OOOs", async ({ page, users }) => { + const teamMatesObj = [{ name: "member-1" }, { name: "member-2" }]; + const owner = await users.create( + { name: "owner" }, + { + hasTeam: true, + isOrg: true, + teammates: teamMatesObj, + } + ); + const member1User = users.get().find((user) => user.name === "member-1"); + + await owner.apiLogin(); + + await page.goto("/settings/my-account/out-of-office"); + await page.waitForLoadState(); + + const addOOOButton = await page.getByTestId("add_entry_ooo"); + const dateButton = await page.locator('[data-testid="date-range"]'); + + //As owner,OOO is created on Next month 1st - 3rd, forwarding to 'member-1' + await clickUntilDialogVisible(addOOOButton, dateButton); + await dateButton.click(); + await selectDateAndCreateOOO(page, "1", "3", "member-1"); + await expect( + page.locator(`data-testid=table-redirect-${member1User?.username ?? "n-a"}`).nth(0) + ).toBeVisible(); + + //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); + await dateButton.click(); + await selectDateAndCreateOOO(page, "4", "5", "owner"); + await expect(page.locator(`data-testid=table-redirect-${owner.username ?? "n-a"}`).nth(0)).toBeVisible(); + }); + + test("User cannot create infinite or overlapping reverse redirect OOOs", async ({ page, users }) => { + const t = await localize("en"); + const teamMatesObj = [{ name: "member-1" }, { name: "member-2" }]; + const owner = await users.create( + { name: "owner" }, + { + hasTeam: true, + isOrg: true, + teammates: teamMatesObj, + } + ); + const member1User = users.get().find((user) => user.name === "member-1"); + + await owner.apiLogin(); + + await page.goto("/settings/my-account/out-of-office"); + await page.waitForLoadState(); + + const addOOOButton = await page.getByTestId("add_entry_ooo"); + const dateButton = await page.locator('[data-testid="date-range"]'); + + //As owner,OOO is created on Next month 1st - 3rd, forwarding to 'member-1' + await clickUntilDialogVisible(addOOOButton, dateButton); + await dateButton.click(); + await selectDateAndCreateOOO(page, "1", "3", "member-1"); + await expect( + page.locator(`data-testid=table-redirect-${member1User?.username ?? "n-a"}`).nth(0) + ).toBeVisible(); + + //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); + await dateButton.click(); + await selectDateAndCreateOOO(page, "2", "5", "owner", 400); + await expect(page.locator(`text=${t("booking_redirect_infinite_not_allowed")}`)).toBeTruthy(); + }); }); async function saveAndWaitForResponse(page: Page, expectedStatusCode = 200) { @@ -312,3 +413,26 @@ async function selectToAndFromDates(page: Page, fromDate: string, toDate: string await page.locator(`button[name="day"]:has-text("${fromDate}")`).nth(0).click(); await page.locator(`button[name="day"]:has-text("${toDate}")`).nth(0).click(); } + +async function selectDateAndCreateOOO( + page: Page, + fromDate: string, + toDate: string, + redirectToUser?: string, + expectedStatusCode = 200 +) { + const t = await localize("en"); + await page.locator(`button[name="next-month"]`).click(); + await page.locator(`button[name="day"]:has-text("${fromDate}")`).nth(0).click(); + await page.locator(`button[name="day"]:has-text("${toDate}")`).nth(0).click(); + await page.locator(`text=${t("create_an_out_of_office")}`).click(); + await page.getByTestId("reason_select").click(); + await page.getByTestId("select-option-4").click(); + await page.getByTestId("notes_input").click(); + await page.getByTestId("notes_input").fill("Demo notes"); + if (redirectToUser) { + await page.getByTestId("profile-redirect-switch").click(); + await page.locator(`text=${redirectToUser}`).click(); + } + await saveAndWaitForResponse(page, expectedStatusCode); +} diff --git a/packages/features/eventtypes/components/CreateEventTypeDialog.tsx b/packages/features/eventtypes/components/CreateEventTypeDialog.tsx index b422089a30..42b40b4c18 100644 --- a/packages/features/eventtypes/components/CreateEventTypeDialog.tsx +++ b/packages/features/eventtypes/components/CreateEventTypeDialog.tsx @@ -114,15 +114,7 @@ export default function CreateEventTypeDialog({ return ( + clearQueryParamsOnClose={["eventPage", "type", "description", "title", "length", "slug", "locations"]}>