2
0
Files
cal/calcom/apps/web/playwright/organization/booking.e2e.ts
2024-08-09 00:39:27 +02:00

529 lines
17 KiB
TypeScript

import type { Page } from "@playwright/test";
import { expect } from "@playwright/test";
import { JSDOM } from "jsdom";
import { uuid } from "short-uuid";
import { getOrgUsernameFromEmail } from "@calcom/features/auth/signup/utils/getOrgUsernameFromEmail";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { MembershipRole, SchedulingType } from "@calcom/prisma/enums";
import { test } from "../lib/fixtures";
import {
bookTimeSlot,
doOnOrgDomain,
selectFirstAvailableTimeSlotNextMonth,
testName,
} from "../lib/testUtils";
import { expectExistingUserToBeInvitedToOrganization } from "../team/expects";
import { gotoPathAndExpectRedirectToOrgDomain } from "./lib/gotoPathAndExpectRedirectToOrgDomain";
import { acceptTeamOrOrgInvite, inviteExistingUserToOrganization } from "./lib/inviteUser";
function getOrgOrigin(orgSlug: string | null) {
if (!orgSlug) {
throw new Error("orgSlug is required");
}
let orgOrigin = WEBAPP_URL.replace("://app", `://${orgSlug}`);
orgOrigin = orgOrigin.includes(orgSlug) ? orgOrigin : WEBAPP_URL.replace("://", `://${orgSlug}.`);
return orgOrigin;
}
test.describe("Bookings", () => {
test.afterEach(async ({ orgs, users, page }) => {
await users.deleteAll();
await orgs.deleteAll();
});
test.describe("Team Event", () => {
test("Can create a booking for Collective EventType", async ({ page, users, orgs }) => {
const org = await orgs.create({
name: "TestOrg",
});
const teamMatesObj = [
{ name: "teammate-1" },
{ name: "teammate-2" },
{ name: "teammate-3" },
{ name: "teammate-4" },
];
const owner = await users.create(
{
username: "pro-user",
name: "pro-user",
organizationId: org.id,
roleInOrganization: MembershipRole.MEMBER,
},
{
hasTeam: true,
teammates: teamMatesObj,
schedulingType: SchedulingType.COLLECTIVE,
}
);
const { team } = await owner.getFirstTeamMembership();
const teamEvent = await owner.getFirstTeamEvent(team.id);
await expectPageToBeNotFound({ page, url: `/team/${team.slug}/${teamEvent.slug}` });
await doOnOrgDomain(
{
orgSlug: org.slug,
page,
},
async () => {
await bookTeamEvent({ page, team, event: teamEvent });
// All the teammates should be in the booking
for (const teammate of teamMatesObj.concat([{ name: owner.name || "" }])) {
await expect(page.getByText(teammate.name, { exact: true })).toBeVisible();
}
}
);
// TODO: Assert whether the user received an email
});
test("Can create a booking for Round Robin EventType", async ({ page, users, orgs }) => {
const org = await orgs.create({
name: "TestOrg",
});
const teamMatesObj = [
{ name: "teammate-1" },
{ name: "teammate-2" },
{ name: "teammate-3" },
{ name: "teammate-4" },
];
const owner = await users.create(
{
username: "pro-user",
name: "pro-user",
organizationId: org.id,
roleInOrganization: MembershipRole.MEMBER,
},
{
hasTeam: true,
teammates: teamMatesObj,
schedulingType: SchedulingType.ROUND_ROBIN,
}
);
const { team } = await owner.getFirstTeamMembership();
const teamEvent = await owner.getFirstTeamEvent(team.id);
await expectPageToBeNotFound({ page, url: `/team/${team.slug}/${teamEvent.slug}` });
await doOnOrgDomain(
{
orgSlug: org.slug,
page,
},
async () => {
await bookTeamEvent({ page, team, event: teamEvent, teamMatesObj });
// Since all the users have the same leastRecentlyBooked value
// Anyone of the teammates could be the Host of the booking.
const chosenUser = await page.getByTestId("booking-host-name").textContent();
expect(chosenUser).not.toBeNull();
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
expect(teamMatesObj.concat([{ name: owner.name! }]).some(({ name }) => name === chosenUser)).toBe(
true
);
}
);
// TODO: Assert whether the user received an email
});
test("Can access booking page with event slug and team page in lowercase/uppercase/mixedcase", async ({
page,
orgs,
users,
}) => {
const org = await orgs.create({
name: "TestOrg",
});
const teamMatesObj = [
{ name: "teammate-1" },
{ name: "teammate-2" },
{ name: "teammate-3" },
{ name: "teammate-4" },
];
const owner = await users.create(
{
username: "pro-user",
name: "pro-user",
organizationId: org.id,
roleInOrganization: MembershipRole.MEMBER,
},
{
hasTeam: true,
teammates: teamMatesObj,
schedulingType: SchedulingType.COLLECTIVE,
}
);
const { team } = await owner.getFirstTeamMembership();
const { slug: teamEventSlug } = await owner.getFirstTeamEvent(team.id);
const teamSlugUpperCase = team.slug?.toUpperCase();
const teamEventSlugUpperCase = teamEventSlug.toUpperCase();
// This is the most closest to the actual user flow as org1.cal.com maps to /org/orgSlug
await page.goto(`/org/${org.slug}/${teamSlugUpperCase}/${teamEventSlugUpperCase}`);
await page.waitForSelector("[data-testid=day]");
});
});
test.describe("User Event", () => {
test("Can create a booking", async ({ page, users, orgs }) => {
const org = await orgs.create({
name: "TestOrg",
});
const user = await users.create({
username: "pro-user",
name: "pro-user",
organizationId: org.id,
roleInOrganization: MembershipRole.MEMBER,
});
const event = await user.getFirstEventAsOwner();
await expectPageToBeNotFound({ page, url: `/${user.username}/${event.slug}` });
await doOnOrgDomain(
{
orgSlug: org.slug,
page,
},
async () => {
await bookUserEvent({ page, user, event });
}
);
});
test.describe("User Event with same slug as another user's", () => {
test("booking is created for first user when first user is booked", async ({ page, users, orgs }) => {
const org = await orgs.create({
name: "TestOrg",
});
const user1 = await users.create({
username: "user1",
name: "User 1",
organizationId: org.id,
roleInOrganization: MembershipRole.MEMBER,
});
const user2 = await users.create({
username: "user2",
name: "User2",
organizationId: org.id,
roleInOrganization: MembershipRole.MEMBER,
});
const user1Event = await user1.getFirstEventAsOwner();
await doOnOrgDomain(
{
orgSlug: org.slug,
page,
},
async () => {
await bookUserEvent({ page, user: user1, event: user1Event });
}
);
});
test("booking is created for second user when second user is booked", async ({ page, users, orgs }) => {
const org = await orgs.create({
name: "TestOrg",
});
const user1 = await users.create({
username: "user1",
name: "User 1",
organizationId: org.id,
roleInOrganization: MembershipRole.MEMBER,
});
const user2 = await users.create({
username: "user2",
name: "User2",
organizationId: org.id,
roleInOrganization: MembershipRole.MEMBER,
});
const user2Event = await user2.getFirstEventAsOwner();
await doOnOrgDomain(
{
orgSlug: org.slug,
page,
},
async () => {
await bookUserEvent({ page, user: user2, event: user2Event });
}
);
});
});
test("check SSR and OG ", async ({ page, users, orgs }) => {
const name = "Test User";
const org = await orgs.create({
name: "TestOrg",
});
const user = await users.create({
name,
organizationId: org.id,
roleInOrganization: MembershipRole.MEMBER,
});
const firstEventType = await user.getFirstEventAsOwner();
const calLink = `/${user.username}/${firstEventType.slug}`;
await doOnOrgDomain(
{
orgSlug: org.slug,
page,
},
async () => {
const [response] = await Promise.all([
// This promise resolves to the main resource response
page.waitForResponse(
(response) => response.url().includes(`${calLink}`) && response.status() === 200
),
// Trigger the page navigation
page.goto(`${calLink}`),
]);
const ssrResponse = await response.text();
const document = new JSDOM(ssrResponse).window.document;
const orgOrigin = getOrgOrigin(org.slug);
const titleText = document.querySelector("title")?.textContent;
const ogImage = document.querySelector('meta[property="og:image"]')?.getAttribute("content");
const ogUrl = document.querySelector('meta[property="og:url"]')?.getAttribute("content");
const canonicalLink = document.querySelector('link[rel="canonical"]')?.getAttribute("href");
expect(titleText).toContain(name);
expect(ogUrl).toEqual(`${orgOrigin}${calLink}`);
expect(canonicalLink).toEqual(`${orgOrigin}${calLink}`);
// Verify that there is correct URL that would generate the awesome OG image
expect(ogImage).toContain(
"/_next/image?w=1200&q=100&url=%2Fapi%2Fsocial%2Fog%2Fimage%3Ftype%3Dmeeting%26title%3D"
);
// Verify Organizer Name in the URL
expect(ogImage).toContain("meetingProfileName%3DTest%2520User%26");
}
);
});
});
test.describe("Scenario with same username in and outside organization", () => {
test("Can create a booking for user with same username in and outside organization", async ({
page,
users,
orgs,
}) => {
const org = await orgs.create({
name: "TestOrg",
});
const username = "john";
const userInsideOrganization = await users.create({
username,
useExactUsername: true,
email: `john-inside-${uuid()}@example.com`,
name: "John Inside Organization",
organizationId: org.id,
roleInOrganization: MembershipRole.MEMBER,
eventTypes: [
{
title: "John Inside Org's Meeting",
slug: "john-inside-org-meeting",
length: 15,
},
],
});
const userOutsideOrganization = await users.create({
username,
name: "John Outside Organization",
email: `john-outside-${uuid()}@example.com`,
useExactUsername: true,
eventTypes: [
{
title: "John Outside Org's Meeting",
slug: "john-outside-org-meeting",
length: 15,
},
],
});
const eventForUserInsideOrganization = await userInsideOrganization.getFirstEventAsOwner();
const eventForUserOutsideOrganization = await userOutsideOrganization.getFirstEventAsOwner();
// John Inside Org's meeting can't be accessed on userOutsideOrganization's namespace
await expectPageToBeNotFound({
page,
url: `/${userOutsideOrganization.username}/john-inside-org-meeting`,
});
await bookUserEvent({ page, user: userOutsideOrganization, event: eventForUserOutsideOrganization });
await doOnOrgDomain(
{
orgSlug: org.slug,
page,
},
async () => {
// John Outside Org's meeting can't be accessed on userInsideOrganization's namespaces
await expectPageToBeNotFound({
page,
url: `/${userInsideOrganization.username}/john-outside-org-meeting`,
});
await bookUserEvent({ page, user: userInsideOrganization, event: eventForUserInsideOrganization });
}
);
});
});
test.describe("Inviting an existing user and then", () => {
test("create a booking on new link", async ({ page, browser, users, orgs, emails }) => {
const org = await orgs.create({
name: "TestOrg",
});
const owner = await users.create({
username: "owner",
name: "owner",
organizationId: org.id,
roleInOrganization: MembershipRole.OWNER,
});
const userOutsideOrganization = await users.create({
username: "john",
name: "John Outside Organization",
});
await owner.apiLogin();
const { invitedUserEmail } = await inviteExistingUserToOrganization({
page,
organizationId: org.id,
user: userOutsideOrganization,
usersFixture: users,
});
const inviteLink = await expectExistingUserToBeInvitedToOrganization(page, emails, invitedUserEmail);
if (!inviteLink) {
throw new Error("Invite link not found");
}
const usernameInOrg = getOrgUsernameFromEmail(
invitedUserEmail,
org.organizationSettings?.orgAutoAcceptEmail ?? null
);
const usernameOutsideOrg = userOutsideOrganization.username;
// Before invite is accepted the booking page isn't available
await expectPageToBeNotFound({ page, url: `/${usernameInOrg}` });
await userOutsideOrganization.apiLogin();
await acceptTeamOrOrgInvite(page);
await test.step("Book through new link", async () => {
await doOnOrgDomain(
{
orgSlug: org.slug,
page,
},
async () => {
await bookUserEvent({
page,
user: {
username: usernameInOrg,
name: userOutsideOrganization.name,
},
event: await userOutsideOrganization.getFirstEventAsOwner(),
});
}
);
});
await test.step("Booking through old link redirects to new link on org domain", async () => {
const event = await userOutsideOrganization.getFirstEventAsOwner();
await gotoPathAndExpectRedirectToOrgDomain({
page,
org,
path: `/${usernameOutsideOrg}/${event.slug}`,
expectedPath: `/${usernameInOrg}/${event.slug}`,
});
// As the redirection correctly happens, the booking would work too which we have verified in previous step. But we can't test that with org domain as that domain doesn't exist.
});
});
});
});
async function bookUserEvent({
page,
user,
event,
}: {
page: Page;
user: {
username: string | null;
name: string | null;
};
event: { slug: string; title: string };
}) {
await page.goto(`/${user.username}/${event.slug}`);
await selectFirstAvailableTimeSlotNextMonth(page);
await bookTimeSlot(page);
await expect(page.getByTestId("success-page")).toBeVisible();
// The title of the booking
const BookingTitle = `${event.title} between ${user.name} and ${testName}`;
await expect(page.getByTestId("booking-title")).toHaveText(BookingTitle);
// The booker should be in the attendee list
await expect(page.getByTestId(`attendee-name-${testName}`)).toHaveText(testName);
}
async function bookTeamEvent({
page,
team,
event,
teamMatesObj,
}: {
page: Page;
team: {
slug: string | null;
name: string | null;
};
event: { slug: string; title: string; schedulingType: SchedulingType | null };
teamMatesObj?: { name: string }[];
}) {
// Note that even though the default way to access a team booking in an organization is to not use /team in the URL, but it isn't testable with playwright as the rewrite is taken care of by Next.js config which can't handle on the fly org slug's handling
// So, we are using /team in the URL to access the team booking
// There are separate tests to verify that the next.config.js rewrites are working
// Also there are additional checkly tests that verify absolute e2e flow. They are in __checks__/organization.spec.ts
await page.goto(`/team/${team.slug}/${event.slug}`);
await selectFirstAvailableTimeSlotNextMonth(page);
await bookTimeSlot(page);
await expect(page.getByTestId("success-page")).toBeVisible();
// The title of the booking
if (event.schedulingType === SchedulingType.ROUND_ROBIN) {
const bookingTitle = await page.getByTestId("booking-title").textContent();
expect(
teamMatesObj?.some((teamMate) => {
const BookingTitle = `${event.title} between ${teamMate.name} and ${testName}`;
return BookingTitle === bookingTitle;
})
).toBe(true);
} else {
const BookingTitle = `${event.title} between ${team.name} and ${testName}`;
await expect(page.getByTestId("booking-title")).toHaveText(BookingTitle);
}
// The booker should be in the attendee list
await expect(page.getByTestId(`attendee-name-${testName}`)).toHaveText(testName);
}
async function expectPageToBeNotFound({ page, url }: { page: Page; url: string }) {
await page.goto(`${url}`);
await expect(page.getByTestId(`404-page`)).toBeVisible();
}