test: Bookings: Add more automated tests for organization (#13576)
* Avoid selecting unused props * Add automated tests * Add existing user invite and booking --------- Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
This commit is contained in:
co-authored by
Joe Au-Yeung
parent
c0c40185fc
commit
1293588ac4
@@ -8,14 +8,14 @@ test.describe("Org", () => {
|
||||
const response = await page.goto("https://i.cal.com/embed");
|
||||
expect(response?.status()).toBe(200);
|
||||
await page.screenshot({ path: "screenshot.jpg" });
|
||||
await expectPageToBeServerSideRendered(page);
|
||||
await expectPageToBeRenderedWithEmbedSsr(page);
|
||||
});
|
||||
|
||||
test("Org User(Peer) Page should be embeddable", async ({ page }) => {
|
||||
const response = await page.goto("https://i.cal.com/peer/embed");
|
||||
expect(response?.status()).toBe(200);
|
||||
await expect(page.locator("text=Peer Richelsen")).toBeVisible();
|
||||
await expectPageToBeServerSideRendered(page);
|
||||
await expectPageToBeRenderedWithEmbedSsr(page);
|
||||
});
|
||||
|
||||
test("Org User Event(peer/meet) Page should be embeddable", async ({ page }) => {
|
||||
@@ -23,14 +23,14 @@ test.describe("Org", () => {
|
||||
expect(response?.status()).toBe(200);
|
||||
await expect(page.locator('[data-testid="decrementMonth"]')).toBeVisible();
|
||||
await expect(page.locator('[data-testid="incrementMonth"]')).toBeVisible();
|
||||
await expectPageToBeServerSideRendered(page);
|
||||
await expectPageToBeRenderedWithEmbedSsr(page);
|
||||
});
|
||||
|
||||
test("Org Team Profile(/sales) page should be embeddable", async ({ page }) => {
|
||||
const response = await page.goto("https://i.cal.com/sales/embed");
|
||||
expect(response?.status()).toBe(200);
|
||||
await expect(page.locator("text=Cal.com Sales")).toBeVisible();
|
||||
await expectPageToBeServerSideRendered(page);
|
||||
await expectPageToBeRenderedWithEmbedSsr(page);
|
||||
});
|
||||
|
||||
test("Org Team Event page(/sales/hippa) should be embeddable", async ({ page }) => {
|
||||
@@ -38,9 +38,10 @@ test.describe("Org", () => {
|
||||
expect(response?.status()).toBe(200);
|
||||
await expect(page.locator('[data-testid="decrementMonth"]')).toBeVisible();
|
||||
await expect(page.locator('[data-testid="incrementMonth"]')).toBeVisible();
|
||||
await expectPageToBeServerSideRendered(page);
|
||||
await expectPageToBeRenderedWithEmbedSsr(page);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Dynamic Group Booking", () => {
|
||||
test("Dynamic Group booking link should load", async ({ page }) => {
|
||||
const users = [
|
||||
@@ -63,12 +64,39 @@ test.describe("Org", () => {
|
||||
expect((await page.locator('[data-testid="event-meta"] [data-testid="avatar"]').all()).length).toBe(3);
|
||||
});
|
||||
});
|
||||
|
||||
test("Organization Homepage - Has Engineering and Marketing Teams", async ({ page }) => {
|
||||
const response = await page.goto("https://i.cal.com");
|
||||
expect(response?.status()).toBe(200);
|
||||
await expect(page.locator("text=Cal.com")).toBeVisible();
|
||||
await expect(page.locator("text=Engineering")).toBeVisible();
|
||||
await expect(page.locator("text=Marketing")).toBeVisible();
|
||||
});
|
||||
|
||||
test.describe("Browse the Engineering Team", async () => {
|
||||
test("By User Navigation", async ({ page }) => {
|
||||
await page.goto("https://i.cal.com");
|
||||
await page.click('text="Engineering"');
|
||||
await expect(page.locator("text=Cal.com Engineering")).toBeVisible();
|
||||
});
|
||||
|
||||
test("By /team/engineering", async ({ page }) => {
|
||||
await page.goto("https://i.cal.com/team/engineering");
|
||||
await expect(page.locator("text=Cal.com Engineering")).toBeVisible();
|
||||
});
|
||||
|
||||
test("By /engineering", async ({ page }) => {
|
||||
await page.goto("https://i.cal.com/engineering");
|
||||
await expect(page.locator("text=Cal.com Engineering")).toBeVisible();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// This ensures that the route is actually mapped to a page that is using withEmbedSsr
|
||||
async function expectPageToBeServerSideRendered(page: Page) {
|
||||
async function expectPageToBeRenderedWithEmbedSsr(page: Page) {
|
||||
expect(
|
||||
await page.evaluate(() => {
|
||||
//@ts-expect-error - __NEXT_DATA__ is a global variable defined by Next.js
|
||||
return window.__NEXT_DATA__.props.pageProps.isEmbed;
|
||||
})
|
||||
).toBe(true);
|
||||
|
||||
@@ -13,6 +13,7 @@ export const createEmailsFixture = () => {
|
||||
if (IS_MAILHOG_ENABLED) {
|
||||
const mailhogAPI = mailhog();
|
||||
return {
|
||||
messages: mailhogAPI.messages.bind(mailhogAPI),
|
||||
search: (query: string, kind?: string, start?: number, limit?: number) => {
|
||||
if (kind === "from" || kind === "to") {
|
||||
if (!hasUUID(query)) {
|
||||
@@ -27,6 +28,7 @@ export const createEmailsFixture = () => {
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
messages: unimplemented,
|
||||
search: unimplemented,
|
||||
deleteMessage: unimplemented,
|
||||
};
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { Page } from "@playwright/test";
|
||||
import type { Team } from "@prisma/client";
|
||||
|
||||
import { prisma } from "@calcom/prisma";
|
||||
import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
|
||||
|
||||
const getRandomSlug = () => `org-${Math.random().toString(36).substring(7)}`;
|
||||
|
||||
@@ -15,8 +16,12 @@ export const createOrgsFixture = (page: Page) => {
|
||||
slug: opts.slug || getRandomSlug(),
|
||||
requestedSlug: opts.requestedSlug,
|
||||
});
|
||||
store.orgs.push(org);
|
||||
return org;
|
||||
const orgWithMetadata = {
|
||||
...org,
|
||||
metadata: teamMetadataSchema.parse(org.metadata),
|
||||
};
|
||||
store.orgs.push(orgWithMetadata);
|
||||
return orgWithMetadata;
|
||||
},
|
||||
get: () => store.orgs,
|
||||
deleteAll: async () => {
|
||||
|
||||
@@ -196,6 +196,9 @@ export const createUsersFixture = (
|
||||
password: opts?.password ?? uname,
|
||||
};
|
||||
},
|
||||
/**
|
||||
* In case organizationId is passed, it simulates a scenario where a nonexistent user is added to an organization.
|
||||
*/
|
||||
create: async (
|
||||
opts?:
|
||||
| (CustomUserOpts & {
|
||||
@@ -726,6 +729,7 @@ const createUser = (
|
||||
throw new Error("Missing role for user in organization");
|
||||
}
|
||||
return {
|
||||
organizationId,
|
||||
profiles: {
|
||||
create: {
|
||||
uid: ProfileRepository.generateProfileUid(),
|
||||
|
||||
@@ -225,7 +225,10 @@ export async function getEmailsReceivedByUser({
|
||||
if (!emails) return null;
|
||||
const matchingEmails = await emails.search(userEmail, "to");
|
||||
if (!matchingEmails?.total) {
|
||||
console.log(`No emails received by ${userEmail}`);
|
||||
console.log(
|
||||
`No emails received by ${userEmail}. All emails sent to:`,
|
||||
(await emails.messages())?.items.map((e) => e.to)
|
||||
);
|
||||
}
|
||||
return matchingEmails;
|
||||
}
|
||||
@@ -358,6 +361,9 @@ export async function doOnOrgDomain(
|
||||
"x-cal-force-slug": orgSlug,
|
||||
});
|
||||
await callback({ page });
|
||||
await page.setExtraHTTPHeaders({
|
||||
"x-cal-force-slug": "",
|
||||
});
|
||||
}
|
||||
|
||||
// When App directory is there, this is the 404 page text. We should work on fixing the 404 page as it changed due to app directory.
|
||||
|
||||
@@ -0,0 +1,488 @@
|
||||
import type { Page } from "@playwright/test";
|
||||
import { expect } from "@playwright/test";
|
||||
|
||||
import { getOrgUsernameFromEmail } from "@calcom/features/auth/signup/utils/getOrgUsernameFromEmail";
|
||||
import { getOrgFullOrigin } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||
import { MembershipRole, SchedulingType } from "@calcom/prisma/enums";
|
||||
|
||||
import { test } from "../lib/fixtures";
|
||||
import {
|
||||
bookTimeSlot,
|
||||
doOnOrgDomain,
|
||||
NotFoundPageTextAppDir,
|
||||
selectFirstAvailableTimeSlotNextMonth,
|
||||
testName,
|
||||
} from "../lib/testUtils";
|
||||
import { expectExistingUserToBeInvitedToOrganization } from "../team/expects";
|
||||
import { acceptTeamOrOrgInvite, inviteExistingUserToOrganization } from "./lib/inviteUser";
|
||||
|
||||
test.describe("Bookings", () => {
|
||||
test.afterEach(({ orgs, users }) => {
|
||||
orgs.deleteAll();
|
||||
users.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 });
|
||||
|
||||
// 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 page.goto(`/${user.username}/${event.slug}`);
|
||||
|
||||
// Shouldn't be servable on the non-org domain
|
||||
await expect(page.locator(`text=${NotFoundPageTextAppDir}`)).toBeVisible();
|
||||
|
||||
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.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,
|
||||
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",
|
||||
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.metadata?.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 expectRedirectToOrgDomain({
|
||||
page,
|
||||
org,
|
||||
eventSlug: `/${usernameOutsideOrg}/${event.slug}`,
|
||||
expectedEventSlug: `/${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,
|
||||
}: {
|
||||
page: Page;
|
||||
team: {
|
||||
slug: string | null;
|
||||
name: string | null;
|
||||
};
|
||||
event: { slug: string; title: 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
|
||||
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.locator(`text=${NotFoundPageTextAppDir}`)).toBeVisible();
|
||||
}
|
||||
|
||||
async function expectRedirectToOrgDomain({
|
||||
page,
|
||||
org,
|
||||
eventSlug,
|
||||
expectedEventSlug,
|
||||
}: {
|
||||
page: Page;
|
||||
org: { slug: string | null };
|
||||
eventSlug: string;
|
||||
expectedEventSlug: string;
|
||||
}) {
|
||||
if (!org.slug) {
|
||||
throw new Error("Org slug is not defined");
|
||||
}
|
||||
page.goto(eventSlug).catch((e) => {
|
||||
console.log("Expected navigation error to happen");
|
||||
});
|
||||
|
||||
const orgSlug = org.slug;
|
||||
|
||||
const orgRedirectUrl = await new Promise(async (resolve) => {
|
||||
page.on("request", (request) => {
|
||||
if (request.isNavigationRequest()) {
|
||||
const requestedUrl = request.url();
|
||||
console.log("Requested navigation to", requestedUrl);
|
||||
// Resolve on redirection to org domain
|
||||
if (requestedUrl.includes(orgSlug)) {
|
||||
resolve(requestedUrl);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
expect(orgRedirectUrl).toContain(`${getOrgFullOrigin(org.slug)}${expectedEventSlug}`);
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import type { Page } from "@playwright/test";
|
||||
import type { createUsersFixture } from "playwright/fixtures/users";
|
||||
|
||||
export const inviteUserToOrganization = async ({
|
||||
page,
|
||||
organizationId,
|
||||
email,
|
||||
usersFixture,
|
||||
}: {
|
||||
page: Page;
|
||||
organizationId: number;
|
||||
email: string;
|
||||
usersFixture: ReturnType<typeof createUsersFixture>;
|
||||
}) => {
|
||||
await page.goto("/settings/organizations/members");
|
||||
await page.waitForLoadState("networkidle");
|
||||
const invitedUserEmail = usersFixture.trackEmail({
|
||||
username: email.split("@")[0],
|
||||
domain: email.split("@")[1],
|
||||
});
|
||||
await inviteAnEmail(page, invitedUserEmail);
|
||||
return { invitedUserEmail };
|
||||
};
|
||||
|
||||
export const inviteExistingUserToOrganization = async ({
|
||||
page,
|
||||
organizationId,
|
||||
user,
|
||||
usersFixture,
|
||||
}: {
|
||||
page: Page;
|
||||
organizationId: number;
|
||||
user: {
|
||||
email: string;
|
||||
};
|
||||
usersFixture: ReturnType<typeof createUsersFixture>;
|
||||
}) => {
|
||||
await page.goto("/settings/organizations/members");
|
||||
await page.waitForLoadState("networkidle");
|
||||
|
||||
await inviteAnEmail(page, user.email);
|
||||
await page.waitForSelector('[data-testid="toast-success"]');
|
||||
return { invitedUserEmail: user.email };
|
||||
};
|
||||
|
||||
export async function acceptTeamOrOrgInvite(page: Page) {
|
||||
await page.goto("/settings/teams");
|
||||
await page.click('[data-testid^="accept-invitation"]');
|
||||
await page.waitForLoadState("networkidle");
|
||||
}
|
||||
|
||||
async function inviteAnEmail(page: Page, invitedUserEmail: string) {
|
||||
await page.locator('button:text("Add")').click();
|
||||
await page.locator('input[name="inviteUser"]').fill(invitedUserEmail);
|
||||
await page.locator('button:text("Send invite")').click();
|
||||
await page.waitForLoadState("networkidle");
|
||||
}
|
||||
@@ -377,7 +377,7 @@ async function signupFromInviteLink({
|
||||
return { email };
|
||||
}
|
||||
|
||||
async function signupFromEmailInviteLink({
|
||||
export async function signupFromEmailInviteLink({
|
||||
browser,
|
||||
inviteLink,
|
||||
expectedUsername,
|
||||
@@ -385,8 +385,8 @@ async function signupFromEmailInviteLink({
|
||||
}: {
|
||||
browser: Browser;
|
||||
inviteLink: string;
|
||||
expectedUsername: string;
|
||||
expectedEmail: string;
|
||||
expectedUsername?: string;
|
||||
expectedEmail?: string;
|
||||
}) {
|
||||
// Follow invite link in new window
|
||||
const context = await browser.newContext();
|
||||
@@ -396,10 +396,14 @@ async function signupFromEmailInviteLink({
|
||||
await signupPage.locator(`[data-testid="signup-usernamefield"]`).waitFor({ state: "visible" });
|
||||
await expect(signupPage.locator(`[data-testid="signup-usernamefield"]`)).toBeDisabled();
|
||||
// await for value. initial value is ""
|
||||
await expect(signupPage.locator(`[data-testid="signup-usernamefield"]`)).toHaveValue(expectedUsername);
|
||||
if (expectedUsername) {
|
||||
await expect(signupPage.locator(`[data-testid="signup-usernamefield"]`)).toHaveValue(expectedUsername);
|
||||
}
|
||||
|
||||
await expect(signupPage.locator(`[data-testid="signup-emailfield"]`)).toBeDisabled();
|
||||
await expect(signupPage.locator(`[data-testid="signup-emailfield"]`)).toHaveValue(expectedEmail);
|
||||
if (expectedEmail) {
|
||||
await expect(signupPage.locator(`[data-testid="signup-emailfield"]`)).toHaveValue(expectedEmail);
|
||||
}
|
||||
|
||||
await signupPage.waitForLoadState("networkidle");
|
||||
// Check required fields
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
import { expect } from "@playwright/test";
|
||||
|
||||
import { IS_TEAM_BILLING_ENABLED } from "@calcom/lib/constants";
|
||||
import { prisma } from "@calcom/prisma";
|
||||
import { MembershipRole } from "@calcom/prisma/enums";
|
||||
|
||||
import { test } from "../lib/fixtures";
|
||||
import { fillStripeTestCheckout } from "../lib/testUtils";
|
||||
|
||||
test.describe("Teams", () => {
|
||||
test.afterEach(({ orgs, users }) => {
|
||||
orgs.deleteAll();
|
||||
users.deleteAll();
|
||||
});
|
||||
|
||||
test("Can create teams via Wizard", async ({ page, users, orgs }) => {
|
||||
const org = await orgs.create({
|
||||
name: "TestOrg",
|
||||
});
|
||||
const user = await users.create({
|
||||
organizationId: org.id,
|
||||
roleInOrganization: MembershipRole.ADMIN,
|
||||
});
|
||||
const inviteeEmail = `${user.username}+invitee@example.com`;
|
||||
await user.apiLogin();
|
||||
await page.goto("/teams");
|
||||
|
||||
await test.step("Can create team", async () => {
|
||||
// Click text=Create Team
|
||||
await page.locator("text=Create a new Team").click();
|
||||
await page.waitForURL((url) => url.pathname === "/settings/teams/new");
|
||||
// Fill input[name="name"]
|
||||
await page.locator('input[name="name"]').fill(`${user.username}'s Team`);
|
||||
// Click text=Continue
|
||||
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 expect(page).toHaveURL(/\/settings\/teams\/(\d+)\/onboard-members.*$/i);
|
||||
await page.waitForSelector('[data-testid="pending-member-list"]');
|
||||
expect(await page.getByTestId("pending-member-item").count()).toBe(1);
|
||||
});
|
||||
|
||||
await test.step("Can add members", async () => {
|
||||
await page.getByTestId("new-member-button").click();
|
||||
await page.locator('[placeholder="email\\@example\\.com"]').fill(inviteeEmail);
|
||||
await page.getByTestId("invite-new-member-button").click();
|
||||
await expect(page.locator(`li:has-text("${inviteeEmail}")`)).toBeVisible();
|
||||
|
||||
// locator.count() does not await for the expected number of elements
|
||||
// https://github.com/microsoft/playwright/issues/14278
|
||||
// using toHaveCount() is more reliable
|
||||
await expect(page.getByTestId("pending-member-item")).toHaveCount(2);
|
||||
});
|
||||
|
||||
await test.step("Can remove members", async () => {
|
||||
await expect(page.getByTestId("pending-member-item")).toHaveCount(2);
|
||||
const lastRemoveMemberButton = page.getByTestId("remove-member-button").last();
|
||||
await lastRemoveMemberButton.click();
|
||||
await page.waitForLoadState("networkidle");
|
||||
await expect(page.getByTestId("pending-member-item")).toHaveCount(1);
|
||||
|
||||
// Cleanup here since this user is created without our fixtures.
|
||||
await prisma.user.delete({ where: { email: inviteeEmail } });
|
||||
});
|
||||
|
||||
await test.step("Can finish team creation", async () => {
|
||||
await page.getByTestId("publish-button").click();
|
||||
await expect(page).toHaveURL(/\/settings\/teams\/(\d+)\/profile$/i);
|
||||
});
|
||||
|
||||
await test.step("Can disband team", async () => {
|
||||
await page.waitForURL(/\/settings\/teams\/(\d+)\/profile$/i);
|
||||
await page.getByTestId("disband-team-button").click();
|
||||
await page.getByTestId("dialog-confirmation").click();
|
||||
await page.waitForURL("/teams");
|
||||
expect(await page.locator(`text=${user.username}'s Team`).count()).toEqual(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -10,7 +10,7 @@ export async function expectInvitationEmailToBeReceived(
|
||||
page: Page,
|
||||
emails: ReturnType<typeof createEmailsFixture>,
|
||||
userEmail: string,
|
||||
subject: string,
|
||||
subject?: string | null,
|
||||
returnLink?: string
|
||||
) {
|
||||
if (!emails) return null;
|
||||
@@ -21,10 +21,20 @@ export async function expectInvitationEmailToBeReceived(
|
||||
expect(receivedEmails?.total).toBe(1);
|
||||
|
||||
const [firstReceivedEmail] = (receivedEmails as Messages).items;
|
||||
|
||||
expect(firstReceivedEmail.subject).toBe(subject);
|
||||
if (subject) {
|
||||
expect(firstReceivedEmail.subject).toBe(subject);
|
||||
}
|
||||
if (!returnLink) return;
|
||||
const dom = new JSDOM(firstReceivedEmail.html);
|
||||
const anchor = dom.window.document.querySelector(`a[href*="${returnLink}"]`);
|
||||
return anchor?.getAttribute("href");
|
||||
}
|
||||
|
||||
export async function expectExistingUserToBeInvitedToOrganization(
|
||||
page: Page,
|
||||
emails: ReturnType<typeof createEmailsFixture>,
|
||||
userEmail: string,
|
||||
subject?: string | null
|
||||
) {
|
||||
return expectInvitationEmailToBeReceived(page, emails, userEmail, subject, "settings/team");
|
||||
}
|
||||
|
||||
@@ -2,15 +2,13 @@ import { expect } from "@playwright/test";
|
||||
|
||||
import { IS_TEAM_BILLING_ENABLED } from "@calcom/lib/constants";
|
||||
import { prisma } from "@calcom/prisma";
|
||||
import { MembershipRole, SchedulingType } from "@calcom/prisma/enums";
|
||||
import { SchedulingType } from "@calcom/prisma/enums";
|
||||
|
||||
import { test } from "./lib/fixtures";
|
||||
import { testBothFutureAndLegacyRoutes } from "./lib/future-legacy-routes";
|
||||
import {
|
||||
bookTimeSlot,
|
||||
doOnOrgDomain,
|
||||
fillStripeTestCheckout,
|
||||
NotFoundPageTextAppDir,
|
||||
selectFirstAvailableTimeSlotNextMonth,
|
||||
testName,
|
||||
todo,
|
||||
@@ -282,210 +280,3 @@ testBothFutureAndLegacyRoutes.describe("Teams - NonOrg", (routeVariant) => {
|
||||
todo("Reschedule a Collective EventType booking");
|
||||
todo("Reschedule a Round Robin EventType booking");
|
||||
});
|
||||
|
||||
test.describe("Teams - Org", () => {
|
||||
test.afterEach(({ orgs, users }) => {
|
||||
orgs.deleteAll();
|
||||
users.deleteAll();
|
||||
});
|
||||
|
||||
test("Can create teams via Wizard", async ({ page, users, orgs }) => {
|
||||
const org = await orgs.create({
|
||||
name: "TestOrg",
|
||||
});
|
||||
const user = await users.create({
|
||||
organizationId: org.id,
|
||||
roleInOrganization: MembershipRole.ADMIN,
|
||||
});
|
||||
const inviteeEmail = `${user.username}+invitee@example.com`;
|
||||
await user.apiLogin();
|
||||
await page.goto("/teams");
|
||||
|
||||
await test.step("Can create team", async () => {
|
||||
// Click text=Create Team
|
||||
await page.locator("text=Create a new Team").click();
|
||||
await page.waitForURL((url) => url.pathname === "/settings/teams/new");
|
||||
// Fill input[name="name"]
|
||||
await page.locator('input[name="name"]').fill(`${user.username}'s Team`);
|
||||
// Click text=Continue
|
||||
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 expect(page).toHaveURL(/\/settings\/teams\/(\d+)\/onboard-members.*$/i);
|
||||
await page.waitForSelector('[data-testid="pending-member-list"]');
|
||||
expect(await page.getByTestId("pending-member-item").count()).toBe(1);
|
||||
});
|
||||
|
||||
await test.step("Can add members", async () => {
|
||||
await page.getByTestId("new-member-button").click();
|
||||
await page.locator('[placeholder="email\\@example\\.com"]').fill(inviteeEmail);
|
||||
await page.getByTestId("invite-new-member-button").click();
|
||||
await expect(page.locator(`li:has-text("${inviteeEmail}")`)).toBeVisible();
|
||||
|
||||
// locator.count() does not await for the expected number of elements
|
||||
// https://github.com/microsoft/playwright/issues/14278
|
||||
// using toHaveCount() is more reliable
|
||||
await expect(page.getByTestId("pending-member-item")).toHaveCount(2);
|
||||
});
|
||||
|
||||
await test.step("Can remove members", async () => {
|
||||
await expect(page.getByTestId("pending-member-item")).toHaveCount(2);
|
||||
const lastRemoveMemberButton = page.getByTestId("remove-member-button").last();
|
||||
await lastRemoveMemberButton.click();
|
||||
await page.waitForLoadState("networkidle");
|
||||
await expect(page.getByTestId("pending-member-item")).toHaveCount(1);
|
||||
|
||||
// Cleanup here since this user is created without our fixtures.
|
||||
await prisma.user.delete({ where: { email: inviteeEmail } });
|
||||
});
|
||||
|
||||
await test.step("Can finish team creation", async () => {
|
||||
await page.getByTestId("publish-button").click();
|
||||
await expect(page).toHaveURL(/\/settings\/teams\/(\d+)\/profile$/i);
|
||||
});
|
||||
|
||||
await test.step("Can disband team", async () => {
|
||||
await page.waitForURL(/\/settings\/teams\/(\d+)\/profile$/i);
|
||||
await page.getByTestId("disband-team-button").click();
|
||||
await page.getByTestId("dialog-confirmation").click();
|
||||
await page.waitForURL("/teams");
|
||||
expect(await page.locator(`text=${user.username}'s Team`).count()).toEqual(0);
|
||||
});
|
||||
});
|
||||
|
||||
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 { title: teamEventTitle, slug: teamEventSlug } = await owner.getFirstTeamEvent(team.id);
|
||||
|
||||
await page.goto(`/team/${team.slug}/${teamEventSlug}`);
|
||||
|
||||
await expect(page.locator(`text=${NotFoundPageTextAppDir}`)).toBeVisible();
|
||||
await doOnOrgDomain(
|
||||
{
|
||||
orgSlug: org.slug,
|
||||
page,
|
||||
},
|
||||
async () => {
|
||||
await page.goto(`/team/${team.slug}/${teamEventSlug}`);
|
||||
await selectFirstAvailableTimeSlotNextMonth(page);
|
||||
await bookTimeSlot(page);
|
||||
await expect(page.getByTestId("success-page")).toBeVisible();
|
||||
|
||||
// The title of the booking
|
||||
const BookingTitle = `${teamEventTitle} 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);
|
||||
|
||||
// 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 }) => {
|
||||
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" },
|
||||
{
|
||||
hasTeam: true,
|
||||
teammates: teamMatesObj,
|
||||
schedulingType: SchedulingType.ROUND_ROBIN,
|
||||
}
|
||||
);
|
||||
|
||||
const { team } = await owner.getFirstTeamMembership();
|
||||
const { title: teamEventTitle, slug: teamEventSlug } = await owner.getFirstTeamEvent(team.id);
|
||||
|
||||
await page.goto(`/team/${team.slug}/${teamEventSlug}`);
|
||||
await selectFirstAvailableTimeSlotNextMonth(page);
|
||||
await bookTimeSlot(page);
|
||||
await expect(page.locator("[data-testid=success-page]")).toBeVisible();
|
||||
|
||||
// The person who booked the meeting should be in the attendee list
|
||||
await expect(page.getByTestId(`attendee-name-${testName}`)).toHaveText(testName);
|
||||
|
||||
// The title of the booking
|
||||
const BookingTitle = `${teamEventTitle} between ${team.name} and ${testName}`;
|
||||
await expect(page.getByTestId("booking-title")).toHaveText(BookingTitle);
|
||||
|
||||
// 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();
|
||||
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]");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -123,7 +123,6 @@ export const getPublicEvent = async (
|
||||
usernameList,
|
||||
orgSlug: org,
|
||||
});
|
||||
console.log("getPublicEvent - dynamic", usersInOrgContext);
|
||||
const users = usersInOrgContext;
|
||||
|
||||
const defaultEvent = getDefaultEvent(eventSlug);
|
||||
|
||||
Reference in New Issue
Block a user