refactor: allow managed event tests parallel runs (#16113)

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
Co-authored-by: Replexica <support@replexica.com>
Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
Co-authored-by: Amit Sharma <74371312+Amit91848@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: Udit Takkar <udit222001@gmail.com>
Co-authored-by: Peer Richelsen <peer@cal.com>
Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>
Co-authored-by: unknown <adhabal2002@gmail.com>
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
Co-authored-by: Syed Ali Shahbaz <52925846+alishaz-polymath@users.noreply.github.com>
Co-authored-by: zomars <zomars@me.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
This commit is contained in:
Omar López
2024-09-06 05:32:16 +00:00
committed by GitHub
co-authored by CarinaWolli Joe Au-Yeung Replexica Morgan Amit Sharma Peer Richelsen Udit Takkar Udit Takkar Peer Richelsen Carina Wollendorfer unknown Anik Dhabal Babu Syed Ali Shahbaz Keith Williams sean-brydon
parent 34ca3bd22d
commit 9fc9876b3f
3 changed files with 278 additions and 235 deletions
+45 -1
View File
@@ -7,6 +7,7 @@ import { hashSync as hash } from "bcryptjs";
import { uuid } from "short-uuid";
import { v4 } from "uuid";
import updateChildrenEventTypes from "@calcom/features/ee/managed-event-types/lib/handleChildrenEventTypes";
import stripe from "@calcom/features/ee/payments/server/stripe";
import { DEFAULT_SCHEDULE, getAvailabilityFromSchedule } from "@calcom/lib/availability";
import { WEBAPP_URL } from "@calcom/lib/constants";
@@ -92,6 +93,7 @@ const createTeamEventType = async (
teamEventSlug?: string;
teamEventLength?: number;
seatsPerTimeSlot?: number;
managedEventUnlockedFields?: Record<string, boolean>;
}
) => {
return await prisma.eventType.create({
@@ -122,6 +124,20 @@ const createTeamEventType = async (
slug: scenario?.teamEventSlug ?? `${teamEventSlug}-team-id-${team.id}`,
length: scenario?.teamEventLength ?? 30,
seatsPerTimeSlot: scenario?.seatsPerTimeSlot,
locations: [{ type: "integrations:daily" }],
metadata:
scenario?.schedulingType === SchedulingType.MANAGED
? {
managedEventConfig: {
unlockedFields: {
locations: true,
scheduleId: true,
destinationCalendar: true,
...scenario?.managedEventUnlockedFields,
},
},
}
: undefined,
},
});
};
@@ -259,6 +275,8 @@ export const createUsersFixture = (
hasSubteam?: true;
isUnpublished?: true;
seatsPerTimeSlot?: number;
addManagedEventToTeamMates?: boolean;
managedEventUnlockedFields?: Record<string, boolean>;
orgRequestedSlug?: string;
} = {}
) => {
@@ -521,6 +539,31 @@ export const createUsersFixture = (
teamMates.push(teamUser);
store.users.push(teammateFixture);
}
// If the teamEvent is a managed one, we add the team mates to it.
if (scenario.schedulingType === SchedulingType.MANAGED && scenario.addManagedEventToTeamMates) {
await updateChildrenEventTypes({
eventTypeId: teamEvent.id,
currentUserId: user.id,
hashedLink: "",
connectedLink: null,
oldEventType: {
team: null,
},
updatedEventType: teamEvent,
children: teamMates.map((tm) => ({
hidden: false,
owner: {
id: tm.id,
name: tm.name || tm.username || "Nameless",
email: tm.email,
eventTypeSlugs: [],
},
})),
profileId: null,
prisma,
updatedValues: {},
});
}
// Add Teammates to OrgUsers
if (scenario.isOrg) {
const orgProfilesCreate = teamMates
@@ -730,10 +773,11 @@ const createUserFixture = (user: UserWithIncludes, page: Page) => {
userId: user.id,
},
}),
getFirstTeamEvent: async (teamId: number) => {
getFirstTeamEvent: async (teamId: number, schedulingType?: SchedulingType) => {
return prisma.eventType.findFirstOrThrow({
where: {
teamId,
schedulingType,
},
});
},
+233 -191
View File
@@ -1,212 +1,247 @@
import type { Page } from "@playwright/test";
import type { Locator, Page } from "@playwright/test";
import { expect } from "@playwright/test";
import { apiLogin } from "playwright/fixtures/users";
import { IS_TEAM_BILLING_ENABLED } from "@calcom/lib/constants";
import { SchedulingType } from "@calcom/prisma/enums";
import { test } from "./lib/fixtures";
import {
bookTimeSlot,
fillStripeTestCheckout,
localize,
selectFirstAvailableTimeSlotNextMonth,
} from "./lib/testUtils";
import { test, type Fixtures } from "./lib/fixtures";
import { bookTimeSlot, localize, selectFirstAvailableTimeSlotNextMonth } from "./lib/testUtils";
test.afterEach(({ users }) => users.deleteAll());
test.afterEach(async ({ users }) => {
await users.deleteAll();
});
/** So we can test different areas in parallel and avoiding the creation flow each time */
async function setupManagedEvent({
users,
unlockedFields,
}: {
users: Fixtures["users"];
unlockedFields?: Record<string, boolean>;
}) {
const teamMateName = "teammate-1";
const teamEventTitle = "Managed";
const adminUser = await users.create(null, {
hasTeam: true,
teammates: [{ name: teamMateName }],
teamEventTitle,
teamEventSlug: "managed",
schedulingType: "MANAGED",
addManagedEventToTeamMates: true,
managedEventUnlockedFields: unlockedFields,
});
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const memberUser = users.get().find((u) => u.name === teamMateName)!;
const { team } = await adminUser.getFirstTeamMembership();
const managedEvent = await adminUser.getFirstTeamEvent(team.id, SchedulingType.MANAGED);
return { adminUser, memberUser, managedEvent, teamMateName, teamEventTitle };
}
/** Short hand to get elements by translation key */
const getByKey = async (page: Page, key: string) => page.getByText((await localize("en"))(key));
test.describe("Managed Event Types", () => {
/** We don't use setupManagedEvent here to test the actual creation flow */
test("Can create managed event type", async ({ page, users }) => {
// Creating the owner user of the team
const adminUser = await users.create();
const adminUser = await users.create(null, {
hasTeam: true,
teammates: [{ name: "teammate-1" }],
});
// Creating the member user of the team
const memberUser = await users.create();
// First we work with owner user, logging in
await adminUser.apiLogin();
// Let's create a team
await page.goto("/settings/teams/new");
// Going to create an event type
await page.goto("/event-types");
await page.getByTestId("new-event-type").click();
await page.getByTestId("option-team-1").click();
// Expecting we can add a managed event type as team owner
await expect(page.locator('button[value="MANAGED"]')).toBeVisible();
await test.step("Managed event option exists for team admin", async () => {
// Filling team creation form wizard
await page.locator('input[name="name"]').fill(`${adminUser.username}'s Team`);
await page.click("[type=submit]");
// TODO: Figure out a way to make this more reliable
// eslint-disable-next-line playwright/no-conditional-in-test
if (IS_TEAM_BILLING_ENABLED) await fillStripeTestCheckout(page);
await page.waitForURL(/\/settings\/teams\/(\d+)\/onboard-members.*$/i);
await page.getByTestId("new-member-button").click();
await page.locator('[placeholder="email\\@example\\.com"]').fill(`${memberUser.username}@example.com`);
await page.getByTestId("invite-new-member-button").click();
// wait for the second member to be added to the pending-member-list.
await page.getByTestId("pending-member-list").locator("li:nth-child(2)").waitFor();
await page.locator("[data-testid=publish-button]").click();
await expect(page).toHaveURL(/\/settings\/teams\/(\d+)\/event-type$/i);
// create a round-robin event type and finish
await expect(page.locator('button[value="ROUND_ROBIN"]')).toBeVisible();
await page.click('button[value="ROUND_ROBIN"]');
await page.fill("[name=title]", "roundRobin");
await page.getByTestId("finish-button").click();
await page.waitForURL(/\/settings\/teams\/(\d+)\/profile$/i);
// Going to create an managed event type
await page.goto("/event-types");
await page.getByTestId("new-event-type").click();
await page.getByTestId("option-team-1").click();
// Expecting we can add a managed event type as team owner
await expect(page.locator('button[value="MANAGED"]')).toBeVisible();
// Actually creating a managed event type to test things further
await page.click('button[value="MANAGED"]');
await page.fill("[name=title]", "managed");
await page.click("[type=submit]");
// Actually creating a managed event type to test things further
await page.click('button[value="MANAGED"]');
await page.fill("[name=title]", "managed");
await page.click("[type=submit]");
await page.waitForURL("event-types/**");
expect(page.url()).toContain("?tabName=team");
});
await page.waitForURL("event-types/**");
/** From here we use setupManagedEvent to avoid repeating the previous flow */
test("Has unlocked fields for admin", async ({ page, users }) => {
const { adminUser, managedEvent } = await setupManagedEvent({ users });
await adminUser.apiLogin();
await page.goto(`/event-types/${managedEvent.id}?tabName=setup`);
await page.getByTestId("update-eventtype").waitFor();
await expect(page.locator('input[name="title"]')).toBeEditable();
await expect(page.locator('input[name="slug"]')).toBeEditable();
await expect(page.locator('input[name="length"]')).toBeEditable();
});
test("Exists for added member", async ({ page, users }) => {
const { memberUser, teamEventTitle } = await setupManagedEvent({
users,
});
await memberUser.apiLogin();
await page.goto("/event-types");
await expect(
page.getByTestId("event-types").locator("div").filter({ hasText: teamEventTitle }).nth(1)
).toBeVisible();
});
await test.step("Managed event type has unlocked fields for admin", async () => {
await page.getByTestId("vertical-tab-event_setup_tab_title").click();
await page.getByTestId("update-eventtype").waitFor();
await expect(page.locator('input[name="title"]')).toBeEditable();
await expect(page.locator('input[name="slug"]')).toBeEditable();
await expect(page.locator('input[name="length"]')).toBeEditable();
test("Can use Organizer's default app as location", async ({ page, users }) => {
const { adminUser, managedEvent } = await setupManagedEvent({ users });
await adminUser.apiLogin();
await page.goto(`/event-types/${managedEvent.id}?tabName=setup`);
await page.locator("#location-select").click();
const optionText = await getByKey(page, "organizer_default_conferencing_app");
await expect(optionText).toBeVisible();
await optionText.click();
await submitAndWaitForResponse(page);
await page.getByTestId("vertical-tab-assignment").click();
await gotoBookingPage(page);
await selectFirstAvailableTimeSlotNextMonth(page);
await bookTimeSlot(page);
await expect(page.getByTestId("success-page")).toBeVisible();
});
test("Has locked fields for added member", async ({ page, users }) => {
const { memberUser } = await setupManagedEvent({
users,
});
await memberUser.apiLogin();
const managedEvent = await memberUser.getFirstEventAsOwner();
await page.goto(`/event-types/${managedEvent.id}?tabName=setup`);
await page.waitForURL("event-types/**");
await expect(page.locator('input[name="title"]')).not.toBeEditable();
await expect(page.locator('input[name="slug"]')).not.toBeEditable();
await expect(page.locator('input[name="length"]')).not.toBeEditable();
});
test("Provides discrete field lock/unlock state for admin", async ({ page, users }) => {
const { adminUser, teamEventTitle } = await setupManagedEvent({ users });
await adminUser.apiLogin();
await page.goto("/event-types");
await page.getByTestId("event-types").locator(`a[title="${teamEventTitle}"]`).click();
await page.waitForURL("event-types/**");
// Locked by default
const titleLockIndicator = page.getByTestId("locked-indicator-title");
await expect(titleLockIndicator).toBeVisible();
await expect(titleLockIndicator.locator("[data-state='checked']")).toHaveCount(1);
// Proceed to unlock and check that it got unlocked
titleLockIndicator.click();
await expect(titleLockIndicator.locator("[data-state='checked']")).toHaveCount(0);
await expect(titleLockIndicator.locator("[data-state='unchecked']")).toHaveCount(1);
// Save changes
await page.locator('[type="submit"]').click();
await expect(titleLockIndicator.locator("[data-state='unchecked']")).toHaveCount(1);
});
test("Shows discretionally unlocked field to member", async ({ page, users }) => {
const { memberUser, teamEventTitle } = await setupManagedEvent({
users,
unlockedFields: {
title: true,
},
});
await memberUser.apiLogin();
await page.goto("/event-types");
await page.getByTestId("event-types").locator(`a[title="${teamEventTitle}"]`).click();
await page.waitForURL("event-types/**");
await expect(page.locator('input[name="title"]')).toBeEditable();
});
test("Should only update the unlocked fields modified by Admin", async ({
page: memberPage,
users,
browser,
}) => {
const { adminUser, memberUser, teamEventTitle } = await setupManagedEvent({
users,
unlockedFields: {
title: true,
},
});
await memberUser.apiLogin();
await memberPage.goto("/event-types");
await memberPage.getByTestId("event-types").locator(`a[title="${teamEventTitle}"]`).click();
await memberPage.waitForURL("event-types/**");
await expect(memberPage.locator('input[name="title"]')).toBeEditable();
await memberPage.locator('input[name="title"]').fill(`Managed Event Title`);
await submitAndWaitForResponse(memberPage);
// We edit the managed event as original owner
const adminContext = await browser.newContext();
const adminPage = await adminContext.newPage();
const adminUserSnapshot = await adminUser.self();
await apiLogin({ ...adminUserSnapshot, password: adminUserSnapshot.username }, adminPage);
await adminPage.goto("/event-types");
await adminPage.getByTestId("event-types").locator(`a[title="${teamEventTitle}"]`).click();
await adminPage.waitForURL("event-types/**");
await adminPage.locator('input[name="length"]').fill(`45`);
await submitAndWaitForResponse(adminPage);
await adminContext.close();
await memberPage.goto("/event-types");
await memberPage.getByTestId("event-types").locator('a[title="Managed Event Title"]').click();
await memberPage.waitForURL("event-types/**");
//match length
expect(await memberPage.locator("[data-testid=duration]").getAttribute("value")).toBe("45");
//ensure description didn't update
expect(await memberPage.locator(`input[name="title"]`).getAttribute("value")).toBe(`Managed Event Title`);
await memberPage.locator('input[name="title"]').fill(`managed`);
// Save changes
await submitAndWaitForResponse(memberPage);
});
const MANAGED_EVENT_TABS: { slug: string; locator: (page: Page) => Locator | Promise<Locator> }[] = [
{ slug: "setup", locator: (page) => getByKey(page, "title") },
{
slug: "team",
locator: (page) => getByKey(page, "automatically_add_all_team_members"),
},
{
slug: "availability",
locator: (page) => getByKey(page, "members_default_schedule_description"),
},
{
slug: "limits",
locator: (page) => getByKey(page, "before_event"),
},
{
slug: "advanced",
locator: (page) => getByKey(page, "event_name_in_calendar"),
},
{
slug: "apps",
locator: (page) => page.getByRole("heading", { name: "No apps installed" }),
},
{
slug: "workflows",
locator: (page) => page.getByTestId("empty-screen").getByRole("heading", { name: "Workflows" }),
},
{
slug: "ai",
locator: (page) => page.getByTestId("empty-screen").getByRole("heading", { name: "Cal.ai" }),
},
];
MANAGED_EVENT_TABS.forEach((tab) => {
test(`Can render "${tab.slug}" tab`, async ({ page, users }) => {
const { adminUser, managedEvent } = await setupManagedEvent({ users });
// First we work with owner user, logging in
await adminUser.apiLogin();
});
await test.step("Managed event type exists for added member", async () => {
// Now we need to accept the invitation as member and come back in as admin to
// assign the member in the managed event type
await memberUser.apiLogin();
await page.goto("/teams");
await page.locator('button[data-testid^="accept-invitation"]').click();
await page.getByText("Member").waitFor();
await page.goto("/auth/logout");
// Coming back as team owner to assign member user to managed event
await adminUser.apiLogin();
await page.goto("/event-types");
await page.getByTestId("event-types").locator('a[title="managed"]').click();
await page.getByTestId("vertical-tab-assignment").click();
await page.getByTestId("assignment-dropdown").click();
await page.getByTestId(`select-option-${memberUser.id}`).click();
await page.locator('[type="submit"]').click();
await page.getByTestId("toast-success").waitFor();
});
await test.step("Managed event type can use Organizer's default app as location", async () => {
await page.getByTestId("vertical-tab-event_setup_tab_title").click();
await page.locator("#location-select").click();
const optionText = (await localize("en"))("organizer_default_conferencing_app");
await page.locator(`text=${optionText}`).click();
await page.locator("[data-testid=update-eventtype]").click();
await page.getByTestId("toast-success").waitFor();
await page.waitForLoadState("networkidle");
await page.getByTestId("vertical-tab-assignment").click();
await gotoBookingPage(page);
await selectFirstAvailableTimeSlotNextMonth(page);
await bookTimeSlot(page);
await expect(page.getByTestId("success-page")).toBeVisible();
});
await test.step("Managed event type has locked fields for added member", async () => {
await adminUser.logout();
// Coming back as member user to see if there is a managed event present after assignment
await memberUser.apiLogin();
await page.goto("/event-types");
await page.getByTestId("event-types").locator('a[title="managed"]').click();
await page.waitForURL("event-types/**");
await expect(page.locator('input[name="title"]')).not.toBeEditable();
await expect(page.locator('input[name="slug"]')).not.toBeEditable();
await expect(page.locator('input[name="length"]')).not.toBeEditable();
await page.goto("/auth/logout");
});
await test.step("Managed event type provides discrete field lock/unlock state for admin", async () => {
await adminUser.apiLogin();
await page.goto("/event-types");
await page.getByTestId("event-types").locator('a[title="managed"]').click();
await page.waitForURL("event-types/**");
// Locked by default
const titleLockIndicator = page.getByTestId("locked-indicator-title");
await expect(titleLockIndicator).toBeVisible();
await expect(titleLockIndicator.locator("[data-state='checked']")).toHaveCount(1);
// Proceed to unlock and check that it got unlocked
titleLockIndicator.click();
await expect(titleLockIndicator.locator("[data-state='checked']")).toHaveCount(0);
await expect(titleLockIndicator.locator("[data-state='unchecked']")).toHaveCount(1);
// Save changes
await page.locator('[type="submit"]').click();
await page.waitForLoadState("networkidle");
await page.goto("/auth/logout");
});
await test.step("Managed event type shows discretionally unlocked field to member", async () => {
await memberUser.apiLogin();
await page.goto("/event-types");
await page.getByTestId("event-types").locator('a[title="managed"]').click();
await page.waitForURL("event-types/**");
await expect(page.locator('input[name="title"]')).toBeEditable();
await page.waitForLoadState("networkidle");
await page.goto("/auth/logout");
});
await test.step("Managed event type should only update the unlocked fields modified by Admin", async () => {
await memberUser.apiLogin();
await page.goto("/event-types");
await page.getByTestId("event-types").locator('a[title="managed"]').click();
await page.waitForURL("event-types/**");
await expect(page.locator('input[name="title"]')).toBeEditable();
await page.locator('input[name="title"]').fill(`Managed Event Title`);
// Save changes
await page.locator('[type="submit"]').click();
await page.getByTestId("toast-success").waitFor();
await page.waitForLoadState("networkidle");
await page.goto("/auth/logout");
await adminUser.apiLogin();
await page.goto("/event-types");
await page.getByTestId("event-types").locator('a[title="managed"]').click();
await page.waitForURL("event-types/**");
await page.locator('input[name="length"]').fill(`45`);
// Save changes
await page.locator('[type="submit"]').click();
await page.getByTestId("toast-success").waitFor();
await page.waitForLoadState("networkidle");
await page.goto("/auth/logout");
await memberUser.apiLogin();
await page.goto("/event-types");
await page.getByTestId("event-types").locator('a[title="Managed Event Title"]').click();
await page.waitForURL("event-types/**");
//match length
expect(await page.locator("[data-testid=duration]").getAttribute("value")).toBe("45");
//ensure description didn't update
expect(await page.locator(`input[name="title"]`).getAttribute("value")).toBe(`Managed Event Title`);
await page.locator('input[name="title"]').fill(`managed`);
// Save changes
await page.locator('[type="submit"]').click();
await page.getByTestId("toast-success").waitFor();
await page.goto(`/event-types/${managedEvent.id}?tabName=${tab.slug}`);
await expect(await tab.locator(page)).toBeVisible();
});
});
});
@@ -216,3 +251,10 @@ async function gotoBookingPage(page: Page) {
await page.goto(previewLink ?? "");
}
async function submitAndWaitForResponse(page: Page) {
const submitPromise = page.waitForResponse("/api/trpc/eventTypes/update?batch=1");
await page.locator('[type="submit"]').click();
const response = await submitPromise;
expect(response.status()).toBe(200);
}
@@ -1,43 +0,0 @@
import { test } from "../lib/fixtures";
test.beforeEach(async ({ page, users, bookingPage }) => {
const teamEventTitle = "Test Managed Event Type";
const userFixture = await users.create(
{ name: "testuser" },
{ hasTeam: true, schedulingType: "MANAGED", teamEventTitle }
);
await userFixture.apiLogin();
await page.goto("/event-types");
await bookingPage.goToEventType(teamEventTitle);
await page.getByTestId("location-select").click();
await page.locator(`text="Cal Video (Global)"`).click();
await bookingPage.goToTab("event_advanced_tab_title");
});
test.describe("Check advanced options in a managed team event type", () => {
test("Check advanced options in a managed team event type without offer seats", async ({ bookingPage }) => {
await bookingPage.checkRequiresConfirmation();
await bookingPage.checkRequiresBookerEmailVerification();
await bookingPage.checkHideNotes();
await bookingPage.checkRedirectOnBooking();
await bookingPage.checkLockTimezone();
await bookingPage.updateEventType();
await bookingPage.goToEventTypesPage();
await bookingPage.checkEventType();
});
test("Check advanced options in a managed team event type with offer seats", async ({ bookingPage }) => {
await bookingPage.checkRequiresConfirmation();
await bookingPage.checkRequiresBookerEmailVerification();
await bookingPage.checkHideNotes();
await bookingPage.checkRedirectOnBooking();
await bookingPage.toggleOfferSeats();
await bookingPage.checkLockTimezone();
await bookingPage.updateEventType();
await bookingPage.goToEventTypesPage();
await bookingPage.checkEventType();
});
});